refactor(backend): 更新 agent 服务和配置层
This commit is contained in:
@@ -160,10 +160,6 @@ class AgentRuntimeSettings(BaseModel):
|
|||||||
user_context_cache_prefix: str = "agent:user-context"
|
user_context_cache_prefix: str = "agent:user-context"
|
||||||
user_context_cache_ttl_seconds: int = Field(default=600, ge=60, le=86400)
|
user_context_cache_ttl_seconds: int = Field(default=600, ge=60, le=86400)
|
||||||
user_context_cache_max_turns: int = Field(default=6, ge=1, le=100)
|
user_context_cache_max_turns: int = Field(default=6, ge=1, le=100)
|
||||||
history_context_cache_prefix: str = "agent:history-context"
|
|
||||||
history_context_cache_ttl_seconds: int = Field(default=86400, ge=60, le=172800)
|
|
||||||
default_model_code: str = ""
|
|
||||||
streaming_enabled: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class LlmSettings(BaseModel):
|
class LlmSettings(BaseModel):
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
agents:
|
agents:
|
||||||
- agent_type: router
|
- agent_type: worker
|
||||||
|
llm_model_code: qwen3.5-35b-a3b
|
||||||
|
status: active
|
||||||
|
config:
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: null
|
||||||
|
timeout_seconds: 30
|
||||||
|
context_messages:
|
||||||
|
mode: number
|
||||||
|
count: 20
|
||||||
|
enabled_tool_groups:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
|
||||||
|
- agent_type: memory
|
||||||
llm_model_code: qwen3.5-flash
|
llm_model_code: qwen3.5-flash
|
||||||
status: active
|
status: active
|
||||||
config:
|
config:
|
||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
max_tokens: null
|
max_tokens: null
|
||||||
timeout_seconds: 30
|
timeout_seconds: 30
|
||||||
|
context_messages:
|
||||||
- agent_type: worker
|
mode: day
|
||||||
llm_model_code: deepseek-chat
|
count: 2
|
||||||
status: active
|
enabled_tool_groups:
|
||||||
config:
|
- read
|
||||||
temperature: 0.7
|
|
||||||
max_tokens: null
|
|
||||||
timeout_seconds: 30
|
|
||||||
|
|||||||
@@ -74,11 +74,37 @@ routes:
|
|||||||
auth_required: true
|
auth_required: true
|
||||||
path_params:
|
path_params:
|
||||||
- id
|
- id
|
||||||
|
- route_id: calendar.event_create
|
||||||
|
path: /calendar/events/new
|
||||||
|
description: Create page for one calendar event.
|
||||||
|
category: calendar
|
||||||
|
auth_required: true
|
||||||
|
query_params:
|
||||||
|
- date
|
||||||
|
- route_id: calendar.event_edit
|
||||||
|
path: /calendar/events/{id}/edit
|
||||||
|
description: Edit page for one calendar event.
|
||||||
|
category: calendar
|
||||||
|
auth_required: true
|
||||||
|
path_params:
|
||||||
|
- id
|
||||||
|
- route_id: calendar.event_share
|
||||||
|
path: /calendar/events/{id}/share
|
||||||
|
description: Share settings page for one calendar event.
|
||||||
|
category: calendar
|
||||||
|
auth_required: true
|
||||||
|
path_params:
|
||||||
|
- id
|
||||||
- route_id: todo.list
|
- route_id: todo.list
|
||||||
path: /todo
|
path: /todo
|
||||||
description: Todo quadrants and backlog overview.
|
description: Todo quadrants and backlog overview.
|
||||||
category: todo
|
category: todo
|
||||||
auth_required: true
|
auth_required: true
|
||||||
|
- route_id: todo.create
|
||||||
|
path: /todo/new
|
||||||
|
description: Create page for one todo item.
|
||||||
|
category: todo
|
||||||
|
auth_required: true
|
||||||
- route_id: todo.detail
|
- route_id: todo.detail
|
||||||
path: /todo/{id}
|
path: /todo/{id}
|
||||||
description: Detail page for one todo item.
|
description: Detail page for one todo item.
|
||||||
@@ -86,6 +112,13 @@ routes:
|
|||||||
auth_required: true
|
auth_required: true
|
||||||
path_params:
|
path_params:
|
||||||
- id
|
- id
|
||||||
|
- route_id: todo.edit
|
||||||
|
path: /todo/{id}/edit
|
||||||
|
description: Dedicated subpage for editing one todo item (not an in-page modal).
|
||||||
|
category: todo
|
||||||
|
auth_required: true
|
||||||
|
path_params:
|
||||||
|
- id
|
||||||
- route_id: settings.main
|
- route_id: settings.main
|
||||||
path: /settings
|
path: /settings
|
||||||
description: Settings hub page.
|
description: Settings hub page.
|
||||||
|
|||||||
@@ -3,17 +3,11 @@ from schemas.agent.forwarded_props import (
|
|||||||
parse_forwarded_props_client_time,
|
parse_forwarded_props_client_time,
|
||||||
)
|
)
|
||||||
from schemas.agent.runtime_models import (
|
from schemas.agent.runtime_models import (
|
||||||
|
AgentOutput,
|
||||||
ResultType,
|
ResultType,
|
||||||
RouterAgentOutput,
|
|
||||||
RouterUiDecision,
|
|
||||||
RunStatus,
|
RunStatus,
|
||||||
ToolAgentOutput,
|
ToolAgentOutput,
|
||||||
ToolStatus,
|
ToolStatus,
|
||||||
UiMode,
|
|
||||||
WorkerAgentOutput,
|
|
||||||
WorkerAgentOutputLite,
|
|
||||||
WorkerAgentOutputRich,
|
|
||||||
resolve_worker_output_model,
|
|
||||||
)
|
)
|
||||||
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
|
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
|
||||||
from schemas.agent.ui_hints import (
|
from schemas.agent.ui_hints import (
|
||||||
@@ -26,23 +20,17 @@ from schemas.agent.ui_hints import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AgentType",
|
"AgentType",
|
||||||
|
"AgentOutput",
|
||||||
"ClientTimeContext",
|
"ClientTimeContext",
|
||||||
"ResultType",
|
"ResultType",
|
||||||
"RouterAgentOutput",
|
|
||||||
"RouterUiDecision",
|
|
||||||
"RunStatus",
|
"RunStatus",
|
||||||
"SystemAgentLLMConfig",
|
"SystemAgentLLMConfig",
|
||||||
"ToolAgentOutput",
|
"ToolAgentOutput",
|
||||||
"ToolStatus",
|
"ToolStatus",
|
||||||
"UiMode",
|
|
||||||
"UiHintAction",
|
"UiHintAction",
|
||||||
"UiHintIntent",
|
"UiHintIntent",
|
||||||
"UiHintSection",
|
"UiHintSection",
|
||||||
"UiHintStatus",
|
"UiHintStatus",
|
||||||
"UiHintsPayload",
|
"UiHintsPayload",
|
||||||
"WorkerAgentOutputLite",
|
|
||||||
"WorkerAgentOutputRich",
|
|
||||||
"WorkerAgentOutput",
|
|
||||||
"resolve_worker_output_model",
|
|
||||||
"parse_forwarded_props_client_time",
|
"parse_forwarded_props_client_time",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,22 +8,6 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||||||
from schemas.agent.ui_hints import UiHintsPayload
|
from schemas.agent.ui_hints import UiHintsPayload
|
||||||
|
|
||||||
|
|
||||||
class TaskType(str, Enum):
|
|
||||||
KNOWLEDGE = "knowledge"
|
|
||||||
RECOMMENDATION = "recommendation"
|
|
||||||
PLANNING = "planning"
|
|
||||||
SCHEDULING = "scheduling"
|
|
||||||
REMINDER_MANAGEMENT = "reminder_management"
|
|
||||||
TODO_MANAGEMENT = "todo_management"
|
|
||||||
COMMUNICATION_DRAFTING = "communication_drafting"
|
|
||||||
INFORMATION_ORGANIZATION = "information_organization"
|
|
||||||
STATUS_TRACKING = "status_tracking"
|
|
||||||
TRANSACTION_ASSIST = "transaction_assist"
|
|
||||||
ACTION_EXECUTION = "action_execution"
|
|
||||||
TROUBLESHOOTING = "troubleshooting"
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
class ResultType(str, Enum):
|
class ResultType(str, Enum):
|
||||||
DIRECT_ANSWER = "direct_answer"
|
DIRECT_ANSWER = "direct_answer"
|
||||||
OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation"
|
OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation"
|
||||||
@@ -42,59 +26,6 @@ class ResultType(str, Enum):
|
|||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
class TaskTyping(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
primary: TaskType = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Primary task category. Choose the single category that best "
|
|
||||||
"represents the core user intent."
|
|
||||||
),
|
|
||||||
examples=["planning"],
|
|
||||||
)
|
|
||||||
secondary: list[TaskType] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description=(
|
|
||||||
"Secondary task categories. Keep only strongly relevant supporting "
|
|
||||||
"categories, up to 3."
|
|
||||||
),
|
|
||||||
examples=[["scheduling", "action_execution"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ResultTyping(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
primary: ResultType = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Primary output type. It should match the execution mode and user "
|
|
||||||
"expectation; avoid unknown whenever possible."
|
|
||||||
),
|
|
||||||
examples=["action_plan"],
|
|
||||||
)
|
|
||||||
secondary: list[ResultType] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description=(
|
|
||||||
"Secondary output types. Use for compatible alternative response "
|
|
||||||
"shapes, up to 3."
|
|
||||||
),
|
|
||||||
examples=[["todo_list", "summary"]],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExecutionMode(str, Enum):
|
|
||||||
ONESTEP = "onestep"
|
|
||||||
TOOL_ASSISTED = "tool_assisted"
|
|
||||||
MULTISTEP = "multistep"
|
|
||||||
|
|
||||||
|
|
||||||
class UiMode(str, Enum):
|
|
||||||
NONE = "none"
|
|
||||||
RICH = "rich"
|
|
||||||
|
|
||||||
|
|
||||||
class RunStatus(str, Enum):
|
class RunStatus(str, Enum):
|
||||||
SUCCESS = "success"
|
SUCCESS = "success"
|
||||||
PARTIAL_SUCCESS = "partial_success"
|
PARTIAL_SUCCESS = "partial_success"
|
||||||
@@ -107,276 +38,33 @@ class ToolStatus(str, Enum):
|
|||||||
PARTIAL = "partial"
|
PARTIAL = "partial"
|
||||||
|
|
||||||
|
|
||||||
class KeyEntity(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
name: str = Field(
|
|
||||||
...,
|
|
||||||
description="Entity name, such as meeting/contact/location/project.",
|
|
||||||
)
|
|
||||||
type: str = Field(
|
|
||||||
...,
|
|
||||||
description="Entity type label, such as person/date/location/task.",
|
|
||||||
)
|
|
||||||
value: str | None = Field(
|
|
||||||
default=None,
|
|
||||||
description="Normalized entity value. Keep null if normalization is uncertain.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConstraintItem(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
key: str = Field(
|
|
||||||
...,
|
|
||||||
description="Constraint key, such as deadline/budget/channel/privacy.",
|
|
||||||
)
|
|
||||||
value: str = Field(
|
|
||||||
...,
|
|
||||||
description="Constraint value in concise natural language or normalized form.",
|
|
||||||
)
|
|
||||||
required: bool = Field(
|
|
||||||
default=True,
|
|
||||||
description=(
|
|
||||||
"Whether this constraint is mandatory. True means execution cannot "
|
|
||||||
"proceed if violated."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NormalizedTaskInput(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
user_text: str = Field(
|
|
||||||
...,
|
|
||||||
description="Normalized core user request text.",
|
|
||||||
examples=["Reschedule tomorrow's 9am standup to 3pm and notify attendees."],
|
|
||||||
)
|
|
||||||
multimodal_summary: list[str] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description="Key points extracted by router from images or attachments.",
|
|
||||||
examples=[["Screenshot shows a calendar conflict at 09:00."]],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouterUiDecision(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
ui_mode: UiMode = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"UI rendering mode decision for downstream worker schema selection. "
|
|
||||||
"Use 'none' when plain text response is sufficient; use 'rich' "
|
|
||||||
"when structured UI hints are beneficial."
|
|
||||||
),
|
|
||||||
examples=["none", "rich"],
|
|
||||||
)
|
|
||||||
ui_decision_reason: str = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Brief reason for UI mode decision, focused on user intent and "
|
|
||||||
"information complexity."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
"User asked a simple factual question; plain text is sufficient.",
|
|
||||||
"User needs actionable options and status blocks; rich UI helps scanning.",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouterAgentOutput(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
normalized_task_input: NormalizedTaskInput = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Normalized task input for routing. Preserve user intent faithfully "
|
|
||||||
"without adding or dropping critical semantics."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
{
|
|
||||||
"user_text": "Reschedule tomorrow's 9am standup to 3pm and notify attendees.",
|
|
||||||
"multimodal_summary": ["Calendar screenshot indicates 09:00 conflict."],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
key_entities: list[KeyEntity] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description=(
|
|
||||||
"Key entities directly relevant to task execution. Return an empty "
|
|
||||||
"list when confidence is low."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
[
|
|
||||||
{"name": "standup", "type": "event", "value": "team-standup"},
|
|
||||||
{
|
|
||||||
"name": "tomorrow 9am",
|
|
||||||
"type": "datetime",
|
|
||||||
"value": "2026-03-14T09:00:00+08:00",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "3pm",
|
|
||||||
"type": "datetime",
|
|
||||||
"value": "2026-03-14T15:00:00+08:00",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
constraints: list[ConstraintItem] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description=(
|
|
||||||
"Execution constraints, including explicit constraints and "
|
|
||||||
"high-confidence inferred constraints."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
[
|
|
||||||
{"key": "must_notify_attendees", "value": "true", "required": True},
|
|
||||||
{"key": "timezone", "value": "Asia/Shanghai", "required": True},
|
|
||||||
]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
task_typing: TaskTyping = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Task typing result used by downstream agents for strategy and "
|
|
||||||
"capability boundaries."
|
|
||||||
),
|
|
||||||
examples=[{"primary": "scheduling", "secondary": ["communication_drafting"]}],
|
|
||||||
)
|
|
||||||
execution_mode: ExecutionMode = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Recommended execution mode: onestep/tool_assisted/multistep. It "
|
|
||||||
"must be feasible under current context and capabilities."
|
|
||||||
),
|
|
||||||
examples=["tool_assisted"],
|
|
||||||
)
|
|
||||||
result_typing: ResultTyping = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Expected result typing used to constrain downstream output "
|
|
||||||
"structure and expression."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
{
|
|
||||||
"primary": "execution_report",
|
|
||||||
"secondary": ["summary", "options_with_recommendation"],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
ui: RouterUiDecision = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Router decision on whether downstream worker should use rich UI "
|
|
||||||
"schema or lightweight text-only schema."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
{
|
|
||||||
"ui_mode": "rich",
|
|
||||||
"ui_decision_reason": "The request includes multiple actionable outcomes and benefits from structured blocks.",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorInfo(BaseModel):
|
class ErrorInfo(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
code: str = Field(
|
code: str = Field(..., description="Stable error code for programmatic handling.")
|
||||||
...,
|
message: str = Field(..., description="Human-readable error message.")
|
||||||
description="Stable error code for programmatic handling and analytics.",
|
retryable: bool = Field(default=False)
|
||||||
)
|
details: dict[str, Any] | None = Field(default=None)
|
||||||
message: str = Field(
|
|
||||||
...,
|
|
||||||
description="Human-readable error message for user or upstream agent.",
|
|
||||||
)
|
|
||||||
retryable: bool = Field(
|
|
||||||
default=False,
|
|
||||||
description="Whether retrying can likely resolve this error.",
|
|
||||||
)
|
|
||||||
details: dict[str, Any] | None = Field(
|
|
||||||
default=None,
|
|
||||||
description="Diagnostic details. Must not contain sensitive data or secrets.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ToolAgentOutput(BaseModel):
|
class ToolAgentOutput(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
tool_name: str = Field(..., description="Invoked tool name.")
|
tool_name: str
|
||||||
tool_call_id: str = Field(
|
tool_call_id: str
|
||||||
..., description="Tool call identifier for this invocation."
|
tool_call_args: dict[str, Any] | None = None
|
||||||
)
|
status: ToolStatus
|
||||||
tool_call_args: dict[str, Any] | None = Field(
|
result: str
|
||||||
default=None,
|
error: ErrorInfo | None = None
|
||||||
description="Snapshot of tool call arguments for traceability and debugging.",
|
|
||||||
)
|
|
||||||
status: ToolStatus = Field(..., description="Tool execution status.")
|
|
||||||
result: str = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Compact machine-oriented tool result. Keep it short but include "
|
|
||||||
"action-critical facts (ids/status/counts) for downstream agent steps."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
error: ErrorInfo | None = Field(
|
|
||||||
default=None, description="Tool execution error details."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkerAgentOutputLite(BaseModel):
|
class AgentOutput(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
status: RunStatus = Field(
|
status: RunStatus = Field(default=RunStatus.SUCCESS)
|
||||||
default=RunStatus.SUCCESS,
|
answer: str
|
||||||
description="Worker execution status: success/partial_success/failed.",
|
key_points: list[str] = Field(default_factory=list)
|
||||||
examples=["success"],
|
result_type: ResultType = Field(default=ResultType.UNKNOWN)
|
||||||
)
|
suggested_actions: list[str] = Field(default_factory=list)
|
||||||
answer: str = Field(
|
error: ErrorInfo | None = None
|
||||||
...,
|
ui_hints: UiHintsPayload | None = None
|
||||||
description=(
|
|
||||||
"Primary user-facing response text. Lead with conclusion, then "
|
|
||||||
"include only necessary details."
|
|
||||||
),
|
|
||||||
examples=[
|
|
||||||
"Done. I moved the standup to 3:00 PM tomorrow and prepared attendee notifications."
|
|
||||||
],
|
|
||||||
)
|
|
||||||
key_points: list[str] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description="Key point summary, recommended 0-5 items, one sentence each.",
|
|
||||||
examples=[["Original slot conflicted at 09:00.", "New slot set to 15:00."]],
|
|
||||||
)
|
|
||||||
result_type: ResultType = Field(
|
|
||||||
default=ResultType.UNKNOWN,
|
|
||||||
description="Structured result type of this response. Avoid unknown whenever possible.",
|
|
||||||
examples=["execution_report"],
|
|
||||||
)
|
|
||||||
suggested_actions: list[str] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
description="Suggested next actions, 0-3 items, actionable and relevant.",
|
|
||||||
examples=[["Review attendee RSVP status after notifications are sent."]],
|
|
||||||
)
|
|
||||||
error: ErrorInfo | None = Field(
|
|
||||||
default=None,
|
|
||||||
description="Error information for failed or partially failed runs; null on success.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkerAgentOutputRich(WorkerAgentOutputLite):
|
|
||||||
ui_hints: UiHintsPayload | None = Field(
|
|
||||||
default=None,
|
|
||||||
description=(
|
|
||||||
"Optional expressive UI semantic annotations. Focus on information "
|
|
||||||
"and interaction intent, not concrete visual styling instructions."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_worker_output_model(ui_mode: UiMode) -> type[WorkerAgentOutputLite]:
|
|
||||||
if ui_mode == UiMode.RICH:
|
|
||||||
return WorkerAgentOutputRich
|
|
||||||
return WorkerAgentOutputLite
|
|
||||||
|
|||||||
@@ -2,15 +2,51 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
from core.agentscope.tools.tool_config import ToolGroup
|
||||||
|
|
||||||
|
|
||||||
class AgentType(str, Enum):
|
class AgentType(str, Enum):
|
||||||
ROUTER = "router"
|
|
||||||
WORKER = "worker"
|
WORKER = "worker"
|
||||||
|
MEMORY = "memory"
|
||||||
|
|
||||||
|
|
||||||
|
class ContextBuildStrategy(str, Enum):
|
||||||
|
DAY = "day"
|
||||||
|
NUMBER = "number"
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMessagesConfig(BaseModel):
|
||||||
|
mode: ContextBuildStrategy = ContextBuildStrategy.NUMBER
|
||||||
|
count: int = Field(default=20, ge=1, le=200)
|
||||||
|
|
||||||
|
|
||||||
class SystemAgentLLMConfig(BaseModel):
|
class SystemAgentLLMConfig(BaseModel):
|
||||||
temperature: float | None = Field(default=None, ge=0.0, le=2.0)
|
temperature: float | None = Field(default=None, ge=0.0, le=2.0)
|
||||||
max_tokens: int | None = Field(default=None, ge=1)
|
max_tokens: int | None = Field(default=None, ge=1)
|
||||||
timeout_seconds: float | None = Field(default=30.0, gt=0.0, le=300.0)
|
timeout_seconds: float | None = Field(default=30.0, gt=0.0, le=300.0)
|
||||||
|
context_messages: ContextMessagesConfig = Field(
|
||||||
|
default_factory=ContextMessagesConfig
|
||||||
|
)
|
||||||
|
enabled_tool_groups: list[ToolGroup] = Field(default_factory=list, max_length=8)
|
||||||
|
|
||||||
|
@field_validator("enabled_tool_groups", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _normalize_enabled_tool_groups(cls, value: object) -> list[ToolGroup]:
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise ValueError("enabled_tool_groups must be a list")
|
||||||
|
normalized: list[ToolGroup] = []
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, ToolGroup):
|
||||||
|
group = item
|
||||||
|
else:
|
||||||
|
raw_group = str(item or "").strip().lower()
|
||||||
|
if not raw_group:
|
||||||
|
continue
|
||||||
|
group = ToolGroup(raw_group)
|
||||||
|
if group not in normalized:
|
||||||
|
normalized.append(group)
|
||||||
|
return normalized
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from schemas.agent.runtime_models import RouterAgentOutput, WorkerAgentOutputRich
|
from schemas.agent.runtime_models import AgentOutput
|
||||||
|
|
||||||
from ..agent import AgentType, ToolAgentOutput
|
from ..agent import AgentType, ToolAgentOutput
|
||||||
|
|
||||||
@@ -24,9 +24,8 @@ class AgentChatMessageMetadata(BaseModel):
|
|||||||
run_id: str
|
run_id: str
|
||||||
agent_type: AgentType | None = None
|
agent_type: AgentType | None = None
|
||||||
user_message_attachments: list[UserMessageAttachment] | None = None
|
user_message_attachments: list[UserMessageAttachment] | None = None
|
||||||
router_agent_output: RouterAgentOutput | None = None
|
|
||||||
tool_agent_output: ToolAgentOutput | None = None
|
tool_agent_output: ToolAgentOutput | None = None
|
||||||
worker_agent_output: WorkerAgentOutputRich | None = None
|
agent_output: AgentOutput | None = None
|
||||||
|
|
||||||
|
|
||||||
class AgentChatMessage(BaseModel):
|
class AgentChatMessage(BaseModel):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
|
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
|
||||||
from models.agent_chat_session import AgentChatSession
|
from models.agent_chat_session import AgentChatSession
|
||||||
|
from models.system_agents import SystemAgents
|
||||||
from schemas.messages.chat_message import (
|
from schemas.messages.chat_message import (
|
||||||
AgentChatMessage as AgentChatMessageSchema,
|
AgentChatMessage as AgentChatMessageSchema,
|
||||||
AgentChatMessageMetadata,
|
AgentChatMessageMetadata,
|
||||||
@@ -194,6 +195,45 @@ class AgentRepository:
|
|||||||
"messages": snapshot_messages,
|
"messages": snapshot_messages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def get_recent_messages_by_user_window(
|
||||||
|
self, *, session_id: str, user_message_limit: int
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
try:
|
||||||
|
session_uuid = UUID(session_id)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail="Invalid session_id") from exc
|
||||||
|
|
||||||
|
safe_user_limit = max(int(user_message_limit), 1)
|
||||||
|
message_stmt = (
|
||||||
|
select(AgentChatMessage)
|
||||||
|
.where(AgentChatMessage.session_id == session_uuid)
|
||||||
|
.where(AgentChatMessage.deleted_at.is_(None))
|
||||||
|
.order_by(AgentChatMessage.seq.desc())
|
||||||
|
)
|
||||||
|
messages_desc = (await self._session.execute(message_stmt)).scalars().all()
|
||||||
|
if not messages_desc:
|
||||||
|
return []
|
||||||
|
|
||||||
|
selected_desc: list[AgentChatMessage] = []
|
||||||
|
user_count = 0
|
||||||
|
for message in messages_desc:
|
||||||
|
selected_desc.append(message)
|
||||||
|
role = (
|
||||||
|
message.role.value
|
||||||
|
if isinstance(message.role, AgentChatMessageRole)
|
||||||
|
else str(message.role)
|
||||||
|
)
|
||||||
|
if role == AgentChatMessageRole.USER.value:
|
||||||
|
user_count += 1
|
||||||
|
if user_count >= safe_user_limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
selected = list(reversed(selected_desc))
|
||||||
|
snapshot_messages: list[dict[str, object]] = []
|
||||||
|
for message in selected:
|
||||||
|
snapshot_messages.append(await self._to_snapshot_message(message))
|
||||||
|
return snapshot_messages
|
||||||
|
|
||||||
async def get_latest_session_id_for_user(self, *, user_id: str) -> str | None:
|
async def get_latest_session_id_for_user(self, *, user_id: str) -> str | None:
|
||||||
try:
|
try:
|
||||||
user_uuid = UUID(user_id)
|
user_uuid = UUID(user_id)
|
||||||
@@ -211,6 +251,23 @@ class AgentRepository:
|
|||||||
return None
|
return None
|
||||||
return str(latest_id)
|
return str(latest_id)
|
||||||
|
|
||||||
|
async def get_system_agent_config(
|
||||||
|
self, *, agent_type: str
|
||||||
|
) -> dict[str, object] | None:
|
||||||
|
normalized_type = agent_type.strip().lower()
|
||||||
|
if not normalized_type:
|
||||||
|
return None
|
||||||
|
stmt = select(SystemAgents).where(SystemAgents.agent_type == normalized_type)
|
||||||
|
row = (await self._session.execute(stmt)).scalar_one_or_none()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
config_payload = row.config if isinstance(row.config, dict) else {}
|
||||||
|
return {
|
||||||
|
"agent_type": normalized_type,
|
||||||
|
"status": str(row.status),
|
||||||
|
"config": config_payload,
|
||||||
|
}
|
||||||
|
|
||||||
async def _to_snapshot_message(
|
async def _to_snapshot_message(
|
||||||
self, message: AgentChatMessage
|
self, message: AgentChatMessage
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
|
|||||||
@@ -168,10 +168,18 @@ class AgentService:
|
|||||||
)
|
)
|
||||||
await self._repository.commit()
|
await self._repository.commit()
|
||||||
|
|
||||||
|
forwarded_props = getattr(run_input, "forwarded_props", None)
|
||||||
|
system_agent_mode = "worker"
|
||||||
|
if isinstance(forwarded_props, dict):
|
||||||
|
raw_mode = forwarded_props.get("system_agent_mode")
|
||||||
|
if isinstance(raw_mode, str) and raw_mode.strip():
|
||||||
|
system_agent_mode = raw_mode.strip().lower()
|
||||||
|
|
||||||
task_id = await self._queue.enqueue(
|
task_id = await self._queue.enqueue(
|
||||||
command={
|
command={
|
||||||
"command": "run",
|
"command": "run",
|
||||||
"owner_id": str(current_user.id),
|
"owner_id": str(current_user.id),
|
||||||
|
"system_agent_mode": system_agent_mode,
|
||||||
"run_input": run_input.model_dump(
|
"run_input": run_input.model_dump(
|
||||||
mode="json", by_alias=True, exclude_none=True
|
mode="json", by_alias=True, exclude_none=True
|
||||||
),
|
),
|
||||||
@@ -185,45 +193,6 @@ class AgentService:
|
|||||||
created=created,
|
created=created,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def load_agent_input_messages(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
thread_id: str,
|
|
||||||
) -> dict[str, object] | None:
|
|
||||||
"""Load recent messages for runtime agent input.
|
|
||||||
|
|
||||||
Returns messages from today and yesterday (if exists).
|
|
||||||
"""
|
|
||||||
today = await self._repository.get_history_day(
|
|
||||||
session_id=thread_id,
|
|
||||||
before=None,
|
|
||||||
)
|
|
||||||
if not today:
|
|
||||||
return None
|
|
||||||
|
|
||||||
yesterday = await self._repository.get_history_day(
|
|
||||||
session_id=thread_id,
|
|
||||||
before=self._parse_history_day(today.get("day")),
|
|
||||||
)
|
|
||||||
|
|
||||||
messages: list[dict[str, object]] = []
|
|
||||||
if yesterday and yesterday.get("messages"):
|
|
||||||
messages.extend(yesterday["messages"]) # type: ignore
|
|
||||||
if today.get("messages"):
|
|
||||||
messages.extend(today["messages"]) # type: ignore
|
|
||||||
|
|
||||||
return {"messages": messages}
|
|
||||||
|
|
||||||
def _parse_history_day(self, value: object) -> date | None:
|
|
||||||
if isinstance(value, date):
|
|
||||||
return value
|
|
||||||
if isinstance(value, str):
|
|
||||||
try:
|
|
||||||
return date.fromisoformat(value)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def _prepare_user_message(
|
async def _prepare_user_message(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def convert_message_to_history(
|
|||||||
|
|
||||||
转换规则:
|
转换规则:
|
||||||
- role=user: 读取 metadata.user_message_attachments,转换为 attachments[]
|
- role=user: 读取 metadata.user_message_attachments,转换为 attachments[]
|
||||||
- role=assistant: 读取 metadata.worker_agent_output.ui_hints,编译成 ui_schema
|
- role=assistant: 读取 metadata.agent_output.ui_hints,编译成 ui_schema
|
||||||
"""
|
"""
|
||||||
role = message.role
|
role = message.role
|
||||||
content = message.content
|
content = message.content
|
||||||
@@ -91,34 +91,31 @@ def _convert_user_attachments(
|
|||||||
def _compile_worker_ui_hints(
|
def _compile_worker_ui_hints(
|
||||||
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
|
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""编译 assistant 消息的 worker ui_hints"""
|
"""编译 assistant 消息的 agent ui_hints"""
|
||||||
if not metadata:
|
if not metadata:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(metadata, AgentChatMessageMetadata):
|
if isinstance(metadata, AgentChatMessageMetadata):
|
||||||
worker_output = metadata.worker_agent_output
|
agent_output = metadata.agent_output
|
||||||
else:
|
else:
|
||||||
worker_output_data = metadata.get("worker_agent_output")
|
agent_output_data = metadata.get("agent_output")
|
||||||
if not worker_output_data:
|
if not agent_output_data:
|
||||||
return None
|
return None
|
||||||
if isinstance(worker_output_data, dict):
|
if isinstance(agent_output_data, dict):
|
||||||
raw_ui_schema = worker_output_data.get("ui_schema")
|
raw_ui_schema = agent_output_data.get("ui_schema")
|
||||||
if isinstance(raw_ui_schema, dict):
|
if isinstance(raw_ui_schema, dict):
|
||||||
return raw_ui_schema
|
return raw_ui_schema
|
||||||
legacy_ui_schema = worker_output_data.get("uiSchema")
|
from schemas.agent.runtime_models import AgentOutput
|
||||||
if isinstance(legacy_ui_schema, dict):
|
|
||||||
return legacy_ui_schema
|
|
||||||
from schemas.agent.runtime_models import WorkerAgentOutputRich
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
worker_output = WorkerAgentOutputRich.model_validate(worker_output_data)
|
agent_output = AgentOutput.model_validate(agent_output_data)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not worker_output:
|
if not agent_output:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ui_hints = worker_output.ui_hints
|
ui_hints = agent_output.ui_hints
|
||||||
if not ui_hints:
|
if not ui_hints:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user