From f3d807d991ed48789aaec5c859413670ebf6ff79 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Thu, 11 Jun 2026 22:04:53 -0700 Subject: [PATCH] feat(diagnose): conversational chat mode + NL source discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New ChatDiagnose.vue: multi-turn chat UI in the Diagnose tab - Textarea input (auto-grows) for long free-form problem descriptions - Source suggestion pre-flight: debounced POST /api/sources/suggest identifies relevant log sources from the query text and shows them as interactive chips (deselect to exclude before searching) - Conversation history preserved across turns with LLM reasoning, collapsible log entries, and "Save as incident" per turn - Reuses existing /api/diagnose/stream — no new pipeline - DiagnoseView.vue: Chat is now default tab; viewport-height layout - POST /api/sources/suggest: token-overlap source ranking, no LLM - Fix: add missing 'import re' causing 500 on suggest route --- app/rest.py | 54 ++++ web/src/components/ChatDiagnose.vue | 370 ++++++++++++++++++++++++++++ web/src/views/DiagnoseView.vue | 60 +++-- 3 files changed, 465 insertions(+), 19 deletions(-) create mode 100644 web/src/components/ChatDiagnose.vue diff --git a/app/rest.py b/app/rest.py index a62070f..246b5cc 100644 --- a/app/rest.py +++ b/app/rest.py @@ -12,6 +12,7 @@ import hmac import json import logging import os +import re import time # Offline mode: must be set before any HuggingFace library is imported. @@ -277,6 +278,10 @@ class DiagnoseRequest(BaseModel): source: str | None = None +class SourceSuggestRequest(BaseModel): + query: str + + class SeverityOverride(BaseModel): name: str pattern: str @@ -523,6 +528,55 @@ async def diagnose_post_stream(body: DiagnoseRequest) -> StreamingResponse: ) +_SUGGEST_STOPWORDS = frozenset({ + "the", "and", "that", "this", "with", "have", "from", "they", + "been", "their", "what", "when", "there", "some", "would", "make", + "like", "into", "time", "look", "just", "know", "take", "year", + "your", "good", "some", "could", "them", "then", "very", "also", + "back", "after", "work", "need", "even", "much", "most", "tell", + "does", "more", "once", "help", "seem", "here", "about", "issue", + "thing", "logs", "error", "again", "still", "these", "those", + "getting", "having", "trying", "going", "where", "which", "cant", + "now", "set", "kind", "weird", "stable", "huge", "real", "nice", +}) + + +@router.post("/api/sources/suggest") +def suggest_sources(body: SourceSuggestRequest) -> dict: + """Return source IDs ranked by relevance to a natural-language problem description.""" + all_sources = _list_sources(DB_PATH) + query_tokens = { + t.lower() + for t in re.findall(r"[a-zA-Z]+", body.query) + if len(t) > 2 and t.lower() not in _SUGGEST_STOPWORDS + } + + suggestions = [] + for src in all_sources: + src_id: str = src["source_id"] + # Tokenise source ID: split on colon, dash, underscore, digits + parts = { + p.lower() + for seg in re.split(r"[:\-_\d]+", src_id) + for p in [seg.strip()] + if len(p) > 2 + } + matched = query_tokens & parts + if matched: + score = round(len(matched) / max(len(parts), 1), 3) + suggestions.append({ + "source_id": src_id, + "score": score, + "matched_tokens": sorted(matched), + }) + + suggestions.sort(key=lambda x: x["score"], reverse=True) + return { + "suggested": suggestions, + "all_source_ids": [s["source_id"] for s in all_sources], + } + + @router.get("/api/settings") def get_settings() -> dict: return _load_prefs() diff --git a/web/src/components/ChatDiagnose.vue b/web/src/components/ChatDiagnose.vue new file mode 100644 index 0000000..eb87110 --- /dev/null +++ b/web/src/components/ChatDiagnose.vue @@ -0,0 +1,370 @@ +