feat: 增强日历功能并集成 AgentScope 代理服务

This commit is contained in:
qzl
2026-03-11 15:28:29 +08:00
parent e55e445906
commit e20e7d2a02
85 changed files with 5175 additions and 885 deletions
@@ -0,0 +1,4 @@
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
from core.agentscope.runtime.react_runner import AgentScopeReActRunner
__all__ = ["AgentScopeRuntimeOrchestrator", "AgentScopeReActRunner"]
@@ -0,0 +1,73 @@
from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core.agent.domain.system_agent_config import SystemAgentLLMConfig
from models.llm import Llm
from models.llm_factory import LlmFactory
from models.system_agents import SystemAgents
@dataclass(frozen=True)
class RuntimeStageConfig:
stage: str
model_code: str
provider_name: str
llm_config: SystemAgentLLMConfig
_LEGACY_AGENT_TYPE_TO_STAGE: dict[str, str] = {
"INTENT_RECOGNITION": "intent",
"TASK_EXECUTION": "execution",
"RESULT_REPORTING": "report",
}
def _normalize_stage(raw_agent_type: str) -> str | None:
lowered = raw_agent_type.strip().lower()
if lowered in {"intent", "execution", "report"}:
return lowered
return _LEGACY_AGENT_TYPE_TO_STAGE.get(raw_agent_type.strip().upper())
async def load_runtime_stage_configs(
*, session: AsyncSession
) -> dict[str, RuntimeStageConfig]:
stmt = (
select(
SystemAgents.agent_type,
Llm.model_code,
LlmFactory.name,
SystemAgents.config,
)
.join(Llm, Llm.id == SystemAgents.llm_id)
.join(LlmFactory, LlmFactory.id == Llm.factory_id)
.where(SystemAgents.status == "active")
)
rows = (await session.execute(stmt)).all()
by_stage: dict[str, RuntimeStageConfig] = {}
for agent_type, model_code, provider_name, raw_config in rows:
stage = _normalize_stage(str(agent_type))
if stage is None:
continue
if stage in by_stage:
raise ValueError(f"duplicate active system agent config for stage: {stage}")
llm_config = SystemAgentLLMConfig.model_validate(raw_config or {})
by_stage[stage] = RuntimeStageConfig(
stage=stage,
model_code=str(model_code),
provider_name=str(provider_name),
llm_config=llm_config,
)
missing = [
stage for stage in ("intent", "execution", "report") if stage not in by_stage
]
if missing:
raise ValueError(
f"missing active system agent configs for stages: {','.join(missing)}"
)
return by_stage
@@ -0,0 +1,189 @@
from __future__ import annotations
from typing import Any, Awaitable, Callable
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from core.agent.domain.user_context import UserAgentContext
from core.agentscope.prompts import (
build_execution_user_prompt,
build_intent_user_prompt,
build_report_user_prompt,
build_system_prompt,
)
from core.agentscope.runtime.config_loader import (
RuntimeStageConfig,
load_runtime_stage_configs,
)
from core.agentscope.runtime.react_runner import AgentScopeReActRunner
from core.agentscope.schemas import (
ExecutionBatchOutput,
ExecutionTaskOutput,
IntentOutput,
ReportOutput,
RuntimeOutput,
)
from core.agentscope.tools.toolkit import build_stage_toolkit
def _tools_payload_from_schema(
schemas: list[dict[str, object]],
) -> list[dict[str, object]]:
payload: list[dict[str, object]] = []
for item in schemas:
function = item.get("function")
if not isinstance(function, dict):
continue
name = function.get("name")
if not isinstance(name, str) or not name:
continue
description = function.get("description")
parameters = function.get("parameters")
payload.append(
{
"name": name,
"description": description if isinstance(description, str) else "",
"parameters": (
parameters if isinstance(parameters, dict) else {"type": "object"}
),
}
)
return payload
class AgentScopeRuntimeOrchestrator:
_runner: Any
_config_loader: Callable[[AsyncSession], Awaitable[dict[str, RuntimeStageConfig]]]
def __init__(
self,
*,
runner: Any | None = None,
config_loader: Callable[
[AsyncSession], Awaitable[dict[str, RuntimeStageConfig]]
]
| None = None,
) -> None:
self._runner = runner or AgentScopeReActRunner()
if config_loader is not None:
self._config_loader = config_loader
else:
self._config_loader = self._default_config_loader
@staticmethod
async def _default_config_loader(
session: AsyncSession,
) -> dict[str, RuntimeStageConfig]:
return await load_runtime_stage_configs(session=session)
async def run(
self,
*,
session: AsyncSession,
owner_id: UUID,
user_token: str,
user_context: UserAgentContext,
user_input: str | list[dict[str, Any]],
) -> RuntimeOutput:
stage_config = await self._config_loader(session)
intent_toolkit = build_stage_toolkit(
stage="intent",
session=session,
owner_id=owner_id,
user_token=user_token,
enable_hitl=False,
)
intent_tools_schema = intent_toolkit.get_json_schemas()
intent_prompt = build_system_prompt(
stage="intent",
user_context=user_context,
tools=_tools_payload_from_schema(intent_tools_schema),
)
intent_payload = await self._runner.run_json_stage(
stage_config=stage_config["intent"],
agent_name="intent-agent",
system_prompt=intent_prompt,
user_prompt=build_intent_user_prompt(user_input=user_input),
toolkit=intent_toolkit,
)
intent_output = IntentOutput.model_validate(intent_payload)
execution_output: ExecutionBatchOutput | None = None
if intent_output.route == "TASK_EXECUTION":
execution_toolkit = build_stage_toolkit(
stage="execution",
session=session,
owner_id=owner_id,
user_token=user_token,
enable_hitl=True,
)
execution_tools_schema = execution_toolkit.get_json_schemas()
execution_prompt = build_system_prompt(
stage="execution",
user_context=user_context,
tools=_tools_payload_from_schema(execution_tools_schema),
)
task_results: list[ExecutionTaskOutput] = []
for task in intent_output.tasks:
task_payload = await self._runner.run_json_stage(
stage_config=stage_config["execution"],
agent_name="execution-agent",
system_prompt=execution_prompt,
user_prompt=build_execution_user_prompt(
task_id=task.task_id,
task_title=task.title,
task_objective=task.objective,
user_input=user_input,
intent_summary=intent_output.intent_summary,
),
toolkit=execution_toolkit,
)
if "task_id" not in task_payload:
task_payload["task_id"] = task.task_id
task_results.append(ExecutionTaskOutput.model_validate(task_payload))
statuses = {item.status for item in task_results}
if statuses == {"SUCCESS"}:
overall_status = "SUCCESS"
elif "FAILED" in statuses:
overall_status = "PARTIAL" if "SUCCESS" in statuses else "FAILED"
else:
overall_status = "PARTIAL"
execution_output = ExecutionBatchOutput(
task_results=task_results,
overall_status=overall_status,
aggregate_summary="; ".join(
item.execution_summary for item in task_results
),
)
report_prompt = build_system_prompt(
stage="report",
user_context=user_context,
tools=[],
)
report_payload = await self._runner.run_json_stage(
stage_config=stage_config["report"],
agent_name="report-agent",
system_prompt=report_prompt,
user_prompt=build_report_user_prompt(
user_input=user_input,
intent_payload=intent_output.model_dump(mode="json"),
execution_payload=(
execution_output.model_dump(mode="json")
if execution_output
else None
),
),
toolkit=None,
)
report_output = ReportOutput.model_validate(report_payload)
return RuntimeOutput(
intent=intent_output,
execution=execution_output,
report=report_output,
)
@@ -0,0 +1,98 @@
from __future__ import annotations
import json
from typing import Any, cast
from core.agentscope.runtime.config_loader import RuntimeStageConfig
from core.config.settings import config
from core.logging import get_logger
logger = get_logger("core.agentscope.runtime.react_runner")
def _to_litellm_model(*, provider_name: str, model_code: str) -> str:
normalized_model = model_code.strip()
if "/" in normalized_model:
return normalized_model
return f"{provider_name.strip().lower()}/{normalized_model}"
def _parse_json_text(raw_text: str) -> dict[str, Any]:
text = raw_text.strip()
if text.startswith("```"):
text = text.strip("`")
if text.startswith("json"):
text = text[4:].strip()
parsed = json.loads(text)
if not isinstance(parsed, dict):
raise ValueError("model output must be a JSON object")
return cast(dict[str, Any], parsed)
class AgentScopeReActRunner:
def _build_model(self, *, stage_config: RuntimeStageConfig) -> Any:
from agentscope.model import OpenAIChatModel
from agentscope.types import JSONSerializableObject
generate_kwargs: dict[str, JSONSerializableObject] = {
"response_format": {"type": "json_object"},
}
if stage_config.llm_config.temperature is not None:
generate_kwargs["temperature"] = stage_config.llm_config.temperature
if stage_config.llm_config.max_tokens is not None:
generate_kwargs["max_tokens"] = stage_config.llm_config.max_tokens
if stage_config.llm_config.timeout_seconds is not None:
generate_kwargs["timeout"] = stage_config.llm_config.timeout_seconds
return OpenAIChatModel(
model_name=_to_litellm_model(
provider_name=stage_config.provider_name,
model_code=stage_config.model_code,
),
api_key=config.litellm.api_key,
stream=False,
client_kwargs={"base_url": config.litellm.base_url},
generate_kwargs=cast(dict[str, JSONSerializableObject], generate_kwargs),
)
async def run_json_stage(
self,
*,
stage_config: RuntimeStageConfig,
agent_name: str,
system_prompt: str,
user_prompt: str,
toolkit: Any | None,
) -> dict[str, Any]:
from agentscope.agent import ReActAgent
from agentscope.formatter import OpenAIChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
agent = ReActAgent(
name=agent_name,
sys_prompt=system_prompt,
model=self._build_model(stage_config=stage_config),
formatter=OpenAIChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
max_iters=6,
)
try:
response = await agent(Msg(name="user", content=user_prompt, role="user"))
text_content = response.get_text_content() or "{}"
return _parse_json_text(text_content)
except json.JSONDecodeError as exc:
logger.exception(
"agentscope stage output is not valid json",
stage=stage_config.stage,
agent_name=agent_name,
)
raise RuntimeError("agent output format invalid") from exc
except Exception as exc:
logger.exception(
"agentscope stage execution failed",
stage=stage_config.stage,
agent_name=agent_name,
)
raise RuntimeError("agent execution failed") from exc