chore: update nanobot to 0.1.4.post6
This commit is contained in:
+175
-77
@@ -15,14 +15,14 @@ if str(PROJECT_ROOT / "nanobot") not in sys.path:
|
||||
sys.path.append(str(PROJECT_ROOT / "nanobot"))
|
||||
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.bus.events import OutboundMessage
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.config.loader import load_config
|
||||
from nanobot.config.paths import get_cron_dir
|
||||
from nanobot.cron.service import CronService
|
||||
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||
from nanobot.providers.openai_codex_provider import OpenAICodexProvider
|
||||
from nanobot.providers.azure_openai_provider import AzureOpenAIProvider
|
||||
from nanobot.providers.litellm_provider import LiteLLMProvider
|
||||
from nanobot.providers.custom_provider import CustomProvider
|
||||
from nanobot.providers.base import GenerationSettings
|
||||
from nanobot.providers.registry import find_by_name
|
||||
from nanobot.session.manager import SessionManager
|
||||
from nanobot.config.schema import Config
|
||||
@@ -32,10 +32,9 @@ from nanobot.config.schema import Config
|
||||
# or just import here if we are confident.
|
||||
# Given the structure, importing here should be fine as long as skills.py doesn't import nanobot.py.
|
||||
from app.api.skills import load_skills
|
||||
from app.services.llm_cache import get_llm_configs
|
||||
from app.services.llm_cache import get_llm_configs, get_active_llm_config
|
||||
|
||||
from app.core.data_root import get_workspace_root
|
||||
from app.core.streaming_provider import StreamingLiteLLMProvider
|
||||
|
||||
class NanobotIntegration:
|
||||
def __init__(self):
|
||||
@@ -47,6 +46,75 @@ class NanobotIntegration:
|
||||
self._model_agent_cache: Dict[tuple[str | None, int | None], AgentLoop] = {}
|
||||
self._model_agent_lock = asyncio.Lock()
|
||||
|
||||
@staticmethod
|
||||
def _normalize_config_value(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _normalize_model_id(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def _extract_response_text(response: Any) -> str:
|
||||
if response is None:
|
||||
return ""
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
if isinstance(response, OutboundMessage):
|
||||
return response.content or ""
|
||||
if isinstance(response, dict):
|
||||
content = response.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
return str(content or "")
|
||||
content = getattr(response, "content", None)
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
return str(response)
|
||||
|
||||
def _need_custom_agent_for_target(self, target_config: Dict[str, Any]) -> bool:
|
||||
if not self.agent:
|
||||
return False
|
||||
|
||||
provider = self.agent.provider
|
||||
target_model = self._normalize_config_value(target_config.get("model"))
|
||||
current_model = self._normalize_config_value(
|
||||
getattr(self.agent, "model", None) or getattr(provider, "default_model", None)
|
||||
)
|
||||
if target_model != current_model:
|
||||
return True
|
||||
|
||||
target_provider = self._normalize_config_value(target_config.get("provider"))
|
||||
current_provider = self._normalize_config_value(getattr(provider, "_provider_name_override", None))
|
||||
if not current_provider:
|
||||
current_provider = self._normalize_config_value(getattr(getattr(provider, "_spec", None), "name", None))
|
||||
if not current_provider and current_model and self.config:
|
||||
current_provider = self._normalize_config_value(self.config.get_provider_name(current_model))
|
||||
if target_provider != current_provider:
|
||||
return True
|
||||
|
||||
target_api_base = self._normalize_config_value(target_config.get("api_base"))
|
||||
current_api_base = self._normalize_config_value(getattr(provider, "api_base", None))
|
||||
if target_api_base != current_api_base:
|
||||
return True
|
||||
|
||||
target_api_key = self._normalize_config_value(target_config.get("api_key"))
|
||||
current_api_key = self._normalize_config_value(getattr(provider, "api_key", None))
|
||||
if target_api_key != current_api_key:
|
||||
return True
|
||||
|
||||
target_headers = target_config.get("extra_headers") or {}
|
||||
current_headers = getattr(provider, "extra_headers", None) or {}
|
||||
return target_headers != current_headers
|
||||
|
||||
def initialize(self):
|
||||
workspace_path = get_workspace_root()
|
||||
workspace_path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -74,12 +142,9 @@ class NanobotIntegration:
|
||||
provider=provider,
|
||||
workspace=self.config.workspace_path,
|
||||
model=self.config.agents.defaults.model,
|
||||
temperature=self.config.agents.defaults.temperature,
|
||||
max_tokens=self.config.agents.defaults.max_tokens,
|
||||
max_iterations=self.config.agents.defaults.max_tool_iterations,
|
||||
memory_window=self.config.agents.defaults.memory_window,
|
||||
reasoning_effort=self.config.agents.defaults.reasoning_effort,
|
||||
brave_api_key=self.config.tools.web.search.api_key or None,
|
||||
context_window_tokens=self.config.agents.defaults.context_window_tokens,
|
||||
web_search_config=self.config.tools.web.search,
|
||||
web_proxy=self.config.tools.web.proxy or None,
|
||||
exec_config=self.config.tools.exec,
|
||||
cron_service=self.cron,
|
||||
@@ -87,6 +152,7 @@ class NanobotIntegration:
|
||||
session_manager=session_manager,
|
||||
mcp_servers=self.config.tools.mcp_servers,
|
||||
channels_config=self.config.channels,
|
||||
timezone=self.config.agents.defaults.timezone,
|
||||
)
|
||||
|
||||
self._register_custom_tools(self.agent)
|
||||
@@ -105,68 +171,94 @@ class NanobotIntegration:
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source_skill_file, target_dir / "SKILL.md")
|
||||
|
||||
def _register_custom_tools(self, agent: AgentLoop):
|
||||
def _register_custom_tools(self, agent: AgentLoop, project_id: int | None = None):
|
||||
from app.tools.nl2sql import NL2SQLTool
|
||||
from app.tools.visualization import VisualizationTool
|
||||
from app.tools.get_schema import GetDatabaseSchemaTool
|
||||
from app.tools.subagent import ListSubagentsTool, InvokeSubagentTool
|
||||
agent.tools.register(NL2SQLTool())
|
||||
agent.tools.register(VisualizationTool())
|
||||
agent.tools.register(GetDatabaseSchemaTool())
|
||||
if project_id is not None:
|
||||
agent.tools.register(ListSubagentsTool(project_id=project_id))
|
||||
agent.tools.register(InvokeSubagentTool(project_id=project_id))
|
||||
|
||||
def _build_provider(
|
||||
self,
|
||||
model: str,
|
||||
provider_name: str | None,
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
extra_headers: dict[str, Any] | None = None,
|
||||
):
|
||||
spec = find_by_name(provider_name) if provider_name else None
|
||||
backend = spec.backend if spec else "openai_compat"
|
||||
|
||||
if backend == "openai_codex" or model.startswith("openai-codex/"):
|
||||
return OpenAICodexProvider(default_model=model)
|
||||
|
||||
if backend == "azure_openai":
|
||||
if not api_key or not api_base:
|
||||
raise ValueError("Azure OpenAI requires api_key and api_base.")
|
||||
return AzureOpenAIProvider(
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
default_model=model,
|
||||
)
|
||||
|
||||
if backend == "anthropic":
|
||||
from nanobot.providers.anthropic_provider import AnthropicProvider
|
||||
return AnthropicProvider(
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
default_model=model,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
return OpenAICompatProvider(
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
default_model=model,
|
||||
extra_headers=extra_headers,
|
||||
spec=spec,
|
||||
)
|
||||
|
||||
def _make_provider(self, config: Config):
|
||||
# Logic adapted from nanobot/cli/commands.py
|
||||
model = config.agents.defaults.model
|
||||
provider_name = config.get_provider_name(model)
|
||||
p = config.get_provider(model)
|
||||
|
||||
# Check if model is using an ID from our database configuration
|
||||
# This requires accessing the database or a cache of LLM configs
|
||||
# Since we are inside NanobotIntegration, we can try to load from the JSON file directly for simplicity
|
||||
# or rely on the caller to have injected the right config if they used environment variables.
|
||||
# But here we need to support dynamic loading based on the `model` string if it matches a stored config ID.
|
||||
|
||||
# However, typically the `model` passed here comes from `config.agents.defaults.model`.
|
||||
# If we want to support dynamic switching per request, we should look at `agent.process_direct` arguments.
|
||||
# The `AgentLoop` initializes with a provider, but `LiteLLMProvider` might be able to handle dynamic models if we pass them.
|
||||
# BUT `LiteLLMProvider` is initialized with a specific `default_model`.
|
||||
|
||||
# To support per-request model changes, we need to ensure the `provider` object or the `agent` can accept a model override.
|
||||
# `AgentLoop` methods like `process_direct` don't typically take a `model` argument to override the provider's default.
|
||||
# We might need to reinstantiate the provider or use a "DynamicProvider" that delegates based on context.
|
||||
|
||||
# For now, let's assume standard initialization.
|
||||
# If the user provides a `model_id` in `process_message`, we will handle it there by creating a temporary provider/agent or updating the current one.
|
||||
|
||||
if provider_name == "openai_codex" or model.startswith("openai-codex/"):
|
||||
return OpenAICodexProvider(default_model=model)
|
||||
|
||||
if provider_name == "custom":
|
||||
return CustomProvider(
|
||||
api_key=p.api_key if p else "no-key",
|
||||
api_base=config.get_api_base(model) or "http://localhost:8000/v1",
|
||||
default_model=model,
|
||||
)
|
||||
|
||||
if provider_name == "azure_openai":
|
||||
if not p or not p.api_key or not p.api_base:
|
||||
raise ValueError("Azure OpenAI requires api_key and api_base.")
|
||||
|
||||
return AzureOpenAIProvider(
|
||||
api_key=p.api_key,
|
||||
api_base=p.api_base,
|
||||
default_model=model,
|
||||
)
|
||||
|
||||
spec = find_by_name(provider_name)
|
||||
# Skip API key check for now to allow initialization without full config
|
||||
|
||||
return StreamingLiteLLMProvider(
|
||||
provider = self._build_provider(
|
||||
model=model,
|
||||
provider_name=provider_name,
|
||||
api_key=p.api_key if p else None,
|
||||
api_base=config.get_api_base(model),
|
||||
default_model=model,
|
||||
extra_headers=p.extra_headers if p else None,
|
||||
provider_name=provider_name,
|
||||
)
|
||||
provider.generation = GenerationSettings(
|
||||
temperature=config.agents.defaults.temperature,
|
||||
max_tokens=config.agents.defaults.max_tokens,
|
||||
reasoning_effort=config.agents.defaults.reasoning_effort,
|
||||
)
|
||||
return provider
|
||||
|
||||
def _make_provider_from_target(self, target_config: Dict[str, Any]):
|
||||
model = self._normalize_config_value(target_config.get("model")) or self.config.agents.defaults.model
|
||||
provider_name = self._normalize_config_value(target_config.get("provider"))
|
||||
if not provider_name and model and self.config:
|
||||
provider_name = self._normalize_config_value(self.config.get_provider_name(model))
|
||||
provider = self._build_provider(
|
||||
model=model,
|
||||
provider_name=provider_name,
|
||||
api_key=self._normalize_config_value(target_config.get("api_key")),
|
||||
api_base=self._normalize_config_value(target_config.get("api_base")),
|
||||
extra_headers=target_config.get("extra_headers"),
|
||||
)
|
||||
provider.generation = GenerationSettings(
|
||||
temperature=self.config.agents.defaults.temperature,
|
||||
max_tokens=self.config.agents.defaults.max_tokens,
|
||||
reasoning_effort=self.config.agents.defaults.reasoning_effort,
|
||||
)
|
||||
return provider
|
||||
|
||||
async def start(self):
|
||||
if self._started:
|
||||
@@ -195,12 +287,9 @@ class NanobotIntegration:
|
||||
provider=provider,
|
||||
workspace=self.config.workspace_path,
|
||||
model=provider.default_model,
|
||||
temperature=self.config.agents.defaults.temperature,
|
||||
max_tokens=self.config.agents.defaults.max_tokens,
|
||||
max_iterations=self.config.agents.defaults.max_tool_iterations,
|
||||
memory_window=self.config.agents.defaults.memory_window,
|
||||
reasoning_effort=self.config.agents.defaults.reasoning_effort,
|
||||
brave_api_key=self.config.tools.web.search.api_key or None,
|
||||
context_window_tokens=self.config.agents.defaults.context_window_tokens,
|
||||
web_search_config=self.config.tools.web.search,
|
||||
web_proxy=self.config.tools.web.proxy or None,
|
||||
exec_config=self.config.tools.exec,
|
||||
cron_service=self.cron,
|
||||
@@ -208,23 +297,19 @@ class NanobotIntegration:
|
||||
session_manager=self.agent.sessions if self.agent else None,
|
||||
mcp_servers=mcp_servers if mcp_servers is not None else self.config.tools.mcp_servers,
|
||||
channels_config=self.config.channels,
|
||||
timezone=self.config.agents.defaults.timezone,
|
||||
)
|
||||
|
||||
async def _get_or_create_model_agent(self, model_id: str | None, target_config: Dict[str, Any] | None, project_id: int | None = None) -> AgentLoop:
|
||||
cache_key = (model_id, project_id)
|
||||
normalized_model_id = self._normalize_model_id(model_id)
|
||||
cache_key = (normalized_model_id, project_id)
|
||||
async with self._model_agent_lock:
|
||||
cached = self._model_agent_cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
if target_config:
|
||||
provider = StreamingLiteLLMProvider(
|
||||
api_key=target_config.get("api_key"),
|
||||
api_base=target_config.get("api_base"),
|
||||
default_model=target_config.get("model"),
|
||||
extra_headers=target_config.get("extra_headers"),
|
||||
provider_name=target_config.get("provider"),
|
||||
)
|
||||
provider = self._make_provider_from_target(target_config)
|
||||
else:
|
||||
provider = self._make_provider(self.config)
|
||||
|
||||
@@ -245,7 +330,7 @@ class NanobotIntegration:
|
||||
mcp_servers_dict[s["name"]] = cfg
|
||||
|
||||
agent = self._build_agent_for_provider(provider, mcp_servers=mcp_servers_dict)
|
||||
self._register_custom_tools(agent)
|
||||
self._register_custom_tools(agent, project_id=project_id)
|
||||
self._model_agent_cache[cache_key] = agent
|
||||
return agent
|
||||
|
||||
@@ -257,6 +342,7 @@ class NanobotIntegration:
|
||||
model_id: str | None = None,
|
||||
project_id: int | None = None,
|
||||
on_progress: Callable[[str], Awaitable[None]] | None = None,
|
||||
on_stream: Callable[[str], Awaitable[None]] | None = None,
|
||||
):
|
||||
if not self.agent:
|
||||
self.initialize()
|
||||
@@ -273,17 +359,28 @@ class NanobotIntegration:
|
||||
need_custom_agent = False
|
||||
target_config = None
|
||||
|
||||
if model_id:
|
||||
selected_model_id = self._normalize_model_id(model_id)
|
||||
if selected_model_id:
|
||||
llm_configs = get_llm_configs()
|
||||
target_config = next((item for item in llm_configs if item.get("id") == model_id), None)
|
||||
if target_config and target_config.get("model") != self.agent.model:
|
||||
need_custom_agent = True
|
||||
target_config = next(
|
||||
(item for item in llm_configs if self._normalize_model_id(item.get("id")) == selected_model_id),
|
||||
None,
|
||||
)
|
||||
|
||||
if target_config is None:
|
||||
active_config = get_active_llm_config()
|
||||
if active_config and active_config.get("id"):
|
||||
selected_model_id = self._normalize_model_id(active_config.get("id"))
|
||||
target_config = active_config
|
||||
|
||||
if target_config and self._need_custom_agent_for_target(target_config):
|
||||
need_custom_agent = True
|
||||
|
||||
if project_id is not None:
|
||||
need_custom_agent = True
|
||||
|
||||
if need_custom_agent:
|
||||
agent_to_use = await self._get_or_create_model_agent(model_id, target_config, project_id)
|
||||
agent_to_use = await self._get_or_create_model_agent(selected_model_id, target_config, project_id)
|
||||
|
||||
full_message = message
|
||||
# We no longer inject the full skill content into the user's message here,
|
||||
@@ -303,8 +400,9 @@ class NanobotIntegration:
|
||||
channel="api",
|
||||
chat_id=session_id,
|
||||
on_progress=on_progress,
|
||||
on_stream=on_stream,
|
||||
)
|
||||
return response
|
||||
return self._extract_response_text(response)
|
||||
|
||||
def _normalize_session_messages(self, messages: List[Any]) -> List[dict[str, Any]]:
|
||||
normalized: List[dict[str, Any]] = []
|
||||
|
||||
Reference in New Issue
Block a user