From 987e297c07c40698e14c3dad8805f91e382ddd6a Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 18:44:26 +0100 Subject: [PATCH 1/9] Erste Version Wp10 --- app/frontend/ui.py | 239 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 10 +- 2 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 app/frontend/ui.py diff --git a/app/frontend/ui.py b/app/frontend/ui.py new file mode 100644 index 0000000..84043a5 --- /dev/null +++ b/app/frontend/ui.py @@ -0,0 +1,239 @@ +import streamlit as st +import requests +import uuid +import os +import json +from datetime import datetime + +# --- CONFIGURATION --- +# Default configuration taken from environment or fallback to localhost +API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002") +CHAT_ENDPOINT = f"{API_BASE_URL}/chat" +FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback" + +# --- PAGE SETUP --- +st.set_page_config( + page_title="mindnet v2.3.1", + page_icon="🧠", + layout="centered" +) + +# Custom CSS for cleaner look +st.markdown(""" + +""", unsafe_allow_html=True) + +# --- SESSION STATE INITIALIZATION --- +if "messages" not in st.session_state: + st.session_state.messages = [] +if "user_id" not in st.session_state: + st.session_state.user_id = str(uuid.uuid4()) + +# --- API CLIENT FUNCTIONS --- + +def send_chat_message(message: str, top_k: int, explain: bool): + """Sends the user message to the FastAPI backend.""" + payload = { + "message": message, + "top_k": top_k, + "explain": explain + } + try: + response = requests.post(CHAT_ENDPOINT, json=payload, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.ConnectionError: + return {"error": "Backend nicht erreichbar. LĂ€uft der Server auf Port 8002?"} + except Exception as e: + return {"error": str(e)} + +def send_feedback(query_id: str, score: int): + """Sends feedback to the backend.""" + # Note: We rate the overall answer. API expects node_id. + # We use 'generated_answer' as a convention for the full response. + payload = { + "query_id": query_id, + "node_id": "generated_answer", + "score": score, + "comment": "User feedback via Streamlit UI" + } + try: + requests.post(FEEDBACK_ENDPOINT, json=payload, timeout=5) + return True + except: + return False + +# --- UI COMPONENTS --- + +def render_sidebar(): + with st.sidebar: + st.header("⚙ Konfiguration") + st.markdown(f"**Backend:** `{API_BASE_URL}`") + + st.markdown("---") + st.subheader("Retrieval Settings") + top_k = st.slider("Quellen (Top-K)", min_value=1, max_value=10, value=5) + explain_mode = st.checkbox("Explanation Layer", value=True, help="Zeigt an, warum Quellen gewĂ€hlt wurden.") + + st.markdown("---") + st.markdown("### 🧠 System Status") + st.info(f"**Version:** v2.3.1\n\n**Modules:**\n- Decision Engine: ✅\n- Hybrid Router: ✅\n- Feedback Loop: ✅") + + if st.button("Clear Chat History"): + st.session_state.messages = [] + st.rerun() + + return top_k, explain_mode + +def render_intent_badge(intent, source): + """Visualizes the Decision Engine state.""" + icon = "🧠" + if intent == "EMPATHY": icon = "❀" + elif intent == "DECISION": icon = "⚖" + elif intent == "CODING": icon = "đŸ’»" + elif intent == "FACT": icon = "📚" + + return f""" +
+ {icon} Intent: {intent} ({source}) +
+ """ + +def render_sources(sources): + """Renders the retrieved sources in expandable cards.""" + if not sources: + return + + st.markdown("#### 📚 Verwendete Quellen") + for idx, hit in enumerate(sources): + score = hit.get('total_score', 0) + payload = hit.get('payload', {}) + note_type = payload.get('type', 'unknown') + title = hit.get('note_id', 'Unbekannt') + + # Determine Header Color/Icon based on score + score_icon = "🟱" if score > 0.8 else "🟡" if score > 0.5 else "âšȘ" + + with st.expander(f"{score_icon} {title} (Typ: {note_type}, Score: {score:.2f})"): + # Content + content = hit.get('source', {}).get('text', 'Kein Text verfĂŒgbar.') + st.markdown(f"_{content[:300]}..._") + + # Explanation (WP-04b) + explanation = hit.get('explanation') + if explanation: + st.markdown("---") + st.caption("**Warum wurde das gefunden?**") + reasons = explanation.get('reasons', []) + for r in reasons: + st.caption(f"- {r.get('message')}") + +# --- MAIN APP LOGIC --- + +top_k_setting, explain_setting = render_sidebar() + +st.title("mindnet v2.3.1") +st.caption("Lead Frontend Architect Edition | WP-10 Chat Interface") + +# 1. Render History +for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + if msg["role"] == "assistant": + # Render Meta-Data first + if "intent" in msg: + st.markdown(render_intent_badge(msg["intent"], msg.get("intent_source", "?")), unsafe_allow_html=True) + + st.markdown(msg["content"]) + + # Render Sources + if "sources" in msg: + render_sources(msg["sources"]) + + # Render Latency info + if "latency_ms" in msg: + st.caption(f"⏱ Antwortzeit: {msg['latency_ms']}ms | Query-ID: `{msg.get('query_id')}`") + + # Render Feedback Controls (Static for history items to prevent re-run issues) + # (Note: In a prod app, we would check if feedback was already given) + + else: + st.markdown(msg["content"]) + +# 2. Handle User Input +if prompt := st.chat_input("Was beschĂ€ftigt dich?"): + # Add User Message + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + # Generate Response + with st.chat_message("assistant"): + message_placeholder = st.empty() + # Placeholder for intent badge + status_placeholder = st.empty() + + with st.spinner("Thinking... (Decision Engine Active)"): + api_response = send_chat_message(prompt, top_k_setting, explain_setting) + + if "error" in api_response: + st.error(api_response["error"]) + else: + # Extract data + answer = api_response.get("answer", "") + intent = api_response.get("intent", "FACT") + source = api_response.get("intent_source", "Unknown") + query_id = api_response.get("query_id") + hits = api_response.get("sources", []) + latency = api_response.get("latency_ms", 0) + + # 1. Show Intent + status_placeholder.markdown(render_intent_badge(intent, source), unsafe_allow_html=True) + + # 2. Show Answer + message_placeholder.markdown(answer) + + # 3. Show Sources + render_sources(hits) + + # 4. Show Latency & Feedback UI + st.caption(f"⏱ {latency}ms | ID: `{query_id}`") + + # Feedback Buttons (Directly here for the *new* message) + col1, col2, col3, col4 = st.columns([1,1,1,4]) + with col1: + if st.button("👍", key=f"up_{query_id}"): + send_feedback(query_id, 5) + st.toast("Feedback gesendet: Positiv!") + with col2: + if st.button("👎", key=f"down_{query_id}"): + send_feedback(query_id, 1) + st.toast("Feedback gesendet: Negativ.") + + # Save to history + st.session_state.messages.append({ + "role": "assistant", + "content": answer, + "intent": intent, + "intent_source": source, + "sources": hits, + "query_id": query_id, + "latency_ms": latency + }) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d896285..1828b3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,25 +11,23 @@ qdrant-client>=1.15.1 pydantic>=2.11.7 numpy>=2.3.2 -# --- Markdown & Parsing (Hier fehlten Pakete!) --- +# --- Markdown & Parsing --- python-frontmatter>=1.1.0 -# WICHTIG: Das fehlte und verursachte den Fehler markdown-it-py>=3.0.0 -# WICHTIG: FĂŒr types.yaml und retriever.yaml PyYAML>=6.0.2 python-slugify>=8.0.4 # --- KI & Embeddings --- sentence-transformers>=5.1.0 -# Torch wird meist durch sentence-transformers geholt, -# aber wir listen es explizit fĂŒr StabilitĂ€t torch>=2.0.0 # --- Utilities --- -# WICHTIG: Damit .env Dateien gelesen werden python-dotenv>=1.1.1 requests>=2.32.5 tqdm>=4.67.1 # --- Testing --- pytest>=8.4.2 + +# --- Frontend (WP-10) --- +streamlit>=1.39.0 \ No newline at end of file From bbc1e589f585381af09ec160656e04eed27f28e9 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 19:00:39 +0100 Subject: [PATCH 2/9] =?UTF-8?q?Nutzung=20von=20.env=20f=C3=BCr=20Timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/frontend/ui.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 84043a5..dc80376 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -4,13 +4,23 @@ import uuid import os import json from datetime import datetime +from dotenv import load_dotenv # --- CONFIGURATION --- -# Default configuration taken from environment or fallback to localhost +# Load .env file explicitly to get timeouts and URLs +load_dotenv() + API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002") CHAT_ENDPOINT = f"{API_BASE_URL}/chat" FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback" +# Timeout strategy: +# 1. Try MINDNET_API_TIMEOUT (specific for frontend) +# 2. Try MINDNET_LLM_TIMEOUT (backend setting) +# 3. Default to 300 seconds (5 minutes) for local inference safety +timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIMEOUT") +API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 + # --- PAGE SETUP --- st.set_page_config( page_title="mindnet v2.3.1", @@ -57,11 +67,14 @@ def send_chat_message(message: str, top_k: int, explain: bool): "explain": explain } try: - response = requests.post(CHAT_ENDPOINT, json=payload, timeout=60) + # Use the configured timeout from .env + response = requests.post(CHAT_ENDPOINT, json=payload, timeout=API_TIMEOUT) response.raise_for_status() return response.json() + except requests.exceptions.ReadTimeout: + return {"error": f"Timeout: Das Backend hat nicht innerhalb von {int(API_TIMEOUT)} Sekunden geantwortet. (Local LLM is busy)."} except requests.exceptions.ConnectionError: - return {"error": "Backend nicht erreichbar. LĂ€uft der Server auf Port 8002?"} + return {"error": f"Backend nicht erreichbar unter {API_BASE_URL}. LĂ€uft der Server?"} except Exception as e: return {"error": str(e)} @@ -87,6 +100,7 @@ def render_sidebar(): with st.sidebar: st.header("⚙ Konfiguration") st.markdown(f"**Backend:** `{API_BASE_URL}`") + st.caption(f"⏱ Timeout: {int(API_TIMEOUT)}s") st.markdown("---") st.subheader("Retrieval Settings") @@ -170,9 +184,6 @@ for msg in st.session_state.messages: # Render Latency info if "latency_ms" in msg: st.caption(f"⏱ Antwortzeit: {msg['latency_ms']}ms | Query-ID: `{msg.get('query_id')}`") - - # Render Feedback Controls (Static for history items to prevent re-run issues) - # (Note: In a prod app, we would check if feedback was already given) else: st.markdown(msg["content"]) @@ -187,7 +198,6 @@ if prompt := st.chat_input("Was beschĂ€ftigt dich?"): # Generate Response with st.chat_message("assistant"): message_placeholder = st.empty() - # Placeholder for intent badge status_placeholder = st.empty() with st.spinner("Thinking... (Decision Engine Active)"): @@ -216,7 +226,7 @@ if prompt := st.chat_input("Was beschĂ€ftigt dich?"): # 4. Show Latency & Feedback UI st.caption(f"⏱ {latency}ms | ID: `{query_id}`") - # Feedback Buttons (Directly here for the *new* message) + # Feedback Buttons col1, col2, col3, col4 = st.columns([1,1,1,4]) with col1: if st.button("👍", key=f"up_{query_id}"): From 76ea8e3350277f9b72111302bce52b3982e81abf Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 22:07:58 +0100 Subject: [PATCH 3/9] =?UTF-8?q?Update=20f=C3=BCr=20Feedback=20Loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/chat.py | 39 +++++++++++++++--- app/services/feedback_service.py | 70 ++++++++++++++++++++------------ 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/app/routers/chat.py b/app/routers/chat.py index 58f5c82..5ab2d3a 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -1,6 +1,12 @@ """ -app/routers/chat.py — RAG Endpunkt (WP-06 Hybrid Router v3) -Update: Transparenz ĂŒber Intent-Source (Keyword vs. LLM). +app/routers/chat.py — RAG Endpunkt (WP-06 Hybrid Router + WP-04c Feedback) +Version: 2.3.2 (Merged Stability Patch) + +Features: +- Hybrid Intent Router (Keyword + LLM) +- Strategic Retrieval (Late Binding via Config) +- Context Enrichment (Payload/Source Fallback) +- Data Flywheel (Feedback Logging Integration) """ from fastapi import APIRouter, HTTPException, Depends @@ -15,6 +21,8 @@ from app.config import get_settings from app.models.dto import ChatRequest, ChatResponse, QueryRequest, QueryHit from app.services.llm_service import LLMService from app.core.retriever import Retriever +# [MERGE] Integration Feedback Service (WP-04c) +from app.services.feedback_service import log_search router = APIRouter() logger = logging.getLogger(__name__) @@ -77,7 +85,7 @@ def _build_enriched_context(hits: List[QueryHit]) -> str: ) title = hit.note_id or "Unbekannt" - # FIX: Wir holen den Typ aus Payload oder Source (Fallback) + # [FIX] Robustes Auslesen des Typs (Payload > Source > Unknown) payload = hit.payload or {} note_type = payload.get("type") or source.get("type", "unknown") note_type = str(note_type).upper() @@ -173,7 +181,7 @@ async def chat_endpoint( retrieve_result = await retriever.search(query_req) hits = retrieve_result.results - # 3. Strategic Retrieval + # 3. Strategic Retrieval (WP-06 Kernfeature) if inject_types: logger.info(f"[{query_id}] Executing Strategic Retrieval for types: {inject_types}...") strategy_req = QueryRequest( @@ -207,18 +215,37 @@ async def chat_endpoint( logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...") - # System-Prompt separat ĂŒbergeben + # System-Prompt separat ĂŒbergeben (WP-06a Fix) answer_text = await llm.generate_raw_response(prompt=final_prompt, system=system_prompt) duration_ms = int((time.time() - start_time) * 1000) + # 6. Logging (Fire & Forget) - [MERGE POINT] + # Wir loggen alles fĂŒr das Data Flywheel (WP-08 Self-Tuning) + try: + log_search( + query_id=query_id, + query_text=request.message, + results=hits, + mode="chat_rag", + metadata={ + "intent": intent, + "intent_source": intent_source, + "generated_answer": answer_text, + "model": llm.settings.LLM_MODEL + } + ) + except Exception as e: + logger.error(f"Logging failed: {e}") + + # 7. Response return ChatResponse( query_id=query_id, answer=answer_text, sources=hits, latency_ms=duration_ms, intent=intent, - intent_source=intent_source # Source durchreichen + intent_source=intent_source ) except Exception as e: diff --git a/app/services/feedback_service.py b/app/services/feedback_service.py index 6e945fe..99bbca2 100644 --- a/app/services/feedback_service.py +++ b/app/services/feedback_service.py @@ -2,13 +2,18 @@ app/services/feedback_service.py Service zum Loggen von Suchanfragen und Feedback (WP-04c). Speichert Daten als JSONL fĂŒr spĂ€teres Self-Tuning (WP-08). + +Version: 1.1 (Chat-Support) """ import json import os import time +import logging from pathlib import Path -from typing import Dict, Any, List -from app.models.dto import QueryRequest, QueryResponse, FeedbackRequest +from typing import Dict, Any, List, Union +from app.models.dto import QueryRequest, QueryResponse, FeedbackRequest, QueryHit + +logger = logging.getLogger(__name__) # Pfad fĂŒr Logs (lokal auf dem Beelink/PC) LOG_DIR = Path("data/logs") @@ -19,18 +24,35 @@ def _ensure_log_dir(): if not LOG_DIR.exists(): os.makedirs(LOG_DIR, exist_ok=True) -def log_search(req: QueryRequest, res: QueryResponse): +def _append_jsonl(file_path: Path, data: dict): + try: + with open(file_path, "a", encoding="utf-8") as f: + f.write(json.dumps(data, ensure_ascii=False) + "\n") + except Exception as e: + logger.error(f"Failed to write log: {e}") + +def log_search( + query_id: str, + query_text: str, + results: List[QueryHit], + mode: str = "unknown", + metadata: Dict[str, Any] = None +): """ - Speichert den "Snapshot" der Suche. - WICHTIG: Wir speichern die Scores (Breakdown), damit wir spĂ€ter wissen, - warum das System so entschieden hat. + Generische Logging-Funktion fĂŒr Suche UND Chat. + + Args: + query_id: UUID der Anfrage. + query_text: User-Eingabe. + results: Liste der Treffer (QueryHit Objekte). + mode: z.B. "semantic", "hybrid", "chat_rag". + metadata: ZusĂ€tzliche Infos (z.B. generierte Antwort, Intent). """ _ensure_log_dir() - # Wir reduzieren die Datenmenge etwas (z.B. keine vollen Texte) hits_summary = [] - for hit in res.results: - # Falls Explanation an war, speichern wir den Breakdown, sonst die Scores + for hit in results: + # Pydantic Model Dump fĂŒr saubere Serialisierung breakdown = None if hit.explanation and hit.explanation.breakdown: breakdown = hit.explanation.breakdown.model_dump() @@ -39,25 +61,24 @@ def log_search(req: QueryRequest, res: QueryResponse): "node_id": hit.node_id, "note_id": hit.note_id, "total_score": hit.total_score, - "breakdown": breakdown, # Wichtig fĂŒr Training! + "breakdown": breakdown, "rank_semantic": hit.semantic_score, - "rank_edge": hit.edge_bonus + "rank_edge": hit.edge_bonus, + "type": hit.source.get("type") if hit.source else "unknown" }) entry = { "timestamp": time.time(), - "query_id": res.query_id, - "query_text": req.query, - "mode": req.mode, - "top_k": req.top_k, - "hits": hits_summary + "query_id": query_id, + "query_text": query_text, + "mode": mode, + "hits_count": len(hits_summary), + "hits": hits_summary, + "metadata": metadata or {} } - try: - with open(SEARCH_LOG_FILE, "a", encoding="utf-8") as f: - f.write(json.dumps(entry, ensure_ascii=False) + "\n") - except Exception as e: - print(f"ERROR logging search: {e}") + _append_jsonl(SEARCH_LOG_FILE, entry) + logger.info(f"Logged search/chat interaction {query_id}") def log_feedback(fb: FeedbackRequest): """ @@ -73,8 +94,5 @@ def log_feedback(fb: FeedbackRequest): "comment": fb.comment } - try: - with open(FEEDBACK_LOG_FILE, "a", encoding="utf-8") as f: - f.write(json.dumps(entry, ensure_ascii=False) + "\n") - except Exception as e: - print(f"ERROR logging feedback: {e}") \ No newline at end of file + _append_jsonl(FEEDBACK_LOG_FILE, entry) + logger.info(f"Logged feedback for {fb.query_id}") \ No newline at end of file From 2d0913cf3b8b81683edb57a91a351dbccd1cc060 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 22:27:56 +0100 Subject: [PATCH 4/9] Feedback UI --- app/frontend/ui.py | 233 +++++++++++++++++++++------------------------ app/models/dto.py | 15 ++- 2 files changed, 121 insertions(+), 127 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index dc80376..d166661 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -2,22 +2,17 @@ import streamlit as st import requests import uuid import os -import json -from datetime import datetime +import time from dotenv import load_dotenv # --- CONFIGURATION --- -# Load .env file explicitly to get timeouts and URLs load_dotenv() API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002") CHAT_ENDPOINT = f"{API_BASE_URL}/chat" FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback" -# Timeout strategy: -# 1. Try MINDNET_API_TIMEOUT (specific for frontend) -# 2. Try MINDNET_LLM_TIMEOUT (backend setting) -# 3. Default to 300 seconds (5 minutes) for local inference safety +# Timeout-Strategie timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIMEOUT") API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 @@ -28,16 +23,10 @@ st.set_page_config( layout="centered" ) -# Custom CSS for cleaner look st.markdown(""" """, unsafe_allow_html=True) -# --- SESSION STATE INITIALIZATION --- +# --- SESSION STATE --- if "messages" not in st.session_state: st.session_state.messages = [] if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4()) -# --- API CLIENT FUNCTIONS --- +# --- API FUNCTIONS --- def send_chat_message(message: str, top_k: int, explain: bool): - """Sends the user message to the FastAPI backend.""" - payload = { - "message": message, - "top_k": top_k, - "explain": explain - } + payload = {"message": message, "top_k": top_k, "explain": explain} try: - # Use the configured timeout from .env response = requests.post(CHAT_ENDPOINT, json=payload, timeout=API_TIMEOUT) response.raise_for_status() return response.json() except requests.exceptions.ReadTimeout: - return {"error": f"Timeout: Das Backend hat nicht innerhalb von {int(API_TIMEOUT)} Sekunden geantwortet. (Local LLM is busy)."} - except requests.exceptions.ConnectionError: - return {"error": f"Backend nicht erreichbar unter {API_BASE_URL}. LĂ€uft der Server?"} + return {"error": f"Timeout ({int(API_TIMEOUT)}s). Das lokale LLM rechnet noch."} except Exception as e: return {"error": str(e)} -def send_feedback(query_id: str, score: int): - """Sends feedback to the backend.""" - # Note: We rate the overall answer. API expects node_id. - # We use 'generated_answer' as a convention for the full response. +def submit_feedback(query_id: str, node_id: str, score: int, comment: str = None): + """Sendet Feedback asynchron.""" payload = { "query_id": query_id, - "node_id": "generated_answer", + "node_id": node_id, "score": score, - "comment": "User feedback via Streamlit UI" + "comment": comment } try: requests.post(FEEDBACK_ENDPOINT, json=payload, timeout=5) - return True - except: - return False + # Wir nutzen st.toast fĂŒr dezentes Feedback ohne Rerun + target = "Antwort" if node_id == "generated_answer" else "Quelle" + st.toast(f"Feedback fĂŒr {target} gespeichert! (Score: {score})") + except Exception as e: + st.error(f"Feedback-Fehler: {e}") # --- UI COMPONENTS --- def render_sidebar(): with st.sidebar: st.header("⚙ Konfiguration") - st.markdown(f"**Backend:** `{API_BASE_URL}`") - st.caption(f"⏱ Timeout: {int(API_TIMEOUT)}s") + st.caption(f"Backend: `{API_BASE_URL}`") - st.markdown("---") - st.subheader("Retrieval Settings") - top_k = st.slider("Quellen (Top-K)", min_value=1, max_value=10, value=5) - explain_mode = st.checkbox("Explanation Layer", value=True, help="Zeigt an, warum Quellen gewĂ€hlt wurden.") + st.subheader("Retrieval") + top_k = st.slider("Quellen Anzahl", 1, 10, 5) + explain_mode = st.toggle("Explanation Layer", value=True) - st.markdown("---") - st.markdown("### 🧠 System Status") - st.info(f"**Version:** v2.3.1\n\n**Modules:**\n- Decision Engine: ✅\n- Hybrid Router: ✅\n- Feedback Loop: ✅") - - if st.button("Clear Chat History"): + st.divider() + st.info("WP-10: Advanced Feedback Loop Active") + if st.button("Reset Chat"): st.session_state.messages = [] st.rerun() - return top_k, explain_mode def render_intent_badge(intent, source): - """Visualizes the Decision Engine state.""" icon = "🧠" if intent == "EMPATHY": icon = "❀" elif intent == "DECISION": icon = "⚖" elif intent == "CODING": icon = "đŸ’»" elif intent == "FACT": icon = "📚" - - return f""" -
- {icon} Intent: {intent} ({source}) -
- """ + return f"""
{icon} Intent: {intent} ({source})
""" -def render_sources(sources): - """Renders the retrieved sources in expandable cards.""" +def render_sources(sources, query_id): + """ + Rendert Quellen inklusive granularem Feedback-Mechanismus. + """ if not sources: return st.markdown("#### 📚 Verwendete Quellen") + for idx, hit in enumerate(sources): score = hit.get('total_score', 0) + node_id = hit.get('node_id') + title = hit.get('note_id', 'Unbekannt') payload = hit.get('payload', {}) note_type = payload.get('type', 'unknown') - title = hit.get('note_id', 'Unbekannt') - # Determine Header Color/Icon based on score + # Icon basierend auf Score score_icon = "🟱" if score > 0.8 else "🟡" if score > 0.5 else "âšȘ" + expander_title = f"{score_icon} {title} (Typ: {note_type}, Score: {score:.2f})" - with st.expander(f"{score_icon} {title} (Typ: {note_type}, Score: {score:.2f})"): - # Content - content = hit.get('source', {}).get('text', 'Kein Text verfĂŒgbar.') - st.markdown(f"_{content[:300]}..._") + with st.expander(expander_title): + # 1. Inhalt + text = hit.get('source', {}).get('text', 'Kein Text') + st.markdown(f"_{text[:300]}..._") - # Explanation (WP-04b) - explanation = hit.get('explanation') - if explanation: - st.markdown("---") - st.caption("**Warum wurde das gefunden?**") - reasons = explanation.get('reasons', []) - for r in reasons: + # 2. Explanation (Why-Layer) + if 'explanation' in hit and hit['explanation']: + st.caption("**Warum gefunden?**") + for r in hit['explanation'].get('reasons', []): st.caption(f"- {r.get('message')}") -# --- MAIN APP LOGIC --- + # 3. Granulares Feedback (Source Level) + st.markdown("---") + c1, c2 = st.columns([3, 1]) + with c1: + st.caption("War diese Quelle hilfreich fĂŒr die Antwort?") + with c2: + # Callback Wrapper fĂŒr Source-Feedback + def on_source_fb(qid=query_id, nid=node_id, k=f"fb_src_{node_id}"): + val = st.session_state.get(k) + # Mapping: Thumbs Up (1) -> Score 5, Thumbs Down (0) -> Score 1 + mapped_score = 5 if val == 1 else 1 + submit_feedback(qid, nid, mapped_score, comment="Source Feedback via UI") -top_k_setting, explain_setting = render_sidebar() + st.feedback( + "thumbs", + key=f"fb_src_{query_id}_{node_id}", # Unique Key pro Query/Node + on_change=on_source_fb, + kwargs={"qid": query_id, "nid": node_id, "k": f"fb_src_{query_id}_{node_id}"} + ) +# --- MAIN APP --- + +top_k, show_explain = render_sidebar() st.title("mindnet v2.3.1") -st.caption("Lead Frontend Architect Edition | WP-10 Chat Interface") -# 1. Render History +# 1. Chat History Rendern for msg in st.session_state.messages: with st.chat_message(msg["role"]): if msg["role"] == "assistant": - # Render Meta-Data first + # Meta-Daten if "intent" in msg: st.markdown(render_intent_badge(msg["intent"], msg.get("intent_source", "?")), unsafe_allow_html=True) + # Antwort-Text st.markdown(msg["content"]) - # Render Sources + # Quellen (mit Feedback-Option, aber Status ist readonly fĂŒr alte Nachrichten in Streamlit oft schwierig, + # daher rendern wir Feedback-Controls idealerweise nur fĂŒr die letzte Nachricht oder speichern Status) + # In dieser Version rendern wir sie immer, Streamlit State managed das. if "sources" in msg: - render_sources(msg["sources"]) + render_sources(msg["sources"], msg["query_id"]) - # Render Latency info - if "latency_ms" in msg: - st.caption(f"⏱ Antwortzeit: {msg['latency_ms']}ms | Query-ID: `{msg.get('query_id')}`") + # Globales Feedback (Sterne) + qid = msg["query_id"] + + def on_global_fb(q=qid, k=f"fb_glob_{qid}"): + val = st.session_state.get(k) # Liefert 0-4 + if val is not None: + submit_feedback(q, "generated_answer", val + 1, comment="Global Star Rating") + + st.caption("Wie gut war diese Antwort?") + st.feedback( + "stars", + key=f"fb_glob_{qid}", + on_change=on_global_fb + ) else: st.markdown(msg["content"]) -# 2. Handle User Input -if prompt := st.chat_input("Was beschĂ€ftigt dich?"): - # Add User Message +# 2. User Input +if prompt := st.chat_input("Deine Frage an das System..."): + # User Message anzeigen st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) - # Generate Response + # API Call with st.chat_message("assistant"): - message_placeholder = st.empty() - status_placeholder = st.empty() + with st.spinner("Thinking..."): + resp = send_chat_message(prompt, top_k, show_explain) - with st.spinner("Thinking... (Decision Engine Active)"): - api_response = send_chat_message(prompt, top_k_setting, explain_setting) - - if "error" in api_response: - st.error(api_response["error"]) + if "error" in resp: + st.error(resp["error"]) else: - # Extract data - answer = api_response.get("answer", "") - intent = api_response.get("intent", "FACT") - source = api_response.get("intent_source", "Unknown") - query_id = api_response.get("query_id") - hits = api_response.get("sources", []) - latency = api_response.get("latency_ms", 0) + # Daten extrahieren + answer = resp.get("answer", "") + intent = resp.get("intent", "FACT") + source = resp.get("intent_source", "Unknown") + query_id = resp.get("query_id") + hits = resp.get("sources", []) + + # Sofort rendern (damit User nicht auf Rerun warten muss) + st.markdown(render_intent_badge(intent, source), unsafe_allow_html=True) + st.markdown(answer) + render_sources(hits, query_id) + + # Feedback Slot fĂŒr die NEUE Nachricht vorbereiten + st.caption("Wie gut war diese Antwort?") + st.feedback("stars", key=f"fb_glob_{query_id}", on_change=lambda: submit_feedback(query_id, "generated_answer", st.session_state[f"fb_glob_{query_id}"] + 1)) - # 1. Show Intent - status_placeholder.markdown(render_intent_badge(intent, source), unsafe_allow_html=True) - - # 2. Show Answer - message_placeholder.markdown(answer) - - # 3. Show Sources - render_sources(hits) - - # 4. Show Latency & Feedback UI - st.caption(f"⏱ {latency}ms | ID: `{query_id}`") - - # Feedback Buttons - col1, col2, col3, col4 = st.columns([1,1,1,4]) - with col1: - if st.button("👍", key=f"up_{query_id}"): - send_feedback(query_id, 5) - st.toast("Feedback gesendet: Positiv!") - with col2: - if st.button("👎", key=f"down_{query_id}"): - send_feedback(query_id, 1) - st.toast("Feedback gesendet: Negativ.") - - # Save to history + # In History speichern st.session_state.messages.append({ "role": "assistant", "content": answer, "intent": intent, "intent_source": source, "sources": hits, - "query_id": query_id, - "latency_ms": latency + "query_id": query_id }) \ No newline at end of file diff --git a/app/models/dto.py b/app/models/dto.py index 85767f8..22e4cff 100644 --- a/app/models/dto.py +++ b/app/models/dto.py @@ -6,7 +6,7 @@ Zweck: WP-06 Update: Intent & Intent-Source in ChatResponse. Version: - 0.6.1 (WP-06: Decision Engine Transparency) + 0.6.2 (WP-06: Decision Engine Transparency, Erweiterung des Feeback Request) Stand: 2025-12-09 """ @@ -64,11 +64,14 @@ class QueryRequest(BaseModel): class FeedbackRequest(BaseModel): """ - User-Feedback zu einem spezifischen Treffer. + User-Feedback zu einem spezifischen Treffer oder der Gesamtantwort. """ query_id: str = Field(..., description="ID der ursprĂŒnglichen Suche") - node_id: str = Field(..., description="ID des bewerteten Treffers") - score: int = Field(..., ge=0, le=1, description="1 (Positiv) oder 0 (Negativ/Irrelevant)") + # node_id ist optional: Wenn leer oder "generated_answer", gilt es fĂŒr die Antwort. + # Wenn eine echte Chunk-ID, gilt es fĂŒr die Quelle. + node_id: str = Field(..., description="ID des bewerteten Treffers oder 'generated_answer'") + # Update: Range auf 1-5 erweitert fĂŒr differenziertes Tuning + score: int = Field(..., ge=1, le=5, description="1 (Irrelevant/Falsch) bis 5 (Perfekt)") comment: Optional[str] = None @@ -152,4 +155,6 @@ class ChatResponse(BaseModel): sources: List[QueryHit] = Field(..., description="Die fĂŒr die Antwort genutzten Quellen") latency_ms: int intent: Optional[str] = Field("FACT", description="WP-06: Erkannter Intent (FACT/DECISION)") - intent_source: Optional[str] = Field("Unknown", description="WP-06: Quelle der Intent-Erkennung (Keyword vs. LLM)") \ No newline at end of file + intent_source: Optional[str] = Field("Unknown", description="WP-06: Quelle der Intent-Erkennung (Keyword vs. LLM)") + + \ No newline at end of file From ec33163d98159c5fecaddac13b8066465096f674 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 22:43:38 +0100 Subject: [PATCH 5/9] verbessertes Feedback --- app/frontend/ui.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index d166661..5f87fcd 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -107,7 +107,7 @@ def render_intent_badge(intent, source): def render_sources(sources, query_id): """ - Rendert Quellen inklusive granularem Feedback-Mechanismus. + Rendert Quellen inklusive granularem Feedback-Mechanismus (1-5 via Faces). """ if not sources: return @@ -136,22 +136,25 @@ def render_sources(sources, query_id): for r in hit['explanation'].get('reasons', []): st.caption(f"- {r.get('message')}") - # 3. Granulares Feedback (Source Level) + # 3. Granulares Feedback (Source Level) - JETZT MIT NUANCEN st.markdown("---") - c1, c2 = st.columns([3, 1]) + c1, c2 = st.columns([2, 2]) with c1: - st.caption("War diese Quelle hilfreich fĂŒr die Antwort?") + st.caption("Relevanz dieser Quelle:") with c2: # Callback Wrapper fĂŒr Source-Feedback def on_source_fb(qid=query_id, nid=node_id, k=f"fb_src_{node_id}"): val = st.session_state.get(k) - # Mapping: Thumbs Up (1) -> Score 5, Thumbs Down (0) -> Score 1 - mapped_score = 5 if val == 1 else 1 - submit_feedback(qid, nid, mapped_score, comment="Source Feedback via UI") + # Mapping: + # Faces liefert 0 (😞) bis 4 (😀). + # Wir mappen das auf 1-5 fĂŒr das Backend. + if val is not None: + submit_feedback(qid, nid, val + 1, comment="Source Feedback (Faces)") + # 'faces' bietet 5 Stufen: 😞(1) 🙁(2) 😐(3) 🙂(4) 😀(5) st.feedback( - "thumbs", - key=f"fb_src_{query_id}_{node_id}", # Unique Key pro Query/Node + "faces", + key=f"fb_src_{query_id}_{node_id}", on_change=on_source_fb, kwargs={"qid": query_id, "nid": node_id, "k": f"fb_src_{query_id}_{node_id}"} ) From 2acaf3c060d292fb94079f41257b7aec7796110d Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 23:05:28 +0100 Subject: [PATCH 6/9] =?UTF-8?q?vorbereitung=20UI=20f=C3=BCr=20WP07?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/frontend/ui.py | 350 +++++++++++++++++++++------------------------ 1 file changed, 162 insertions(+), 188 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 5f87fcd..3cf9c27 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -2,240 +2,214 @@ import streamlit as st import requests import uuid import os -import time +import json +from pathlib import Path from dotenv import load_dotenv # --- CONFIGURATION --- load_dotenv() - API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002") CHAT_ENDPOINT = f"{API_BASE_URL}/chat" FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback" +HISTORY_FILE = Path("data/logs/search_history.jsonl") -# Timeout-Strategie +# Timeout Strategy timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIMEOUT") API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 # --- PAGE SETUP --- -st.set_page_config( - page_title="mindnet v2.3.1", - page_icon="🧠", - layout="centered" -) +st.set_page_config(page_title="mindnet v2.3.1", page_icon="🧠", layout="wide") +# --- CSS STYLING (VISUAL POLISH) --- st.markdown(""" """, unsafe_allow_html=True) # --- SESSION STATE --- -if "messages" not in st.session_state: - st.session_state.messages = [] -if "user_id" not in st.session_state: - st.session_state.user_id = str(uuid.uuid4()) +if "messages" not in st.session_state: st.session_state.messages = [] +if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4()) +if "draft_note" not in st.session_state: st.session_state.draft_note = {"title": "", "content": "", "type": "concept"} -# --- API FUNCTIONS --- +# --- HELPER FUNCTIONS --- + +def load_history_from_logs(limit=10): + """Liest die letzten N Queries aus dem Logfile (WP-04c Data Flywheel).""" + queries = [] + if HISTORY_FILE.exists(): + try: + with open(HISTORY_FILE, "r", encoding="utf-8") as f: + # Datei rĂŒckwĂ€rts oder komplett lesen (bei großen Logs besser `tail`) + lines = f.readlines() + for line in reversed(lines): + try: + entry = json.loads(line) + q = entry.get("query_text") + if q and q not in queries: + queries.append(q) + if len(queries) >= limit: break + except: continue + except Exception as e: + st.sidebar.warning(f"Log-Fehler: {e}") + return queries def send_chat_message(message: str, top_k: int, explain: bool): - payload = {"message": message, "top_k": top_k, "explain": explain} try: - response = requests.post(CHAT_ENDPOINT, json=payload, timeout=API_TIMEOUT) + response = requests.post( + CHAT_ENDPOINT, + json={"message": message, "top_k": top_k, "explain": explain}, + timeout=API_TIMEOUT + ) response.raise_for_status() return response.json() - except requests.exceptions.ReadTimeout: - return {"error": f"Timeout ({int(API_TIMEOUT)}s). Das lokale LLM rechnet noch."} except Exception as e: return {"error": str(e)} -def submit_feedback(query_id: str, node_id: str, score: int, comment: str = None): - """Sendet Feedback asynchron.""" - payload = { - "query_id": query_id, - "node_id": node_id, - "score": score, - "comment": comment - } +def submit_feedback(query_id, node_id, score, comment=None): try: - requests.post(FEEDBACK_ENDPOINT, json=payload, timeout=5) - # Wir nutzen st.toast fĂŒr dezentes Feedback ohne Rerun + requests.post(FEEDBACK_ENDPOINT, json={"query_id": query_id, "node_id": node_id, "score": score, "comment": comment}, timeout=2) target = "Antwort" if node_id == "generated_answer" else "Quelle" st.toast(f"Feedback fĂŒr {target} gespeichert! (Score: {score})") - except Exception as e: - st.error(f"Feedback-Fehler: {e}") + except: pass # --- UI COMPONENTS --- def render_sidebar(): with st.sidebar: - st.header("⚙ Konfiguration") - st.caption(f"Backend: `{API_BASE_URL}`") + st.title("🧠 mindnet") + st.caption("v2.3.1 | WP-10 UI") - st.subheader("Retrieval") - top_k = st.slider("Quellen Anzahl", 1, 10, 5) - explain_mode = st.toggle("Explanation Layer", value=True) + mode = st.radio("Modus", ["💬 Chat", "📝 Neuer Eintrag (WP-07)"], index=0) st.divider() - st.info("WP-10: Advanced Feedback Loop Active") - if st.button("Reset Chat"): - st.session_state.messages = [] - st.rerun() - return top_k, explain_mode + st.subheader("⚙ Settings") + top_k = st.slider("Quellen (Top-K)", 1, 10, 5) + explain = st.toggle("Explanation Layer", True) + + st.divider() + st.subheader("🕒 Verlauf") + history = load_history_from_logs(8) + if not history: + st.caption("Noch keine EintrĂ€ge.") + for q in history: + if st.button(f"🔎 {q[:30]}...", key=f"hist_{q}", help=q, use_container_width=True): + # Trick: Query in Input 'injecten' geht schwer, wir feuern direkt ab + st.session_state.messages.append({"role": "user", "content": q}) + st.rerun() -def render_intent_badge(intent, source): - icon = "🧠" - if intent == "EMPATHY": icon = "❀" - elif intent == "DECISION": icon = "⚖" - elif intent == "CODING": icon = "đŸ’»" - elif intent == "FACT": icon = "📚" - return f"""
{icon} Intent: {intent} ({source})
""" + return mode, top_k, explain -def render_sources(sources, query_id): - """ - Rendert Quellen inklusive granularem Feedback-Mechanismus (1-5 via Faces). - """ - if not sources: - return +def render_chat_interface(top_k, explain): + # Render History + for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + if msg["role"] == "assistant": + # Intent Badge + if "intent" in msg: + icon = {"EMPATHY": "❀", "DECISION": "⚖", "CODING": "đŸ’»", "FACT": "📚"}.get(msg["intent"], "🧠") + st.markdown(f'
{icon} Intent: {msg["intent"]}
', unsafe_allow_html=True) + + st.markdown(msg["content"]) + + # Sources + if "sources" in msg: + for hit in msg["sources"]: + score = hit.get('total_score', 0) + icon = "🟱" if score > 0.8 else "🟡" if score > 0.5 else "âšȘ" + with st.expander(f"{icon} {hit.get('note_id', '?')} ({score:.2f})"): + st.markdown(f"_{hit.get('source', {}).get('text', '')[:300]}..._") + if hit.get('explanation'): + st.caption(f"Grund: {hit['explanation']['reasons'][0]['message']}") + + # Granular Feedback + def _cb(qid=msg["query_id"], nid=hit['node_id']): + val = st.session_state.get(f"fb_src_{qid}_{nid}") + if val is not None: submit_feedback(qid, nid, val+1, "Faces UI") + + st.feedback("faces", key=f"fb_src_{msg['query_id']}_{hit['node_id']}", on_change=_cb) + + # Global Feedback + qid = msg["query_id"] + st.feedback("stars", key=f"fb_glob_{qid}", on_change=lambda: submit_feedback(qid, "generated_answer", st.session_state[f"fb_glob_{qid}"]+1)) + + else: + st.markdown(msg["content"]) + + # Input Logic + # PrĂŒfen ob wir aus der History kommen (letzte Nachricht User und noch keine Antwort?) + last_msg_is_user = len(st.session_state.messages) > 0 and st.session_state.messages[-1]["role"] == "user" + # Da st.rerun() die ganze App neu lĂ€dt, mĂŒssen wir prĂŒfen, ob wir auf eine Antwort warten + # Aber Streamlit Flow ist: Input -> Rerun -> Code lĂ€uft -> Render. + # Wir brauchen einen Trigger. - st.markdown("#### 📚 Verwendete Quellen") + if prompt := st.chat_input("Frage Mindnet..."): + st.session_state.messages.append({"role": "user", "content": prompt}) + st.rerun() + + # Wenn die letzte Nachricht vom User ist (egal ob via Input oder History Button), generiere Antwort + if last_msg_is_user: + last_prompt = st.session_state.messages[-1]["content"] + with st.chat_message("assistant"): + with st.spinner("Thinking..."): + resp = send_chat_message(last_prompt, top_k, explain) + + if "error" in resp: + st.error(resp["error"]) + # Entferne die User Nachricht, damit man es nochmal probieren kann? Optional. + else: + # Speichern und Rerun fĂŒr sauberes Rendering + msg_data = { + "role": "assistant", + "content": resp.get("answer"), + "intent": resp.get("intent", "FACT"), + "sources": resp.get("sources", []), + "query_id": resp.get("query_id") + } + st.session_state.messages.append(msg_data) + st.rerun() + +def render_creation_interface(): + st.header("📝 Neuer Wissens-Eintrag (WP-07/11)") + st.info("Hier kannst du strukturierte Notizen erstellen, die direkt in den Obsidian Vault gespeichert werden.") - for idx, hit in enumerate(sources): - score = hit.get('total_score', 0) - node_id = hit.get('node_id') - title = hit.get('note_id', 'Unbekannt') - payload = hit.get('payload', {}) - note_type = payload.get('type', 'unknown') + with st.form("new_entry"): + col1, col2 = st.columns([3, 1]) + title = col1.text_input("Titel der Notiz", placeholder="z.B. Projekt Gamma Meeting") + n_type = col2.selectbox("Typ", ["concept", "meeting", "person", "project", "decision"]) - # Icon basierend auf Score - score_icon = "🟱" if score > 0.8 else "🟡" if score > 0.5 else "âšȘ" - expander_title = f"{score_icon} {title} (Typ: {note_type}, Score: {score:.2f})" + content = st.text_area("Inhalt (Markdown)", height=300, placeholder="# Protokoll\n\n- Punkt 1...") - with st.expander(expander_title): - # 1. Inhalt - text = hit.get('source', {}).get('text', 'Kein Text') - st.markdown(f"_{text[:300]}..._") - - # 2. Explanation (Why-Layer) - if 'explanation' in hit and hit['explanation']: - st.caption("**Warum gefunden?**") - for r in hit['explanation'].get('reasons', []): - st.caption(f"- {r.get('message')}") - - # 3. Granulares Feedback (Source Level) - JETZT MIT NUANCEN - st.markdown("---") - c1, c2 = st.columns([2, 2]) - with c1: - st.caption("Relevanz dieser Quelle:") - with c2: - # Callback Wrapper fĂŒr Source-Feedback - def on_source_fb(qid=query_id, nid=node_id, k=f"fb_src_{node_id}"): - val = st.session_state.get(k) - # Mapping: - # Faces liefert 0 (😞) bis 4 (😀). - # Wir mappen das auf 1-5 fĂŒr das Backend. - if val is not None: - submit_feedback(qid, nid, val + 1, comment="Source Feedback (Faces)") - - # 'faces' bietet 5 Stufen: 😞(1) 🙁(2) 😐(3) 🙂(4) 😀(5) - st.feedback( - "faces", - key=f"fb_src_{query_id}_{node_id}", - on_change=on_source_fb, - kwargs={"qid": query_id, "nid": node_id, "k": f"fb_src_{query_id}_{node_id}"} - ) - -# --- MAIN APP --- - -top_k, show_explain = render_sidebar() -st.title("mindnet v2.3.1") - -# 1. Chat History Rendern -for msg in st.session_state.messages: - with st.chat_message(msg["role"]): - if msg["role"] == "assistant": - # Meta-Daten - if "intent" in msg: - st.markdown(render_intent_badge(msg["intent"], msg.get("intent_source", "?")), unsafe_allow_html=True) - - # Antwort-Text - st.markdown(msg["content"]) - - # Quellen (mit Feedback-Option, aber Status ist readonly fĂŒr alte Nachrichten in Streamlit oft schwierig, - # daher rendern wir Feedback-Controls idealerweise nur fĂŒr die letzte Nachricht oder speichern Status) - # In dieser Version rendern wir sie immer, Streamlit State managed das. - if "sources" in msg: - render_sources(msg["sources"], msg["query_id"]) - - # Globales Feedback (Sterne) - qid = msg["query_id"] - - def on_global_fb(q=qid, k=f"fb_glob_{qid}"): - val = st.session_state.get(k) # Liefert 0-4 - if val is not None: - submit_feedback(q, "generated_answer", val + 1, comment="Global Star Rating") - - st.caption("Wie gut war diese Antwort?") - st.feedback( - "stars", - key=f"fb_glob_{qid}", - on_change=on_global_fb - ) - - else: - st.markdown(msg["content"]) - -# 2. User Input -if prompt := st.chat_input("Deine Frage an das System..."): - # User Message anzeigen - st.session_state.messages.append({"role": "user", "content": prompt}) - with st.chat_message("user"): - st.markdown(prompt) - - # API Call - with st.chat_message("assistant"): - with st.spinner("Thinking..."): - resp = send_chat_message(prompt, top_k, show_explain) + st.markdown("**Automatische Vernetzung:**") + st.caption("Verwende `[[Link]]` fĂŒr Referenzen und `[[rel:depends_on X]]` fĂŒr logische Kanten.") - if "error" in resp: - st.error(resp["error"]) - else: - # Daten extrahieren - answer = resp.get("answer", "") - intent = resp.get("intent", "FACT") - source = resp.get("intent_source", "Unknown") - query_id = resp.get("query_id") - hits = resp.get("sources", []) - - # Sofort rendern (damit User nicht auf Rerun warten muss) - st.markdown(render_intent_badge(intent, source), unsafe_allow_html=True) - st.markdown(answer) - render_sources(hits, query_id) - - # Feedback Slot fĂŒr die NEUE Nachricht vorbereiten - st.caption("Wie gut war diese Antwort?") - st.feedback("stars", key=f"fb_glob_{query_id}", on_change=lambda: submit_feedback(query_id, "generated_answer", st.session_state[f"fb_glob_{query_id}"] + 1)) + submitted = st.form_submit_button("đŸ’Ÿ Speichern & Indizieren") + if submitted: + # TODO: Hier mĂŒsste der POST Request an eine /ingest API gehen + # Da diese API in v2.3.1 noch fehlt, simulieren wir es. + st.success(f"Mockup: Notiz '{title}' ({n_type}) wĂ€re jetzt gespeichert worden!") + st.balloons() - # In History speichern - st.session_state.messages.append({ - "role": "assistant", - "content": answer, - "intent": intent, - "intent_source": source, - "sources": hits, - "query_id": query_id - }) \ No newline at end of file +# --- MAIN LOOP --- +mode, top_k, explain = render_sidebar() + +if mode == "💬 Chat": + render_chat_interface(top_k, explain) +else: + render_creation_interface() \ No newline at end of file From 9bb6566419a492132c0219e71be506d44a9be53f Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 23:12:07 +0100 Subject: [PATCH 7/9] =?UTF-8?q?kleine=20=C3=84nderungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/frontend/ui.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 3cf9c27..5504d7f 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -124,8 +124,10 @@ def render_chat_interface(top_k, explain): # Intent Badge if "intent" in msg: icon = {"EMPATHY": "❀", "DECISION": "⚖", "CODING": "đŸ’»", "FACT": "📚"}.get(msg["intent"], "🧠") - st.markdown(f'
{icon} Intent: {msg["intent"]}
', unsafe_allow_html=True) - + # st.markdown(f'
{icon} Intent: {msg["intent"]}
', unsafe_allow_html=True) + # NEU (mit Source): + source_info = msg.get("intent_source", "Unknown") + st.markdown(f'
{icon} Intent: {msg["intent"]} via {source_info}
', unsafe_allow_html=True) st.markdown(msg["content"]) # Sources From 38abde8516a366f76e38f400552a181ade99633e Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 23:16:28 +0100 Subject: [PATCH 8/9] Gui Anpassung. --- app/frontend/ui.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 5504d7f..418e82c 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -20,7 +20,7 @@ API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 # --- PAGE SETUP --- st.set_page_config(page_title="mindnet v2.3.1", page_icon="🧠", layout="wide") -# --- CSS STYLING (VISUAL POLISH) --- +# --- CSS STYLING --- st.markdown("""