chore: update nanobot to 0.1.4.post6
This commit is contained in:
@@ -11,7 +11,7 @@ NANOBOT_ROOT = PROJECT_ROOT / "nanobot"
|
||||
if str(NANOBOT_ROOT) not in sys.path:
|
||||
sys.path.append(str(NANOBOT_ROOT))
|
||||
|
||||
from nanobot.providers.litellm_provider import LiteLLMProvider
|
||||
from app.core.llm_provider import build_llm_provider
|
||||
from app.schemas.chart import ChartGenerationResponse
|
||||
from app.services.llm_cache import get_active_llm_config
|
||||
|
||||
@@ -150,12 +150,12 @@ async def generate_chart(data: List[Dict[str, Any]], query: str) -> ChartGenerat
|
||||
)
|
||||
|
||||
try:
|
||||
provider = LiteLLMProvider(
|
||||
provider = build_llm_provider(
|
||||
model=active_config.get("model"),
|
||||
provider=active_config.get("provider"),
|
||||
api_key=active_config.get("api_key"),
|
||||
api_base=active_config.get("api_base"),
|
||||
default_model=active_config.get("model"),
|
||||
extra_headers=active_config.get("extra_headers") or {},
|
||||
provider_name=active_config.get("provider")
|
||||
)
|
||||
except Exception as e:
|
||||
return ChartGenerationResponse(
|
||||
|
||||
@@ -19,7 +19,7 @@ NANOBOT_ROOT = PROJECT_ROOT / "nanobot"
|
||||
if str(NANOBOT_ROOT) not in sys.path:
|
||||
sys.path.append(str(NANOBOT_ROOT))
|
||||
|
||||
from nanobot.providers.litellm_provider import LiteLLMProvider
|
||||
from app.core.llm_provider import build_llm_provider
|
||||
from app.connectors.postgres import postgres_connector
|
||||
from app.connectors.clickhouse import clickhouse_connector
|
||||
from app.connectors.factory import get_connector
|
||||
@@ -358,12 +358,12 @@ async def process_nl2sql(
|
||||
|
||||
# 3. Initialize Provider
|
||||
try:
|
||||
provider = LiteLLMProvider(
|
||||
provider = build_llm_provider(
|
||||
model=active_config.get("model"),
|
||||
provider=active_config.get("provider"),
|
||||
api_key=active_config.get("api_key"),
|
||||
api_base=active_config.get("api_base"),
|
||||
default_model=active_config.get("model"),
|
||||
extra_headers=active_config.get("extra_headers") or {},
|
||||
provider_name=active_config.get("provider")
|
||||
)
|
||||
except Exception as e:
|
||||
return NL2SQLResponse(sql="", result=[], error=f"Failed to initialize LLM provider: {e}")
|
||||
@@ -410,8 +410,6 @@ Language: Chinese (Simplified)
|
||||
max_tokens=NL2SQL_MAX_TOKENS,
|
||||
temperature=NL2SQL_TEMPERATURE,
|
||||
reasoning_effort=NL2SQL_REASONING_EFFORT,
|
||||
request_timeout=NL2SQL_LLM_REQUEST_TIMEOUT_SECONDS,
|
||||
num_retries=0,
|
||||
),
|
||||
timeout=NL2SQL_LLM_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
+24
-46
@@ -7,7 +7,7 @@ from jose import jwt, JWTError
|
||||
from pydantic import BaseModel, Field
|
||||
from app.core.security import SECRET_KEY, ALGORITHM
|
||||
from app.core.data_root import get_data_root
|
||||
from litellm import completion
|
||||
from app.core.llm_provider import build_llm_provider
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBearer()
|
||||
@@ -154,52 +154,30 @@ def delete_llm_config(config_id: str, _: CurrentUser = Depends(get_admin_user)):
|
||||
return {"message": "LLM configuration deleted successfully"}
|
||||
|
||||
@router.post("/llm/test")
|
||||
def test_connection(request: TestConnectionRequest, _: CurrentUser = Depends(get_admin_user)):
|
||||
async def test_connection(request: TestConnectionRequest, _: CurrentUser = Depends(get_admin_user)):
|
||||
try:
|
||||
# Use litellm to test connection
|
||||
# litellm handles many providers
|
||||
kwargs = {
|
||||
"model": request.model,
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"max_tokens": 5
|
||||
provider = build_llm_provider(
|
||||
model=request.model.strip(),
|
||||
provider=request.provider,
|
||||
api_key=request.api_key,
|
||||
api_base=request.api_base,
|
||||
extra_headers=request.extra_headers,
|
||||
)
|
||||
response = await provider.chat(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
max_tokens=5,
|
||||
temperature=0,
|
||||
)
|
||||
if response.finish_reason == "error":
|
||||
raise ValueError(response.content or "Unknown provider error")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Connection successful",
|
||||
"details": {
|
||||
"content": response.content,
|
||||
"finish_reason": response.finish_reason,
|
||||
"usage": response.usage,
|
||||
},
|
||||
}
|
||||
|
||||
if request.api_key:
|
||||
kwargs["api_key"] = request.api_key
|
||||
|
||||
if request.api_base:
|
||||
kwargs["api_base"] = request.api_base
|
||||
|
||||
if request.extra_headers:
|
||||
kwargs["extra_headers"] = request.extra_headers
|
||||
|
||||
# For OpenAI-compatible endpoints that are not standard OpenAI (like Local, vLLM etc)
|
||||
# usually user sets provider to "openai" and api_base to their custom URL.
|
||||
# litellm usually works well if we pass custom_llm_provider="openai" if provider is openai but custom url
|
||||
|
||||
# If provider is "local" or "openai", we generally use "openai" format
|
||||
if request.provider == "local":
|
||||
kwargs["custom_llm_provider"] = "openai"
|
||||
elif request.provider:
|
||||
kwargs["custom_llm_provider"] = request.provider
|
||||
|
||||
# If user explicitly selected provider in UI, we might want to respect that
|
||||
# But litellm completion main arg is 'model'.
|
||||
# If the UI 'model' input doesn't have prefix, we might need to add it or pass custom_llm_provider.
|
||||
|
||||
# Simple heuristic: if provider is set, try to pass it if litellm supports it or just rely on env vars/args
|
||||
# For this simple test, we just try to call it.
|
||||
|
||||
try:
|
||||
response = completion(**kwargs)
|
||||
except Exception as first_error:
|
||||
error_text = str(first_error)
|
||||
if request.provider and "Provider NOT provided" in error_text and "/" not in request.model:
|
||||
retry_kwargs = kwargs.copy()
|
||||
retry_kwargs["model"] = f"{request.provider}/{request.model}"
|
||||
response = completion(**retry_kwargs)
|
||||
else:
|
||||
raise first_error
|
||||
return {"success": True, "message": "Connection successful", "details": str(response)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Connection failed: {str(e)}")
|
||||
|
||||
@@ -114,6 +114,22 @@ def _save_data(data: List[Dict[str, Any]]):
|
||||
with open(DATA_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def _dedupe_skills(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
deduped: Dict[str, Dict[str, Any]] = {}
|
||||
for item in data:
|
||||
skill_id = str(item.get("id") or "").strip()
|
||||
if not skill_id:
|
||||
continue
|
||||
existing = deduped.get(skill_id)
|
||||
if existing is None:
|
||||
deduped[skill_id] = item
|
||||
continue
|
||||
existing_project = existing.get("project_id")
|
||||
incoming_project = item.get("project_id")
|
||||
if existing_project is None and incoming_project is not None:
|
||||
deduped[skill_id] = item
|
||||
return list(deduped.values())
|
||||
|
||||
def _safe_skill_dir_name(value: str) -> str:
|
||||
safe = re.sub(r'[^a-zA-Z0-9_\-]', '_', value or "").lower()
|
||||
return safe or "skill"
|
||||
@@ -183,9 +199,10 @@ def load_skills(project_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
data.append(new_skill)
|
||||
registered_paths.add(skill_dir)
|
||||
|
||||
deduped = _dedupe_skills(data)
|
||||
if project_id is not None:
|
||||
return [item for item in data if item.get("project_id") == project_id or item.get("project_id") is None]
|
||||
return data
|
||||
return [item for item in deduped if item.get("project_id") == project_id or item.get("project_id") is None]
|
||||
return deduped
|
||||
|
||||
@router.get("/skills", response_model=List[Skill])
|
||||
def list_skills(project_id: Optional[int] = None):
|
||||
@@ -384,7 +401,7 @@ def delete_skill(skill_id: str, project_id: Optional[int] = None):
|
||||
if item["id"] == skill_id:
|
||||
if item.get("is_builtin"):
|
||||
raise HTTPException(status_code=400, detail="Builtin skills cannot be deleted")
|
||||
if project_id is not None and item.get("project_id") != project_id:
|
||||
if project_id is not None and item.get("project_id") not in (project_id, None):
|
||||
new_data.append(item)
|
||||
continue
|
||||
found = True
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models.subagent import Subagent
|
||||
from app.models.project import Project
|
||||
from app.schemas.subagent import SubagentCreate, SubagentUpdate, Subagent as SubagentSchema
|
||||
from app.core.security import get_current_user, CurrentUser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/projects/{project_id}/subagents", response_model=List[SubagentSchema])
|
||||
def list_subagents(
|
||||
project_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
if not current_user.is_admin and project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||
|
||||
subagents = db.query(Subagent).filter(Subagent.project_id == project_id).offset(skip).limit(limit).all()
|
||||
return subagents
|
||||
|
||||
@router.post("/projects/{project_id}/subagents", response_model=SubagentSchema)
|
||||
def create_subagent(
|
||||
project_id: int,
|
||||
subagent: SubagentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
if not current_user.is_admin and project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||
|
||||
db_subagent = Subagent(**subagent.dict(), project_id=project_id)
|
||||
db.add(db_subagent)
|
||||
db.commit()
|
||||
db.refresh(db_subagent)
|
||||
return db_subagent
|
||||
|
||||
@router.get("/subagents/{subagent_id}", response_model=SubagentSchema)
|
||||
def read_subagent(
|
||||
subagent_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
db_subagent = db.query(Subagent).filter(Subagent.id == subagent_id).first()
|
||||
if db_subagent is None:
|
||||
raise HTTPException(status_code=404, detail="Subagent not found")
|
||||
|
||||
project = db.query(Project).filter(Project.id == db_subagent.project_id).first()
|
||||
if not current_user.is_admin and project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||
|
||||
return db_subagent
|
||||
|
||||
@router.put("/subagents/{subagent_id}", response_model=SubagentSchema)
|
||||
def update_subagent(
|
||||
subagent_id: int,
|
||||
subagent: SubagentUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
db_subagent = db.query(Subagent).filter(Subagent.id == subagent_id).first()
|
||||
if db_subagent is None:
|
||||
raise HTTPException(status_code=404, detail="Subagent not found")
|
||||
|
||||
project = db.query(Project).filter(Project.id == db_subagent.project_id).first()
|
||||
if not current_user.is_admin and project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||
|
||||
subagent_data = subagent.dict(exclude_unset=True)
|
||||
for key, value in subagent_data.items():
|
||||
setattr(db_subagent, key, value)
|
||||
|
||||
db.add(db_subagent)
|
||||
db.commit()
|
||||
db.refresh(db_subagent)
|
||||
return db_subagent
|
||||
|
||||
@router.delete("/subagents/{subagent_id}")
|
||||
def delete_subagent(
|
||||
subagent_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
db_subagent = db.query(Subagent).filter(Subagent.id == subagent_id).first()
|
||||
if db_subagent is None:
|
||||
raise HTTPException(status_code=404, detail="Subagent not found")
|
||||
|
||||
project = db.query(Project).filter(Project.id == db_subagent.project_id).first()
|
||||
if not current_user.is_admin and project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||
|
||||
db.delete(db_subagent)
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
@@ -2,14 +2,32 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from app.core.data_root import get_data_root, get_reports_root, get_uploads_root, get_workspace_root
|
||||
from app.core.data_root import (
|
||||
BACKEND_ROOT,
|
||||
LEGACY_DATA_ROOT,
|
||||
get_data_root,
|
||||
get_reports_root,
|
||||
get_uploads_root,
|
||||
get_workspace_root,
|
||||
)
|
||||
|
||||
|
||||
data_root = get_data_root()
|
||||
workspace_root = get_workspace_root()
|
||||
uploads_root = get_uploads_root()
|
||||
reports_root = get_reports_root()
|
||||
allowed_artifact_roots = (workspace_root, uploads_root, reports_root)
|
||||
legacy_workspace_root = LEGACY_DATA_ROOT / "workspace"
|
||||
legacy_uploads_root = LEGACY_DATA_ROOT / "uploads"
|
||||
legacy_reports_root = LEGACY_DATA_ROOT / "data"
|
||||
backend_root = BACKEND_ROOT
|
||||
allowed_artifact_roots = (
|
||||
workspace_root,
|
||||
uploads_root,
|
||||
reports_root,
|
||||
legacy_workspace_root,
|
||||
legacy_uploads_root,
|
||||
legacy_reports_root,
|
||||
)
|
||||
|
||||
|
||||
def resolve_upload_file_path(file_url: Optional[str]) -> Path:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
from typing import Optional, Dict
|
||||
|
||||
from nanobot.providers.azure_openai_provider import AzureOpenAIProvider
|
||||
from nanobot.providers.openai_codex_provider import OpenAICodexProvider
|
||||
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||
from nanobot.providers.registry import find_by_name
|
||||
|
||||
|
||||
def normalize_provider_name(provider: Optional[str]) -> Optional[str]:
|
||||
if not provider:
|
||||
return None
|
||||
normalized = provider.strip().lower()
|
||||
alias_map = {
|
||||
"azure": "azure_openai",
|
||||
"local": "vllm",
|
||||
}
|
||||
return alias_map.get(normalized, normalized)
|
||||
|
||||
|
||||
def build_llm_provider(
|
||||
*,
|
||||
model: str,
|
||||
provider: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
provider_name = normalize_provider_name(provider)
|
||||
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,
|
||||
)
|
||||
+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]] = []
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import contextvars
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
from loguru import logger
|
||||
from nanobot.providers.litellm_provider import LiteLLMProvider
|
||||
from nanobot.providers.base import LLMResponse
|
||||
from litellm import acompletion, stream_chunk_builder
|
||||
|
||||
streaming_queue_var = contextvars.ContextVar("streaming_queue", default=None)
|
||||
|
||||
class StreamingLiteLLMProvider(LiteLLMProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._provider_name_override = kwargs.get("provider_name")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _get_active_spec(self, model: str):
|
||||
from nanobot.providers.registry import find_by_model, find_by_name
|
||||
spec = None
|
||||
if self._provider_name_override:
|
||||
spec = find_by_name(self._provider_name_override)
|
||||
if not spec:
|
||||
spec = find_by_model(model)
|
||||
return spec
|
||||
|
||||
def _setup_env(self, api_key: str, api_base: str | None, model: str) -> None:
|
||||
"""Set environment variables based on detected provider."""
|
||||
import os
|
||||
spec = self._gateway or self._get_active_spec(model)
|
||||
if not spec:
|
||||
return
|
||||
if not spec.env_key:
|
||||
return
|
||||
|
||||
if self._gateway:
|
||||
os.environ[spec.env_key] = api_key
|
||||
else:
|
||||
# os.environ.setdefault 会在已存在且为空字符串时保留空字符串,导致 litellm 无法识别
|
||||
# 因此强制更新
|
||||
os.environ[spec.env_key] = api_key
|
||||
|
||||
effective_base = api_base or spec.default_api_base
|
||||
for env_name, env_val in spec.env_extras:
|
||||
resolved = env_val.replace("{api_key}", api_key)
|
||||
resolved = resolved.replace("{api_base}", effective_base)
|
||||
os.environ[env_name] = resolved
|
||||
|
||||
def _resolve_model(self, model: str) -> str:
|
||||
"""Resolve model name by applying provider/gateway prefixes, using override if available."""
|
||||
if self._gateway:
|
||||
prefix = self._gateway.litellm_prefix
|
||||
if self._gateway.strip_model_prefix:
|
||||
model = model.split("/")[-1]
|
||||
if prefix and not model.startswith(f"{prefix}/"):
|
||||
model = f"{prefix}/{model}"
|
||||
return model
|
||||
|
||||
spec = self._get_active_spec(model)
|
||||
if spec and spec.litellm_prefix:
|
||||
model = self._canonicalize_explicit_prefix(model, spec.name, spec.litellm_prefix)
|
||||
if not any(model.startswith(s) for s in spec.skip_prefixes):
|
||||
model = f"{spec.litellm_prefix}/{model}"
|
||||
elif spec and not spec.litellm_prefix and "/" not in model:
|
||||
# For standard providers like openai, anthropic, litellm requires the prefix for unknown models
|
||||
# but registry sets litellm_prefix="" to rely on native matching.
|
||||
# If native matching fails (e.g. non-standard model name), we should force prefix.
|
||||
# We only force prefix if provider was explicitly set and model has no prefix.
|
||||
if self._provider_name_override:
|
||||
model = f"{spec.name}/{model}"
|
||||
|
||||
return model
|
||||
|
||||
def _apply_model_overrides(self, model: str, kwargs: dict[str, Any]) -> None:
|
||||
"""Apply model-specific parameter overrides from the registry."""
|
||||
model_lower = model.lower()
|
||||
spec = self._get_active_spec(model)
|
||||
if spec:
|
||||
for pattern, overrides in spec.model_overrides:
|
||||
if pattern in model_lower:
|
||||
kwargs.update(overrides)
|
||||
return
|
||||
|
||||
def _extra_msg_keys(self, original_model: str, resolved_model: str) -> frozenset[str]:
|
||||
"""Return provider-specific extra keys to preserve in request messages."""
|
||||
spec = self._get_active_spec(original_model) or self._get_active_spec(resolved_model)
|
||||
if (spec and spec.name == "anthropic") or "claude" in original_model.lower() or resolved_model.startswith("anthropic/"):
|
||||
# _ANTHROPIC_EXTRA_KEYS is defined in nanobot.providers.litellm_provider, let's just use the string
|
||||
return frozenset({"thinking_blocks"})
|
||||
return frozenset()
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 4000,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
request_timeout: Optional[int] = None,
|
||||
num_retries: Optional[int] = None,
|
||||
) -> LLMResponse:
|
||||
original_model = model or self.default_model
|
||||
model_name = self._resolve_model(original_model)
|
||||
extra_msg_keys = self._extra_msg_keys(original_model, model_name)
|
||||
|
||||
if self._supports_cache_control(original_model):
|
||||
messages, tools = self._apply_cache_control(messages, tools)
|
||||
|
||||
kwargs: Dict[str, Any] = {
|
||||
"model": model_name,
|
||||
"messages": self._sanitize_messages(self._sanitize_empty_content(messages), extra_keys=extra_msg_keys),
|
||||
"temperature": temperature,
|
||||
"max_tokens": max(1, max_tokens),
|
||||
"stream": True, # 强制开启流式
|
||||
}
|
||||
|
||||
self._apply_model_overrides(model_name, kwargs)
|
||||
|
||||
if self.api_key and self.api_key != "no-key":
|
||||
kwargs["api_key"] = self.api_key
|
||||
if self.api_base:
|
||||
kwargs["api_base"] = self.api_base
|
||||
if self.extra_headers:
|
||||
kwargs["extra_headers"] = self.extra_headers
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
kwargs["tool_choice"] = "auto"
|
||||
if request_timeout is not None:
|
||||
kwargs["timeout"] = request_timeout
|
||||
if num_retries is not None:
|
||||
kwargs["num_retries"] = max(0, int(num_retries))
|
||||
|
||||
if reasoning_effort:
|
||||
kwargs["reasoning_effort"] = reasoning_effort
|
||||
kwargs["drop_params"] = True
|
||||
|
||||
try:
|
||||
response_stream = await acompletion(**kwargs)
|
||||
chunks = []
|
||||
queue = streaming_queue_var.get()
|
||||
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
if queue is not None:
|
||||
# 提取普通内容或 think 内容
|
||||
delta = chunk.choices[0].delta if chunk.choices else None
|
||||
if delta:
|
||||
content = getattr(delta, "content", None)
|
||||
reasoning_content = getattr(delta, "reasoning_content", None)
|
||||
|
||||
if content:
|
||||
await queue.put({"type": "delta", "content": content})
|
||||
if reasoning_content:
|
||||
await queue.put({"type": "progress", "content": reasoning_content, "is_reasoning": True})
|
||||
|
||||
# 还原为完整的 response 对象供 nanobot 处理
|
||||
full_response = stream_chunk_builder(chunks, messages=messages)
|
||||
return self._parse_response(full_response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("StreamingLiteLLMProvider failed: {}", e)
|
||||
raise
|
||||
@@ -14,3 +14,4 @@ class Project(Base):
|
||||
|
||||
owner = relationship("User", back_populates="projects")
|
||||
data_sources = relationship("DataSource", back_populates="project", cascade="all, delete-orphan")
|
||||
subagents = relationship("Subagent", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
|
||||
class Subagent(Base):
|
||||
__tablename__ = "subagents"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
instructions = Column(String, nullable=True)
|
||||
model = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
project = relationship("Project", back_populates="subagents")
|
||||
@@ -0,0 +1,27 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
class SubagentBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
instructions: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
|
||||
class SubagentCreate(SubagentBase):
|
||||
pass
|
||||
|
||||
class SubagentUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
instructions: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
|
||||
class Subagent(SubagentBase):
|
||||
id: int
|
||||
project_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,149 @@
|
||||
from typing import Any, Optional
|
||||
import json
|
||||
|
||||
from nanobot.agent.tools.base import Tool
|
||||
from app.database import SessionLocal
|
||||
from app.models.subagent import Subagent
|
||||
from app.core.nanobot import nanobot_service
|
||||
from app.services.llm_cache import get_llm_configs, get_active_llm_config
|
||||
|
||||
class ListSubagentsTool(Tool):
|
||||
"""
|
||||
Tool to list available subagents for the current project.
|
||||
"""
|
||||
def __init__(self, project_id: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.project_id = project_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "list_subagents"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "List all available subagents in the current project, including their names and descriptions."
|
||||
|
||||
@property
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
|
||||
async def execute(self, **kwargs: Any) -> str:
|
||||
if not self.project_id:
|
||||
return "Error: No project context available to list subagents."
|
||||
|
||||
with SessionLocal() as db:
|
||||
subagents = db.query(Subagent).filter(Subagent.project_id == self.project_id).all()
|
||||
|
||||
if not subagents:
|
||||
return "No subagents found in the current project."
|
||||
|
||||
result = []
|
||||
for sa in subagents:
|
||||
result.append({
|
||||
"id": sa.id,
|
||||
"name": sa.name,
|
||||
"description": sa.description,
|
||||
})
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
class InvokeSubagentTool(Tool):
|
||||
"""
|
||||
Tool to invoke a specific subagent to perform a task.
|
||||
"""
|
||||
def __init__(self, project_id: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.project_id = project_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "invoke_subagent"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return (
|
||||
"Invoke a subagent by name to perform a specific task. "
|
||||
"You should first use list_subagents to find the correct subagent name."
|
||||
)
|
||||
|
||||
@property
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subagent_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the subagent to invoke.",
|
||||
},
|
||||
"task": {
|
||||
"type": "string",
|
||||
"description": "The specific task or query to send to the subagent.",
|
||||
}
|
||||
},
|
||||
"required": ["subagent_name", "task"],
|
||||
}
|
||||
|
||||
async def execute(self, **kwargs: Any) -> str:
|
||||
subagent_name = kwargs.get("subagent_name")
|
||||
task = kwargs.get("task")
|
||||
|
||||
if not self.project_id:
|
||||
return "Error: No project context available to invoke subagent."
|
||||
|
||||
if not subagent_name or not task:
|
||||
return "Error: subagent_name and task are required."
|
||||
|
||||
with SessionLocal() as db:
|
||||
subagent = db.query(Subagent).filter(
|
||||
Subagent.project_id == self.project_id,
|
||||
Subagent.name == subagent_name
|
||||
).first()
|
||||
|
||||
if not subagent:
|
||||
return f"Error: Subagent '{subagent_name}' not found."
|
||||
|
||||
# Construct the message with subagent instructions
|
||||
instructions = subagent.instructions or "You are a helpful assistant."
|
||||
message = f"[System: You are acting as subagent '{subagent.name}'. Instructions: {instructions}]\n\nTask: {task}"
|
||||
resolved_model_id = None
|
||||
llm_configs = get_llm_configs()
|
||||
target = None
|
||||
raw_model = (getattr(subagent, "model", None) or "").strip()
|
||||
if raw_model:
|
||||
target = next((item for item in llm_configs if item.get("id") == raw_model), None)
|
||||
if target is None:
|
||||
normalized = raw_model.lower()
|
||||
target = next(
|
||||
(
|
||||
item for item in llm_configs
|
||||
if (
|
||||
str(item.get("model") or "").strip().lower() == normalized
|
||||
or str(item.get("name") or "").strip().lower() == normalized
|
||||
)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if target is None:
|
||||
target = get_active_llm_config()
|
||||
if target and target.get("id"):
|
||||
resolved_model_id = target.get("id")
|
||||
|
||||
try:
|
||||
from app.context import current_session_id
|
||||
parent_session_id = current_session_id.get() or "default"
|
||||
subagent_session_id = f"{parent_session_id}:subagent:{subagent.id}"
|
||||
|
||||
response = await nanobot_service.process_message(
|
||||
message=message,
|
||||
session_id=subagent_session_id,
|
||||
project_id=self.project_id,
|
||||
model_id=resolved_model_id,
|
||||
)
|
||||
return f"Subagent '{subagent.name}' completed the task.\nResult:\n{response}"
|
||||
except Exception as e:
|
||||
return f"Error invoking subagent '{subagent.name}': {str(e)}"
|
||||
Reference in New Issue
Block a user