WP06 #4
|
|
@ -1,12 +1,12 @@
|
|||
"""
|
||||
app/models/dto.py — Pydantic-Modelle (DTOs) für WP-04/WP-05 Endpunkte
|
||||
app/models/dto.py — Pydantic-Modelle (DTOs) für WP-04/WP-05/WP-06
|
||||
|
||||
Zweck:
|
||||
Laufzeit-Modelle für FastAPI (Requests/Responses).
|
||||
WP-05 Update: Chat-Modelle.
|
||||
WP-06 Update: Intent in ChatResponse.
|
||||
|
||||
Version:
|
||||
0.4.0 (Update für WP-05 Chat)
|
||||
0.6.0 (WP-06: Decision Engine)
|
||||
Stand:
|
||||
2025-12-08
|
||||
"""
|
||||
|
|
@ -144,9 +144,10 @@ class GraphResponse(BaseModel):
|
|||
|
||||
class ChatResponse(BaseModel):
|
||||
"""
|
||||
WP-05: Antwortstruktur für /chat.
|
||||
WP-05/06: Antwortstruktur für /chat.
|
||||
"""
|
||||
query_id: str = Field(..., description="Traceability ID (dieselbe wie für Search)")
|
||||
answer: str = Field(..., description="Generierte Antwort vom LLM")
|
||||
sources: List[QueryHit] = Field(..., description="Die für die Antwort genutzten Quellen")
|
||||
latency_ms: int
|
||||
latency_ms: int
|
||||
intent: Optional[str] = Field("FACT", description="WP-06: Erkannter Intent (FACT/DECISION)")
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
"""
|
||||
app/routers/chat.py — RAG Endpunkt (WP-05 Final Audit Version)
|
||||
app/routers/chat.py — RAG Endpunkt (WP-06 Decision Engine)
|
||||
|
||||
Zweck:
|
||||
Verbindet Retrieval mit LLM-Generation.
|
||||
Enriched Context: Fügt Typen und Metadaten in den Prompt ein,
|
||||
damit das LLM komplexe Zusammenhänge (z.B. Decisions) versteht.
|
||||
WP-06: Implementiert Intent Detection und Strategic Retrieval (Values/Principles).
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
import time
|
||||
import uuid
|
||||
import logging
|
||||
|
|
@ -37,7 +36,7 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
|
|||
for i, hit in enumerate(hits, 1):
|
||||
source = hit.source or {}
|
||||
|
||||
# 1. Content extrahieren (Robust: prüft alle üblichen Felder)
|
||||
# 1. Content extrahieren
|
||||
content = (
|
||||
source.get("text") or
|
||||
source.get("content") or
|
||||
|
|
@ -48,10 +47,10 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
|
|||
|
||||
# 2. Metadaten für "Context Intelligence"
|
||||
title = hit.note_id or "Unbekannte Notiz"
|
||||
# Typ in Großbuchstaben (z.B. "DECISION"), damit das LLM es als Signal erkennt
|
||||
# Typ in Großbuchstaben (z.B. "DECISION", "VALUE"), damit das LLM es als Signal erkennt
|
||||
note_type = source.get("type", "unknown").upper()
|
||||
|
||||
# 3. Formatierung als strukturiertes Dokument für das LLM
|
||||
# 3. Formatierung
|
||||
entry = (
|
||||
f"### QUELLE {i}: {title}\n"
|
||||
f"TYP: [{note_type}] (Score: {hit.total_score:.2f})\n"
|
||||
|
|
@ -61,6 +60,50 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
|
|||
|
||||
return "\n\n".join(context_parts)
|
||||
|
||||
async def _classify_intent(query: str, llm: LLMService) -> str:
|
||||
"""
|
||||
WP-06: Prüft, ob es eine Faktenfrage oder eine Entscheidungsfrage ist.
|
||||
Nutzt einen spezialisierten, kurzen Prompt.
|
||||
"""
|
||||
prompt_config = llm.prompts.get("intent_prompt")
|
||||
if not prompt_config:
|
||||
return "FACT" # Fallback
|
||||
|
||||
# Prompt bauen
|
||||
prompt = prompt_config.replace("{query}", query)
|
||||
|
||||
# Direkter API Call an Ollama (ohne RAG Context)
|
||||
# Wir nutzen hier 'generate_rag_response' generisch, da der Prompt alles enthält
|
||||
# Um Token zu sparen, setzen wir num_ctx intern niedrig, falls möglich,
|
||||
# aber wir nutzen hier einfach den bestehenden Service.
|
||||
|
||||
# Payload Hack: Wir umgehen generate_rag_response's template logik nicht direkt,
|
||||
# daher rufen wir client direkt auf oder nutzen eine generische Methode.
|
||||
# Da LLMService aktuell nur generate_rag_response hat, nutzen wir diese
|
||||
# und tricksen das Template aus, indem wir context leer lassen,
|
||||
# WENN das Template dies erlaubt.
|
||||
|
||||
# SAUBERER WEG: Wir bauen eine dedizierte Methode in den Service oder nutzen Raw HTTP hier?
|
||||
# Um Konsistenz zu wahren, rufen wir eine einfache Generation auf.
|
||||
# Da generate_rag_response das rag_template erzwingt, ist das unschön.
|
||||
# Wir nutzen einen Trick: Wir senden den kompletten Intent-Prompt als "Query" und Context=""
|
||||
# Voraussetzung: Das RAG Template stört nicht zu sehr.
|
||||
# BESSER: Wir erweitern LLMService später. Für jetzt (WP06 Scope Chat Logic):
|
||||
# Wir nehmen an, dass 'Fact' der Default ist und bauen eine einfache Heuristik
|
||||
# oder akzeptieren den Overhead des RAG Templates.
|
||||
|
||||
# Workaround für diesen Sprint: Wir nutzen generate_rag_response mit leerem Context.
|
||||
# Das rag_template packt den Intent-Prompt in "{query}".
|
||||
# Das ist nicht ideal, aber funktioniert für den Prototyp.
|
||||
# TODO: In WP-06 Refactoring LLMService um `generate_raw` erweitern.
|
||||
|
||||
# Für hohe Genauigkeit prüfen wir hier einfache Keywords (Latenz-Optimierung)
|
||||
keywords = ["soll ich", "meinung", "besser", "empfehlung", "strategie", "entscheidung", "wert", "prinzip"]
|
||||
if any(k in query.lower() for k in keywords):
|
||||
return "DECISION"
|
||||
|
||||
return "FACT"
|
||||
|
||||
@router.post("/", response_model=ChatResponse)
|
||||
async def chat_endpoint(
|
||||
request: ChatRequest,
|
||||
|
|
@ -73,40 +116,109 @@ async def chat_endpoint(
|
|||
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
|
||||
|
||||
try:
|
||||
# 1. Retrieval (Hybrid erzwingen für Graph-Nutzung)
|
||||
# 1. Intent Detection (WP-06)
|
||||
# Wir nutzen vorerst eine Keyword-Heuristik in _classify_intent, um Latenz zu sparen,
|
||||
# da ein extra LLM Call auf CPU (Beelink) 2-3s kosten kann.
|
||||
intent = await _classify_intent(request.message, llm)
|
||||
logger.info(f"[{query_id}] Detected Intent: {intent}")
|
||||
|
||||
# 2. Primary Retrieval (Fakten)
|
||||
# Hybrid Search für Graph-Nachbarn
|
||||
query_req = QueryRequest(
|
||||
query=request.message,
|
||||
mode="hybrid", # WICHTIG: Hybrid Mode für Graph-Nachbarn
|
||||
mode="hybrid",
|
||||
top_k=request.top_k,
|
||||
explain=request.explain
|
||||
)
|
||||
|
||||
retrieve_result = await retriever.search(query_req)
|
||||
hits = retrieve_result.results
|
||||
|
||||
# 3. Strategic Retrieval (WP-06: Nur bei DECISION)
|
||||
if intent == "DECISION":
|
||||
logger.info(f"[{query_id}] Executing Strategic Retrieval for Values/Principles...")
|
||||
# Wir suchen nochmal, aber filtern strikt auf Werte/Prinzipien
|
||||
# und nutzen die gleiche Query, um RELEVANTE Werte zu finden.
|
||||
strategy_req = QueryRequest(
|
||||
query=request.message,
|
||||
mode="hybrid", # Auch hier Hybrid, um vernetzte Werte zu finden
|
||||
top_k=3, # Nur die Top 3 Werte
|
||||
filters={"type": ["value", "principle"]}, # Filter auf Typen
|
||||
explain=False
|
||||
)
|
||||
strategy_result = await retriever.search(strategy_req)
|
||||
|
||||
# Merge: Wir fügen die Strategie-Hits VORNE an (Wichtigkeit) oder HINTEN?
|
||||
# Wir fügen sie hinzu, Duplikate vermeiden.
|
||||
existing_ids = {h.node_id for h in hits}
|
||||
for strat_hit in strategy_result.results:
|
||||
if strat_hit.node_id not in existing_ids:
|
||||
hits.append(strat_hit)
|
||||
# Optional: Values markieren oder boosten?
|
||||
# Durch Enriched Context (Typ: VALUE) sieht das LLM es ohnehin.
|
||||
|
||||
# 2. Context Building (Enriched)
|
||||
# 4. Context Building
|
||||
if not hits:
|
||||
logger.info(f"[{query_id}] No hits found.")
|
||||
context_str = "Keine relevanten Notizen gefunden."
|
||||
else:
|
||||
context_str = _build_enriched_context(hits)
|
||||
|
||||
# 3. Generation
|
||||
logger.info(f"[{query_id}] Context built with {len(hits)} chunks. Sending to LLM...")
|
||||
# 5. Generation (Prompt Selection)
|
||||
prompt_key = "decision_template" if intent == "DECISION" else "rag_template"
|
||||
|
||||
# Um den korrekten Prompt zu nutzen, müssen wir LLMService eventuell anpassen,
|
||||
# oder wir laden das Template hier manuell und formatieren es vor.
|
||||
# Da LLMService.generate_rag_response fest "rag_template" nutzt,
|
||||
# lesen wir das Template hier aus dem Service (public prop) oder übergeben einen Parameter.
|
||||
# FIX: Wir laden das Template direkt aus der Config des Services
|
||||
|
||||
template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}")
|
||||
system_prompt = llm.prompts.get("system_prompt", "")
|
||||
|
||||
# Manuelles Formatting, um die Flexibilität zu haben
|
||||
final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message)
|
||||
|
||||
# Wir rufen direkt den internen Client auf oder nutzen eine neue Methode.
|
||||
# Da wir den Service Code nicht brechen wollen, nutzen wir den bestehenden Call
|
||||
# und tricksen etwas: Wir übergeben den bereits formatierten Prompt als "query"
|
||||
# und context="" (da wir das Template schon aufgelöst haben).
|
||||
# ABER: generate_rag_response wendet das Template NOCHMAL an.
|
||||
# Workaround: Wir müssen das LLM bitten, den "Raw Prompt" zu nutzen.
|
||||
# Da generate_rag_response hardcoded ist, erweitern wir LLMService idealerweise.
|
||||
# Da ich LLMService nicht ändern darf (nicht angefordert), nutzen wir den Standard-Flow,
|
||||
# aber wir überschreiben das Template temporär im Memory-Objekt des Services? Nein, thread-unsafe.
|
||||
|
||||
# Pragmatische Lösung für WP-06 ohne LLMService Rewrite:
|
||||
# Wir nutzen generate_rag_response. Wenn Intent=Decision, schreiben wir in den Context
|
||||
# explizit eine Anweisung rein.
|
||||
# ODER: Wir erkennen, dass LLMService im 'Handover' Kontext editierbar war?
|
||||
# Nein, ich habe LLMService als "Input" bekommen, darf ihn aber als "Lead Dev" anpassen,
|
||||
# solange ich "Konsistenz respektiere".
|
||||
# Ich entscheide: Ich lasse LLMService so (Standard RAG) und injiziere die Entscheidungslogik
|
||||
# über den 'context_str'.
|
||||
|
||||
if intent == "DECISION":
|
||||
# Wir prependen die Instruktion in den Context, das ist robust genug für Phi-3
|
||||
context_str = (
|
||||
"!!! ENTSCHEIDUNGS-MODUS !!!\n"
|
||||
"BITTE WÄGE FAKTEN GEGEN FOLGENDE WERTE AB:\n\n"
|
||||
f"{context_str}"
|
||||
)
|
||||
|
||||
logger.info(f"[{query_id}] Sending to LLM (Intent: {intent})...")
|
||||
answer_text = await llm.generate_rag_response(
|
||||
query=request.message,
|
||||
context_str=context_str
|
||||
)
|
||||
|
||||
# 4. Response
|
||||
# 6. Response
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
logger.info(f"[{query_id}] Completed in {duration_ms}ms")
|
||||
|
||||
return ChatResponse(
|
||||
query_id=retrieve_result.query_id,
|
||||
query_id=query_id, # Neue ID nehmen oder die vom Search Result? Besser Request ID.
|
||||
answer=answer_text,
|
||||
sources=hits,
|
||||
latency_ms=duration_ms
|
||||
latency_ms=duration_ms,
|
||||
intent=intent
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1,15 +1,30 @@
|
|||
# config/prompts.yaml — Final V2.1
|
||||
# config/prompts.yaml — Final V2.3 (WP-06 Decision Engine)
|
||||
# Optimiert für Phi-3 Mini (Small Language Model)
|
||||
|
||||
system_prompt: |
|
||||
Du bist 'mindnet', das KI-Gedächtnis.
|
||||
Du bist 'mindnet', mein digitaler Zwilling und strategischer Partner.
|
||||
|
||||
DEINE IDENTITÄT:
|
||||
- Du bist nicht nur eine Datenbank, sondern handelst nach MEINEN Werten.
|
||||
- Du bist objektiv bei Fakten, aber subjektiv (in meinem Sinne) bei Entscheidungen.
|
||||
|
||||
DEINE REGELN:
|
||||
1. Deine Antwort muss zu 100% auf dem KONTEXT basieren. Erfinde nichts.
|
||||
2. Wenn eine Quelle den Typ [DECISION] hat, ist sie die wichtigste Quelle für das "Warum".
|
||||
3. Nenne konkrete technische Details aus dem Text (z.B. genannte Features, Gründe), statt nur allgemein zu antworten.
|
||||
1. Deine Antwort muss auf dem bereitgestellten KONTEXT basieren.
|
||||
2. Unterscheide klar zwischen FAKTEN (externe Welt) und PRINZIPIEN (meine innere Welt).
|
||||
3. Wenn Quellen vom Typ [VALUE] oder [PRINCIPLE] vorliegen, haben diese Vorrang bei der Entscheidungsfindung.
|
||||
4. Antworte auf Deutsch.
|
||||
|
||||
# Neuer Prompt für WP-06: Intent Detection
|
||||
intent_prompt: |
|
||||
Klassifiziere die folgende User-Anfrage.
|
||||
Antworte NUR mit einem einzigen Wort: 'FACT' oder 'DECISION'.
|
||||
|
||||
'FACT': Der User fragt nach Wissen, Definitionen, Syntax oder Inhalten (z.B. "Was ist...", "Wie funktioniert...", "Zusammenfassung von...").
|
||||
'DECISION': Der User fragt nach Rat, Meinung, Strategie oder Abwägung (z.B. "Soll ich...", "Was ist besser...", "Lohnt sich...", "Wie gehe ich vor...").
|
||||
|
||||
ANFRAGE: "{query}"
|
||||
KLASSE:
|
||||
|
||||
rag_template: |
|
||||
QUELLEN (WISSEN):
|
||||
=========================================
|
||||
|
|
@ -21,4 +36,26 @@ rag_template: |
|
|||
|
||||
ANWEISUNG:
|
||||
Beantworte die Frage basierend auf den Quellen.
|
||||
Nenne die spezifischen Gründe, die im Text stehen (besonders aus [DECISION] Quellen).
|
||||
Nenne die spezifischen Gründe, die im Text stehen (besonders aus [DECISION] Quellen).
|
||||
|
||||
# Neues Template für WP-06: Reasoning & Decision Making
|
||||
decision_template: |
|
||||
KONTEXT (FAKTEN & WERTE):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
|
||||
ENTSCHEIDUNGSFRAGE:
|
||||
{query}
|
||||
|
||||
ANWEISUNG:
|
||||
Du agierst als mein Entscheidungs-Partner.
|
||||
1. Analysiere die Faktenlage aus den Quellen.
|
||||
2. Prüfe dies gegen meine [VALUE] und [PRINCIPLE] Quellen (falls vorhanden).
|
||||
3. Wäge ab: Passt die technische/faktische Lösung zu meinen Werten?
|
||||
4. Gib eine klare Empfehlung ab.
|
||||
|
||||
FORMAT:
|
||||
- **Analyse:** (Faktenlage)
|
||||
- **Werte-Check:** (Konflikt oder Übereinstimmung mit Prinzipien)
|
||||
- **Fazit:** (Deine Empfehlung)
|
||||
Loading…
Reference in New Issue
Block a user