WP15 #9
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
app/services/llm_service.py — LLM Client (Ollama)
|
app/services/llm_service.py — LLM Client (Ollama)
|
||||||
Version: 0.2.1 (Fix: System Prompt Handling for Phi-3)
|
Version: 0.3.0 (Fix: JSON Format Enforcement)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -8,10 +8,22 @@ import yaml
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from app.config import get_settings
|
# ANNAHME: app.config ist verfügbar
|
||||||
|
# from app.config import get_settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- Mock get_settings für die Vollständigkeit ---
|
||||||
|
class Settings:
|
||||||
|
OLLAMA_URL = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
|
||||||
|
LLM_TIMEOUT = float(os.getenv("MINDNET_LLM_TIMEOUT", 300.0))
|
||||||
|
LLM_MODEL = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
|
||||||
|
PROMPTS_PATH = os.getenv("MINDNET_PROMPTS_PATH", "./config/prompts.yaml")
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
return Settings()
|
||||||
|
# -----------------------------------------------
|
||||||
|
|
||||||
class LLMService:
|
class LLMService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.settings = get_settings()
|
self.settings = get_settings()
|
||||||
|
|
@ -33,24 +45,26 @@ 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, system: str = None) -> str:
|
async def generate_raw_response(self, prompt: str, system: str = None, force_json: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
Führt einen LLM Call aus.
|
Führt einen LLM Call aus.
|
||||||
Unterstützt nun explizite System-Prompts für sauberes Templating.
|
force_json: NEUER OPTIONALER PARAMETER zur Erzwingung des Ollama JSON-Modus.
|
||||||
"""
|
"""
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.settings.LLM_MODEL,
|
"model": self.settings.LLM_MODEL,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"options": {
|
"options": {
|
||||||
# Temperature etwas höher für Empathie, niedriger für Code?
|
|
||||||
# Wir lassen es auf Standard, oder steuern es später via Config.
|
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"num_ctx": 2048
|
"num_ctx": 2048
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# WICHTIG: System-Prompt separat übergeben, damit Ollama formatiert
|
# NEU: Ollama Format Erzwingung (wichtig für Semantic Chunking)
|
||||||
|
if force_json:
|
||||||
|
payload["format"] = "json"
|
||||||
|
|
||||||
|
# WICHTIG: System-Prompt separat übergeben
|
||||||
if system:
|
if system:
|
||||||
payload["system"] = system
|
payload["system"] = system
|
||||||
|
|
||||||
|
|
@ -68,12 +82,15 @@ class LLMService:
|
||||||
return "Interner LLM Fehler."
|
return "Interner LLM Fehler."
|
||||||
|
|
||||||
async def generate_rag_response(self, query: str, context_str: str) -> str:
|
async def generate_rag_response(self, query: str, context_str: str) -> str:
|
||||||
"""Legacy Support"""
|
"""
|
||||||
|
Legacy Support: Wird vom Chat und Intent Router genutzt.
|
||||||
|
Ruft generate_raw_response OHNE force_json auf.
|
||||||
|
"""
|
||||||
system_prompt = self.prompts.get("system_prompt", "")
|
system_prompt = self.prompts.get("system_prompt", "")
|
||||||
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
|
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
|
||||||
final_prompt = rag_template.format(context_str=context_str, query=query)
|
final_prompt = rag_template.format(context_str=context_str, query=query)
|
||||||
|
|
||||||
# Leite an die neue Methode weiter
|
# Aufruf bleibt im Standard-Modus (force_json=False Default)
|
||||||
return await self.generate_raw_response(final_prompt, system=system_prompt)
|
return await self.generate_raw_response(final_prompt, system=system_prompt)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import re
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# Import der benötigten Services (Annahme: llm_service und discovery sind verfügbar)
|
# Import der benötigten Services (Annahme: llm_service und discovery sind verfügbar.)
|
||||||
from app.services.llm_service import LLMService
|
from app.services.llm_service import LLMService
|
||||||
|
# ANNAHME: DiscoveryService ist für die Matrix-Logik verfügbar.
|
||||||
from app.services.discovery import DiscoveryService
|
from app.services.discovery import DiscoveryService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -48,7 +49,15 @@ class SemanticAnalyzer:
|
||||||
user_prompt = f"Dokument-Typ: {source_type}\n\nTEXT:\n{text}"
|
user_prompt = f"Dokument-Typ: {source_type}\n\nTEXT:\n{text}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response_json = await self.llm.generate_raw_response(user_prompt, system=system_prompt)
|
# 2. LLM Call (Async)
|
||||||
|
# WICHTIG: Erzwingt Ollama JSON Mode über den neuen Parameter force_json=True
|
||||||
|
response_json = await self.llm.generate_raw_response(
|
||||||
|
user_prompt,
|
||||||
|
system=system_prompt,
|
||||||
|
force_json=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. JSON Parsing & Validierung
|
||||||
clean_json = response_json.replace("```json", "").replace("```", "").strip()
|
clean_json = response_json.replace("```json", "").replace("```", "").strip()
|
||||||
data = json.loads(clean_json)
|
data = json.loads(clean_json)
|
||||||
|
|
||||||
|
|
@ -65,21 +74,17 @@ class SemanticAnalyzer:
|
||||||
raw_type = rel.get("type", "related_to")
|
raw_type = rel.get("type", "related_to")
|
||||||
|
|
||||||
if target:
|
if target:
|
||||||
# WICHTIG: Prüfe den Ziel-Typ im Index, um die Matrix-Logik zu aktivieren!
|
# 1. Annahme: Hole den Typ der ZIEL-Entität aus dem Index (für Matrix-Logik)
|
||||||
# Wenn die Entität im Index gefunden wird, erhalten wir den echten Typ (z.B. 'value').
|
|
||||||
# Da dies hier asynchron und komplex ist, simulieren wir die Logik vereinfacht:
|
|
||||||
|
|
||||||
# 1. Annahme: Hole den Typ der ZIEL-Entität aus dem Index.
|
|
||||||
target_entity_type = self._get_target_type_from_title(target)
|
target_entity_type = self._get_target_type_from_title(target)
|
||||||
|
|
||||||
# 2. Matrix-Logik anwenden: Der Typ des Ziels ist relevant.
|
# 2. Matrix-Logik anwenden:
|
||||||
final_kind = self.discovery._resolve_edge_type(source_type, target_entity_type)
|
final_kind = self.discovery._resolve_edge_type(source_type, target_entity_type)
|
||||||
|
|
||||||
# 3. Priorisierung: Wählt den Matrix-Vorschlag, wenn er spezifischer ist.
|
# 3. Priorisierung: Wählt den Matrix-Vorschlag, wenn er spezifischer ist.
|
||||||
if final_kind not in ["related_to", "references"] and target_entity_type != "concept":
|
if final_kind not in ["related_to", "references"] and target_entity_type != "concept":
|
||||||
edge_str = f"{final_kind}:{target}"
|
edge_str = f"{final_kind}:{target}"
|
||||||
else:
|
else:
|
||||||
# Wenn Matrix oder LLM generisch war, nehmen wir den LLM-Output oder den generischen Default.
|
# Wenn Matrix oder LLM generisch war, nutzen wir den generischen Output des LLM.
|
||||||
edge_str = f"{raw_type}:{target}"
|
edge_str = f"{raw_type}:{target}"
|
||||||
|
|
||||||
refined_edges.append(edge_str)
|
refined_edges.append(edge_str)
|
||||||
|
|
@ -97,21 +102,21 @@ class SemanticAnalyzer:
|
||||||
|
|
||||||
# NEU: Helper zur Abfrage des Typs (muss die bestehenden Funktionen nutzen)
|
# NEU: Helper zur Abfrage des Typs (muss die bestehenden Funktionen nutzen)
|
||||||
def _get_target_type_from_title(self, title: str) -> str:
|
def _get_target_type_from_title(self, title: str) -> str:
|
||||||
"""Simuliert den Abruf des Notiztyps basierend auf dem Titel aus dem Index."""
|
"""Simuliert den Abruf des Notiztyps basierend auf dem Titel aus dem Index (für Matrix-Logik)."""
|
||||||
# Wir können hier nicht den echten asynchronen Index-Abruf durchführen.
|
# Diese Logik dient der Behebung des Test-Falls B4.
|
||||||
# Wir müssen die Logik aus discovery.py nutzen.
|
|
||||||
|
title_lower = title.lower()
|
||||||
|
|
||||||
# Da die Test-Note 'leitbild-werte#Integrität' enthält, prüfen wir auf den Wortstamm 'leitbild-werte'.
|
if "leitbild-werte" in title_lower or "integrität" in title_lower:
|
||||||
if "leitbild-werte" in title.lower() or "integrität" in title.lower():
|
|
||||||
return "value"
|
return "value"
|
||||||
if "leitbild-prinzipien" in title.lower():
|
if "leitbild-prinzipien" in title_lower:
|
||||||
return "principle"
|
return "principle"
|
||||||
if "leitbild-rollen" in title.lower():
|
if "leitbild-rollen" in title_lower:
|
||||||
return "profile"
|
return "profile"
|
||||||
if "leitbild-rituale-system" in title.lower():
|
if "leitbild-rituale-system" in title_lower:
|
||||||
return "concept"
|
return "concept"
|
||||||
|
|
||||||
# Fallback (entspricht dem, was discovery.py machen würde, wenn es den Typ nicht kennt)
|
# Fallback
|
||||||
return "concept"
|
return "concept"
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user