From 508e247c82388c0fd8e727a97ab3a7d9a082560a Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Sun, 19 Apr 2026 09:51:54 +0000 Subject: [PATCH] docs: remove feature showcase and update memory and Python SDK documentation for clarity and completeness --- docs/features-showcase.md | 22 ---- docs/memory.md | 2 - docs/python-sdk.md | 206 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 197 insertions(+), 33 deletions(-) delete mode 100644 docs/features-showcase.md diff --git a/docs/features-showcase.md b/docs/features-showcase.md deleted file mode 100644 index bb988b06..00000000 --- a/docs/features-showcase.md +++ /dev/null @@ -1,22 +0,0 @@ -# Feature Showcase - - - - - - - - - - - - - - - - - - - - -

📈 24/7 Real-Time Market Analysis

🚀 Full-Stack Software Engineer

📅 Smart Daily Routine Manager

📚 Personal Knowledge Assistant

Discovery • Insights • TrendsDevelop • Deploy • ScaleSchedule • Automate • OrganizeLearn • Memory • Reasoning
diff --git a/docs/memory.md b/docs/memory.md index 2fde1c68..763e0643 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -1,7 +1,5 @@ # Memory in nanobot -> **Note:** This design is currently an experiment in the latest source code version and is planned to officially ship in `v0.1.5`. - nanobot's memory is built on a simple belief: memory should feel alive, but it should not feel chaotic. Good memory is not a pile of notes. It is a quiet system of attention. It notices what is worth keeping, lets go of what no longer needs the spotlight, and turns lived experience into something calm, durable, and useful. diff --git a/docs/python-sdk.md b/docs/python-sdk.md index d035238d..5ee66a34 100644 --- a/docs/python-sdk.md +++ b/docs/python-sdk.md @@ -1,31 +1,219 @@ # Python SDK -Use nanobot as a library — no CLI, no gateway, just Python: +Use nanobot as a library — no CLI, no gateway, just Python. + +## Quick Start + +```python +import asyncio + +from nanobot import Nanobot + + +async def main() -> None: + bot = Nanobot.from_config() + result = await bot.run("What time is it in Tokyo?") + print(result.content) + + +asyncio.run(main()) +``` + +`Nanobot.from_config()` reuses your normal `~/.nanobot/config.json`, so the SDK follows the same provider, model, tools, and workspace defaults as the CLI unless you override them. + +## Common Patterns + +### Use a specific config or workspace ```python from nanobot import Nanobot -bot = Nanobot.from_config() -result = await bot.run("Summarize the README") -print(result.content) +bot = Nanobot.from_config( + config_path="~/.nanobot/config.json", + workspace="/my/project", +) ``` -Each call carries a `session_key` for conversation isolation — different keys get independent history: +### Isolate conversations with `session_key` + +Different session keys keep independent conversation history: ```python await bot.run("hi", session_key="user-alice") await bot.run("hi", session_key="task-42") ``` -Add lifecycle hooks to observe or customize the agent: +### Attach hooks for observability + +Hooks let you inspect tool calls, streaming, and iteration state without modifying nanobot internals: ```python from nanobot.agent import AgentHook, AgentHookContext + class AuditHook(AgentHook): - async def before_execute_tools(self, ctx: AgentHookContext) -> None: - for tc in ctx.tool_calls: + async def before_execute_tools(self, context: AgentHookContext) -> None: + for tc in context.tool_calls: print(f"[tool] {tc.name}") -result = await bot.run("Hello", hooks=[AuditHook()]) + +result = await bot.run("Review this change", hooks=[AuditHook()]) +``` + +## API Reference + +### `Nanobot.from_config(config_path=None, *, workspace=None)` + +Create a `Nanobot` instance 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 the workspace directory from config. | + +Raises `FileNotFoundError` if an explicit config path does not exist. + +### `await bot.run(message, *, session_key="sdk:default", hooks=None)` + +Run the agent once and return 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. | + +### `RunResult` + +| Field | Type | Description | +|-------|------|-------------| +| `content` | `str` | The agent's final text response. | +| `tools_used` | `list[str]` | Reserved for richer SDK introspection; may be empty in current versions. | +| `messages` | `list[dict]` | Reserved for richer SDK introspection; may be empty in current versions. | + +## Hooks + +Hooks let you observe or customize the agent loop. Subclass `AgentHook` and override the methods you need. + +### Hook lifecycle + +| Method | When | +|--------|------| +| `wants_streaming()` | Return `True` if you want token-by-token `on_stream()` callbacks | +| `before_iteration(context)` | Before each LLM call | +| `on_stream(context, delta)` | On each streamed token when streaming is enabled | +| `on_stream_end(context, *, resuming)` | When streaming finishes | +| `before_execute_tools(context)` | Before tool execution | +| `after_iteration(context)` | After each iteration | +| `finalize_content(context, content)` | Transform final output text | + +Useful fields on `AgentHookContext` include: + +- `iteration` +- `messages` +- `response` +- `usage` +- `tool_calls` +- `tool_results` +- `tool_events` +- `final_content` +- `stop_reason` +- `error` + +### Example: audit tool calls + +```python +from nanobot.agent import AgentHook, AgentHookContext + + +class AuditHook(AgentHook): + def __init__(self) -> None: + super().__init__() + self.calls: list[str] = [] + + async def before_execute_tools(self, context: AgentHookContext) -> None: + for tc in context.tool_calls: + self.calls.append(tc.name) + print(f"[audit] {tc.name}({tc.arguments})") +``` + +```python +hook = AuditHook() +result = await bot.run("List files in /tmp", hooks=[hook]) +print(result.content) +print(f"Tools observed: {hook.calls}") +``` + +### Example: receive streaming tokens + +```python +from nanobot.agent import AgentHook, AgentHookContext + + +class StreamingHook(AgentHook): + def wants_streaming(self) -> bool: + return True + + async def on_stream(self, context: AgentHookContext, delta: str) -> None: + print(delta, end="", flush=True) + + async def on_stream_end(self, context: AgentHookContext, *, resuming: bool) -> None: + print() +``` + +### Compose multiple hooks + +Pass multiple hooks when you want to combine behaviors: + +```python +result = await bot.run("hi", hooks=[AuditHook(), MetricsHook()]) +``` + +Async hook methods are fan-out with error isolation. `finalize_content` is a pipeline: each hook receives the previous hook's output. + +### Example: post-process final content + +```python +from nanobot.agent import AgentHook + + +class Censor(AgentHook): + def finalize_content(self, context, content): + return content.replace("secret", "***") if content else content +``` + +## Full Example + +```python +import asyncio +import time + +from nanobot import Nanobot +from nanobot.agent import AgentHook, AgentHookContext + + +class TimingHook(AgentHook): + def __init__(self) -> None: + super().__init__() + self._started_at = 0.0 + + async def before_iteration(self, context: AgentHookContext) -> None: + self._started_at = time.perf_counter() + + async def after_iteration(self, context: AgentHookContext) -> None: + elapsed_ms = (time.perf_counter() - self._started_at) * 1000 + print(f"[timing] iteration {context.iteration} took {elapsed_ms:.1f}ms") + + +async def main() -> None: + bot = Nanobot.from_config(workspace="/my/project") + result = await bot.run( + "Explain the main function", + session_key="sdk:demo", + hooks=[TimingHook()], + ) + print(result.content) + + +asyncio.run(main()) ```