Implement comprehensive IGDB integration for real-time game information
## Major Features Added - **Enhanced igdblib.py**: * Added search_games() method with fuzzy game search * Added get_game_details() for comprehensive game information * Added AI-friendly data formatting with _format_game_for_ai() * Added OpenAI function definitions via get_openai_functions() - **OpenAI Function Calling Integration**: * Modified OpenAIResponder to support function calling * Added IGDB function execution with _execute_igdb_function() * Backward compatible - gracefully falls back if IGDB unavailable * Auto-detects gaming queries and fetches real-time data - **Configuration & Setup**: * Added IGDB configuration options to config.toml * Updated system prompt to inform AI of gaming capabilities * Added comprehensive IGDB_SETUP.md documentation * Graceful initialization with proper error handling ## Technical Implementation - **Function Calling**: Uses OpenAI's tools/function calling API - **Smart Game Search**: Includes ratings, platforms, developers, genres - **Error Handling**: Robust fallbacks and logging - **Data Formatting**: Optimized for AI comprehension and user presentation - **Rate Limiting**: Respects IGDB API limits ## Usage Users can now ask natural gaming questions: - "Tell me about Elden Ring" - "What are good RPG games from 2023?" - "Is Cyberpunk 2077 on PlayStation?" The AI automatically detects gaming queries, calls IGDB API, and presents accurate, real-time game information seamlessly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -7,6 +8,7 @@ import aiohttp
|
||||
import openai
|
||||
|
||||
from .ai_responder import AIResponder, async_cache_to_file, exponential_backoff, pp
|
||||
from .igdblib import IGDBQuery
|
||||
from .leonardo_draw import LeonardoAIDrawMixIn
|
||||
|
||||
|
||||
@@ -27,6 +29,21 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
def __init__(self, config: Dict[str, Any], channel: Optional[str] = None) -> None:
|
||||
super().__init__(config, channel)
|
||||
self.client = openai.AsyncOpenAI(api_key=self.config.get("openai-token", self.config.get("openai-key", "")))
|
||||
|
||||
# Initialize IGDB if enabled
|
||||
self.igdb = None
|
||||
if (self.config.get("enable-game-info", False) and
|
||||
self.config.get("igdb-client-id") and
|
||||
self.config.get("igdb-access-token")):
|
||||
try:
|
||||
self.igdb = IGDBQuery(
|
||||
self.config["igdb-client-id"],
|
||||
self.config["igdb-access-token"]
|
||||
)
|
||||
logging.info("IGDB integration enabled for game information")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to initialize IGDB: {e}")
|
||||
self.igdb = None
|
||||
|
||||
async def draw_openai(self, description: str) -> BytesIO:
|
||||
for _ in range(3):
|
||||
@@ -46,12 +63,61 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
else:
|
||||
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
||||
try:
|
||||
result = await openai_chat(
|
||||
self.client,
|
||||
model=model,
|
||||
messages=messages,
|
||||
)
|
||||
answer_obj = result.choices[0].message
|
||||
# Prepare function calls if IGDB is enabled
|
||||
chat_kwargs = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
if self.igdb and self.config.get("enable-game-info", False):
|
||||
chat_kwargs["tools"] = [
|
||||
{"type": "function", "function": func}
|
||||
for func in self.igdb.get_openai_functions()
|
||||
]
|
||||
chat_kwargs["tool_choice"] = "auto"
|
||||
|
||||
result = await openai_chat(self.client, **chat_kwargs)
|
||||
|
||||
# Handle function calls if present
|
||||
message = result.choices[0].message
|
||||
|
||||
# Check if we have function/tool calls and IGDB is enabled
|
||||
has_tool_calls = (hasattr(message, 'tool_calls') and message.tool_calls and
|
||||
self.igdb and self.config.get("enable-game-info", False))
|
||||
|
||||
if has_tool_calls:
|
||||
try:
|
||||
# Process function calls
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": message.content or "",
|
||||
"tool_calls": [tc.dict() if hasattr(tc, 'dict') else tc for tc in message.tool_calls]
|
||||
})
|
||||
|
||||
# Execute function calls
|
||||
for tool_call in message.tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
function_args = json.loads(tool_call.function.arguments)
|
||||
|
||||
# Execute IGDB function
|
||||
function_result = await self._execute_igdb_function(function_name, function_args)
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": json.dumps(function_result) if function_result else "No results found"
|
||||
})
|
||||
|
||||
# Get final response after function execution
|
||||
final_result = await openai_chat(self.client, **chat_kwargs)
|
||||
answer_obj = final_result.choices[0].message
|
||||
except Exception as e:
|
||||
# If function calling fails, fall back to regular response
|
||||
logging.warning(f"Function calling failed, using regular response: {e}")
|
||||
answer_obj = message
|
||||
else:
|
||||
answer_obj = message
|
||||
|
||||
answer = {"content": answer_obj.content, "role": answer_obj.role}
|
||||
self.rate_limit_backoff = exponential_backoff()
|
||||
logging.info(f"generated response {result.usage}: {repr(answer)}")
|
||||
@@ -135,3 +201,42 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
except Exception as err:
|
||||
logging.warning(f"failed to create new memory: {repr(err)}")
|
||||
return memory
|
||||
|
||||
async def _execute_igdb_function(self, function_name: str, function_args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Execute IGDB function calls from OpenAI.
|
||||
"""
|
||||
if not self.igdb:
|
||||
return None
|
||||
|
||||
try:
|
||||
if function_name == "search_games":
|
||||
query = function_args.get("query", "")
|
||||
limit = function_args.get("limit", 5)
|
||||
|
||||
if not query:
|
||||
return {"error": "No search query provided"}
|
||||
|
||||
results = self.igdb.search_games(query, limit)
|
||||
if results:
|
||||
return {"games": results}
|
||||
else:
|
||||
return {"games": [], "message": f"No games found matching '{query}'"}
|
||||
|
||||
elif function_name == "get_game_details":
|
||||
game_id = function_args.get("game_id")
|
||||
|
||||
if not game_id:
|
||||
return {"error": "No game ID provided"}
|
||||
|
||||
result = self.igdb.get_game_details(game_id)
|
||||
if result:
|
||||
return {"game": result}
|
||||
else:
|
||||
return {"error": f"Game with ID {game_id} not found"}
|
||||
else:
|
||||
return {"error": f"Unknown function: {function_name}"}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error executing IGDB function {function_name}: {e}")
|
||||
return {"error": f"Failed to execute {function_name}: {str(e)}"}
|
||||
|
||||
Reference in New Issue
Block a user