6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
246 lines
7.7 KiB
Python
246 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Landvex Artikelproduktion med RIKTIGT LLM-anrop
|
|
Använder Anthropic Claude API
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import anthropic
|
|
|
|
# API-konfiguration
|
|
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
|
|
DEFAULT_MODEL = "claude-sonnet-5"
|
|
|
|
|
|
def log(msg):
|
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
print(f"[{ts}] {msg}", flush=True)
|
|
|
|
|
|
def ladda_prompt(sokvag):
|
|
"""Ladda systemprompt från fil."""
|
|
if not Path(sokvag).exists():
|
|
return "Du är en expert på infrastruktur och teknisk dokumentation."
|
|
with open(sokvag, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def generera_artikel(amne, doman, system_prompt, webb_data=None):
|
|
"""
|
|
Generera en komplett artikel med Anthropic Claude.
|
|
|
|
Args:
|
|
amne: Objektets namn (t.ex. "Avstängningsventil")
|
|
doman: Domänkod (t.ex. "VAT")
|
|
system_prompt: Systemprompt från fil
|
|
webb_data: Resultat från webbsökning (valfritt)
|
|
|
|
Returns:
|
|
Dict med komplett artikeldata
|
|
"""
|
|
if not ANTHROPIC_API_KEY:
|
|
raise ValueError("ANTHROPIC_API_KEY måste sättas")
|
|
|
|
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
|
|
|
|
# Bygg användarprompt med webbdata om tillgängligt
|
|
webb_kontext = ""
|
|
if webb_data:
|
|
webb_kontext = "\n\n## Webbresearch:\n"
|
|
for i, result in enumerate(webb_data[:3], 1):
|
|
webb_kontext += f"\n### Källa {i}: {result.get('title', 'Okänd')}\n"
|
|
webb_kontext += f"URL: {result.get('url', 'N/A')}\n"
|
|
content = result.get('content', result.get('snippet', ''))
|
|
webb_kontext += f"Innehåll: {content[:800]}...\n"
|
|
|
|
user_prompt = f"""Generera en komplett artikel om följande infrastrukturobjekt:
|
|
|
|
Objekt: {amne}
|
|
Domän: {doman}
|
|
{webb_kontext}
|
|
|
|
Returnera SVARET SOM JSON i följande format:
|
|
{{
|
|
"slug": "url-vanlig-slug-pa-engelska",
|
|
"namn": {{
|
|
"sv": "Svenskt namn",
|
|
"en": "Engelskt namn"
|
|
}},
|
|
"beskrivning": "Detaljerad beskrivning av objektet, dess funktion och användningsområden (minst 300 ord på svenska)",
|
|
"teknik": {{
|
|
"material": ["Material som används"],
|
|
"dimensioner": "Typiska dimensioner",
|
|
"standarder": ["Tillämpliga standarder och normer"],
|
|
"livslangd": "Förväntad livslängd"
|
|
}},
|
|
"anvandningsomraden": ["Lista över användningsområden"],
|
|
"relaterade_objekt": ["Lista över relaterade infrastrukturobjekt"],
|
|
"bilder": [
|
|
{{
|
|
"beskrivning": "Beskrivning av vad bilden ska visa",
|
|
"alt_text": "Alt-text för tillgänglighet"
|
|
}}
|
|
],
|
|
"kallor": [
|
|
{{
|
|
"titel": "Källans titel",
|
|
"url": "https://example.com",
|
|
"typ": "webbsida|standard|dokument"
|
|
}}
|
|
]
|
|
}}
|
|
|
|
VIKTIGT:
|
|
1. Returnera ENDAST JSON, ingen annan text före eller efter.
|
|
2. Beskrivningen ska vara på SVENSKA och minst 300 ord.
|
|
3. Inkludera tekniska detaljer som är specifika för objektet.
|
|
4. Använd domän-expertis: för VAT (vatten) inkludera tryckklasser, för TRP (trafik) inkludera belastningsklasser, etc."""
|
|
|
|
log(f"Anropar Claude API för: {amne}")
|
|
|
|
# Anropa LLM
|
|
response = client.messages.create(
|
|
model=DEFAULT_MODEL,
|
|
max_tokens=4000,
|
|
system=system_prompt,
|
|
messages=[
|
|
{"role": "user", "content": user_prompt}
|
|
]
|
|
)
|
|
|
|
# Parsa JSON-svar
|
|
try:
|
|
# Hitta text content (skip thinking blocks)
|
|
content = None
|
|
for block in response.content:
|
|
if hasattr(block, 'text'):
|
|
content = block.text
|
|
break
|
|
|
|
if not content:
|
|
raise ValueError("Ingen text hittades i svaret")
|
|
|
|
# Extrahera JSON om det är omgivet av markdown
|
|
if "```json" in content:
|
|
content = content.split("```json")[1].split("```")[0].strip()
|
|
elif "```" in content:
|
|
content = content.split("```")[1].split("```")[0].strip()
|
|
|
|
result = json.loads(content)
|
|
log(f"✓ Artikel genererad: {amne}")
|
|
return result
|
|
except (json.JSONDecodeError, ValueError) as e:
|
|
log(f"✗ Kunde inte parsa LLM-svar: {e}")
|
|
if content:
|
|
log(f"Rått svar: {content[:500]}...")
|
|
raise
|
|
|
|
|
|
def producera_kandidatpost(post, agent_id, system_prompt, webb_data=None):
|
|
"""
|
|
Produktion av komplett kandidatpost med LLM.
|
|
"""
|
|
ko_id = post["ko_id"]
|
|
amne = post["mal"]
|
|
doman = post["doman"]
|
|
|
|
log(f"Produktion startar: {ko_id} - {amne}")
|
|
|
|
# Generera artikel med LLM
|
|
artikel = generera_artikel(amne, doman, system_prompt, webb_data)
|
|
|
|
# Bygg kandidatpost
|
|
kandidat_id = f"K-{datetime.now(timezone.utc).strftime('%Y%m%d')}-{agent_id}-{ko_id}"
|
|
|
|
kandidat = {
|
|
"kandidat_id": kandidat_id,
|
|
"atgard": "ny",
|
|
"serie": post.get("serie", {"typ": "klass", "tier": 2}),
|
|
"paket_ref": [],
|
|
"patch": {},
|
|
"post": {
|
|
"posttyp": "objektklass" if post["serie"]["typ"] == "klass" else "produktmodell",
|
|
"slug": artikel.get("slug", amne.lower().replace(" ", "-")),
|
|
"namn": artikel.get("namn", {"sv": amne, "en": amne}),
|
|
"doman": doman,
|
|
"identitet": {
|
|
"status": "aktiv",
|
|
"beskrivning": artikel.get("beskrivning", f"{amne} — infrastrukturkomponent")
|
|
},
|
|
"teknik": artikel.get("teknik", {}),
|
|
"geografi": {
|
|
"marknader": ["US", "EU", "UK", "CA", "AU"]
|
|
},
|
|
"standarder": [],
|
|
"ai": {
|
|
"ocr": {
|
|
"falt": []
|
|
}
|
|
},
|
|
"problem": [],
|
|
"relationer": [
|
|
{"typ": "relaterad_till", "mal": rel}
|
|
for rel in artikel.get("relaterade_objekt", [])
|
|
],
|
|
"proveniens": {
|
|
"skapad": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
|
"uppdaterad": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
|
"verifieringsniva": "obekraftad",
|
|
"konfidens": 0.7,
|
|
"kallor": artikel.get("kallor", [])
|
|
}
|
|
},
|
|
"verifieringsniva_forslag": "obekraftad",
|
|
"motivering": f"LLM-genererad post för {amne} med tekniska detaljer och källhänvisningar"
|
|
}
|
|
|
|
# Lägg till tier eller parent
|
|
if post["serie"]["typ"] == "klass":
|
|
kandidat["post"]["tier"] = post["serie"]["tier"]
|
|
else:
|
|
kandidat["post"]["parent"] = post.get("parent", "")
|
|
|
|
# Lägg till standarder från artikel
|
|
if "standarder" in artikel.get("teknik", {}):
|
|
for std in artikel["teknik"]["standarder"]:
|
|
kandidat["post"]["standarder"].append({
|
|
"beteckning": std,
|
|
"organ": "Standardiseringsorgan",
|
|
"roll": "referens"
|
|
})
|
|
|
|
# Lägg till ledtrådar som standarder om tillgängliga
|
|
if "ledtrad_standard" in post:
|
|
kandidat["post"]["standarder"].append({
|
|
"beteckning": post["ledtrad_standard"],
|
|
"organ": "Standardiseringsorgan",
|
|
"roll": "referens"
|
|
})
|
|
|
|
log(f"✓ Kandidatpost klar: {kandidat_id}")
|
|
return kandidat
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test
|
|
if len(sys.argv) < 3:
|
|
print("Användning: python3 producera_artikel.py '<amne>' '<doman>'")
|
|
sys.exit(1)
|
|
|
|
amne = sys.argv[1]
|
|
doman = sys.argv[2]
|
|
|
|
system_prompt = ladda_prompt("lvx-artikelautomation-prompt.md")
|
|
|
|
try:
|
|
result = generera_artikel(amne, doman, system_prompt)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
except Exception as e:
|
|
log(f"Fel: {e}")
|
|
sys.exit(1)
|