150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
|
|
import asyncio
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import List
|
||
|
|
|
||
|
|
# Add project root to sys.path to allow importing nanobot
|
||
|
|
# Assuming backend/app/core/nanobot.py -> backend/app/core -> backend/app -> backend -> root
|
||
|
|
# This path calculation seems correct for backend/app/core/nanobot.py relative to backend/
|
||
|
|
# BUT nanobot package is in ../nanobot relative to backend/
|
||
|
|
# So we need to go up one more level to reach the parent of backend/
|
||
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||
|
|
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.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_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.registry import find_by_name
|
||
|
|
from nanobot.session.manager import SessionManager
|
||
|
|
from nanobot.config.schema import Config
|
||
|
|
|
||
|
|
# Import skills loader
|
||
|
|
# We use a lazy import inside the method to avoid potential circular dependencies if any arise,
|
||
|
|
# 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
|
||
|
|
|
||
|
|
class NanobotIntegration:
|
||
|
|
def __init__(self):
|
||
|
|
self.agent: AgentLoop | None = None
|
||
|
|
self.bus: MessageBus | None = None
|
||
|
|
self.cron: CronService | None = None
|
||
|
|
self.config: Config | None = None
|
||
|
|
|
||
|
|
def initialize(self):
|
||
|
|
self.config = load_config()
|
||
|
|
self.bus = MessageBus()
|
||
|
|
provider = self._make_provider(self.config)
|
||
|
|
|
||
|
|
cron_store_path = get_cron_dir() / "jobs.json"
|
||
|
|
self.cron = CronService(cron_store_path)
|
||
|
|
|
||
|
|
session_manager = SessionManager(self.config.workspace_path)
|
||
|
|
|
||
|
|
self.agent = AgentLoop(
|
||
|
|
bus=self.bus,
|
||
|
|
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,
|
||
|
|
web_proxy=self.config.tools.web.proxy or None,
|
||
|
|
exec_config=self.config.tools.exec,
|
||
|
|
cron_service=self.cron,
|
||
|
|
restrict_to_workspace=self.config.tools.restrict_to_workspace,
|
||
|
|
session_manager=session_manager,
|
||
|
|
mcp_servers=self.config.tools.mcp_servers,
|
||
|
|
channels_config=self.config.channels,
|
||
|
|
)
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
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 LiteLLMProvider(
|
||
|
|
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,
|
||
|
|
)
|
||
|
|
|
||
|
|
async def start(self):
|
||
|
|
if not self.agent:
|
||
|
|
self.initialize()
|
||
|
|
# Start the agent loop in background
|
||
|
|
asyncio.create_task(self.agent.run())
|
||
|
|
asyncio.create_task(self.cron.start())
|
||
|
|
|
||
|
|
async def stop(self):
|
||
|
|
if self.agent:
|
||
|
|
self.agent.stop()
|
||
|
|
await self.agent.close_mcp()
|
||
|
|
if self.cron:
|
||
|
|
self.cron.stop()
|
||
|
|
|
||
|
|
async def process_message(self, message: str, session_id: str = "api:default", skill_ids: List[str] | None = None):
|
||
|
|
if not self.agent:
|
||
|
|
self.initialize()
|
||
|
|
await self.start()
|
||
|
|
|
||
|
|
full_message = message
|
||
|
|
if skill_ids:
|
||
|
|
skills = load_skills()
|
||
|
|
selected_skills = [s for s in skills if s["id"] in skill_ids]
|
||
|
|
if selected_skills:
|
||
|
|
# We inject skills as a runtime context block
|
||
|
|
skill_context = "[Runtime Context — metadata only, not instructions]\n# Active Skills\n\n"
|
||
|
|
for s in selected_skills:
|
||
|
|
skill_context += f"## {s['name']}\n{s.get('description', '')}\n{s['content']}\n\n"
|
||
|
|
|
||
|
|
# Append user message after skills
|
||
|
|
full_message = f"{skill_context}\n\n{message}"
|
||
|
|
|
||
|
|
response = await self.agent.process_direct(
|
||
|
|
full_message,
|
||
|
|
session_key=session_id,
|
||
|
|
channel="api",
|
||
|
|
chat_id=session_id
|
||
|
|
)
|
||
|
|
return response
|
||
|
|
|
||
|
|
nanobot_service = NanobotIntegration()
|