img-edit-agent-api / llm /agent.py
GitHub Actions
Deploy to HF Spaces: 2025-09-06 20:27:31 UTC
235461a
Raw
History Blame Contribute Delete
7.87 kB
import logging
import os
from datetime import datetime
from typing import List, Optional
from dotenv import load_dotenv
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.prebuilt import create_react_agent
from llm.connection_manager import get_checkpointer
from llm.prompt import system_message
from llm.tools import initialize_tools
from llm.utils import cleanup_old_tool_results, get_tool_result
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Global agent instance
_agent_executor = None
# Counter for periodic cleanup
_request_count = 0
def _get_agent():
"""Get or create the agent instance."""
global _agent_executor
if _agent_executor is None:
# Build LLM
print("[AGENT] building LLM")
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
# Build tools
print("[AGENT] initializing tools")
tools = initialize_tools()
# Create agent with fresh checkpointer
print("[AGENT] creating agent")
_agent_executor = create_react_agent(
llm,
tools=tools,
prompt=system_message,
checkpointer=get_checkpointer(),
)
return _agent_executor
def _build_message_with_context(message: str, selected_images: Optional[List[dict]], user_id: str) -> str:
"""Build the full message with image context if provided."""
if not selected_images or len(selected_images) == 0:
return message
image_context = "\n\nSelected Images:\n"
for i, img in enumerate(selected_images, 1):
image_context += f"{i}. {img.get('title', 'Untitled')} (ID: {img.get('id', 'unknown')})\n"
image_context += f" Type: {img.get('type', 'unknown')}\n"
image_context += f" Description: {img.get('description', 'No description')}\n"
if img.get("url"):
image_context += f" URL: {img.get('url')}\n"
image_context += "\n"
return message + image_context + f"\n\nUser ID: {user_id}"
def _extract_agent_response(response) -> str:
"""Extract the agent's response text from the response object."""
if not response or "messages" not in response or len(response["messages"]) == 0:
return "I'm sorry, I couldn't process your request. Please try again."
last_message = response["messages"][-1]
# Handle None or unexpected message types
if last_message is None:
return "I'm sorry, I couldn't process your request. Please try again."
# Handle both AIMessage objects and dictionaries
if hasattr(last_message, "content"):
content = last_message.content
if content is None:
return "I'm sorry, I couldn't process your request. Please try again."
return content
elif isinstance(last_message, dict) and "content" in last_message:
content = last_message["content"]
if content is None:
return "I'm sorry, I couldn't process your request. Please try again."
return content
return "I'm sorry, I couldn't process your request. Please try again."
def _generate_presigned_url(user_id: str, image_id: str) -> Optional[str]:
"""Generate a presigned URL for an image."""
import boto3
s3_client = boto3.client(
"s3",
region_name=os.environ.get("AWS_REGION", "us-east-1"),
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
)
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
if not bucket_name:
print("[AGENT] AWS_S3_BUCKET_NAME not set")
return None
try:
s3_key = f"users/{user_id}/images/{image_id}"
print(f"[AGENT] Generating presigned URL for S3 key: {s3_key}")
presigned_url = s3_client.generate_presigned_url(
"get_object",
Params={"Bucket": bucket_name, "Key": s3_key},
ExpiresIn=7200, # 2 hours
)
print(f"[AGENT] Generated presigned URL: {presigned_url[:50]}...")
return presigned_url
except Exception as e:
print(f"[AGENT] Error generating presigned URL: {e}")
return None
def _process_generated_image(user_id: str, tool_result: dict) -> Optional[dict]:
"""Process a generated image tool result and return image data."""
image_id = tool_result.get("image_id")
title = tool_result.get("title", "Generated Image")
prompt = tool_result.get("prompt", "Based on your request")
if not image_id:
print("[AGENT] No image_id found in tool result")
return None
print(f"[AGENT] Processing generated image with ID: {image_id}")
# Generate presigned URL
presigned_url = _generate_presigned_url(user_id, image_id)
if not presigned_url:
return None
# Create image data structure using data from tool result
generated_image_data = {
"id": image_id,
"url": presigned_url,
"title": title,
"description": f"AI-generated image: {prompt}",
"timestamp": datetime.now().isoformat(),
"type": "generated",
}
print(f"[AGENT] Created generated_image_data: {generated_image_data}")
return generated_image_data
def _process_tool_results(user_id: str) -> Optional[dict]:
"""Process any tool results for the user and return generated image data if found."""
print(f"[AGENT] Checking for tool results for user {user_id}")
tool_result = get_tool_result(user_id, "generate_image")
if tool_result:
print(f"[AGENT] Found tool result: {tool_result}")
return _process_generated_image(user_id, tool_result)
else:
print(f"[AGENT] No tool result found for user {user_id}")
return None
def chat_with_agent(
message: str,
client_ip: str,
user_id: str = "default",
selected_images: Optional[List[dict]] = None,
) -> tuple[str, Optional[dict]]:
"""
Send a message to the agent and get a response.
Args:
message: The user's message
user_id: Unique identifier for the user/thread
selected_images: List of selected image objects (optional)
client_ip: IP address of the client
Returns:
Tuple of (agent_response, generated_image_data)
"""
global _request_count
# Periodic cleanup every 10 requests
_request_count += 1
if _request_count % 10 == 0:
print(f"[AGENT] Running periodic cleanup (request #{_request_count})")
cleanup_old_tool_results()
print(f"[AGENT] Starting chat_with_agent - user_id: {user_id}, message: {message[:100]}...")
agent = _get_agent()
# Prepare the message with context
print("[AGENT] building message with context")
full_message = _build_message_with_context(message, selected_images, user_id)
# Configure thread ID for conversation continuity
config = {"configurable": {"thread_id": user_id, "client_ip": client_ip}}
# Get response from agent
print(f"[AGENT] Invoking agent with config: {config}")
response = agent.invoke({"messages": [{"role": "user", "content": full_message}]}, config=config)
print(f"[AGENT] Agent response received: {type(response)}")
# Extract the agent's response
agent_response = _extract_agent_response(response)
print(f"[AGENT] Extracted agent response: {agent_response[:100]}...")
# Check for tool results and process generated images
generated_image_data = _process_tool_results(user_id)
print(f"[AGENT] Returning response - agent_response length: {len(agent_response)}, generated_image_data: {generated_image_data is not None}")
return agent_response, generated_image_data
if __name__ == "__main__":
# Test the agent
response = chat_with_agent("Hello! How can you help me with image editing?", "127.0.0.1")
print(response)