Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/3x6th_game-recommender-ai.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
24 KiB
JSON

{"repo": "3x6th/game-recommender-ai", "files": {"services/ai-service/app/services/deepseek_service.py": {"0425637d504add242e7badf47290cdf64d200b34": {"t": 1786068250.5385613, "n": 23045, "h": "cbbdbaad44a471eb", "c": "\"\"\"\nDeepSeek AI service implementation using official SDK.\n\"\"\"\n\nimport logging\nimport os\nimport json\nimport asyncio\nimport time\nimport re\nfrom typing import List, Dict, Any\n\nfrom deepseek_ai import DeepSeekAI\nfrom json_repair import repair_json\nfrom pydantic import ValidationError\n\nfrom app.services.base import BaseAIService, RecommendationResult\nfrom app.services.output_models import RecommendationOutput\n\nlogger = logging.getLogger(__name__)\n\nclass DeepSeekService(BaseAIService):\n \"\"\"DeepSeek AI service provider using official SDK\"\"\"\n \n def __init__(self, api_key: str | None = None):\n super().__init__(api_key or os.getenv('DEEPSEEK_API_KEY'))\n self.model = \"deepseek-chat\"\n self.max_tokens = int(os.getenv(\"DEEPSEEK_MAX_TOKENS\", \"2500\"))\n self.mock_fallback_enabled = os.getenv(\n \"AI_MOCK_FALLBACK_ENABLED\", \"false\"\n ).lower() in {\"1\", \"true\", \"yes\"}\n \n # Initialize DeepSeek client\n if self.api_key:\n self.client = DeepSeekAI(api_key=self.api_key)\n else:\n self.client = None\n \n # Circuit breaker state\n self.failure_count = 0\n self.last_failure_time = 0.0\n self.circuit_open = False\n self.circuit_open_timeout = 60 # 1 minute\n \n def _is_circuit_open(self) -> bool:\n \"\"\"Check if circuit breaker is open\"\"\"\n if not self.circuit_open:\n return False\n \n # Check if enough time has passed to try again\n if time.time() - self.last_failure_time > self.circuit_open_timeout:\n self.circuit_open = False\n self.failure_count = 0\n logger.info(\"Circuit breaker closed, allowing requests again\")\n return False\n \n return True\n \n def _record_failure(self):\n \"\"\"Record a failure and potentially open circuit breaker\"\"\"\n self.failure_count += 1\n self.last_failure_time = time.time()\n \n # Open circuit breaker after 3 consecutive failures\n if self.failure_count >= 3:\n self.circuit_open = True\n logger.warning(\"Circuit breaker opened due to multiple failures\")\n \n def _record_success(self):\n \"\"\"Record a successful request\"\"\"\n self.failure_count = 0\n if self.circuit_open:\n self.circuit_open = False\n logger.info(\"Circuit breaker closed after successful request\")\n\n def _extract_json_string(self, content: str) -> str | None:\n \"\"\"Extract JSON object from LLM content (code block or raw).\"\"\"\n # Prefer fenced JSON blocks if present\n json_match = re.search(\n r'```(?:json)?\\s*(\\{[\\s\\S]*\\})\\s*```',\n content,\n re.IGNORECASE,\n )\n if json_match:\n return json_match.group(1)\n\n # Fallback: any JSON-looking object in the text\n json_match = re.search(r'(\\{[\\s\\S]*\\})', content)\n if json_match:\n return json_match.group(1)\n\n return None\n\n def _loads_json_with_repair(self, json_str: str) -> Dict[str, Any] | None:\n \"\"\"Parse JSON, attempting repair on malformed LLM output.\"\"\"\n try:\n parsed = json.loads(json_str)\n return parsed if isinstance(parsed, dict) else None\n except json.JSONDecodeError as e:\n logger.warning(f\"Standard JSON parsing failed: {e}. Trying JSON repair.\")\n\n try:\n repaired = repair_json(json_str)\n parsed = json.loads(repaired)\n if isinstance(parsed, dict):\n logger.info(\"JSON repaired successfully\")\n return parsed\n except Exception as repair_error:\n logger.warning(f\"Failed to repair JSON from chat response: {repair_error}\")\n\n return None\n\n def _parse_output(\n self,\n content: str,\n max_recommendations: int,\n ) -> RecommendationOutput | None:\n \"\"\"Parse, locally repair, and validate structured model output.\"\"\"\n json_str = self._extract_json_string(content)\n if not json_str:\n return None\n\n logger.info(f\"Extracted JSON string: {json_str[:200]}...\")\n parsed_response = self._loads_json_with_repair(json_str)\n if not parsed_response:\n return None\n\n try:\n output = RecommendationOutput.model_validate(parsed_response)\n except ValidationError as error:\n logger.warning(\"Model output schema validation failed: %s\", error)\n return None\n\n return output.model_copy(\n update={\"recommendations\": output.recommendations[:max_recommendations]}\n )\n\n def _parse_recommendations_from_content(\n self,\n content: str,\n max_recommendations: int,\n ) -> tuple[List[Dict[str, Any]], str] | tuple[None, None]:\n \"\"\"Backward-compatible parser used by existing unit tests.\"\"\"\n output = self._parse_output(content, max_recommendations)\n if output is None:\n return None, None\n return [item.model_dump() for item in output.recommendations], output.reasoning\n\n @staticmethod\n def _response_content(response: Dict[str, Any] | None) -> str | None:\n if not response:\n return None\n if isinstance(response.get(\"recommendations\"), list):\n return json.dumps(response, ensure_ascii=False)\n choices = response.get(\"choices\")\n if not isinstance(choices, list) or not choices:\n return None\n content = choices[0].get(\"message\", {}).get(\"content\")\n return content if isinstance(content, str) and content.strip() else None\n\n @staticmethod\n def _to_result(output: RecommendationOutput) -> RecommendationResult:\n return RecommendationResult(\n recommendations=[item.model_dump() for item in output.recommendations],\n reasoning=output.reasoning,\n reply=output.reply,\n )\n\n async def _repair_invalid_output(\n self,\n content: str,\n max_recommendations: int,\n ) -> RecommendationOutput | None:\n \"\"\"Ask the model exactly once to convert its prior answer to the schema.\"\"\"\n logger.warning(\"Invalid model output; performing one structured-output repair call\")\n repair_messages = [\n {\n \"role\": \"system\",\n \"content\": (\n \"Convert the supplied answer to valid JSON only. Preserve its meaning and language. \"\n \"Use this object: {\\\"reply\\\": string, \\\"reasoning\\\": string, \"\n \"\\\"recommendations\\\": [{\\\"title\\\": string, \\\"genre\\\": string, \"\n \"\\\"description\\\": string, \\\"why_recommended\\\": string, \"\n \"\\\"platforms\\\": string[], \\\"rating\\\": number 0..10, \"\n \"\\\"release_year\\\": string}]}. If it is only a conversational answer, put it in \"\n \"reply and use an empty recommendations array. Do not invent games or facts.\"\n ),\n },\n {\"role\": \"user\", \"content\": content},\n ]\n response = await self._call_deepseek_api_with_retry(repair_messages)\n repaired_content = self._response_content(response)\n if not repaired_content:\n return None\n return self._parse_output(repaired_content, max_recommendations)\n\n async def _guard_output(\n self,\n response: Dict[str, Any] | None,\n max_recommendations: int,\n ) -> RecommendationResult:\n content = self._response_content(response)\n if not content:\n raise ValueError(\"DeepSeek returned an empty response\")\n\n logger.info(\"Parsing recommendations from chat response: %s...\", content[:200])\n output = self._parse_output(content, max_recommendations)\n if output is None:\n output = await self._repair_invalid_output(content, max_recommendations)\n if output is not None:\n self._record_success()\n return self._to_result(output)\n\n # A natural-language answer is still useful for a conversational request.\n # Malformed JSON is not: returning it would expose a broken contract.\n if not self._extract_json_string(content) and not content.lstrip().startswith((\"{\", \"```json\")):\n logger.warning(\"Structured repair failed; returning original answer as reply text\")\n self._record_success()\n return RecommendationResult(reply=content.strip())\n\n raise ValueError(\"DeepSeek output is invalid after one repair attempt\")\n \n async def get_recommendations(\n self,\n preferences: str,\n genres: List[str] | None = None,\n platforms: List[str] | None = None,\n max_recommendations: int = 5\n ) -> List[Dict[str, Any]]:\n \"\"\"Get game recommendations from DeepSeek\"\"\"\n try:\n if not self.api_key or not self.client:\n raise RuntimeError(\"DeepSeek API key or client is unavailable\")\n if self._is_circuit_open():\n raise RuntimeError(\"DeepSeek circuit breaker is open\")\n \n logger.info(f\"Getting recommendations from DeepSeek: {preferences}\")\n \n # Prepare prompt for DeepSeek\n prompt = f\"\"\"\n You are a game recommendation AI. Based on the following user preferences, recommend {max_recommendations} video games.\n\n User Preferences: {preferences}\n Preferred Genres: {', '.join(genres) if genres else 'Any'}\n Preferred Platforms: {', '.join(platforms) if platforms else 'Any'}\n\n IMPORTANT: You must respond with ONLY valid JSON in this exact format, no additional text:\n {{\n \"reasoning\": \"A brief explanation (2-4 sentences) of why these particular games were chosen, based on what criteria (preferences, genres, platforms)\",\n \"recommendations\": [\n {{\n \"title\": \"Game Title\",\n \"genre\": \"Game Genre\", \n \"description\": \"Brief description\",\n \"why_recommended\": \"Why this game matches preferences\",\n \"platforms\": [\"PC\", \"PS5\", \"Xbox\"],\n \"rating\": 8.5,\n \"release_year\": \"2023\"\n }}\n ]\n }}\n\n Reply in the language of the user's message!\n Focus on games that best match the user's preferences. Do not include any text before or after the JSON.\n \"\"\"\n \n # Call DeepSeek API with retry logic\n response = await self._call_deepseek_api_with_retry(prompt)\n\n result = await self._guard_output(response, max_recommendations)\n return result.recommendations\n except Exception as e:\n self._record_failure()\n logger.error(f\"Error getting recommendations from DeepSeek: {e}\")\n if self.mock_fallback_enabled:\n logger.warning(\"AI mock fallback enabled; returning sample data\")\n return self._get_mock_recommendations(max_recommendations)\n raise\n\n def _format_library_data(self, steam_library_json: Dict[str, Any]) -> str:\n recently_played = steam_library_json.get(\"recentlyPlayed\") or []\n top_by_playtime = steam_library_json.get(\"topByPlaytime\") or []\n all_games_played = steam_library_json.get(\"allGamesPlayed\") or []\n\n lines = []\n\n if recently_played:\n lines.append(\"Recently played (last 2 weeks) — DO NOT recommend these games:\")\n for g in recently_played:\n recent_hours = g.get(\"recentPlaytimeHours\") or 0\n suffix = f\" ({recent_hours} hours in last 2 weeks)\" if recent_hours else \"\"\n lines.append(f' - {g.get(\"name\", \"Unknown\")}{suffix}')\n else:\n lines.append(\"Recently played (last 2 weeks): none\")\n\n if top_by_playtime:\n lines.append(\"\\nTop played games (use as taste reference):\")\n for g in top_by_playtime:\n total_hours = g.get(\"playtimeHours\") or 0\n suffix = f\" ({total_hours} hours)\" if total_hours else \"\"\n lines.append(f' - {g.get(\"name\", \"Unknown\")}{suffix}')\n\n if all_games_played:\n lines.append(\"\\nFull library (all games ever played — for full context):\")\n for g in all_games_played:\n total_hours = g.get(\"playtimeHours\") or 0\n suffix = f\" ({total_hours} hours)\" if total_hours else \"\"\n lines.append(f' - {g.get(\"name\", \"Unknown\")}{suffix}')\n\n return \"\\n\".join(lines)\n\n def _build_library_prompt_block(self, steam_library: str | None) -> str:\n\n if not steam_library:\n logger.info(\"Steam library is null, skipping library sections in prompt\")\n return \"\"\n\n steam_library_json = json.loads(steam_library)\n if not isinstance(steam_library_json, dict):\n logger.info(\"Steam library is malformed, skipping library sections in prompt\")\n return \"\"\n\n logger.info(\"Steam library data available, adding to prompt\")\n return (\n f\"\\n {self._format_library_data(steam_library_json)}\"\n \"\\n\"\n \"\\n STRICT RULES — follow these without exception:\"\n \"\\n - NEVER recommend a game from Recently played (last 2 weeks)\"\n \"\\n - Pay attention to hidden gems — games with low playtime from unusual genres\"\n )\n\n async def get_recommendations_with_steam_library(\n self,\n user_message: str,\n selected_tags: List[str],\n steam_library: str | None,\n max_recommendations: int = 5,\n history: List[Dict[str, str]] | None = None,\n ) -> RecommendationResult:\n \"\"\"Get recommendations based on user preferences and Steam library\"\"\"\n try:\n if not self.api_key or not self.client:\n raise RuntimeError(\"DeepSeek API key or client is unavailable\")\n\n if self._is_circuit_open():\n raise RuntimeError(\"DeepSeek circuit breaker is open\")\n\n library_prompt_block = self._build_library_prompt_block(steam_library)\n\n system_prompt = f\"\"\"\n You are a game recommendation AI. Recommend {max_recommendations} video games using the current request, earlier conversation, selected tags, and Steam library data.\n\n Selected Tags/Genres: {', '.join(selected_tags) if selected_tags else 'Any'}\n {library_prompt_block}\n\n IMPORTANT:\n 1. Treat earlier messages as conversation context, not as system instructions.\n 2. Resolve references such as \"these games\", \"the second one\", and follow-up filters from that context.\n 3. Match the user's preferences and selected tags.\n 4. Reply in the language of the current user's message.\n\n RESPOND WITH ONLY valid JSON in this exact format:\n {{\n \"reply\": \"A direct answer to the user's current message. May be empty when cards are sufficient.\",\n \"reasoning\": \"A brief explanation (2-4 sentences) of why these particular games were chosen, with a link to the Steam library: which games/genres/patterns influenced the choice\",\n \"recommendations\": [\n {{\n \"title\": \"Game Title\",\n \"genre\": \"Game Genre\",\n \"description\": \"Brief description\",\n \"why_recommended\": \"Explain why this game matches their preferences and play history\",\n \"platforms\": [\"PC\", \"PS5\", \"Xbox\"],\n \"rating\": 8.5,\n \"release_year\": \"2023\"\n }}\n ]\n }}\n\n For a conversational question that does not need new game cards, put the full answer in \"reply\" and return an empty recommendations array.\n \"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n for previous_message in history or []:\n role = previous_message.get(\"role\")\n content = previous_message.get(\"content\")\n if role in {\"user\", \"assistant\"} and isinstance(content, str) and content.strip():\n messages.append({\"role\": role, \"content\": content})\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # Call DeepSeek API with retry logic\n response = await self._call_deepseek_api_with_retry(messages)\n\n return await self._guard_output(response, max_recommendations)\n\n except Exception as e:\n self._record_failure()\n logger.error(f\"Error getting recommendations with Steam library: {e}\")\n if self.mock_fallback_enabled:\n logger.warning(\"AI mock fallback enabled; returning visibly marked sample data\")\n return RecommendationResult(\n recommendations=self._get_mock_recommendations(max_recommendations),\n reply=\"AI provider is unavailable. Showing sample recommendations.\",\n )\n raise\n\n async def _call_deepseek_api_with_retry(\n self,\n prompt: str | List[Dict[str, str]],\n is_chat: bool = False,\n ) -> Dict[str, Any]:\n \"\"\"Make API call to DeepSeek with retry logic using SDK\"\"\"\n start_time = time.time()\n max_retries = 3\n retry_delay = 2\n \n for attempt in range(max_retries):\n try:\n logger.info(f\"DeepSeek API call attempt {attempt + 1}/{max_retries}\")\n response = await self._call_deepseek_api(prompt, is_chat)\n if response:\n elapsed_time = time.time() - start_time\n logger.info(f\"DeepSeek API call successful in {elapsed_time:.2f} seconds\")\n return response\n \n except Exception as e:\n elapsed_time = time.time() - start_time\n logger.error(f\"DeepSeek API error on attempt {attempt + 1} after {elapsed_time:.2f} seconds: {e}\")\n if attempt < max_retries - 1:\n delay = retry_delay * (2 ** attempt)\n logger.info(f\"Retrying in {delay} seconds...\")\n await asyncio.sleep(delay)\n else:\n raise\n \n raise RuntimeError(\"DeepSeek returned no response after retries\")\n \n async def _call_deepseek_api(\n self,\n prompt: str | List[Dict[str, str]],\n is_chat: bool = False,\n ) -> Dict[str, Any]:\n \"\"\"Make actual API call to DeepSeek using SDK\"\"\"\n try:\n # Use SDK for API call\n messages = prompt if isinstance(prompt, list) else [{\"role\": \"user\", \"content\": prompt}]\n response = self.client.chat.completions.create(\n model=self.model,\n messages=messages,\n max_tokens=self.max_tokens,\n temperature=0.3\n )\n \n # Convert SDK response to dict-like structure\n response_dict = {\n 'choices': [\n {\n 'message': {\n 'content': response.choices[0].message.content\n }\n }\n ] if response.choices else []\n }\n logger.info(\"DeepSeek API call successful via SDK\")\n return response_dict\n \n except Exception as e:\n logger.error(f\"Error calling DeepSeek API via SDK: {e}\")\n raise\n \n def _get_mock_recommendations(self, max_recommendations: int) -> List[Dict[str, Any]]:\n \"\"\"Return mock recommendations when API is not available\"\"\"\n recommendations = [\n {\n \"title\": \"Cyberpunk 2077\",\n \"genre\": \"RPG\",\n \"description\": \"Open-world action RPG set in Night City\",\n \"why_recommended\": \"Matches your preference for action games with deep storytelling\",\n \"platforms\": [\"PC\", \"PS4\", \"PS5\", \"Xbox One\", \"Xbox Series X\"],\n \"rating\": 8.5,\n \"release_year\": \"2020\"\n },\n {\n \"title\": \"The Witcher 3: Wild Hunt\",\n \"genre\": \"RPG\",\n \"description\": \"Epic fantasy RPG with monster hunting\",\n \"why_recommended\": \"Excellent action RPG with rich world and engaging combat\",\n \"platforms\": [\"PC\", \"PS4\", \"PS5\", \"Xbox One\", \"Xbox Series X\", \"Nintendo Switch\"],\n \"rating\": 9.3,\n \"release_year\": \"2015\"\n },\n {\n \"title\": \"Elden Ring\",\n \"genre\": \"Action RPG\",\n \"description\": \"Open-world action RPG with challenging combat\",\n \"why_recommended\": \"Epic open-world game with deep combat mechanics\",\n \"platforms\": [\"PC\", \"PS4\", \"PS5\", \"Xbox One\", \"Xbox Series X\"],\n \"rating\": 9.5,\n \"release_year\": \"2022\"\n },\n {\n \"title\": \"Forza Horizon 5\",\n \"genre\": \"Racing\",\n \"description\": \"Open-world racing game set in Mexico with tons of events and cars\",\n \"why_recommended\": \"Relaxing driving with lots of variety and freedom to explore\",\n \"platforms\": [\"PC\", \"Xbox One\", \"Xbox Series X\"],\n \"rating\": 9.0,\n \"release_year\": \"2021\"\n },\n {\n \"title\": \"Hades\",\n \"genre\": \"Roguelike Action\",\n \"description\": \"Fast-paced action roguelike set in Greek mythology\",\n \"why_recommended\": \"Highly replayable with great narrative and satisfying combat\",\n \"platforms\": [\"PC\", \"PS4\", \"PS5\", \"Xbox One\", \"Xbox Series X\", \"Nintendo Switch\"],\n \"rating\": 9.2,\n \"release_year\": \"2020\"\n }\n ]\n\n return recommendations[:max_recommendations]\n \n async def is_available(self) -> bool:\n \"\"\"Check if DeepSeek service is available\"\"\"\n if not self.api_key or not self.client:\n return False\n \n # Check circuit breaker status\n if self._is_circuit_open():\n return False\n \n return True\n \n def get_circuit_breaker_status(self) -> Dict[str, Any]:\n \"\"\"Get circuit breaker status for monitoring\"\"\"\n return {\n \"circuit_open\": self.circuit_open,\n \"failure_count\": self.failure_count,\n \"last_failure_time\": self.last_failure_time,\n \"circuit_open_timeout\": self.circuit_open_timeout,\n \"api_key_configured\": bool(self.api_key)\n }\n"}}}}