| 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") |