File size: 7,866 Bytes
235461a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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)