# Python SDK Use nanobot programmatically — load config, run the agent, get results. ## Quick Start ```python import asyncio from nanobot import Nanobot async def main(): bot = Nanobot.from_config() result = await bot.run("What time is it in Tokyo?") print(result.content) asyncio.run(main()) ``` ## API ### `Nanobot.from_config(config_path?, *, workspace?)` Create a `Nanobot` from a config file. | Param | Type | Default | Description | |-------|------|---------|-------------| | `config_path` | `str \| Path \| None` | `None` | Path to `config.json`. Defaults to `~/.nanobot/config.json`. | | `workspace` | `str \| Path \| None` | `None` | Override workspace directory from config. | Raises `FileNotFoundError` if an explicit path doesn't exist. ### `await bot.run(message, *, session_key?, hooks?)` Run the agent once. Returns a `RunResult`. | Param | Type | Default | Description | |-------|------|---------|-------------| | `message` | `str` | *(required)* | The user message to process. | | `session_key` | `str` | `"sdk:default"` | Session identifier for conversation isolation. Different keys get independent history. | | `hooks` | `list[AgentHook] \| None` | `None` | Lifecycle hooks for this run only. | ```python # Isolated sessions — each user gets independent conversation history await bot.run("hi", session_key="user-alice") await bot.run("hi", session_key="user-bob") ``` ### `RunResult` | Field | Type | Description | |-------|------|-------------| | `content` | `str` | The agent's final text response. | | `tools_used` | `list[str]` | Tool names invoked during the run. | | `messages` | `list[dict]` | Raw message history (for debugging). | ## Hooks Hooks let you observe or modify the agent loop without touching internals. Subclass `AgentHook` and override any method: | Method | When | |--------|------| | `before_iteration(ctx)` | Before each LLM call | | `on_stream(ctx, delta)` | On each streamed token | | `on_stream_end(ctx)` | When streaming finishes | | `before_execute_tools(ctx)` | Before tool execution (inspect `ctx.tool_calls`) | | `after_iteration(ctx, response)` | After each LLM response | | `finalize_content(ctx, content)` | Transform final output text | ### Example: Audit Hook ```python from nanobot.agent import AgentHook, AgentHookContext class AuditHook(AgentHook): def __init__(self): self.calls = [] async def before_execute_tools(self, ctx: AgentHookContext) -> None: for tc in ctx.tool_calls: self.calls.append(tc.name) print(f"[audit] {tc.name}({tc.arguments})") hook = AuditHook() result = await bot.run("List files in /tmp", hooks=[hook]) print(f"Tools used: {hook.calls}") ``` ### Composing Hooks Pass multiple hooks — they run in order, errors in one don't block others: ```python result = await bot.run("hi", hooks=[AuditHook(), MetricsHook()]) ``` Under the hood this uses `CompositeHook` for fan-out with error isolation. ### `finalize_content` Pipeline Unlike the async methods (fan-out), `finalize_content` is a pipeline — each hook's output feeds the next: ```python class Censor(AgentHook): def finalize_content(self, ctx, content): return content.replace("secret", "***") if content else content ``` ## Full Example ```python import asyncio from nanobot import Nanobot from nanobot.agent import AgentHook, AgentHookContext class TimingHook(AgentHook): async def before_iteration(self, ctx: AgentHookContext) -> None: import time ctx.metadata["_t0"] = time.time() async def after_iteration(self, ctx, response) -> None: import time elapsed = time.time() - ctx.metadata.get("_t0", 0) print(f"[timing] iteration took {elapsed:.2f}s") async def main(): bot = Nanobot.from_config(workspace="/my/project") result = await bot.run( "Explain the main function", hooks=[TimingHook()], ) print(result.content) asyncio.run(main()) ```