From 2e6b0a36b6b9b1f2422b473500d6a8eade90c1d2 Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Wed, 22 Apr 2026 22:12:37 +0530 Subject: [PATCH 01/12] API Documentation Co-authored-by: Copilot --- .github/workflows/pages.yml | 13 ++ docs/api-docs/bots/index.md | 17 ++ docs/api-docs/bots/log-analysis-bot.md | 29 +++ docs/api-docs/components/bot.md | 50 ++++++ docs/api-docs/components/index.md | 10 ++ docs/api-docs/overview.md | 15 ++ mkdocs.yml | 37 +++- src/microbots/MicroBot.py | 238 +++++++++++++------------ src/microbots/constants.py | 8 + src/microbots/extras/mount.py | 42 ++--- 10 files changed, 320 insertions(+), 139 deletions(-) create mode 100644 docs/api-docs/bots/index.md create mode 100644 docs/api-docs/bots/log-analysis-bot.md create mode 100644 docs/api-docs/components/bot.md create mode 100644 docs/api-docs/components/index.md create mode 100644 docs/api-docs/overview.md diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 44fe9c7f..ceebdbf2 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -3,6 +3,14 @@ name: Deploy GitHub Pages on: push: branches: ["main"] + paths: + - "docs/**" + - "mkdocs.yml" + - "src/microbots/MicroBot.py" + - "src/microbots/bot/**" + - "src/microbots/llm/**" + - "src/microbots/environment/**" + - "src/microbots/tools/**" workflow_dispatch: permissions: @@ -29,6 +37,11 @@ jobs: - name: Install Zensical run: pip install zensical + - name: Install mkdocstrings and project + run: | + pip install "mkdocstrings[python]" + pip install -e . + - name: Build site run: zensical build --clean diff --git a/docs/api-docs/bots/index.md b/docs/api-docs/bots/index.md new file mode 100644 index 00000000..67690b5c --- /dev/null +++ b/docs/api-docs/bots/index.md @@ -0,0 +1,17 @@ +# Bots + +This section covers the different types of bots that can be built with Microbots, along with detailed guides and examples for each type. + +All bots extend the core [`MicroBot`](../components/bot.md) class with specialized system prompts, permissions, and tools. + +| Bot | Purpose | Access Level | +|-----|---------|-------------| +| [LogAnalysisBot](log-analysis-bot.md) | Analyze log files and identify root causes | Read-only | +| ReadingBot | Code comprehension and analysis | Read-only | +| WritingBot | Controlled file edits | Read-write (restricted commands) | +| BrowsingBot | Web search and browsing | N/A | +| AgentBoss | Task decomposition and delegation | Read-write | +| CopilotBot | GitHub Copilot SDK wrapper | Configurable | + +!!! note "Auto-generated API references" + The API references on these pages are **auto-generated from source code docstrings**. When the source code changes, the documentation updates automatically on the next build. diff --git a/docs/api-docs/bots/log-analysis-bot.md b/docs/api-docs/bots/log-analysis-bot.md new file mode 100644 index 00000000..cc419c18 --- /dev/null +++ b/docs/api-docs/bots/log-analysis-bot.md @@ -0,0 +1,29 @@ +# LogAnalysisBot + +The `LogAnalysisBot` analyzes log files and identifies root causes of failures. It mounts a code directory as read-only context and copies the target log file into the container for analysis. + +## Quick Example + +```python +from microbots import LogAnalysisBot + +bot = LogAnalysisBot( + model="azure-openai/gpt-4.1", + folder_to_mount="/path/to/source/code", +) + +result = bot.run(file_name="/path/to/error.log") +print(result.status, result.result) +``` + +## How It Works + +1. The source code directory is mounted **read-only** at the sandbox path for context. +2. The log file is **copied** into `/var/log/` inside the container. +3. The bot analyzes the log, cross-references with the source code, and identifies the root cause. + +## API Reference + + + +::: microbots.bot.LogAnalysisBot.LogAnalysisBot diff --git a/docs/api-docs/components/bot.md b/docs/api-docs/components/bot.md new file mode 100644 index 00000000..4f330d06 --- /dev/null +++ b/docs/api-docs/components/bot.md @@ -0,0 +1,50 @@ +# Bot + +The `MicroBot` class is the core autonomous agent. All specialized bots (`ReadingBot`, `WritingBot`, etc.) extend this class. + +You can use `MicroBot` directly for custom bots or subclass it for specialized behavior. + +## Quick Example + +```python +from microbots import MicroBot +from microbots.extras.mount import Mount, MountType, PermissionLabels + +bot = MicroBot( + model="azure-openai/gpt-5-swe-agent", + system_prompt="You are a helpful coding assistant.", + folder_to_mount=Mount( + host_path="code", + mount_type=MountType.MOUNT, + permission=PermissionLabels.READ_ONLY, + ), +) + +result = bot.run(task="Analyze the project structure") +print(result.status, result.result) +``` + +## API Reference + + + +::: microbots.MicroBot.MicroBot + options: + show_source: false + + +::: microbots.MicroBot.BotRunResult + options: + show_source: false + +::: microbots.constants.ModelProvider + options: + show_source: false + +::: microbots.extras.mount.MountType + options: + show_source: false + +::: microbots.extras.mount.Mount + options: + show_source: false diff --git a/docs/api-docs/components/index.md b/docs/api-docs/components/index.md new file mode 100644 index 00000000..ef6b4ac1 --- /dev/null +++ b/docs/api-docs/components/index.md @@ -0,0 +1,10 @@ +# Components + +This section covers the core components of Microbots, providing in-depth explanations and API references for each. + +Microbots is built around these below foundational components: + +- **[Bot](bot.md)** — The core `MicroBot` class that powers all autonomous agents. Every specialized bot extends this base class. + +!!! note "Auto-generated API references" + The API references on these pages are **auto-generated from source code docstrings** using [mkdocstrings](https://mkdocstrings.github.io/). When the source code changes, the documentation updates automatically on the next build. diff --git a/docs/api-docs/overview.md b/docs/api-docs/overview.md new file mode 100644 index 00000000..e29ab041 --- /dev/null +++ b/docs/api-docs/overview.md @@ -0,0 +1,15 @@ +# API Documentation + +This section provides comprehensive API references for Microbots, auto-generated from source code docstrings. When the source code changes, the documentation updates automatically on the next build. + +## Components + +The foundational building blocks of Microbots. + +- **[Bot](components/bot.md)** — The core `MicroBot` class that powers all autonomous agents. Covers the base class, `BotRunResult`, `BotType`, and the agent execution loop. + +## Bots + +Specialized bot implementations, each tailored for a specific use case. + +- **[LogAnalysisBot](bots/log-analysis-bot.md)** — Analyzes log files inside a sandboxed container and identifies root causes by cross-referencing with source code. diff --git a/mkdocs.yml b/mkdocs.yml index e4eca852..078dd53e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,13 +22,38 @@ theme: icon: material/brightness-4 name: Switch to light mode features: - - navigation.instant - - navigation.sections + - navigation.indexes - navigation.top - navigation.tabs - content.code.copy +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: [src] + options: + docstring_style: numpy + show_source: true + show_root_heading: true + show_symbol_type_heading: true + show_symbol_type_toc: true + heading_level: 2 + members_order: source + show_signature_annotations: true + separate_signature: true + merge_init_into_class: true + show_labels: true + show_if_no_docstring: true + group_by_category: false + filters: + - "!^_" + markdown_extensions: + - toc: + permalink: true + toc_depth: 4 - pymdownx.highlight: anchor_linenums: true - pymdownx.superfences @@ -42,6 +67,14 @@ markdown_extensions: nav: - Getting Started: - Home: index.md + - API Documentation: + - Overview: api-docs/overview.md + - Components: + - api-docs/components/index.md + - Bot: api-docs/components/bot.md + - Bots: + - api-docs/bots/index.md + - LogAnalysisBot: api-docs/bots/log-analysis-bot.md - Guides: - CopilotBot: copilot-bot.md - Authentication: authentication.md diff --git a/src/microbots/MicroBot.py b/src/microbots/MicroBot.py index a8c9b7a1..eaf3d218 100644 --- a/src/microbots/MicroBot.py +++ b/src/microbots/MicroBot.py @@ -1,24 +1,24 @@ -from collections.abc import Iterable import json import os -from pprint import pformat import re import time +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum from logging import getLogger +from pprint import pformat from typing import Optional from microbots.constants import ModelProvider from microbots.environment.local_docker.LocalDockerEnvironment import ( LocalDockerEnvironment, ) +from microbots.extras.mount import Mount, MountType from microbots.llm.anthropic_api import AnthropicApi -from microbots.llm.openai_api import OpenAIApi -from microbots.llm.ollama_local import OllamaLocal from microbots.llm.llm import llm_output_format_str +from microbots.llm.ollama_local import OllamaLocal +from microbots.llm.openai_api import OpenAIApi from microbots.tools.tool import ToolAbstract, get_tool_from_call -from microbots.extras.mount import Mount, MountType from microbots.utils.logger import LogLevelEmoji from microbots.utils.network import get_free_port @@ -61,38 +61,46 @@ class BotType(StrEnum): @dataclass class BotRunResult: + """Result of a bot run execution. + + Contains the status, result output, and any error information from a bot's run. + """ + status: bool + """Whether the bot run completed successfully.""" result: str | None + """The output produced by the bot run, or None if no output was generated.""" error: Optional[str] + """Error message if the run failed, or None if successful.""" class MicroBot: - """ - The core Microbot class. + """The core Microbot class. - MicroBot class is the core class representing the autonomous agent. Other bots are extensions of this class. + MicroBot is the core class representing the autonomous agent. Other bots are extensions of this class. If you want to create a custom bot, you can directly use this class or extend it into your own bot class. - Attributes + Parameters ---------- - model : str - The model to use for the bot, in the format /. - bot_type : BotType - The type of bot being created. It's unused. Will be removed soon. - system_prompt : Optional[str] - The system prompt to guide the bot's behavior. - environment : Optional[any] - The execution environment for the bot. If not provided, a default - LocalDockerEnvironment will be created. - additional_tools : Optional[list[ToolAbstract]] - A list of additional tools to install in the bot's environment. - folder_to_mount : Optional[Mount] - A folder to mount into the bot's environment. The bot will be given - access to this folder based on the specified permissions. This will - be the main code folder where the bot will work. Additional folders - can be mounted during the run() method. Refer to `Mount` class - regarding the directory structure and permission details. Defaults - to None. + model : str + The model to use, in the format ``/``. + See [ModelProvider][microbots.constants.ModelProvider] for supported providers. + bot_type : BotType, optional + The type of bot being created. Defaults to ``BotType.CUSTOM_BOT``. + system_prompt : str, optional + The system prompt to guide the bot's behavior. Defaults to None. + environment : any, optional + The execution environment for the bot. If not provided, a default + ``LocalDockerEnvironment`` will be created. + additional_tools : list[ToolAbstract], optional + A list of additional tools to install in the bot's environment. + Defaults to None. + folder_to_mount : Mount, optional + A folder to mount into the bot's environment. See + [Mount][microbots.extras.mount.Mount] for details. Defaults to None. + token_provider : any, optional + A token provider for authentication. Required for Azure OpenAI + with Azure AD auth. Defaults to None. """ def __init__( @@ -105,65 +113,43 @@ def __init__( folder_to_mount: Optional[Mount] = None, token_provider: Optional[any] = None, ): - """ - Init function for MicroBot class. - - Parameters - ---------- - model :str - The model to use for the bot, in the format /. - bot_type :BotType - The type of bot being created. It's unused. Will be removed soon. - system_prompt :Optional[str] - The system prompt to guide the bot's behavior. Defaults to None. - environment :Optional[any] - The execution environment for the bot. If not provided, a default - LocalDockerEnvironment will be created. - additional_tools :Optional[list[ToolAbstract]] - A list of additional tools to install in the bot's environment. - Defaults to None (treated as an empty list). - folder_to_mount :Optional[Mount] - A folder to mount into the bot's environment. The bot will be given - access to this folder based on the specified permissions. This will - be the main code folder where the bot will work. Additional folders - can be mounted using the run() method. Refer to `Mount` class - regarding the directory structure and permission details. Defaults - to None. - - Note: Supports only mount type MountType.MOUNT for now. - """ - - self.folder_to_mount = folder_to_mount + """Create a new MicroBot instance.""" - # TODO : Need to check on the purpose of variable `mounted` - # 1. If we allow user to mount multiple directories, - # we should able to get it as an argument and store them in self.mounted. - # This require changes in _create_environment to handle multiple mount directories or files. - # - # 2. We should let user to mount only one directory. In that case self.mounted may not be required. - # Just one self.folder_to_mount and necessary extra mounts at the derived class similar to LogAnalyticsBot. + self.folder_to_mount: Optional[Mount] = folder_to_mount + """A folder to mount into the bot's environment. The bot will be given access to this folder based on the specified permissions. This will be the main code folder where the bot will work. Additional folders can be mounted during the ``run()`` method. Refer to [Mount][microbots.extras.mount.Mount] for directory structure and permission details. Supports only ``MountType.MOUNT`` for now.""" - self.mounted = [] + self.mounted: list[Mount] = [] + """List of all mounted directories in the bot's environment.""" if folder_to_mount is not None: self._validate_folder_to_mount(folder_to_mount) self.mounted.append(folder_to_mount) - self.system_prompt = system_prompt - self.model = model - self.bot_type = bot_type - self.environment = environment - self.additional_tools = additional_tools or [] - - # TODO: Replace iteration_count and max_iterations with cost management. - # Iteration count represents overall LLM interactions including interactions - # done by sub agents. - self.iteration_count = 0 - self.max_iterations = 0 - self.step_count = 0 + self.system_prompt: Optional[str] = system_prompt + """The system prompt to guide the bot's behavior. Defaults to None.""" + self.model: str = model + """The model to use for the bot, in the format ``/``. See [ModelProvider][microbots.constants.ModelProvider] for supported providers.""" + self.bot_type: BotType = bot_type + """The type of bot being created. It's unused. Will be removed soon.""" + self.environment: Optional[any] = environment + """The execution environment for the bot. If not provided, a default ``LocalDockerEnvironment`` will be created.""" + self.additional_tools: list[ToolAbstract] = additional_tools or [] + """A list of additional tools to install in the bot's environment. Defaults to an empty list.""" + + self.iteration_count: int = 0 + """Number of LLM interactions so far, including sub-agent interactions.""" + self.max_iterations: int = 0 + """Maximum allowed iterations for the current run.""" + self.step_count: int = 0 + """Number of steps completed in the current run.""" self._validate_model_and_provider(model) - self.model_provider = model.split("/")[0] - self.deployment_name = model.split("/")[1] + self.model_provider: str = model.split("/")[0] + """The LLM provider extracted from the model string. See [ModelProvider][microbots.constants.ModelProvider] for supported values.""" + self.deployment_name: str = model.split("/")[1] + """The model deployment name extracted from the model string.""" + + self.token_provider: Optional[any] = None + """Token provider for Azure AD authentication. If not provided and ``AZURE_AUTH_METHOD`` is set to ``azure_ad``, a default provider using ``DefaultAzureCredential`` will be created automatically.""" # Only auto-create token provider from env for providers that support Azure AD tokens. if token_provider is not None: @@ -173,7 +159,10 @@ def __init__( and self.model_provider == ModelProvider.OPENAI ): try: - from azure.identity import DefaultAzureCredential, get_bearer_token_provider + from azure.identity import ( + DefaultAzureCredential, + get_bearer_token_provider, + ) except ImportError: raise ImportError( "Azure AD authentication requires the 'azure-identity' package. " @@ -200,9 +189,32 @@ def run( task: str, additional_mounts: Optional[list[Mount]] = None, max_iterations: int = 20, - timeout_in_seconds: int = 200 + timeout_in_seconds: int = 200, ) -> BotRunResult: + """Execute a task with the bot. + Parameters + ---------- + task : str + The task to execute. + additional_mounts : Optional[list[Mount]] + Extra mounts to add to the environment. Only ``MountType.COPY`` is + supported. Defaults to None. + max_iterations : int + Maximum number of agent loop iterations. Defaults to 20. + timeout_in_seconds : int + Timeout for the task in seconds. Defaults to 200. + + Returns + ------- + BotRunResult + The result containing status, output, and any errors. + + Raises + ------ + ValueError + If ``max_iterations`` is less than or equal to 0. + """ if max_iterations <= 0: raise ValueError("max_iterations must be greater than 0") @@ -253,7 +265,6 @@ def run( return_value.error = f"Timeout of {timeout} seconds reached" return return_value - logger.info("%s Step-%d %s", "-" * 20, self.step_count, "-" * 20) if llm_response.thoughts: logger.info( @@ -268,7 +279,9 @@ def run( if not is_safe: error_msg = f"Dangerous command detected and blocked: {llm_response.command}\n{explanation}" logger.info("%s %s", LogLevelEmoji.WARNING, error_msg) - llm_response = self.llm.ask(f"COMMAND_ERROR: {error_msg}\nPlease provide a safer alternative command.") + llm_response = self.llm.ask( + f"COMMAND_ERROR: {error_msg}\nPlease provide a safer alternative command." + ) continue tool = get_tool_from_call(llm_response.command, self.additional_tools) @@ -278,11 +291,11 @@ def run( llm_command_output = self.environment.execute(llm_response.command) logger.debug( - " 🔧 Command executed.\nReturn Code: %d\nStdout:\n%s\nStderr:\n%s", - llm_command_output.return_code, - llm_command_output.stdout, - llm_command_output.stderr, - ) + " 🔧 Command executed.\nReturn Code: %d\nStdout:\n%s\nStderr:\n%s", + llm_command_output.return_code, + llm_command_output.stdout, + llm_command_output.stderr, + ) if llm_command_output.return_code == 0: if llm_command_output.stdout: @@ -290,12 +303,17 @@ def run( # HACK: anthropic-text-editor tool extra formats the output try: output_json = json.loads(llm_command_output.stdout) - if isinstance(output_json, Iterable) and "content" in output_json: + if ( + isinstance(output_json, Iterable) + and "content" in output_json + ): output_text = pformat(output_json["content"]) except json.JSONDecodeError: pass except Exception as e: - logger.warning("Failed to parse command output as JSON, using raw stdout") + logger.warning( + "Failed to parse command output as JSON, using raw stdout" + ) logger.debug("Error details: %s", str(e)) else: output_text = f"Command executed successfully with no output\nreturn code: {llm_command_output.return_code}\nstdout: {llm_command_output.stdout}\nstderr: {llm_command_output.stderr}" @@ -350,7 +368,8 @@ def _create_llm(self): if self.model_provider == ModelProvider.OPENAI: self.llm = OpenAIApi( - system_prompt=system_prompt_with_tools, deployment_name=self.deployment_name, + system_prompt=system_prompt_with_tools, + deployment_name=self.deployment_name, token_provider=self.token_provider, ) elif self.model_provider == ModelProvider.OLLAMA_LOCAL: @@ -359,7 +378,8 @@ def _create_llm(self): ) elif self.model_provider == ModelProvider.ANTHROPIC: self.llm = AnthropicApi( - system_prompt=system_prompt_with_tools, deployment_name=self.deployment_name, + system_prompt=system_prompt_with_tools, + deployment_name=self.deployment_name, token_provider=self.token_provider, ) # No Else case required as model provider is already validated using _validate_model_and_provider @@ -378,9 +398,7 @@ def _validate_folder_to_mount(self, folder_to_mount: Mount): "%s Only MOUNT mount type is supported for folder_to_mount", LogLevelEmoji.ERROR, ) - raise ValueError( - "Only MOUNT mount type is supported for folder_to_mount" - ) + raise ValueError("Only MOUNT mount type is supported for folder_to_mount") def _get_dangerous_command_explanation(self, command: str) -> Optional[str]: """Provides detailed explanation for why a command is dangerous and suggests alternatives. @@ -403,34 +421,34 @@ def _get_dangerous_command_explanation(self, command: str) -> Optional[str]: # Note: Don't convert to lowercase before checking, as we need case-sensitive pattern matching dangerous_checks = [ { - 'pattern': r'\bls\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)', - 'reason': 'Recursive ls commands (ls -R) can generate massive output in large repositories, exceeding context limits', - 'alternative': 'Use targeted paths like "ls drivers/block/" or "ls -la " instead' + "pattern": r"\bls\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)", + "reason": "Recursive ls commands (ls -R) can generate massive output in large repositories, exceeding context limits", + "alternative": 'Use targeted paths like "ls drivers/block/" or "ls -la " instead', }, { - 'pattern': r'\btree\b', - 'reason': 'Tree command recursively lists entire directory structures, which can exceed context limits', - 'alternative': 'Use "ls -la " or "find -maxdepth 2 -type d" for controlled exploration' + "pattern": r"\btree\b", + "reason": "Tree command recursively lists entire directory structures, which can exceed context limits", + "alternative": 'Use "ls -la " or "find -maxdepth 2 -type d" for controlled exploration', }, { - 'pattern': r'\brm\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)', - 'reason': 'Recursive rm commands (rm -r/-rf) can delete entire directory trees, which is destructive', - 'alternative': 'Delete specific files individually or use "rm " to avoid accidental data loss' + "pattern": r"\brm\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)", + "reason": "Recursive rm commands (rm -r/-rf) can delete entire directory trees, which is destructive", + "alternative": 'Delete specific files individually or use "rm " to avoid accidental data loss', }, { - 'pattern': r'\brm\s+--recursive\b', - 'reason': 'Recursive rm commands can delete entire directory trees, which is destructive', - 'alternative': 'Delete specific files individually or use "rm " to avoid accidental data loss' + "pattern": r"\brm\s+--recursive\b", + "reason": "Recursive rm commands can delete entire directory trees, which is destructive", + "alternative": 'Delete specific files individually or use "rm " to avoid accidental data loss', }, { - 'pattern': r'\bfind\b(?!.*-maxdepth)', - 'reason': 'Find command without -maxdepth can recursively search entire filesystems, causing excessive output', - 'alternative': 'Use "find -name "*.ext" -maxdepth 2" to limit search depth and control output size' + "pattern": r"\bfind\b(?!.*-maxdepth)", + "reason": "Find command without -maxdepth can recursively search entire filesystems, causing excessive output", + "alternative": 'Use "find -name "*.ext" -maxdepth 2" to limit search depth and control output size', }, ] for check in dangerous_checks: - if re.search(check['pattern'], stripped_command, re.IGNORECASE): + if re.search(check["pattern"], stripped_command, re.IGNORECASE): return f"REASON: {check['reason']}\nALTERNATIVE: {check['alternative']}" return None diff --git a/src/microbots/constants.py b/src/microbots/constants.py index c4a7ff98..bcfb563c 100644 --- a/src/microbots/constants.py +++ b/src/microbots/constants.py @@ -3,9 +3,17 @@ class ModelProvider(StrEnum): + """Supported LLM providers for MicroBot. + + Use these values as the provider prefix in the model string: ``/``. + """ + OPENAI = "azure-openai" + """Azure OpenAI provider. Example: ``azure-openai/gpt-4o``""" OLLAMA_LOCAL = "ollama-local" + """Local Ollama provider. Example: ``ollama-local/llama3``""" ANTHROPIC = "anthropic" + """Anthropic Claude provider. Example: ``anthropic/claude-sonnet-4-20250514``""" class ModelEnum(StrEnum): diff --git a/src/microbots/extras/mount.py b/src/microbots/extras/mount.py index a36014b2..cc088a49 100644 --- a/src/microbots/extras/mount.py +++ b/src/microbots/extras/mount.py @@ -3,55 +3,41 @@ from pathlib import Path from microbots.constants import PermissionLabels, PermissionMapping -from microbots.utils.path import PathInfo, get_path_info, ends_with_separator +from microbots.utils.path import PathInfo, ends_with_separator, get_path_info class MountType(StrEnum): - """ - Enum representing the type of mount operation. + """Type of mount operation for the bot's sandbox environment.""" - MOUNT : Mount the folder from host to sandbox environment. - COPY : Copy the folder from host to sandbox environment. - """ MOUNT = "mount" + """Mount the folder from host to sandbox environment.""" COPY = "copy" + """Copy the folder from host to sandbox environment.""" @dataclass class Mount: - """ - Folder mount configuration for a microbot environment. + """Folder mount configuration for a microbot environment. All the folders and files to be presented for the Bot should be either mounted or copied to the Bot's sandbox environment using this class. - - Attributes - ---------- - host_path : str - The absolute path on the host machine to be mounted or copied. - sandbox_path : str - The absolute path inside the Bot's sandbox environment where the - host_path will be mounted or copied. If the host_path is a file - and the sandbox_path ends with a path separator ("/"), then - the file will be placed inside the sandbox_path directory with - the same base name as the host file. - permission : PermissionLabels - The permission level for the mounted/copied folder. It should - be one of the values from PermissionLabels enum. - mount_type : MountType, optional - The type of mount operation. Mounting and copying have are the - options. Possible values are MountType enum values. - Default is MountType.MOUNT. """ + host_path: str + """The absolute path on the host machine to be mounted or copied.""" sandbox_path: str + """The absolute path inside the Bot's sandbox environment where the host_path will be mounted or copied. If the host_path is a file and the sandbox_path ends with a path separator, the file will be placed inside the sandbox_path directory with the same base name.""" permission: PermissionLabels + """The permission level for the mounted/copied folder. See [PermissionLabels][microbots.constants.PermissionLabels] for supported values.""" mount_type: MountType = MountType.MOUNT + """The type of mount operation. See [MountType][microbots.extras.mount.MountType] for supported values. Defaults to ``MountType.MOUNT``.""" # These will be set in __post_init__ permission_key: str = field(init=False) + """Resolved permission key string (e.g. ``ro``, ``rw``). Set automatically.""" host_path_info: PathInfo = field(init=False) + """Path metadata for the host path. Set automatically.""" def __post_init__(self): self.permission_key = PermissionMapping.MAPPING.get(self.permission) @@ -65,7 +51,9 @@ def __post_init__(self): f"sandbox_path must be an absolute path. Given: {self.sandbox_path}" ) - if not ends_with_separator(self.host_path) and ends_with_separator(self.sandbox_path): + if not ends_with_separator(self.host_path) and ends_with_separator( + self.sandbox_path + ): # If host_path is a file and sandbox_path ends with separator, # place the file inside the sandbox_path directory with same base name sandbox_path = str(sandbox_path / self.host_path_info.base_name) From a13d6e6e6f8ec8f012afda404a03dda328d988cd Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 11:58:53 +0530 Subject: [PATCH 02/12] microbots doc update Co-authored-by: Copilot --- docs/api-docs/components/bot.md | 9 +++++++++ src/microbots/constants.py | 4 ++++ src/microbots/extras/mount.py | 16 ++++++++++++++++ src/microbots/llm/openai_api.py | 33 ++++++++++++++++++++++----------- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/docs/api-docs/components/bot.md b/docs/api-docs/components/bot.md index 4f330d06..c1e59cba 100644 --- a/docs/api-docs/components/bot.md +++ b/docs/api-docs/components/bot.md @@ -15,6 +15,7 @@ bot = MicroBot( system_prompt="You are a helpful coding assistant.", folder_to_mount=Mount( host_path="code", + sandbox_path="/home/user/code", mount_type=MountType.MOUNT, permission=PermissionLabels.READ_ONLY, ), @@ -28,6 +29,10 @@ print(result.status, result.result) +!!! info "Parameters vs Attributes" + **Parameters** are the arguments you pass when creating an instance of a class (e.g., `MicroBot(model=..., system_prompt=...)`). + **Attributes** are the internal variables available on the instance after creation, used by the class during its operation. + ::: microbots.MicroBot.MicroBot options: show_source: false @@ -48,3 +53,7 @@ print(result.status, result.result) ::: microbots.extras.mount.Mount options: show_source: false + +::: microbots.constants.PermissionLabels + options: + show_source: false diff --git a/src/microbots/constants.py b/src/microbots/constants.py index bcfb563c..2bbb52df 100644 --- a/src/microbots/constants.py +++ b/src/microbots/constants.py @@ -21,8 +21,12 @@ class ModelEnum(StrEnum): class PermissionLabels(StrEnum): + """Permission levels for mounted folders in the bot's sandbox environment.""" + READ_ONLY = "READ_ONLY" + """Read-only access to the mounted folder.""" READ_WRITE = "READ_WRITE" + """Read and write access to the mounted folder.""" class PermissionMapping: diff --git a/src/microbots/extras/mount.py b/src/microbots/extras/mount.py index cc088a49..c38e9a56 100644 --- a/src/microbots/extras/mount.py +++ b/src/microbots/extras/mount.py @@ -22,6 +22,22 @@ class Mount: All the folders and files to be presented for the Bot should be either mounted or copied to the Bot's sandbox environment using this class. + + Parameters + ---------- + host_path : str + The absolute path on the host machine to be mounted or copied. + sandbox_path : str + The absolute path inside the Bot's sandbox environment where the + host_path will be mounted or copied. If the host_path is a file + and the sandbox_path ends with a path separator, the file will be + placed inside the sandbox_path directory with the same base name. + permission : PermissionLabels + The permission level for the mounted/copied folder. See + [PermissionLabels][microbots.constants.PermissionLabels] for supported values. + mount_type : MountType, optional + The type of mount operation. See [MountType][microbots.extras.mount.MountType] + for supported values. Defaults to ``MountType.MOUNT``. """ host_path: str diff --git a/src/microbots/llm/openai_api.py b/src/microbots/llm/openai_api.py index 5713d5d6..a2b1413d 100644 --- a/src/microbots/llm/openai_api.py +++ b/src/microbots/llm/openai_api.py @@ -4,8 +4,8 @@ from dataclasses import asdict from dotenv import load_dotenv -from openai import AzureOpenAI, OpenAI from microbots.llm.llm import LLMAskResponse, LLMInterface +from openai import AzureOpenAI, OpenAI load_dotenv() @@ -17,8 +17,13 @@ class OpenAIApi(LLMInterface): - def __init__(self, system_prompt, deployment_name=deployment_name, max_retries=3, - token_provider: Callable[[], str] | None = None): + def __init__( + self, + system_prompt, + deployment_name=deployment_name, + max_retries=3, + token_provider: Callable[[], str] | None = None, + ): self.token_provider = token_provider if not token_provider and not api_key: @@ -29,7 +34,9 @@ def __init__(self, system_prompt, deployment_name=deployment_name, max_retries=3 if token_provider: if not callable(token_provider): - raise ValueError("token_provider must be a callable that returns a string token.") + raise ValueError( + "token_provider must be a callable that returns a string token." + ) try: token = token_provider() except Exception as e: @@ -44,9 +51,10 @@ def __init__(self, system_prompt, deployment_name=deployment_name, max_retries=3 ) else: # Non-Azure users with a plain API key - self.ai_client = OpenAI( - base_url=endpoint, - api_key=api_key, + self.ai_client = AzureOpenAI( + azure_endpoint=endpoint, + azure_ad_token_provider=token_provider, + api_version=api_version, ) self.deployment_name = deployment_name self.system_prompt = system_prompt @@ -57,7 +65,7 @@ def __init__(self, system_prompt, deployment_name=deployment_name, max_retries=3 self.retries = 0 def ask(self, message) -> LLMAskResponse: - self.retries = 0 # reset retries for each ask. Handled in parent class. + self.retries = 0 # reset retries for each ask. Handled in parent class. self.messages.append({"role": "user", "content": message}) @@ -68,11 +76,15 @@ def ask(self, message) -> LLMAskResponse: input=self.messages, ) self.messages.append({"role": "assistant", "content": response.output_text}) - valid, askResponse = self._validate_llm_response(response=response.output_text) + valid, askResponse = self._validate_llm_response( + response=response.output_text + ) # Remove last assistant message and replace with structured response self.messages.pop() - self.messages.append({"role": "assistant", "content": json.dumps(asdict(askResponse))}) + self.messages.append( + {"role": "assistant", "content": json.dumps(asdict(askResponse))} + ) return askResponse @@ -84,4 +96,3 @@ def clear_history(self): } ] return True - From 52e659f8fb8ae4523d8081709b38b9b1dcd6bc4d Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 12:12:40 +0530 Subject: [PATCH 03/12] fix changed microbot class order --- src/microbots/MicroBot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/microbots/MicroBot.py b/src/microbots/MicroBot.py index eaf3d218..47efb982 100644 --- a/src/microbots/MicroBot.py +++ b/src/microbots/MicroBot.py @@ -1,24 +1,24 @@ +from collections.abc import Iterable import json import os +from pprint import pformat import re import time -from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum from logging import getLogger -from pprint import pformat from typing import Optional from microbots.constants import ModelProvider from microbots.environment.local_docker.LocalDockerEnvironment import ( LocalDockerEnvironment, ) -from microbots.extras.mount import Mount, MountType from microbots.llm.anthropic_api import AnthropicApi -from microbots.llm.llm import llm_output_format_str -from microbots.llm.ollama_local import OllamaLocal from microbots.llm.openai_api import OpenAIApi +from microbots.llm.ollama_local import OllamaLocal +from microbots.llm.llm import llm_output_format_str from microbots.tools.tool import ToolAbstract, get_tool_from_call +from microbots.extras.mount import Mount, MountType from microbots.utils.logger import LogLevelEmoji from microbots.utils.network import get_free_port From fd9aa4a4039538d526906340652c29294abb7767 Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 12:17:15 +0530 Subject: [PATCH 04/12] fix format changes in microbot.py --- src/microbots/MicroBot.py | 73 +++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/src/microbots/MicroBot.py b/src/microbots/MicroBot.py index 47efb982..276daf9e 100644 --- a/src/microbots/MicroBot.py +++ b/src/microbots/MicroBot.py @@ -159,10 +159,7 @@ def __init__( and self.model_provider == ModelProvider.OPENAI ): try: - from azure.identity import ( - DefaultAzureCredential, - get_bearer_token_provider, - ) + from azure.identity import DefaultAzureCredential, get_bearer_token_provider except ImportError: raise ImportError( "Azure AD authentication requires the 'azure-identity' package. " @@ -265,6 +262,7 @@ def run( return_value.error = f"Timeout of {timeout} seconds reached" return return_value + logger.info("%s Step-%d %s", "-" * 20, self.step_count, "-" * 20) if llm_response.thoughts: logger.info( @@ -279,9 +277,7 @@ def run( if not is_safe: error_msg = f"Dangerous command detected and blocked: {llm_response.command}\n{explanation}" logger.info("%s %s", LogLevelEmoji.WARNING, error_msg) - llm_response = self.llm.ask( - f"COMMAND_ERROR: {error_msg}\nPlease provide a safer alternative command." - ) + llm_response = self.llm.ask(f"COMMAND_ERROR: {error_msg}\nPlease provide a safer alternative command.") continue tool = get_tool_from_call(llm_response.command, self.additional_tools) @@ -291,11 +287,11 @@ def run( llm_command_output = self.environment.execute(llm_response.command) logger.debug( - " 🔧 Command executed.\nReturn Code: %d\nStdout:\n%s\nStderr:\n%s", - llm_command_output.return_code, - llm_command_output.stdout, - llm_command_output.stderr, - ) + " 🔧 Command executed.\nReturn Code: %d\nStdout:\n%s\nStderr:\n%s", + llm_command_output.return_code, + llm_command_output.stdout, + llm_command_output.stderr, + ) if llm_command_output.return_code == 0: if llm_command_output.stdout: @@ -303,17 +299,12 @@ def run( # HACK: anthropic-text-editor tool extra formats the output try: output_json = json.loads(llm_command_output.stdout) - if ( - isinstance(output_json, Iterable) - and "content" in output_json - ): + if isinstance(output_json, Iterable) and "content" in output_json: output_text = pformat(output_json["content"]) except json.JSONDecodeError: pass except Exception as e: - logger.warning( - "Failed to parse command output as JSON, using raw stdout" - ) + logger.warning("Failed to parse command output as JSON, using raw stdout") logger.debug("Error details: %s", str(e)) else: output_text = f"Command executed successfully with no output\nreturn code: {llm_command_output.return_code}\nstdout: {llm_command_output.stdout}\nstderr: {llm_command_output.stderr}" @@ -368,8 +359,7 @@ def _create_llm(self): if self.model_provider == ModelProvider.OPENAI: self.llm = OpenAIApi( - system_prompt=system_prompt_with_tools, - deployment_name=self.deployment_name, + system_prompt=system_prompt_with_tools, deployment_name=self.deployment_name, token_provider=self.token_provider, ) elif self.model_provider == ModelProvider.OLLAMA_LOCAL: @@ -378,8 +368,7 @@ def _create_llm(self): ) elif self.model_provider == ModelProvider.ANTHROPIC: self.llm = AnthropicApi( - system_prompt=system_prompt_with_tools, - deployment_name=self.deployment_name, + system_prompt=system_prompt_with_tools, deployment_name=self.deployment_name, token_provider=self.token_provider, ) # No Else case required as model provider is already validated using _validate_model_and_provider @@ -398,7 +387,9 @@ def _validate_folder_to_mount(self, folder_to_mount: Mount): "%s Only MOUNT mount type is supported for folder_to_mount", LogLevelEmoji.ERROR, ) - raise ValueError("Only MOUNT mount type is supported for folder_to_mount") + raise ValueError( + "Only MOUNT mount type is supported for folder_to_mount" + ) def _get_dangerous_command_explanation(self, command: str) -> Optional[str]: """Provides detailed explanation for why a command is dangerous and suggests alternatives. @@ -421,34 +412,34 @@ def _get_dangerous_command_explanation(self, command: str) -> Optional[str]: # Note: Don't convert to lowercase before checking, as we need case-sensitive pattern matching dangerous_checks = [ { - "pattern": r"\bls\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)", - "reason": "Recursive ls commands (ls -R) can generate massive output in large repositories, exceeding context limits", - "alternative": 'Use targeted paths like "ls drivers/block/" or "ls -la " instead', + 'pattern': r'\bls\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)', + 'reason': 'Recursive ls commands (ls -R) can generate massive output in large repositories, exceeding context limits', + 'alternative': 'Use targeted paths like "ls drivers/block/" or "ls -la " instead' }, { - "pattern": r"\btree\b", - "reason": "Tree command recursively lists entire directory structures, which can exceed context limits", - "alternative": 'Use "ls -la " or "find -maxdepth 2 -type d" for controlled exploration', + 'pattern': r'\btree\b', + 'reason': 'Tree command recursively lists entire directory structures, which can exceed context limits', + 'alternative': 'Use "ls -la " or "find -maxdepth 2 -type d" for controlled exploration' }, { - "pattern": r"\brm\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)", - "reason": "Recursive rm commands (rm -r/-rf) can delete entire directory trees, which is destructive", - "alternative": 'Delete specific files individually or use "rm " to avoid accidental data loss', + 'pattern': r'\brm\s+(?:[^-]*\s+)?-[a-z]*[rR](?:[a-z]*\b|\s|$)', + 'reason': 'Recursive rm commands (rm -r/-rf) can delete entire directory trees, which is destructive', + 'alternative': 'Delete specific files individually or use "rm " to avoid accidental data loss' }, { - "pattern": r"\brm\s+--recursive\b", - "reason": "Recursive rm commands can delete entire directory trees, which is destructive", - "alternative": 'Delete specific files individually or use "rm " to avoid accidental data loss', + 'pattern': r'\brm\s+--recursive\b', + 'reason': 'Recursive rm commands can delete entire directory trees, which is destructive', + 'alternative': 'Delete specific files individually or use "rm " to avoid accidental data loss' }, { - "pattern": r"\bfind\b(?!.*-maxdepth)", - "reason": "Find command without -maxdepth can recursively search entire filesystems, causing excessive output", - "alternative": 'Use "find -name "*.ext" -maxdepth 2" to limit search depth and control output size', + 'pattern': r'\bfind\b(?!.*-maxdepth)', + 'reason': 'Find command without -maxdepth can recursively search entire filesystems, causing excessive output', + 'alternative': 'Use "find -name "*.ext" -maxdepth 2" to limit search depth and control output size' }, ] for check in dangerous_checks: - if re.search(check["pattern"], stripped_command, re.IGNORECASE): + if re.search(check['pattern'], stripped_command, re.IGNORECASE): return f"REASON: {check['reason']}\nALTERNATIVE: {check['alternative']}" return None @@ -470,4 +461,4 @@ def _is_safe_command(self, command: str) -> tuple[bool, Optional[str]]: """ explanation = self._get_dangerous_command_explanation(command) is_safe = explanation is None - return is_safe, explanation + return is_safe, explanation \ No newline at end of file From 2a04b8dfc16a2459053a2c383f5a109936373448 Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 12:21:15 +0530 Subject: [PATCH 05/12] fix unwanted formatting issues --- src/microbots/MicroBot.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/microbots/MicroBot.py b/src/microbots/MicroBot.py index 276daf9e..e6073f5d 100644 --- a/src/microbots/MicroBot.py +++ b/src/microbots/MicroBot.py @@ -116,6 +116,13 @@ def __init__( """Create a new MicroBot instance.""" self.folder_to_mount: Optional[Mount] = folder_to_mount + # TODO : Need to check on the purpose of variable `mounted` + # 1. If we allow user to mount multiple directories, + # we should able to get it as an argument and store them in self.mounted. + # This require changes in _create_environment to handle multiple mount directories or files. + # + # 2. We should let user to mount only one directory. In that case self.mounted may not be required. + # Just one self.folder_to_mount and necessary extra mounts at the derived class similar to LogAnalyticsBot. """A folder to mount into the bot's environment. The bot will be given access to this folder based on the specified permissions. This will be the main code folder where the bot will work. Additional folders can be mounted during the ``run()`` method. Refer to [Mount][microbots.extras.mount.Mount] for directory structure and permission details. Supports only ``MountType.MOUNT`` for now.""" self.mounted: list[Mount] = [] @@ -262,7 +269,6 @@ def run( return_value.error = f"Timeout of {timeout} seconds reached" return return_value - logger.info("%s Step-%d %s", "-" * 20, self.step_count, "-" * 20) if llm_response.thoughts: logger.info( From ef742d56a698e2b6a49d3f1e70fb9715bf9eb525 Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 12:22:28 +0530 Subject: [PATCH 06/12] remove comma added in timout_in_seconds parameter --- src/microbots/MicroBot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/microbots/MicroBot.py b/src/microbots/MicroBot.py index e6073f5d..25ae18ea 100644 --- a/src/microbots/MicroBot.py +++ b/src/microbots/MicroBot.py @@ -193,7 +193,7 @@ def run( task: str, additional_mounts: Optional[list[Mount]] = None, max_iterations: int = 20, - timeout_in_seconds: int = 200, + timeout_in_seconds: int = 200 ) -> BotRunResult: """Execute a task with the bot. From e5fef48d56324760e1c5fd25d1d5d0f40839992c Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 12:24:14 +0530 Subject: [PATCH 07/12] revert openai_api file changes --- src/microbots/llm/openai_api.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/microbots/llm/openai_api.py b/src/microbots/llm/openai_api.py index a2b1413d..a7979401 100644 --- a/src/microbots/llm/openai_api.py +++ b/src/microbots/llm/openai_api.py @@ -4,8 +4,8 @@ from dataclasses import asdict from dotenv import load_dotenv -from microbots.llm.llm import LLMAskResponse, LLMInterface from openai import AzureOpenAI, OpenAI +from microbots.llm.llm import LLMAskResponse, LLMInterface load_dotenv() @@ -51,10 +51,9 @@ def __init__( ) else: # Non-Azure users with a plain API key - self.ai_client = AzureOpenAI( - azure_endpoint=endpoint, - azure_ad_token_provider=token_provider, - api_version=api_version, + self.ai_client = OpenAI( + base_url=endpoint, + api_key=api_key, ) self.deployment_name = deployment_name self.system_prompt = system_prompt @@ -95,4 +94,4 @@ def clear_history(self): "content": self.system_prompt, } ] - return True + return True \ No newline at end of file From 5b5db20a6775dd62d6031921f8dc49602170b5aa Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 12:31:56 +0530 Subject: [PATCH 08/12] revert openai changes --- src/microbots/llm/openai_api.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/microbots/llm/openai_api.py b/src/microbots/llm/openai_api.py index a7979401..6c57ea9b 100644 --- a/src/microbots/llm/openai_api.py +++ b/src/microbots/llm/openai_api.py @@ -17,13 +17,8 @@ class OpenAIApi(LLMInterface): - def __init__( - self, - system_prompt, - deployment_name=deployment_name, - max_retries=3, - token_provider: Callable[[], str] | None = None, - ): + def __init__(self, system_prompt, deployment_name=deployment_name, max_retries=3, + token_provider: Callable[[], str] | None = None): self.token_provider = token_provider if not token_provider and not api_key: @@ -34,9 +29,7 @@ def __init__( if token_provider: if not callable(token_provider): - raise ValueError( - "token_provider must be a callable that returns a string token." - ) + raise ValueError("token_provider must be a callable that returns a string token.") try: token = token_provider() except Exception as e: @@ -64,7 +57,7 @@ def __init__( self.retries = 0 def ask(self, message) -> LLMAskResponse: - self.retries = 0 # reset retries for each ask. Handled in parent class. + self.retries = 0 # reset retries for each ask. Handled in parent class. self.messages.append({"role": "user", "content": message}) @@ -75,15 +68,11 @@ def ask(self, message) -> LLMAskResponse: input=self.messages, ) self.messages.append({"role": "assistant", "content": response.output_text}) - valid, askResponse = self._validate_llm_response( - response=response.output_text - ) + valid, askResponse = self._validate_llm_response(response=response.output_text) # Remove last assistant message and replace with structured response self.messages.pop() - self.messages.append( - {"role": "assistant", "content": json.dumps(asdict(askResponse))} - ) + self.messages.append({"role": "assistant", "content": json.dumps(asdict(askResponse))}) return askResponse From 2a6f4845e88354514f8aad46b3c93362ae8d4092 Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 15:41:55 +0530 Subject: [PATCH 09/12] fix mount class format issues --- src/microbots/extras/mount.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/microbots/extras/mount.py b/src/microbots/extras/mount.py index c38e9a56..df0d445b 100644 --- a/src/microbots/extras/mount.py +++ b/src/microbots/extras/mount.py @@ -3,7 +3,7 @@ from pathlib import Path from microbots.constants import PermissionLabels, PermissionMapping -from microbots.utils.path import PathInfo, ends_with_separator, get_path_info +from microbots.utils.path import PathInfo, get_path_info, ends_with_separator class MountType(StrEnum): @@ -43,7 +43,7 @@ class Mount: host_path: str """The absolute path on the host machine to be mounted or copied.""" sandbox_path: str - """The absolute path inside the Bot's sandbox environment where the host_path will be mounted or copied. If the host_path is a file and the sandbox_path ends with a path separator, the file will be placed inside the sandbox_path directory with the same base name.""" + """The absolute path inside the Bot's sandbox environment where the host_path will be mounted or copied. If the host_path is a file and the sandbox_path ends with a path separator ("/"), then the file will be placed inside the sandbox_path directory with the same base name.""" permission: PermissionLabels """The permission level for the mounted/copied folder. See [PermissionLabels][microbots.constants.PermissionLabels] for supported values.""" mount_type: MountType = MountType.MOUNT @@ -67,9 +67,7 @@ def __post_init__(self): f"sandbox_path must be an absolute path. Given: {self.sandbox_path}" ) - if not ends_with_separator(self.host_path) and ends_with_separator( - self.sandbox_path - ): + if not ends_with_separator(self.host_path) and ends_with_separator(self.sandbox_path): # If host_path is a file and sandbox_path ends with separator, # place the file inside the sandbox_path directory with same base name sandbox_path = str(sandbox_path / self.host_path_info.base_name) From 4a9b074e0fbfd137d6bd6636a971ea4626cf83da Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 15:55:44 +0530 Subject: [PATCH 10/12] add log analysis bot parameters Co-authored-by: Copilot --- src/microbots/bot/LogAnalysisBot.py | 51 ++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/microbots/bot/LogAnalysisBot.py b/src/microbots/bot/LogAnalysisBot.py index b2c682b1..1d3335ec 100644 --- a/src/microbots/bot/LogAnalysisBot.py +++ b/src/microbots/bot/LogAnalysisBot.py @@ -3,14 +3,39 @@ from typing import Optional from microbots.constants import DOCKER_WORKING_DIR, LOG_FILE_DIR, PermissionLabels +from microbots.extras.mount import Mount, MountType from microbots.MicroBot import BotType, MicroBot, system_prompt_common from microbots.tools.tool import ToolAbstract -from microbots.extras.mount import Mount, MountType logger = logging.getLogger(__name__) class LogAnalysisBot(MicroBot): + """A specialized bot for analyzing log files. + + LogAnalysisBot extends [MicroBot][microbots.MicroBot.MicroBot] to analyze log files + inside a sandboxed container and identify root causes by cross-referencing + with source code. The source code folder is mounted as read-only. + + Parameters + ---------- + model : str + The model to use, in the format ``/``. + See [ModelProvider][microbots.constants.ModelProvider] for supported providers. + folder_to_mount : str + The absolute path to the source code folder on the host machine. + This folder will be mounted as read-only inside the bot's sandbox + for cross-referencing with log entries. + environment : any, optional + The execution environment for the bot. If not provided, a default + ``LocalDockerEnvironment`` will be created. + additional_tools : list[ToolAbstract], optional + A list of additional tools to install in the bot's environment. + Defaults to None. + token_provider : any, optional + A token provider for authentication. Required for Azure OpenAI + with Azure AD auth. Defaults to None. + """ def __init__( self, @@ -26,7 +51,7 @@ def __init__( folder_mount_info = Mount( folder_to_mount, f"/{DOCKER_WORKING_DIR}/{os.path.basename(folder_to_mount)}", - PermissionLabels.READ_ONLY + PermissionLabels.READ_ONLY, ) system_prompt = f""" @@ -48,7 +73,25 @@ def __init__( token_provider=token_provider, ) - def run(self, file_name: str, max_iterations: int = 20, timeout_in_seconds: int = 300) -> any: + def run( + self, file_name: str, max_iterations: int = 20, timeout_in_seconds: int = 300 + ) -> any: + """Run the log analysis bot on a log file. + + Parameters + ---------- + file_name : str + The absolute path to the log file on the host machine to analyze. + max_iterations : int, optional + Maximum number of LLM interactions allowed. Defaults to 20. + timeout_in_seconds : int, optional + Maximum time in seconds before the run times out. Defaults to 300. + + Returns + ------- + BotRunResult + The result of the log analysis. + """ # Add the logic to copy the file from the user path to /var/log path in container file_mount_info = Mount( @@ -65,5 +108,5 @@ def run(self, file_name: str, max_iterations: int = 20, timeout_in_seconds: int task=file_name_prompt, additional_mounts=[file_mount_info], max_iterations=max_iterations, - timeout_in_seconds=timeout_in_seconds + timeout_in_seconds=timeout_in_seconds, ) From 792ecc702da9cc8972e131421350193764a4df87 Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 16:32:57 +0530 Subject: [PATCH 11/12] remove unwanted format changes in log analysis bot .py --- src/microbots/bot/LogAnalysisBot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/microbots/bot/LogAnalysisBot.py b/src/microbots/bot/LogAnalysisBot.py index 1d3335ec..e8b133cb 100644 --- a/src/microbots/bot/LogAnalysisBot.py +++ b/src/microbots/bot/LogAnalysisBot.py @@ -73,9 +73,7 @@ def __init__( token_provider=token_provider, ) - def run( - self, file_name: str, max_iterations: int = 20, timeout_in_seconds: int = 300 - ) -> any: + def run(self, file_name: str, max_iterations: int = 20, timeout_in_seconds: int = 300) -> any: """Run the log analysis bot on a log file. Parameters @@ -108,5 +106,5 @@ def run( task=file_name_prompt, additional_mounts=[file_mount_info], max_iterations=max_iterations, - timeout_in_seconds=timeout_in_seconds, + timeout_in_seconds=timeout_in_seconds ) From d460b8ac7585370d3d1c6b4256438eed9b2a01ea Mon Sep 17 00:00:00 2001 From: shivashanmugam Date: Thu, 23 Apr 2026 16:35:05 +0530 Subject: [PATCH 12/12] add missed todo --- src/microbots/MicroBot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/microbots/MicroBot.py b/src/microbots/MicroBot.py index 25ae18ea..907c0ea2 100644 --- a/src/microbots/MicroBot.py +++ b/src/microbots/MicroBot.py @@ -142,6 +142,10 @@ def __init__( self.additional_tools: list[ToolAbstract] = additional_tools or [] """A list of additional tools to install in the bot's environment. Defaults to an empty list.""" + # TODO: Replace iteration_count and max_iterations with cost management. + # Iteration count represents overall LLM interactions including interactions + + # done by sub agents. self.iteration_count: int = 0 """Number of LLM interactions so far, including sub-agent interactions.""" self.max_iterations: int = 0 @@ -467,4 +471,4 @@ def _is_safe_command(self, command: str) -> tuple[bool, Optional[str]]: """ explanation = self._get_dangerous_command_explanation(command) is_safe = explanation is None - return is_safe, explanation \ No newline at end of file + return is_safe, explanation