11import atexit
22import os
3- import re
43from datetime import datetime
54from functools import lru_cache
65from typing import List , Optional
1211
1312from llm .prompt import system_message
1413from llm .tools import initialize_tools
14+ from llm .utils import cleanup_old_tool_results , get_tool_result
1515
1616load_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 \n Selected 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+
73194def 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 \n Selected 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
0 commit comments