chore: nothing special, subagent enhanced
This commit is contained in:
@@ -185,9 +185,8 @@ class NanobotIntegration:
|
||||
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))
|
||||
agent.tools.register(ListSubagentsTool(project_id=project_id))
|
||||
agent.tools.register(InvokeSubagentTool(project_id=project_id))
|
||||
|
||||
def _build_provider(
|
||||
self,
|
||||
@@ -357,9 +356,9 @@ class NanobotIntegration:
|
||||
|
||||
if project_id is None:
|
||||
from app.core.session_alias_store import session_alias_store
|
||||
alias_info = session_alias_store.get_alias(session_id)
|
||||
if alias_info and alias_info.get("project_id"):
|
||||
project_id = alias_info.get("project_id")
|
||||
alias_meta = session_alias_store.get_alias_meta(session_id)
|
||||
if alias_meta and alias_meta.get("project_id") is not None:
|
||||
project_id = alias_meta.get("project_id")
|
||||
|
||||
agent_to_use = self.agent
|
||||
need_custom_agent = False
|
||||
|
||||
@@ -173,6 +173,22 @@ class SessionAliasStore:
|
||||
alias = row["alias"]
|
||||
return str(alias) if alias else None
|
||||
|
||||
def get_alias_meta(self, session_key: str) -> dict[str, Any] | None:
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT alias, pinned, archived, project_id FROM session_cache WHERE session_key = ?",
|
||||
(session_key,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
alias = (row["alias"] or "").strip()
|
||||
return {
|
||||
"alias": alias or None,
|
||||
"pinned": bool(row["pinned"]) if "pinned" in row.keys() else False,
|
||||
"archived": bool(row["archived"]) if "archived" in row.keys() else False,
|
||||
"project_id": row["project_id"] if "project_id" in row.keys() else None,
|
||||
}
|
||||
|
||||
def delete_session(self, session_key: str) -> None:
|
||||
with self._connect() as conn:
|
||||
conn.execute("DELETE FROM session_cache WHERE session_key = ?", (session_key,))
|
||||
|
||||
@@ -5,8 +5,23 @@ 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.core.session_alias_store import session_alias_store
|
||||
from app.services.llm_cache import get_llm_configs, get_active_llm_config
|
||||
|
||||
|
||||
def _resolve_project_id(preferred_project_id: Optional[int]) -> Optional[int]:
|
||||
if preferred_project_id is not None:
|
||||
return preferred_project_id
|
||||
from app.context import current_session_id
|
||||
session_id = (current_session_id.get() or "").strip()
|
||||
if not session_id:
|
||||
return None
|
||||
alias_meta = session_alias_store.get_alias_meta(session_id)
|
||||
if not alias_meta:
|
||||
return None
|
||||
project_id = alias_meta.get("project_id")
|
||||
return project_id if isinstance(project_id, int) else None
|
||||
|
||||
class ListSubagentsTool(Tool):
|
||||
"""
|
||||
Tool to list available subagents for the current project.
|
||||
@@ -32,11 +47,12 @@ class ListSubagentsTool(Tool):
|
||||
}
|
||||
|
||||
async def execute(self, **kwargs: Any) -> str:
|
||||
if not self.project_id:
|
||||
resolved_project_id = _resolve_project_id(self.project_id)
|
||||
if resolved_project_id is None:
|
||||
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()
|
||||
subagents = db.query(Subagent).filter(Subagent.project_id == resolved_project_id).all()
|
||||
|
||||
if not subagents:
|
||||
return "No subagents found in the current project."
|
||||
@@ -91,8 +107,9 @@ class InvokeSubagentTool(Tool):
|
||||
async def execute(self, **kwargs: Any) -> str:
|
||||
subagent_name = kwargs.get("subagent_name")
|
||||
task = kwargs.get("task")
|
||||
resolved_project_id = _resolve_project_id(self.project_id)
|
||||
|
||||
if not self.project_id:
|
||||
if resolved_project_id is None:
|
||||
return "Error: No project context available to invoke subagent."
|
||||
|
||||
if not subagent_name or not task:
|
||||
@@ -100,7 +117,7 @@ class InvokeSubagentTool(Tool):
|
||||
|
||||
with SessionLocal() as db:
|
||||
subagent = db.query(Subagent).filter(
|
||||
Subagent.project_id == self.project_id,
|
||||
Subagent.project_id == resolved_project_id,
|
||||
Subagent.name == subagent_name
|
||||
).first()
|
||||
|
||||
@@ -141,7 +158,7 @@ class InvokeSubagentTool(Tool):
|
||||
response = await nanobot_service.process_message(
|
||||
message=message,
|
||||
session_id=subagent_session_id,
|
||||
project_id=self.project_id,
|
||||
project_id=resolved_project_id,
|
||||
model_id=resolved_model_id,
|
||||
)
|
||||
return f"Subagent '{subagent.name}' completed the task.\nResult:\n{response}"
|
||||
|
||||
@@ -214,6 +214,7 @@ def preview_web_artifact_resource(root_token: str, resource_path: str):
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: str = "api:default"
|
||||
project_id: Optional[int] = None
|
||||
skill_ids: Optional[List[str]] = None
|
||||
model_id: Optional[str] = None
|
||||
source: str = "postgres"
|
||||
@@ -238,6 +239,15 @@ def _resolve_effective_source(request: ChatRequest) -> str:
|
||||
effective_source = session_source
|
||||
return effective_source
|
||||
|
||||
|
||||
def _sync_session_project(session_id: str, project_id: Optional[int]) -> None:
|
||||
if project_id is None:
|
||||
return
|
||||
session_alias_store.update_alias_meta(
|
||||
session_key=session_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
class SessionAliasUpdateRequest(BaseModel):
|
||||
title: Optional[str] = None
|
||||
pinned: Optional[bool] = None
|
||||
@@ -277,6 +287,7 @@ def _persist_assistant_enrichment(
|
||||
@app.post("/nanobot/chat")
|
||||
async def nanobot_chat(request: ChatRequest):
|
||||
try:
|
||||
_sync_session_project(request.session_id, request.project_id)
|
||||
resolved_source = _resolve_effective_source(request)
|
||||
current_data_source.set(resolved_source)
|
||||
current_file_url.set(request.file_url)
|
||||
@@ -300,6 +311,7 @@ async def nanobot_chat(request: ChatRequest):
|
||||
session_id=request.session_id,
|
||||
skill_ids=request.skill_ids,
|
||||
model_id=request.model_id,
|
||||
project_id=request.project_id,
|
||||
)
|
||||
text = response or ""
|
||||
session_messages = []
|
||||
@@ -331,6 +343,7 @@ async def nanobot_chat_stream(request: ChatRequest):
|
||||
async def event_generator():
|
||||
current_task = None
|
||||
try:
|
||||
_sync_session_project(request.session_id, request.project_id)
|
||||
resolved_source = _resolve_effective_source(request)
|
||||
current_data_source.set(resolved_source)
|
||||
current_file_url.set(request.file_url)
|
||||
@@ -369,6 +382,7 @@ async def nanobot_chat_stream(request: ChatRequest):
|
||||
session_id=request.session_id,
|
||||
skill_ids=request.skill_ids,
|
||||
model_id=request.model_id,
|
||||
project_id=request.project_id,
|
||||
on_progress=_on_progress,
|
||||
on_stream=_on_stream,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = BACKEND_ROOT.parent
|
||||
NANOBOT_ROOT = REPO_ROOT / "nanobot"
|
||||
if str(BACKEND_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
if str(NANOBOT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(NANOBOT_ROOT))
|
||||
|
||||
import main
|
||||
|
||||
|
||||
def test_nanobot_chat_syncs_project_id(monkeypatch) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
process_kwargs: list[dict[str, object]] = []
|
||||
|
||||
def fake_update_alias_meta(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return kwargs
|
||||
|
||||
async def fake_process_message(*args, **kwargs):
|
||||
process_kwargs.append(kwargs)
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(main.session_alias_store, "update_alias_meta", fake_update_alias_meta)
|
||||
monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message)
|
||||
monkeypatch.setattr(main.nanobot_service, "agent", None)
|
||||
|
||||
request = main.ChatRequest(message="hello", session_id="api:test-1", project_id=101)
|
||||
response = asyncio.run(main.nanobot_chat(request))
|
||||
|
||||
assert response["response"] == "ok"
|
||||
assert calls == [{"session_key": "api:test-1", "project_id": 101}]
|
||||
assert process_kwargs and process_kwargs[0]["project_id"] == 101
|
||||
|
||||
|
||||
def test_nanobot_chat_without_project_id_does_not_sync(monkeypatch) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
process_kwargs: list[dict[str, object]] = []
|
||||
|
||||
def fake_update_alias_meta(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return kwargs
|
||||
|
||||
async def fake_process_message(*args, **kwargs):
|
||||
process_kwargs.append(kwargs)
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(main.session_alias_store, "update_alias_meta", fake_update_alias_meta)
|
||||
monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message)
|
||||
monkeypatch.setattr(main.nanobot_service, "agent", None)
|
||||
|
||||
request = main.ChatRequest(message="hello", session_id="api:test-2")
|
||||
response = asyncio.run(main.nanobot_chat(request))
|
||||
|
||||
assert response["response"] == "ok"
|
||||
assert calls == []
|
||||
assert process_kwargs and process_kwargs[0]["project_id"] is None
|
||||
|
||||
|
||||
def test_nanobot_chat_stream_syncs_project_id(monkeypatch) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
process_kwargs: list[dict[str, object]] = []
|
||||
|
||||
def fake_update_alias_meta(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return kwargs
|
||||
|
||||
async def fake_process_message(*args, **kwargs):
|
||||
process_kwargs.append(kwargs)
|
||||
on_stream = kwargs.get("on_stream")
|
||||
if on_stream:
|
||||
await on_stream("stream-token")
|
||||
return "stream-complete"
|
||||
|
||||
async def collect_stream_chunks(response) -> list[str]:
|
||||
chunks: list[str] = []
|
||||
async for chunk in response.body_iterator:
|
||||
if isinstance(chunk, bytes):
|
||||
chunks.append(chunk.decode("utf-8"))
|
||||
else:
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
monkeypatch.setattr(main.session_alias_store, "update_alias_meta", fake_update_alias_meta)
|
||||
monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message)
|
||||
monkeypatch.setattr(main.nanobot_service, "agent", None)
|
||||
|
||||
request = main.ChatRequest(message="hello", session_id="api:test-3", project_id=202)
|
||||
response = asyncio.run(main.nanobot_chat_stream(request))
|
||||
chunks = asyncio.run(collect_stream_chunks(response))
|
||||
content = "".join(chunks)
|
||||
|
||||
assert "stream-token" in content
|
||||
assert "stream-complete" in content
|
||||
assert calls == [{"session_key": "api:test-3", "project_id": 202}]
|
||||
assert process_kwargs and process_kwargs[0]["project_id"] == 202
|
||||
@@ -0,0 +1,110 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.core.nanobot import NanobotIntegration
|
||||
from app.context import current_session_id
|
||||
|
||||
|
||||
class _DummySessions:
|
||||
def __init__(self) -> None:
|
||||
self.saved = []
|
||||
self._session = SimpleNamespace(messages=[])
|
||||
|
||||
def get_or_create(self, _session_id: str):
|
||||
return self._session
|
||||
|
||||
def save(self, session) -> None:
|
||||
self.saved.append(session)
|
||||
|
||||
|
||||
class _DummyAgent:
|
||||
def __init__(self) -> None:
|
||||
self.sessions = _DummySessions()
|
||||
self.provider = SimpleNamespace(default_model="demo-model")
|
||||
self.model = "demo-model"
|
||||
|
||||
async def process_direct(self, *_args, **_kwargs):
|
||||
return "ok"
|
||||
|
||||
|
||||
def test_process_message_project_id_fallback_from_session_alias(monkeypatch) -> None:
|
||||
service = NanobotIntegration()
|
||||
base_agent = _DummyAgent()
|
||||
custom_agent = _DummyAgent()
|
||||
service.agent = base_agent
|
||||
service._started = True
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_get_or_create_model_agent(model_id, target_config, project_id):
|
||||
captured["project_id"] = project_id
|
||||
return custom_agent
|
||||
|
||||
monkeypatch.setattr(service, "_get_or_create_model_agent", fake_get_or_create_model_agent)
|
||||
monkeypatch.setattr("app.core.nanobot.get_llm_configs", lambda: [])
|
||||
monkeypatch.setattr("app.core.nanobot.get_active_llm_config", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
"app.core.session_alias_store.session_alias_store.get_alias_meta",
|
||||
lambda _session_id: {"project_id": 77},
|
||||
)
|
||||
|
||||
response = asyncio.run(service.process_message("hello", session_id="api:s1"))
|
||||
|
||||
assert response == "ok"
|
||||
assert captured["project_id"] == 77
|
||||
|
||||
|
||||
def test_process_message_project_id_prefers_request_value(monkeypatch) -> None:
|
||||
service = NanobotIntegration()
|
||||
base_agent = _DummyAgent()
|
||||
custom_agent = _DummyAgent()
|
||||
service.agent = base_agent
|
||||
service._started = True
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_get_or_create_model_agent(model_id, target_config, project_id):
|
||||
captured["project_id"] = project_id
|
||||
return custom_agent
|
||||
|
||||
monkeypatch.setattr(service, "_get_or_create_model_agent", fake_get_or_create_model_agent)
|
||||
monkeypatch.setattr("app.core.nanobot.get_llm_configs", lambda: [])
|
||||
monkeypatch.setattr("app.core.nanobot.get_active_llm_config", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
"app.core.session_alias_store.session_alias_store.get_alias_meta",
|
||||
lambda _session_id: {"project_id": 88},
|
||||
)
|
||||
|
||||
response = asyncio.run(service.process_message("hello", session_id="api:s2", project_id=9))
|
||||
|
||||
assert response == "ok"
|
||||
assert captured["project_id"] == 9
|
||||
|
||||
|
||||
def test_register_custom_tools_always_contains_subagent_tools() -> None:
|
||||
service = NanobotIntegration()
|
||||
names: list[str] = []
|
||||
|
||||
class _ToolRegistry:
|
||||
def register(self, tool) -> None:
|
||||
names.append(tool.name)
|
||||
|
||||
fake_agent = SimpleNamespace(tools=_ToolRegistry())
|
||||
service._register_custom_tools(fake_agent, project_id=None)
|
||||
|
||||
assert "list_subagents" in names
|
||||
assert "invoke_subagent" in names
|
||||
|
||||
|
||||
def test_subagent_tool_resolves_project_from_session_alias(monkeypatch) -> None:
|
||||
from app.tools.subagent import _resolve_project_id
|
||||
|
||||
token = current_session_id.set("api:subagent-test")
|
||||
try:
|
||||
monkeypatch.setattr(
|
||||
"app.tools.subagent.session_alias_store.get_alias_meta",
|
||||
lambda _session_id: {"project_id": 66},
|
||||
)
|
||||
assert _resolve_project_id(None) == 66
|
||||
finally:
|
||||
current_session_id.reset(token)
|
||||
@@ -0,0 +1,102 @@
|
||||
import sys
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = BACKEND_ROOT.parent
|
||||
NANOBOT_ROOT = REPO_ROOT / "nanobot"
|
||||
if str(BACKEND_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
if str(NANOBOT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(NANOBOT_ROOT))
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.security import CurrentUser, get_current_user
|
||||
from app.database import Base, get_db
|
||||
from app.models.project import Project
|
||||
from app.models.subagent import Subagent
|
||||
from app.models.user import User
|
||||
from main import app
|
||||
|
||||
|
||||
def _seed_subagent(db: Session) -> tuple[User, Project, Subagent]:
|
||||
user = User(
|
||||
username="task3-owner",
|
||||
email="task3-owner@example.com",
|
||||
hashed_password="test",
|
||||
is_admin=False,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
project = Project(
|
||||
name="task3-project",
|
||||
description="task3",
|
||||
owner_id=user.id,
|
||||
)
|
||||
db.add(project)
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
|
||||
subagent = Subagent(
|
||||
project_id=project.id,
|
||||
name="task3-subagent",
|
||||
description="task3",
|
||||
instructions="do task3",
|
||||
model="gpt",
|
||||
)
|
||||
db.add(subagent)
|
||||
db.commit()
|
||||
db.refresh(subagent)
|
||||
return user, project, subagent
|
||||
|
||||
|
||||
def test_subagent_detail_route_is_global_and_project_scoped_route_is_invalid() -> None:
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db = testing_session_local()
|
||||
user, project, subagent = _seed_subagent(db)
|
||||
user_id = user.id
|
||||
username = user.username
|
||||
project_id = project.id
|
||||
subagent_id = subagent.id
|
||||
db.close()
|
||||
|
||||
def override_get_db() -> Generator[Session, None, None]:
|
||||
override_db = testing_session_local()
|
||||
try:
|
||||
yield override_db
|
||||
finally:
|
||||
override_db.close()
|
||||
|
||||
def override_current_user() -> CurrentUser:
|
||||
return CurrentUser(id=user_id, username=username, is_admin=False)
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
app.dependency_overrides[get_current_user] = override_current_user
|
||||
|
||||
try:
|
||||
client = TestClient(app)
|
||||
response = client.get(f"/api/v1/subagents/{subagent_id}")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["id"] == subagent_id
|
||||
assert body["project_id"] == project_id
|
||||
|
||||
legacy_path_response = client.get(f"/api/v1/projects/{project_id}/subagents/{subagent_id}")
|
||||
assert legacy_path_response.status_code == 404
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
engine.dispose()
|
||||
@@ -0,0 +1,136 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = BACKEND_ROOT.parent
|
||||
NANOBOT_ROOT = REPO_ROOT / "nanobot"
|
||||
if str(BACKEND_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
if str(NANOBOT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(NANOBOT_ROOT))
|
||||
|
||||
from app.context import current_session_id
|
||||
from app.core.security import CurrentUser, get_current_user
|
||||
from app.database import Base, get_db
|
||||
from app.models.project import Project
|
||||
from app.models.user import User
|
||||
from app.tools.subagent import InvokeSubagentTool, ListSubagentsTool
|
||||
from main import app
|
||||
|
||||
|
||||
def _seed_owner_and_project(db: Session) -> tuple[User, Project]:
|
||||
user = User(
|
||||
username="task4-owner",
|
||||
email="task4-owner@example.com",
|
||||
hashed_password="test",
|
||||
is_admin=False,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
project = Project(
|
||||
name="task4-project",
|
||||
description="task4",
|
||||
owner_id=user.id,
|
||||
)
|
||||
db.add(project)
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
return user, project
|
||||
|
||||
|
||||
def test_create_subagent_then_list_and_invoke_success(monkeypatch) -> None:
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db = testing_session_local()
|
||||
user, project = _seed_owner_and_project(db)
|
||||
user_id = user.id
|
||||
username = user.username
|
||||
project_id = project.id
|
||||
db.close()
|
||||
|
||||
def override_get_db() -> Generator[Session, None, None]:
|
||||
override_db = testing_session_local()
|
||||
try:
|
||||
yield override_db
|
||||
finally:
|
||||
override_db.close()
|
||||
|
||||
def override_current_user() -> CurrentUser:
|
||||
return CurrentUser(id=user_id, username=username, is_admin=False)
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
app.dependency_overrides[get_current_user] = override_current_user
|
||||
|
||||
token = current_session_id.set("api:task4-regression")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_process_message(message, session_id, project_id, model_id):
|
||||
captured["message"] = message
|
||||
captured["session_id"] = session_id
|
||||
captured["project_id"] = project_id
|
||||
captured["model_id"] = model_id
|
||||
return "invoke-ok"
|
||||
|
||||
try:
|
||||
monkeypatch.setattr("app.tools.subagent.SessionLocal", testing_session_local)
|
||||
monkeypatch.setattr(
|
||||
"app.tools.subagent.session_alias_store.get_alias_meta",
|
||||
lambda _session_id: {"project_id": project_id},
|
||||
)
|
||||
monkeypatch.setattr("app.tools.subagent.get_llm_configs", lambda: [])
|
||||
monkeypatch.setattr("app.tools.subagent.get_active_llm_config", lambda: None)
|
||||
monkeypatch.setattr("app.tools.subagent.nanobot_service.process_message", fake_process_message)
|
||||
|
||||
client = TestClient(app)
|
||||
create_response = client.post(
|
||||
f"/api/v1/projects/{project_id}/subagents",
|
||||
json={
|
||||
"name": "task4-subagent",
|
||||
"description": "task4-desc",
|
||||
"instructions": "focus on regression",
|
||||
"model": "gpt-x",
|
||||
},
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
created = create_response.json()
|
||||
assert created["project_id"] == project_id
|
||||
assert created["name"] == "task4-subagent"
|
||||
|
||||
listed = asyncio.run(ListSubagentsTool().execute())
|
||||
listed_payload = json.loads(listed)
|
||||
assert len(listed_payload) == 1
|
||||
assert listed_payload[0]["name"] == "task4-subagent"
|
||||
assert listed_payload[0]["description"] == "task4-desc"
|
||||
|
||||
invoke_result = asyncio.run(
|
||||
InvokeSubagentTool().execute(
|
||||
subagent_name="task4-subagent",
|
||||
task="run regression task",
|
||||
)
|
||||
)
|
||||
assert "completed the task" in invoke_result
|
||||
assert "invoke-ok" in invoke_result
|
||||
assert captured["project_id"] == project_id
|
||||
assert captured["session_id"] == f"api:task4-regression:subagent:{created['id']}"
|
||||
assert "focus on regression" in str(captured["message"])
|
||||
finally:
|
||||
current_session_id.reset(token)
|
||||
app.dependency_overrides.clear()
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
engine.dispose()
|
||||
Reference in New Issue
Block a user