diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a135bdcc9..98d17fb1c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: agent0ai +github: frdel diff --git a/README.md b/README.md index 6aff4ccba..d2c0d9b4b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Agent Zero now supports **Projects** – isolated workspaces with their own prom -[![Showcase](/docs/res/showcase-thumb.png)](https://youtu.be/MdzLhWWoxEs) +[![Showcase](/docs/res/showcase-thumb.png)](https://youtu.be/lazLNcEYsiQ) diff --git a/agent.py b/agent.py index c5c79a55d..50d47e4c3 100644 --- a/agent.py +++ b/agent.py @@ -8,7 +8,6 @@ from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any, Awaitable, Coroutine, Dict, Literal from enum import Enum -import uuid import models from python.helpers import ( @@ -564,7 +563,7 @@ class Agent: self.handle_critical_exception(e) error_message = errors.format_error(e) - + self.context.log.log( type="warning", content="Critical error occurred, retrying..." ) diff --git a/models.py b/models.py index 55c58a369..4676352c2 100644 --- a/models.py +++ b/models.py @@ -22,7 +22,7 @@ from litellm.types.utils import ModelResponse from python.helpers import dotenv from python.helpers import settings, dirty_json from python.helpers.dotenv import load_dotenv -from python.helpers.providers import get_provider_config +from python.helpers.providers import ModelType as ProviderModelType, get_provider_config from python.helpers.rate_limiter import RateLimiter from python.helpers.tokens import approximate_tokens from python.helpers import dirty_json, browser_use_monkeypatch @@ -844,7 +844,7 @@ def _adjust_call_args(provider_name: str, model_name: str, kwargs: dict): def _merge_provider_defaults( - provider_type: str, original_provider: str, kwargs: dict + provider_type: ProviderModelType, original_provider: str, kwargs: dict ) -> tuple[str, dict]: # Normalize .env-style numeric strings (e.g., "timeout=30") into ints/floats for LiteLLM def _normalize_values(values: dict) -> dict: diff --git a/python/api/settings_get.py b/python/api/settings_get.py index 5b5bf95c7..5285b4fd4 100644 --- a/python/api/settings_get.py +++ b/python/api/settings_get.py @@ -4,8 +4,9 @@ from python.helpers import settings class GetSettings(ApiHandler): async def process(self, input: dict, request: Request) -> dict | Response: - set = settings.convert_out(settings.get_settings()) - return {"settings": set} + backend = settings.get_settings() + out = settings.convert_out(backend) + return dict(out) @classmethod def get_methods(cls) -> list[str]: diff --git a/python/api/settings_set.py b/python/api/settings_set.py index c24a3cc66..3213eada7 100644 --- a/python/api/settings_set.py +++ b/python/api/settings_set.py @@ -7,6 +7,8 @@ from typing import Any class SetSettings(ApiHandler): async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response: - set = settings.convert_in(input) - set = settings.set_settings(set) - return {"settings": set} + frontend = input.get("settings", input) + backend = settings.convert_in(settings.Settings(**frontend)) + backend = settings.set_settings(backend) + out = settings.convert_out(backend) + return dict(out) diff --git a/python/helpers/files.py b/python/helpers/files.py index ac2e51a9e..0ed9cb06d 100644 --- a/python/helpers/files.py +++ b/python/helpers/files.py @@ -96,10 +96,7 @@ def parse_file( content = f.read() is_json = is_full_json_template(content) - # Only strip code fences for full JSON templates - embedded fenced blocks in markdown prompts - # should remain intact, otherwise examples/instructions lose structure. - if is_json: - content = remove_code_fences(content) + content = remove_code_fences(content) variables = load_plugin_variables(absolute_path, _directories, **kwargs) or {} # type: ignore variables.update(kwargs) if is_json: @@ -336,16 +333,17 @@ def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*"): def remove_code_fences(text): - # Strip fenced blocks (``` / ~~~) only when the fence marker is at the start of a line. - # - # This prevents accidental stripping when the fence marker appears inline, e.g. - # "... do not wrap ~~~markdown" (which should remain literal text). - pattern = r"(?ms)^[ \t]*(```|~~~)[^\n]*\n(.*?)(?:^[ \t]*\1[ \t]*$)" + # Pattern to match code fences with optional language specifier + pattern = r"(```|~~~)(.*?\n)(.*?)(\1)" + # Function to replace the code fences def replacer(match): - return match.group(2) # Return the code without fences + return match.group(3) # Return the code without fences - return re.sub(pattern, replacer, text) + # Use re.DOTALL to make '.' match newlines + result = re.sub(pattern, replacer, text, flags=re.DOTALL) + + return result def is_full_json_template(text): @@ -599,3 +597,4 @@ def list_files_in_dir_recursively(relative_path: str) -> list[str]: rel_path = os.path.relpath(file_path, abs_path) result.append(rel_path) return result + \ No newline at end of file diff --git a/python/helpers/providers.py b/python/helpers/providers.py index cd139e88a..f60238bd5 100644 --- a/python/helpers/providers.py +++ b/python/helpers/providers.py @@ -1,7 +1,8 @@ import yaml from python.helpers import files -from typing import List, Dict, Optional, TypedDict +from typing import List, Dict, Optional, TypedDict, Literal +ModelType = Literal["chat", "embedding"] # Type alias for UI option items class FieldOption(TypedDict): @@ -68,16 +69,15 @@ class ProviderManager: opts.append({"value": pid, "label": name}) self._options[p_type] = opts - def get_providers(self, provider_type: str) -> List[FieldOption]: + def get_providers(self, provider_type: ModelType) -> List[FieldOption]: """Returns a list of providers for a given type (e.g., 'chat', 'embedding').""" return self._options.get(provider_type, []) if self._options else [] - - def get_raw_providers(self, provider_type: str) -> List[Dict[str, str]]: + def get_raw_providers(self, provider_type: ModelType) -> List[Dict[str, str]]: """Return raw provider dictionaries for advanced use-cases.""" return self._raw.get(provider_type, []) if self._raw else [] - def get_provider_config(self, provider_type: str, provider_id: str) -> Optional[Dict[str, str]]: + def get_provider_config(self, provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]: """Return the metadata dict for a single provider id (case-insensitive).""" provider_id_low = provider_id.lower() for p in self.get_raw_providers(provider_type): @@ -86,16 +86,16 @@ class ProviderManager: return None -def get_providers(provider_type: str) -> List[FieldOption]: +def get_providers(provider_type: ModelType) -> List[FieldOption]: """Convenience function to get providers of a specific type.""" return ProviderManager.get_instance().get_providers(provider_type) -def get_raw_providers(provider_type: str) -> List[Dict[str, str]]: +def get_raw_providers(provider_type: ModelType) -> List[Dict[str, str]]: """Return full metadata for providers of a given type.""" return ProviderManager.get_instance().get_raw_providers(provider_type) -def get_provider_config(provider_type: str, provider_id: str) -> Optional[Dict[str, str]]: +def get_provider_config(provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]: """Return metadata for a single provider (None if not found).""" - return ProviderManager.get_instance().get_provider_config(provider_type, provider_id) \ No newline at end of file + return ProviderManager.get_instance().get_provider_config(provider_type, provider_id) \ No newline at end of file diff --git a/python/helpers/settings.py b/python/helpers/settings.py index f09124bd7..7a2b8b0d6 100644 --- a/python/helpers/settings.py +++ b/python/helpers/settings.py @@ -10,7 +10,7 @@ import models from python.helpers import runtime, whisper, defer, git from . import files, dotenv from python.helpers.print_style import PrintStyle -from python.helpers.providers import get_providers +from python.helpers.providers import get_providers, FieldOption as ProvidersFO from python.helpers.secrets import get_default_secrets_manager from python.helpers import dirty_json @@ -156,7 +156,6 @@ class FieldOption(TypedDict): value: str label: str - class SettingsField(TypedDict, total=False): id: str title: str @@ -188,9 +187,21 @@ class SettingsSection(TypedDict, total=False): fields: list[SettingsField] tab: str # Indicates which tab this section belongs to +class ModelProvider(ProvidersFO): + pass + +class SettingsOutputAdditional(TypedDict): + chat_providers: list[ModelProvider] + embedding_providers: list[ModelProvider] + shell_interfaces: list[FieldOption] + agent_subdirs: list[FieldOption] + knowledge_subdirs: list[FieldOption] + stt_models: list[FieldOption] + is_dockerized: bool class SettingsOutput(TypedDict): - sections: list[SettingsSection] + settings: Settings + additional: SettingsOutputAdditional PASSWORD_PLACEHOLDER = "****PSWD****" @@ -199,1141 +210,98 @@ API_KEY_PLACEHOLDER = "************" SETTINGS_FILE = files.get_abs_path("tmp/settings.json") _settings: Settings | None = None +OptionT = TypeVar("OptionT", bound=FieldOption) + +def _ensure_option_present(options: list[OptionT] | None, current_value: str | None) -> list[OptionT]: + """ + Ensure the currently selected value exists in a dropdown options list. + If missing, inserts it at the front as {value: current_value, label: current_value}. + """ + opts = list(options or []) + if not current_value: + return opts + for o in opts: + if o.get("value") == current_value: + return opts + opts.insert(0, cast(OptionT, {"value": current_value, "label": current_value})) + return opts def convert_out(settings: Settings) -> SettingsOutput: - default_settings = get_default_settings() - - # main model section - chat_model_fields: list[SettingsField] = [] - chat_model_fields.append( - { - "id": "chat_model_provider", - "title": "Chat model provider", - "description": "Select provider for main chat model used by Agent Zero", - "type": "select", - "value": settings["chat_model_provider"], - "options": cast(list[FieldOption], get_providers("chat")), - } - ) - chat_model_fields.append( - { - "id": "chat_model_name", - "title": "Chat model name", - "description": "Exact name of model from selected provider", - "type": "text", - "value": settings["chat_model_name"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_api_base", - "title": "Chat model API base URL", - "description": "API base URL for main chat model. Leave empty for default. Only relevant for Azure, local and custom (other) providers.", - "type": "text", - "value": settings["chat_model_api_base"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_ctx_length", - "title": "Chat model context length", - "description": "Maximum number of tokens in the context window for LLM. System prompt, chat history, RAG and response all count towards this limit.", - "type": "number", - "value": settings["chat_model_ctx_length"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_ctx_history", - "title": "Context window space for chat history", - "description": "Portion of context window dedicated to chat history visible to the agent. Chat history will automatically be optimized to fit. Smaller size will result in shorter and more summarized history. The remaining space will be used for system prompt, RAG and response.", - "type": "range", - "min": 0.01, - "max": 1, - "step": 0.01, - "value": settings["chat_model_ctx_history"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_vision", - "title": "Supports Vision", - "description": "Models capable of Vision can for example natively see the content of image attachments.", - "type": "switch", - "value": settings["chat_model_vision"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_rl_requests", - "title": "Requests per minute limit", - "description": "Limits the number of requests per minute to the chat model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["chat_model_rl_requests"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_rl_input", - "title": "Input tokens per minute limit", - "description": "Limits the number of input tokens per minute to the chat model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["chat_model_rl_input"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_rl_output", - "title": "Output tokens per minute limit", - "description": "Limits the number of output tokens per minute to the chat model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["chat_model_rl_output"], - } - ) - - chat_model_fields.append( - { - "id": "chat_model_kwargs", - "title": "Chat model additional parameters", - "description": "Any other parameters supported by LiteLLM. Format is KEY=VALUE on individual lines, like .env file. Value can also contain JSON objects - when unquoted, it is treated as object, number etc., when quoted, it is treated as string.", - "type": "textarea", - "value": _dict_to_env(settings["chat_model_kwargs"]), - } - ) - - chat_model_section: SettingsSection = { - "id": "chat_model", - "title": "Chat Model", - "description": "Selection and settings for main chat model used by Agent Zero", - "fields": chat_model_fields, - "tab": "agent", - } - - # main model section - util_model_fields: list[SettingsField] = [] - util_model_fields.append( - { - "id": "util_model_provider", - "title": "Utility model provider", - "description": "Select provider for utility model used by the framework", - "type": "select", - "value": settings["util_model_provider"], - "options": cast(list[FieldOption], get_providers("chat")), - } - ) - util_model_fields.append( - { - "id": "util_model_name", - "title": "Utility model name", - "description": "Exact name of model from selected provider", - "type": "text", - "value": settings["util_model_name"], - } - ) - - util_model_fields.append( - { - "id": "util_model_api_base", - "title": "Utility model API base URL", - "description": "API base URL for utility model. Leave empty for default. Only relevant for Azure, local and custom (other) providers.", - "type": "text", - "value": settings["util_model_api_base"], - } - ) - - util_model_fields.append( - { - "id": "util_model_rl_requests", - "title": "Requests per minute limit", - "description": "Limits the number of requests per minute to the utility model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["util_model_rl_requests"], - } - ) - - util_model_fields.append( - { - "id": "util_model_rl_input", - "title": "Input tokens per minute limit", - "description": "Limits the number of input tokens per minute to the utility model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["util_model_rl_input"], - } - ) - - util_model_fields.append( - { - "id": "util_model_rl_output", - "title": "Output tokens per minute limit", - "description": "Limits the number of output tokens per minute to the utility model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["util_model_rl_output"], - } - ) - - util_model_fields.append( - { - "id": "util_model_kwargs", - "title": "Utility model additional parameters", - "description": "Any other parameters supported by LiteLLM. Format is KEY=VALUE on individual lines, like .env file. Value can also contain JSON objects - when unquoted, it is treated as object, number etc., when quoted, it is treated as string.", - "type": "textarea", - "value": _dict_to_env(settings["util_model_kwargs"]), - } - ) - - util_model_section: SettingsSection = { - "id": "util_model", - "title": "Utility model", - "description": "Smaller, cheaper, faster model for handling utility tasks like organizing memory, preparing prompts, summarizing.", - "fields": util_model_fields, - "tab": "agent", - } - - # embedding model section - embed_model_fields: list[SettingsField] = [] - embed_model_fields.append( - { - "id": "embed_model_provider", - "title": "Embedding model provider", - "description": "Select provider for embedding model used by the framework", - "type": "select", - "value": settings["embed_model_provider"], - "options": cast(list[FieldOption], get_providers("embedding")), - } - ) - embed_model_fields.append( - { - "id": "embed_model_name", - "title": "Embedding model name", - "description": "Exact name of model from selected provider", - "type": "text", - "value": settings["embed_model_name"], - } - ) - - embed_model_fields.append( - { - "id": "embed_model_api_base", - "title": "Embedding model API base URL", - "description": "API base URL for embedding model. Leave empty for default. Only relevant for Azure, local and custom (other) providers.", - "type": "text", - "value": settings["embed_model_api_base"], - } - ) - - embed_model_fields.append( - { - "id": "embed_model_rl_requests", - "title": "Requests per minute limit", - "description": "Limits the number of requests per minute to the embedding model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["embed_model_rl_requests"], - } - ) - - embed_model_fields.append( - { - "id": "embed_model_rl_input", - "title": "Input tokens per minute limit", - "description": "Limits the number of input tokens per minute to the embedding model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.", - "type": "number", - "value": settings["embed_model_rl_input"], - } - ) - - embed_model_fields.append( - { - "id": "embed_model_kwargs", - "title": "Embedding model additional parameters", - "description": "Any other parameters supported by LiteLLM. Format is KEY=VALUE on individual lines, like .env file. Value can also contain JSON objects - when unquoted, it is treated as object, number etc., when quoted, it is treated as string.", - "type": "textarea", - "value": _dict_to_env(settings["embed_model_kwargs"]), - } - ) - - embed_model_section: SettingsSection = { - "id": "embed_model", - "title": "Embedding Model", - "description": f"Settings for the embedding model used by Agent Zero.

⚠️ No need to change

The default HuggingFace model {default_settings['embed_model_name']} is preloaded and runs locally within the docker container and there's no need to change it unless you have a specific requirements for embedding.", - "fields": embed_model_fields, - "tab": "agent", - } - - # embedding model section - browser_model_fields: list[SettingsField] = [] - browser_model_fields.append( - { - "id": "browser_model_provider", - "title": "Web Browser model provider", - "description": "Select provider for web browser model used by browser-use framework", - "type": "select", - "value": settings["browser_model_provider"], - "options": cast(list[FieldOption], get_providers("chat")), - } - ) - browser_model_fields.append( - { - "id": "browser_model_name", - "title": "Web Browser model name", - "description": "Exact name of model from selected provider", - "type": "text", - "value": settings["browser_model_name"], - } - ) - - browser_model_fields.append( - { - "id": "browser_model_api_base", - "title": "Web Browser model API base URL", - "description": "API base URL for web browser model. Leave empty for default. Only relevant for Azure, local and custom (other) providers.", - "type": "text", - "value": settings["browser_model_api_base"], - } - ) - - browser_model_fields.append( - { - "id": "browser_model_vision", - "title": "Use Vision", - "description": "Models capable of Vision can use it to analyze web pages from screenshots. Increases quality but also token usage.", - "type": "switch", - "value": settings["browser_model_vision"], - } - ) - - browser_model_fields.append( - { - "id": "browser_model_rl_requests", - "title": "Web Browser model rate limit requests", - "description": "Rate limit requests for web browser model.", - "type": "number", - "value": settings["browser_model_rl_requests"], - } - ) - - browser_model_fields.append( - { - "id": "browser_model_rl_input", - "title": "Web Browser model rate limit input", - "description": "Rate limit input for web browser model.", - "type": "number", - "value": settings["browser_model_rl_input"], - } - ) - - browser_model_fields.append( - { - "id": "browser_model_rl_output", - "title": "Web Browser model rate limit output", - "description": "Rate limit output for web browser model.", - "type": "number", - "value": settings["browser_model_rl_output"], - } - ) - - browser_model_fields.append( - { - "id": "browser_model_kwargs", - "title": "Web Browser model additional parameters", - "description": "Any other parameters supported by LiteLLM. Format is KEY=VALUE on individual lines, like .env file. Value can also contain JSON objects - when unquoted, it is treated as object, number etc., when quoted, it is treated as string.", - "type": "textarea", - "value": _dict_to_env(settings["browser_model_kwargs"]), - } - ) - - browser_model_fields.append( - { - "id": "browser_http_headers", - "title": "HTTP Headers", - "description": "HTTP headers to include with all browser requests. Format is KEY=VALUE on individual lines, like .env file. Value can also contain JSON objects - when unquoted, it is treated as object, number etc., when quoted, it is treated as string. Example: Authorization=Bearer token123", - "type": "textarea", - "value": _dict_to_env(settings.get("browser_http_headers", {})), - } - ) - - browser_model_section: SettingsSection = { - "id": "browser_model", - "title": "Web Browser Model", - "description": "Settings for the web browser model. Agent Zero uses browser-use agentic framework to handle web interactions.", - "fields": browser_model_fields, - "tab": "agent", - } - - # basic auth section - auth_fields: list[SettingsField] = [] - - auth_fields.append( - { - "id": "auth_login", - "title": "UI Login", - "description": "Set user name for web UI", - "type": "text", - "value": dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or "", - } - ) - - auth_fields.append( - { - "id": "auth_password", - "title": "UI Password", - "description": "Set user password for web UI", - "type": "password", - "value": ( - PASSWORD_PLACEHOLDER - if dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) - else "" - ), - } - ) - - if runtime.is_dockerized(): - auth_fields.append( - { - "id": "root_password", - "title": "root Password", - "description": "Change linux root password in docker container. This password can be used for SSH access. Original password was randomly generated during setup.", - "type": "password", - "value": "", - } - ) - - auth_section: SettingsSection = { - "id": "auth", - "title": "Authentication", - "description": "Settings for authentication to use Agent Zero Web UI.", - "fields": auth_fields, - "tab": "external", - } - - # api keys model section - api_keys_fields: list[SettingsField] = [] - - # Collect unique providers from both chat and embedding sections - providers_seen: set[str] = set() - for p_type in ("chat", "embedding"): - for provider in get_providers(p_type): - pid_lower = provider["value"].lower() - if pid_lower in providers_seen: - continue - providers_seen.add(pid_lower) - api_keys_fields.append( - _get_api_key_field(settings, pid_lower, provider["label"]) - ) - - api_keys_section: SettingsSection = { - "id": "api_keys", - "title": "API Keys", - "description": "API keys for model providers and services used by Agent Zero. You can set multiple API keys separated by a comma (,). They will be used in round-robin fashion.
For more information abou Agent Zero Venice provider, see Agent Zero Venice.", - "fields": api_keys_fields, - "tab": "external", - } - - # LiteLLM global config section - litellm_fields: list[SettingsField] = [] - - litellm_fields.append( - { - "id": "litellm_global_kwargs", - "title": "LiteLLM global parameters", - "description": "Global LiteLLM params (e.g. timeout, stream_timeout) in .env format: one KEY=VALUE per line. Example: stream_timeout=30. Applied to all LiteLLM calls unless overridden. See LiteLLM and timeouts.", - "type": "textarea", - "value": _dict_to_env(settings["litellm_global_kwargs"]), - "style": "height: 12em", - } - ) - - litellm_section: SettingsSection = { - "id": "litellm", - "title": "LiteLLM Global Settings", - "description": "Configure global parameters passed to LiteLLM for all providers.", - "fields": litellm_fields, - "tab": "external", - } - - # Agent config section - agent_fields: list[SettingsField] = [] - - agent_fields.append( - { - "id": "agent_profile", - "title": "Default agent profile", - "description": "Subdirectory of /agents folder to be used by default agent no. 0. Subordinate agents can be spawned with other profiles, that is on their superior agent to decide. This setting affects the behaviour of the top level agent you communicate with.", - "type": "select", - "value": settings["agent_profile"], - "options": [ - {"value": subdir, "label": subdir} + out = SettingsOutput( + settings = settings.copy(), + additional = SettingsOutputAdditional( + chat_providers=get_providers("chat"), + embedding_providers=get_providers("embedding"), + shell_interfaces=[{"value": "local", "label": "Local Python TTY"}, {"value": "ssh", "label": "SSH"}], + is_dockerized=runtime.is_dockerized(), + agent_subdirs=[{"value": subdir, "label": subdir} for subdir in files.get_subdirectories("agents") - if subdir != "_example" - ], - } - ) - - agent_fields.append( - { - "id": "agent_knowledge_subdir", - "title": "Knowledge subdirectory", - "description": "Subdirectory of /knowledge folder to use for agent knowledge import. 'default' subfolder is always imported and contains framework knowledge.", - "type": "select", - "value": settings["agent_knowledge_subdir"], - "options": [ - {"value": subdir, "label": subdir} - for subdir in files.get_subdirectories("knowledge", exclude="default") - ], - } - ) - - agent_section: SettingsSection = { - "id": "agent", - "title": "Agent Config", - "description": "Agent parameters.", - "fields": agent_fields, - "tab": "agent", - } - - memory_fields: list[SettingsField] = [] - - memory_fields.append( - { - "id": "agent_memory_subdir", - "title": "Memory Subdirectory", - "description": "Subdirectory of /memory folder to use for agent memory storage. Used to separate memory storage between different instances.", - "type": "text", - "value": settings["agent_memory_subdir"], - # "options": [ - # {"value": subdir, "label": subdir} - # for subdir in files.get_subdirectories("memory", exclude="embeddings") - # ], - } - ) - - memory_fields.append( - { - "id": "memory_dashboard", - "title": "Memory Dashboard", - "description": "View and explore all stored memories in a table format with filtering and search capabilities.", - "type": "button", - "value": "Open Dashboard", - } - ) - - memory_fields.append( - { - "id": "memory_recall_enabled", - "title": "Memory auto-recall enabled", - "description": "Agent Zero will automatically recall memories based on convesation context.", - "type": "switch", - "value": settings["memory_recall_enabled"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_delayed", - "title": "Memory auto-recall delayed", - "description": "The agent will not wait for auto memory recall. Memories will be delivered one message later. This speeds up agent's response time but may result in less relevant first step.", - "type": "switch", - "value": settings["memory_recall_delayed"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_query_prep", - "title": "Auto-recall AI query preparation", - "description": "Enables vector DB query preparation from conversation context by utility LLM for auto-recall. Improves search quality, adds 1 utility LLM call per auto-recall.", - "type": "switch", - "value": settings["memory_recall_query_prep"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_post_filter", - "title": "Auto-recall AI post-filtering", - "description": "Enables memory relevance filtering by utility LLM for auto-recall. Improves search quality, adds 1 utility LLM call per auto-recall.", - "type": "switch", - "value": settings["memory_recall_post_filter"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_interval", - "title": "Memory auto-recall interval", - "description": "Memories are recalled after every user or superior agent message. During agent's monologue, memories are recalled every X turns based on this parameter.", - "type": "range", - "min": 1, - "max": 10, - "step": 1, - "value": settings["memory_recall_interval"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_history_len", - "title": "Memory auto-recall history length", - "description": "The length of conversation history passed to memory recall LLM for context (in characters).", - "type": "number", - "value": settings["memory_recall_history_len"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_similarity_threshold", - "title": "Memory auto-recall similarity threshold", - "description": "The threshold for similarity search in memory recall (0 = no similarity, 1 = exact match).", - "type": "range", - "min": 0, - "max": 1, - "step": 0.01, - "value": settings["memory_recall_similarity_threshold"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_memories_max_search", - "title": "Memory auto-recall max memories to search", - "description": "The maximum number of memories returned by vector DB for further processing.", - "type": "number", - "value": settings["memory_recall_memories_max_search"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_memories_max_result", - "title": "Memory auto-recall max memories to use", - "description": "The maximum number of memories to inject into A0's context window.", - "type": "number", - "value": settings["memory_recall_memories_max_result"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_solutions_max_search", - "title": "Memory auto-recall max solutions to search", - "description": "The maximum number of solutions returned by vector DB for further processing.", - "type": "number", - "value": settings["memory_recall_solutions_max_search"], - } - ) - - memory_fields.append( - { - "id": "memory_recall_solutions_max_result", - "title": "Memory auto-recall max solutions to use", - "description": "The maximum number of solutions to inject into A0's context window.", - "type": "number", - "value": settings["memory_recall_solutions_max_result"], - } - ) - - memory_fields.append( - { - "id": "memory_memorize_enabled", - "title": "Auto-memorize enabled", - "description": "A0 will automatically memorize facts and solutions from conversation history.", - "type": "switch", - "value": settings["memory_memorize_enabled"], - } - ) - - memory_fields.append( - { - "id": "memory_memorize_consolidation", - "title": "Auto-memorize AI consolidation", - "description": "A0 will automatically consolidate similar memories using utility LLM. Improves memory quality over time, adds 2 utility LLM calls per memory.", - "type": "switch", - "value": settings["memory_memorize_consolidation"], - } - ) - - memory_fields.append( - { - "id": "memory_memorize_replace_threshold", - "title": "Auto-memorize replacement threshold", - "description": "Only applies when AI consolidation is disabled. Replaces previous similar memories with new ones based on this threshold. 0 = replace even if not similar at all, 1 = replace only if exact match.", - "type": "range", - "min": 0, - "max": 1, - "step": 0.01, - "value": settings["memory_memorize_replace_threshold"], - } - ) - - memory_section: SettingsSection = { - "id": "memory", - "title": "Memory", - "description": "Configuration of A0's memory system. A0 memorizes and recalls memories automatically to help it's context awareness.", - "fields": memory_fields, - "tab": "agent", - } - - dev_fields: list[SettingsField] = [] - - dev_fields.append( - { - "id": "shell_interface", - "title": "Shell Interface", - "description": "Terminal interface used for Code Execution Tool. Local Python TTY works locally in both dockerized and development environments. SSH always connects to dockerized environment (automatically at localhost or RFC host address).", - "type": "select", - "value": settings["shell_interface"], - "options": [{"value": "local", "label": "Local Python TTY"}, {"value": "ssh", "label": "SSH"}], - } - ) - - if runtime.is_development(): - # dev_fields.append( - # { - # "id": "rfc_auto_docker", - # "title": "RFC Auto Docker Management", - # "description": "Automatically create dockerized instance of A0 for RFCs using this instance's code base and, settings and .env.", - # "type": "text", - # "value": settings["rfc_auto_docker"], - # } - # ) - - dev_fields.append( - { - "id": "rfc_url", - "title": "RFC Destination URL", - "description": "URL of dockerized A0 instance for remote function calls. Do not specify port here.", - "type": "text", - "value": settings["rfc_url"], - } - ) - - dev_fields.append( - { - "id": "rfc_password", - "title": "RFC Password", - "description": "Password for remote function calls. Passwords must match on both instances. RFCs can not be used with empty password.", - "type": "password", - "value": ( - PASSWORD_PLACEHOLDER - if dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD) - else "" - ), - } - ) - - if runtime.is_development(): - dev_fields.append( - { - "id": "rfc_port_http", - "title": "RFC HTTP port", - "description": "HTTP port for dockerized instance of A0.", - "type": "text", - "value": settings["rfc_port_http"], - } - ) - - dev_fields.append( - { - "id": "rfc_port_ssh", - "title": "RFC SSH port", - "description": "SSH port for dockerized instance of A0.", - "type": "text", - "value": settings["rfc_port_ssh"], - } - ) - - dev_section: SettingsSection = { - "id": "dev", - "title": "Development", - "description": "Parameters for A0 framework development. RFCs (remote function calls) are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker. This is crucial for development as A0 needs to run in standardized environment to support all features.", - "fields": dev_fields, - "tab": "developer", - } - - # code_exec_fields: list[SettingsField] = [] - - # code_exec_fields.append( - # { - # "id": "code_exec_ssh_enabled", - # "title": "Use SSH for code execution", - # "description": "Code execution will use SSH to connect to the terminal. When disabled, a local python terminal interface is used instead. SSH should only be used in development environment or when encountering issues with the local python terminal interface.", - # "type": "switch", - # "value": settings["code_exec_ssh_enabled"], - # } - # ) - - # code_exec_fields.append( - # { - # "id": "code_exec_ssh_addr", - # "title": "Code execution SSH address", - # "description": "Address of the SSH server for code execution. Only applies when SSH is enabled.", - # "type": "text", - # "value": settings["code_exec_ssh_addr"], - # } - # ) - - # code_exec_fields.append( - # { - # "id": "code_exec_ssh_port", - # "title": "Code execution SSH port", - # "description": "Port of the SSH server for code execution. Only applies when SSH is enabled.", - # "type": "text", - # "value": settings["code_exec_ssh_port"], - # } - # ) - - # code_exec_section: SettingsSection = { - # "id": "code_exec", - # "title": "Code execution", - # "description": "Configuration of code execution by the agent.", - # "fields": code_exec_fields, - # "tab": "developer", - # } - - # Speech to text section - stt_fields: list[SettingsField] = [] - - stt_fields.append( - { - "id": "stt_microphone_section", - "title": "Microphone device", - "description": "Select the microphone device to use for speech-to-text.", - "value": "", - "type": "html", - } - ) - - stt_fields.append( - { - "id": "stt_model_size", - "title": "Speech-to-text model size", - "description": "Select the speech-to-text model size", - "type": "select", - "value": settings["stt_model_size"], - "options": [ + if subdir != "_example"], + knowledge_subdirs=[{"value": subdir, "label": subdir} + for subdir in files.get_subdirectories("knowledge", exclude="default")], + stt_models=[ {"value": "tiny", "label": "Tiny (39M, English)"}, {"value": "base", "label": "Base (74M, English)"}, {"value": "small", "label": "Small (244M, English)"}, {"value": "medium", "label": "Medium (769M, English)"}, {"value": "large", "label": "Large (1.5B, Multilingual)"}, {"value": "turbo", "label": "Turbo (Multilingual)"}, - ], - } + ] + + ) ) - stt_fields.append( - { - "id": "stt_language", - "title": "Speech-to-text language code", - "description": "Language code (e.g. en, fr, it)", - "type": "text", - "value": settings["stt_language"], - } + # ensure dropdown options include currently selected values + additional = out["additional"] + current = out["settings"] + + additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("chat_model_provider")) + additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("util_model_provider")) + additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("browser_model_provider")) + additional["embedding_providers"] = _ensure_option_present(additional.get("embedding_providers"), current.get("embed_model_provider")) + additional["shell_interfaces"] = _ensure_option_present(additional.get("shell_interfaces"), current.get("shell_interface")) + additional["agent_subdirs"] = _ensure_option_present(additional.get("agent_subdirs"), current.get("agent_profile")) + additional["knowledge_subdirs"] = _ensure_option_present(additional.get("knowledge_subdirs"), current.get("agent_knowledge_subdir")) + additional["stt_models"] = _ensure_option_present(additional.get("stt_models"), current.get("stt_model_size")) + + # masked api keys + providers = get_providers("chat") + get_providers("embedding") + for provider in providers: + provider_name = provider["value"] + api_key = settings["api_keys"].get(provider_name, models.get_api_key(provider_name)) + settings["api_keys"][provider_name] = API_KEY_PLACEHOLDER if api_key and api_key != "None" else "" + + # load auth from dotenv + out["settings"]["auth_login"] = dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or "" + out["settings"]["auth_password"] = ( + PASSWORD_PLACEHOLDER if dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) else "" + ) + out["settings"]["rfc_password"] = ( + PASSWORD_PLACEHOLDER if dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD) else "" + ) + out["settings"]["root_password"] = ( + PASSWORD_PLACEHOLDER if dotenv.get_dotenv_value(dotenv.KEY_ROOT_PASSWORD) else "" ) - stt_fields.append( - { - "id": "stt_silence_threshold", - "title": "Microphone silence threshold", - "description": "Silence detection threshold. Lower values are more sensitive to noise.", - "type": "range", - "min": 0, - "max": 1, - "step": 0.01, - "value": settings["stt_silence_threshold"], - } - ) - - stt_fields.append( - { - "id": "stt_silence_duration", - "title": "Microphone silence duration (ms)", - "description": "Duration of silence before the system considers speaking to have ended.", - "type": "text", - "value": settings["stt_silence_duration"], - } - ) - - stt_fields.append( - { - "id": "stt_waiting_timeout", - "title": "Microphone waiting timeout (ms)", - "description": "Duration of silence before the system closes the microphone.", - "type": "text", - "value": settings["stt_waiting_timeout"], - } - ) - - # TTS fields - tts_fields: list[SettingsField] = [] - - tts_fields.append( - { - "id": "tts_kokoro", - "title": "Enable Kokoro TTS", - "description": "Enable higher quality server-side AI (Kokoro) instead of browser-based text-to-speech.", - "type": "switch", - "value": settings["tts_kokoro"], - } - ) - - speech_section: SettingsSection = { - "id": "speech", - "title": "Speech", - "description": "Voice transcription and speech synthesis settings.", - "fields": stt_fields + tts_fields, - "tab": "agent", - } - - # MCP section - mcp_client_fields: list[SettingsField] = [] - - mcp_client_fields.append( - { - "id": "mcp_servers_config", - "title": "MCP Servers Configuration", - "description": "External MCP servers can be configured here.", - "type": "button", - "value": "Open", - } - ) - - mcp_client_fields.append( - { - "id": "mcp_servers", - "title": "MCP Servers", - "description": "(JSON list of) >> RemoteServer <<: [name, url, headers, timeout (opt), sse_read_timeout (opt), disabled (opt)] / >> Local Server <<: [name, command, args, env, encoding (opt), encoding_error_handler (opt), disabled (opt)]", - "type": "textarea", - "value": settings["mcp_servers"], - "hidden": True, - } - ) - - mcp_client_fields.append( - { - "id": "mcp_client_init_timeout", - "title": "MCP Client Init Timeout", - "description": "Timeout for MCP client initialization (in seconds). Higher values might be required for complex MCPs, but might also slowdown system startup.", - "type": "number", - "value": settings["mcp_client_init_timeout"], - } - ) - - mcp_client_fields.append( - { - "id": "mcp_client_tool_timeout", - "title": "MCP Client Tool Timeout", - "description": "Timeout for MCP client tool execution. Higher values might be required for complex tools, but might also result in long responses with failing tools.", - "type": "number", - "value": settings["mcp_client_tool_timeout"], - } - ) - - mcp_client_section: SettingsSection = { - "id": "mcp_client", - "title": "External MCP Servers", - "description": "Agent Zero can use external MCP servers, local or remote as tools.", - "fields": mcp_client_fields, - "tab": "mcp", - } - - # Secrets section - secrets_fields: list[SettingsField] = [] - + #secrets secrets_manager = get_default_secrets_manager() try: - secrets = secrets_manager.get_masked_secrets() + out["settings"]["secrets"] = secrets_manager.get_masked_secrets() except Exception: - secrets = "" + out["settings"]["secrets"] = "" - secrets_fields.append({ - "id": "variables", - "title": "Variables Store", - "description": "Store non-sensitive variables in .env format e.g. EMAIL_IMAP_SERVER=\"imap.gmail.com\", one item per line. You can use comments starting with # to add descriptions for the agent. See example.
These variables are visible to LLMs and in chat history, they are not being masked.", - "type": "textarea", - "value": settings["variables"].strip(), - "style": "height: 20em", - }) + # mask API keys before sending to frontend + if isinstance(out["settings"].get("api_keys"), dict): + for provider, value in list(out["settings"]["api_keys"].items()): + if value: + out["settings"]["api_keys"][provider] = API_KEY_PLACEHOLDER - secrets_fields.append({ - "id": "secrets", - "title": "Secrets Store", - "description": "Store secrets and credentials in .env format e.g. EMAIL_PASSWORD=\"s3cret-p4$$w0rd\", one item per line. You can use comments starting with # to add descriptions for the agent. See example.
These variables are not visile to LLMs and in chat history, they are being masked. ⚠️ only values with length >= 4 are being masked to prevent false positives. ", - "type": "textarea", - "value": secrets, - "style": "height: 20em", - }) - - secrets_section: SettingsSection = { - "id": "secrets", - "title": "Secrets Management", - "description": "Manage secrets and credentials that agents can use without exposing values to LLMs, chat history or logs. Placeholders are automatically replaced with values just before tool calls. If bare passwords occur in tool results, they are masked back to placeholders.", - "fields": secrets_fields, - "tab": "external", - } - - mcp_server_fields: list[SettingsField] = [] - - mcp_server_fields.append( - { - "id": "mcp_server_enabled", - "title": "Enable A0 MCP Server", - "description": "Expose Agent Zero as an SSE/HTTP MCP server. This will make this A0 instance available to MCP clients.", - "type": "switch", - "value": settings["mcp_server_enabled"], - } - ) - - mcp_server_fields.append( - { - "id": "mcp_server_token", - "title": "MCP Server Token", - "description": "Token for MCP server authentication.", - "type": "text", - "hidden": True, - "value": settings["mcp_server_token"], - } - ) - - mcp_server_section: SettingsSection = { - "id": "mcp_server", - "title": "A0 MCP Server", - "description": "Agent Zero can be exposed as an SSE MCP server. See connection example.", - "fields": mcp_server_fields, - "tab": "mcp", - } - - # -------- A2A Section -------- - a2a_fields: list[SettingsField] = [] - - a2a_fields.append( - { - "id": "a2a_server_enabled", - "title": "Enable A2A server", - "description": "Expose Agent Zero as A2A server. This allows other agents to connect to A0 via A2A protocol.", - "type": "switch", - "value": settings["a2a_server_enabled"], - } - ) - - a2a_section: SettingsSection = { - "id": "a2a_server", - "title": "A0 A2A Server", - "description": "Agent Zero can be exposed as an A2A server. See connection example.", - "fields": a2a_fields, - "tab": "mcp", - } - - - # External API section - external_api_fields: list[SettingsField] = [] - - external_api_fields.append( - { - "id": "external_api_examples", - "title": "API Examples", - "description": "View examples for using Agent Zero's external API endpoints with API key authentication.", - "type": "button", - "value": "Show API Examples", - } - ) - - external_api_section: SettingsSection = { - "id": "external_api", - "title": "External API", - "description": "Agent Zero provides external API endpoints for integration with other applications. " - "These endpoints use API key authentication and support text messages and file attachments.", - "fields": external_api_fields, - "tab": "external", - } - - # update checker section - update_checker_fields: list[SettingsField] = [] - - update_checker_fields.append( - { - "id": "update_check_enabled", - "title": "Enable Update Checker", - "description": "Enable update checker to notify about newer versions of Agent Zero.", - "type": "switch", - "value": settings["update_check_enabled"], - } - ) - - update_checker_section: SettingsSection = { - "id": "update_checker", - "title": "Update Checker", - "description": "Update checker periodically checks for new releases of Agent Zero and will notify when an update is recommended.
No personal data is sent to the update server, only randomized+anonymized unique ID and current version number, which help us evaluate the importance of the update in case of critical bug fixes etc.", - "fields": update_checker_fields, - "tab": "external", - } - - # Backup & Restore section - backup_fields: list[SettingsField] = [] - - backup_fields.append( - { - "id": "backup_create", - "title": "Create Backup", - "description": "Create a backup archive of selected files and configurations " - "using customizable patterns.", - "type": "button", - "value": "Create Backup", - } - ) - - backup_fields.append( - { - "id": "backup_restore", - "title": "Restore from Backup", - "description": "Restore files and configurations from a backup archive " - "with pattern-based selection.", - "type": "button", - "value": "Restore Backup", - } - ) - - backup_section: SettingsSection = { - "id": "backup_restore", - "title": "Backup & Restore", - "description": "Backup and restore Agent Zero data and configurations " - "using glob pattern-based file selection.", - "fields": backup_fields, - "tab": "backup", - } - - # Add the section to the result - result: SettingsOutput = { - "sections": [ - agent_section, - chat_model_section, - util_model_section, - browser_model_section, - embed_model_section, - memory_section, - speech_section, - api_keys_section, - litellm_section, - secrets_section, - auth_section, - mcp_client_section, - mcp_server_section, - a2a_section, - external_api_section, - update_checker_section, - backup_section, - dev_section, - # code_exec_section, - ] - } - return result + # normalize certain fields + for key, value in list(out["settings"].items()): + # convert kwargs dicts to .env format + if (key.endswith("_kwargs") or key=="browser_http_headers") and isinstance(value, dict): + out["settings"][key] = _dict_to_env(value) + return out def _get_api_key_field(settings: Settings, provider: str, title: str) -> SettingsField: @@ -1347,27 +315,19 @@ def _get_api_key_field(settings: Settings, provider: str, title: str) -> Setting } -def convert_in(settings: dict) -> Settings: +def convert_in(settings: Settings) -> Settings: current = get_settings() - for section in settings["sections"]: - if "fields" in section: - for field in section["fields"]: - # Skip saving if value is a placeholder - should_skip = ( - field["value"] == PASSWORD_PLACEHOLDER or - field["value"] == API_KEY_PLACEHOLDER - ) - if not should_skip: - # Special handling for browser_http_headers - if field["id"] == "browser_http_headers" or field["id"].endswith("_kwargs"): - current[field["id"]] = _env_to_dict(field["value"]) - elif field["id"].startswith("api_key_"): - current["api_keys"][field["id"]] = field["value"] - else: - current[field["id"]] = field["value"] + for key, value in settings.items(): + # Special handling for browser_http_headers and *_kwargs (stored as .env text) + if (key == "browser_http_headers" or key.endswith("_kwargs")) and isinstance(value, str): + current[key] = _env_to_dict(value) + continue + + current[key] = value return current + def get_settings() -> Settings: global _settings if not _settings: @@ -1385,12 +345,13 @@ def set_settings(settings: Settings, apply: bool = True): _write_settings_file(_settings) if apply: _apply_settings(previous) + return _settings def set_settings_delta(delta: dict, apply: bool = True): current = get_settings() new = {**current, **delta} - set_settings(new, apply) # type: ignore + return set_settings(new, apply) # type: ignore def merge_settings(original: Settings, delta: dict) -> Settings: @@ -1468,17 +429,16 @@ def _remove_sensitive_settings(settings: Settings): def _write_sensitive_settings(settings: Settings): for key, val in settings["api_keys"].items(): - dotenv.save_dotenv_value(key.upper(), val) + if val != API_KEY_PLACEHOLDER: + dotenv.save_dotenv_value(key.upper(), val) dotenv.save_dotenv_value(dotenv.KEY_AUTH_LOGIN, settings["auth_login"]) - if settings["auth_password"]: + if settings["auth_password"] != PASSWORD_PLACEHOLDER: dotenv.save_dotenv_value(dotenv.KEY_AUTH_PASSWORD, settings["auth_password"]) - if settings["rfc_password"]: + if settings["rfc_password"] != PASSWORD_PLACEHOLDER: dotenv.save_dotenv_value(dotenv.KEY_RFC_PASSWORD, settings["rfc_password"]) - - if settings["root_password"]: + if settings["root_password"] != PASSWORD_PLACEHOLDER: dotenv.save_dotenv_value(dotenv.KEY_ROOT_PASSWORD, settings["root_password"]) - if settings["root_password"]: set_root_password(settings["root_password"]) # Handle secrets separately - merge with existing preserving comments/order and support deletions diff --git a/python/helpers/task_scheduler.py b/python/helpers/task_scheduler.py index 5f9321754..1938db367 100644 --- a/python/helpers/task_scheduler.py +++ b/python/helpers/task_scheduler.py @@ -22,7 +22,7 @@ from python.helpers.print_style import PrintStyle from python.helpers.defer import DeferredTask from python.helpers.files import get_abs_path, make_dirs, read_file, write_file from python.helpers.localization import Localization -from python.helpers import projects +from python.helpers import projects, guids import pytz from typing import Annotated @@ -118,7 +118,7 @@ class TaskPlan(BaseModel): class BaseTask(BaseModel): - uuid: str = Field(default_factory=lambda: str(uuid.uuid4())) + uuid: str = Field(default_factory=lambda: guids.generate_id()) context_id: Optional[str] = Field(default=None) state: TaskState = Field(default=TaskState.IDLE) name: str = Field() diff --git a/python/helpers/vector_db.py b/python/helpers/vector_db.py index 2b94960e3..8a813caba 100644 --- a/python/helpers/vector_db.py +++ b/python/helpers/vector_db.py @@ -1,5 +1,4 @@ from typing import Any, List, Sequence -import uuid from langchain_community.vectorstores import FAISS # faiss needs to be patched for python 3.12 on arm #TODO remove once not needed @@ -17,6 +16,7 @@ from langchain.embeddings import CacheBackedEmbeddings from simpleeval import simple_eval from agent import Agent +from python.helpers import guids class MyFaiss(FAISS): @@ -99,7 +99,7 @@ class VectorDB: return result async def insert_documents(self, docs: list[Document]): - ids = [str(uuid.uuid4()) for _ in range(len(docs))] + ids = [guids.generate_id() for _ in range(len(docs))] if ids: for doc, id in zip(docs, ids): diff --git a/tests/test_prompt_fence_policy.py b/tests/test_prompt_fence_policy.py deleted file mode 100644 index 624aedee9..000000000 --- a/tests/test_prompt_fence_policy.py +++ /dev/null @@ -1,40 +0,0 @@ -import sys -import json -from pathlib import Path -from types import SimpleNamespace - -PROJECT_ROOT = Path(__file__).resolve().parents[1] -if str(PROJECT_ROOT) not in sys.path: - sys.path.insert(0, str(PROJECT_ROOT)) - -from agent import Agent - - -class _DummyContext: - def get_data(self, key: str, recursive: bool = True): - return None - - -def _dummy_agent(profile: str = "agent0"): - # Agent.read_prompt only needs config.profile and context.get_data() to resolve prompt paths. - return SimpleNamespace(config=SimpleNamespace(profile=profile), context=_DummyContext()) - - -def test_agent_read_prompt_preserves_embedded_fenced_examples_in_markdown_prompts(): - dummy = _dummy_agent() - text = Agent.read_prompt(dummy, "agent.system.tool.response.md") - - assert "usage:" in text - assert "~~~json" in text - assert "~~~" in text - - -def test_agent_read_prompt_strips_full_json_template_fences_for_json_consumers(): - dummy = _dummy_agent() - text = Agent.read_prompt(dummy, "fw.initial_message.md") - - assert "```" not in text - assert "~~~" not in text - - parsed = json.loads(text) - assert parsed.get("tool_name") == "response" diff --git a/tests/test_remove_code_fences.py b/tests/test_remove_code_fences.py deleted file mode 100644 index 4ea7d114d..000000000 --- a/tests/test_remove_code_fences.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -from pathlib import Path - -PROJECT_ROOT = Path(__file__).resolve().parents[1] -if str(PROJECT_ROOT) not in sys.path: - sys.path.insert(0, str(PROJECT_ROOT)) - -from python.helpers.files import remove_code_fences - - -def test_remove_code_fences_does_not_strip_inline_tildes_or_join_lines(): - src = ( - "full message is automatically markdown do not wrap ~~~markdown\n" - "use emojis as icons improve readability\n" - "usage:\n" - "~~~json\n" - "{\n" - ' \"a\": 1\n' - "}\n" - "~~~\n" - ) - - out = remove_code_fences(src) - - assert "wrap ~~~markdown\nuse emojis" in out - assert "wrap ~~~markdownuse emojis" not in out - assert "~~~json" not in out - assert "\n~~~\n" not in out - assert '"a": 1' in out diff --git a/webui/components/chat/input/bottom-actions.html b/webui/components/chat/input/bottom-actions.html index 5c603314e..1353fbbd2 100644 --- a/webui/components/chat/input/bottom-actions.html +++ b/webui/components/chat/input/bottom-actions.html @@ -4,6 +4,7 @@ import { store } from "/components/chat/input/input-store.js"; import { store as historyStore } from "/components/modals/history/history-store.js"; import { store as contextStore } from "/components/modals/context/context-store.js"; + import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; @@ -24,6 +25,16 @@ + + + + + + + + + + + + + + + diff --git a/webui/components/modals/full-screen-input/full-screen-input.html b/webui/components/modals/full-screen-input/full-screen-input.html index 0cd403a40..c3aa1cb9f 100644 --- a/webui/components/modals/full-screen-input/full-screen-input.html +++ b/webui/components/modals/full-screen-input/full-screen-input.html @@ -33,7 +33,7 @@
-