This commit is contained in:
Lars 2025-12-11 08:18:41 +01:00
parent d2ee48555a
commit a7cda3f51c
3 changed files with 102 additions and 63 deletions

View File

@ -268,35 +268,38 @@ class IngestionService:
markdown_content: str, markdown_content: str,
filename: str, filename: str,
vault_root: str, vault_root: str,
folder: str = "Inbox" # Standard-Ordner für neue Files folder: str = "00_Inbox"
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
WP-11: Schreibt Text in eine physische Datei und indiziert sie sofort. WP-11 Persistence: Schreibt Text sicher und indiziert ihn.
Erstellt Verzeichnisse automatisch.
""" """
# 1. Pfad vorbereiten # 1. Zielordner vorbereiten
target_dir = os.path.join(vault_root, folder) target_dir = os.path.join(vault_root, folder)
os.makedirs(target_dir, exist_ok=True) try:
os.makedirs(target_dir, exist_ok=True)
except Exception as e:
return {"status": "error", "error": f"Could not create folder {target_dir}: {e}"}
# Dateiname bereinigen (Sicherheit) # 2. Dateiname bereinigen
safe_filename = os.path.basename(filename) safe_filename = os.path.basename(filename)
if not safe_filename.endswith(".md"): if not safe_filename.endswith(".md"):
safe_filename += ".md" safe_filename += ".md"
file_path = os.path.join(target_dir, safe_filename) file_path = os.path.join(target_dir, safe_filename)
# 2. Schreiben (Write to Disk - Single Source of Truth) # 3. Schreiben
try: try:
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
f.write(markdown_content) f.write(markdown_content)
except Exception as e: except Exception as e:
return {"status": "error", "error": f"Disk write failed: {str(e)}"} return {"status": "error", "error": f"Disk write failed at {file_path}: {str(e)}"}
# 3. Indizieren (Ingest) # 4. Indizieren (Single File Upsert)
# Wir rufen einfach die existierende Logik auf!
return self.process_file( return self.process_file(
file_path=file_path, file_path=file_path,
vault_root=vault_root, vault_root=vault_root,
apply=True, # Sofort schreiben apply=True,
force_replace=True, # Da neu, erzwingen wir Update force_replace=True,
purge_before=True # Sauberer Start purge_before=True
) )

View File

@ -1,9 +1,11 @@
""" """
app/routers/ingest.py app/routers/ingest.py
API-Endpunkte für WP-11 (Discovery & Persistence). API-Endpunkte für WP-11 (Discovery & Persistence).
Robustified for Frontend Integration.
""" """
import os import os
import time import time
import logging
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@ -11,9 +13,12 @@ from typing import Optional, List, Dict, Any
from app.core.ingestion import IngestionService from app.core.ingestion import IngestionService
from app.services.discovery import DiscoveryService from app.services.discovery import DiscoveryService
# Logger für Backend-Debugging aktivieren
logger = logging.getLogger("uvicorn.error")
router = APIRouter() router = APIRouter()
# --- DTOs --- # --- DTOs (Data Transfer Objects) ---
class AnalyzeRequest(BaseModel): class AnalyzeRequest(BaseModel):
text: str text: str
@ -21,8 +26,8 @@ class AnalyzeRequest(BaseModel):
class SaveRequest(BaseModel): class SaveRequest(BaseModel):
markdown_content: str markdown_content: str
filename: Optional[str] = None # Optional, fallback auf Timestamp filename: Optional[str] = None
folder: str = "00_Inbox" # Zielordner folder: str = "00_Inbox" # Standard-Ordner
class SaveResponse(BaseModel): class SaveResponse(BaseModel):
status: str status: str
@ -36,54 +41,66 @@ discovery_service = DiscoveryService()
@router.post("/analyze") @router.post("/analyze")
async def analyze_draft(req: AnalyzeRequest): async def analyze_draft(req: AnalyzeRequest):
""" """
WP-11 Intelligence: Analysiert einen Entwurf und liefert Link-Vorschläge. WP-11 Intelligence: Liefert Link-Vorschläge basierend auf Text und Typ.
""" """
try: try:
# Prio 2: Intelligence Service aufrufen
result = await discovery_service.analyze_draft(req.text, req.type) result = await discovery_service.analyze_draft(req.text, req.type)
return result return result
except Exception as e: except Exception as e:
logger.error(f"Analyze failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}") raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
@router.post("/save", response_model=SaveResponse) @router.post("/save", response_model=SaveResponse)
async def save_note(req: SaveRequest): async def save_note(req: SaveRequest):
""" """
WP-11 Persistence: Speichert Markdown physisch und indiziert es in Qdrant. WP-11 Persistence: Speichert Markdown physisch und indiziert es sofort.
""" """
# 1. Vault Root ermitteln try:
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault") # 1. Vault Root sicher ermitteln
if not os.path.exists(vault_root): vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
# Fallback relative paths
if os.path.exists("vault"):
vault_root = "vault"
elif os.path.exists("../vault"):
vault_root = "../vault"
else:
raise HTTPException(status_code=500, detail="Vault root not configured or missing")
# 2. Filename generieren falls fehlend
final_filename = req.filename
if not final_filename:
final_filename = f"draft_{int(time.time())}.md"
# 3. Ingestion Service nutzen
ingest_service = IngestionService()
result = ingest_service.create_from_text(
markdown_content=req.markdown_content,
filename=final_filename,
vault_root=os.path.abspath(vault_root),
folder=req.folder
)
if result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error"))
return SaveResponse( # Absolute Pfade auflösen, um CWD-Probleme zu vermeiden
status="success", abs_vault_root = os.path.abspath(vault_root)
file_path=result["path"],
note_id=result.get("note_id", "unknown"), if not os.path.exists(abs_vault_root):
stats={ error_msg = f"Vault root not found at: {abs_vault_root}. Check MINDNET_VAULT_ROOT in .env"
"chunks": result.get("chunks_count", 0), logger.error(error_msg)
"edges": result.get("edges_count", 0) raise HTTPException(status_code=500, detail=error_msg)
}
) # 2. Filename Fallback
final_filename = req.filename
if not final_filename:
final_filename = f"draft_{int(time.time())}.md"
# 3. Ingestion Service aufrufen
ingest_service = IngestionService()
logger.info(f"Attempting to save {final_filename} to {req.folder} in {abs_vault_root}")
result = ingest_service.create_from_text(
markdown_content=req.markdown_content,
filename=final_filename,
vault_root=abs_vault_root,
folder=req.folder
)
# Fehler vom Service abfangen
if result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error"))
return SaveResponse(
status="success",
file_path=result["path"],
note_id=result.get("note_id", "unknown"),
stats={
"chunks": result.get("chunks_count", 0),
"edges": result.get("edges_count", 0)
}
)
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"Save failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}")

View File

@ -102,9 +102,12 @@ class DiscoveryService:
path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
if not os.path.exists(path): if not os.path.exists(path):
# Fallback relative Pfade # Fallback relative Pfade
if os.path.exists("types.yaml"): path = "types.yaml" if os.path.exists("types.yaml"):
elif os.path.exists("../config/types.yaml"): path = "../config/types.yaml" path = "types.yaml"
else: return {} elif os.path.exists("../config/types.yaml"):
path = "../config/types.yaml"
else:
return {}
try: try:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
@ -132,7 +135,7 @@ class DiscoveryService:
# 3. Fallback, falls nichts konfiguriert ist # 3. Fallback, falls nichts konfiguriert ist
return "related_to" return "related_to"
# --- Core Logic (Unverändert) --- # --- Core Logic ---
def _fetch_all_titles_and_aliases(self) -> List[Dict]: def _fetch_all_titles_and_aliases(self) -> List[Dict]:
notes = [] notes = []
@ -150,15 +153,19 @@ class DiscoveryService:
) )
for point in res: for point in res:
pl = point.payload or {} pl = point.payload or {}
# Aliases robust lesen (kann Liste oder String sein)
aliases = pl.get("aliases") or [] aliases = pl.get("aliases") or []
if isinstance(aliases, str): aliases = [aliases] if isinstance(aliases, str):
aliases = [aliases]
notes.append({ notes.append({
"id": pl.get("note_id"), "id": pl.get("note_id"),
"title": pl.get("title"), "title": pl.get("title"),
"aliases": aliases "aliases": aliases
}) })
if next_page is None: break if next_page is None:
break
except Exception as e: except Exception as e:
logger.error(f"Error fetching titles: {e}") logger.error(f"Error fetching titles: {e}")
return [] return []
@ -168,14 +175,25 @@ class DiscoveryService:
found = [] found = []
text_lower = text.lower() text_lower = text.lower()
for entity in entities: for entity in entities:
# 1. Title Match
title = entity.get("title") title = entity.get("title")
if title and title.lower() in text_lower: if title and title.lower() in text_lower:
found.append({"match": title, "title": title, "id": entity["id"]}) found.append({
"match": title,
"title": title,
"id": entity["id"]
})
continue continue
# 2. Alias Match
aliases = entity.get("aliases", []) aliases = entity.get("aliases", [])
for alias in aliases: for alias in aliases:
if alias and str(alias).lower() in text_lower: if alias and str(alias).lower() in text_lower:
found.append({"match": alias, "title": title, "id": entity["id"]}) found.append({
"match": alias,
"title": title,
"id": entity["id"]
})
break break
return found return found
@ -184,5 +202,6 @@ class DiscoveryService:
try: try:
res = hybrid_retrieve(req) res = hybrid_retrieve(req)
return res.results return res.results
except Exception: except Exception as e:
logger.error(f"Semantic suggestion error: {e}")
return [] return []