#!/usr/bin/env python3 """ Landvex Extraktor — LLM-baserad schemastyrd extraktion Dokument in, strukturerade fakta ut. """ import json import os from pathlib import Path from typing import List, Dict, Optional from datetime import datetime from openai import AsyncOpenAI # Schema för extraktion (från lvx-objekt-schema.json) EXTRAKTIONS_SCHEMA = { "objektklass": { "required": ["lvx_id", "posttyp", "namn", "doman", "proveniens"], "fields": [ "lvx_id", "posttyp", "slug", "namn", "doman", "parent", "tier", "identitet", "teknik", "historik", "geografi", "standarder", "ai", "problem", "dokumentation", "relationer", "proveniens" ] }, "produktmodell": { "required": ["lvx_id", "posttyp", "namn", "doman", "parent", "proveniens"], "fields": [ "lvx_id", "posttyp", "slug", "namn", "doman", "parent", "tier", "identitet", "teknik", "historik", "geografi", "standarder", "ai", "problem", "dokumentation", "relationer", "proveniens" ] } } EXTRAKTIONS_PROMPT = """Du är Landvex Extraktor — en noggrann forskarassistent specialiserad på infrastrukturobjekt. DIN UPPGIFT: Analysera följande dokument och extrahera strukturerade fakta om infrastrukturobjekt. REGLER: 1. Extrahera ENDAST fakta som finns i dokumentet — uppfinn inget 2. Varje faktum måste ha en källa (URL eller dokumentreferens) 3. Använd Landvex-schemat strikt 4. Om något är osäkert, markera med låg konfidens 5. Problem/Kända fel kräver alltid källa OUTPUT: JSON enligt följande struktur: { "kandidater": [ { "atgard": "ny" | "uppdatera", "posttyp": "objektklass" | "produktmodell", "post": { ... }, "patch": { ... }, "verifieringsniva_forslag": "obekraftad" | "kallbelagd" | "faltverifierad" | "tillverkarbekraftad", "kallor": [ {"titel": "...", "url": "...", "hamtad": "2026-07-05"} ] } ] } DOKUMENT: {content} """ class LandvexExtraktor: def __init__(self, api_key: Optional[str] = None): self.client = AsyncOpenAI(api_key=api_key or os.getenv("OPENAI_API_KEY")) self.model = "gpt-4o-mini" # Kostnadseffektiv för volym async def extrahera(self, dokument: str, url: str, doman: str) -> dict: """Extrahera strukturerade fakta från ett dokument.""" prompt = EXTRAKTIONS_PROMPT.format(content=dokument[:15000]) # Begränsa längd try: response = await self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": "Du är Landvex Extraktor. Extrahera strukturerade fakta om infrastrukturobjekt."}, {"role": "user", "content": prompt} ], response_format={"type": "json_object"}, temperature=0.1, # Låg för determinism ) resultat = json.loads(response.choices[0].message.content) # Lägg till proveniens for kand in resultat.get("kandidater", []): kand.setdefault("kallor", []).append({ "titel": f"Extraherat från {url}", "url": url, "hamtad": datetime.utcnow().isoformat() }) return resultat except Exception as e: print(f" ⚠️ Extraktionsfel: {e}") return {"kandidater": [], "fel": str(e)} async def extrahera_batch(self, dokument: List[dict]) -> List[dict]: """Extrahera från flera dokument parallellt.""" import asyncio uppgifter = [ self.extrahera(d["html"], d["url"], d.get("doman", "TRP")) for d in dokument ] resultat = await asyncio.gather(*uppgifter, return_exceptions=True) return [r for r in resultat if not isinstance(r, Exception)] async def main(): """Demo: extrahera från testdokument.""" extraktor = LandvexExtraktor() test_doc = """ ABB Kabeldon CDC-LT är ett lågspänningskabelskåp för utomhusbruk. Material: Varmförzinkad stålplåt, IP34D enligt SS-EN 60529. Standard: SS-EN 61439-5 (provnings- och konstruktionskrav). Tillverkare: ABB Kabeldon, Alingsås. Lås: Trekantslås standard, cylinderlås som tillval. """ resultat = await extraktor.extrahera(test_doc, "https://example.com/test", "ELN") print(json.dumps(resultat, ensure_ascii=False, indent=2)) if __name__ == "__main__": import asyncio asyncio.run(main())