feat(agentscope): add memory system and automation job support

- Add consumer_registry and pipeline_registry for runtime orchestration
- Add Visibility schema for message filtering
- Add PipelineSpec for agent pipeline configuration
- Add automation job models and configuration
- Remove memory_prompt.py (consolidated into memory system)
- Update runtime components: context_loader, context_service, orchestrator, runner, tasks
- Update toolkit: tool_config, tool_middleware, custom tools (calendar, user_lookup)
- Add auth_helpers and calendar_domain utilities
- Add system_agents.yaml configuration
This commit is contained in:
qzl
2026-03-19 18:42:35 +08:00
parent 0661016827
commit 0abf51e837
55 changed files with 2172 additions and 1233 deletions
@@ -0,0 +1,44 @@
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class AgentConsumerBinding(BaseModel):
model_config = ConfigDict(extra="forbid")
agent_type: str = Field(..., min_length=1, max_length=64)
bit: int = Field(..., ge=16, le=63)
@field_validator("agent_type")
@classmethod
def _normalize_agent_type(cls, value: str) -> str:
normalized = value.strip().lower()
if not normalized:
raise ValueError("agent_type must not be empty")
return normalized
class ConsumerRegistry(BaseModel):
model_config = ConfigDict(extra="forbid")
bindings: list[AgentConsumerBinding] = Field(default_factory=list)
@model_validator(mode="after")
def _validate_unique_bindings(self) -> "ConsumerRegistry":
by_agent: set[str] = set()
by_bit: set[int] = set()
for item in self.bindings:
if item.agent_type in by_agent:
raise ValueError(f"duplicate agent_type binding: {item.agent_type}")
if item.bit in by_bit:
raise ValueError(f"duplicate visibility bit binding: {item.bit}")
by_agent.add(item.agent_type)
by_bit.add(item.bit)
return self
def resolve_agent_bit(self, *, agent_type: str) -> int:
target = agent_type.strip().lower()
for item in self.bindings:
if item.agent_type == target:
return item.bit
raise ValueError(f"agent visibility bit not configured: {target}")
@@ -0,0 +1,63 @@
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, field_validator
class ExecutorKind(str, Enum):
SINGLE_SHOT = "single_shot"
REACT = "react"
class ContextWindowMode(str, Enum):
DAY = "day"
NUMBER = "number"
class ContextPolicy(BaseModel):
model_config = ConfigDict(extra="forbid")
consumer_agent_type: str = Field(..., min_length=1, max_length=64)
window_mode: ContextWindowMode = ContextWindowMode.NUMBER
count: int = Field(default=20, ge=1, le=200)
@field_validator("consumer_agent_type")
@classmethod
def _normalize_consumer_agent_type(cls, value: str) -> str:
normalized = value.strip().lower()
if not normalized:
raise ValueError("consumer_agent_type must not be empty")
return normalized
class StageSpec(BaseModel):
model_config = ConfigDict(extra="forbid")
stage_name: str = Field(..., min_length=1, max_length=64)
executor_kind: ExecutorKind
default_visibility_mask: int = Field(..., ge=0, le=(1 << 63) - 1)
context_policy: ContextPolicy
@field_validator("stage_name")
@classmethod
def _normalize_stage_name(cls, value: str) -> str:
normalized = value.strip().lower()
if not normalized:
raise ValueError("stage_name must not be empty")
return normalized
class PipelineSpec(BaseModel):
model_config = ConfigDict(extra="forbid")
mode: str = Field(..., min_length=1, max_length=64)
stages: list[StageSpec] = Field(..., min_length=1)
@field_validator("mode")
@classmethod
def _normalize_mode(cls, value: str) -> str:
normalized = value.strip().lower()
if not normalized:
raise ValueError("mode must not be empty")
return normalized
+50
View File
@@ -0,0 +1,50 @@
from __future__ import annotations
from enum import IntEnum
from pydantic import BaseModel, ConfigDict, Field, field_validator
class SystemVisibilityBit(IntEnum):
UI_HISTORY = 0
UI_REALTIME = 1
class VisibilityMask(BaseModel):
model_config = ConfigDict(extra="forbid")
value: int = Field(..., ge=0, le=(1 << 63) - 1)
@classmethod
def from_bits(cls, *, bits: list[int]) -> "VisibilityMask":
mask = 0
for bit in bits:
validate_visibility_bit(bit=bit)
mask |= 1 << bit
return cls(value=mask)
def contains(self, *, bit: int) -> bool:
validate_visibility_bit(bit=bit)
return bool(self.value & (1 << bit))
class VisibilityBitRef(BaseModel):
model_config = ConfigDict(extra="forbid")
bit: int = Field(..., ge=0, le=63)
@field_validator("bit")
@classmethod
def _validate_bit(cls, value: int) -> int:
validate_visibility_bit(bit=value)
return value
def validate_visibility_bit(*, bit: int) -> None:
if bit < 0 or bit > 63:
raise ValueError("visibility bit must be in range [0, 63]")
def bit_mask(*, bit: int) -> int:
validate_visibility_bit(bit=bit)
return 1 << bit
@@ -0,0 +1,20 @@
from schemas.automation.config import (
AutomationAgentType,
AutomationContextSource,
AutomationContextWindowMode,
AutomationJobConfig,
AutomationMemoryContextConfig,
default_memory_job_config,
)
from schemas.automation.scheduler import DueAutomationJob, SchedulerDispatchCommand
__all__ = [
"AutomationAgentType",
"AutomationContextSource",
"AutomationContextWindowMode",
"AutomationJobConfig",
"AutomationMemoryContextConfig",
"default_memory_job_config",
"DueAutomationJob",
"SchedulerDispatchCommand",
]
+62
View File
@@ -0,0 +1,62 @@
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, field_validator
from core.agentscope.tools.tool_config import AgentTool
class AutomationAgentType(str, Enum):
MEMORY = "memory"
class AutomationContextSource(str, Enum):
LATEST_CHAT = "latest_chat"
class AutomationContextWindowMode(str, Enum):
DAY = "day"
NUMBER = "number"
class AutomationMemoryContextConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
source: AutomationContextSource = AutomationContextSource.LATEST_CHAT
window_mode: AutomationContextWindowMode = AutomationContextWindowMode.DAY
window_count: int = Field(default=2, ge=1, le=200)
class AutomationJobConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
agent_type: AutomationAgentType = AutomationAgentType.MEMORY
model_code: str = Field(default="qwen3.5-flash", min_length=1, max_length=64)
enabled_tools: list[AgentTool] = Field(default_factory=list, max_length=32)
input_template: str = Field(..., min_length=1, max_length=4000)
context: AutomationMemoryContextConfig = Field(
default_factory=AutomationMemoryContextConfig
)
@field_validator("model_code")
@classmethod
def _validate_model_code(cls, value: str) -> str:
normalized = value.strip()
if normalized != "qwen3.5-flash":
raise ValueError("model_code must be qwen3.5-flash")
return normalized
def default_memory_job_config() -> AutomationJobConfig:
return AutomationJobConfig(
agent_type=AutomationAgentType.MEMORY,
model_code="qwen3.5-flash",
enabled_tools=[AgentTool.CALENDAR_READ, AgentTool.USER_LOOKUP],
input_template="请基于最近聊天上下文生成一段可执行的记忆总结与建议。",
context=AutomationMemoryContextConfig(
source=AutomationContextSource.LATEST_CHAT,
window_mode=AutomationContextWindowMode.DAY,
window_count=2,
),
)
@@ -0,0 +1,28 @@
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from models.automation_jobs import ScheduleType
class DueAutomationJob(BaseModel):
model_config = ConfigDict(extra="forbid")
id: UUID
owner_id: UUID
schedule_type: ScheduleType
timezone: str = Field(..., min_length=1, max_length=50)
next_run_at: datetime
class SchedulerDispatchCommand(BaseModel):
model_config = ConfigDict(extra="forbid")
owner_id: UUID
automation_job_id: UUID
thread_id: UUID
run_id: str = Field(..., min_length=1, max_length=128)
input_text: str = Field(..., min_length=1, max_length=4000)