Add send_tenor_gif tool with AI selection and update GIF search limits
Introduce send_tenor_gif tool that uses AI to select the best GIF from Tenor search results and send it to the channel. Increase the default and maximum limits for Tenor GIF search tools to improve selection quality. Update parameter descriptions and validation accordingly.
This commit is contained in:
parent
bb1c34f0c5
commit
563d7319db
@ -1624,9 +1624,7 @@ def create_tools_list():
|
||||
"required": ["user_id"]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# --- Tenor GIF Search Tool ---
|
||||
) # --- Tenor GIF Search Tool ---
|
||||
tool_declarations.append(
|
||||
FunctionDeclaration(
|
||||
name="search_tenor_gifs",
|
||||
@ -1640,7 +1638,29 @@ def create_tools_list():
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Optional: The maximum number of GIF URLs to return (default 1, max 10)."
|
||||
"description": "Optional: The maximum number of GIF URLs to return (default 8, max 15)."
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# --- Send Tenor GIF with AI Selection Tool ---
|
||||
tool_declarations.append(
|
||||
FunctionDeclaration(
|
||||
name="send_tenor_gif",
|
||||
description="Searches for multiple GIFs, uses AI to analyze and pick the best one, then sends it to the current channel. This is better than search_tenor_gifs when you want to actually send a GIF rather than just get URLs.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query for Tenor GIFs."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Optional: The maximum number of GIFs to consider for AI selection (default 8, max 15). More GIFs give better selection but take longer."
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
|
182
gurt/tools.py
182
gurt/tools.py
@ -2951,11 +2951,11 @@ TOOL_MAPPING = {
|
||||
}
|
||||
|
||||
# --- Tenor GIF Search Tool Implementation ---
|
||||
async def tool_search_tenor_gifs(cog: commands.Cog, query: str, limit: int = 1) -> Dict[str, Any]:
|
||||
async def tool_search_tenor_gifs(cog: commands.Cog, query: str, limit: int = 8) -> Dict[str, Any]:
|
||||
"""Searches Tenor for GIFs and returns a list of URLs."""
|
||||
print(f"Executing tool_search_tenor_gifs: Query='{query}', Limit={limit}")
|
||||
# Ensure limit is within a reasonable range, e.g., 1-10
|
||||
limit = max(1, min(limit, 10))
|
||||
# Ensure limit is within a reasonable range, e.g., 1-15
|
||||
limit = max(1, min(limit, 15))
|
||||
try:
|
||||
gif_urls = await search_tenor_gifs(cog, query, limit=limit)
|
||||
if gif_urls:
|
||||
@ -2990,7 +2990,7 @@ async def tool_search_tenor_gifs(cog: commands.Cog, query: str, limit: int = 1)
|
||||
}
|
||||
|
||||
# --- Tenor GIF Search Tool Implementation ---
|
||||
async def search_tenor_gifs(cog: commands.Cog, query: str, limit: int = 5) -> List[str]:
|
||||
async def search_tenor_gifs(cog: commands.Cog, query: str, limit: int = 8) -> List[str]:
|
||||
"""Searches Tenor for GIFs and returns a list of URLs."""
|
||||
if not cog.TENOR_API_KEY:
|
||||
print("Tenor API key not configured.")
|
||||
@ -3021,5 +3021,179 @@ async def search_tenor_gifs(cog: commands.Cog, query: str, limit: int = 5) -> Li
|
||||
print(f"Exception during Tenor API call: {e}")
|
||||
return []
|
||||
|
||||
# --- Send Tenor GIF Tool with AI Selection ---
|
||||
async def send_tenor_gif(cog: commands.Cog, query: str, limit: int = 8) -> Dict[str, Any]:
|
||||
"""Searches for multiple GIFs, has AI pick the best one, and sends it."""
|
||||
print(f"Executing send_tenor_gif: Query='{query}', Limit={limit}")
|
||||
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from .config import DEFAULT_MODEL, PROJECT_ID, LOCATION
|
||||
|
||||
# Search for GIFs
|
||||
gif_urls = await search_tenor_gifs(cog, query, limit=limit)
|
||||
|
||||
if not gif_urls:
|
||||
return {
|
||||
"status": "no_results",
|
||||
"query": query,
|
||||
"error": f"No Tenor GIFs found for query: {query}",
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# If only one GIF found, use it directly
|
||||
if len(gif_urls) == 1:
|
||||
selected_gif = gif_urls[0]
|
||||
selection_reason = "Only one GIF found"
|
||||
else:
|
||||
# Prepare GIFs for AI selection
|
||||
gif_parts = []
|
||||
for i, gif_url in enumerate(gif_urls):
|
||||
try:
|
||||
# Download GIF data for AI analysis
|
||||
if not cog.session:
|
||||
cog.session = aiohttp.ClientSession()
|
||||
|
||||
async with cog.session.get(gif_url, timeout=15) as response:
|
||||
if response.status == 200:
|
||||
gif_data = await response.read()
|
||||
# Create a Part with file data for the AI
|
||||
gif_parts.append({
|
||||
"index": i,
|
||||
"url": gif_url,
|
||||
"part": types.Part.from_bytes(gif_data, mime_type="image/gif")
|
||||
})
|
||||
else:
|
||||
print(f"Failed to download GIF {i}: {response.status}")
|
||||
except Exception as e:
|
||||
print(f"Error downloading GIF {i}: {e}")
|
||||
continue
|
||||
|
||||
if not gif_parts:
|
||||
return {
|
||||
"status": "error",
|
||||
"query": query,
|
||||
"error": "Failed to download any GIFs for analysis",
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Prepare AI prompt for GIF selection
|
||||
selection_prompt = f"""You are selecting the best GIF from multiple options for the query: "{query}"
|
||||
|
||||
I will show you {len(gif_parts)} GIF options. Please analyze each one and select the BEST match for the query "{query}".
|
||||
|
||||
Consider:
|
||||
- Relevance to the search query
|
||||
- Quality and clarity of the GIF
|
||||
- Appropriateness for a Discord chat
|
||||
- Visual appeal and entertainment value
|
||||
|
||||
Respond with ONLY a JSON object in this exact format:
|
||||
{{
|
||||
"selected_index": <number>,
|
||||
"reason": "<brief explanation of why this GIF is the best choice>"
|
||||
}}
|
||||
|
||||
The selected_index should be a number from 0 to {len(gif_parts)-1}."""
|
||||
|
||||
# Build content with text and GIF parts
|
||||
ai_content = [types.Part.from_text(selection_prompt)]
|
||||
for gif_data in gif_parts:
|
||||
ai_content.append(gif_data["part"])
|
||||
|
||||
# Initialize AI client if needed
|
||||
if not hasattr(cog, 'genai_client') or not cog.genai_client:
|
||||
cog.genai_client = genai.Client(
|
||||
vertexai=True,
|
||||
project=PROJECT_ID,
|
||||
location=LOCATION
|
||||
)
|
||||
|
||||
# Generate AI selection
|
||||
try:
|
||||
model = cog.genai_client.models.get(DEFAULT_MODEL)
|
||||
response = await model.generate_content_async(
|
||||
contents=[types.Content(role="user", parts=ai_content)],
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=0.1, # Low temperature for consistent selection
|
||||
max_output_tokens=150
|
||||
)
|
||||
)
|
||||
|
||||
# Parse AI response
|
||||
ai_text = response.text.strip()
|
||||
|
||||
# Clean up the response to extract JSON
|
||||
if "```json" in ai_text:
|
||||
ai_text = ai_text.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in ai_text:
|
||||
ai_text = ai_text.split("```")[1].strip()
|
||||
|
||||
try:
|
||||
selection_data = json.loads(ai_text)
|
||||
selected_index = selection_data.get("selected_index", 0)
|
||||
selection_reason = selection_data.get("reason", "AI selected this GIF")
|
||||
|
||||
# Validate index
|
||||
if 0 <= selected_index < len(gif_parts):
|
||||
selected_gif = gif_parts[selected_index]["url"]
|
||||
else:
|
||||
# Fallback to first GIF if index is invalid
|
||||
selected_gif = gif_urls[0]
|
||||
selection_reason = "Fallback: AI provided invalid index"
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# Fallback to first GIF if JSON parsing fails
|
||||
selected_gif = gif_urls[0]
|
||||
selection_reason = "Fallback: Could not parse AI selection"
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in AI GIF selection: {e}")
|
||||
# Fallback to first GIF
|
||||
selected_gif = gif_urls[0]
|
||||
selection_reason = f"Fallback: AI selection failed ({str(e)})"
|
||||
|
||||
# Send the selected GIF to the current channel
|
||||
channel = cog.current_channel
|
||||
if channel:
|
||||
try:
|
||||
await channel.send(selected_gif)
|
||||
return {
|
||||
"status": "success",
|
||||
"query": query,
|
||||
"selected_gif": selected_gif,
|
||||
"total_found": len(gif_urls),
|
||||
"selection_reason": selection_reason,
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"query": query,
|
||||
"error": f"Failed to send GIF: {str(e)}",
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"query": query,
|
||||
"error": "No current channel available to send GIF",
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Error in send_tenor_gif: {str(e)}"
|
||||
print(error_message)
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"status": "error",
|
||||
"query": query,
|
||||
"error": error_message,
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Update TOOL_MAPPING to include the new Tenor GIF tool
|
||||
TOOL_MAPPING["search_tenor_gifs"] = tool_search_tenor_gifs
|
||||
TOOL_MAPPING["send_tenor_gif"] = send_tenor_gif
|
||||
|
Loading…
x
Reference in New Issue
Block a user