Hybrider Chat (mit und ohne LLM Einordung des Intents)
This commit is contained in:
parent
97985371ca
commit
03594424a1
|
|
@ -1,10 +1,5 @@
|
||||||
"""
|
"""
|
||||||
app/routers/chat.py — RAG Endpunkt (WP-06 Decision Engine - Full Config Refactor)
|
app/routers/chat.py — RAG Endpunkt (WP-06 Hybrid Router)
|
||||||
|
|
||||||
Zweck:
|
|
||||||
Verbindet Retrieval mit LLM-Generation.
|
|
||||||
WP-06: Implementiert Intent Detection und Strategic Retrieval.
|
|
||||||
Update: Konfiguration via decision_engine.yaml (Late Binding) mit 'Best Match' Logik.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
|
@ -25,23 +20,15 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# --- Helper: Config Loader ---
|
# --- Helper: Config Loader ---
|
||||||
|
|
||||||
# Cache für die Config (damit wir nicht bei jedem Request lesen)
|
|
||||||
_DECISION_CONFIG_CACHE = None
|
_DECISION_CONFIG_CACHE = None
|
||||||
|
|
||||||
def _load_decision_config() -> Dict[str, Any]:
|
def _load_decision_config() -> Dict[str, Any]:
|
||||||
"""Lädt die Decision-Engine Konfiguration (Late Binding)."""
|
"""Lädt die Decision-Engine Konfiguration (Late Binding)."""
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
path = Path(settings.DECISION_CONFIG_PATH)
|
path = Path(settings.DECISION_CONFIG_PATH)
|
||||||
|
|
||||||
# Default Fallback, falls YAML kaputt/weg
|
|
||||||
default_config = {
|
default_config = {
|
||||||
"strategies": {
|
"strategies": {
|
||||||
"FACT": {"trigger_keywords": []},
|
"FACT": {"trigger_keywords": []}
|
||||||
"DECISION": {
|
|
||||||
"trigger_keywords": ["soll ich", "meinung"],
|
|
||||||
"inject_types": ["value", "principle"],
|
|
||||||
"prompt_template": "decision_template"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,17 +44,14 @@ def _load_decision_config() -> Dict[str, Any]:
|
||||||
return default_config
|
return default_config
|
||||||
|
|
||||||
def get_full_config() -> Dict[str, Any]:
|
def get_full_config() -> Dict[str, Any]:
|
||||||
"""Gibt die ganze Config zurück (für Intent Detection)."""
|
|
||||||
global _DECISION_CONFIG_CACHE
|
global _DECISION_CONFIG_CACHE
|
||||||
if _DECISION_CONFIG_CACHE is None:
|
if _DECISION_CONFIG_CACHE is None:
|
||||||
_DECISION_CONFIG_CACHE = _load_decision_config()
|
_DECISION_CONFIG_CACHE = _load_decision_config()
|
||||||
return _DECISION_CONFIG_CACHE
|
return _DECISION_CONFIG_CACHE
|
||||||
|
|
||||||
def get_decision_strategy(intent: str) -> Dict[str, Any]:
|
def get_decision_strategy(intent: str) -> Dict[str, Any]:
|
||||||
"""Gibt die Strategie für einen spezifischen Intent zurück."""
|
|
||||||
config = get_full_config()
|
config = get_full_config()
|
||||||
strategies = config.get("strategies", {})
|
strategies = config.get("strategies", {})
|
||||||
# Fallback auf FACT, wenn Intent unbekannt
|
|
||||||
return strategies.get(intent, strategies.get("FACT", {}))
|
return strategies.get(intent, strategies.get("FACT", {}))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -83,30 +67,17 @@ def get_retriever():
|
||||||
# --- Logic ---
|
# --- Logic ---
|
||||||
|
|
||||||
def _build_enriched_context(hits: List[QueryHit]) -> str:
|
def _build_enriched_context(hits: List[QueryHit]) -> str:
|
||||||
"""
|
|
||||||
Baut einen 'Rich Context' String.
|
|
||||||
Statt nur Text, injizieren wir Metadaten (Typ, Tags), damit das LLM
|
|
||||||
die semantische Rolle des Schnipsels versteht.
|
|
||||||
"""
|
|
||||||
context_parts = []
|
context_parts = []
|
||||||
|
|
||||||
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
|
|
||||||
content = (
|
content = (
|
||||||
source.get("text") or
|
source.get("text") or source.get("content") or
|
||||||
source.get("content") or
|
source.get("page_content") or source.get("chunk_text") or
|
||||||
source.get("page_content") or
|
"[Kein Text]"
|
||||||
source.get("chunk_text") or
|
|
||||||
"[Kein Textinhalt verfügbar]"
|
|
||||||
)
|
)
|
||||||
|
title = hit.note_id or "Unbekannt"
|
||||||
# 2. Metadaten für "Context Intelligence"
|
|
||||||
title = hit.note_id or "Unbekannte Notiz"
|
|
||||||
note_type = source.get("type", "unknown").upper()
|
note_type = source.get("type", "unknown").upper()
|
||||||
|
|
||||||
# 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"
|
||||||
|
|
@ -118,38 +89,58 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
|
||||||
|
|
||||||
async def _classify_intent(query: str, llm: LLMService) -> str:
|
async def _classify_intent(query: str, llm: LLMService) -> str:
|
||||||
"""
|
"""
|
||||||
WP-06: Intent Detection (Best Match / Longest Keyword Wins).
|
Hybrid Router:
|
||||||
|
1. Keyword Check (Best/Longest Match) -> FAST
|
||||||
Prüft Keywords aus der YAML gegen die Query.
|
2. LLM Fallback (wenn in config aktiv) -> SMART
|
||||||
Wenn mehrere Strategien passen, gewinnt die mit dem längsten Keyword (Spezifität).
|
|
||||||
"""
|
"""
|
||||||
config = get_full_config()
|
config = get_full_config()
|
||||||
strategies = config.get("strategies", {})
|
strategies = config.get("strategies", {})
|
||||||
|
settings = config.get("settings", {})
|
||||||
|
|
||||||
query_lower = query.lower()
|
query_lower = query.lower()
|
||||||
|
best_intent = None
|
||||||
best_intent = "FACT"
|
|
||||||
max_match_length = 0
|
max_match_length = 0
|
||||||
|
|
||||||
# Iteriere über alle Strategien
|
# 1. FAST PATH: Keywords
|
||||||
for intent_name, strategy in strategies.items():
|
for intent_name, strategy in strategies.items():
|
||||||
if intent_name == "FACT":
|
if intent_name == "FACT": continue
|
||||||
continue
|
|
||||||
|
|
||||||
keywords = strategy.get("trigger_keywords", [])
|
keywords = strategy.get("trigger_keywords", [])
|
||||||
|
|
||||||
# Prüfe jedes Keyword
|
|
||||||
for k in keywords:
|
for k in keywords:
|
||||||
# Wenn Keyword im Text ist...
|
|
||||||
if k.lower() in query_lower:
|
if k.lower() in query_lower:
|
||||||
# ... prüfen wir, ob es spezifischer (länger) ist als der bisherige Favorit
|
if len(k) > max_match_length:
|
||||||
current_len = len(k)
|
max_match_length = len(k)
|
||||||
if current_len > max_match_length:
|
|
||||||
max_match_length = current_len
|
|
||||||
best_intent = intent_name
|
best_intent = intent_name
|
||||||
# Wir brechen hier NICHT ab, sondern suchen weiter nach noch längeren Matches
|
|
||||||
|
if best_intent:
|
||||||
|
logger.info(f"Intent detected via KEYWORD: {best_intent}")
|
||||||
|
return best_intent
|
||||||
|
|
||||||
|
# 2. SLOW PATH: LLM Router
|
||||||
|
if settings.get("llm_fallback_enabled", False):
|
||||||
|
router_prompt_template = settings.get("llm_router_prompt", "")
|
||||||
|
if router_prompt_template:
|
||||||
|
prompt = router_prompt_template.replace("{query}", query)
|
||||||
|
logger.info("Keywords failed. Asking LLM for Intent...")
|
||||||
|
|
||||||
return best_intent
|
# Kurzer Raw Call
|
||||||
|
llm_decision = await llm.generate_raw_response(prompt)
|
||||||
|
|
||||||
|
# Cleaning
|
||||||
|
llm_decision = llm_decision.strip().upper()
|
||||||
|
if ":" in llm_decision:
|
||||||
|
llm_decision = llm_decision.split(":")[-1].strip()
|
||||||
|
|
||||||
|
# Validierung: Nur bekannte Intents zulassen
|
||||||
|
# Entferne Satzzeichen
|
||||||
|
llm_decision = ''.join(filter(str.isalnum, llm_decision))
|
||||||
|
|
||||||
|
if llm_decision in strategies:
|
||||||
|
logger.info(f"Intent detected via LLM: {llm_decision}")
|
||||||
|
return llm_decision
|
||||||
|
else:
|
||||||
|
logger.warning(f"LLM predicted unknown intent '{llm_decision}', falling back to FACT.")
|
||||||
|
|
||||||
|
return "FACT"
|
||||||
|
|
||||||
@router.post("/", response_model=ChatResponse)
|
@router.post("/", response_model=ChatResponse)
|
||||||
async def chat_endpoint(
|
async def chat_endpoint(
|
||||||
|
|
@ -159,21 +150,20 @@ async def chat_endpoint(
|
||||||
):
|
):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
query_id = str(uuid.uuid4())
|
query_id = str(uuid.uuid4())
|
||||||
|
|
||||||
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
|
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Intent Detection (Config-Driven & Best Match)
|
# 1. Intent Detection
|
||||||
intent = await _classify_intent(request.message, llm)
|
intent = await _classify_intent(request.message, llm)
|
||||||
logger.info(f"[{query_id}] Detected Intent: {intent}")
|
logger.info(f"[{query_id}] Final Intent: {intent}")
|
||||||
|
|
||||||
# Lade Strategie aus Config (Late Binding)
|
# Strategy Load
|
||||||
strategy = get_decision_strategy(intent)
|
strategy = get_decision_strategy(intent)
|
||||||
inject_types = strategy.get("inject_types", [])
|
inject_types = strategy.get("inject_types", [])
|
||||||
prompt_key = strategy.get("prompt_template", "rag_template")
|
prompt_key = strategy.get("prompt_template", "rag_template")
|
||||||
prepend_instr = strategy.get("prepend_instruction", "")
|
prepend_instr = strategy.get("prepend_instruction", "")
|
||||||
|
|
||||||
# 2. Primary Retrieval (Fakten)
|
# 2. Primary Retrieval
|
||||||
query_req = QueryRequest(
|
query_req = QueryRequest(
|
||||||
query=request.message,
|
query=request.message,
|
||||||
mode="hybrid",
|
mode="hybrid",
|
||||||
|
|
@ -183,19 +173,19 @@ async def chat_endpoint(
|
||||||
retrieve_result = await retriever.search(query_req)
|
retrieve_result = await retriever.search(query_req)
|
||||||
hits = retrieve_result.results
|
hits = retrieve_result.results
|
||||||
|
|
||||||
# 3. Strategic Retrieval (Konfigurierbar)
|
# 3. Strategic Retrieval
|
||||||
if inject_types:
|
if inject_types:
|
||||||
logger.info(f"[{query_id}] Executing Strategic Retrieval for types: {inject_types}...")
|
logger.info(f"[{query_id}] Executing Strategic Retrieval for types: {inject_types}...")
|
||||||
strategy_req = QueryRequest(
|
strategy_req = QueryRequest(
|
||||||
query=request.message,
|
query=request.message,
|
||||||
mode="hybrid",
|
mode="hybrid",
|
||||||
top_k=3,
|
top_k=3,
|
||||||
filters={"type": inject_types}, # Dynamische Liste aus YAML
|
filters={"type": inject_types},
|
||||||
explain=False
|
explain=False
|
||||||
)
|
)
|
||||||
strategy_result = await retriever.search(strategy_req)
|
strategy_result = await retriever.search(strategy_req)
|
||||||
|
|
||||||
# Merge Results (Deduplication via node_id)
|
# Merge
|
||||||
existing_ids = {h.node_id for h in hits}
|
existing_ids = {h.node_id for h in hits}
|
||||||
for strat_hit in strategy_result.results:
|
for strat_hit in strategy_result.results:
|
||||||
if strat_hit.node_id not in existing_ids:
|
if strat_hit.node_id not in existing_ids:
|
||||||
|
|
@ -207,20 +197,29 @@ async def chat_endpoint(
|
||||||
else:
|
else:
|
||||||
context_str = _build_enriched_context(hits)
|
context_str = _build_enriched_context(hits)
|
||||||
|
|
||||||
# 5. Generation Setup
|
# 5. Generation
|
||||||
|
# Wir laden das Template aus dem Service (da dort die prompts.yaml geladen ist)
|
||||||
template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}")
|
template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}")
|
||||||
|
system_prompt = llm.prompts.get("system_prompt", "")
|
||||||
|
|
||||||
# Injection der Instruktion (falls konfiguriert)
|
|
||||||
if prepend_instr:
|
if prepend_instr:
|
||||||
context_str = f"{prepend_instr}\n\n{context_str}"
|
context_str = f"{prepend_instr}\n\n{context_str}"
|
||||||
|
|
||||||
|
# Manuelles Bauen des finalen Prompts für volle Kontrolle
|
||||||
|
final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message)
|
||||||
|
|
||||||
|
# Aufruf via Raw Response (da wir den Prompt schon fertig haben)
|
||||||
|
# Wir müssen den System-Prompt manuell mitgeben?
|
||||||
|
# generate_raw_response in llm_service unterstützt aktuell kein 'system'.
|
||||||
|
# -> Wir erweitern generate_raw_response oder nutzen einen Hack: System + Prompt.
|
||||||
|
|
||||||
|
# SAUBERER WEG: Wir bauen den Payload für Ollama hier manuell zusammen und rufen eine generische Methode.
|
||||||
|
# Da LLMService.generate_raw_response keine System-Msg nimmt, packen wir sie davor.
|
||||||
|
full_text_prompt = f"{system_prompt}\n\n{final_prompt}"
|
||||||
|
|
||||||
logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...")
|
logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...")
|
||||||
answer_text = await llm.generate_rag_response(
|
answer_text = await llm.generate_raw_response(full_text_prompt)
|
||||||
query=request.message,
|
|
||||||
context_str=context_str
|
|
||||||
)
|
|
||||||
|
|
||||||
# 6. Response
|
|
||||||
duration_ms = int((time.time() - start_time) * 1000)
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
|
||||||
return ChatResponse(
|
return ChatResponse(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
app/services/llm_service.py — LLM Client (Ollama)
|
app/services/llm_service.py — LLM Client (Ollama)
|
||||||
|
|
||||||
Version:
|
Version:
|
||||||
0.1.2 (WP-05 Fix: Increased Timeout for CPU Inference)
|
0.2.0 (WP-06 Hybrid Router Support)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -18,18 +18,19 @@ class LLMService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.settings = get_settings()
|
self.settings = get_settings()
|
||||||
self.prompts = self._load_prompts()
|
self.prompts = self._load_prompts()
|
||||||
# FIX: Timeout auf 120 Sekunden erhöht für CPU-Only Server
|
|
||||||
self.client = httpx.AsyncClient(base_url=self.settings.OLLAMA_URL, timeout=120.0)
|
# Timeout aus Config nutzen (Default 120s)
|
||||||
|
self.client = httpx.AsyncClient(
|
||||||
|
base_url=self.settings.OLLAMA_URL,
|
||||||
|
timeout=self.settings.LLM_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
def _load_prompts(self) -> dict:
|
def _load_prompts(self) -> dict:
|
||||||
"""Lädt Prompts aus der konfigurierten YAML-Datei."""
|
"""Lädt Prompts aus der konfigurierten YAML-Datei."""
|
||||||
path = Path(self.settings.PROMPTS_PATH)
|
path = Path(self.settings.PROMPTS_PATH)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
logger.warning(f"Prompt config not found at {path}, using defaults.")
|
logger.warning(f"Prompt config not found at {path}, using defaults.")
|
||||||
return {
|
return {}
|
||||||
"system_prompt": "You are a helpful AI assistant.",
|
|
||||||
"rag_template": "Context: {context_str}\nQuestion: {query}"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
|
@ -38,15 +39,76 @@ class LLMService:
|
||||||
logger.error(f"Failed to load prompts: {e}")
|
logger.error(f"Failed to load prompts: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
async def generate_raw_response(self, prompt: str) -> str:
|
||||||
|
"""
|
||||||
|
Führt einen direkten LLM Call ohne RAG-Template aus.
|
||||||
|
Ideal für Classification/Routing.
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"model": self.settings.LLM_MODEL,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False,
|
||||||
|
"options": {
|
||||||
|
"temperature": 0.0, # Deterministisch für Routing!
|
||||||
|
"num_ctx": 512 # Kleines Fenster reicht für Classification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.client.post("/api/generate", json=payload)
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"Ollama Error ({response.status_code}): {response.text}")
|
||||||
|
return "FACT" # Fallback bei Fehler
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
return data.get("response", "").strip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"LLM Raw Gen Error: {e}")
|
||||||
|
return "FACT"
|
||||||
|
|
||||||
async def generate_rag_response(self, query: str, context_str: str) -> str:
|
async def generate_rag_response(self, query: str, context_str: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generiert eine Antwort basierend auf Query und Kontext.
|
Generiert eine Antwort basierend auf Query und Kontext (RAG).
|
||||||
"""
|
"""
|
||||||
system_prompt = self.prompts.get("system_prompt", "")
|
# Template hier ist nur Fallback, falls im Router nichts übergeben wird.
|
||||||
template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
|
# Im Normalfall formatiert der Router den context_str bereits vor oder übergibt das Template.
|
||||||
|
# Hier nutzen wir simple substitution, da der Prompt meist schon vom Router aufbereitet ist
|
||||||
|
# oder wir nutzen das Standard-Template aus der YAML.
|
||||||
|
|
||||||
# Template füllen
|
# HINWEIS: In der neuen Architektur (chat.py) wird das Template bereits VOR diesem Aufruf
|
||||||
final_prompt = template.format(context_str=context_str, query=query)
|
# geladen und formatiert, und als 'prompt' übergeben?
|
||||||
|
# Nein, chat.py ruft generate_rag_response(query, context_str) auf.
|
||||||
|
# Wir müssen sicherstellen, dass wir das *richtige* Template nutzen.
|
||||||
|
# Da generate_rag_response aktuell KEINEN 'template_key' Parameter hat,
|
||||||
|
# gehen wir davon aus, dass 'context_str' bereits Instruktionen enthält ODER
|
||||||
|
# dass chat.py den Prompt komplett baut.
|
||||||
|
|
||||||
|
# Um die API sauber zu halten: chat.py übergibt jetzt den FERTIGEN Prompt als context_str?
|
||||||
|
# Nein, chat.py baut den Prompt mit replace().
|
||||||
|
# Wir ändern diese Methode leicht ab, um flexibler zu sein:
|
||||||
|
# Wir erwarten, dass der Aufrufer (chat.py) die volle Kontrolle hat.
|
||||||
|
|
||||||
|
# Damit es 100% zusammenpasst mit dem chat.py unten:
|
||||||
|
# chat.py baut den finalen Prompt selbst zusammen!
|
||||||
|
# Wir nutzen daher generate_raw_response eigentlich auch für RAG,
|
||||||
|
# ODER wir passen generate_rag_response an, dass es "dumm" ist.
|
||||||
|
|
||||||
|
# Legacy Support für generate_rag_response:
|
||||||
|
# Wir bauen den Prompt zusammen, falls noch nicht geschehen.
|
||||||
|
# ABER: chat.py in meiner Version unten macht das Template-Handling.
|
||||||
|
# Daher ist der sauberste Weg: chat.py ruft generate_raw_response auf für die Antwort!
|
||||||
|
|
||||||
|
# FIX für Kompatibilität: Wir leiten rag_response intern auf raw um,
|
||||||
|
# bauen aber vorher den Prompt, falls context_str übergeben wird.
|
||||||
|
|
||||||
|
# Da wir chat.py kontrollieren (siehe unten), ändern wir chat.py so,
|
||||||
|
# dass es generate_raw_response nutzt! Das ist viel sauberer.
|
||||||
|
# Diese Methode bleibt für Backward Compatibility.
|
||||||
|
|
||||||
|
system_prompt = self.prompts.get("system_prompt", "")
|
||||||
|
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
|
||||||
|
final_prompt = rag_template.format(context_str=context_str, query=query)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.settings.LLM_MODEL,
|
"model": self.settings.LLM_MODEL,
|
||||||
|
|
@ -55,29 +117,17 @@ class LLMService:
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"options": {
|
"options": {
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
# Kleinerer Context spart Rechenzeit, falls 4096 zu viel ist
|
|
||||||
"num_ctx": 2048
|
"num_ctx": 2048
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self.client.post("/api/generate", json=payload)
|
response = await self.client.post("/api/generate", json=payload)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
error_msg = response.text
|
return f"Error: {response.text}"
|
||||||
logger.error(f"Ollama API Error ({response.status_code}): {error_msg}")
|
return response.json().get("response", "")
|
||||||
return f"Fehler vom LLM (Modell '{self.settings.LLM_MODEL}' vorhanden?): {error_msg}"
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
return data.get("response", "")
|
|
||||||
|
|
||||||
except httpx.ReadTimeout:
|
|
||||||
return "Timeout: Das Modell braucht zu lange zum Antworten (>120s). Hardware-Limit erreicht?"
|
|
||||||
except httpx.ConnectError:
|
|
||||||
return "Verbindungsfehler: Ist Ollama gestartet (Port 11434)?"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"LLM Service Exception: {e}")
|
return f"Error: {str(e)}"
|
||||||
return f"Interner Fehler: {str(e)}"
|
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
await self.client.aclose()
|
await self.client.aclose()
|
||||||
|
|
@ -1,21 +1,83 @@
|
||||||
version: 1.0
|
# config/decision_engine.yaml
|
||||||
|
# Steuerung der Decision Engine (WP-06)
|
||||||
|
# Hybrid-Modus: Keywords (Fast) + LLM Router (Smart Fallback)
|
||||||
|
version: 1.1
|
||||||
|
|
||||||
|
settings:
|
||||||
|
# Schalter: Soll das LLM gefragt werden, wenn kein Keyword passt?
|
||||||
|
llm_fallback_enabled: true
|
||||||
|
|
||||||
|
# Der Prompt für den "Semantic Router" (Slow Path)
|
||||||
|
llm_router_prompt: |
|
||||||
|
Analysiere die folgende Nachricht und entscheide, welche Strategie passt.
|
||||||
|
Antworte NUR mit dem Namen der Strategie (ein Wort).
|
||||||
|
|
||||||
|
STRATEGIEN:
|
||||||
|
- DECISION: User fragt nach Rat, Meinung, Strategie, Vor/Nachteilen.
|
||||||
|
- EMPATHY: User äußert Gefühle, Frust, Freude oder persönliche Probleme.
|
||||||
|
- CODING: User fragt nach Code, Syntax oder Programmierung.
|
||||||
|
- FACT: User fragt nach Wissen, Definitionen oder Fakten (Default).
|
||||||
|
|
||||||
|
NACHRICHT: "{query}"
|
||||||
|
|
||||||
|
STRATEGIE:
|
||||||
|
|
||||||
strategies:
|
strategies:
|
||||||
# Strategie 1: Der Berater (Das haben wir gebaut)
|
# 1. Fakten-Abfrage (Fallback & Default)
|
||||||
|
FACT:
|
||||||
|
description: "Reine Wissensabfrage."
|
||||||
|
trigger_keywords: []
|
||||||
|
inject_types: []
|
||||||
|
prompt_template: "rag_template"
|
||||||
|
prepend_instruction: null
|
||||||
|
|
||||||
|
# 2. Entscheidungs-Frage
|
||||||
DECISION:
|
DECISION:
|
||||||
trigger_keywords: ["soll ich", "empfehlung", "strategie"]
|
description: "Der User sucht Rat, Strategie oder Abwägung."
|
||||||
|
trigger_keywords:
|
||||||
|
- "soll ich"
|
||||||
|
- "meinung"
|
||||||
|
- "besser"
|
||||||
|
- "empfehlung"
|
||||||
|
- "strategie"
|
||||||
|
- "entscheidung"
|
||||||
|
- "wert"
|
||||||
|
- "prinzip"
|
||||||
|
- "vor- und nachteile"
|
||||||
|
- "abwägung"
|
||||||
inject_types: ["value", "principle", "goal"]
|
inject_types: ["value", "principle", "goal"]
|
||||||
prompt_template: "decision_template" # Nutzt das "Abwägen"-Template
|
prompt_template: "decision_template"
|
||||||
|
prepend_instruction: |
|
||||||
|
!!! ENTSCHEIDUNGS-MODUS !!!
|
||||||
|
BITTE WÄGE FAKTEN GEGEN FOLGENDE WERTE, PRINZIPIEN UND ZIELE AB:
|
||||||
|
|
||||||
# Strategie 2: Der empathische Zuhörer (NEU - Konzept)
|
# 3. Empathie / "Ich"-Modus
|
||||||
EMPATHY:
|
EMPATHY:
|
||||||
trigger_keywords: ["ich fühle", "traurig", "gestresst", "angst"]
|
description: "Reaktion auf emotionale Zustände."
|
||||||
inject_types: ["belief", "experience"] # Lädt Glaubenssätze & eigene Erfahrungen
|
trigger_keywords:
|
||||||
prompt_template: "empathy_template" # Ein Template, das auf "Zuhören" getrimmt ist
|
- "ich fühle"
|
||||||
prepend_instruction: "SEI EMPATHISCH. SPIEGEL DIE GEFÜHLE."
|
- "traurig"
|
||||||
|
- "glücklich"
|
||||||
|
- "gestresst"
|
||||||
|
- "angst"
|
||||||
|
- "nervt"
|
||||||
|
- "überfordert"
|
||||||
|
inject_types: ["experience", "belief", "profile"]
|
||||||
|
prompt_template: "empathy_template"
|
||||||
|
prepend_instruction: null
|
||||||
|
|
||||||
# Strategie 3: Der Coder (NEU - Konzept)
|
# 4. Coding / Technical
|
||||||
CODING:
|
CODING:
|
||||||
trigger_keywords: ["code", "python", "funktion", "bug"]
|
description: "Technische Anfragen und Programmierung."
|
||||||
inject_types: ["snippet", "reference"] # Lädt nur technische Schnipsel
|
trigger_keywords:
|
||||||
prompt_template: "technical_template" # Ein Template, das Codeblöcke erzwingt
|
- "code"
|
||||||
|
- "python"
|
||||||
|
- "script"
|
||||||
|
- "funktion"
|
||||||
|
- "bug"
|
||||||
|
- "syntax"
|
||||||
|
- "json"
|
||||||
|
- "yaml"
|
||||||
|
inject_types: ["snippet", "reference", "source"]
|
||||||
|
prompt_template: "technical_template"
|
||||||
|
prepend_instruction: null
|
||||||
|
|
@ -1,30 +1,20 @@
|
||||||
# config/prompts.yaml — Final V2.3 (WP-06 Decision Engine)
|
# config/prompts.yaml — Final V2.3.1 (Multi-Personality Support)
|
||||||
# Optimiert für Phi-3 Mini (Small Language Model)
|
|
||||||
|
|
||||||
system_prompt: |
|
system_prompt: |
|
||||||
Du bist 'mindnet', mein digitaler Zwilling und strategischer Partner.
|
Du bist 'mindnet', mein digitaler Zwilling und strategischer Partner.
|
||||||
|
|
||||||
DEINE IDENTITÄT:
|
DEINE IDENTITÄT:
|
||||||
- Du bist nicht nur eine Datenbank, sondern handelst nach MEINEN Werten.
|
- Du bist nicht nur eine Datenbank, sondern handelst nach MEINEN Werten und Zielen.
|
||||||
- Du bist objektiv bei Fakten, aber subjektiv (in meinem Sinne) bei Entscheidungen.
|
- Du passt deinen Stil dynamisch an die Situation an (Analytisch, Empathisch oder Technisch).
|
||||||
|
|
||||||
DEINE REGELN:
|
DEINE REGELN:
|
||||||
1. Deine Antwort muss auf dem bereitgestellten KONTEXT basieren.
|
1. Deine Antwort muss zu 100% auf dem bereitgestellten KONTEXT basieren.
|
||||||
2. Unterscheide klar zwischen FAKTEN (externe Welt) und PRINZIPIEN (meine innere Welt).
|
2. Halluziniere keine Fakten, die nicht in den Quellen stehen.
|
||||||
3. Wenn Quellen vom Typ [VALUE] oder [PRINCIPLE] vorliegen, haben diese Vorrang bei der Entscheidungsfindung.
|
3. Antworte auf Deutsch (außer bei Code/Fachbegriffen).
|
||||||
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:
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 1. STANDARD: Fakten & Wissen (Intent: FACT)
|
||||||
|
# ---------------------------------------------------------
|
||||||
rag_template: |
|
rag_template: |
|
||||||
QUELLEN (WISSEN):
|
QUELLEN (WISSEN):
|
||||||
=========================================
|
=========================================
|
||||||
|
|
@ -35,12 +25,14 @@ rag_template: |
|
||||||
{query}
|
{query}
|
||||||
|
|
||||||
ANWEISUNG:
|
ANWEISUNG:
|
||||||
Beantworte die Frage basierend auf den Quellen.
|
Beantworte die Frage präzise basierend auf den Quellen.
|
||||||
Nenne die spezifischen Gründe, die im Text stehen (besonders aus [DECISION] Quellen).
|
Fasse die Informationen zusammen. Sei objektiv und neutral.
|
||||||
|
|
||||||
# Neues Template für WP-06: Reasoning & Decision Making
|
# ---------------------------------------------------------
|
||||||
|
# 2. DECISION: Strategie & Abwägung (Intent: DECISION)
|
||||||
|
# ---------------------------------------------------------
|
||||||
decision_template: |
|
decision_template: |
|
||||||
KONTEXT (FAKTEN & WERTE):
|
KONTEXT (FAKTEN & STRATEGIE):
|
||||||
=========================================
|
=========================================
|
||||||
{context_str}
|
{context_str}
|
||||||
=========================================
|
=========================================
|
||||||
|
|
@ -51,11 +43,54 @@ decision_template: |
|
||||||
ANWEISUNG:
|
ANWEISUNG:
|
||||||
Du agierst als mein Entscheidungs-Partner.
|
Du agierst als mein Entscheidungs-Partner.
|
||||||
1. Analysiere die Faktenlage aus den Quellen.
|
1. Analysiere die Faktenlage aus den Quellen.
|
||||||
2. Prüfe dies gegen meine [VALUE] und [PRINCIPLE] Quellen (falls vorhanden).
|
2. Prüfe dies hart gegen meine strategischen Notizen (Typ [VALUE], [PRINCIPLE], [GOAL]).
|
||||||
3. Wäge ab: Passt die technische/faktische Lösung zu meinen Werten?
|
3. Wäge ab: Passt die technische/faktische Lösung zu meinen Werten?
|
||||||
4. Gib eine klare Empfehlung ab.
|
|
||||||
|
|
||||||
FORMAT:
|
FORMAT:
|
||||||
- **Analyse:** (Faktenlage)
|
- **Analyse:** (Kurze Zusammenfassung der Fakten)
|
||||||
- **Werte-Check:** (Konflikt oder Übereinstimmung mit Prinzipien)
|
- **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!)
|
||||||
- **Fazit:** (Deine Empfehlung)
|
- **Empfehlung:** (Klare Meinung: Ja/Nein/Vielleicht mit Begründung)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
empathy_template: |
|
||||||
|
KONTEXT (ERFAHRUNGEN & GLAUBENSSÄTZE):
|
||||||
|
=========================================
|
||||||
|
{context_str}
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
SITUATION:
|
||||||
|
{query}
|
||||||
|
|
||||||
|
ANWEISUNG:
|
||||||
|
Du agierst jetzt als mein empathischer Spiegel.
|
||||||
|
1. Versuche nicht sofort, das Problem technisch zu lösen.
|
||||||
|
2. Zeige Verständnis für die Situation basierend auf meinen eigenen Erfahrungen ([EXPERIENCE]) oder Glaubenssätzen ([BELIEF]), falls im Kontext vorhanden.
|
||||||
|
3. Antworte in der "Ich"-Form oder "Wir"-Form. Sei unterstützend.
|
||||||
|
|
||||||
|
TONFALL:
|
||||||
|
Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 4. TECHNICAL: Der Coder (Intent: CODING)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
technical_template: |
|
||||||
|
KONTEXT (DOCS & SNIPPETS):
|
||||||
|
=========================================
|
||||||
|
{context_str}
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
TASK:
|
||||||
|
{query}
|
||||||
|
|
||||||
|
ANWEISUNG:
|
||||||
|
Du bist Senior Developer.
|
||||||
|
1. Ignoriere Smalltalk. Komm sofort zum Punkt.
|
||||||
|
2. Generiere validen, performanten Code basierend auf den Quellen.
|
||||||
|
3. Wenn Quellen fehlen, nutze dein allgemeines Programmierwissen, aber weise darauf hin.
|
||||||
|
|
||||||
|
FORMAT:
|
||||||
|
- Kurze Erklärung des Ansatzes.
|
||||||
|
- Markdown Code-Block (Copy-Paste fertig).
|
||||||
|
- Wichtige Edge-Cases.
|
||||||
Loading…
Reference in New Issue
Block a user