WP20 initial
This commit is contained in:
parent
2d43e0596c
commit
234949800b
|
|
@ -1,10 +1,11 @@
|
|||
"""
|
||||
FILE: app/config.py
|
||||
DESCRIPTION: Zentrale Pydantic-Konfiguration (Env-Vars für Qdrant, LLM, Retriever).
|
||||
VERSION: 0.4.0
|
||||
Erweitert um WP-20 Hybrid-Optionen.
|
||||
VERSION: 0.5.0
|
||||
STATUS: Active
|
||||
DEPENDENCIES: os, functools, pathlib
|
||||
LAST_ANALYSIS: 2025-12-15
|
||||
LAST_ANALYSIS: 2025-12-23
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
|
|
@ -12,29 +13,38 @@ from functools import lru_cache
|
|||
from pathlib import Path
|
||||
|
||||
class Settings:
|
||||
# Qdrant
|
||||
# Qdrant Verbindung
|
||||
QDRANT_URL: str = os.getenv("QDRANT_URL", "http://127.0.0.1:6333")
|
||||
QDRANT_API_KEY: str | None = os.getenv("QDRANT_API_KEY")
|
||||
COLLECTION_PREFIX: str = os.getenv("MINDNET_PREFIX", "mindnet")
|
||||
VECTOR_SIZE: int = int(os.getenv("MINDNET_VECTOR_SIZE", "384"))
|
||||
DISTANCE: str = os.getenv("MINDNET_DISTANCE", "Cosine")
|
||||
|
||||
# Embeddings
|
||||
# Embeddings (lokal)
|
||||
MODEL_NAME: str = os.getenv("MINDNET_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
||||
|
||||
# WP-05 LLM / Ollama
|
||||
# WP-20 Hybrid LLM Provider
|
||||
# Erlaubt: "ollama" oder "gemini"
|
||||
MINDNET_LLM_PROVIDER: str = os.getenv("MINDNET_LLM_PROVIDER", "ollama").lower()
|
||||
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY")
|
||||
GEMINI_MODEL: str = os.getenv("MINDNET_GEMINI_MODEL", "gemini-1.5-flash")
|
||||
LLM_FALLBACK_ENABLED: bool = os.getenv("MINDNET_LLM_FALLBACK", "true").lower() == "true"
|
||||
|
||||
# WP-05 LLM / Ollama (Local)
|
||||
OLLAMA_URL: str = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
|
||||
LLM_MODEL: str = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
|
||||
PROMPTS_PATH: str = os.getenv("MINDNET_PROMPTS_PATH", "config/prompts.yaml")
|
||||
|
||||
# NEU für WP-06
|
||||
# WP-06 / WP-14 Performance & Timeouts
|
||||
LLM_TIMEOUT: float = float(os.getenv("MINDNET_LLM_TIMEOUT", "120.0"))
|
||||
DECISION_CONFIG_PATH: str = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
|
||||
BACKGROUND_LIMIT: int = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2"))
|
||||
|
||||
# API
|
||||
# API & Debugging
|
||||
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
|
||||
MINDNET_VAULT_ROOT: str = os.getenv("MINDNET_VAULT_ROOT", "./vault")
|
||||
|
||||
# WP-04 Retriever Defaults
|
||||
# WP-04 Retriever Gewichte (Semantik vs. Graph)
|
||||
RETRIEVER_W_SEM: float = float(os.getenv("MINDNET_WP04_W_SEM", "0.70"))
|
||||
RETRIEVER_W_EDGE: float = float(os.getenv("MINDNET_WP04_W_EDGE", "0.25"))
|
||||
RETRIEVER_W_CENT: float = float(os.getenv("MINDNET_WP04_W_CENT", "0.05"))
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
"""
|
||||
FILE: app/core/ingestion.py
|
||||
DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen (Notes, Chunks, Edges).
|
||||
FIX: Korrekte Priorisierung von Frontmatter für chunk_profile und retriever_weight.
|
||||
Lade Chunk-Config basierend auf dem effektiven Profil, nicht nur dem Notiz-Typ.
|
||||
WP-22: Integration von Content Lifecycle (Status Gate) und Edge Registry Validation.
|
||||
WP-22: Multi-Hash Refresh für konsistente Change Detection.
|
||||
VERSION: 2.8.6 (WP-22 Lifecycle & Registry)
|
||||
WP-20: Integration von Smart Edge Allocation via Hybrid LLM (Gemini/Ollama).
|
||||
WP-22: Integration von Content Lifecycle (Status Gate) und Edge Registry Validation.
|
||||
WP-22: Kontextsensitive Kanten-Validierung mit Fundort-Reporting (Zeilennummern).
|
||||
WP-22: Multi-Hash Refresh für konsistente Change Detection.
|
||||
VERSION: 2.11.0 (WP-20 Full Integration: Hybrid Smart Edges)
|
||||
STATUS: Active
|
||||
DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.core.derive_edges, app.core.qdrant*, app.services.embeddings_client, app.services.edge_registry
|
||||
EXTERNAL_CONFIG: config/types.yaml
|
||||
DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.services.llm_service, app.services.edge_registry
|
||||
EXTERNAL_CONFIG: config/types.yaml, config/prompts.yaml
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
|
@ -21,6 +22,7 @@ from app.core.parser import (
|
|||
read_markdown,
|
||||
normalize_frontmatter,
|
||||
validate_required_frontmatter,
|
||||
extract_edges_with_context, #
|
||||
)
|
||||
from app.core.note_payload import make_note_payload
|
||||
from app.core.chunker import assemble_chunks, get_chunk_config
|
||||
|
|
@ -42,6 +44,7 @@ from app.core.qdrant_points import (
|
|||
|
||||
from app.services.embeddings_client import EmbeddingsClient
|
||||
from app.services.edge_registry import registry as edge_registry
|
||||
from app.services.llm_service import LLMService #
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -66,18 +69,15 @@ def effective_chunk_profile_name(fm: dict, note_type: str, reg: dict) -> str:
|
|||
Ermittelt den Namen des zu nutzenden Chunk-Profils.
|
||||
Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default
|
||||
"""
|
||||
# 1. Frontmatter Override
|
||||
override = fm.get("chunking_profile") or fm.get("chunk_profile")
|
||||
if override and isinstance(override, str):
|
||||
return override
|
||||
|
||||
# 2. Type Config
|
||||
t_cfg = reg.get("types", {}).get(note_type, {})
|
||||
if t_cfg:
|
||||
cp = t_cfg.get("chunking_profile") or t_cfg.get("chunk_profile")
|
||||
if cp: return cp
|
||||
|
||||
# 3. Global Default
|
||||
return reg.get("defaults", {}).get("chunking_profile", "sliding_standard")
|
||||
|
||||
def effective_retriever_weight(fm: dict, note_type: str, reg: dict) -> float:
|
||||
|
|
@ -85,34 +85,32 @@ def effective_retriever_weight(fm: dict, note_type: str, reg: dict) -> float:
|
|||
Ermittelt das effektive retriever_weight für das Scoring.
|
||||
Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default
|
||||
"""
|
||||
# 1. Frontmatter Override
|
||||
override = fm.get("retriever_weight")
|
||||
if override is not None:
|
||||
try: return float(override)
|
||||
except: pass
|
||||
|
||||
# 2. Type Config
|
||||
t_cfg = reg.get("types", {}).get(note_type, {})
|
||||
if t_cfg and "retriever_weight" in t_cfg:
|
||||
return float(t_cfg["retriever_weight"])
|
||||
|
||||
# 3. Global Default
|
||||
return float(reg.get("defaults", {}).get("retriever_weight", 1.0))
|
||||
|
||||
|
||||
class IngestionService:
|
||||
def __init__(self, collection_prefix: str = None):
|
||||
env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
|
||||
self.prefix = collection_prefix or env_prefix
|
||||
from app.config import get_settings
|
||||
self.settings = get_settings() #
|
||||
|
||||
self.prefix = collection_prefix or self.settings.COLLECTION_PREFIX
|
||||
self.cfg = QdrantConfig.from_env()
|
||||
self.cfg.prefix = self.prefix
|
||||
self.client = get_client(self.cfg)
|
||||
self.dim = self.cfg.dim
|
||||
self.registry = load_type_registry()
|
||||
self.dim = self.cfg.VECTOR_SIZE #
|
||||
self.type_registry = load_type_registry()
|
||||
self.embedder = EmbeddingsClient()
|
||||
self.llm = LLMService() #
|
||||
|
||||
# Change Detection Modus (full oder body)
|
||||
self.active_hash_mode = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full")
|
||||
|
||||
try:
|
||||
|
|
@ -122,8 +120,8 @@ class IngestionService:
|
|||
logger.warning(f"DB init warning: {e}")
|
||||
|
||||
def _get_chunk_config_by_profile(self, profile_name: str, note_type: str) -> Dict[str, Any]:
|
||||
"""Holt die Chunker-Parameter (max, target, overlap) für ein spezifisches Profil."""
|
||||
profiles = self.registry.get("chunking_profiles", {})
|
||||
"""Holt die Chunker-Parameter für ein spezifisches Profil."""
|
||||
profiles = self.type_registry.get("chunking_profiles", {})
|
||||
if profile_name in profiles:
|
||||
cfg = profiles[profile_name].copy()
|
||||
if "overlap" in cfg and isinstance(cfg["overlap"], list):
|
||||
|
|
@ -131,6 +129,43 @@ class IngestionService:
|
|||
return cfg
|
||||
return get_chunk_config(note_type)
|
||||
|
||||
async def _perform_smart_edge_allocation(self, text: str, note_id: str) -> List[Dict]:
|
||||
"""
|
||||
WP-20: Nutzt den Hybrid LLM Service für die semantische Kanten-Extraktion.
|
||||
Verwendet provider-spezifische Prompts aus der config.
|
||||
"""
|
||||
provider = self.settings.MINDNET_LLM_PROVIDER #
|
||||
|
||||
# Prompt-Lookup (Fallback auf Standard-Struktur falls Key fehlt)
|
||||
prompt_data = self.llm.prompts.get("edge_extraction", {})
|
||||
if isinstance(prompt_data, dict):
|
||||
template = prompt_data.get(provider, prompt_data.get("ollama", ""))
|
||||
else:
|
||||
template = str(prompt_data)
|
||||
|
||||
if not template:
|
||||
template = "Extrahiere semantische Relationen aus: {text}. Antworte als JSON: [{\"to\": \"X\", \"kind\": \"Y\"}]"
|
||||
|
||||
prompt = template.format(text=text[:6000], note_id=note_id)
|
||||
|
||||
try:
|
||||
# Nutzt die Semaphore für Hintergrund-Tasks
|
||||
response_json = await self.llm.generate_raw_response(
|
||||
prompt=prompt,
|
||||
priority="background",
|
||||
force_json=True
|
||||
)
|
||||
data = json.loads(response_json)
|
||||
|
||||
# Anreicherung mit Provenance-Metadaten für WP-22 Registry
|
||||
for item in data:
|
||||
item["provenance"] = "semantic_ai"
|
||||
item["line"] = f"ai-{provider}"
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.warning(f"Smart Edge Allocation skipped for {note_id}: {e}")
|
||||
return []
|
||||
|
||||
async def process_file(
|
||||
self,
|
||||
file_path: str,
|
||||
|
|
@ -142,10 +177,7 @@ class IngestionService:
|
|||
hash_source: str = "parsed",
|
||||
hash_normalize: str = "canonical"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Verarbeitet eine Markdown-Datei und schreibt sie in den Graphen.
|
||||
Folgt dem 14-Schritte-Workflow.
|
||||
"""
|
||||
"""Verarbeitet eine Markdown-Datei und schreibt sie in den Graphen."""
|
||||
result = {"path": file_path, "status": "skipped", "changed": False, "error": None}
|
||||
|
||||
# 1. Parse & Frontmatter Validation
|
||||
|
|
@ -158,42 +190,29 @@ class IngestionService:
|
|||
logger.error(f"Validation failed for {file_path}: {e}")
|
||||
return {**result, "error": f"Validation failed: {str(e)}"}
|
||||
|
||||
# --- WP-22: Content Lifecycle Gate (Teil A) ---
|
||||
# --- WP-22: Content Lifecycle Gate ---
|
||||
status = fm.get("status", "draft").lower().strip()
|
||||
|
||||
# Hard Skip für System- oder Archiv-Dateien
|
||||
if status in ["system", "template", "archive", "hidden"]:
|
||||
logger.info(f"Skipping file {file_path} (Status: {status})")
|
||||
return {**result, "status": "skipped", "reason": f"lifecycle_status_{status}"}
|
||||
|
||||
# 2. Type & Config Resolution
|
||||
note_type = resolve_note_type(fm.get("type"), self.registry)
|
||||
note_type = resolve_note_type(fm.get("type"), self.type_registry)
|
||||
fm["type"] = note_type
|
||||
|
||||
effective_profile = effective_chunk_profile_name(fm, note_type, self.registry)
|
||||
effective_weight = effective_retriever_weight(fm, note_type, self.registry)
|
||||
effective_profile = effective_chunk_profile_name(fm, note_type, self.type_registry)
|
||||
effective_weight = effective_retriever_weight(fm, note_type, self.type_registry)
|
||||
|
||||
fm["chunk_profile"] = effective_profile
|
||||
fm["retriever_weight"] = effective_weight
|
||||
|
||||
# 3. Build Note Payload (Inkl. Multi-Hash für WP-22)
|
||||
try:
|
||||
note_pl = make_note_payload(
|
||||
parsed,
|
||||
vault_root=vault_root,
|
||||
hash_normalize=hash_normalize,
|
||||
hash_source=hash_source,
|
||||
file_path=file_path
|
||||
)
|
||||
# Text Body Fallback
|
||||
note_pl = make_note_payload(parsed, vault_root=vault_root, hash_normalize=hash_normalize, hash_source=hash_source, file_path=file_path)
|
||||
if not note_pl.get("fulltext"): note_pl["fulltext"] = getattr(parsed, "body", "") or ""
|
||||
|
||||
# Sicherstellen der effektiven Werte im Payload
|
||||
note_pl["retriever_weight"] = effective_weight
|
||||
note_pl["chunk_profile"] = effective_profile
|
||||
# WP-22: Status speichern
|
||||
note_pl["status"] = status
|
||||
|
||||
note_id = note_pl["note_id"]
|
||||
except Exception as e:
|
||||
logger.error(f"Payload build failed: {e}")
|
||||
|
|
@ -205,15 +224,12 @@ class IngestionService:
|
|||
old_payload = self._fetch_note_payload(note_id)
|
||||
|
||||
has_old = old_payload is not None
|
||||
# Prüfung gegen den aktuell konfigurierten Hash-Modus (body oder full)
|
||||
check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}"
|
||||
|
||||
old_hashes = (old_payload or {}).get("hashes")
|
||||
if isinstance(old_hashes, dict): old_hash = old_hashes.get(check_key)
|
||||
elif isinstance(old_hashes, str) and self.active_hash_mode == "body": old_hash = old_hashes
|
||||
else: old_hash = None
|
||||
|
||||
old_hashes = (old_payload or {}).get("hashes", {})
|
||||
old_hash = old_hashes.get(check_key) if isinstance(old_hashes, dict) else None
|
||||
new_hash = note_pl.get("hashes", {}).get(check_key)
|
||||
|
||||
hash_changed = (old_hash != new_hash)
|
||||
chunks_missing, edges_missing = self._artifacts_missing(note_id)
|
||||
|
||||
|
|
@ -228,49 +244,38 @@ class IngestionService:
|
|||
# 5. Processing (Chunking, Embedding, Edge Generation)
|
||||
try:
|
||||
body_text = getattr(parsed, "body", "") or ""
|
||||
|
||||
# Konfiguration für das spezifische Profil laden
|
||||
edge_registry.ensure_latest() #
|
||||
|
||||
chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type)
|
||||
|
||||
chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config)
|
||||
|
||||
# Chunks mit Metadaten anreichern
|
||||
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
|
||||
|
||||
vecs = []
|
||||
if chunk_pls:
|
||||
texts = [c.get("window") or c.get("text") or "" for c in chunk_pls]
|
||||
try:
|
||||
if hasattr(self.embedder, 'embed_documents'):
|
||||
vecs = await self.embedder.embed_documents(texts)
|
||||
else:
|
||||
for t in texts:
|
||||
v = await self.embedder.embed_query(t)
|
||||
vecs.append(v)
|
||||
except Exception as e:
|
||||
logger.error(f"Embedding failed: {e}")
|
||||
raise RuntimeError(f"Embedding failed: {e}")
|
||||
vecs = await self.embedder.embed_documents(texts)
|
||||
|
||||
# Kanten generieren
|
||||
try:
|
||||
raw_edges = build_edges_for_note(
|
||||
note_id,
|
||||
chunk_pls,
|
||||
note_level_references=note_pl.get("references", []),
|
||||
include_note_scope_refs=note_scope_refs
|
||||
)
|
||||
except TypeError:
|
||||
raw_edges = build_edges_for_note(note_id, chunk_pls)
|
||||
|
||||
# --- WP-22: Edge Registry Validation (Teil B) ---
|
||||
# --- WP-22: Kanten-Extraktion & Validierung ---
|
||||
edges = []
|
||||
if raw_edges:
|
||||
for edge in raw_edges:
|
||||
original_kind = edge.get("kind", "related_to")
|
||||
# Normalisierung über die Registry (Alias-Auflösung)
|
||||
canonical_kind = edge_registry.resolve(original_kind)
|
||||
edge["kind"] = canonical_kind
|
||||
edges.append(edge)
|
||||
context = {"file": file_path, "note_id": note_id}
|
||||
|
||||
# A. Explizite User-Kanten mit Zeilennummern
|
||||
explicit_edges = extract_edges_with_context(parsed)
|
||||
for e in explicit_edges:
|
||||
e["kind"] = edge_registry.resolve(edge_type=e["kind"], provenance="explicit", context={**context, "line": e.get("line")})
|
||||
edges.append(e)
|
||||
|
||||
# B. WP-20: Smart AI Edges (Hybrid Acceleration)
|
||||
ai_edges = await self._perform_smart_edge_allocation(body_text, note_id)
|
||||
for e in ai_edges:
|
||||
e["kind"] = edge_registry.resolve(edge_type=e.get("kind"), provenance="semantic_ai", context={**context, "line": e.get("line")})
|
||||
edges.append(e)
|
||||
|
||||
# C. System-Kanten (Struktur: belongs_to, next, prev)
|
||||
raw_system_edges = build_edges_for_note(note_id, chunk_pls, note_level_references=note_pl.get("references", []))
|
||||
for e in raw_system_edges:
|
||||
e["kind"] = edge_registry.resolve(edge_type=e.get("kind", "belongs_to"), provenance="structure", context={**context, "line": "system"})
|
||||
if e["kind"]: edges.append(e)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Processing failed: {e}", exc_info=True)
|
||||
|
|
@ -278,31 +283,23 @@ class IngestionService:
|
|||
|
||||
# 6. Upsert in Qdrant
|
||||
try:
|
||||
# Alte Fragmente löschen, um "Geister-Chunks" zu vermeiden
|
||||
if purge_before and has_old:
|
||||
self._purge_artifacts(note_id)
|
||||
|
||||
# Note Metadaten
|
||||
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
|
||||
upsert_batch(self.client, n_name, n_pts)
|
||||
|
||||
# Chunks (Vektoren)
|
||||
if chunk_pls and vecs:
|
||||
c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)
|
||||
upsert_batch(self.client, c_name, c_pts)
|
||||
|
||||
# Kanten
|
||||
if edges:
|
||||
e_name, e_pts = points_for_edges(self.prefix, edges)
|
||||
upsert_batch(self.client, e_name, e_pts)
|
||||
|
||||
return {
|
||||
"path": file_path,
|
||||
"status": "success",
|
||||
"changed": True,
|
||||
"note_id": note_id,
|
||||
"chunks_count": len(chunk_pls),
|
||||
"edges_count": len(edges)
|
||||
"path": file_path, "status": "success", "changed": True, "note_id": note_id,
|
||||
"chunks_count": len(chunk_pls), "edges_count": len(edges)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Upsert failed: {e}", exc_info=True)
|
||||
|
|
@ -319,19 +316,17 @@ class IngestionService:
|
|||
except: return None
|
||||
|
||||
def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]:
|
||||
"""Prüft, ob Chunks oder Kanten für eine Note fehlen (Integritätscheck)."""
|
||||
"""Prüft, ob Chunks oder Kanten für eine Note fehlen."""
|
||||
from qdrant_client.http import models as rest
|
||||
c_col = f"{self.prefix}_chunks"
|
||||
e_col = f"{self.prefix}_edges"
|
||||
try:
|
||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
c_pts, _ = self.client.scroll(collection_name=c_col, scroll_filter=f, limit=1)
|
||||
e_pts, _ = self.client.scroll(collection_name=e_col, scroll_filter=f, limit=1)
|
||||
c_pts, _ = self.client.scroll(collection_name=f"{self.prefix}_chunks", scroll_filter=f, limit=1)
|
||||
e_pts, _ = self.client.scroll(collection_name=f"{self.prefix}_edges", scroll_filter=f, limit=1)
|
||||
return (not bool(c_pts)), (not bool(e_pts))
|
||||
except: return True, True
|
||||
|
||||
def _purge_artifacts(self, note_id: str):
|
||||
"""Löscht alle Chunks und Edges einer Note (vor dem Neu-Schreiben)."""
|
||||
"""Löscht alle Chunks und Edges einer Note."""
|
||||
from qdrant_client.http import models as rest
|
||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
selector = rest.FilterSelector(filter=f)
|
||||
|
|
@ -341,7 +336,7 @@ class IngestionService:
|
|||
except Exception: pass
|
||||
|
||||
async def create_from_text(self, markdown_content: str, filename: str, vault_root: str, folder: str = "00_Inbox") -> Dict[str, Any]:
|
||||
"""Hilfsmethode zur Erstellung einer Note aus einem Textstream (Editor-Save)."""
|
||||
"""Hilfsmethode zur Erstellung einer Note aus einem Textstream."""
|
||||
target_dir = os.path.join(vault_root, folder)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
file_path = os.path.join(target_dir, filename)
|
||||
|
|
@ -351,7 +346,6 @@ class IngestionService:
|
|||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
await asyncio.sleep(0.1)
|
||||
logger.info(f"Written file to {file_path}")
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": f"Disk write failed: {str(e)}"}
|
||||
return await self.process_file(file_path=file_path, vault_root=vault_root, apply=True, force_replace=True, purge_before=True)
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
"""
|
||||
FILE: app/routers/ingest.py
|
||||
DESCRIPTION: Endpunkte für WP-11. Nimmt Markdown entgegen.
|
||||
Refactored für WP-14: Nutzt BackgroundTasks für non-blocking Save.
|
||||
VERSION: 0.7.0 (Fix: Timeout WP-14)
|
||||
Refactored für WP-14: Nutzt BackgroundTasks für non-blocking Save.
|
||||
Update WP-20: Unterstützung für Hybrid-Cloud-Analyse Feedback.
|
||||
VERSION: 0.8.0 (WP-20 Hybrid Ready)
|
||||
STATUS: Active
|
||||
DEPENDENCIES: app.core.ingestion, app.services.discovery, fastapi, pydantic
|
||||
"""
|
||||
|
|
@ -44,6 +45,7 @@ class SaveResponse(BaseModel):
|
|||
async def run_ingestion_task(markdown_content: str, filename: str, vault_root: str, folder: str):
|
||||
"""
|
||||
Führt die Ingestion im Hintergrund aus, damit der Request nicht blockiert.
|
||||
Integrierter WP-20 Hybrid-Modus über den IngestionService.
|
||||
"""
|
||||
logger.info(f"🔄 Background Task started: Ingesting {filename}...")
|
||||
try:
|
||||
|
|
@ -80,15 +82,17 @@ async def analyze_draft(req: AnalyzeRequest):
|
|||
async def save_note(req: SaveRequest, background_tasks: BackgroundTasks):
|
||||
"""
|
||||
WP-14 Fix: Startet Ingestion im Hintergrund (Fire & Forget).
|
||||
Verhindert Timeouts bei aktiver Smart-Edge-Allocation (WP-15).
|
||||
Verhindert Timeouts bei aktiver Smart-Edge-Allocation (WP-15) und Cloud-Hybrid-Modus (WP-20).
|
||||
"""
|
||||
try:
|
||||
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
|
||||
abs_vault_root = os.path.abspath(vault_root)
|
||||
|
||||
if not os.path.exists(abs_vault_root):
|
||||
try: os.makedirs(abs_vault_root, exist_ok=True)
|
||||
except: pass
|
||||
try:
|
||||
os.makedirs(abs_vault_root, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not create vault root: {e}")
|
||||
|
||||
final_filename = req.filename or f"draft_{int(time.time())}.md"
|
||||
|
||||
|
|
@ -109,7 +113,7 @@ async def save_note(req: SaveRequest, background_tasks: BackgroundTasks):
|
|||
status="queued",
|
||||
file_path=os.path.join(req.folder, final_filename),
|
||||
note_id="pending",
|
||||
message="Speicherung & KI-Analyse im Hintergrund gestartet.",
|
||||
message="Speicherung & Hybrid-KI-Analyse (WP-20) im Hintergrund gestartet.",
|
||||
stats={
|
||||
"chunks": -1, # Indikator für Async
|
||||
"edges": -1
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"""
|
||||
FILE: app/services/llm_service.py
|
||||
DESCRIPTION: Asynchroner Client für Ollama. Verwaltet Prompts und Background-Last (Semaphore).
|
||||
VERSION: 2.8.0
|
||||
DESCRIPTION: Hybrid-Client für Ollama & Google Gemini.
|
||||
Verwaltet Prompts, Background-Last (Semaphore) und Cloud-Routing.
|
||||
VERSION: 3.1.0 (WP-20 Full Integration: Provider-Aware Prompting)
|
||||
STATUS: Active
|
||||
DEPENDENCIES: httpx, yaml, asyncio, app.config
|
||||
DEPENDENCIES: httpx, yaml, asyncio, google-generativeai, app.config
|
||||
EXTERNAL_CONFIG: config/prompts.yaml
|
||||
LAST_ANALYSIS: 2025-12-15
|
||||
"""
|
||||
|
||||
import httpx
|
||||
|
|
@ -13,47 +13,47 @@ import yaml
|
|||
import logging
|
||||
import os
|
||||
import asyncio
|
||||
import json
|
||||
import google.generativeai as genai
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, Literal
|
||||
from app.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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")
|
||||
|
||||
# NEU: Konfigurierbares Limit für Hintergrund-Last
|
||||
# Default auf 2 (konservativ), kann in .env erhöht werden.
|
||||
BACKGROUND_LIMIT = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2"))
|
||||
|
||||
def get_settings():
|
||||
return Settings()
|
||||
|
||||
class LLMService:
|
||||
# GLOBALER SEMAPHOR (Lazy Initialization)
|
||||
# Wir initialisieren ihn erst, wenn wir die Settings kennen.
|
||||
# GLOBALER SEMAPHOR für Hintergrund-Last Steuerung (WP-06 / WP-20)
|
||||
_background_semaphore = None
|
||||
|
||||
def __init__(self):
|
||||
self.settings = get_settings()
|
||||
self.prompts = self._load_prompts()
|
||||
|
||||
# Initialisiere Semaphore einmalig auf Klassen-Ebene basierend auf Config
|
||||
# Initialisiere Semaphore einmalig auf Klassen-Ebene
|
||||
if LLMService._background_semaphore is None:
|
||||
limit = self.settings.BACKGROUND_LIMIT
|
||||
limit = getattr(self.settings, "BACKGROUND_LIMIT", 2)
|
||||
logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}")
|
||||
LLMService._background_semaphore = asyncio.Semaphore(limit)
|
||||
|
||||
# Ollama Setup
|
||||
self.timeout = httpx.Timeout(self.settings.LLM_TIMEOUT, connect=10.0)
|
||||
|
||||
self.client = httpx.AsyncClient(
|
||||
self.ollama_client = httpx.AsyncClient(
|
||||
base_url=self.settings.OLLAMA_URL,
|
||||
timeout=self.timeout
|
||||
)
|
||||
|
||||
# Gemini Setup [WP-20]
|
||||
if hasattr(self.settings, "GOOGLE_API_KEY") and self.settings.GOOGLE_API_KEY:
|
||||
genai.configure(api_key=self.settings.GOOGLE_API_KEY)
|
||||
model_name = getattr(self.settings, "GEMINI_MODEL", "gemini-1.5-flash")
|
||||
self.gemini_model = genai.GenerativeModel(model_name)
|
||||
logger.info(f"✨ LLMService: Gemini Cloud Mode active ({model_name})")
|
||||
else:
|
||||
self.gemini_model = None
|
||||
logger.warning("⚠️ LLMService: No GOOGLE_API_KEY found. Gemini mode disabled.")
|
||||
|
||||
def _load_prompts(self) -> dict:
|
||||
"""Lädt die Prompt-Konfiguration aus der YAML-Datei."""
|
||||
path = Path(self.settings.PROMPTS_PATH)
|
||||
if not path.exists(): return {}
|
||||
try:
|
||||
|
|
@ -62,86 +62,124 @@ class LLMService:
|
|||
logger.error(f"Failed to load prompts: {e}")
|
||||
return {}
|
||||
|
||||
def get_prompt(self, key: str, provider: str = None) -> str:
|
||||
"""
|
||||
Wählt das Template basierend auf dem Provider aus (WP-20).
|
||||
Unterstützt sowohl flache Strings als auch Dictionary-basierte Provider-Zweige.
|
||||
"""
|
||||
active_provider = provider or getattr(self.settings, "MINDNET_LLM_PROVIDER", "ollama")
|
||||
data = self.prompts.get(key, "")
|
||||
|
||||
if isinstance(data, dict):
|
||||
# Versuche den Provider-Key, Fallback auf 'ollama'
|
||||
return data.get(active_provider, data.get("ollama", ""))
|
||||
return str(data)
|
||||
|
||||
async def generate_raw_response(
|
||||
self,
|
||||
prompt: str,
|
||||
system: str = None,
|
||||
force_json: bool = False,
|
||||
max_retries: int = 0,
|
||||
max_retries: int = 2,
|
||||
base_delay: float = 2.0,
|
||||
priority: Literal["realtime", "background"] = "realtime"
|
||||
priority: Literal["realtime", "background"] = "realtime",
|
||||
provider: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Führt einen LLM Call aus.
|
||||
priority="realtime": Chat (Sofort, keine Bremse).
|
||||
priority="background": Import/Analyse (Gedrosselt durch Semaphore).
|
||||
Führt einen LLM Call aus mit Priority-Handling und Provider-Wahl.
|
||||
"""
|
||||
# Bestimme Provider: Parameter-Override > Config-Default
|
||||
target_provider = provider or getattr(self.settings, "MINDNET_LLM_PROVIDER", "ollama")
|
||||
|
||||
use_semaphore = (priority == "background")
|
||||
|
||||
if use_semaphore and LLMService._background_semaphore:
|
||||
async with LLMService._background_semaphore:
|
||||
return await self._execute_request(prompt, system, force_json, max_retries, base_delay)
|
||||
return await self._dispatch_request(target_provider, prompt, system, force_json, max_retries, base_delay)
|
||||
else:
|
||||
# Realtime oder Fallback (falls Semaphore Init fehlschlug)
|
||||
return await self._execute_request(prompt, system, force_json, max_retries, base_delay)
|
||||
return await self._dispatch_request(target_provider, prompt, system, force_json, max_retries, base_delay)
|
||||
|
||||
async def _execute_request(self, prompt, system, force_json, max_retries, base_delay):
|
||||
async def _dispatch_request(self, provider, prompt, system, force_json, max_retries, base_delay):
|
||||
"""Routet die Anfrage an den gewählten Provider mit Fallback-Logik."""
|
||||
try:
|
||||
if provider == "gemini" and self.gemini_model:
|
||||
return await self._execute_gemini(prompt, system, force_json)
|
||||
else:
|
||||
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
|
||||
except Exception as e:
|
||||
# Automatischer Fallback auf Ollama bei Cloud-Fehlern (WP-20)
|
||||
if provider == "gemini" and getattr(self.settings, "LLM_FALLBACK_ENABLED", True):
|
||||
logger.warning(f"🔄 Gemini failed: {e}. Falling back to Ollama.")
|
||||
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
|
||||
raise e
|
||||
|
||||
async def _execute_gemini(self, prompt, system, force_json) -> str:
|
||||
"""Asynchroner Google Gemini Call (WP-20)."""
|
||||
full_prompt = f"System: {system}\n\nUser: {prompt}" if system else prompt
|
||||
|
||||
# Gemini JSON Mode Support
|
||||
gen_config = {}
|
||||
if force_json:
|
||||
gen_config["response_mime_type"] = "application/json"
|
||||
|
||||
response = await self.gemini_model.generate_content_async(
|
||||
full_prompt,
|
||||
generation_config=gen_config
|
||||
)
|
||||
return response.text.strip()
|
||||
|
||||
async def _execute_ollama(self, prompt, system, force_json, max_retries, base_delay) -> str:
|
||||
"""Ollama Call mit exponentieller Backoff-Retry-Logik."""
|
||||
payload: Dict[str, Any] = {
|
||||
"model": self.settings.LLM_MODEL,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": 0.1 if force_json else 0.7,
|
||||
"temperature": 0.1 if force_json else 0.7,
|
||||
"num_ctx": 8192
|
||||
}
|
||||
}
|
||||
|
||||
if force_json:
|
||||
payload["format"] = "json"
|
||||
|
||||
if system:
|
||||
payload["system"] = system
|
||||
if force_json: payload["format"] = "json"
|
||||
if system: payload["system"] = system
|
||||
|
||||
attempt = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
response = await self.client.post("/api/generate", json=payload)
|
||||
|
||||
response = await self.ollama_client.post("/api/generate", json=payload)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get("response", "").strip()
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json().get("response", "").strip()
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
if attempt > max_retries:
|
||||
logger.error(f"LLM Final Error (Versuch {attempt}): {e}")
|
||||
raise e
|
||||
|
||||
logger.error(f"Ollama Error after {attempt} retries: {e}")
|
||||
raise e
|
||||
# Exponentieller Backoff: base_delay * (2 ^ (attempt - 1))
|
||||
wait_time = base_delay * (2 ** (attempt - 1))
|
||||
logger.warning(f"⚠️ LLM Retry ({attempt}/{max_retries}) in {wait_time}s: {e}")
|
||||
logger.warning(f"⚠️ Ollama attempt {attempt} failed. Retrying in {wait_time}s...")
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
async def generate_rag_response(self, query: str, context_str: str) -> str:
|
||||
"""
|
||||
Chat-Wrapper: Immer Realtime.
|
||||
"""
|
||||
system_prompt = self.prompts.get("system_prompt", "")
|
||||
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
|
||||
"""Standard RAG Chat-Interface mit Provider-spezifischen Templates."""
|
||||
provider = getattr(self.settings, "MINDNET_LLM_PROVIDER", "ollama")
|
||||
|
||||
# Holen der Templates über die neue get_prompt Methode
|
||||
system_prompt = self.get_prompt("system_prompt", provider)
|
||||
rag_template = self.get_prompt("rag_template", provider)
|
||||
|
||||
# Fallback für RAG Template Struktur
|
||||
if not rag_template:
|
||||
rag_template = "{context_str}\n\n{query}"
|
||||
|
||||
final_prompt = rag_template.format(context_str=context_str, query=query)
|
||||
|
||||
return await self.generate_raw_response(
|
||||
final_prompt,
|
||||
system=system_prompt,
|
||||
max_retries=0,
|
||||
force_json=False,
|
||||
priority="realtime"
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
if self.client:
|
||||
await self.client.aclose()
|
||||
"""Schließt alle offenen HTTP-Verbindungen."""
|
||||
if self.ollama_client:
|
||||
await self.ollama_client.aclose()
|
||||
|
|
@ -1,31 +1,37 @@
|
|||
# config/decision_engine.yaml
|
||||
# Steuerung der Decision Engine (Intent Recognition & Graph Routing)
|
||||
# Version: 2.5.0 (WP-22: Semantic Graph Routing)
|
||||
# VERSION: 2.6.0 (WP-20: Hybrid LLM & WP-22: Semantic Graph Routing)
|
||||
# STATUS: Active
|
||||
|
||||
version: 2.5
|
||||
version: 2.6
|
||||
|
||||
settings:
|
||||
llm_fallback_enabled: true
|
||||
|
||||
# Strategie für den Router selbst (Welches Modell erkennt den Intent?)
|
||||
# "auto" nutzt den in MINDNET_LLM_PROVIDER gesetzten Standard.
|
||||
router_provider: "auto"
|
||||
|
||||
# Few-Shot Prompting für den LLM-Router (Slow Path)
|
||||
# Gemini 1.5 nutzt diesen Kontext für hochpräzise Intent-Erkennung.
|
||||
llm_router_prompt: |
|
||||
Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie.
|
||||
Du bist der zentrale Intent-Klassifikator für Mindnet, einen digitalen Zwilling.
|
||||
Analysiere die Nachricht und wähle die passende Strategie.
|
||||
Antworte NUR mit dem Namen der Strategie.
|
||||
|
||||
STRATEGIEN:
|
||||
- INTERVIEW: User will Wissen erfassen, Notizen anlegen oder Dinge festhalten.
|
||||
- DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich".
|
||||
- EMPATHY: Gefühle, Frust, Freude, Probleme.
|
||||
- CODING: Code, Syntax, Programmierung.
|
||||
- FACT: Wissen, Fakten, Definitionen.
|
||||
- DECISION: Rat, Strategie, Abwägung von Werten, "Soll ich tun X?".
|
||||
- EMPATHY: Gefühle, Reflexion der eigenen Verfassung, Frust, Freude.
|
||||
- CODING: Code-Erstellung, Debugging, technische Dokumentation.
|
||||
- FACT: Reine Wissensabfrage, Definitionen, Suchen von Informationen.
|
||||
|
||||
BEISPIELE:
|
||||
User: "Wie funktioniert Qdrant?" -> FACT
|
||||
User: "Soll ich Qdrant nutzen?" -> DECISION
|
||||
User: "Ich möchte etwas notieren" -> INTERVIEW
|
||||
User: "Lass uns das festhalten" -> INTERVIEW
|
||||
User: "Schreibe ein Python Script" -> CODING
|
||||
User: "Alles ist grau und sinnlos" -> EMPATHY
|
||||
User: "Wie funktioniert die Qdrant-Vektor-DB?" -> FACT
|
||||
User: "Soll ich mein Startup jetzt verkaufen?" -> DECISION
|
||||
User: "Notiere mir kurz meine Gedanken zum Meeting." -> INTERVIEW
|
||||
User: "Ich fühle mich heute sehr erschöpft." -> EMPATHY
|
||||
User: "Schreibe eine FastAPI-Route für den Ingest." -> CODING
|
||||
|
||||
NACHRICHT: "{query}"
|
||||
|
||||
|
|
@ -35,6 +41,7 @@ strategies:
|
|||
# 1. Fakten-Abfrage (Fallback & Default)
|
||||
FACT:
|
||||
description: "Reine Wissensabfrage."
|
||||
preferred_provider: "ollama" # Schnell und lokal ausreichend
|
||||
trigger_keywords: []
|
||||
inject_types: []
|
||||
# WP-22: Definitionen & Hierarchien bevorzugen
|
||||
|
|
@ -46,9 +53,10 @@ strategies:
|
|||
prompt_template: "rag_template"
|
||||
prepend_instruction: null
|
||||
|
||||
# 2. Entscheidungs-Frage
|
||||
# 2. Entscheidungs-Frage (Power-Strategie)
|
||||
DECISION:
|
||||
description: "Der User sucht Rat, Strategie oder Abwägung."
|
||||
preferred_provider: "gemini" # Nutzt Gemini's Reasoning-Power für WP-20
|
||||
trigger_keywords:
|
||||
- "soll ich"
|
||||
- "meinung"
|
||||
|
|
@ -68,12 +76,13 @@ strategies:
|
|||
impacts: 2.0 # NEU: Zeige mir alles, was von dieser Entscheidung betroffen ist!
|
||||
prompt_template: "decision_template"
|
||||
prepend_instruction: |
|
||||
!!! ENTSCHEIDUNGS-MODUS !!!
|
||||
!!! ENTSCHEIDUNGS-MODUS (HYBRID AI) !!!
|
||||
BITTE WÄGE FAKTEN GEGEN FOLGENDE WERTE, PRINZIPIEN UND ZIELE AB:
|
||||
|
||||
# 3. Empathie / "Ich"-Modus
|
||||
# 3. Empathie / "Ich"-Modus (Privacy-Fokus)
|
||||
EMPATHY:
|
||||
description: "Reaktion auf emotionale Zustände."
|
||||
preferred_provider: "ollama" # Private Emotionen bleiben lokal!
|
||||
trigger_keywords:
|
||||
- "ich fühle"
|
||||
- "traurig"
|
||||
|
|
@ -96,6 +105,7 @@ strategies:
|
|||
# 4. Coding / Technical
|
||||
CODING:
|
||||
description: "Technische Anfragen und Programmierung."
|
||||
preferred_provider: "gemini" # Höheres Weltwissen für moderne Libraries
|
||||
trigger_keywords:
|
||||
- "code"
|
||||
- "python"
|
||||
|
|
@ -116,10 +126,9 @@ strategies:
|
|||
prepend_instruction: null
|
||||
|
||||
# 5. Interview / Datenerfassung
|
||||
# HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch
|
||||
# über die types.yaml erkannt. Hier stehen nur generische Trigger.
|
||||
INTERVIEW:
|
||||
description: "Der User möchte Wissen erfassen."
|
||||
preferred_provider: "ollama" # Lokale Erfassung ist performant genug
|
||||
trigger_keywords:
|
||||
- "neue notiz"
|
||||
- "etwas notieren"
|
||||
|
|
@ -135,8 +144,7 @@ strategies:
|
|||
edge_boosts: {}
|
||||
prompt_template: "interview_template"
|
||||
prepend_instruction: null
|
||||
# Schemas: Hier nur der Fallback.
|
||||
# Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml!
|
||||
# Schemas kommen aus types.yaml (WP-22)
|
||||
schemas:
|
||||
default:
|
||||
fields:
|
||||
|
|
|
|||
|
|
@ -15,148 +15,167 @@ system_prompt: |
|
|||
# ---------------------------------------------------------
|
||||
# 1. STANDARD: Fakten & Wissen (Intent: FACT)
|
||||
# ---------------------------------------------------------
|
||||
rag_template: |
|
||||
QUELLEN (WISSEN):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
rag_template:
|
||||
ollama: |
|
||||
QUELLEN (WISSEN):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
|
||||
FRAGE:
|
||||
{query}
|
||||
FRAGE:
|
||||
{query}
|
||||
|
||||
ANWEISUNG:
|
||||
Beantworte die Frage präzise basierend auf den Quellen.
|
||||
Fasse die Informationen zusammen. Sei objektiv und neutral.
|
||||
ANWEISUNG:
|
||||
Beantworte die Frage präzise basierend auf den Quellen.
|
||||
Fasse die Informationen zusammen. Sei objektiv und neutral.
|
||||
gemini: |
|
||||
Analysiere diesen Kontext meines digitalen Zwillings:
|
||||
{context_str}
|
||||
Beantworte die Anfrage detailliert und prüfe auf Widersprüche: {query}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 2. DECISION: Strategie & Abwägung (Intent: DECISION)
|
||||
# ---------------------------------------------------------
|
||||
decision_template: |
|
||||
KONTEXT (FAKTEN & STRATEGIE):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
decision_template:
|
||||
ollama: |
|
||||
KONTEXT (FAKTEN & STRATEGIE):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
|
||||
ENTSCHEIDUNGSFRAGE:
|
||||
{query}
|
||||
ENTSCHEIDUNGSFRAGE:
|
||||
{query}
|
||||
|
||||
ANWEISUNG:
|
||||
Du agierst als mein Entscheidungs-Partner.
|
||||
1. Analysiere die Faktenlage aus den Quellen.
|
||||
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?
|
||||
|
||||
FORMAT:
|
||||
- **Analyse:** (Kurze Zusammenfassung der Fakten)
|
||||
- **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!)
|
||||
- **Empfehlung:** (Klare Meinung: Ja/Nein/Vielleicht mit Begründung)
|
||||
ANWEISUNG:
|
||||
Du agierst als mein Entscheidungs-Partner.
|
||||
1. Analysiere die Faktenlage aus den Quellen.
|
||||
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?
|
||||
|
||||
FORMAT:
|
||||
- **Analyse:** (Kurze Zusammenfassung der Fakten)
|
||||
- **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!)
|
||||
- **Empfehlung:** (Klare Meinung: Ja/Nein/Vielleicht mit Begründung)
|
||||
gemini: |
|
||||
Agierte als Senior Strategy Consultant. Nutze den Kontext {context_str}, um die Frage {query}
|
||||
tiefgreifend gegen meine langfristigen Ziele abzuwägen.
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY)
|
||||
# ---------------------------------------------------------
|
||||
empathy_template: |
|
||||
KONTEXT (ERFAHRUNGEN & GLAUBENSSÄTZE):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
empathy_template:
|
||||
ollama: |
|
||||
KONTEXT (ERFAHRUNGEN & GLAUBENSSÄTZE):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
|
||||
SITUATION:
|
||||
{query}
|
||||
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.
|
||||
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}
|
||||
=========================================
|
||||
technical_template:
|
||||
ollama: |
|
||||
KONTEXT (DOCS & SNIPPETS):
|
||||
=========================================
|
||||
{context_str}
|
||||
=========================================
|
||||
|
||||
TASK:
|
||||
{query}
|
||||
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.
|
||||
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.
|
||||
# ---------------------------------------------------------
|
||||
# 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode)
|
||||
# ---------------------------------------------------------
|
||||
interview_template: |
|
||||
TASK:
|
||||
Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'.
|
||||
interview_template:
|
||||
ollama: |
|
||||
TASK:
|
||||
Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'.
|
||||
|
||||
STRUKTUR (Nutze EXAKT diese Überschriften):
|
||||
{schema_fields}
|
||||
|
||||
USER INPUT:
|
||||
"{query}"
|
||||
|
||||
ANWEISUNG ZUM INHALT:
|
||||
1. Analysiere den Input genau.
|
||||
2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben.
|
||||
3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei.
|
||||
4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu.
|
||||
|
||||
OUTPUT FORMAT (YAML + MARKDOWN):
|
||||
---
|
||||
type: {target_type}
|
||||
status: draft
|
||||
title: (Erstelle einen treffenden, kurzen Titel für den Inhalt)
|
||||
tags: [Tag1, Tag2]
|
||||
---
|
||||
|
||||
# (Wiederhole den Titel hier)
|
||||
|
||||
## (Erster Begriff aus STRUKTUR)
|
||||
(Text...)
|
||||
|
||||
## (Zweiter Begriff aus STRUKTUR)
|
||||
(Text...)
|
||||
|
||||
(usw.)
|
||||
|
||||
STRUKTUR (Nutze EXAKT diese Überschriften):
|
||||
{schema_fields}
|
||||
|
||||
USER INPUT:
|
||||
"{query}"
|
||||
|
||||
ANWEISUNG ZUM INHALT:
|
||||
1. Analysiere den Input genau.
|
||||
2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben.
|
||||
3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei.
|
||||
4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu.
|
||||
|
||||
OUTPUT FORMAT (YAML + MARKDOWN):
|
||||
---
|
||||
type: {target_type}
|
||||
status: draft
|
||||
title: (Erstelle einen treffenden, kurzen Titel für den Inhalt)
|
||||
tags: [Tag1, Tag2]
|
||||
---
|
||||
|
||||
# (Wiederhole den Titel hier)
|
||||
|
||||
## (Erster Begriff aus STRUKTUR)
|
||||
(Text...)
|
||||
|
||||
## (Zweiter Begriff aus STRUKTUR)
|
||||
(Text...)
|
||||
|
||||
(usw.)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER)
|
||||
# ---------------------------------------------------------
|
||||
edge_allocation_template: |
|
||||
TASK:
|
||||
Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings).
|
||||
Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind.
|
||||
edge_allocation_template:
|
||||
ollama: |
|
||||
TASK:
|
||||
Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings).
|
||||
Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind.
|
||||
|
||||
TEXTABSCHNITT:
|
||||
"""
|
||||
{chunk_text}
|
||||
"""
|
||||
TEXTABSCHNITT:
|
||||
"""
|
||||
{chunk_text}
|
||||
"""
|
||||
|
||||
KANDIDATEN (Auswahl-Pool):
|
||||
{edge_list}
|
||||
KANDIDATEN (Auswahl-Pool):
|
||||
{edge_list}
|
||||
|
||||
REGELN:
|
||||
1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein (z.B. uses, blocks, inspired_by, loves, etc.).
|
||||
2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen.
|
||||
3. Erfinde KEINE neuen Kanten. Nutze exakt die Schreibweise aus der Liste.
|
||||
4. Antworte als flache JSON-Liste.
|
||||
REGELN:
|
||||
1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein (z.B. uses, blocks, inspired_by, loves, etc.).
|
||||
2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen.
|
||||
3. Erfinde KEINE neuen Kanten. Nutze exakt die Schreibweise aus der Liste.
|
||||
4. Antworte als flache JSON-Liste.
|
||||
|
||||
BEISPIEL (Zur Demonstration der Logik):
|
||||
Input Text: "Das Projekt Alpha scheitert, weil Budget fehlt."
|
||||
Input Kandidaten: ["blocks:Projekt Alpha", "inspired_by:Buch der Weisen", "needs:Budget"]
|
||||
Output: ["blocks:Projekt Alpha", "needs:Budget"]
|
||||
BEISPIEL (Zur Demonstration der Logik):
|
||||
Input Text: "Das Projekt Alpha scheitert, weil Budget fehlt."
|
||||
Input Kandidaten: ["blocks:Projekt Alpha", "inspired_by:Buch der Weisen", "needs:Budget"]
|
||||
Output: ["blocks:Projekt Alpha", "needs:Budget"]
|
||||
|
||||
DEIN OUTPUT (JSON):
|
||||
DEIN OUTPUT (JSON):
|
||||
|
||||
gemini: |
|
||||
Extrahiere semantische Kanten für den Graphen ({note_id}).
|
||||
Finde auch implizite Verbindungen.
|
||||
JSON: [{"to": "X", "kind": "Y", "reason": "Z"}].
|
||||
TEXT: {text}
|
||||
|
|
@ -34,4 +34,7 @@ streamlit>=1.39.0
|
|||
|
||||
# Visualization (Parallelbetrieb)
|
||||
streamlit-agraph>=0.0.45
|
||||
st-cytoscape
|
||||
st-cytoscape
|
||||
|
||||
# Google gemini API
|
||||
google-generativeai>=0.8.3
|
||||
Loading…
Reference in New Issue
Block a user