Files
dreamweaver/backend/app/services/achievement_extractor.py
zhangtuo e9d7f8832a Initial commit: clean project structure
- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+)
- Frontend: Vue 3 + TypeScript + Pinia + Tailwind
- Admin Frontend: separate Vue 3 app for management
- Docker Compose: 9 services orchestration
- Specs: design prototypes, memory system PRD, product roadmap

Cleanup performed:
- Removed temporary debug scripts from backend root
- Removed deprecated admin_app.py (embedded UI)
- Removed duplicate docs from admin-frontend
- Updated .gitignore for Vite cache and egg-info
2026-01-20 18:20:03 +08:00

86 lines
2.5 KiB
Python

"""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