feat(maker): AI parsing schede tecniche PDF via OpenRouter

- Nuovo servizio ai_service.py: estrae testo da PDF (pdfplumber) + analisi AI (OpenRouter)
- Endpoint POST /api/recipes/{id}/parse-technical-sheet con validazione file
- UI: bottone "Importa da PDF" nel task editor con modale upload + preview editabile
- Task suggeriti modificabili/rimovibili prima della creazione bulk
- Config: OPENROUTER_API_KEY e OPENROUTER_MODEL in .env
- Dipendenze: pdfplumber + httpx aggiunti a server deps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-24 11:01:00 +00:00
parent 9bd605c958
commit d2bf0b5828
10 changed files with 539 additions and 10 deletions
+74
View File
@@ -0,0 +1,74 @@
"""AI service for parsing technical sheets via OpenRouter."""
import json
import logging
from io import BytesIO
import httpx
import pdfplumber
from src.backend.config import settings
logger = logging.getLogger(__name__)
SYSTEM_PROMPT = """Sei un assistente specializzato nell'analisi di schede tecniche industriali.
Analizza il testo estratto da una scheda tecnica e identifica i blocchi operativi distinti.
Per ogni blocco, estrai:
- title: titolo breve del blocco (es. "MATERIALI", "TEMPERATURA", "MISURA", "IMBALLO")
- directive: istruzione operativa principale (es. "VERIFICARE ATTREZZATURE")
- description: dettagli completi del blocco, preservando gli "a capo" originali
Rispondi SOLO con un array JSON valido, senza markdown o testo aggiuntivo.
Esempio:
[
{"title": "MATERIALI", "directive": "Verificare materiali", "description": "RIGIDO (36): AFT9/UV CRI 7\\nMORBIDO (E38): M70"},
{"title": "TEMPERATURA", "directive": "Verificare impostazioni", "description": "RIGIDO: TEMP: 170/170/170/170/170\\nMORBIDO: TEMP: 130-135-140-145"}
]"""
def extract_text_from_pdf(pdf_bytes: bytes) -> str:
with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
pages = []
for page in pdf.pages:
text = page.extract_text()
if text:
pages.append(text)
return "\n\n---\n\n".join(pages)
async def parse_technical_sheet(pdf_bytes: bytes) -> list[dict]:
text = extract_text_from_pdf(pdf_bytes)
if not text.strip():
return []
if not settings.openrouter_api_key:
raise ValueError("OPENROUTER_API_KEY non configurata")
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {settings.openrouter_api_key}",
"Content-Type": "application/json",
},
json={
"model": settings.openrouter_model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Analizza questa scheda tecnica ed estrai i task:\n\n{text}"},
],
"temperature": 0.1,
},
)
resp.raise_for_status()
data = resp.json()
content = data["choices"][0]["message"]["content"].strip()
# Strip markdown fences if present
if content.startswith("```"):
content = content.split("\n", 1)[1]
if content.endswith("```"):
content = content[:-3]
content = content.strip()
return json.loads(content)