返回 JoyAI-Echo
python-sdk.md
1 # Python SDK
2
3 Use nanobot as a library — no CLI, no gateway, just Python.
4
5 ## Quick Start
6
7 ```python
8 import asyncio
9
10 from nanobot import Nanobot
11
12
13 async def main() -> None:
14 bot = Nanobot.from_config()
15 result = await bot.run("What time is it in Tokyo?")
16 print(result.content)
17
18
19 asyncio.run(main())
20 ```
21
22 `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.
23
24 ## Common Patterns
25
26 ### Use a specific config or workspace
27
28 ```python
29 from nanobot import Nanobot
30
31 bot = Nanobot.from_config(
32 config_path="~/.nanobot/config.json",
33 workspace="/my/project",
34 )
35 ```
36
37 ### Isolate conversations with `session_key`
38
39 Different session keys keep independent conversation history:
40
41 ```python
42 await bot.run("hi", session_key="user-alice")
43 await bot.run("hi", session_key="task-42")
44 ```
45
46 ### Attach hooks for observability
47
48 Hooks let you inspect tool calls, streaming, and iteration state without modifying nanobot internals:
49
50 ```python
51 from nanobot.agent import AgentHook, AgentHookContext
52
53
54 class AuditHook(AgentHook):
55 async def before_execute_tools(self, context: AgentHookContext) -> None:
56 for tc in context.tool_calls:
57 print(f"[tool] {tc.name}")
58
59
60 result = await bot.run("Review this change", hooks=[AuditHook()])
61 ```
62
63 ## API Reference
64
65 ### `Nanobot.from_config(config_path=None, *, workspace=None)`
66
67 Create a `Nanobot` instance from a config file.
68
69 | Param | Type | Default | Description |
70 |-------|------|---------|-------------|
71 | `config_path` | `str \| Path \| None` | `None` | Path to `config.json`. Defaults to `~/.nanobot/config.json`. |
72 | `workspace` | `str \| Path \| None` | `None` | Override the workspace directory from config. |
73
74 Raises `FileNotFoundError` if an explicit config path does not exist.
75
76 ### `await bot.run(message, *, session_key="sdk:default", hooks=None)`
77
78 Run the agent once and return a `RunResult`.
79
80 | Param | Type | Default | Description |
81 |-------|------|---------|-------------|
82 | `message` | `str` | *(required)* | The user message to process. |
83 | `session_key` | `str` | `"sdk:default"` | Session identifier for conversation isolation. Different keys get independent history. |
84 | `hooks` | `list[AgentHook] \| None` | `None` | Lifecycle hooks for this run only. |
85
86 ### `RunResult`
87
88 | Field | Type | Description |
89 |-------|------|-------------|
90 | `content` | `str` | The agent's final text response. |
91 | `tools_used` | `list[str]` | Reserved for richer SDK introspection; may be empty in current versions. |
92 | `messages` | `list[dict]` | Reserved for richer SDK introspection; may be empty in current versions. |
93
94 ## Hooks
95
96 Hooks let you observe or customize the agent loop. Subclass `AgentHook` and override the methods you need.
97
98 ### Hook lifecycle
99
100 | Method | When |
101 |--------|------|
102 | `wants_streaming()` | Return `True` if you want token-by-token `on_stream()` callbacks |
103 | `before_iteration(context)` | Before each LLM call |
104 | `on_stream(context, delta)` | On each streamed token when streaming is enabled |
105 | `on_stream_end(context, *, resuming)` | When streaming finishes |
106 | `before_execute_tools(context)` | Before tool execution |
107 | `after_iteration(context)` | After each iteration |
108 | `finalize_content(context, content)` | Transform final output text |
109
110 Useful fields on `AgentHookContext` include:
111
112 - `iteration`
113 - `messages`
114 - `response`
115 - `usage`
116 - `tool_calls`
117 - `tool_results`
118 - `tool_events`
119 - `final_content`
120 - `stop_reason`
121 - `error`
122
123 ### Example: audit tool calls
124
125 ```python
126 from nanobot.agent import AgentHook, AgentHookContext
127
128
129 class AuditHook(AgentHook):
130 def __init__(self) -> None:
131 super().__init__()
132 self.calls: list[str] = []
133
134 async def before_execute_tools(self, context: AgentHookContext) -> None:
135 for tc in context.tool_calls:
136 self.calls.append(tc.name)
137 print(f"[audit] {tc.name}({tc.arguments})")
138 ```
139
140 ```python
141 hook = AuditHook()
142 result = await bot.run("List files in /tmp", hooks=[hook])
143 print(result.content)
144 print(f"Tools observed: {hook.calls}")
145 ```
146
147 ### Example: receive streaming tokens
148
149 ```python
150 from nanobot.agent import AgentHook, AgentHookContext
151
152
153 class StreamingHook(AgentHook):
154 def wants_streaming(self) -> bool:
155 return True
156
157 async def on_stream(self, context: AgentHookContext, delta: str) -> None:
158 print(delta, end="", flush=True)
159
160 async def on_stream_end(self, context: AgentHookContext, *, resuming: bool) -> None:
161 print()
162 ```
163
164 ### Compose multiple hooks
165
166 Pass multiple hooks when you want to combine behaviors:
167
168 ```python
169 result = await bot.run("hi", hooks=[AuditHook(), MetricsHook()])
170 ```
171
172 Async hook methods are fan-out with error isolation. `finalize_content` is a pipeline: each hook receives the previous hook's output.
173
174 ### Example: post-process final content
175
176 ```python
177 from nanobot.agent import AgentHook
178
179
180 class Censor(AgentHook):
181 def finalize_content(self, context, content):
182 return content.replace("secret", "***") if content else content
183 ```
184
185 ## Full Example
186
187 ```python
188 import asyncio
189 import time
190
191 from nanobot import Nanobot
192 from nanobot.agent import AgentHook, AgentHookContext
193
194
195 class TimingHook(AgentHook):
196 def __init__(self) -> None:
197 super().__init__()
198 self._started_at = 0.0
199
200 async def before_iteration(self, context: AgentHookContext) -> None:
201 self._started_at = time.perf_counter()
202
203 async def after_iteration(self, context: AgentHookContext) -> None:
204 elapsed_ms = (time.perf_counter() - self._started_at) * 1000
205 print(f"[timing] iteration {context.iteration} took {elapsed_ms:.1f}ms")
206
207
208 async def main() -> None:
209 bot = Nanobot.from_config(workspace="/my/project")
210 result = await bot.run(
211 "Explain the main function",
212 session_key="sdk:demo",
213 hooks=[TimingHook()],
214 )
215 print(result.content)
216
217
218 asyncio.run(main())
219 ```
220
220 lines MARKDOWN