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:
@@ -11,7 +11,7 @@ from core.agentscope.tools.utils.calendar_domain import (
|
||||
map_calendar_exception,
|
||||
merge_schedule_metadata_for_update,
|
||||
parse_iso_datetime,
|
||||
resolve_share_target_email_map,
|
||||
resolve_share_target_phone_map,
|
||||
schedule_event_to_dict,
|
||||
)
|
||||
from core.agentscope.tools.utils.calendar_ui import (
|
||||
@@ -580,11 +580,11 @@ async def calendar_share(
|
||||
)
|
||||
target_uuid = UUID(event_id)
|
||||
|
||||
email_map = resolve_share_target_email_map(
|
||||
phone_map = resolve_share_target_phone_map(
|
||||
[invitee.user_id for invitee in invitees]
|
||||
)
|
||||
|
||||
if not email_map:
|
||||
if not phone_map:
|
||||
return calendar_error_output(
|
||||
tool_name=tool_name,
|
||||
tool_call_args=tool_call_args,
|
||||
@@ -599,8 +599,8 @@ async def calendar_share(
|
||||
normalized_user_id = str(UUID(invitee.user_id.strip()))
|
||||
except ValueError:
|
||||
continue
|
||||
email = email_map.get(normalized_user_id)
|
||||
if email is None:
|
||||
phone = phone_map.get(normalized_user_id)
|
||||
if phone is None:
|
||||
continue
|
||||
permission = {
|
||||
"permission_view": invitee.permission_view,
|
||||
@@ -608,15 +608,15 @@ async def calendar_share(
|
||||
"permission_invite": invitee.permission_invite,
|
||||
}
|
||||
await service.share(
|
||||
target_uuid, ScheduleItemShareRequest(email=email, **permission)
|
||||
target_uuid, ScheduleItemShareRequest(phone=phone, **permission)
|
||||
)
|
||||
invited.append(email)
|
||||
invited.append(phone)
|
||||
if not invited:
|
||||
return calendar_error_output(
|
||||
tool_name=tool_name,
|
||||
tool_call_args=tool_call_args,
|
||||
code="NOT_FOUND",
|
||||
message="邀请目标均无有效邮箱",
|
||||
message="邀请目标均无有效手机号",
|
||||
retryable=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from agentscope.tool import ToolResponse
|
||||
from core.agentscope.tools.tool_call_context import get_current_tool_call_id
|
||||
from core.agentscope.tools.utils import (
|
||||
find_auth_email_by_user_id,
|
||||
find_auth_phone_by_user_id,
|
||||
list_auth_users,
|
||||
)
|
||||
from core.agentscope.tools.utils.tool_response_builder import (
|
||||
@@ -46,22 +46,22 @@ def _lookup_error_output(
|
||||
async def _resolve_identity(
|
||||
*,
|
||||
session: AsyncSession,
|
||||
user_email: str | None,
|
||||
user_phone: str | None,
|
||||
user_name: str | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve user identity by email or username."""
|
||||
email = user_email.strip().lower() if isinstance(user_email, str) else ""
|
||||
"""Resolve user identity by phone or username."""
|
||||
phone = user_phone.strip() if isinstance(user_phone, str) else ""
|
||||
name = user_name.strip() if isinstance(user_name, str) else ""
|
||||
|
||||
if bool(email) == bool(name):
|
||||
if bool(phone) == bool(name):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="请提供 email 或 username 其中之一",
|
||||
detail="请提供 phone 或 username 其中之一",
|
||||
)
|
||||
|
||||
if email:
|
||||
if phone:
|
||||
auth_gateway = SupabaseAuthGateway()
|
||||
user = await auth_gateway.get_user_by_email(email)
|
||||
user = await auth_gateway.get_user_by_phone(phone)
|
||||
user_id = UUID(user.id)
|
||||
|
||||
stmt = (
|
||||
@@ -73,9 +73,9 @@ async def _resolve_identity(
|
||||
|
||||
return {
|
||||
"userId": str(user_id),
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"username": username,
|
||||
"matchedBy": "email",
|
||||
"matchedBy": "phone",
|
||||
}
|
||||
|
||||
stmt = (
|
||||
@@ -90,20 +90,20 @@ async def _resolve_identity(
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
users = list_auth_users()
|
||||
email_value = find_auth_email_by_user_id(users=users, user_id=profile.id)
|
||||
phone_value = find_auth_phone_by_user_id(users=users, user_id=profile.id)
|
||||
|
||||
return {
|
||||
"userId": str(profile.id),
|
||||
"email": email_value,
|
||||
"phone": phone_value,
|
||||
"username": profile.username,
|
||||
"matchedBy": "username",
|
||||
}
|
||||
|
||||
|
||||
async def user_lookup(
|
||||
user_email: Annotated[
|
||||
user_phone: Annotated[
|
||||
str | None,
|
||||
Field(description="User email address to look up."),
|
||||
Field(description="User phone to look up."),
|
||||
] = None,
|
||||
user_name: Annotated[
|
||||
str | None,
|
||||
@@ -112,16 +112,16 @@ async def user_lookup(
|
||||
session: Any = None,
|
||||
owner_id: Any = None,
|
||||
) -> ToolResponse:
|
||||
"""Look up user identity by email or username.
|
||||
"""Look up user identity by phone or username.
|
||||
|
||||
Args:
|
||||
user_email: User email address for lookup.
|
||||
user_phone: User phone for lookup.
|
||||
user_name: Username for lookup.
|
||||
|
||||
Returns:
|
||||
ToolResponse with serialized ToolAgentOutput payload.
|
||||
"""
|
||||
tool_call_args = {"user_email": user_email, "user_name": user_name}
|
||||
tool_call_args = {"user_phone": user_phone, "user_name": user_name}
|
||||
|
||||
if session is None or owner_id is None:
|
||||
return _lookup_error_output(
|
||||
@@ -134,17 +134,17 @@ async def user_lookup(
|
||||
try:
|
||||
resolved = await _resolve_identity(
|
||||
session=cast(AsyncSession, session),
|
||||
user_email=user_email,
|
||||
user_phone=user_phone,
|
||||
user_name=user_name,
|
||||
)
|
||||
|
||||
username = str(resolved.get("username") or "")
|
||||
email = str(resolved.get("email") or "")
|
||||
phone = str(resolved.get("phone") or "")
|
||||
user_id = str(resolved.get("userId") or "")
|
||||
matched_by = str(resolved.get("matchedBy") or "")
|
||||
summary = (
|
||||
f"status=success matched_by={matched_by} user_id={user_id} "
|
||||
f"username={username} has_email={str(bool(email)).lower()}"
|
||||
f"username={username} has_phone={str(bool(phone)).lower()}"
|
||||
)
|
||||
return _dump_tool_output(
|
||||
ToolAgentOutput(
|
||||
|
||||
@@ -6,7 +6,15 @@ from enum import Enum
|
||||
|
||||
class ToolGroup(str, Enum):
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
EXECUTE = "execute"
|
||||
MEMORY = "memory"
|
||||
|
||||
|
||||
class AgentTool(str, Enum):
|
||||
CALENDAR_READ = "calendar.read"
|
||||
CALENDAR_WRITE = "calendar.write"
|
||||
CALENDAR_SHARE = "calendar.share"
|
||||
USER_LOOKUP = "user.lookup"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -29,30 +37,51 @@ TOOL_CONFIGS: dict[str, ToolConfig] = {
|
||||
),
|
||||
"user_lookup": ToolConfig(
|
||||
name="user_lookup",
|
||||
group=ToolGroup.READ,
|
||||
group=ToolGroup.MEMORY,
|
||||
approval=ToolApprovalConfig(required=False),
|
||||
),
|
||||
"calendar_write": ToolConfig(
|
||||
name="calendar_write",
|
||||
group=ToolGroup.WRITE,
|
||||
group=ToolGroup.EXECUTE,
|
||||
approval=ToolApprovalConfig(required=False),
|
||||
),
|
||||
"calendar_share": ToolConfig(
|
||||
name="calendar_share",
|
||||
group=ToolGroup.WRITE,
|
||||
group=ToolGroup.EXECUTE,
|
||||
approval=ToolApprovalConfig(required=False),
|
||||
),
|
||||
}
|
||||
|
||||
AGENT_TOOL_TO_FUNCTION_NAME: dict[AgentTool, str] = {
|
||||
AgentTool.CALENDAR_READ: "calendar_read",
|
||||
AgentTool.CALENDAR_WRITE: "calendar_write",
|
||||
AgentTool.CALENDAR_SHARE: "calendar_share",
|
||||
AgentTool.USER_LOOKUP: "user_lookup",
|
||||
}
|
||||
|
||||
def get_tool_config(tool_name: str) -> ToolConfig:
|
||||
config = TOOL_CONFIGS.get(tool_name)
|
||||
if config is None:
|
||||
raise ValueError(f"unknown tool: {tool_name}")
|
||||
return config
|
||||
TOOL_NAME_ALIASES: dict[str, AgentTool] = {
|
||||
AgentTool.CALENDAR_READ.value: AgentTool.CALENDAR_READ,
|
||||
"calendar_read": AgentTool.CALENDAR_READ,
|
||||
AgentTool.CALENDAR_WRITE.value: AgentTool.CALENDAR_WRITE,
|
||||
"calendar_write": AgentTool.CALENDAR_WRITE,
|
||||
AgentTool.CALENDAR_SHARE.value: AgentTool.CALENDAR_SHARE,
|
||||
"calendar_share": AgentTool.CALENDAR_SHARE,
|
||||
AgentTool.USER_LOOKUP.value: AgentTool.USER_LOOKUP,
|
||||
"user_lookup": AgentTool.USER_LOOKUP,
|
||||
}
|
||||
|
||||
|
||||
def resolve_tool_names_by_groups(groups: set[ToolGroup]) -> set[str]:
|
||||
if not groups:
|
||||
return set()
|
||||
return {name for name, config in TOOL_CONFIGS.items() if config.group in groups}
|
||||
def parse_agent_tool(value: object) -> AgentTool:
|
||||
if isinstance(value, AgentTool):
|
||||
return value
|
||||
raw_value = str(value or "").strip().lower()
|
||||
if not raw_value:
|
||||
raise ValueError("enabled tool value cannot be empty")
|
||||
tool = TOOL_NAME_ALIASES.get(raw_value)
|
||||
if tool is None:
|
||||
raise ValueError(f"unknown enabled tool: {raw_value}")
|
||||
return tool
|
||||
|
||||
|
||||
def resolve_tool_function_names(tools: set[AgentTool]) -> set[str]:
|
||||
return {AGENT_TOOL_TO_FUNCTION_NAME[tool] for tool in tools}
|
||||
|
||||
@@ -7,10 +7,15 @@ from core.agentscope.tools.tool_call_context import (
|
||||
reset_current_tool_call_id,
|
||||
set_current_tool_call_id,
|
||||
)
|
||||
from core.agentscope.tools.tool_config import (
|
||||
AGENT_TOOL_TO_FUNCTION_NAME,
|
||||
TOOL_CONFIGS,
|
||||
ToolConfig,
|
||||
parse_agent_tool,
|
||||
)
|
||||
from core.agentscope.tools.utils.tool_response_builder import (
|
||||
build_error_response,
|
||||
)
|
||||
from core.agentscope.tools.tool_config import ToolConfig, TOOL_CONFIGS
|
||||
|
||||
|
||||
def register_tool_middlewares(
|
||||
@@ -59,6 +64,18 @@ def create_approval_middleware(
|
||||
approval_resolver: Callable[[str, dict[str, Any], ToolConfig], str | None]
|
||||
| None = None,
|
||||
) -> Callable[..., AsyncGenerator[Any, None]]:
|
||||
def _resolve_tool_config(*, tool_name: str) -> ToolConfig | None:
|
||||
config = config_by_name.get(tool_name)
|
||||
if config is not None:
|
||||
return config
|
||||
try:
|
||||
normalized_tool_name = AGENT_TOOL_TO_FUNCTION_NAME[
|
||||
parse_agent_tool(tool_name)
|
||||
]
|
||||
except ValueError:
|
||||
return None
|
||||
return config_by_name.get(normalized_tool_name)
|
||||
|
||||
def _resolve_tool_call_id(tool_call: dict[str, Any]) -> str:
|
||||
raw_tool_call_id = tool_call.get("id")
|
||||
if isinstance(raw_tool_call_id, str) and raw_tool_call_id.strip():
|
||||
@@ -81,7 +98,7 @@ def create_approval_middleware(
|
||||
yield response
|
||||
return
|
||||
|
||||
config = config_by_name.get(tool_name)
|
||||
config = _resolve_tool_config(tool_name=tool_name)
|
||||
if config is None or not config.approval.required:
|
||||
async for response in await next_handler(**kwargs):
|
||||
yield response
|
||||
@@ -134,15 +151,3 @@ def create_approval_middleware(
|
||||
yield pending_response
|
||||
|
||||
return approval_middleware
|
||||
|
||||
|
||||
def create_hitl_middleware(
|
||||
*,
|
||||
meta_by_name: dict[str, ToolConfig],
|
||||
approval_resolver: Callable[[str, dict[str, Any], ToolConfig], str | None]
|
||||
| None = None,
|
||||
) -> Callable[..., AsyncGenerator[Any, None]]:
|
||||
return create_approval_middleware(
|
||||
config_by_name=meta_by_name,
|
||||
approval_resolver=approval_resolver,
|
||||
)
|
||||
|
||||
@@ -13,8 +13,6 @@ from core.agentscope.tools.custom.calendar import (
|
||||
from core.agentscope.tools.custom.user_lookup import user_lookup
|
||||
from core.agentscope.tools.tool_config import (
|
||||
TOOL_CONFIGS,
|
||||
ToolGroup,
|
||||
resolve_tool_names_by_groups,
|
||||
)
|
||||
from core.agentscope.tools.tool_middleware import register_tool_middlewares
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -28,46 +26,36 @@ TOOL_FUNCTIONS: dict[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
AGENT_TYPE_TO_GROUPS: dict[AgentType, set[ToolGroup]] = {
|
||||
AgentType.WORKER: {ToolGroup.READ, ToolGroup.WRITE},
|
||||
AgentType.MEMORY: {ToolGroup.READ, ToolGroup.WRITE},
|
||||
AGENT_TYPE_TO_DEFAULT_TOOLS: dict[AgentType, set[str]] = {
|
||||
AgentType.WORKER: {
|
||||
"calendar_read",
|
||||
"calendar_write",
|
||||
"calendar_share",
|
||||
"user_lookup",
|
||||
},
|
||||
AgentType.MEMORY: {"calendar_read", "user_lookup"},
|
||||
}
|
||||
|
||||
|
||||
def _resolve_enabled_tools(
|
||||
*,
|
||||
groups: set[ToolGroup] | None,
|
||||
enabled_tool_names: set[str] | None,
|
||||
) -> set[str]:
|
||||
if enabled_tool_names is not None:
|
||||
unknown = enabled_tool_names - set(TOOL_FUNCTIONS)
|
||||
if unknown:
|
||||
raise ValueError(f"unknown tools in enabled_tool_names: {sorted(unknown)}")
|
||||
return set(enabled_tool_names)
|
||||
|
||||
if groups is None:
|
||||
return set(TOOL_FUNCTIONS)
|
||||
|
||||
resolved = resolve_tool_names_by_groups(groups)
|
||||
unknown = resolved - set(TOOL_FUNCTIONS)
|
||||
def _validate_enabled_tool_names(enabled_tool_names: set[str]) -> set[str]:
|
||||
unknown = enabled_tool_names - set(TOOL_FUNCTIONS)
|
||||
if unknown:
|
||||
raise ValueError(f"tool config contains unknown tools: {sorted(unknown)}")
|
||||
return resolved
|
||||
raise ValueError(f"unknown tools in enabled_tool_names: {sorted(unknown)}")
|
||||
return enabled_tool_names
|
||||
|
||||
|
||||
def build_toolkit(
|
||||
*,
|
||||
session: AsyncSession,
|
||||
owner_id: UUID,
|
||||
groups: set[ToolGroup] | None = None,
|
||||
enabled_tool_names: set[str] | None = None,
|
||||
enable_hitl: bool | None = None,
|
||||
):
|
||||
toolkit = Toolkit()
|
||||
enabled_names = _resolve_enabled_tools(
|
||||
groups=groups,
|
||||
enabled_tool_names=enabled_tool_names,
|
||||
)
|
||||
if enabled_tool_names is None:
|
||||
enabled_names = set(TOOL_FUNCTIONS)
|
||||
else:
|
||||
enabled_names = _validate_enabled_tool_names(set(enabled_tool_names))
|
||||
|
||||
preset_kwargs = cast(
|
||||
dict[str, JSONSerializableObject],
|
||||
@@ -100,15 +88,13 @@ def build_stage_toolkit(
|
||||
enabled_tool_names: set[str] | None = None,
|
||||
enable_hitl: bool | None = None,
|
||||
):
|
||||
groups = AGENT_TYPE_TO_GROUPS.get(agent_type)
|
||||
if groups is None:
|
||||
default_tools = AGENT_TYPE_TO_DEFAULT_TOOLS.get(agent_type)
|
||||
if default_tools is None:
|
||||
raise ValueError(f"unknown agent_type: {agent_type}")
|
||||
|
||||
stage_enabled_names = resolve_tool_names_by_groups(set(groups))
|
||||
selected_names = (
|
||||
stage_enabled_names
|
||||
set(default_tools)
|
||||
if enabled_tool_names is None
|
||||
else stage_enabled_names | set(enabled_tool_names)
|
||||
else _validate_enabled_tool_names(set(enabled_tool_names))
|
||||
)
|
||||
|
||||
return build_toolkit(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from core.agentscope.tools.utils.auth_helpers import (
|
||||
find_auth_email_by_user_id,
|
||||
find_auth_phone_by_user_id,
|
||||
list_auth_users,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"list_auth_users",
|
||||
"find_auth_email_by_user_id",
|
||||
"find_auth_phone_by_user_id",
|
||||
]
|
||||
|
||||
@@ -25,12 +25,12 @@ def list_auth_users() -> list[Any]:
|
||||
return users
|
||||
|
||||
|
||||
def find_auth_email_by_user_id(*, users: list[Any], user_id: UUID) -> str | None:
|
||||
"""Find auth email by user id from fetched user list."""
|
||||
def find_auth_phone_by_user_id(*, users: list[Any], user_id: UUID) -> str | None:
|
||||
"""Find auth phone by user id from fetched user list."""
|
||||
target = str(user_id)
|
||||
for user in users:
|
||||
if str(getattr(user, "id", "")) == target:
|
||||
email = getattr(user, "email", None)
|
||||
if isinstance(email, str) and email.strip():
|
||||
return email.strip()
|
||||
phone = getattr(user, "phone", None)
|
||||
if isinstance(phone, str) and phone.strip():
|
||||
return phone.strip()
|
||||
return None
|
||||
|
||||
@@ -9,7 +9,7 @@ from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agentscope.tools.utils.auth_helpers import (
|
||||
find_auth_email_by_user_id,
|
||||
find_auth_phone_by_user_id,
|
||||
list_auth_users,
|
||||
)
|
||||
from core.auth.models import CurrentUser
|
||||
@@ -125,7 +125,7 @@ def parse_iso_datetime(value: str | None) -> datetime | None:
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def resolve_share_target_email_map(invitee_user_ids: list[str]) -> dict[str, str]:
|
||||
def resolve_share_target_phone_map(invitee_user_ids: list[str]) -> dict[str, str]:
|
||||
users = list_auth_users()
|
||||
resolved: dict[str, str] = {}
|
||||
for raw_user_id in invitee_user_ids:
|
||||
@@ -138,7 +138,7 @@ def resolve_share_target_email_map(invitee_user_ids: list[str]) -> dict[str, str
|
||||
user_uuid = UUID(normalized_user_id)
|
||||
except ValueError:
|
||||
continue
|
||||
email = find_auth_email_by_user_id(users=users, user_id=user_uuid)
|
||||
if email:
|
||||
resolved[str(user_uuid)] = email.lower()
|
||||
phone = find_auth_phone_by_user_id(users=users, user_id=user_uuid)
|
||||
if phone:
|
||||
resolved[str(user_uuid)] = phone
|
||||
return resolved
|
||||
|
||||
Reference in New Issue
Block a user