main
py 163 lines 6.87 KB
Raw
1 import base64
2 import os
3 import uuid
4 from datetime import datetime, timezone
5 from agent import AgentContext, UserMessage, AgentContextType
6 from helpers.api import ApiHandler, Request, Response
7 from helpers import files, projects
8 from helpers.print_style import PrintStyle
9 from helpers.projects import activate_project
10 from helpers.security import safe_filename
11 from initialize import initialize_agent
12
13
14 class ApiMessage(ApiHandler):
15 @classmethod
16 def requires_auth(cls) -> bool:
17 return False # No web auth required
18
19 @classmethod
20 def requires_csrf(cls) -> bool:
21 return False # No CSRF required
22
23 @classmethod
24 def requires_api_key(cls) -> bool:
25 return True # Require API key
26
27 async def process(self, input: dict, request: Request) -> dict | Response:
28 # Extract parameters
29 context_id = input.get("context_id", "")
30 message = input.get("message", "")
31 attachments = input.get("attachments", [])
32 lifetime_hours = input.get("lifetime_hours", 24) # Default 24 hours
33 project_name = input.get("project_name", None)
34 agent_profile = input.get("agent_profile", None)
35 try:
36 lifetime_hours = float(lifetime_hours)
37 if lifetime_hours <= 0:
38 raise ValueError("lifetime_hours must be greater than 0")
39 except (TypeError, ValueError):
40 return Response(
41 '{"error": "lifetime_hours must be a positive number"}',
42 status=400,
43 mimetype="application/json",
44 )
45
46 # Set an agent if profile provided
47 override_settings = {}
48 if agent_profile:
49 override_settings["agent_profile"] = agent_profile
50
51 if not message:
52 return Response('{"error": "Message is required"}', status=400, mimetype="application/json")
53
54 # Handle attachments (base64 encoded)
55 attachment_paths = []
56 if attachments:
57 upload_folder_int = "/a0/usr/uploads"
58 upload_folder_ext = files.get_abs_path("usr/uploads")
59 os.makedirs(upload_folder_ext, exist_ok=True)
60
61 for attachment in attachments:
62 if not isinstance(attachment, dict) or "filename" not in attachment or "base64" not in attachment:
63 continue
64
65 try:
66 filename = safe_filename(attachment["filename"])
67 if not filename:
68 raise ValueError("Invalid filename")
69
70 # Decode base64 content
71 file_content = base64.b64decode(attachment["base64"])
72
73 # Save to temp file
74 save_path = os.path.join(upload_folder_ext, filename)
75 with open(save_path, "wb") as f:
76 f.write(file_content)
77
78 attachment_paths.append(os.path.join(upload_folder_int, filename))
79 except Exception as e:
80 PrintStyle.error(f"Failed to process attachment {attachment.get('filename', 'unknown')}: {e}")
81 continue
82
83 # Get or create context
84 if context_id:
85 context = AgentContext.use(context_id)
86 if not context:
87 return Response('{"error": "Context not found"}', status=404, mimetype="application/json")
88
89 # Validation: if agent profile is provided, it must match the exising
90 if agent_profile and context.agent0.config.profile != agent_profile:
91 return Response('{"error": "Cannot override agent profile on existing context"}', status=400, mimetype="application/json")
92
93
94 # Validation: if project is provided but context already has different project
95 existing_project = context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
96 if project_name and existing_project and existing_project != project_name:
97 return Response('{"error": "Project can only be set on first message"}', status=400, mimetype="application/json")
98 else:
99 config = initialize_agent(override_settings=override_settings)
100 context = AgentContext(config=config, type=AgentContextType.USER)
101 AgentContext.use(context.id)
102 context_id = context.id
103 # Activate project if provided
104 if project_name:
105 try:
106 activate_project(context_id, project_name)
107 except Exception as e:
108 # Handle project or context errors more gracefully
109 error_msg = str(e)
110 PrintStyle.error(f"Failed to activate project '{project_name}' for context '{context_id}': {error_msg}")
111 return Response(
112 f'{{"error": "Failed to activate project \\"{project_name}\\""}}',
113 status=500,
114 mimetype="application/json",
115 )
116
117 # Activate project if provided
118 if project_name:
119 try:
120 projects.activate_project(context_id, project_name)
121 except Exception as e:
122 return Response(f'{{"error": "Failed to activate project: {str(e)}"}}', status=400, mimetype="application/json")
123
124 # Persist API chat lifetime in context data so cleanup survives restarts.
125 context.set_data("lifetime_hours", lifetime_hours)
126 context.last_message = datetime.now(timezone.utc)
127
128 # Process message
129 try:
130 # Log the message
131 attachment_filenames = [os.path.basename(path) for path in attachment_paths] if attachment_paths else []
132
133 PrintStyle(
134 background_color="#6C3483", font_color="white", bold=True, padding=True
135 ).print("External API message:")
136 PrintStyle(font_color="white", padding=False).print(f"> {message}")
137 if attachment_filenames:
138 PrintStyle(font_color="white", padding=False).print("Attachments:")
139 for filename in attachment_filenames:
140 PrintStyle(font_color="white", padding=False).print(f"- {filename}")
141
142 # Add user message to chat history so it's visible in the UI
143 msg_id = str(uuid.uuid4())
144 context.log.log(
145 type="user",
146 heading="",
147 content=message,
148 kvps={"attachments": attachment_filenames},
149 id=msg_id,
150 )
151
152 # Send message to agent
153 task = context.communicate(UserMessage(message=message, attachments=attachment_paths, id=msg_id))
154 result = await task.result()
155
156 return {
157 "context_id": context_id,
158 "response": result
159 }
160
161 except Exception as e:
162 PrintStyle.error(f"External API error: {e}")
163 return Response(f'{{"error": "{str(e)}"}}', status=500, mimetype="application/json")