Files
llm-wiki-web/backend/ingest.py
T

205 lines
5.9 KiB
Python

"""
Ingest pipeline - due fasi: analisi + generazione
"""
import json
import re
from pathlib import Path
from typing import List, Dict, Optional
from . import llm, wiki_ops
import config
SYSTEM_PROMPT_ANALYSIS = """Sei un bibliotecario esperto. Analizza il documento fornito e estrai informazioni strutturate.
Rispondi SOLO con JSON valido in questo formato:
{
"entities": ["nome persona/organizzazione/strumento"],
"concepts": ["concetto/tecnica/metodo"],
"claims": ["affermazione importante trovata"],
"connections": ["riferimento a [[pagina-wiki]] esistente"],
"conflicts": ["affermazione che contraddice qualcosa di esistente"]
}
Non inventare informazioni. Solo cio che e esplicitamente nel documento."""
SYSTEM_PROMPT_GENERATION = """Sei un wiki maintainer. Genera pagine markdown per una wiki basata sull'analisi fornita.
Regole:
- Una pagina per ogni entity e concept
- Usa il formato:
# Nome Pagina
> Una frase che riassume.
## Tags
`entity` o `concept`
## Body
Contenuto dettagliato...
## Related
- [[pagina-correlata]]
## Sources
- [nome source](path)
- Per cross-references usa [[nome-pagina]]
- Mantieni i nomi delle pagine in minuscolo con trattini
-rispondi con JSON: {"pages": [{"name": "nome", "category": "entities|concepts", "content": "contenuto markdown"}]}"""
def chunk_text(text: str, max_chars: int = 3000) -> List[str]:
"""Suddivide testo in chunks se troppo lungo per Gemma."""
if len(text) <= max_chars:
return [text]
chunks = []
paragraphs = text.split("\n\n")
current_chunk = ""
for para in paragraphs:
if len(current_chunk) + len(para) <= max_chars:
current_chunk += "\n\n" + para
else:
if current_chunk:
chunks.append(current_chunk.strip())
current_chunk = para
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
async def run_analysis(source_path: Path) -> Dict:
"""Fase 1: analizza il source e ritorna struttura."""
content = source_path.read_text(encoding="utf-8")
prompt = f"{SYSTEM_PROMPT_ANALYSIS}\n\n## Documento:\n{content}"
try:
response = await llm.llm_call(prompt)
# Estrai JSON dalla risposta
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if json_match:
return json.loads(json_match.group())
except Exception as e:
return {"error": str(e), "entities": [], "concepts": [], "claims": [], "connections": [], "conflicts": []}
return {"entities": [], "concepts": [], "claims": [], "connections": [], "conflicts": []}
async def run_generation(source_path: Path, analysis: Dict) -> List[Dict]:
"""Fase 2: genera pagine wiki dall'analisi."""
content = source_path.read_text(encoding="utf-8")
source_name = source_path.stem
prompt = f"""{SYSTEM_PROMPT_GENERATION}
## Source file: {source_path.name}
## Analysis: {json.dumps(analysis, ensure_ascii=False)}
## Source content (per reference):
{content[:2000]}...
"""
try:
response = await llm.llm_call(prompt)
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if json_match:
data = json.loads(json_match.group())
return data.get("pages", [])
except Exception:
pass
return []
async def ingest_source(source_path: Path) -> Dict:
"""Pipeline completa: analisi -> generazione -> scrittura file."""
result = {
"source": str(source_path),
"analysis": None,
"pages_created": [],
"pages_updated": [],
"error": None
}
# Fase 1: Analisi
analysis = await run_analysis(source_path)
result["analysis"] = analysis
if "error" in analysis:
result["error"] = analysis["error"]
return result
# Fase 2: Generazione
pages = await run_generation(source_path, analysis)
result["pages_created"] = [p["name"] for p in pages]
# Scrittura file
wiki_root = config.WIKI_ROOT
source_category = "sources"
created_pages = []
for page in pages:
page_name = page["name"].lower().replace(" ", "-")
category = page.get("category", "concepts")
page_content = page["content"]
category_dir = wiki_root / "wiki" / category
page_path = category_dir / f"{page_name}.md"
wiki_ops.write_page(page_path, page_content)
created_pages.append(page_name)
# Aggiorna index
if created_pages:
wiki_ops.update_index(wiki_root, created_pages, category)
# Append log
wiki_ops.append_log(wiki_root, source_path.name, created_pages, [])
return result
async def ingest_streaming(source_path: Path):
"""Pipeline con streaming per feedback in tempo reale."""
wiki_root = config.WIKI_ROOT
yield f"data: Inizio ingest di {source_path.name}\n\n"
# Fase 1: Analisi
yield f"data: [1/2] Analisi in corso...\n\n"
analysis = await run_analysis(source_path)
yield f"data: Analisi completata. Trovati: {len(analysis.get('entities', []))} entities, {len(analysis.get('concepts', []))} concepts\n\n"
if "error" in analysis:
yield f"data: ERROR: {analysis['error']}\n\n"
return
# Fase 2: Generazione
yield f"data: [2/2] Generazione pagine wiki...\n\n"
pages = await run_generation(source_path, analysis)
created = []
for page in pages:
page_name = page["name"].lower().replace(" ", "-")
category = page.get("category", "concepts")
page_content = page["content"]
category_dir = wiki_root / "wiki" / category
page_path = category_dir / f"{page_name}.md"
wiki_ops.write_page(page_path, page_content)
created.append(page_name)
yield f"data: Creata: [[{page_name}]]\n\n"
# Aggiorna index e log
if created:
wiki_ops.update_index(wiki_root, created, "concepts")
wiki_ops.append_log(wiki_root, source_path.name, created, [])
yield f"data: DONE: {len(created)} pagine create\n\n"