"""Achievement extraction service.""" import json import re import httpx from app.core.config import settings from app.core.logging import get_logger from app.core.prompts import ACHIEVEMENT_EXTRACTION_PROMPT logger = get_logger(__name__) TEXT_API_BASE = "https://generativelanguage.googleapis.com/v1beta/models" async def extract_achievements(story_text: str) -> list[dict]: """Extract achievements from story text using LLM.""" if not settings.text_api_key: logger.warning("achievement_extraction_skipped", reason="missing_text_api_key") return [] model = settings.text_model or "gemini-2.0-flash" url = f"{TEXT_API_BASE}/{model}:generateContent" prompt = ACHIEVEMENT_EXTRACTION_PROMPT.format(story_text=story_text) payload = { "contents": [{"parts": [{"text": prompt}]}], "generationConfig": { "responseMimeType": "application/json", "temperature": 0.2, "topP": 0.9, }, } async with httpx.AsyncClient(timeout=30) as client: response = await client.post( url, json=payload, headers={"x-goog-api-key": settings.text_api_key}, ) response.raise_for_status() result = response.json() candidates = result.get("candidates") or [] if not candidates: logger.warning("achievement_extraction_empty") return [] parts = candidates[0].get("content", {}).get("parts") or [] if not parts or "text" not in parts[0]: logger.warning("achievement_extraction_missing_text") return [] response_text = parts[0]["text"] clean_json = response_text if response_text.startswith("```json"): clean_json = re.sub(r"^```json\n|```$", "", response_text) try: parsed = json.loads(clean_json) except json.JSONDecodeError: logger.warning("achievement_extraction_parse_failed") return [] achievements = parsed.get("achievements") if not isinstance(achievements, list): return [] normalized: list[dict] = [] for item in achievements: if not isinstance(item, dict): continue a_type = str(item.get("type", "")).strip() description = str(item.get("description", "")).strip() score = item.get("score", 0) if not a_type or not description: continue normalized.append({ "type": a_type, "description": description, "score": score }) return normalized