Erste Version WP06
This commit is contained in:
parent
4d6fce2d93
commit
07b7f419de
|
|
@ -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:
|
Zweck:
|
||||||
Laufzeit-Modelle für FastAPI (Requests/Responses).
|
Laufzeit-Modelle für FastAPI (Requests/Responses).
|
||||||
WP-05 Update: Chat-Modelle.
|
WP-06 Update: Intent in ChatResponse.
|
||||||
|
|
||||||
Version:
|
Version:
|
||||||
0.4.0 (Update für WP-05 Chat)
|
0.6.0 (WP-06: Decision Engine)
|
||||||
Stand:
|
Stand:
|
||||||
2025-12-08
|
2025-12-08
|
||||||
"""
|
"""
|
||||||
|
|
@ -144,9 +144,10 @@ class GraphResponse(BaseModel):
|
||||||
|
|
||||||
class ChatResponse(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)")
|
query_id: str = Field(..., description="Traceability ID (dieselbe wie für Search)")
|
||||||
answer: str = Field(..., description="Generierte Antwort vom LLM")
|
answer: str = Field(..., description="Generierte Antwort vom LLM")
|
||||||
sources: List[QueryHit] = Field(..., description="Die für die Antwort genutzten Quellen")
|
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:
|
Zweck:
|
||||||
Verbindet Retrieval mit LLM-Generation.
|
Verbindet Retrieval mit LLM-Generation.
|
||||||
Enriched Context: Fügt Typen und Metadaten in den Prompt ein,
|
WP-06: Implementiert Intent Detection und Strategic Retrieval (Values/Principles).
|
||||||
damit das LLM komplexe Zusammenhänge (z.B. Decisions) versteht.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from typing import List
|
from typing import List, Dict
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -37,7 +36,7 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
|
||||||
for i, hit in enumerate(hits, 1):
|
for i, hit in enumerate(hits, 1):
|
||||||
source = hit.source or {}
|
source = hit.source or {}
|
||||||
|
|
||||||
# 1. Content extrahieren (Robust: prüft alle üblichen Felder)
|
# 1. Content extrahieren
|
||||||
content = (
|
content = (
|
||||||
source.get("text") or
|
source.get("text") or
|
||||||
source.get("content") or
|
source.get("content") or
|
||||||
|
|
@ -48,10 +47,10 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
|
||||||
|
|
||||||
# 2. Metadaten für "Context Intelligence"
|
# 2. Metadaten für "Context Intelligence"
|
||||||
title = hit.note_id or "Unbekannte Notiz"
|
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()
|
note_type = source.get("type", "unknown").upper()
|
||||||
|
|
||||||
# 3. Formatierung als strukturiertes Dokument für das LLM
|
# 3. Formatierung
|
||||||
entry = (
|
entry = (
|
||||||
f"### QUELLE {i}: {title}\n"
|
f"### QUELLE {i}: {title}\n"
|
||||||
f"TYP: [{note_type}] (Score: {hit.total_score:.2f})\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)
|
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)
|
@router.post("/", response_model=ChatResponse)
|
||||||
async def chat_endpoint(
|
async def chat_endpoint(
|
||||||
request: ChatRequest,
|
request: ChatRequest,
|
||||||
|
|
@ -73,40 +116,109 @@ async def chat_endpoint(
|
||||||
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
|
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
|
||||||
|
|
||||||
try:
|
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_req = QueryRequest(
|
||||||
query=request.message,
|
query=request.message,
|
||||||
mode="hybrid", # WICHTIG: Hybrid Mode für Graph-Nachbarn
|
mode="hybrid",
|
||||||
top_k=request.top_k,
|
top_k=request.top_k,
|
||||||
explain=request.explain
|
explain=request.explain
|
||||||
)
|
)
|
||||||
|
|
||||||
retrieve_result = await retriever.search(query_req)
|
retrieve_result = await retriever.search(query_req)
|
||||||
hits = retrieve_result.results
|
hits = retrieve_result.results
|
||||||
|
|
||||||
# 2. Context Building (Enriched)
|
# 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.
|
||||||
|
|
||||||
|
# 4. Context Building
|
||||||
if not hits:
|
if not hits:
|
||||||
logger.info(f"[{query_id}] No hits found.")
|
|
||||||
context_str = "Keine relevanten Notizen gefunden."
|
context_str = "Keine relevanten Notizen gefunden."
|
||||||
else:
|
else:
|
||||||
context_str = _build_enriched_context(hits)
|
context_str = _build_enriched_context(hits)
|
||||||
|
|
||||||
# 3. Generation
|
# 5. Generation (Prompt Selection)
|
||||||
logger.info(f"[{query_id}] Context built with {len(hits)} chunks. Sending to LLM...")
|
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(
|
answer_text = await llm.generate_rag_response(
|
||||||
query=request.message,
|
query=request.message,
|
||||||
context_str=context_str
|
context_str=context_str
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Response
|
# 6. Response
|
||||||
duration_ms = int((time.time() - start_time) * 1000)
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
logger.info(f"[{query_id}] Completed in {duration_ms}ms")
|
|
||||||
|
|
||||||
return ChatResponse(
|
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,
|
answer=answer_text,
|
||||||
sources=hits,
|
sources=hits,
|
||||||
latency_ms=duration_ms
|
latency_ms=duration_ms,
|
||||||
|
intent=intent
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
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)
|
# Optimiert für Phi-3 Mini (Small Language Model)
|
||||||
|
|
||||||
system_prompt: |
|
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:
|
DEINE REGELN:
|
||||||
1. Deine Antwort muss zu 100% auf dem KONTEXT basieren. Erfinde nichts.
|
1. Deine Antwort muss auf dem bereitgestellten KONTEXT basieren.
|
||||||
2. Wenn eine Quelle den Typ [DECISION] hat, ist sie die wichtigste Quelle für das "Warum".
|
2. Unterscheide klar zwischen FAKTEN (externe Welt) und PRINZIPIEN (meine innere Welt).
|
||||||
3. Nenne konkrete technische Details aus dem Text (z.B. genannte Features, Gründe), statt nur allgemein zu antworten.
|
3. Wenn Quellen vom Typ [VALUE] oder [PRINCIPLE] vorliegen, haben diese Vorrang bei der Entscheidungsfindung.
|
||||||
4. Antworte auf Deutsch.
|
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: |
|
rag_template: |
|
||||||
QUELLEN (WISSEN):
|
QUELLEN (WISSEN):
|
||||||
=========================================
|
=========================================
|
||||||
|
|
@ -22,3 +37,25 @@ rag_template: |
|
||||||
ANWEISUNG:
|
ANWEISUNG:
|
||||||
Beantworte die Frage basierend auf den Quellen.
|
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