Skip to content

Commit 6c6ea2c

Browse files
committed
fixed, optimized, and organized api code for sending image meta data back to client
1 parent f1ad448 commit 6c6ea2c

File tree

3 files changed

+239
-115
lines changed

3 files changed

+239
-115
lines changed

api/llm/agent.py

Lines changed: 137 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import atexit
22
import os
3-
import re
43
from datetime import datetime
54
from functools import lru_cache
65
from typing import List, Optional
@@ -12,12 +11,15 @@
1211

1312
from llm.prompt import system_message
1413
from llm.tools import initialize_tools
14+
from llm.utils import cleanup_old_tool_results, get_tool_result
1515

1616
load_dotenv()
1717

1818
# Global agent instance
1919
_agent_executor = None
2020
_checkpointer = None
21+
# Counter for periodic cleanup
22+
_request_count = 0
2123

2224

2325
@lru_cache
@@ -70,6 +72,125 @@ def get_agent():
7072
return _agent_executor
7173

7274

75+
def _build_message_with_context(message: str, selected_images: Optional[List[dict]]) -> str:
76+
"""Build the full message with image context if provided."""
77+
if not selected_images or len(selected_images) == 0:
78+
return message
79+
80+
image_context = "\n\nSelected Images:\n"
81+
for i, img in enumerate(selected_images, 1):
82+
image_context += f"{i}. {img.get('title', 'Untitled')} (ID: {img.get('id', 'unknown')})\n"
83+
image_context += f" Type: {img.get('type', 'unknown')}\n"
84+
image_context += f" Description: {img.get('description', 'No description')}\n"
85+
if img.get("url"):
86+
image_context += f" URL: {img.get('url')}\n"
87+
image_context += "\n"
88+
89+
return message + image_context
90+
91+
92+
def _extract_agent_response(response) -> str:
93+
"""Extract the agent's response text from the response object."""
94+
if not response or "messages" not in response or len(response["messages"]) == 0:
95+
return "I'm sorry, I couldn't process your request. Please try again."
96+
97+
last_message = response["messages"][-1]
98+
99+
# Handle None or unexpected message types
100+
if last_message is None:
101+
return "I'm sorry, I couldn't process your request. Please try again."
102+
103+
# Handle both AIMessage objects and dictionaries
104+
if hasattr(last_message, "content"):
105+
content = last_message.content
106+
if content is None:
107+
return "I'm sorry, I couldn't process your request. Please try again."
108+
return content
109+
elif isinstance(last_message, dict) and "content" in last_message:
110+
content = last_message["content"]
111+
if content is None:
112+
return "I'm sorry, I couldn't process your request. Please try again."
113+
return content
114+
115+
return "I'm sorry, I couldn't process your request. Please try again."
116+
117+
118+
def _generate_presigned_url(user_id: str, image_id: str) -> Optional[str]:
119+
"""Generate a presigned URL for an image."""
120+
import boto3
121+
122+
s3_client = boto3.client(
123+
"s3",
124+
region_name=os.environ.get("AWS_REGION", "us-east-1"),
125+
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
126+
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
127+
)
128+
129+
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
130+
if not bucket_name:
131+
print("[AGENT] AWS_S3_BUCKET_NAME not set")
132+
return None
133+
134+
try:
135+
s3_key = f"users/{user_id}/images/{image_id}"
136+
print(f"[AGENT] Generating presigned URL for S3 key: {s3_key}")
137+
138+
presigned_url = s3_client.generate_presigned_url(
139+
"get_object",
140+
Params={"Bucket": bucket_name, "Key": s3_key},
141+
ExpiresIn=7200, # 2 hours
142+
)
143+
print(f"[AGENT] Generated presigned URL: {presigned_url[:50]}...")
144+
return presigned_url
145+
146+
except Exception as e:
147+
print(f"[AGENT] Error generating presigned URL: {e}")
148+
return None
149+
150+
151+
def _process_generated_image(user_id: str, tool_result: dict) -> Optional[dict]:
152+
"""Process a generated image tool result and return image data."""
153+
image_id = tool_result.get("image_id")
154+
title = tool_result.get("title", "Generated Image")
155+
prompt = tool_result.get("prompt", "Based on your request")
156+
157+
if not image_id:
158+
print("[AGENT] No image_id found in tool result")
159+
return None
160+
161+
print(f"[AGENT] Processing generated image with ID: {image_id}")
162+
163+
# Generate presigned URL
164+
presigned_url = _generate_presigned_url(user_id, image_id)
165+
if not presigned_url:
166+
return None
167+
168+
# Create image data structure using data from tool result
169+
generated_image_data = {
170+
"id": image_id,
171+
"url": presigned_url,
172+
"title": title,
173+
"description": f"AI-generated image: {prompt}",
174+
"timestamp": datetime.now().isoformat(),
175+
"type": "generated",
176+
}
177+
178+
print(f"[AGENT] Created generated_image_data: {generated_image_data}")
179+
return generated_image_data
180+
181+
182+
def _process_tool_results(user_id: str) -> Optional[dict]:
183+
"""Process any tool results for the user and return generated image data if found."""
184+
print(f"[AGENT] Checking for tool results for user {user_id}")
185+
tool_result = get_tool_result(user_id, "generate_image")
186+
187+
if tool_result:
188+
print(f"[AGENT] Found tool result: {tool_result}")
189+
return _process_generated_image(user_id, tool_result)
190+
191+
return None
192+
193+
73194
def chat_with_agent(message: str, user_id: str = "default", selected_images: Optional[List[dict]] = None) -> tuple[str, Optional[dict]]:
74195
"""
75196
Send a message to the agent and get a response.
@@ -82,21 +203,19 @@ def chat_with_agent(message: str, user_id: str = "default", selected_images: Opt
82203
Returns:
83204
Tuple of (agent_response, generated_image_data)
84205
"""
206+
global _request_count
207+
208+
# Periodic cleanup every 10 requests
209+
_request_count += 1
210+
if _request_count % 10 == 0:
211+
print(f"[AGENT] Running periodic cleanup (request #{_request_count})")
212+
cleanup_old_tool_results()
213+
85214
print(f"[AGENT] Starting chat_with_agent - user_id: {user_id}, message: {message[:100]}...")
86215
agent = get_agent()
87216

88217
# Prepare the message with context
89-
full_message = message
90-
if selected_images and len(selected_images) > 0:
91-
image_context = "\n\nSelected Images:\n"
92-
for i, img in enumerate(selected_images, 1):
93-
image_context += f"{i}. {img.get('title', 'Untitled')} (ID: {img.get('id', 'unknown')})\n"
94-
image_context += f" Type: {img.get('type', 'unknown')}\n"
95-
image_context += f" Description: {img.get('description', 'No description')}\n"
96-
if img.get("url"):
97-
image_context += f" URL: {img.get('url')}\n"
98-
image_context += "\n"
99-
full_message = message + image_context
218+
full_message = _build_message_with_context(message, selected_images)
100219

101220
# Configure thread ID for conversation continuity
102221
config = {"configurable": {"thread_id": user_id}}
@@ -106,100 +225,12 @@ def chat_with_agent(message: str, user_id: str = "default", selected_images: Opt
106225
response = agent.invoke({"messages": [{"role": "user", "content": full_message}]}, config=config)
107226
print(f"[AGENT] Agent response received: {type(response)}")
108227

109-
# Extract the last message from the agent
110-
agent_response = "I'm sorry, I couldn't process your request. Please try again."
111-
generated_image_data = None
112-
113-
if response and "messages" in response and len(response["messages"]) > 0:
114-
print(f"[AGENT] Found {len(response['messages'])} messages in response")
115-
last_message = response["messages"][-1]
116-
print(f"[AGENT] Last message type: {type(last_message)}")
117-
# Handle both AIMessage objects and dictionaries
118-
if hasattr(last_message, "content"):
119-
agent_response = last_message.content
120-
elif isinstance(last_message, dict) and "content" in last_message:
121-
agent_response = last_message["content"]
122-
print(f"[AGENT] Extracted agent response: {agent_response[:100]}...")
123-
124-
# Check if any tools were used (image generation)
125-
print(f"[AGENT] Checking intermediate steps: {response.get('intermediate_steps', [])}")
126-
if "intermediate_steps" in response and response["intermediate_steps"]:
127-
print(f"[AGENT] Found {len(response['intermediate_steps'])} intermediate steps")
128-
for i, step in enumerate(response["intermediate_steps"]):
129-
print(f"[AGENT] Step {i}: {step}")
130-
if len(step) >= 2 and "generate_image" in str(step[0]):
131-
# Extract image data from the tool result
132-
tool_result = step[1]
133-
print(f"[AGENT] Found generate_image tool result: {tool_result}")
134-
135-
# Try multiple patterns to find the image ID
136-
image_id = None
137-
title = "Generated Image"
138-
139-
# Pattern 1: "Image ID: uuid"
140-
match = re.search(r"Image ID: ([a-f0-9-]+)", tool_result)
141-
if match:
142-
image_id = match.group(1)
143-
144-
# Pattern 2: "ID: uuid"
145-
if not image_id:
146-
match = re.search(r"ID: ([a-f0-9-]+)", tool_result)
147-
if match:
148-
image_id = match.group(1)
149-
150-
# Extract title if present
151-
title_match = re.search(r"Title: (.+?)(?:\n|$)", tool_result)
152-
if title_match:
153-
title = title_match.group(1)
154-
155-
if image_id:
156-
print(f"[AGENT] Found image_id: {image_id}, attempting to get S3 metadata")
157-
# Get metadata from S3
158-
import boto3
159-
160-
s3_client = boto3.client(
161-
"s3",
162-
region_name=os.environ.get("AWS_REGION", "us-east-1"),
163-
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
164-
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
165-
)
166-
167-
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
168-
print(f"[AGENT] Using bucket: {bucket_name}")
169-
if bucket_name:
170-
try:
171-
# Get metadata from S3
172-
s3_key = f"users/{user_id}/images/{image_id}"
173-
print(f"[AGENT] Getting metadata for S3 key: {s3_key}")
174-
metadata_response = s3_client.head_object(Bucket=bucket_name, Key=s3_key)
175-
metadata = metadata_response.get("Metadata", {})
176-
print(f"[AGENT] Retrieved metadata: {metadata}")
177-
178-
# Generate presigned URL
179-
presigned_url = s3_client.generate_presigned_url(
180-
"get_object",
181-
Params={"Bucket": bucket_name, "Key": f"users/{user_id}/images/{image_id}"},
182-
ExpiresIn=7200, # 2 hours
183-
)
184-
print(f"[AGENT] Generated presigned URL: {presigned_url[:50]}...")
185-
186-
generated_image_data = {
187-
"id": image_id,
188-
"url": presigned_url,
189-
"title": metadata.get("title", title),
190-
"description": f"AI-generated image: {metadata.get('generationPrompt', 'Based on your request')}",
191-
"timestamp": metadata.get("uploadedAt", datetime.now().isoformat()),
192-
"type": "generated",
193-
}
194-
print(f"[AGENT] Created generated_image_data: {generated_image_data}")
195-
196-
except Exception as e:
197-
print(f"[AGENT] Error getting S3 metadata: {e}")
198-
# Don't return image data if we can't get a valid URL
199-
generated_image_data = None
200-
# Add error message to agent response
201-
agent_response += "\n\n⚠️ Note: I generated the image successfully, \
202-
but there was an issue retrieving it from the database. Please try again."
228+
# Extract the agent's response
229+
agent_response = _extract_agent_response(response)
230+
print(f"[AGENT] Extracted agent response: {agent_response[:100]}...")
231+
232+
# Check for tool results and process generated images
233+
generated_image_data = _process_tool_results(user_id)
203234

204235
print(f"[AGENT] Returning response - agent_response length: {len(agent_response)}, generated_image_data: {generated_image_data is not None}")
205236
return agent_response, generated_image_data

api/llm/tools.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dotenv import load_dotenv
77
from langchain_core.tools import tool
88

9-
from llm.utils import upload_generated_image_to_s3
9+
from llm.utils import store_tool_result, upload_generated_image_to_s3
1010

1111
load_dotenv()
1212

@@ -82,6 +82,10 @@ def generate_image(
8282
print(f"[TOOL] S3 upload result: {s3_result}")
8383

8484
if s3_result["success"]:
85+
# Store structured result for the agent to retrieve
86+
tool_result = {"image_id": image_id, "title": title, "prompt": prompt, "success": True}
87+
store_tool_result(user_id, "generate_image", tool_result)
88+
8589
result_msg = f"Image generated successfully! User can find it his/her gallery. \
8690
Image ID: {image_id}, Title: {title}"
8791
print(f"[TOOL] Returning success: {result_msg}")
@@ -111,7 +115,8 @@ def generate_image(
111115
{
112116
"prompt": "A woman in a beautiful sunset over a calm ocean",
113117
"user_id": "123",
114-
"image_url": "https://replicate.delivery/pbxt/N55l5TWGh8mSlNzW8usReoaNhGbFwvLeZR3TX1NL4pd2Wtfv/replicate-prediction-f2d25rg6gnrma0cq257vdw2n4c.png",
118+
"image_url": "https://example.com/image.jpg",
119+
"title": "Test Image",
115120
}
116121
)
117122
print(output)

0 commit comments

Comments
 (0)