feat: External API Endpoints

Rafael Uzarowski committed Jul 27, 2025 at 14:40 UTC e74dabb40ac90aa46d8f66ef9db47a3736deebed
11 files changed +1187 -11
python/api/api_files_get.py new
+95
@@ -0,0 +1,95 @@
1 +import base64
2 +import os
3 +from python.helpers.api import ApiHandler, Request, Response
4 +from python.helpers import files
5 +from python.helpers.print_style import PrintStyle
6 +import json
7 +
8 +
9 +class ApiFilesGet(ApiHandler):
10 + @classmethod
11 + def requires_auth(cls) -> bool:
12 + return False
13 +
14 + @classmethod
15 + def requires_csrf(cls) -> bool:
16 + return False
17 +
18 + @classmethod
19 + def requires_api_key(cls) -> bool:
20 + return True
21 +
22 + @classmethod
23 + def get_methods(cls) -> list[str]:
24 + return ["POST"]
25 +
26 + async def process(self, input: dict, request: Request) -> dict | Response:
27 + try:
28 + # Get paths from input
29 + paths = input.get("paths", [])
30 +
31 + if not paths:
32 + return Response(
33 + '{"error": "paths array is required"}',
34 + status=400,
35 + mimetype="application/json"
36 + )
37 +
38 + if not isinstance(paths, list):
39 + return Response(
40 + '{"error": "paths must be an array"}',
41 + status=400,
42 + mimetype="application/json"
43 + )
44 +
45 + result = {}
46 +
47 + for path in paths:
48 + try:
49 + # Convert internal paths to external paths
50 + if path.startswith("/a0/tmp/uploads/"):
51 + # Internal path - convert to external
52 + filename = path.replace("/a0/tmp/uploads/", "")
53 + external_path = files.get_abs_path("tmp/uploads", filename)
54 + filename = os.path.basename(external_path)
55 + elif path.startswith("/a0/"):
56 + # Other internal Agent Zero paths
57 + relative_path = path.replace("/a0/", "")
58 + external_path = files.get_abs_path(relative_path)
59 + filename = os.path.basename(external_path)
60 + else:
61 + # Assume it's already an external/absolute path
62 + external_path = path
63 + filename = os.path.basename(path)
64 +
65 + # Check if file exists
66 + if not os.path.exists(external_path):
67 + PrintStyle.warning(f"File not found: {path}")
68 + continue
69 +
70 + # Read and encode file
71 + with open(external_path, "rb") as f:
72 + file_content = f.read()
73 + base64_content = base64.b64encode(file_content).decode('utf-8')
74 + result[filename] = base64_content
75 +
76 + PrintStyle().print(f"Retrieved file: {filename} ({len(file_content)} bytes)")
77 +
78 + except Exception as e:
79 + PrintStyle.error(f"Failed to read file {path}: {str(e)}")
80 + continue
81 +
82 + # Log the retrieval
83 + PrintStyle(
84 + background_color="#2ECC71", font_color="white", bold=True, padding=True
85 + ).print(f"API Files retrieved: {len(result)} files")
86 +
87 + return result
88 +
89 + except Exception as e:
90 + PrintStyle.error(f"API files get error: {str(e)}")
91 + return Response(
92 + json.dumps({"error": f"Internal server error: {str(e)}"}),
93 + status=500,
94 + mimetype="application/json"
95 + )
python/api/api_log_get.py new
+64
@@ -0,0 +1,64 @@
1 +from agent import AgentContext
2 +from python.helpers.api import ApiHandler, Request, Response
3 +
4 +
5 +class ApiLogGet(ApiHandler):
6 + @classmethod
7 + def get_methods(cls) -> list[str]:
8 + return ["GET", "POST"]
9 +
10 + @classmethod
11 + def requires_auth(cls) -> bool:
12 + return False # No web auth required
13 +
14 + @classmethod
15 + def requires_csrf(cls) -> bool:
16 + return False # No CSRF required
17 +
18 + @classmethod
19 + def requires_api_key(cls) -> bool:
20 + return True # Require API key
21 +
22 + async def process(self, input: dict, request: Request) -> dict | Response:
23 + # Extract parameters (support both query params for GET and body for POST)
24 + if request.method == "GET":
25 + context_id = request.args.get("context_id", "")
26 + length = int(request.args.get("length", 100))
27 + else:
28 + context_id = input.get("context_id", "")
29 + length = input.get("length", 100)
30 +
31 + if not context_id:
32 + return Response('{"error": "context_id is required"}', status=400, mimetype="application/json")
33 +
34 + # Get context
35 + context = AgentContext.get(context_id)
36 + if not context:
37 + return Response('{"error": "Context not found"}', status=404, mimetype="application/json")
38 +
39 + try:
40 + # Get total number of log items
41 + total_items = len(context.log.logs)
42 +
43 + # Calculate start position (from newest, so we work backwards)
44 + start_pos = max(0, total_items - length)
45 +
46 + # Get log items from the calculated start position
47 + log_items = context.log.output(start=start_pos)
48 +
49 + # Return log data with metadata
50 + return {
51 + "context_id": context_id,
52 + "log": {
53 + "guid": context.log.guid,
54 + "total_items": total_items,
55 + "returned_items": len(log_items),
56 + "start_position": start_pos,
57 + "progress": context.log.progress,
58 + "progress_active": context.log.progress_active,
59 + "items": log_items
60 + }
61 + }
62 +
63 + except Exception as e:
64 + return Response(f'{{"error": "{str(e)}"}}', status=500, mimetype="application/json")
python/api/api_message.py new
+140
@@ -0,0 +1,140 @@
1 +import base64
2 +import os
3 +from datetime import datetime, timedelta
4 +from agent import AgentContext, UserMessage, AgentContextType
5 +from python.helpers.api import ApiHandler, Request, Response
6 +from python.helpers import files
7 +from python.helpers.print_style import PrintStyle
8 +from werkzeug.utils import secure_filename
9 +from initialize import initialize_agent
10 +import threading
11 +
12 +
13 +class ApiMessage(ApiHandler):
14 + # Track chat lifetimes for cleanup
15 + _chat_lifetimes = {}
16 + _cleanup_lock = threading.Lock()
17 +
18 + @classmethod
19 + def requires_auth(cls) -> bool:
20 + return False # No web auth required
21 +
22 + @classmethod
23 + def requires_csrf(cls) -> bool:
24 + return False # No CSRF required
25 +
26 + @classmethod
27 + def requires_api_key(cls) -> bool:
28 + return True # Require API key
29 +
30 + async def process(self, input: dict, request: Request) -> dict | Response:
31 + # Extract parameters
32 + context_id = input.get("context_id", "")
33 + message = input.get("message", "")
34 + attachments = input.get("attachments", [])
35 + lifetime_hours = input.get("lifetime_hours", 24) # Default 24 hours
36 +
37 + if not message:
38 + return Response('{"error": "Message is required"}', status=400, mimetype="application/json")
39 +
40 + # Handle attachments (base64 encoded)
41 + attachment_paths = []
42 + if attachments:
43 + upload_folder_int = "/a0/tmp/uploads"
44 + upload_folder_ext = files.get_abs_path("tmp/uploads")
45 + os.makedirs(upload_folder_ext, exist_ok=True)
46 +
47 + for attachment in attachments:
48 + if not isinstance(attachment, dict) or "filename" not in attachment or "base64" not in attachment:
49 + continue
50 +
51 + try:
52 + filename = secure_filename(attachment["filename"])
53 + if not filename:
54 + continue
55 +
56 + # Decode base64 content
57 + file_content = base64.b64decode(attachment["base64"])
58 +
59 + # Save to temp file
60 + save_path = os.path.join(upload_folder_ext, filename)
61 + with open(save_path, "wb") as f:
62 + f.write(file_content)
63 +
64 + attachment_paths.append(os.path.join(upload_folder_int, filename))
65 + except Exception as e:
66 + PrintStyle.error(f"Failed to process attachment {attachment.get('filename', 'unknown')}: {e}")
67 + continue
68 +
69 + # Get or create context
70 + if context_id:
71 + context = AgentContext.get(context_id)
72 + if not context:
73 + return Response('{"error": "Context not found"}', status=404, mimetype="application/json")
74 + else:
75 + config = initialize_agent()
76 + context = AgentContext(config=config, type=AgentContextType.USER)
77 + context_id = context.id
78 +
79 + # Update chat lifetime
80 + with self._cleanup_lock:
81 + self._chat_lifetimes[context_id] = datetime.now() + timedelta(hours=lifetime_hours)
82 +
83 + # Process message
84 + try:
85 + # Log the message
86 + attachment_filenames = [os.path.basename(path) for path in attachment_paths] if attachment_paths else []
87 +
88 + PrintStyle(
89 + background_color="#6C3483", font_color="white", bold=True, padding=True
90 + ).print("External API message:")
91 + PrintStyle(font_color="white", padding=False).print(f"> {message}")
92 + if attachment_filenames:
93 + PrintStyle(font_color="white", padding=False).print("Attachments:")
94 + for filename in attachment_filenames:
95 + PrintStyle(font_color="white", padding=False).print(f"- {filename}")
96 +
97 + # Add user message to chat history so it's visible in the UI
98 + context.log.log(
99 + type="user",
100 + heading="User message",
101 + content=message,
102 + kvps={"attachments": attachment_filenames},
103 + )
104 +
105 + # Send message to agent
106 + task = context.communicate(UserMessage(message, attachment_paths))
107 + result = await task.result()
108 +
109 + # Clean up expired chats
110 + self._cleanup_expired_chats()
111 +
112 + return {
113 + "context_id": context_id,
114 + "response": result
115 + }
116 +
117 + except Exception as e:
118 + PrintStyle.error(f"External API error: {e}")
119 + return Response(f'{{"error": "{str(e)}"}}', status=500, mimetype="application/json")
120 +
121 + @classmethod
122 + def _cleanup_expired_chats(cls):
123 + """Clean up expired chats"""
124 + with cls._cleanup_lock:
125 + now = datetime.now()
126 + expired_contexts = [
127 + context_id for context_id, expiry in cls._chat_lifetimes.items()
128 + if now > expiry
129 + ]
130 +
131 + for context_id in expired_contexts:
132 + try:
133 + context = AgentContext.get(context_id)
134 + if context:
135 + context.reset()
136 + AgentContext.remove(context_id)
137 + del cls._chat_lifetimes[context_id]
138 + PrintStyle().print(f"Cleaned up expired chat: {context_id}")
139 + except Exception as e:
140 + PrintStyle.error(f"Failed to cleanup chat {context_id}: {e}")
python/api/api_reset_chat.py new
+69
@@ -0,0 +1,69 @@
1 +from agent import AgentContext
2 +from python.helpers.api import ApiHandler, Request, Response
3 +from python.helpers.print_style import PrintStyle
4 +from python.helpers import persist_chat
5 +import json
6 +
7 +
8 +class ApiResetChat(ApiHandler):
9 + @classmethod
10 + def requires_auth(cls) -> bool:
11 + return False
12 +
13 + @classmethod
14 + def requires_csrf(cls) -> bool:
15 + return False
16 +
17 + @classmethod
18 + def requires_api_key(cls) -> bool:
19 + return True
20 +
21 + @classmethod
22 + def get_methods(cls) -> list[str]:
23 + return ["POST"]
24 +
25 + async def process(self, input: dict, request: Request) -> dict | Response:
26 + try:
27 + # Get context_id from input
28 + context_id = input.get("context_id")
29 +
30 + if not context_id:
31 + return Response(
32 + '{"error": "context_id is required"}',
33 + status=400,
34 + mimetype="application/json"
35 + )
36 +
37 + # Check if context exists
38 + context = AgentContext.get(context_id)
39 + if not context:
40 + return Response(
41 + '{"error": "Chat context not found"}',
42 + status=404,
43 + mimetype="application/json"
44 + )
45 +
46 + # Reset the chat context (clears history but keeps context alive)
47 + context.reset()
48 + # Save the reset context to persist the changes
49 + persist_chat.save_tmp_chat(context)
50 +
51 + # Log the reset
52 + PrintStyle(
53 + background_color="#3498DB", font_color="white", bold=True, padding=True
54 + ).print(f"API Chat reset: {context_id}")
55 +
56 + # Return success response
57 + return {
58 + "success": True,
59 + "message": "Chat reset successfully",
60 + "context_id": context_id
61 + }
62 +
63 + except Exception as e:
64 + PrintStyle.error(f"API reset chat error: {str(e)}")
65 + return Response(
66 + json.dumps({"error": f"Internal server error: {str(e)}"}),
67 + status=500,
68 + mimetype="application/json"
69 + )
python/api/api_terminate_chat.py new
+68
@@ -0,0 +1,68 @@
1 +from agent import AgentContext
2 +from python.helpers.api import ApiHandler, Request, Response
3 +from python.helpers.persist_chat import remove_chat
4 +from python.helpers.print_style import PrintStyle
5 +import json
6 +
7 +
8 +class ApiTerminateChat(ApiHandler):
9 + @classmethod
10 + def requires_auth(cls) -> bool:
11 + return False
12 +
13 + @classmethod
14 + def requires_csrf(cls) -> bool:
15 + return False
16 +
17 + @classmethod
18 + def requires_api_key(cls) -> bool:
19 + return True
20 +
21 + @classmethod
22 + def get_methods(cls) -> list[str]:
23 + return ["POST"]
24 +
25 + async def process(self, input: dict, request: Request) -> dict | Response:
26 + try:
27 + # Get context_id from input
28 + context_id = input.get("context_id")
29 +
30 + if not context_id:
31 + return Response(
32 + '{"error": "context_id is required"}',
33 + status=400,
34 + mimetype="application/json"
35 + )
36 +
37 + # Check if context exists
38 + context = AgentContext.get(context_id)
39 + if not context:
40 + return Response(
41 + '{"error": "Chat context not found"}',
42 + status=404,
43 + mimetype="application/json"
44 + )
45 +
46 + # Delete the chat context
47 + AgentContext.remove(context.id)
48 + remove_chat(context.id)
49 +
50 + # Log the deletion
51 + PrintStyle(
52 + background_color="#E74C3C", font_color="white", bold=True, padding=True
53 + ).print(f"API Chat deleted: {context_id}")
54 +
55 + # Return success response
56 + return {
57 + "success": True,
58 + "message": "Chat deleted successfully",
59 + "context_id": context_id
60 + }
61 +
62 + except Exception as e:
63 + PrintStyle.error(f"API terminate chat error: {str(e)}")
64 + return Response(
65 + json.dumps({"error": f"Internal server error: {str(e)}"}),
66 + status=500,
67 + mimetype="application/json"
68 + )
python/helpers/settings.py
+25 -2
@@ -67,7 +67,7 @@ class Settings(TypedDict):
67 memory_memorize_enabled: bool
68 memory_memorize_consolidation: bool
69 memory_memorize_replace_threshold: float
70 -
70 +
71
72 api_keys: dict[str, str]
73
@@ -880,7 +880,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
880
881 # TTS fields
882 tts_fields: list[SettingsField] = []
883 -
883 +
884 tts_fields.append(
885 {
886 "id": "tts_kokoro",
@@ -982,6 +982,28 @@ def convert_out(settings: Settings) -> SettingsOutput:
982 "tab": "mcp",
983 }
984
985 + # External API section
986 + external_api_fields: list[SettingsField] = []
987 +
988 + external_api_fields.append(
989 + {
990 + "id": "external_api_examples",
991 + "title": "API Examples",
992 + "description": "View examples for using Agent Zero's external API endpoints with API key authentication.",
993 + "type": "button",
994 + "value": "Show API Examples",
995 + }
996 + )
997 +
998 + external_api_section: SettingsSection = {
999 + "id": "external_api",
1000 + "title": "External API",
1001 + "description": "Agent Zero provides external API endpoints for integration with other applications. "
1002 + "These endpoints use API key authentication and support text messages and file attachments.",
1003 + "fields": external_api_fields,
1004 + "tab": "external",
1005 + }
1006 +
1007 # Backup & Restore section
1008 backup_fields: list[SettingsField] = []
1009
@@ -1030,6 +1052,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
1052 auth_section,
1053 mcp_client_section,
1054 mcp_server_section,
1055 + external_api_section,
1056 backup_section,
1057 dev_section,
1058 ]
run_ui.py
+8 -8
@@ -1,18 +1,15 @@
1 from datetime import timedelta
2 import os
3 import secrets
4 -import sys
4 import time
5 import socket
6 import struct
7 from functools import wraps
8 import threading
10 -import signal
11 -from typing import override
9 from flask import Flask, request, Response, session
10 from flask_basicauth import BasicAuth
11 import initialize
15 -from python.helpers import errors, files, git, mcp_server
12 +from python.helpers import files, git, mcp_server
13 from python.helpers.files import get_abs_path
14 from python.helpers import runtime, dotenv, process
15 from python.helpers.extract_tools import load_classes_from_folder
@@ -81,14 +78,17 @@ def is_loopback_address(address):
78 def requires_api_key(f):
79 @wraps(f)
80 async def decorated(*args, **kwargs):
84 - valid_api_key = dotenv.get_dotenv_value("API_KEY")
81 + # Use the auth token from settings (same as MCP server)
82 + from python.helpers.settings import get_settings
83 + valid_api_key = get_settings()["mcp_server_token"]
84 +
85 if api_key := request.headers.get("X-API-KEY"):
86 if api_key != valid_api_key:
87 - return Response("API key required", 401)
87 + return Response("Invalid API key", 401)
88 elif request.json and request.json.get("api_key"):
89 api_key = request.json.get("api_key")
90 if api_key != valid_api_key:
91 - return Response("API key required", 401)
91 + return Response("Invalid API key", 401)
92 else:
93 return Response("API key required", 401)
94 return await f(*args, **kwargs)
@@ -171,7 +171,7 @@ def run():
171 from werkzeug.serving import WSGIRequestHandler
172 from werkzeug.serving import make_server
173 from werkzeug.middleware.dispatcher import DispatcherMiddleware
174 - from a2wsgi import ASGIMiddleware, WSGIMiddleware
174 + from a2wsgi import ASGIMiddleware
175
176 PrintStyle().print("Starting server...")
177
webui/components/settings/external/api-examples.html new
+678
@@ -0,0 +1,678 @@
1 +<html>
2 +
3 +<head>
4 + <title>Agent Zero External API Examples</title>
5 +</head>
6 +
7 +<body>
8 + <div x-data>
9 + <p>Agent Zero provides external API endpoints for integration with other applications.</p>
10 + <p>These endpoints use API key authentication and support text messages and file attachments.</p>
11 +
12 + <!-- API Token Information -->
13 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 16px 0;">
14 + <h4 style="margin: 0 0 8px 0; color: var(--color-text-primary);">API Token Information</h4>
15 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
16 + The API token is automatically generated from your username and password.
17 + This same token is used for both MCP server connections and external API endpoints.
18 + The token changes when you update your credentials.
19 + </p>
20 + </div>
21 +
22 + <!-- Section 1: api_message Endpoint -->
23 + <div class="api-section">
24 + <h2>POST /api_message</h2>
25 + <p style="margin: 0 0 16px 0; color: var(--color-text-secondary); font-size: 14px;">
26 + Send messages to Agent Zero and receive responses. Supports text messages, file attachments, and conversation continuity.
27 + </p>
28 +
29 + <!-- API Reference -->
30 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">API Reference</h3>
31 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 0 0 20px 0;">
32 + <p style="margin: 0 0 8px 0; color: var(--color-text-secondary); font-size: 14px;">
33 + <strong>Parameters:</strong><br>
34 + • <code>context_id</code> (string, optional): Existing chat context ID<br>
35 + • <code>message</code> (string, required): The message to send<br>
36 + • <code>attachments</code> (array, optional): Array of {filename, base64} objects<br>
37 + • <code>lifetime_hours</code> (number, optional): Chat lifetime in hours (default: 24)
38 + </p>
39 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
40 + <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code>
41 + </p>
42 + </div>
43 +
44 + <!-- JavaScript Examples -->
45 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">JavaScript Examples</h3>
46 +
47 + <h4>Basic Usage Example</h4>
48 + <div id="api-basic-example"></div>
49 +
50 + <h4>Conversation Continuation Example</h4>
51 + <div id="api-continuation-example"></div>
52 +
53 + <h4>File Attachment Example</h4>
54 + <div id="api-attachment-example"></div>
55 + </div>
56 +
57 + <!-- Section 2: api_log_get Endpoint -->
58 + <div class="api-section">
59 + <h2>GET/POST /api_log_get</h2>
60 + <p style="margin: 0 0 16px 0; color: var(--color-text-secondary); font-size: 14px;">
61 + Retrieve log data by context ID, limited to a specified number of entries from the newest.
62 + </p>
63 +
64 + <!-- API Reference -->
65 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">API Reference</h3>
66 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 0 0 20px 0;">
67 + <p style="margin: 0 0 8px 0; color: var(--color-text-secondary); font-size: 14px;">
68 + <strong>Parameters:</strong><br>
69 + • <code>context_id</code> (string, required): Context ID to get logs from<br>
70 + • <code>length</code> (integer, optional): Number of log items to return from newest (default: 100)
71 + </p>
72 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
73 + <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code> (for POST)
74 + </p>
75 + </div>
76 +
77 + <!-- JavaScript Examples -->
78 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">JavaScript Examples</h3>
79 +
80 + <h4>GET Request Example</h4>
81 + <div id="api-log-get-example"></div>
82 +
83 + <h4>POST Request Example</h4>
84 + <div id="api-log-post-example"></div>
85 + </div>
86 +
87 + <!-- Section 3: api_terminate_chat Endpoint -->
88 + <div class="api-section">
89 + <h2>POST /api_terminate_chat</h2>
90 + <p style="margin: 0 0 16px 0; color: var(--color-text-secondary); font-size: 14px;">
91 + Terminate and remove a chat context to free up resources. Similar to the MCP finish_chat function.
92 + </p>
93 +
94 + <!-- API Reference -->
95 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">API Reference</h3>
96 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 0 0 20px 0;">
97 + <p style="margin: 0 0 8px 0; color: var(--color-text-secondary); font-size: 14px;">
98 + <strong>Parameters:</strong><br>
99 + • <code>context_id</code> (string, required): Context ID of the chat to terminate
100 + </p>
101 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
102 + <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code>
103 + </p>
104 + </div>
105 +
106 + <!-- JavaScript Examples -->
107 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">JavaScript Examples</h3>
108 + <h4>Basic Termination Examples</h4>
109 + <div id="api-terminate-example"></div>
110 + </div>
111 +
112 + <!-- Section 4: api_reset_chat Endpoint -->
113 + <div class="api-section">
114 + <h2>POST /api_reset_chat</h2>
115 + <p style="margin: 0 0 16px 0; color: var(--color-text-secondary); font-size: 14px;">
116 + Reset a chat context to clear conversation history while keeping the context_id alive for continued use.
117 + </p>
118 +
119 + <!-- API Reference -->
120 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">API Reference</h3>
121 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 0 0 20px 0;">
122 + <p style="margin: 0 0 8px 0; color: var(--color-text-secondary); font-size: 14px;">
123 + <strong>Parameters:</strong><br>
124 + • <code>context_id</code> (string, required): Context ID of the chat to reset
125 + </p>
126 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
127 + <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code>
128 + </p>
129 + </div>
130 +
131 + <!-- JavaScript Examples -->
132 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">JavaScript Examples</h3>
133 + <h4>Basic Reset Examples</h4>
134 + <div id="api-reset-example"></div>
135 + </div>
136 +
137 + <!-- Section 5: api_files_get Endpoint -->
138 + <div class="api-section">
139 + <h2>POST /api_files_get</h2>
140 + <p style="margin: 0 0 16px 0; color: var(--color-text-secondary); font-size: 14px;">
141 + Retrieve file contents by paths, returning files as base64 encoded data. Useful for retrieving uploaded attachments.
142 + </p>
143 +
144 + <!-- API Reference -->
145 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">API Reference</h3>
146 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 0 0 20px 0;">
147 + <p style="margin: 0 0 8px 0; color: var(--color-text-secondary); font-size: 14px;">
148 + <strong>Parameters:</strong><br>
149 + • <code>paths</code> (array, required): Array of file paths to retrieve (e.g., ["/a0/tmp/uploads/file.txt"])
150 + </p>
151 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
152 + <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code>
153 + </p>
154 + </div>
155 +
156 + <!-- JavaScript Examples -->
157 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">JavaScript Examples</h3>
158 + <h4>File Retrieval Examples</h4>
159 + <div id="api-files-get-example"></div>
160 + </div>
161 +
162 + <!-- Section 6: Additional Endpoints -->
163 + <!--
164 + Example template for new endpoint sections:
165 +
166 + <div class="api-section">
167 + <h2>POST /endpoint_name</h2>
168 + <p style="margin: 0 0 16px 0; color: var(--color-text-secondary); font-size: 14px;">
169 + Description of what this endpoint does...
170 + </p>
171 +
172 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">API Reference</h3>
173 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 0 0 20px 0;">
174 + <p style="margin: 0 0 8px 0; color: var(--color-text-secondary); font-size: 14px;">
175 + <strong>Parameters:</strong><br>
176 + • <code>param_name</code> (type, required/optional): Description
177 + </p>
178 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
179 + <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code>
180 + </p>
181 + </div>
182 +
183 + <h3 style="margin: 20px 0 12px 0; color: var(--color-text-primary);">JavaScript Examples</h3>
184 + <h4>Example Title</h4>
185 + <div id="example-id"></div>
186 + </div>
187 + -->
188 +
189 + <script>
190 + setTimeout(() => {
191 + const url = window.location.origin;
192 + const token = settingsModalProxy.settings.sections.filter(x => x.id == "mcp_server")[0].fields.filter(x => x.id == "mcp_server_token")[0].value;
193 +
194 + // Basic usage example
195 + const basicExample = `// Basic message example
196 +async function sendMessage() {
197 + try {
198 + const response = await fetch('${url}/api_message', {
199 + method: 'POST',
200 + headers: {
201 + 'Content-Type': 'application/json',
202 + 'X-API-KEY': '${token}'
203 + },
204 + body: JSON.stringify({
205 + message: "Hello, how can you help me?",
206 + lifetime_hours: 24
207 + })
208 + });
209 +
210 + const data = await response.json();
211 +
212 + if (response.ok) {
213 + console.log('✅ Success!');
214 + console.log('Response:', data.response);
215 + console.log('Context ID:', data.context_id);
216 + return data;
217 + } else {
218 + console.error('❌ Error:', data.error);
219 + return null;
220 + }
221 + } catch (error) {
222 + console.error('❌ Request failed:', error);
223 + return null;
224 + }
225 +}
226 +
227 +// Call the function
228 +sendMessage().then(result => {
229 + if (result) {
230 + console.log('Message sent successfully!');
231 + }
232 +});`;
233 +
234 + // Continuation example
235 + const continuationExample = `// Continue conversation example
236 +async function continueConversation(contextId) {
237 + try {
238 + const response = await fetch('${url}/api_message', {
239 + method: 'POST',
240 + headers: {
241 + 'Content-Type': 'application/json',
242 + 'X-API-KEY': '${token}'
243 + },
244 + body: JSON.stringify({
245 + context_id: contextId,
246 + message: "Can you tell me more about that?",
247 + lifetime_hours: 24
248 + })
249 + });
250 +
251 + const data = await response.json();
252 +
253 + if (response.ok) {
254 + console.log('✅ Continuation Success!');
255 + console.log('Response:', data.response);
256 + return data;
257 + } else {
258 + console.error('❌ Error:', data.error);
259 + return null;
260 + }
261 + } catch (error) {
262 + console.error('❌ Request failed:', error);
263 + return null;
264 + }
265 +}
266 +
267 +// Example: First send a message, then continue the conversation
268 +async function fullConversationExample() {
269 + const firstResult = await sendMessage();
270 + if (firstResult && firstResult.context_id) {
271 + await continueConversation(firstResult.context_id);
272 + }
273 +}
274 +
275 +fullConversationExample();`;
276 +
277 + // Attachment example
278 + const attachmentExample = `// File attachment example
279 +async function sendWithAttachment() {
280 + try {
281 + // Example with text content (convert to base64)
282 + const textContent = "Hello World from attachment!";
283 + const base64Content = btoa(textContent);
284 +
285 + const response = await fetch('${url}/api_message', {
286 + method: 'POST',
287 + headers: {
288 + 'Content-Type': 'application/json',
289 + 'X-API-KEY': '${token}'
290 + },
291 + body: JSON.stringify({
292 + message: "Please analyze this file:",
293 + attachments: [
294 + {
295 + filename: "document.txt",
296 + base64: base64Content
297 + }
298 + ],
299 + lifetime_hours: 12
300 + })
301 + });
302 +
303 + const data = await response.json();
304 +
305 + if (response.ok) {
306 + console.log('✅ File sent successfully!');
307 + console.log('Response:', data.response);
308 + return data;
309 + } else {
310 + console.error('❌ Error:', data.error);
311 + return null;
312 + }
313 + } catch (error) {
314 + console.error('❌ Request failed:', error);
315 + return null;
316 + }
317 +}
318 +
319 +// Call the function
320 +sendWithAttachment();`;
321 +
322 + // Log GET example
323 + const logGetExample = `// Get logs using GET request
324 +async function getLogsGET(contextId, length = 50) {
325 + try {
326 + const params = new URLSearchParams({
327 + context_id: contextId,
328 + length: length.toString()
329 + });
330 +
331 + const response = await fetch('${url}/api_log_get?' + params, {
332 + method: 'GET',
333 + headers: {
334 + 'X-API-KEY': '${token}'
335 + }
336 + });
337 +
338 + const data = await response.json();
339 +
340 + if (response.ok) {
341 + console.log('✅ Logs retrieved successfully!');
342 + console.log('Total items:', data.log.total_items);
343 + console.log('Returned items:', data.log.returned_items);
344 + console.log('Log items:', data.log.items);
345 + return data;
346 + } else {
347 + console.error('❌ Error:', data.error);
348 + return null;
349 + }
350 + } catch (error) {
351 + console.error('❌ Request failed:', error);
352 + return null;
353 + }
354 +}
355 +
356 +// Example usage
357 +getLogsGET('ctx_abc123', 20);`;
358 +
359 + // Log POST example
360 + const logPostExample = `// Get logs using POST request
361 +async function getLogsPOST(contextId, length = 50) {
362 + try {
363 + const response = await fetch('${url}/api_log_get', {
364 + method: 'POST',
365 + headers: {
366 + 'Content-Type': 'application/json',
367 + 'X-API-KEY': '${token}'
368 + },
369 + body: JSON.stringify({
370 + context_id: contextId,
371 + length: length
372 + })
373 + });
374 +
375 + const data = await response.json();
376 +
377 + if (response.ok) {
378 + console.log('✅ Logs retrieved successfully!');
379 + console.log('Context ID:', data.context_id);
380 + console.log('Log GUID:', data.log.guid);
381 + console.log('Total items:', data.log.total_items);
382 + console.log('Returned items:', data.log.returned_items);
383 + console.log('Start position:', data.log.start_position);
384 + console.log('Progress:', data.log.progress);
385 + console.log('Log items:', data.log.items);
386 + return data;
387 + } else {
388 + console.error('❌ Error:', data.error);
389 + return null;
390 + }
391 + } catch (error) {
392 + console.error('❌ Request failed:', error);
393 + return null;
394 + }
395 +}
396 +
397 +// Example usage - get latest 10 log entries
398 +getLogsPOST('ctx_abc123', 10);`;
399 +
400 + // Terminate chat example
401 + const terminateExample = `// Basic terminate chat function
402 +async function terminateChat(contextId) {
403 + try {
404 + const response = await fetch('${url}/api_terminate_chat', {
405 + method: 'POST',
406 + headers: {
407 + 'Content-Type': 'application/json',
408 + 'X-API-KEY': '${token}'
409 + },
410 + body: JSON.stringify({
411 + context_id: contextId
412 + })
413 + });
414 +
415 + const data = await response.json();
416 +
417 + if (response.ok) {
418 + console.log('✅ Chat deleted successfully!');
419 + console.log('Message:', data.message);
420 + return data;
421 + } else {
422 + console.error('❌ Error:', data.error);
423 + return null;
424 + }
425 + } catch (error) {
426 + console.error('❌ Request failed:', error);
427 + return null;
428 + }
429 +}
430 +
431 +// Example 1: Terminate a specific chat
432 +terminateChat('ctx_abc123');
433 +
434 +// Example 2: Complete workflow - send message, then terminate
435 +async function simpleWorkflow() {
436 + // Send a message
437 + const result = await sendMessage();
438 +
439 + if (result && result.context_id) {
440 + console.log('Chat created:', result.context_id);
441 +
442 + // Do some work with the chat...
443 + // await continueConversation(result.context_id);
444 +
445 + // Clean up when done
446 + await terminateChat(result.context_id);
447 + console.log('Chat cleaned up');
448 + }
449 +}
450 +
451 +// Run the workflow
452 +simpleWorkflow();`;
453 +
454 + // Reset chat example
455 + const resetExample = `// Basic reset chat function
456 +async function resetChat(contextId) {
457 + try {
458 + const response = await fetch('${url}/api_reset_chat', {
459 + method: 'POST',
460 + headers: {
461 + 'Content-Type': 'application/json',
462 + 'X-API-KEY': '${token}'
463 + },
464 + body: JSON.stringify({
465 + context_id: contextId
466 + })
467 + });
468 +
469 + const data = await response.json();
470 +
471 + if (response.ok) {
472 + console.log('✅ Chat reset successfully!');
473 + console.log('Message:', data.message);
474 + console.log('Context ID:', data.context_id);
475 + return data;
476 + } else {
477 + console.error('❌ Error:', data.error);
478 + return null;
479 + }
480 + } catch (error) {
481 + console.error('❌ Request failed:', error);
482 + return null;
483 + }
484 +}
485 +
486 +// Example 1: Reset a specific chat
487 +resetChat('ctx_abc123');
488 +
489 +// Example 2: Reset and continue conversation
490 +async function resetAndContinue() {
491 + const contextId = 'ctx_abc123';
492 +
493 + // Reset the chat to clear history
494 + const resetResult = await resetChat(contextId);
495 +
496 + if (resetResult) {
497 + console.log('Chat reset, starting fresh conversation...');
498 +
499 + // Continue with same context_id but fresh history
500 + const response = await fetch('${url}/api_message', {
501 + method: 'POST',
502 + headers: {
503 + 'Content-Type': 'application/json',
504 + 'X-API-KEY': '${token}'
505 + },
506 + body: JSON.stringify({
507 + context_id: contextId, // Same context ID
508 + message: "Hello, this is a fresh start!",
509 + lifetime_hours: 24
510 + })
511 + });
512 +
513 + const data = await response.json();
514 + console.log('New conversation started:', data.response);
515 + }
516 +}
517 +
518 +// Run the example
519 +resetAndContinue();`;
520 +
521 + // Files get example
522 + const filesGetExample = `// Basic file retrieval
523 +async function getFiles(filePaths) {
524 + try {
525 + const response = await fetch('${url}/api_files_get', {
526 + method: 'POST',
527 + headers: {
528 + 'Content-Type': 'application/json',
529 + 'X-API-KEY': '${token}'
530 + },
531 + body: JSON.stringify({
532 + paths: filePaths
533 + })
534 + });
535 +
536 + const data = await response.json();
537 +
538 + if (response.ok) {
539 + console.log('✅ Files retrieved successfully!');
540 + console.log('Retrieved files:', Object.keys(data));
541 +
542 + // Convert base64 back to text for display
543 + for (const [filename, base64Content] of Object.entries(data)) {
544 + try {
545 + const textContent = atob(base64Content);
546 + console.log(\`\${filename}: \${textContent.substring(0, 100)}...\`);
547 + } catch (e) {
548 + console.log(\`\${filename}: Binary file (\${base64Content.length} chars)\`);
549 + }
550 + }
551 +
552 + return data;
553 + } else {
554 + console.error('❌ Error:', data.error);
555 + return null;
556 + }
557 + } catch (error) {
558 + console.error('❌ Request failed:', error);
559 + return null;
560 + }
561 +}
562 +
563 +// Example 1: Get specific files
564 +const filePaths = [
565 + "/a0/tmp/uploads/document.txt",
566 + "/a0/tmp/uploads/data.json"
567 +];
568 +getFiles(filePaths);
569 +
570 +// Example 2: Complete attachment workflow
571 +async function attachmentWorkflow() {
572 + // Step 1: Send message with attachments
573 + const messageResponse = await fetch('${url}/api_message', {
574 + method: 'POST',
575 + headers: {
576 + 'Content-Type': 'application/json',
577 + 'X-API-KEY': '${token}'
578 + },
579 + body: JSON.stringify({
580 + message: "Please analyze this file",
581 + attachments: [{
582 + filename: "test.txt",
583 + base64: btoa("Hello, this is test content!")
584 + }],
585 + lifetime_hours: 1
586 + })
587 + });
588 +
589 + if (messageResponse.ok) {
590 + console.log('Message sent with attachment');
591 +
592 + // Step 2: Retrieve the uploaded file
593 + const retrievedFiles = await getFiles(["/a0/tmp/uploads/test.txt"]);
594 +
595 + if (retrievedFiles && retrievedFiles["test.txt"]) {
596 + const originalContent = atob(retrievedFiles["test.txt"]);
597 + console.log('Retrieved content:', originalContent);
598 + }
599 + }
600 +}
601 +
602 +// Run the complete workflow
603 +attachmentWorkflow();`;
604 +
605 + // Initialize ACE editors
606 + const editors = [
607 + { id: "api-basic-example", content: basicExample },
608 + { id: "api-continuation-example", content: continuationExample },
609 + { id: "api-attachment-example", content: attachmentExample },
610 + { id: "api-log-get-example", content: logGetExample },
611 + { id: "api-log-post-example", content: logPostExample },
612 + { id: "api-terminate-example", content: terminateExample },
613 + { id: "api-reset-example", content: resetExample },
614 + { id: "api-files-get-example", content: filesGetExample }
615 + ];
616 +
617 + editors.forEach(({ id, content }) => {
618 + const editor = ace.edit(id);
619 + const dark = localStorage.getItem("darkMode");
620 + if (dark != "false") {
621 + editor.setTheme("ace/theme/github_dark");
622 + } else {
623 + editor.setTheme("ace/theme/tomorrow");
624 + }
625 + editor.session.setMode("ace/mode/javascript");
626 + editor.setValue(content);
627 + editor.clearSelection();
628 + editor.setReadOnly(true);
629 + });
630 + }, 0);
631 + </script>
632 + </div>
633 +
634 + <style>
635 + #api-basic-example,
636 + #api-continuation-example,
637 + #api-attachment-example,
638 + #api-log-get-example,
639 + #api-log-post-example,
640 + #api-terminate-example,
641 + #api-reset-example,
642 + #api-files-get-example {
643 + width: 100%;
644 + height: 20em;
645 + margin: 8px 0 16px 0;
646 + border-radius: 4px;
647 + }
648 +
649 + /* Section styling */
650 + .api-section {
651 + border: 1px solid var(--color-border);
652 + border-radius: 8px;
653 + margin: 20px 0;
654 + padding: 16px;
655 + }
656 +
657 + .api-section h2 {
658 + margin: 0 0 16px 0;
659 + color: var(--color-text-primary);
660 + border-bottom: 1px solid var(--color-border);
661 + padding-bottom: 8px;
662 + }
663 +
664 + .api-section h3 {
665 + margin: 20px 0 12px 0;
666 + color: var(--color-text-primary);
667 + font-size: 1.2em;
668 + }
669 +
670 + .api-section h4 {
671 + margin: 16px 0 8px 0;
672 + color: var(--color-text-primary);
673 + }
674 + </style>
675 +
676 +</body>
677 +
678 +</html>
webui/components/settings/mcp/server/example.html
+10 -1
@@ -10,6 +10,15 @@
10 <p>Agent Zero MCP Server is an SSE MCP running on the same URL and port as the Web UI + /mcp/sse path.</p>
11 <p>The same applies if you run A0 on a public URL using a tunnel.</p>
12
13 + <!-- API Token Information -->
14 + <div style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 6px; padding: 12px; margin: 16px 0;">
15 + <h4 style="margin: 0 0 8px 0; color: var(--color-text-primary);">API Token Information</h4>
16 + <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
17 + The token used in the URL is automatically generated from your username and password.
18 + This same token is also used for external API endpoints. The token changes when you update your credentials.
19 + </p>
20 + </div>
21 +
22 <h3>Example MCP Server Configuration JSON</h3>
23 <div id="mcp-server-example"></div>
24
@@ -52,4 +61,4 @@
61
62 </body>
63
55 -</html>
\ No newline at end of file
64 +</html>
webui/js/settings.js
+2
@@ -291,6 +291,8 @@ const settingsModalProxy = {
291 openModal("settings/backup/backup.html");
292 } else if (field.id === "backup_restore") {
293 openModal("settings/backup/restore.html");
294 + } else if (field.id === "external_api_examples") {
295 + openModal("settings/external/api-examples.html");
296 }
297 }
298 };
webui/public/external_api.svg new
+28
@@ -0,0 +1,28 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
2 + <g fill="none" stroke="#000" stroke-width="0.8" stroke-miterlimit="10">
3 + <!-- Central hub node -->
4 + <circle cx="10" cy="10" r="2.5"/>
5 +
6 + <!-- External connection nodes -->
7 + <circle cx="3" cy="3" r="1.5"/>
8 + <circle cx="17" cy="3" r="1.5"/>
9 + <circle cx="3" cy="17" r="1.5"/>
10 + <circle cx="17" cy="17" r="1.5"/>
11 +
12 + <!-- Connection lines -->
13 + <line x1="5.1" y1="4.9" x2="7.9" y2="7.1"/>
14 + <line x1="14.9" y1="4.9" x2="12.1" y2="7.1"/>
15 + <line x1="5.1" y1="15.1" x2="7.9" y2="12.9"/>
16 + <line x1="14.9" y1="15.1" x2="12.1" y2="12.9"/>
17 +
18 + <!-- API endpoints indicators -->
19 + <rect x="1" y="7" width="1" height="6" rx="0.5"/>
20 + <rect x="18" y="7" width="1" height="6" rx="0.5"/>
21 + <rect x="7" y="1" width="6" height="1" rx="0.5"/>
22 + <rect x="7" y="18" width="6" height="1" rx="0.5"/>
23 +
24 + <!-- Data flow arrows -->
25 + <path d="M6 8.5 L7.5 9.5 L6 10.5" fill="none" stroke-width="0.6"/>
26 + <path d="M14 9.5 L12.5 8.5 L14 7.5" fill="none" stroke-width="0.6"/>
27 + </g>
28 +</svg>