feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agent.domain.system_agent_config import SystemAgentLLMConfig
|
||||
from core.agent.domain.user_context import UserAgentContext, parse_profile_settings
|
||||
from core.agentscope.runtime.config_loader import RuntimeStageConfig
|
||||
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
|
||||
|
||||
|
||||
def _ctx() -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="alice",
|
||||
bio=None,
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _stage_config() -> dict[str, RuntimeStageConfig]:
|
||||
llm = SystemAgentLLMConfig(temperature=0.1, max_tokens=256, timeout_seconds=30)
|
||||
return {
|
||||
"intent": RuntimeStageConfig("intent", "qwen3.5-flash", "dashscope", llm),
|
||||
"execution": RuntimeStageConfig("execution", "deepseek-chat", "deepseek", llm),
|
||||
"report": RuntimeStageConfig("report", "deepseek-chat", "deepseek", llm),
|
||||
}
|
||||
|
||||
|
||||
class _FakeRunner:
|
||||
def __init__(self) -> None:
|
||||
self.intent_calls = 0
|
||||
self.execution_calls = 0
|
||||
self.report_calls = 0
|
||||
|
||||
async def run_json_stage(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: Any | None,
|
||||
) -> dict[str, Any]:
|
||||
del agent_name, system_prompt, user_prompt, toolkit
|
||||
if stage_config.stage == "intent":
|
||||
self.intent_calls += 1
|
||||
return {
|
||||
"route": "DIRECT_RESPONSE",
|
||||
"intent_summary": "直接问候",
|
||||
"direct_response": "你好",
|
||||
"tasks": [],
|
||||
"complexity": "simple",
|
||||
}
|
||||
self.report_calls += 1
|
||||
return {
|
||||
"assistant_text": "已完成",
|
||||
"response_metadata": {"source": "report-agent"},
|
||||
}
|
||||
|
||||
|
||||
class _ComplexRunner(_FakeRunner):
|
||||
async def run_json_stage(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: Any | None,
|
||||
) -> dict[str, Any]:
|
||||
del agent_name, system_prompt, user_prompt, toolkit
|
||||
if stage_config.stage == "intent":
|
||||
self.intent_calls += 1
|
||||
return {
|
||||
"route": "TASK_EXECUTION",
|
||||
"intent_summary": "需要写入日历",
|
||||
"direct_response": None,
|
||||
"tasks": [
|
||||
{"task_id": "t1", "title": "创建事件", "objective": "写入明天会议"}
|
||||
],
|
||||
"complexity": "complex",
|
||||
}
|
||||
if stage_config.stage == "execution":
|
||||
self.execution_calls += 1
|
||||
return {
|
||||
"task_id": "t1",
|
||||
"status": "SUCCESS",
|
||||
"execution_summary": "done",
|
||||
"execution_data": {},
|
||||
"user_feedback_needs": [],
|
||||
}
|
||||
self.report_calls += 1
|
||||
return {
|
||||
"assistant_text": "任务执行完成",
|
||||
"response_metadata": {"source": "report-agent"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_direct_response_skips_execution(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_runner = _FakeRunner()
|
||||
|
||||
async def _fake_config_loader(
|
||||
_session: AsyncSession,
|
||||
) -> dict[str, RuntimeStageConfig]:
|
||||
return _stage_config()
|
||||
|
||||
class _FakeToolkit:
|
||||
def get_json_schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar.read",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
async def call_tool_function(self, tool_call: dict[str, Any]):
|
||||
del tool_call
|
||||
if False:
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: _FakeToolkit(),
|
||||
)
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=fake_runner,
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
result = await orchestrator.run(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_ctx(),
|
||||
user_input="你好",
|
||||
)
|
||||
|
||||
assert result.intent.route == "DIRECT_RESPONSE"
|
||||
assert result.execution is None
|
||||
assert result.report.assistant_text == "已完成"
|
||||
assert fake_runner.execution_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_complex_route_runs_execution(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_runner = _ComplexRunner()
|
||||
|
||||
async def _fake_config_loader(
|
||||
_session: AsyncSession,
|
||||
) -> dict[str, RuntimeStageConfig]:
|
||||
return _stage_config()
|
||||
|
||||
class _FakeToolkit:
|
||||
def get_json_schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar.read",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar.write",
|
||||
"description": "write",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async def call_tool_function(self, tool_call: dict[str, Any]):
|
||||
del tool_call
|
||||
if False:
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: _FakeToolkit(),
|
||||
)
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=fake_runner,
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
result = await orchestrator.run(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_ctx(),
|
||||
user_input="帮我安排明天会议",
|
||||
)
|
||||
|
||||
assert result.intent.route == "TASK_EXECUTION"
|
||||
assert result.execution is not None
|
||||
assert result.execution.overall_status == "SUCCESS"
|
||||
assert fake_runner.execution_calls == 1
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from core.agent.domain.system_agent_config import SystemAgentLLMConfig
|
||||
from core.agentscope.runtime.config_loader import RuntimeStageConfig
|
||||
from core.agentscope.runtime.react_runner import (
|
||||
AgentScopeReActRunner,
|
||||
_parse_json_text,
|
||||
_to_litellm_model,
|
||||
)
|
||||
|
||||
|
||||
def _stage_config() -> RuntimeStageConfig:
|
||||
return RuntimeStageConfig(
|
||||
stage="intent",
|
||||
model_code="qwen3.5-flash",
|
||||
provider_name="dashscope",
|
||||
llm_config=SystemAgentLLMConfig(
|
||||
temperature=0.1, max_tokens=128, timeout_seconds=30
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_to_litellm_model_keeps_prefixed_model() -> None:
|
||||
assert (
|
||||
_to_litellm_model(provider_name="dashscope", model_code="openai/gpt-4o")
|
||||
== "openai/gpt-4o"
|
||||
)
|
||||
|
||||
|
||||
def test_to_litellm_model_builds_prefixed_model() -> None:
|
||||
assert (
|
||||
_to_litellm_model(provider_name="dashscope", model_code="qwen3.5-flash")
|
||||
== "dashscope/qwen3.5-flash"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_json_text_supports_fenced_json() -> None:
|
||||
parsed = _parse_json_text('```json\n{"route":"DIRECT_RESPONSE"}\n```')
|
||||
assert parsed["route"] == "DIRECT_RESPONSE"
|
||||
|
||||
|
||||
def test_parse_json_text_rejects_non_json() -> None:
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
_parse_json_text("not-json")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_json_stage_wraps_json_decode_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pytest.importorskip("agentscope")
|
||||
import agentscope.agent as agent_module
|
||||
import agentscope.formatter as formatter_module
|
||||
import agentscope.memory as memory_module
|
||||
|
||||
class _FakeAgent:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def __call__(self, _msg: object) -> object:
|
||||
return SimpleNamespace(get_text_content=lambda: "not-json")
|
||||
|
||||
monkeypatch.setattr(agent_module, "ReActAgent", _FakeAgent)
|
||||
monkeypatch.setattr(formatter_module, "OpenAIChatFormatter", lambda: object())
|
||||
monkeypatch.setattr(memory_module, "InMemoryMemory", lambda: object())
|
||||
|
||||
runner = AgentScopeReActRunner()
|
||||
monkeypatch.setattr(runner, "_build_model", lambda **_: object())
|
||||
|
||||
with pytest.raises(RuntimeError, match="agent output format invalid"):
|
||||
await runner.run_json_stage(
|
||||
stage_config=_stage_config(),
|
||||
agent_name="intent-agent",
|
||||
system_prompt="sys",
|
||||
user_prompt="user",
|
||||
toolkit=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_json_stage_wraps_runtime_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pytest.importorskip("agentscope")
|
||||
import agentscope.agent as agent_module
|
||||
import agentscope.formatter as formatter_module
|
||||
import agentscope.memory as memory_module
|
||||
|
||||
class _FakeAgent:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def __call__(self, _msg: object) -> object:
|
||||
raise ValueError("boom")
|
||||
|
||||
monkeypatch.setattr(agent_module, "ReActAgent", _FakeAgent)
|
||||
monkeypatch.setattr(formatter_module, "OpenAIChatFormatter", lambda: object())
|
||||
monkeypatch.setattr(memory_module, "InMemoryMemory", lambda: object())
|
||||
|
||||
runner = AgentScopeReActRunner()
|
||||
monkeypatch.setattr(runner, "_build_model", lambda **_: object())
|
||||
|
||||
with pytest.raises(RuntimeError, match="agent execution failed"):
|
||||
await runner.run_json_stage(
|
||||
stage_config=_stage_config(),
|
||||
agent_name="intent-agent",
|
||||
system_prompt="sys",
|
||||
user_prompt="user",
|
||||
toolkit=None,
|
||||
)
|
||||
Reference in New Issue
Block a user