2026-03-30 18:46:11 +00:00
|
|
|
"""High-level programmatic interface to nanobot."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
2026-05-04 23:26:20 +08:00
|
|
|
from nanobot.agent.hook import AgentHook, SDKCaptureHook
|
2026-03-30 18:46:11 +00:00
|
|
|
from nanobot.agent.loop import AgentLoop
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class RunResult:
|
|
|
|
|
"""Result of a single agent run."""
|
|
|
|
|
|
|
|
|
|
content: str
|
|
|
|
|
tools_used: list[str]
|
|
|
|
|
messages: list[dict[str, Any]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Nanobot:
|
|
|
|
|
"""Programmatic facade for running the nanobot agent.
|
|
|
|
|
|
|
|
|
|
Usage::
|
|
|
|
|
|
|
|
|
|
bot = Nanobot.from_config()
|
|
|
|
|
result = await bot.run("Summarize this repo", hooks=[MyHook()])
|
|
|
|
|
print(result.content)
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, loop: AgentLoop) -> None:
|
|
|
|
|
self._loop = loop
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_config(
|
|
|
|
|
cls,
|
|
|
|
|
config_path: str | Path | None = None,
|
|
|
|
|
*,
|
|
|
|
|
workspace: str | Path | None = None,
|
|
|
|
|
) -> Nanobot:
|
|
|
|
|
"""Create a Nanobot instance from a config file.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config_path: Path to ``config.json``. Defaults to
|
|
|
|
|
``~/.nanobot/config.json``.
|
|
|
|
|
workspace: Override the workspace directory from config.
|
|
|
|
|
"""
|
2026-04-05 23:55:50 +02:00
|
|
|
from nanobot.config.loader import load_config, resolve_config_env_vars
|
2026-03-30 18:46:11 +00:00
|
|
|
from nanobot.config.schema import Config
|
|
|
|
|
|
|
|
|
|
resolved: Path | None = None
|
|
|
|
|
if config_path is not None:
|
|
|
|
|
resolved = Path(config_path).expanduser().resolve()
|
|
|
|
|
if not resolved.exists():
|
|
|
|
|
raise FileNotFoundError(f"Config not found: {resolved}")
|
|
|
|
|
|
2026-04-05 23:55:50 +02:00
|
|
|
config: Config = resolve_config_env_vars(load_config(resolved))
|
2026-03-30 18:46:11 +00:00
|
|
|
if workspace is not None:
|
|
|
|
|
config.agents.defaults.workspace = str(
|
|
|
|
|
Path(workspace).expanduser().resolve()
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-09 14:24:18 +08:00
|
|
|
loop = AgentLoop.from_config(
|
|
|
|
|
config,
|
2026-05-08 09:40:15 +00:00
|
|
|
image_generation_provider_configs={
|
|
|
|
|
"openrouter": config.providers.openrouter,
|
|
|
|
|
"aihubmix": config.providers.aihubmix,
|
|
|
|
|
},
|
2026-03-30 18:46:11 +00:00
|
|
|
)
|
|
|
|
|
return cls(loop)
|
|
|
|
|
|
|
|
|
|
async def run(
|
|
|
|
|
self,
|
|
|
|
|
message: str,
|
|
|
|
|
*,
|
|
|
|
|
session_key: str = "sdk:default",
|
|
|
|
|
hooks: list[AgentHook] | None = None,
|
|
|
|
|
) -> RunResult:
|
|
|
|
|
"""Run the agent once and return the result.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
message: The user message to process.
|
|
|
|
|
session_key: Session identifier for conversation isolation.
|
|
|
|
|
Different keys get independent history.
|
|
|
|
|
hooks: Optional lifecycle hooks for this run.
|
|
|
|
|
"""
|
2026-05-04 23:26:20 +08:00
|
|
|
capture = SDKCaptureHook()
|
2026-03-30 18:46:11 +00:00
|
|
|
prev = self._loop._extra_hooks
|
2026-04-17 09:51:59 -04:00
|
|
|
base_hooks = list(hooks) if hooks is not None else list(prev or [])
|
|
|
|
|
self._loop._extra_hooks = [capture, *base_hooks]
|
2026-03-30 18:46:11 +00:00
|
|
|
try:
|
|
|
|
|
response = await self._loop.process_direct(
|
|
|
|
|
message, session_key=session_key,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
self._loop._extra_hooks = prev
|
|
|
|
|
|
|
|
|
|
content = (response.content if response else None) or ""
|
2026-04-17 09:51:59 -04:00
|
|
|
return RunResult(
|
|
|
|
|
content=content,
|
|
|
|
|
tools_used=capture.tools_used,
|
|
|
|
|
messages=capture.messages,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|