Erste Version WP06

This commit is contained in:
Lars 2025-12-08 18:48:24 +01:00
parent 4d6fce2d93
commit 07b7f419de
3 changed files with 179 additions and 29 deletions

View File

@ -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)")

View File

@ -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:

View File

@ -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)