From 079ef0698ddba6395f38ceb7281edf5bd9942e5d Mon Sep 17 00:00:00 2001 From: Eddie Pick Date: Sun, 14 Dec 2025 12:32:48 -0500 Subject: [PATCH 1/8] Add Z.AI provider support for general and coding APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Z.AI provider with base URL https://api.z.ai/api/paas/v4 - Add Z.AI Coding provider with base URL https://api.z.ai/api/coding/paas/v4 - Both use OpenAI-compatible protocol via LiteLLM Available models: glm-4.6, glm-4.6v (vision), glm-4.5, glm-4.5-air 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- conf/model_providers.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/conf/model_providers.yaml b/conf/model_providers.yaml index e805376e3..999b499e8 100644 --- a/conf/model_providers.yaml +++ b/conf/model_providers.yaml @@ -84,6 +84,16 @@ chat: xai: name: xAI litellm_provider: xai + zai: + name: Z.AI + litellm_provider: openai + kwargs: + api_base: https://api.z.ai/api/paas/v4 + zai_coding: + name: Z.AI Coding + litellm_provider: openai + kwargs: + api_base: https://api.z.ai/api/coding/paas/v4 other: name: Other OpenAI compatible litellm_provider: openai From f5fef3fbe5d8cada3ec400bf447e3567bc6bc05f Mon Sep 17 00:00:00 2001 From: Eddie Pick Date: Sun, 14 Dec 2025 13:33:58 -0500 Subject: [PATCH 2/8] Add AWS Bedrock provider support for chat and embeddings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Bedrock to chat providers with native LiteLLM support - Add Bedrock to embedding providers Available models (from AWS docs): Claude: - anthropic.claude-opus-4-5-20251101-v1:0 (Opus 4.5) - anthropic.claude-sonnet-4-5-20250929-v1:0 (Sonnet 4.5) - anthropic.claude-haiku-4-5-20251001-v1:0 (Haiku 4.5) Amazon Nova: - amazon.nova-pro-v1:0, amazon.nova-lite-v1:0 - amazon.nova-2-lite-v1:0, amazon.nova-2-sonic-v1:0 Qwen: - qwen.qwen3-235b-a22b-2507-v1:0 - qwen.qwen3-coder-480b-a35b-v1:0 Others: MiniMax M2, Mistral Large 3, DeepSeek R1/V3, Llama 3.3, Titan Embeddings Auth: Set BEDROCK_API_KEY (new AWS Bedrock API key) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- conf/model_providers.yaml | 6 ++++++ requirements.txt | 1 + 2 files changed, 7 insertions(+) diff --git a/conf/model_providers.yaml b/conf/model_providers.yaml index e805376e3..45c964715 100644 --- a/conf/model_providers.yaml +++ b/conf/model_providers.yaml @@ -64,6 +64,9 @@ chat: azure: name: OpenAI Azure litellm_provider: azure + bedrock: + name: AWS Bedrock + litellm_provider: bedrock openrouter: name: OpenRouter litellm_provider: openrouter @@ -110,6 +113,9 @@ embedding: azure: name: OpenAI Azure litellm_provider: azure + bedrock: + name: AWS Bedrock + litellm_provider: bedrock # TODO: OpenRouter not yet supported by LiteLLM, replace with native litellm_provider openrouter and remove api_base when ready openrouter: name: OpenRouter diff --git a/requirements.txt b/requirements.txt index 07be99756..a14a85339 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,5 +45,6 @@ soundfile==0.13.1 imapclient>=3.0.1 html2text>=2024.2.26 beautifulsoup4>=4.12.3 +boto3>=1.35.0 exchangelib>=5.4.3 pywinpty==3.0.2; sys_platform == "win32" \ No newline at end of file From dd018d66a49e79259d443b7655375eadd885f5e4 Mon Sep 17 00:00:00 2001 From: frdel <38891707+frdel@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:30:08 +0100 Subject: [PATCH 3/8] pipx, subagent paths, alpine directives, CSRF allowed origins autoset --- agent.py | 6 +- docker/base/fs/ins/install_python.sh | 2 +- python/api/csrf_token.py | 40 ++++++- .../agent_init/_15_load_profile_settings.py | 2 +- python/helpers/extension.py | 2 +- python/helpers/subagents.py | 101 +++++++----------- webui/js/initFw.js | 55 ++++++++-- 7 files changed, 128 insertions(+), 80 deletions(-) diff --git a/agent.py b/agent.py index f5e0602f8..f6001098c 100644 --- a/agent.py +++ b/agent.py @@ -619,14 +619,14 @@ class Agent: return system_prompt def parse_prompt(self, _prompt_file: str, **kwargs): - dirs = subagents.get_agent_paths_chain(self, "prompts") + dirs = subagents.get_paths(self, "prompts") prompt = files.parse_file( _prompt_file, _directories=dirs, _agent=self, **kwargs ) return prompt def read_prompt(self, file: str, **kwargs) -> str: - dirs = subagents.get_agent_paths_chain(self, "prompts") + dirs = subagents.get_paths(self, "prompts") prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs) prompt = files.remove_code_fences(prompt) return prompt @@ -958,7 +958,7 @@ class Agent: classes = [] # search for tools in agent's folder hierarchy - paths = subagents.get_agent_paths_chain(self, "tools", name + ".py", default_root="python") + paths = subagents.get_paths(self, "tools", name + ".py", default_root="python") for path in paths: try: classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type] diff --git a/docker/base/fs/ins/install_python.sh b/docker/base/fs/ins/install_python.sh index 82f2fc383..417395ebc 100644 --- a/docker/base/fs/ins/install_python.sh +++ b/docker/base/fs/ins/install_python.sh @@ -20,7 +20,7 @@ python3.13 -m venv /opt/venv source /opt/venv/bin/activate # upgrade pip and install static packages -pip install --no-cache-dir --upgrade pip ipython requests +pip install --no-cache-dir --upgrade pip pipx ipython requests echo "====================PYTHON PYVENV====================" diff --git a/python/api/csrf_token.py b/python/api/csrf_token.py index 0e46a4d46..f4d1d63c0 100644 --- a/python/api/csrf_token.py +++ b/python/api/csrf_token.py @@ -11,6 +11,8 @@ from python.helpers.api import ( from python.helpers import runtime, dotenv, login import fnmatch +ALLOWED_ORIGINS_KEY = "ALLOWED_ORIGINS" + class GetCsrfToken(ApiHandler): @@ -44,9 +46,11 @@ class GetCsrfToken(ApiHandler): } async def check_allowed_origin(self, request: Request): - # if login is required, this che + # if login is required, this check is unnecessary if login.is_login_required(): return {"ok": True, "origin": "", "allowed_origins": ""} + # initialize allowed origins if not yet set + self.initialize_allowed_origins(request) # otherwise, check if the origin is allowed return await self.is_allowed_origin(request) @@ -66,6 +70,7 @@ class GetCsrfToken(ApiHandler): ) return {"ok": match, "origin": origin, "allowed_origins": allowed_origins} + def get_origin_from_request(self, request: Request): # get from origin r = request.headers.get("Origin") or request.environ.get("HTTP_ORIGIN") @@ -88,7 +93,7 @@ class GetCsrfToken(ApiHandler): # get the allowed origins from the environment allowed_origins = [ origin.strip() - for origin in (dotenv.get_dotenv_value("ALLOWED_ORIGINS") or "").split(",") + for origin in (dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY) or "").split(",") if origin.strip() ] @@ -110,3 +115,34 @@ class GetCsrfToken(ApiHandler): def get_default_allowed_origins(self) -> list[str]: return ["*://localhost:*", "*://127.0.0.1:*", "*://0.0.0.0:*"] + + def initialize_allowed_origins(self, request: Request): + """ + If A0 is hosted on a server, add the first visit origin to ALLOWED_ORIGINS. + This simplifies deployment process as users can access their new instance without + additional setup while keeping it secure. + """ + # dotenv value is already set, do nothing + denv = dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY) + if denv: + return + + # get the origin from the request + req_origin = self.get_origin_from_request(request) + if not req_origin: + return + + # check if the origin is allowed by default + allowed_origins = self.get_default_allowed_origins() + match = any( + fnmatch.fnmatch(req_origin, allowed_origin) + for allowed_origin in allowed_origins + ) + if match: + return + + # if not, add it to the allowed origins + allowed_origins.append(req_origin) + dotenv.save_dotenv_value(ALLOWED_ORIGINS_KEY, ",".join(allowed_origins)) + + \ No newline at end of file diff --git a/python/extensions/agent_init/_15_load_profile_settings.py b/python/extensions/agent_init/_15_load_profile_settings.py index d4261d7b9..d4c9b5ab4 100644 --- a/python/extensions/agent_init/_15_load_profile_settings.py +++ b/python/extensions/agent_init/_15_load_profile_settings.py @@ -10,7 +10,7 @@ class LoadProfileSettings(Extension): if not self.agent or not self.agent.config.profile: return - config_files = subagents.get_agent_paths_chain(self.agent, "settings.json", include_default=False) + config_files = subagents.get_paths(self.agent, "settings.json", include_default=False) settings_override = {} for settings_path in config_files: diff --git a/python/helpers/extension.py b/python/helpers/extension.py index d28e86c4e..186099cc0 100644 --- a/python/helpers/extension.py +++ b/python/helpers/extension.py @@ -30,7 +30,7 @@ async def call_extensions( from python.helpers import projects, subagents # search for extension folders in all agent's paths - paths = subagents.get_agent_paths_chain(agent, "extensions", extension_point, default_root="python") + paths = subagents.get_paths(agent, "extensions", extension_point, default_root="python") all_exts = [cls for path in paths for cls in _get_extensions(path)] # merge: first ocurrence of file name is the override diff --git a/python/helpers/subagents.py b/python/helpers/subagents.py index fb8886a4a..66f320f00 100644 --- a/python/helpers/subagents.py +++ b/python/helpers/subagents.py @@ -246,84 +246,63 @@ def get_available_agents_dict( return filtered_agents -def get_agent_paths( - agent: "Agent", *subpaths, must_exist_completely: bool = True -) -> list[str]: - """Returns list of possible paths for the given agent and subpaths. Order is from lowest priority (global).""" - - if not agent or not agent.config.profile: - return [] - from python.helpers import projects - - project_name = projects.get_context_project_name(agent.context) - return get_agent_profile_paths( - agent.config.profile, - project_name, - *subpaths, - must_exist_completely=must_exist_completely, - ) - - -def get_agent_paths_chain( +def get_paths( agent: "Agent|None", *subpaths, - must_exist_completely: bool = True, + must_exist_completely: bool = True, include_project: bool = True, include_user: bool = True, include_default: bool = True, default_root: str = "", ) -> list[str]: """Returns list of file paths for the given agent and subpaths, searched in order of priority: - project/agents/, usr/agents/, agents/, project/, usr/, default.""" - from python.helpers import projects + project/agents/, project/, usr/agents/, agents/, usr/, default.""" + paths: list[str] = [] + check_subpaths = subpaths if must_exist_completely else [] + profile_name = agent.config.profile if agent and agent.config.profile else "" + project_name = "" - if agent and agent.config.profile: - project_name = projects.get_context_project_name(agent.context) - paths = get_agent_profile_paths( - agent.config.profile, - project_name, - *subpaths, - must_exist_completely=must_exist_completely, - ) - list.reverse(paths) # reverse for proper priority - else: - paths = [] - project_name = "" + if include_project and agent: + from python.helpers import projects - if include_project and project_name: - path = projects.get_project_meta_folder(project_name, *subpaths) - if (not must_exist_completely) or files.exists(path): + project_name = projects.get_context_project_name(agent.context) or "" + + if project_name and profile_name: + # project/agents//... + project_agent_dir = projects.get_project_meta_folder( + project_name, "agents", profile_name + ) + if files.exists(files.get_abs_path(project_agent_dir, *check_subpaths)): + paths.append(files.get_abs_path(project_agent_dir, *subpaths)) + + if project_name: + # project/.a0proj/... + path = projects.get_project_meta_folder(project_name, *subpaths) + if (not must_exist_completely) or files.exists(path): + paths.append(path) + + if profile_name: + + # usr/agents//... + path = files.get_abs_path(USER_AGENTS_DIR, profile_name, *subpaths) + if (not must_exist_completely) or files.exists(files.get_abs_path(USER_AGENTS_DIR, profile_name, *check_subpaths)): paths.append(path) + + # agents//... + path = files.get_abs_path(DEFAULT_AGENTS_DIR, profile_name, *subpaths) + if (not must_exist_completely) or files.exists(files.get_abs_path(DEFAULT_AGENTS_DIR, profile_name, *check_subpaths)): + paths.append(path) + if include_user: + # usr/... path = files.get_abs_path(USER_DIR, *subpaths) if (not must_exist_completely) or files.exists(path): paths.append(path) + if include_default: + # default_root/... path = files.get_abs_path(default_root, *subpaths) if (not must_exist_completely) or files.exists(path): paths.append(path) + return paths - - -def get_agent_profile_paths( - name: str, - project_name: str | None = None, - *subpaths, - must_exist_completely: bool = True, -) -> list[str]: - result = [] - check_subpaths = subpaths if must_exist_completely else [] - - if files.exists(files.get_abs_path(DEFAULT_AGENTS_DIR, name, *check_subpaths)): - result.append(files.get_abs_path(DEFAULT_AGENTS_DIR, name, *subpaths)) - if files.exists(files.get_abs_path(USER_AGENTS_DIR, name, *check_subpaths)): - result.append(files.get_abs_path(USER_AGENTS_DIR, name, *subpaths)) - if project_name: - from python.helpers import projects - - project_agent_dir = projects.get_project_meta_folder( - project_name, "agents", name - ) - if files.exists(files.get_abs_path(project_agent_dir, *check_subpaths)): - result.append(files.get_abs_path(project_agent_dir, *subpaths)) - return result diff --git a/webui/js/initFw.js b/webui/js/initFw.js index 69abde220..c758b6d6c 100644 --- a/webui/js/initFw.js +++ b/webui/js/initFw.js @@ -10,15 +10,48 @@ await import("../vendor/alpine/alpine.min.js"); // add x-destroy directive to alpine Alpine.directive( - "destroy", - (el, { expression }, { evaluateLater, cleanup }) => { - const onDestroy = evaluateLater(expression); - cleanup(() => onDestroy()); - } -); + "destroy", + (_el, { expression }, { evaluateLater, cleanup }) => { + const onDestroy = evaluateLater(expression); + cleanup(() => onDestroy()); + } + ); -// add x-create directive to alpine -Alpine.directive("create", (_el, { expression }, { evaluateLater }) => { - const onCreate = evaluateLater(expression); - onCreate(); -}); + // add x-create directive to alpine + Alpine.directive( + "create", + (_el, { expression }, { evaluateLater }) => { + const onCreate = evaluateLater(expression); + onCreate(); + } + ); + + // run every second if the component is active + Alpine.directive( + "every-second", + (_el, { expression }, { evaluateLater, cleanup }) => { + const onTick = evaluateLater(expression); + const intervalId = setInterval(() => onTick(), 1000); + cleanup(() => clearInterval(intervalId)); + } + ); + + // run every minute if the component is active + Alpine.directive( + "every-minute", + (_el, { expression }, { evaluateLater, cleanup }) => { + const onTick = evaluateLater(expression); + const intervalId = setInterval(() => onTick(), 60_000); + cleanup(() => clearInterval(intervalId)); + } + ); + + // run every hour if the component is active + Alpine.directive( + "every-hour", + (_el, { expression }, { evaluateLater, cleanup }) => { + const onTick = evaluateLater(expression); + const intervalId = setInterval(() => onTick(), 3_600_000); + cleanup(() => clearInterval(intervalId)); + } + ); From 5a63cf7bfbca1ab9fe2e174d9a220d063152de60 Mon Sep 17 00:00:00 2001 From: "dipi.evil" Date: Fri, 19 Dec 2025 10:08:32 -0300 Subject: [PATCH 4/8] feat: add support for agent and project on API --- python/api/api_message.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/python/api/api_message.py b/python/api/api_message.py index 385d556dd..6ccea07b2 100644 --- a/python/api/api_message.py +++ b/python/api/api_message.py @@ -5,6 +5,7 @@ from agent import AgentContext, UserMessage, AgentContextType from python.helpers.api import ApiHandler, Request, Response from python.helpers import files from python.helpers.print_style import PrintStyle +from python.helpers.projects import activate_project from werkzeug.utils import secure_filename from initialize import initialize_agent import threading @@ -33,6 +34,13 @@ class ApiMessage(ApiHandler): message = input.get("message", "") attachments = input.get("attachments", []) lifetime_hours = input.get("lifetime_hours", 24) # Default 24 hours + project_name = input.get("project_name", None) + agent_profile = input.get("agent_profile", None) + + # Initialize agent if profile provided + override_settings = {} + if agent_profile: + override_settings["agent_profile"] = agent_profile if not message: return Response('{"error": "Message is required"}', status=400, mimetype="application/json") @@ -72,11 +80,14 @@ class ApiMessage(ApiHandler): if not context: return Response('{"error": "Context not found"}', status=404, mimetype="application/json") else: - config = initialize_agent() + config = initialize_agent(override_settings=override_settings) context = AgentContext(config=config, type=AgentContextType.USER) AgentContext.use(context.id) context_id = context.id + if project_name: + activate_project(context_id, project_name) + # Update chat lifetime with self._cleanup_lock: self._chat_lifetimes[context_id] = datetime.now() + timedelta(hours=lifetime_hours) From 8639ef19e6dce415b22f6d48308ab934d7f92baa Mon Sep 17 00:00:00 2001 From: "dipi.evil" Date: Fri, 19 Dec 2025 10:34:51 -0300 Subject: [PATCH 5/8] chore: comments fixes --- python/api/api_message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/api/api_message.py b/python/api/api_message.py index 6ccea07b2..de0859387 100644 --- a/python/api/api_message.py +++ b/python/api/api_message.py @@ -37,7 +37,7 @@ class ApiMessage(ApiHandler): project_name = input.get("project_name", None) agent_profile = input.get("agent_profile", None) - # Initialize agent if profile provided + # Set an agent if profile provided override_settings = {} if agent_profile: override_settings["agent_profile"] = agent_profile @@ -85,6 +85,7 @@ class ApiMessage(ApiHandler): AgentContext.use(context.id) context_id = context.id + # Activate project if provided if project_name: activate_project(context_id, project_name) From a96387d54767d0c128974aa9ea3adb603526f972 Mon Sep 17 00:00:00 2001 From: "dipi.evil" Date: Fri, 19 Dec 2025 11:17:29 -0300 Subject: [PATCH 6/8] fix: validate existing agent for context --- python/api/api_message.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/api/api_message.py b/python/api/api_message.py index de0859387..8eb5b53ef 100644 --- a/python/api/api_message.py +++ b/python/api/api_message.py @@ -76,7 +76,9 @@ class ApiMessage(ApiHandler): # Get or create context if context_id: - context = AgentContext.use(context_id) + if agent_profile: + return Response('{"error": "Cannot override agent profile on existing context"}', status=400, mimetype="application/json") + context = AgentContext.use(context_id) if not context: return Response('{"error": "Context not found"}', status=404, mimetype="application/json") else: From 07daf31b395831b43d2bbddfdf26a84c84c7713c Mon Sep 17 00:00:00 2001 From: "dipi.evil" Date: Fri, 19 Dec 2025 11:18:10 -0300 Subject: [PATCH 7/8] fix: check if project exists when activating --- python/api/api_message.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/python/api/api_message.py b/python/api/api_message.py index 8eb5b53ef..f36fa1faf 100644 --- a/python/api/api_message.py +++ b/python/api/api_message.py @@ -86,10 +86,26 @@ class ApiMessage(ApiHandler): context = AgentContext(config=config, type=AgentContextType.USER) AgentContext.use(context.id) context_id = context.id - - # Activate project if provided - if project_name: - activate_project(context_id, project_name) + # Activate project if provided + if project_name: + try: + activate_project(context_id, project_name) + except Exception as e: + # Handle non-existent project or context errors more gracefully + error_msg = str(e) + PrintStyle.error(f"Failed to activate project '{project_name}' for context '{context_id}': {error_msg}") + # If the error message suggests a missing resource, return 404; otherwise, 500 + if "not found" in error_msg.lower() or "does not exist" in error_msg.lower(): + return Response( + f'{{"error": "Project \\"{project_name}\\" not found"}}', + status=404, + mimetype="application/json", + ) + return Response( + f'{{"error": "Failed to activate project \\"{project_name}\\""}}', + status=500, + mimetype="application/json", + ) # Update chat lifetime with self._cleanup_lock: From 265e31e4bbb832f2b5b650906e0d88ff20e9c815 Mon Sep 17 00:00:00 2001 From: "dipi.evil" Date: Fri, 19 Dec 2025 11:34:14 -0300 Subject: [PATCH 8/8] fix: improve error handling for project activation in existing context --- python/api/api_message.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/python/api/api_message.py b/python/api/api_message.py index f36fa1faf..9fb433335 100644 --- a/python/api/api_message.py +++ b/python/api/api_message.py @@ -78,7 +78,7 @@ class ApiMessage(ApiHandler): if context_id: if agent_profile: return Response('{"error": "Cannot override agent profile on existing context"}', status=400, mimetype="application/json") - context = AgentContext.use(context_id) + context = AgentContext.use(context_id) if not context: return Response('{"error": "Context not found"}', status=404, mimetype="application/json") else: @@ -91,16 +91,9 @@ class ApiMessage(ApiHandler): try: activate_project(context_id, project_name) except Exception as e: - # Handle non-existent project or context errors more gracefully + # Handle project or context errors more gracefully error_msg = str(e) PrintStyle.error(f"Failed to activate project '{project_name}' for context '{context_id}': {error_msg}") - # If the error message suggests a missing resource, return 404; otherwise, 500 - if "not found" in error_msg.lower() or "does not exist" in error_msg.lower(): - return Response( - f'{{"error": "Project \\"{project_name}\\" not found"}}', - status=404, - mimetype="application/json", - ) return Response( f'{{"error": "Failed to activate project \\"{project_name}\\""}}', status=500,