Files
boc/projects/landvex/20-automation/kor_agent_kimi.py
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- 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
2026-07-08 19:56:03 +00:00

342 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Landvex Produktionsagent — kor_agent_kimi.py
Använder Kimi (Moonshot) via OpenClaw's interna API
Skriver artiklar på ENGELSKA
"""
import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
# Konfiguration
DEFAULT_KO = Path(__file__).parent / "intag" / "artikelko.json"
DEFAULT_PROMPT = Path(__file__).parent / "lvx-artikelautomation-prompt.md"
DEFAULT_OUTPUT_DIR = Path(__file__).parent / "intag"
DEFAULT_SEO_DIR = Path(__file__).parent / ".." / ".." / "landvex-site" / "public"
def log(msg):
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
print(f"[{ts}] {msg}", flush=True)
import fcntl
def ladda_ko(ko_path):
with open(ko_path, "r", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_SH)
try:
return json.load(f)
finally:
fcntl.flock(f, fcntl.LOCK_UN)
def spara_ko(ko_path, data):
with open(ko_path, "r+", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_EX)
try:
f.seek(0)
json.dump(data, f, ensure_ascii=False, indent=2)
f.truncate()
finally:
fcntl.flock(f, fcntl.LOCK_UN)
def hitta_nasta(ko, agent_id):
"""Hitta översta olåsta posten (lägst prio = högst först)."""
oppen = [p for p in ko["ko"] if p.get("status") == "oppen"]
if not oppen:
return None
oppen.sort(key=lambda x: x["prio"])
return oppen[0]
def las_las(post, agent_id):
"""Lås en post för arbete."""
post["status"] = "in_arbete"
post["agent_id"] = agent_id
post["las_tid"] = datetime.now(timezone.utc).isoformat()
return post
def markera_klar(post):
"""Markera post som klar."""
post["status"] = "klar"
post["klar_tid"] = datetime.now(timezone.utc).isoformat()
if "las_tid" in post:
del post["las_tid"]
def skapa_kandidatfil(kandidater, agent_id, output_dir):
"""Skriv kandidatposter till fil."""
datum = datetime.now(timezone.utc).strftime("%Y-%m-%d")
filnamn = f"kandidatposter-{datum}-{agent_id}.json"
sokvag = output_dir / filnamn
payload = {
"beskrivning": f"Landvex produktionsagent — kandidatposter {datum}",
"extraktor": "landvex-produktionsagent",
"genererad": datum,
"kandidater": kandidater
}
with open(sokvag, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
return sokvag
def generera_artikel_med_kimi(amne, doman, system_prompt):
"""
Generera en komplett artikel med Kimi (Moonshot) via OpenClaw API.
Skriver på ENGELSKA.
"""
import anthropic
# Använd Anthropic som proxy (OpenClaw har Kimi konfigurerat internt)
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
user_prompt = f"""Generate a comprehensive technical article about the following infrastructure object.
Write in ENGLISH.
Object: {amne}
Domain: {doman}
Return the response as JSON in this exact format:
{{
"slug": "url-friendly-slug-in-english",
"namn": {{
"sv": "Swedish name",
"en": "English name"
}},
"beskrivning": "Detailed description of the object, its function and applications (at least 300 words in ENGLISH)",
"teknik": {{
"material": ["Materials used"],
"dimensioner": "Typical dimensions",
"standarder": ["Applicable standards and norms"],
"livslangd": "Expected lifespan"
}},
"anvandningsomraden": ["List of applications"],
"relaterade_objekt": ["List of related infrastructure objects"],
"bilder": [
{{
"beskrivning": "Description of what the image should show",
"alt_text": "Alt text for accessibility"
}}
],
"kallor": [
{{
"titel": "Source title",
"url": "https://example.com",
"typ": "webbsida|standard|dokument"
}}
]
}}
IMPORTANT:
1. Return ONLY JSON, no other text before or after.
2. Description must be in ENGLISH and at least 300 words.
3. Include technical details specific to the object.
4. Use domain expertise: for VAT (water) include pressure classes, for TRP (traffic) include load classes, etc."""
log(f"Calling Claude API for: {amne} (ENGLISH)")
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4000,
system=system_prompt + "\n\nYou are writing technical documentation for an international infrastructure database. Write all content in ENGLISH.",
messages=[{"role": "user", "content": user_prompt}]
)
# Parsa JSON-svar
content = None
for block in response.content:
if hasattr(block, 'text'):
content = block.text
break
if not content:
raise ValueError("No text found in response")
# 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()
# Rensa kontrolltecken
import re
content = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', content)
return json.loads(content)
def producera_kandidatpost(post, agent_id, system_prompt):
"""Produktion av komplett kandidatpost med LLM."""
ko_id = post["ko_id"]
amne = post["mal"]
doman = post["doman"]
log(f"Production starting: {ko_id} - {amne}")
# Generera artikel med LLM
artikel = generera_artikel_med_kimi(amne, doman, system_prompt)
# 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} — infrastructure component")
},
"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-generated post for {amne} with technical details and source references"
}
# 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"✓ Candidate post ready: {kandidat_id}")
return kandidat
def kor_agent(agent_id, cap, ko_path, prompt_path, output_dir):
"""Huvudloop för agenten."""
log(f"Agent {agent_id} starting (cap={cap})")
if not os.environ.get("ANTHROPIC_API_KEY"):
log("ERROR: ANTHROPIC_API_KEY missing")
return 0
# Ladda system prompt
system_prompt = "You are an expert in infrastructure and technical documentation."
if Path(prompt_path).exists():
with open(prompt_path, "r", encoding="utf-8") as f:
system_prompt = f.read()
ko = ladda_ko(ko_path)
kandidater = []
producerade = 0
while producerade < cap:
post = hitta_nasta(ko, agent_id)
if not post:
log("No more open posts in queue.")
break
# Lås posten
las_las(post, agent_id)
spara_ko(ko_path, ko)
log(f"Locked {post['ko_id']}: {post['mal']} (prio={post['prio']})")
try:
# Produktion med LLM
kandidat = producera_kandidatpost(post, agent_id, system_prompt)
kandidater.append(kandidat)
producerade += 1
# Markera som klar
markera_klar(post)
log(f"Done {post['ko_id']}: {post['mal']}")
except Exception as e:
log(f"ERROR {post['ko_id']}: {e}")
import traceback
traceback.print_exc()
# Släpp låset vid fel
post["status"] = "oppen"
if "las_tid" in post:
del post["las_tid"]
spara_ko(ko_path, ko)
# Spara kandidatfil om vi producerade något
if kandidater:
fil = skapa_kandidatfil(kandidater, agent_id, output_dir)
log(f"Saved {len(kandidater)} candidates to {fil}")
log(f"Agent {agent_id} finished. Produced: {producerade}/{cap}")
return producerade
def main():
parser = argparse.ArgumentParser(description="Landvex Production Agent v3.0 - ENGLISH")
parser.add_argument("--agent-id", required=True, help="Unique agent ID")
parser.add_argument("--cap", type=int, default=5, help="Max posts per run")
parser.add_argument("--ko", type=Path, default=DEFAULT_KO, help="Queue path")
parser.add_argument("--prompt", type=Path, default=DEFAULT_PROMPT, help="System prompt")
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="Output dir")
args = parser.parse_args()
kor_agent(
agent_id=args.agent_id,
cap=args.cap,
ko_path=args.ko,
prompt_path=args.prompt,
output_dir=args.output_dir
)
if __name__ == "__main__":
main()