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
This commit is contained in:
85
backend/app/services/achievement_extractor.py
Normal file
85
backend/app/services/achievement_extractor.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user