2025-12-14 02:38:35 +07:00
|
|
|
|
import json
|
|
|
|
|
|
from openai import AsyncOpenAI
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
from app.schemas import ChallengeGenerated
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GPTService:
|
|
|
|
|
|
"""Service for generating challenges using OpenAI GPT"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
|
|
|
|
|
|
|
|
|
|
|
async def generate_challenges(
|
|
|
|
|
|
self,
|
2025-12-16 22:12:12 +07:00
|
|
|
|
games: list[dict]
|
|
|
|
|
|
) -> dict[int, list[ChallengeGenerated]]:
|
2025-12-14 02:38:35 +07:00
|
|
|
|
"""
|
2025-12-16 22:12:12 +07:00
|
|
|
|
Generate challenges for multiple games in one API call.
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-12-16 22:12:12 +07:00
|
|
|
|
games: List of dicts with keys: id, title, genre
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-12-16 22:12:12 +07:00
|
|
|
|
Dict mapping game_id to list of generated challenges
|
2025-12-14 02:38:35 +07:00
|
|
|
|
"""
|
2025-12-16 22:12:12 +07:00
|
|
|
|
if not games:
|
|
|
|
|
|
return {}
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
2025-12-16 22:12:12 +07:00
|
|
|
|
games_text = "\n".join([
|
|
|
|
|
|
f"- {g['title']}" + (f" (жанр: {g['genre']})" if g.get('genre') else "")
|
|
|
|
|
|
for g in games
|
|
|
|
|
|
])
|
2025-12-16 03:41:34 +07:00
|
|
|
|
|
2025-12-16 22:12:12 +07:00
|
|
|
|
prompt = f"""Ты — эксперт по видеоиграм. Сгенерируй по 6 КОНКРЕТНЫХ челленджей для каждой из следующих игр:
|
|
|
|
|
|
|
|
|
|
|
|
{games_text}
|
|
|
|
|
|
|
|
|
|
|
|
ВАЖНО: Челленджи должны быть СПЕЦИФИЧНЫМИ для каждой игры!
|
2025-12-16 03:41:34 +07:00
|
|
|
|
- Используй РЕАЛЬНЫЕ названия локаций, боссов, персонажей, миссий, уровней из игры
|
2025-12-16 22:12:12 +07:00
|
|
|
|
- Основывайся на том, какие челленджи РЕАЛЬНО делают игроки в этой игре
|
2025-12-16 03:41:34 +07:00
|
|
|
|
- НЕ генерируй абстрактные челленджи типа "пройди уровень" или "убей 10 врагов"
|
|
|
|
|
|
|
2025-12-16 22:12:12 +07:00
|
|
|
|
Требования по сложности ДЛЯ КАЖДОЙ ИГРЫ:
|
|
|
|
|
|
- 2 лёгких (15-30 мин): простые задачи
|
|
|
|
|
|
- 2 средних (1-2 часа): требуют навыка
|
|
|
|
|
|
- 2 сложных (3+ часа): серьёзный челлендж
|
|
|
|
|
|
|
|
|
|
|
|
Формат ответа — JSON с объектом где ключи это ТОЧНЫЕ названия игр, как они указаны в запросе:
|
|
|
|
|
|
{{
|
|
|
|
|
|
"Название игры 1": {{
|
|
|
|
|
|
"challenges": [
|
|
|
|
|
|
{{"title": "...", "description": "...", "type": "completion|no_death|speedrun|collection|achievement|challenge_run", "difficulty": "easy|medium|hard", "points": 50, "estimated_time": 30, "proof_type": "screenshot|video|steam", "proof_hint": "..."}}
|
|
|
|
|
|
]
|
|
|
|
|
|
}},
|
|
|
|
|
|
"Название игры 2": {{
|
|
|
|
|
|
"challenges": [...]
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
points: easy=20-40, medium=45-75, hard=90-150
|
|
|
|
|
|
Ответь ТОЛЬКО JSON."""
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
|
|
|
|
|
response = await self.client.chat.completions.create(
|
2025-12-16 03:41:34 +07:00
|
|
|
|
model="gpt-5-mini",
|
2025-12-14 02:38:35 +07:00
|
|
|
|
messages=[{"role": "user", "content": prompt}],
|
|
|
|
|
|
response_format={"type": "json_object"},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
content = response.choices[0].message.content
|
|
|
|
|
|
data = json.loads(content)
|
|
|
|
|
|
|
2025-12-16 22:12:12 +07:00
|
|
|
|
# Map game titles to IDs (case-insensitive, strip whitespace)
|
|
|
|
|
|
title_to_id = {g['title'].lower().strip(): g['id'] for g in games}
|
|
|
|
|
|
|
|
|
|
|
|
# Also keep original titles for logging
|
|
|
|
|
|
id_to_title = {g['id']: g['title'] for g in games}
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[GPT] Requested games: {[g['title'] for g in games]}")
|
|
|
|
|
|
print(f"[GPT] Response keys: {list(data.keys())}")
|
|
|
|
|
|
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
for game_title, game_data in data.items():
|
|
|
|
|
|
# Try exact match first, then case-insensitive
|
|
|
|
|
|
game_id = title_to_id.get(game_title.lower().strip())
|
|
|
|
|
|
|
|
|
|
|
|
if not game_id:
|
|
|
|
|
|
# Try partial match if exact match fails
|
|
|
|
|
|
for stored_title, gid in title_to_id.items():
|
|
|
|
|
|
if stored_title in game_title.lower() or game_title.lower() in stored_title:
|
|
|
|
|
|
game_id = gid
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not game_id:
|
|
|
|
|
|
print(f"[GPT] Could not match game: '{game_title}'")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
challenges = []
|
|
|
|
|
|
for ch in game_data.get("challenges", []):
|
|
|
|
|
|
challenges.append(self._parse_challenge(ch))
|
|
|
|
|
|
|
|
|
|
|
|
result[game_id] = challenges
|
|
|
|
|
|
print(f"[GPT] Generated {len(challenges)} challenges for '{id_to_title.get(game_id)}'")
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_challenge(self, ch: dict) -> ChallengeGenerated:
|
|
|
|
|
|
"""Parse and validate a single challenge from GPT response"""
|
|
|
|
|
|
ch_type = ch.get("type", "completion")
|
|
|
|
|
|
if ch_type not in ["completion", "no_death", "speedrun", "collection", "achievement", "challenge_run"]:
|
|
|
|
|
|
ch_type = "completion"
|
|
|
|
|
|
|
|
|
|
|
|
difficulty = ch.get("difficulty", "medium")
|
|
|
|
|
|
if difficulty not in ["easy", "medium", "hard"]:
|
|
|
|
|
|
difficulty = "medium"
|
|
|
|
|
|
|
|
|
|
|
|
proof_type = ch.get("proof_type", "screenshot")
|
|
|
|
|
|
if proof_type not in ["screenshot", "video", "steam"]:
|
|
|
|
|
|
proof_type = "screenshot"
|
|
|
|
|
|
|
|
|
|
|
|
points = ch.get("points", 30)
|
|
|
|
|
|
if not isinstance(points, int) or points < 1:
|
|
|
|
|
|
points = 30
|
|
|
|
|
|
if difficulty == "easy":
|
|
|
|
|
|
points = max(20, min(40, points))
|
|
|
|
|
|
elif difficulty == "medium":
|
|
|
|
|
|
points = max(45, min(75, points))
|
|
|
|
|
|
elif difficulty == "hard":
|
|
|
|
|
|
points = max(90, min(150, points))
|
|
|
|
|
|
|
|
|
|
|
|
return ChallengeGenerated(
|
|
|
|
|
|
title=ch.get("title", "Unnamed Challenge")[:100],
|
|
|
|
|
|
description=ch.get("description", "Complete the challenge"),
|
|
|
|
|
|
type=ch_type,
|
|
|
|
|
|
difficulty=difficulty,
|
|
|
|
|
|
points=points,
|
|
|
|
|
|
estimated_time=ch.get("estimated_time"),
|
|
|
|
|
|
proof_type=proof_type,
|
|
|
|
|
|
proof_hint=ch.get("proof_hint"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gpt_service = GPTService()
|