WP07 #7
|
|
@ -1,10 +1,11 @@
|
|||
"""
|
||||
app/routers/chat.py — RAG Endpunkt (WP-06 Hybrid Router + WP-04c Feedback)
|
||||
Version: 2.3.2 (Merged Stability Patch)
|
||||
app/routers/chat.py — RAG Endpunkt (WP-06 Hybrid Router + WP-07 Interview Mode)
|
||||
Version: 2.4.0 (Interview Support)
|
||||
|
||||
Features:
|
||||
- Hybrid Intent Router (Keyword + LLM)
|
||||
- Strategic Retrieval (Late Binding via Config)
|
||||
- Interview Loop (Schema-driven Data Collection)
|
||||
- Context Enrichment (Payload/Source Fallback)
|
||||
- Data Flywheel (Feedback Logging Integration)
|
||||
"""
|
||||
|
|
@ -21,7 +22,6 @@ 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()
|
||||
|
|
@ -62,6 +62,47 @@ def get_decision_strategy(intent: str) -> Dict[str, Any]:
|
|||
strategies = config.get("strategies", {})
|
||||
return strategies.get(intent, strategies.get("FACT", {}))
|
||||
|
||||
# --- Helper: Target Type Detection (WP-07) ---
|
||||
|
||||
def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Versucht zu erraten, welchen Notiz-Typ der User erstellen will.
|
||||
Nutzt Keywords und Mappings.
|
||||
"""
|
||||
message_lower = message.lower()
|
||||
|
||||
# 1. Direkter Match mit Schema-Keys (z.B. "projekt", "entscheidung")
|
||||
# Ignoriere 'default' hier
|
||||
for type_key in configured_schemas.keys():
|
||||
if type_key == "default":
|
||||
continue
|
||||
if type_key in message_lower:
|
||||
return type_key
|
||||
|
||||
# 2. Synonym-Mapping (Deutsch -> Schema Key)
|
||||
# Dies verbessert die UX, falls User deutsche Begriffe nutzen
|
||||
synonyms = {
|
||||
"projekt": "project",
|
||||
"vorhaben": "project",
|
||||
"entscheidung": "decision",
|
||||
"beschluss": "decision",
|
||||
"ziel": "goal",
|
||||
"erfahrung": "experience",
|
||||
"lektion": "experience",
|
||||
"wert": "value",
|
||||
"prinzip": "principle",
|
||||
"grundsatz": "principle",
|
||||
"notiz": "default",
|
||||
"idee": "default"
|
||||
}
|
||||
|
||||
for term, schema_key in synonyms.items():
|
||||
if term in message_lower:
|
||||
# Prüfen, ob der gemappte Key auch konfiguriert ist
|
||||
if schema_key in configured_schemas:
|
||||
return schema_key
|
||||
|
||||
return "default"
|
||||
|
||||
# --- Dependencies ---
|
||||
|
||||
|
|
@ -167,8 +208,56 @@ async def chat_endpoint(
|
|||
|
||||
# Strategy Load
|
||||
strategy = get_decision_strategy(intent)
|
||||
inject_types = strategy.get("inject_types", [])
|
||||
prompt_key = strategy.get("prompt_template", "rag_template")
|
||||
|
||||
# --- SPLIT LOGIC: INTERVIEW vs. RAG ---
|
||||
|
||||
sources_hits = []
|
||||
final_prompt = ""
|
||||
|
||||
if intent == "INTERVIEW":
|
||||
# --- WP-07: INTERVIEW MODE ---
|
||||
# Kein Retrieval. Wir nutzen den Dialog-Kontext.
|
||||
|
||||
# 1. Schema Loading (Late Binding)
|
||||
schemas = strategy.get("schemas", {})
|
||||
target_type = _detect_target_type(request.message, schemas)
|
||||
active_schema = schemas.get(target_type, schemas.get("default"))
|
||||
|
||||
logger.info(f"[{query_id}] Starting Interview for Type: {target_type}")
|
||||
|
||||
# Robustes Schema-Parsing (Dict vs List)
|
||||
if isinstance(active_schema, dict):
|
||||
fields_list = active_schema.get("fields", [])
|
||||
hint_str = active_schema.get("hint", "")
|
||||
else:
|
||||
fields_list = active_schema # Fallback falls nur Liste definiert
|
||||
hint_str = ""
|
||||
|
||||
fields_str = "\n- " + "\n- ".join(fields_list)
|
||||
|
||||
# 2. Context Logic
|
||||
# Hinweis: In einer Stateless-API ist {context_str} idealerweise die History.
|
||||
# Da ChatRequest (noch) kein History-Feld hat, nutzen wir einen Placeholder
|
||||
# oder verlassen uns darauf, dass der Client die History im Prompt mitschickt
|
||||
# (Streamlit Pattern: Appends history to prompt).
|
||||
# Wir labeln es hier explizit.
|
||||
context_str = "Bisheriger Verlauf (falls vorhanden): Siehe oben/unten."
|
||||
|
||||
# 3. Prompt Assembly
|
||||
template = llm.prompts.get(prompt_key, "")
|
||||
final_prompt = template.replace("{context_str}", context_str) \
|
||||
.replace("{query}", request.message) \
|
||||
.replace("{target_type}", target_type) \
|
||||
.replace("{schema_fields}", fields_str) \
|
||||
.replace("{schema_hint}", hint_str)
|
||||
|
||||
# Keine Hits im Interview
|
||||
sources_hits = []
|
||||
|
||||
else:
|
||||
# --- WP-06: STANDARD RAG MODE ---
|
||||
inject_types = strategy.get("inject_types", [])
|
||||
prepend_instr = strategy.get("prepend_instruction", "")
|
||||
|
||||
# 2. Primary Retrieval
|
||||
|
|
@ -204,30 +293,33 @@ async def chat_endpoint(
|
|||
else:
|
||||
context_str = _build_enriched_context(hits)
|
||||
|
||||
# 5. Generation
|
||||
# 5. Generation Setup
|
||||
template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}")
|
||||
system_prompt = llm.prompts.get("system_prompt", "")
|
||||
|
||||
if prepend_instr:
|
||||
context_str = f"{prepend_instr}\n\n{context_str}"
|
||||
|
||||
final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message)
|
||||
sources_hits = hits
|
||||
|
||||
# --- COMMON GENERATION ---
|
||||
|
||||
system_prompt = llm.prompts.get("system_prompt", "")
|
||||
|
||||
logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...")
|
||||
|
||||
# System-Prompt separat übergeben (WP-06a Fix)
|
||||
# System-Prompt separat übergeben
|
||||
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)
|
||||
# 6. Logging (Fire & Forget)
|
||||
try:
|
||||
log_search(
|
||||
query_id=query_id,
|
||||
query_text=request.message,
|
||||
results=hits,
|
||||
mode="chat_rag",
|
||||
results=sources_hits,
|
||||
mode="interview" if intent == "INTERVIEW" else "chat_rag",
|
||||
metadata={
|
||||
"intent": intent,
|
||||
"intent_source": intent_source,
|
||||
|
|
@ -242,7 +334,7 @@ async def chat_endpoint(
|
|||
return ChatResponse(
|
||||
query_id=query_id,
|
||||
answer=answer_text,
|
||||
sources=hits,
|
||||
sources=sources_hits,
|
||||
latency_ms=duration_ms,
|
||||
intent=intent,
|
||||
intent_source=intent_source
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# config/decision_engine.yaml
|
||||
# Steuerung der Decision Engine (WP-06)
|
||||
# Steuerung der Decision Engine (WP-06 + WP-07)
|
||||
# Hybrid-Modus: Keywords (Fast) + LLM Router (Smart Fallback)
|
||||
version: 1.2
|
||||
version: 1.3
|
||||
|
||||
settings:
|
||||
llm_fallback_enabled: true
|
||||
|
||||
# Few-Shot Prompting für bessere SLM-Performance
|
||||
# Erweitert um INTERVIEW Beispiele
|
||||
llm_router_prompt: |
|
||||
Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie.
|
||||
Antworte NUR mit dem Namen der Strategie.
|
||||
|
||||
STRATEGIEN:
|
||||
- INTERVIEW: User will Wissen strukturieren, Notizen anlegen, Projekte starten ("Neu", "Festhalten").
|
||||
- DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich".
|
||||
- EMPATHY: Gefühle, Frust, Freude, Probleme, "Alles ist sinnlos", "Ich bin traurig".
|
||||
- CODING: Code, Syntax, Programmierung, Python.
|
||||
|
|
@ -20,6 +22,8 @@ settings:
|
|||
BEISPIELE:
|
||||
User: "Wie funktioniert Qdrant?" -> FACT
|
||||
User: "Soll ich Qdrant nutzen?" -> DECISION
|
||||
User: "Ich möchte ein neues Projekt anlegen" -> INTERVIEW
|
||||
User: "Lass uns eine Entscheidung festhalten" -> INTERVIEW
|
||||
User: "Schreibe ein Python Script" -> CODING
|
||||
User: "Alles ist grau und sinnlos" -> EMPATHY
|
||||
User: "Mir geht es heute gut" -> EMPATHY
|
||||
|
|
@ -87,3 +91,53 @@ strategies:
|
|||
inject_types: ["snippet", "reference", "source"]
|
||||
prompt_template: "technical_template"
|
||||
prepend_instruction: null
|
||||
|
||||
# 5. Interview / Datenerfassung (WP-07)
|
||||
INTERVIEW:
|
||||
description: "Der User möchte strukturiertes Wissen erfassen (Projekt, Notiz, Idee)."
|
||||
trigger_keywords:
|
||||
- "neue notiz"
|
||||
- "neues projekt"
|
||||
- "neue entscheidung"
|
||||
- "neues ziel"
|
||||
- "festhalten"
|
||||
- "entwurf erstellen"
|
||||
- "interview"
|
||||
- "dokumentieren"
|
||||
- "erfassen"
|
||||
- "idee speichern"
|
||||
inject_types: [] # Keine RAG-Suche, reiner Kontext-Dialog
|
||||
prompt_template: "interview_template"
|
||||
prepend_instruction: null
|
||||
|
||||
# LATE BINDING SCHEMAS:
|
||||
# Definition der Pflichtfelder pro Typ (korrespondiert mit types.yaml)
|
||||
# Wenn ein Typ hier fehlt, wird 'default' genutzt.
|
||||
schemas:
|
||||
default:
|
||||
fields: ["Titel", "Thema/Inhalt", "Tags"]
|
||||
hint: "Halte es einfach und übersichtlich."
|
||||
|
||||
project:
|
||||
fields: ["Titel", "Zielsetzung (Goal)", "Status (draft/active)", "Wichtige Stakeholder", "Nächste Schritte"]
|
||||
hint: "Achte darauf, Abhängigkeiten zu anderen Projekten mit [[rel:depends_on]] zu erfragen."
|
||||
|
||||
decision:
|
||||
fields: ["Titel", "Kontext (Warum entscheiden wir?)", "Getroffene Entscheidung", "Betrachtete Alternativen", "Status (proposed/final)"]
|
||||
hint: "Wichtig: Frage explizit nach den Gründen gegen die Alternativen."
|
||||
|
||||
goal:
|
||||
fields: ["Titel", "Zeitrahmen (Deadline)", "Messkriterien (KPIs)", "Verbundene Werte"]
|
||||
hint: "Ziele sollten SMART formuliert sein."
|
||||
|
||||
experience:
|
||||
fields: ["Titel", "Situation (Kontext)", "Erkenntnis (Learning)", "Emotionale Keywords (für Empathie-Suche)"]
|
||||
hint: "Fokussiere dich auf die persönliche Lektion."
|
||||
|
||||
value:
|
||||
fields: ["Titel (Name des Werts)", "Definition (Was bedeutet das für uns?)", "Anti-Beispiel (Was ist es nicht?)"]
|
||||
hint: "Werte dienen als Entscheidungsgrundlage."
|
||||
|
||||
principle:
|
||||
fields: ["Titel", "Handlungsanweisung", "Begründung"]
|
||||
hint: "Prinzipien sind härter als Werte."
|
||||
|
|
@ -94,3 +94,46 @@ technical_template: |
|
|||
- Kurze Erklärung des Ansatzes.
|
||||
- Markdown Code-Block (Copy-Paste fertig).
|
||||
- Wichtige Edge-Cases.
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 5. INTERVIEW: Der Analyst (Intent: INTERVIEW)
|
||||
# ---------------------------------------------------------
|
||||
interview_template: |
|
||||
CHAT_HISTORIE (BISHERIGER KONTEXT):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
|
||||
AKTUELLE USER-EINGABE:
|
||||
{query}
|
||||
|
||||
DEINE ROLLE:
|
||||
Du bist 'Mindnet Analyst'. Dein Ziel ist es, einen strukturierten Entwurf für eine Notiz vom Typ '{target_type}' zu erstellen.
|
||||
|
||||
PFLICHTFELDER (SCHEMA):
|
||||
{schema_fields}
|
||||
|
||||
HINWEIS ZUM TYP:
|
||||
{schema_hint}
|
||||
|
||||
ANWEISUNG:
|
||||
1. Analysiere den bisherigen Verlauf und die Eingabe. Welche der Pflichtfelder sind bereits bekannt?
|
||||
2. STATUS CHECK:
|
||||
- Fehlen Pflichtfelder? -> Stelle GENAU EINE gezielte Frage, um das nächste fehlende Feld zu klären. Warte auf die Antwort.
|
||||
- Sind alle Felder grob geklärt? -> Generiere den finalen Entwurf.
|
||||
|
||||
OUTPUT FORMAT (Nur wenn alle Infos da sind):
|
||||
Erstelle einen Markdown-Codeblock. Nutze Frontmatter.
|
||||
Verlinke erkannte Entitäten aggressiv mit [[Wikilinks]] oder [[rel:relation Ziel]].
|
||||
|
||||
Beispiel Output:
|
||||
"Danke, ich habe alle Infos. Hier ist dein Entwurf:"
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: {target_type}
|
||||
status: draft
|
||||
tags: [...]
|
||||
---
|
||||
# Titel
|
||||
...
|
||||
48
tests/test_interview_intent.py
Normal file
48
tests/test_interview_intent.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
# URL anpassen, falls du auf Port 8001 (Prod) oder 8002 (Dev) bist
|
||||
API_URL = "http://localhost:8002/chat/"
|
||||
|
||||
def test_intent(message, expected_intent, expected_type_hint):
|
||||
payload = {
|
||||
"message": message,
|
||||
"top_k": 0, # Für Interview irrelevant
|
||||
"explain": False
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(API_URL, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
intent = data.get("intent")
|
||||
answer = data.get("answer")
|
||||
|
||||
print(f"--- TEST: '{message}' ---")
|
||||
print(f"Erkannter Intent: {intent}")
|
||||
print(f"Antwort-Snippet: {answer[:100]}...")
|
||||
|
||||
if intent == expected_intent:
|
||||
print("✅ Intent SUCCESS")
|
||||
else:
|
||||
print(f"❌ Intent FAILED (Erwartet: {expected_intent})")
|
||||
|
||||
if expected_type_hint.lower() in answer.lower():
|
||||
print(f"✅ Context Check SUCCESS (Typ '{expected_type_hint}' erkannt)")
|
||||
else:
|
||||
print(f"⚠️ Context Check WARNING (Typ '{expected_type_hint}' nicht explizit im Start-Prompt gefunden)")
|
||||
print("\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test 1: Projekt-Start
|
||||
test_intent("Ich möchte ein neues Projekt anlegen", "INTERVIEW", "project")
|
||||
|
||||
# Test 2: Entscheidungs-Doku
|
||||
test_intent("Lass uns eine Entscheidung festhalten", "INTERVIEW", "decision")
|
||||
|
||||
# Test 3: Standard Chat (Gegenprobe)
|
||||
test_intent("Was ist ein Vektor?", "FACT", "")
|
||||
Loading…
Reference in New Issue
Block a user