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
176 lines
5.2 KiB
Python
176 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LLM Client för Landvex Produktionsagent
|
|
Använder Moonshot/Kimi API (OpenAI-kompatibelt)
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import time
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
try:
|
|
from openai import OpenAI
|
|
except ImportError:
|
|
raise ImportError("openai paket krävs. Kör: pip install openai")
|
|
|
|
# Konfiguration
|
|
MOONSHOT_API_KEY = os.environ.get("MOONSHOT_API_KEY") or os.environ.get("KIMI_API_KEY")
|
|
MOONSHOT_BASE_URL = "https://api.moonshot.cn/v1"
|
|
DEFAULT_MODEL = "moonshot-v1-128k" # 128k context för långa artiklar
|
|
|
|
|
|
class LLMClient:
|
|
"""Klient för att kommunicera med Moonshot/Kimi LLM API."""
|
|
|
|
def __init__(self, api_key: Optional[str] = None, model: str = DEFAULT_MODEL):
|
|
self.api_key = api_key or MOONSHOT_API_KEY
|
|
if not self.api_key:
|
|
raise ValueError("MOONSHOT_API_KEY eller KIMI_API_KEY måste sättas")
|
|
|
|
self.model = model
|
|
self.client = OpenAI(
|
|
api_key=self.api_key,
|
|
base_url=MOONSHOT_BASE_URL
|
|
)
|
|
|
|
def generera_artikel(
|
|
self,
|
|
amne: str,
|
|
doman: str,
|
|
system_prompt: str,
|
|
webb_resultat: Optional[List[Dict]] = None,
|
|
max_tokens: int = 4000,
|
|
temperatur: float = 0.7
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Generera en komplett artikel om ett infrastrukturobjekt.
|
|
|
|
Args:
|
|
amne: Objektets namn (t.ex. "Avstängningsventil")
|
|
doman: Domänkod (t.ex. "VAT")
|
|
system_prompt: Systemprompt från fil
|
|
webb_resultat: Resultat från webbsökning (valfritt)
|
|
max_tokens: Max tokens i svaret
|
|
temperatur: Kreativitet (0.0-1.0)
|
|
|
|
Returns:
|
|
Dict med komplett artikeldata
|
|
"""
|
|
|
|
# Bygg användarprompt med webbdata om tillgängligt
|
|
webb_kontext = ""
|
|
if webb_resultat:
|
|
webb_kontext = "\n\n## Webbresearch:\n"
|
|
for i, result in enumerate(webb_resultat[: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"
|
|
webb_kontext += f"Innehåll: {result.get('content', result.get('snippet', ''))[:500]}...\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-vänlig-slug",
|
|
"namn": {{
|
|
"sv": "Svenskt namn",
|
|
"en": "Engelskt namn"
|
|
}},
|
|
"beskrivning": "Detaljerad beskrivning av objektet, dess funktion och användningsområden (minst 200 ord)",
|
|
"teknik": {{
|
|
"material": ["Material som används"],
|
|
"dimensioner": "Typiska dimensioner",
|
|
"standarder": ["Tillämpliga standarder"]
|
|
}},
|
|
"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: Returnera ENDAST JSON, ingen annan text."""
|
|
|
|
# Anropa LLM
|
|
response = self.client.chat.completions.create(
|
|
model=self.model,
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
temperature=temperatur,
|
|
max_tokens=max_tokens,
|
|
response_format={"type": "json_object"}
|
|
)
|
|
|
|
# Parsa JSON-svar
|
|
try:
|
|
result = json.loads(response.choices[0].message.content)
|
|
return result
|
|
except json.JSONDecodeError as e:
|
|
raise ValueError(f"Kunde inte parsa LLM-svar som JSON: {e}")
|
|
|
|
def forbattra_artikel(
|
|
self,
|
|
befintlig_artikel: Dict[str, Any],
|
|
instruktioner: str,
|
|
max_tokens: int = 4000
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Förbättra en befintlig artikel baserat på instruktioner.
|
|
"""
|
|
user_prompt = f"""Förbättra följande artikel enligt instruktionerna:
|
|
|
|
## Instruktioner:
|
|
{instruktioner}
|
|
|
|
## Befintlig artikel:
|
|
{json.dumps(befintlig_artikel, ensure_ascii=False, indent=2)}
|
|
|
|
Returnera den förbättrade artikeln som JSON i samma format."""
|
|
|
|
response = self.client.chat.completions.create(
|
|
model=self.model,
|
|
messages=[
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
temperature=0.5,
|
|
max_tokens=max_tokens,
|
|
response_format={"type": "json_object"}
|
|
)
|
|
|
|
return json.loads(response.choices[0].message.content)
|
|
|
|
|
|
def testa_llm():
|
|
"""Testa LLM-anslutning."""
|
|
client = LLMClient()
|
|
print("✓ LLM-klient skapad")
|
|
|
|
# Testa med enkelt anrop
|
|
response = client.client.chat.completions.create(
|
|
model=DEFAULT_MODEL,
|
|
messages=[{"role": "user", "content": "Svara med ett enkelt 'OK'"}],
|
|
max_tokens=10
|
|
)
|
|
print(f"✓ LLM svarade: {response.choices[0].message.content}")
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
testa_llm()
|