refactor: clean CLI taxonomy — canonical subcommands, merged memory.update, no aliases
- calendar: split write → create/read/update/delete/share - contacts: rename lookup → read - memory: merge write+forget → update (unified action field in operations) - Remove all alias/normalization logic from adapter and handlers - Update tool_postprocessor ui_hints builders to canonical keys - Remove frontend legacy TOOL_CALL_START/ARGS/END events and ToolCallItem - Update SKILL.md files and protocol docs - Update tests and settings screens
This commit is contained in:
@@ -9,6 +9,7 @@ from core.agentscope.caches.context_messages_cache import (
|
||||
create_context_messages_cache,
|
||||
)
|
||||
from core.agentscope.events.persistence import MessageRepository, SessionRepository
|
||||
from core.agentscope.utils.parsing import project_tool_result_text
|
||||
from core.logging import get_logger
|
||||
from schemas.agent.forwarded_props import RuntimeMode
|
||||
from schemas.enums import AgentChatMessageRole, AgentChatSessionStatus
|
||||
@@ -339,7 +340,7 @@ class SqlAlchemyEventStore:
|
||||
)
|
||||
return
|
||||
|
||||
content = tool_output.result
|
||||
content = project_tool_result_text(tool_output.result)
|
||||
|
||||
locked_session = await session_repo.lock_session_for_update(
|
||||
session_id=session_id
|
||||
|
||||
@@ -22,6 +22,11 @@ from core.agentscope.utils import (
|
||||
finalize_json_response,
|
||||
patch_agentscope_json_repair_compat,
|
||||
)
|
||||
from core.auth.credential_issuer import create_credential_issuer
|
||||
from core.auth.tool_credential_context import (
|
||||
set_tool_credential,
|
||||
reset_tool_credential,
|
||||
)
|
||||
from core.config.settings import config
|
||||
from core.db.session import AsyncSessionLocal
|
||||
from models.llm import Llm
|
||||
@@ -37,7 +42,7 @@ from schemas.agent.runtime_models import (
|
||||
RouterAgentOutput,
|
||||
WorkerAgentOutputLite,
|
||||
)
|
||||
from schemas.agent.skill_config import SkillName
|
||||
from schemas.agent.skill_config import ProjectCliCommand, SkillName
|
||||
from schemas.agent.system_agent import (
|
||||
AgentType,
|
||||
SystemAgentLLMConfig,
|
||||
@@ -107,7 +112,8 @@ class AgentScopeRunner:
|
||||
agent_type=AgentType.WORKER,
|
||||
)
|
||||
worker_toolkit = self._build_toolkit(
|
||||
enabled_skills=runtime_config.enabled_skills
|
||||
enabled_skills=runtime_config.enabled_skills,
|
||||
allowed_commands=runtime_config.allowed_commands,
|
||||
)
|
||||
|
||||
router_output = await self._execute_router_step(
|
||||
@@ -174,10 +180,13 @@ class AgentScopeRunner:
|
||||
self,
|
||||
*,
|
||||
enabled_skills: list[SkillName],
|
||||
allowed_commands: list[ProjectCliCommand],
|
||||
) -> Any:
|
||||
enabled_skill_names = {str(skill.value) for skill in enabled_skills}
|
||||
allowed_command_names = {str(command.value) for command in allowed_commands}
|
||||
return build_toolkit(
|
||||
enabled_skill_names=enabled_skill_names if enabled_skill_names else None
|
||||
enabled_skill_names=enabled_skill_names if enabled_skill_names else None,
|
||||
allowed_commands=allowed_command_names if allowed_command_names else None,
|
||||
)
|
||||
|
||||
async def _load_stage_config(
|
||||
@@ -388,56 +397,66 @@ class AgentScopeRunner:
|
||||
work_memory: WorkProfileContent | None,
|
||||
requires_tool_evidence: bool = False,
|
||||
) -> StageExecutionResult:
|
||||
tracking_model = self._build_model(stage_config=stage_config)
|
||||
emitter = PipelineStageEmitter(
|
||||
pipeline=pipeline,
|
||||
session_id=run_input.thread_id,
|
||||
run_id=run_input.run_id,
|
||||
stage=stage_config.agent_type.value,
|
||||
runtime_mode=runtime_mode.value,
|
||||
emit_text_events=True,
|
||||
emit_tool_events=True,
|
||||
issuer = create_credential_issuer()
|
||||
credential = issuer.issue(
|
||||
owner_id=str(user_context.id),
|
||||
mode=runtime_mode.value,
|
||||
)
|
||||
agent = self._build_agent(
|
||||
agent_name=stage_config.agent_type.value,
|
||||
system_prompt=build_system_prompt(
|
||||
agent_type=stage_config.agent_type,
|
||||
llm_config=stage_config.llm_config,
|
||||
user_context=user_context,
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
runtime_client_time=runtime_client_time,
|
||||
extra_context=stage_config.extra_context,
|
||||
work_memory=work_memory,
|
||||
),
|
||||
toolkit=toolkit,
|
||||
model=tracking_model,
|
||||
emitter=emitter,
|
||||
force_tool_on_first_reasoning=requires_tool_evidence,
|
||||
)
|
||||
async with self._active_agent_lock:
|
||||
self._active_agent = agent
|
||||
credential_token = set_tool_credential(credential)
|
||||
|
||||
try:
|
||||
response_msg = await agent.reply_json(
|
||||
input_messages, output_model=worker_output_model
|
||||
tracking_model = self._build_model(stage_config=stage_config)
|
||||
emitter = PipelineStageEmitter(
|
||||
pipeline=pipeline,
|
||||
session_id=run_input.thread_id,
|
||||
run_id=run_input.run_id,
|
||||
stage=stage_config.agent_type.value,
|
||||
runtime_mode=runtime_mode.value,
|
||||
emit_text_events=True,
|
||||
emit_tool_events=True,
|
||||
)
|
||||
agent = self._build_agent(
|
||||
agent_name=stage_config.agent_type.value,
|
||||
system_prompt=build_system_prompt(
|
||||
agent_type=stage_config.agent_type,
|
||||
llm_config=stage_config.llm_config,
|
||||
user_context=user_context,
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
runtime_client_time=runtime_client_time,
|
||||
extra_context=stage_config.extra_context,
|
||||
work_memory=work_memory,
|
||||
),
|
||||
toolkit=toolkit,
|
||||
model=tracking_model,
|
||||
emitter=emitter,
|
||||
force_tool_on_first_reasoning=requires_tool_evidence,
|
||||
)
|
||||
async with self._active_agent_lock:
|
||||
self._active_agent = agent
|
||||
try:
|
||||
response_msg = await agent.reply_json(
|
||||
input_messages, output_model=worker_output_model
|
||||
)
|
||||
finally:
|
||||
async with self._active_agent_lock:
|
||||
if self._active_agent is agent:
|
||||
self._active_agent = None
|
||||
worker_payload = worker_output_model.model_validate(response_msg.metadata or {})
|
||||
response_metadata = self._llm_pricing_service.build_usage_metadata(
|
||||
model=stage_config.model_code,
|
||||
usage_summary=tracking_model.usage_summary(),
|
||||
)
|
||||
await emitter.emit_final_text_end(
|
||||
worker_output=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
return StageExecutionResult(
|
||||
message=response_msg,
|
||||
payload=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
finally:
|
||||
async with self._active_agent_lock:
|
||||
if self._active_agent is agent:
|
||||
self._active_agent = None
|
||||
worker_payload = worker_output_model.model_validate(response_msg.metadata or {})
|
||||
response_metadata = self._llm_pricing_service.build_usage_metadata(
|
||||
model=stage_config.model_code,
|
||||
usage_summary=tracking_model.usage_summary(),
|
||||
)
|
||||
await emitter.emit_final_text_end(
|
||||
worker_output=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
return StageExecutionResult(
|
||||
message=response_msg,
|
||||
payload=worker_payload.model_dump(mode="json", exclude_none=True),
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
reset_tool_credential(credential_token)
|
||||
|
||||
def _build_worker_input_messages(
|
||||
self,
|
||||
|
||||
@@ -63,6 +63,12 @@ async def invoke_cli_tool(
|
||||
if not isinstance(args, dict):
|
||||
args = {}
|
||||
|
||||
tool_call_args = {
|
||||
**tool_call_args,
|
||||
"subcommand": subcommand,
|
||||
"args": args,
|
||||
}
|
||||
|
||||
if tool_name != "project_cli":
|
||||
return _build_error(
|
||||
tool_name=tool_name,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from core.agentscope.tools.cli.models import CliCommand, CliCommandResult
|
||||
from core.agentscope.tools.utils.calendar_domain import (
|
||||
@@ -12,6 +14,7 @@ from core.agentscope.tools.utils.calendar_domain import (
|
||||
parse_iso_datetime,
|
||||
schedule_event_to_dict,
|
||||
)
|
||||
from schemas.agent.runtime_models import ErrorInfo
|
||||
from schemas.enums import ScheduleItemStatus
|
||||
from v1.schedule_items.schemas import (
|
||||
ScheduleItemCreateRequest,
|
||||
@@ -24,83 +27,181 @@ from v1.schedule_items.schemas import (
|
||||
async def handle_calendar_read(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
start_at = str(request.args.get("start_at", ""))
|
||||
end_at = str(request.args.get("end_at", ""))
|
||||
parsed_start = parse_iso_datetime(start_at)
|
||||
parsed_end = parse_iso_datetime(end_at)
|
||||
parsed_start, parsed_end, read_error = _resolve_read_range(request)
|
||||
if read_error is not None:
|
||||
return _fail(request=request, code="INVALID_ARGUMENT", message=read_error)
|
||||
if parsed_start is None or parsed_end is None:
|
||||
return _fail(request=request, code="INVALID_ARGUMENT", message="start_at and end_at are required")
|
||||
return _fail(
|
||||
request=request,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="start_at and end_at are required",
|
||||
)
|
||||
if parsed_start >= parsed_end:
|
||||
return _fail(request=request, code="INVALID_ARGUMENT", message="start_at must be before end_at")
|
||||
return _fail(
|
||||
request=request,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="start_at must be before end_at",
|
||||
)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
service = create_schedule_service(session, UUID(request.owner_id))
|
||||
list_request = ScheduleItemListRequest(start_at=parsed_start, end_at=parsed_end)
|
||||
items = await service.list_by_date_range(list_request)
|
||||
event_items = [schedule_event_to_dict(item) for item in items]
|
||||
return CliCommandResult(ok=True, command="calendar", subcommand="read", data={"total": len(event_items), "items": event_items})
|
||||
return CliCommandResult(
|
||||
ok=True,
|
||||
command="calendar",
|
||||
subcommand="read",
|
||||
data={"total": len(event_items), "items": event_items},
|
||||
)
|
||||
|
||||
|
||||
async def handle_calendar_write(request: CliCommand) -> CliCommandResult:
|
||||
async def handle_calendar_create(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
operations = request.args.get("operations")
|
||||
if not isinstance(operations, list):
|
||||
operations = []
|
||||
async with AsyncSessionLocal() as session:
|
||||
service = create_schedule_service(session, UUID(request.owner_id))
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
success_ids: list[str] = []
|
||||
result_items: list[dict[str, Any]] = []
|
||||
|
||||
for op in operations:
|
||||
action = op.get("action")
|
||||
try:
|
||||
if action == "create":
|
||||
res = await _create_event(service, op)
|
||||
success_count += 1
|
||||
success_ids.append(res["eventId"])
|
||||
result_items.append(res)
|
||||
elif action == "update":
|
||||
res = await _update_event(service, op)
|
||||
success_count += 1
|
||||
success_ids.append(res["eventId"])
|
||||
result_items.append(res)
|
||||
elif action == "delete":
|
||||
event_id = op.get("event_id")
|
||||
if not event_id:
|
||||
raise ValueError("delete requires event_id")
|
||||
await service.delete(UUID(event_id))
|
||||
success_count += 1
|
||||
success_ids.append(event_id)
|
||||
result_items.append({"status": "success", "eventId": event_id})
|
||||
else:
|
||||
raise ValueError(f"unknown action: {action}")
|
||||
except Exception as exc:
|
||||
code, message, _ = map_calendar_exception(exc)
|
||||
failed_count += 1
|
||||
result_items.append({
|
||||
try:
|
||||
result_item = await _create_event(service, request.args)
|
||||
event_id = str(result_item.get("eventId") or "")
|
||||
return CliCommandResult(
|
||||
ok=True,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": "success",
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"ids": [event_id] if event_id else [],
|
||||
"results": [result_item],
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
code, message, retryable = map_calendar_exception(exc)
|
||||
return CliCommandResult(
|
||||
ok=False,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": "failure",
|
||||
"eventId": op.get("event_id"),
|
||||
"code": code,
|
||||
"message": message,
|
||||
})
|
||||
"success": 0,
|
||||
"failed": 1,
|
||||
"ids": [],
|
||||
"results": [
|
||||
{
|
||||
"action": "create",
|
||||
"status": "failure",
|
||||
"eventId": "",
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
],
|
||||
},
|
||||
error=ErrorInfo(code=code, message=message, retryable=retryable),
|
||||
)
|
||||
|
||||
status = _batch_status(success_count, failed_count)
|
||||
return CliCommandResult(
|
||||
ok=status != "failure",
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": status,
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"ids": success_ids,
|
||||
"results": result_items,
|
||||
},
|
||||
)
|
||||
|
||||
async def handle_calendar_update(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
service = create_schedule_service(session, UUID(request.owner_id))
|
||||
event_id = str(request.args.get("event_id") or "").strip()
|
||||
try:
|
||||
result_item = await _update_event(service, request.args)
|
||||
event_id = str(result_item.get("eventId") or event_id)
|
||||
return CliCommandResult(
|
||||
ok=True,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": "success",
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"ids": [event_id] if event_id else [],
|
||||
"results": [result_item],
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
code, message, retryable = map_calendar_exception(exc)
|
||||
return CliCommandResult(
|
||||
ok=False,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": "failure",
|
||||
"success": 0,
|
||||
"failed": 1,
|
||||
"ids": [],
|
||||
"results": [
|
||||
{
|
||||
"action": "update",
|
||||
"status": "failure",
|
||||
"eventId": event_id,
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
],
|
||||
},
|
||||
error=ErrorInfo(code=code, message=message, retryable=retryable),
|
||||
)
|
||||
|
||||
|
||||
async def handle_calendar_delete(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
service = create_schedule_service(session, UUID(request.owner_id))
|
||||
event_id = str(request.args.get("event_id") or "").strip()
|
||||
if not event_id:
|
||||
return _fail(
|
||||
request=request,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="event_id is required",
|
||||
)
|
||||
try:
|
||||
await service.delete(UUID(event_id))
|
||||
return CliCommandResult(
|
||||
ok=True,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": "success",
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"ids": [event_id],
|
||||
"results": [
|
||||
{
|
||||
"action": "delete",
|
||||
"status": "success",
|
||||
"eventId": event_id,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
code, message, retryable = map_calendar_exception(exc)
|
||||
return CliCommandResult(
|
||||
ok=False,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": "failure",
|
||||
"success": 0,
|
||||
"failed": 1,
|
||||
"ids": [],
|
||||
"results": [
|
||||
{
|
||||
"action": "delete",
|
||||
"status": "failure",
|
||||
"eventId": event_id,
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
],
|
||||
},
|
||||
error=ErrorInfo(code=code, message=message, retryable=retryable),
|
||||
)
|
||||
|
||||
|
||||
async def handle_calendar_share(request: CliCommand) -> CliCommandResult:
|
||||
@@ -121,7 +222,14 @@ async def handle_calendar_share(request: CliCommand) -> CliCommandResult:
|
||||
raw_phone = inv.get("phone", "").strip()
|
||||
normalized_phone = _normalize_phone(raw_phone)
|
||||
if not normalized_phone:
|
||||
result_items.append({"phone": raw_phone, "status": "failure", "code": "INVALID_ARGUMENT", "message": "invalid phone"})
|
||||
result_items.append(
|
||||
{
|
||||
"phone": raw_phone,
|
||||
"status": "failure",
|
||||
"code": "INVALID_ARGUMENT",
|
||||
"message": "invalid phone",
|
||||
}
|
||||
)
|
||||
continue
|
||||
permission = {
|
||||
"permission_view": inv.get("permission_view", True),
|
||||
@@ -129,12 +237,22 @@ async def handle_calendar_share(request: CliCommand) -> CliCommandResult:
|
||||
"permission_invite": inv.get("permission_invite", False),
|
||||
}
|
||||
try:
|
||||
await service.share(target_uuid, ScheduleItemShareRequest(phone=normalized_phone, **permission))
|
||||
await service.share(
|
||||
target_uuid,
|
||||
ScheduleItemShareRequest(phone=normalized_phone, **permission),
|
||||
)
|
||||
invited.append(normalized_phone)
|
||||
result_items.append({"phone": normalized_phone, "status": "success"})
|
||||
except Exception as exc:
|
||||
code, message, _ = map_calendar_exception(exc)
|
||||
result_items.append({"phone": normalized_phone, "status": "failure", "code": code, "message": message})
|
||||
result_items.append(
|
||||
{
|
||||
"phone": normalized_phone,
|
||||
"status": "failure",
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
|
||||
failure_count = len([r for r in result_items if r["status"] == "failure"])
|
||||
success_count = len(invited)
|
||||
@@ -152,64 +270,101 @@ async def handle_calendar_share(request: CliCommand) -> CliCommandResult:
|
||||
)
|
||||
|
||||
|
||||
async def _create_event(service: Any, op: dict[str, Any]) -> dict[str, Any]:
|
||||
start_at = op.get("start_at")
|
||||
if not start_at:
|
||||
async def _create_event(service: Any, args: dict[str, Any]) -> dict[str, Any]:
|
||||
start_at = args.get("start_at")
|
||||
if not isinstance(start_at, str) or not start_at.strip():
|
||||
raise ValueError("create requires start_at")
|
||||
event_timezone = op.get("event_timezone")
|
||||
if not event_timezone:
|
||||
event_timezone = args.get("event_timezone")
|
||||
if not isinstance(event_timezone, str) or not event_timezone.strip():
|
||||
raise ValueError("create requires event_timezone")
|
||||
parsed_start = parse_iso_datetime(start_at)
|
||||
if parsed_start is None:
|
||||
raise ValueError("invalid start_at")
|
||||
end_at = op.get("end_at")
|
||||
parsed_end = parse_iso_datetime(end_at) if end_at else None
|
||||
|
||||
parsed_end = None
|
||||
end_at = args.get("end_at")
|
||||
if isinstance(end_at, str) and end_at.strip():
|
||||
parsed_end = parse_iso_datetime(end_at)
|
||||
if parsed_end is None:
|
||||
raise ValueError("invalid end_at")
|
||||
|
||||
created = await service.create_agent_generated(
|
||||
ScheduleItemCreateRequest(
|
||||
title=(op.get("title") or "new event").strip(),
|
||||
description=op.get("description", "").strip() or None,
|
||||
title=str(args.get("title") or "new event").strip(),
|
||||
description=(str(args.get("description") or "").strip() or None),
|
||||
start_at=parsed_start,
|
||||
end_at=parsed_end,
|
||||
timezone=event_timezone.strip(),
|
||||
metadata=build_schedule_metadata(
|
||||
op.get("location"),
|
||||
op.get("color"),
|
||||
op.get("reminder_minutes"),
|
||||
args.get("location"),
|
||||
args.get("color"),
|
||||
args.get("reminder_minutes"),
|
||||
),
|
||||
)
|
||||
)
|
||||
return {"status": "success", "eventId": str(created.id)}
|
||||
return {"action": "create", "status": "success", "eventId": str(created.id)}
|
||||
|
||||
|
||||
async def _update_event(service: Any, op: dict[str, Any]) -> dict[str, Any]:
|
||||
event_id = op.get("event_id")
|
||||
if not event_id:
|
||||
async def _update_event(service: Any, args: dict[str, Any]) -> dict[str, Any]:
|
||||
event_id = args.get("event_id")
|
||||
if not isinstance(event_id, str) or not event_id.strip():
|
||||
raise ValueError("update requires event_id")
|
||||
|
||||
update_data: dict[str, Any] = {}
|
||||
if "title" in op:
|
||||
update_data["title"] = op["title"].strip()
|
||||
if "description" in op:
|
||||
update_data["description"] = op["description"].strip()
|
||||
if "start_at" in op:
|
||||
update_data["start_at"] = parse_iso_datetime(op["start_at"])
|
||||
if "end_at" in op:
|
||||
update_data["end_at"] = parse_iso_datetime(op["end_at"])
|
||||
if "event_timezone" in op:
|
||||
update_data["timezone"] = op["event_timezone"].strip()
|
||||
if "status" in op:
|
||||
update_data["status"] = ScheduleItemStatus(op["status"])
|
||||
if any(k in op for k in ("location", "color", "reminder_minutes")):
|
||||
if "title" in args:
|
||||
update_data["title"] = str(args.get("title") or "").strip()
|
||||
if "description" in args:
|
||||
update_data["description"] = str(args.get("description") or "").strip()
|
||||
if "start_at" in args:
|
||||
start_value = args.get("start_at")
|
||||
if not isinstance(start_value, str) or not start_value.strip():
|
||||
raise ValueError("start_at must be non-empty string")
|
||||
parsed_start = parse_iso_datetime(start_value)
|
||||
if parsed_start is None:
|
||||
raise ValueError("invalid start_at")
|
||||
update_data["start_at"] = parsed_start
|
||||
if "end_at" in args:
|
||||
end_value = args.get("end_at")
|
||||
if end_value in (None, ""):
|
||||
update_data["end_at"] = None
|
||||
elif isinstance(end_value, str):
|
||||
parsed_end = parse_iso_datetime(end_value)
|
||||
if parsed_end is None:
|
||||
raise ValueError("invalid end_at")
|
||||
update_data["end_at"] = parsed_end
|
||||
else:
|
||||
raise ValueError("end_at must be string or null")
|
||||
if "event_timezone" in args:
|
||||
timezone_value = args.get("event_timezone")
|
||||
if not isinstance(timezone_value, str) or not timezone_value.strip():
|
||||
raise ValueError("event_timezone must be non-empty string")
|
||||
update_data["timezone"] = timezone_value.strip()
|
||||
if "status" in args:
|
||||
update_data["status"] = ScheduleItemStatus(str(args.get("status")))
|
||||
|
||||
if any(key in args for key in ("location", "color", "reminder_minutes")):
|
||||
existing = await service.get_by_id(UUID(event_id))
|
||||
update_data["metadata"] = merge_schedule_metadata_for_update(
|
||||
existing_metadata=existing.metadata,
|
||||
location=op.get("location"),
|
||||
color=op.get("color"),
|
||||
reminder_minutes=op.get("reminder_minutes"),
|
||||
location=args.get("location"),
|
||||
color=args.get("color"),
|
||||
reminder_minutes=args.get("reminder_minutes"),
|
||||
)
|
||||
|
||||
if not update_data:
|
||||
raise ValueError("update requires at least one mutable field")
|
||||
|
||||
changed_fields = sorted(update_data.keys())
|
||||
updated = await service.update(UUID(event_id), ScheduleItemUpdateRequest.model_validate(update_data))
|
||||
return {"status": "success", "eventId": str(updated.id), "changedFields": changed_fields}
|
||||
updated = await service.update(
|
||||
UUID(event_id),
|
||||
ScheduleItemUpdateRequest.model_validate(update_data),
|
||||
)
|
||||
return {
|
||||
"action": "update",
|
||||
"status": "success",
|
||||
"eventId": str(updated.id),
|
||||
"changedFields": changed_fields,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_phone(raw: str) -> str:
|
||||
@@ -222,7 +377,12 @@ def _normalize_phone(raw: str) -> str:
|
||||
phone = f"+{phone}"
|
||||
elif phone.startswith("1") and phone.isdigit():
|
||||
phone = f"+86{phone}"
|
||||
if len(phone) != 14 or not phone.startswith("+861") or not phone[1:].isdigit() or phone[4] not in "3456789":
|
||||
if (
|
||||
len(phone) != 14
|
||||
or not phone.startswith("+861")
|
||||
or not phone[1:].isdigit()
|
||||
or phone[4] not in "3456789"
|
||||
):
|
||||
return ""
|
||||
return phone
|
||||
|
||||
@@ -235,9 +395,52 @@ def _batch_status(success: int, failed: int) -> str:
|
||||
return "partial"
|
||||
|
||||
|
||||
def _fail(*, request: CliCommand, code: str, message: str) -> CliCommandResult:
|
||||
from schemas.agent.runtime_models import ErrorInfo
|
||||
def _resolve_read_range(
|
||||
request: CliCommand,
|
||||
) -> tuple[datetime | None, datetime | None, str | None]:
|
||||
start_at = str(request.args.get("start_at", "")).strip()
|
||||
end_at = str(request.args.get("end_at", "")).strip()
|
||||
if start_at and end_at:
|
||||
try:
|
||||
return parse_iso_datetime(start_at), parse_iso_datetime(end_at), None
|
||||
except ValueError as exc:
|
||||
return None, None, str(exc)
|
||||
|
||||
raw_date = str(request.args.get("date", "")).strip()
|
||||
if not raw_date:
|
||||
return None, None, None
|
||||
|
||||
timezone_name = (
|
||||
str(request.args.get("timezone", "Asia/Shanghai")).strip() or "Asia/Shanghai"
|
||||
)
|
||||
try:
|
||||
zone = ZoneInfo(timezone_name)
|
||||
except Exception:
|
||||
return None, None, "timezone is invalid"
|
||||
|
||||
try:
|
||||
target_date = date.fromisoformat(raw_date)
|
||||
except ValueError:
|
||||
return None, None, "date must be YYYY-MM-DD"
|
||||
|
||||
start_local = datetime(
|
||||
year=target_date.year,
|
||||
month=target_date.month,
|
||||
day=target_date.day,
|
||||
hour=0,
|
||||
minute=0,
|
||||
second=0,
|
||||
tzinfo=zone,
|
||||
)
|
||||
end_local = start_local + timedelta(days=1)
|
||||
return (
|
||||
parse_iso_datetime(start_local.isoformat()),
|
||||
parse_iso_datetime(end_local.isoformat()),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _fail(*, request: CliCommand, code: str, message: str) -> CliCommandResult:
|
||||
return CliCommandResult(
|
||||
ok=False,
|
||||
command=request.command,
|
||||
|
||||
@@ -13,7 +13,7 @@ from v1.auth.gateway import SupabaseAuthGateway
|
||||
from v1.users.contact_resolver import resolve_contacts_by_user_ids
|
||||
|
||||
|
||||
async def handle_contacts_lookup(request: CliCommand) -> CliCommandResult:
|
||||
async def handle_contacts_read(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
|
||||
@@ -9,127 +9,130 @@ from core.agentscope.tools.utils.memory_domain import (
|
||||
create_memories_service,
|
||||
map_memory_exception,
|
||||
)
|
||||
from schemas.agent.runtime_models import ErrorInfo
|
||||
from schemas.enums import MemoryType
|
||||
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
|
||||
|
||||
|
||||
async def handle_memory_write(request: CliCommand) -> CliCommandResult:
|
||||
async def handle_memory_update(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
operations = request.args.get("operations")
|
||||
if not isinstance(operations, list):
|
||||
operations = []
|
||||
if not isinstance(operations, list) or not operations:
|
||||
return _invalid_argument(
|
||||
request=request,
|
||||
message="operations must be a non-empty list",
|
||||
)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
service = create_memories_service(session=session, owner_id=UUID(request.owner_id))
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
updated_types: list[str] = []
|
||||
forgotten_total = 0
|
||||
failed_ops: list[dict[str, Any]] = []
|
||||
result_items: list[dict[str, Any]] = []
|
||||
|
||||
for idx, op in enumerate(operations):
|
||||
memory_type = MemoryType(op.get("memory_type", "user"))
|
||||
if not isinstance(op, dict):
|
||||
failed_count += 1
|
||||
failed_ops.append(
|
||||
{
|
||||
"code": "INVALID_ARGUMENT",
|
||||
"message": "operation item must be object",
|
||||
"retryable": False,
|
||||
}
|
||||
)
|
||||
result_items.append(
|
||||
{
|
||||
"idx": idx,
|
||||
"memoryType": "unknown",
|
||||
"action": "invalid",
|
||||
"status": "failure",
|
||||
"code": "INVALID_ARGUMENT",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
action = str(op.get("action") or "").strip().lower()
|
||||
if action not in {"update", "delete"}:
|
||||
failed_count += 1
|
||||
failed_ops.append(
|
||||
{
|
||||
"code": "INVALID_ARGUMENT",
|
||||
"message": "action must be update or delete",
|
||||
"retryable": False,
|
||||
}
|
||||
)
|
||||
result_items.append(
|
||||
{
|
||||
"idx": idx,
|
||||
"memoryType": str(op.get("memory_type") or "unknown"),
|
||||
"action": action or "invalid",
|
||||
"status": "failure",
|
||||
"code": "INVALID_ARGUMENT",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
memory_type = MemoryType(str(op.get("memory_type") or "user"))
|
||||
try:
|
||||
existing = await service.get_memory_model(memory_type=memory_type)
|
||||
if memory_type == MemoryType.USER:
|
||||
content_data = op.get("user_content", {})
|
||||
base = UserMemoryContent.model_validate(existing.content) if existing else UserMemoryContent()
|
||||
patch = UserMemoryContent.model_validate(content_data)
|
||||
merged = _deep_merge_dict(base.model_dump(), patch.model_dump(exclude_unset=True))
|
||||
validated = UserMemoryContent.model_validate(merged)
|
||||
updated = await service.update_user_memory(content=validated)
|
||||
if action == "update":
|
||||
result = await _apply_update_operation(
|
||||
service=service,
|
||||
memory_type=memory_type,
|
||||
op=op,
|
||||
)
|
||||
else:
|
||||
content_data = op.get("work_content", {})
|
||||
base = WorkProfileContent.model_validate(existing.content) if existing else WorkProfileContent()
|
||||
patch = WorkProfileContent.model_validate(content_data)
|
||||
merged = _deep_merge_dict(base.model_dump(), patch.model_dump(exclude_unset=True))
|
||||
validated = WorkProfileContent.model_validate(merged)
|
||||
updated = await service.update_work_memory(content=validated)
|
||||
result = await _apply_delete_operation(
|
||||
service=service,
|
||||
memory_type=memory_type,
|
||||
op=op,
|
||||
)
|
||||
|
||||
success_count += 1
|
||||
updated_types.append(memory_type.value)
|
||||
memory_id = str(getattr(updated, "id", "") or (getattr(existing, "id", "") if existing else "") or "")
|
||||
result_items.append({"idx": idx, "memoryType": memory_type.value, "status": "success", "memoryId": memory_id})
|
||||
forgotten_total += int(result.get("forgotten") or 0)
|
||||
result_items.append(
|
||||
{
|
||||
"idx": idx,
|
||||
"memoryType": memory_type.value,
|
||||
"action": action,
|
||||
"status": "success",
|
||||
**result,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
failed_count += 1
|
||||
code, message, retryable = map_memory_exception(exc)
|
||||
failed_ops.append({"memory_type": memory_type.value, "code": code, "message": message, "retryable": retryable})
|
||||
result_items.append({"idx": idx, "memoryType": memory_type.value, "status": "failure", "code": code})
|
||||
failed_ops.append(
|
||||
{
|
||||
"memory_type": memory_type.value,
|
||||
"code": code,
|
||||
"message": message,
|
||||
"retryable": retryable,
|
||||
}
|
||||
)
|
||||
result_items.append(
|
||||
{
|
||||
"idx": idx,
|
||||
"memoryType": memory_type.value,
|
||||
"action": action,
|
||||
"status": "failure",
|
||||
"code": code,
|
||||
}
|
||||
)
|
||||
|
||||
status = _batch_status(success_count, failed_count)
|
||||
error = None
|
||||
error_info = None
|
||||
if failed_ops:
|
||||
first = failed_ops[0]
|
||||
error = {"code": first.get("code", "MEMORY_BATCH_FAILED"), "message": first.get("message", "memory batch write failed"), "retryable": bool(first.get("retryable"))}
|
||||
error_info = map_memory_error(error) if isinstance(error, dict) else None
|
||||
return CliCommandResult(
|
||||
ok=status != "failure",
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
data={
|
||||
"status": status,
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"updated_types": updated_types,
|
||||
"results": result_items,
|
||||
},
|
||||
error=error_info,
|
||||
)
|
||||
|
||||
|
||||
async def handle_memory_forget(request: CliCommand) -> CliCommandResult:
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
operations = request.args.get("operations")
|
||||
if not isinstance(operations, list):
|
||||
operations = []
|
||||
async with AsyncSessionLocal() as session:
|
||||
service = create_memories_service(session=session, owner_id=UUID(request.owner_id))
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
forgotten_total = 0
|
||||
processed_types: list[str] = []
|
||||
failed_ops: list[dict[str, Any]] = []
|
||||
result_items: list[dict[str, Any]] = []
|
||||
|
||||
for idx, op in enumerate(operations):
|
||||
memory_type = MemoryType(op.get("memory_type", "user"))
|
||||
forget_paths = op.get("forget_paths", [])
|
||||
try:
|
||||
existing = await service.get_memory_model(memory_type=memory_type)
|
||||
if existing is None:
|
||||
success_count += 1
|
||||
processed_types.append(memory_type.value)
|
||||
result_items.append({"idx": idx, "memoryType": memory_type.value, "status": "success", "forgotten": 0, "memoryId": ""})
|
||||
continue
|
||||
|
||||
if memory_type == MemoryType.USER:
|
||||
base = UserMemoryContent.model_validate(existing.content)
|
||||
updated_dict, removed = _remove_content_paths(base.model_dump(), forget_paths)
|
||||
validated = UserMemoryContent.model_validate(updated_dict)
|
||||
await service.update_user_memory(content=validated)
|
||||
else:
|
||||
base = WorkProfileContent.model_validate(existing.content)
|
||||
updated_dict, removed = _remove_content_paths(base.model_dump(), forget_paths)
|
||||
validated = WorkProfileContent.model_validate(updated_dict)
|
||||
await service.update_work_memory(content=validated)
|
||||
|
||||
forgotten_total += len(removed)
|
||||
success_count += 1
|
||||
processed_types.append(memory_type.value)
|
||||
result_items.append({"idx": idx, "memoryType": memory_type.value, "status": "success", "forgotten": len(removed), "memoryId": str(getattr(existing, "id", "") or "")})
|
||||
except Exception as exc:
|
||||
failed_count += 1
|
||||
code, message, retryable = map_memory_exception(exc)
|
||||
failed_ops.append({"memory_type": memory_type.value, "code": code, "message": message, "retryable": retryable})
|
||||
result_items.append({"idx": idx, "memoryType": memory_type.value, "status": "failure", "code": code})
|
||||
|
||||
status = _batch_status(success_count, failed_count)
|
||||
error = None
|
||||
if failed_ops:
|
||||
first = failed_ops[0]
|
||||
error = {"code": first.get("code", "MEMORY_BATCH_FAILED"), "message": first.get("message", "memory batch forget failed"), "retryable": bool(first.get("retryable"))}
|
||||
error_info = map_memory_error(error) if isinstance(error, dict) else None
|
||||
error_info = ErrorInfo(
|
||||
code=str(first.get("code") or "MEMORY_BATCH_FAILED"),
|
||||
message=str(first.get("message") or "memory batch update failed"),
|
||||
retryable=bool(first.get("retryable")),
|
||||
)
|
||||
|
||||
return CliCommandResult(
|
||||
ok=status != "failure",
|
||||
command=request.command,
|
||||
@@ -138,21 +141,104 @@ async def handle_memory_forget(request: CliCommand) -> CliCommandResult:
|
||||
"status": status,
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"updated_types": sorted(set(updated_types)),
|
||||
"forgotten": forgotten_total,
|
||||
"processed_types": processed_types,
|
||||
"results": result_items,
|
||||
},
|
||||
error=error_info,
|
||||
)
|
||||
|
||||
|
||||
def map_memory_error(error: dict[str, Any]):
|
||||
from schemas.agent.runtime_models import ErrorInfo
|
||||
async def _apply_update_operation(
|
||||
*,
|
||||
service: Any,
|
||||
memory_type: MemoryType,
|
||||
op: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
existing = await service.get_memory_model(memory_type=memory_type)
|
||||
if memory_type == MemoryType.USER:
|
||||
content_data = op.get("user_content")
|
||||
if not isinstance(content_data, dict):
|
||||
raise ValueError("update action for user memory requires user_content")
|
||||
base = (
|
||||
UserMemoryContent.model_validate(existing.content)
|
||||
if existing
|
||||
else UserMemoryContent()
|
||||
)
|
||||
patch = UserMemoryContent.model_validate(content_data)
|
||||
merged = _deep_merge_dict(
|
||||
base.model_dump(),
|
||||
patch.model_dump(exclude_unset=True),
|
||||
)
|
||||
validated = UserMemoryContent.model_validate(merged)
|
||||
updated = await service.update_user_memory(content=validated)
|
||||
else:
|
||||
content_data = op.get("work_content")
|
||||
if not isinstance(content_data, dict):
|
||||
raise ValueError("update action for work memory requires work_content")
|
||||
base = (
|
||||
WorkProfileContent.model_validate(existing.content)
|
||||
if existing
|
||||
else WorkProfileContent()
|
||||
)
|
||||
patch = WorkProfileContent.model_validate(content_data)
|
||||
merged = _deep_merge_dict(
|
||||
base.model_dump(),
|
||||
patch.model_dump(exclude_unset=True),
|
||||
)
|
||||
validated = WorkProfileContent.model_validate(merged)
|
||||
updated = await service.update_work_memory(content=validated)
|
||||
|
||||
return ErrorInfo(
|
||||
code=str(error.get("code", "MEMORY_BATCH_FAILED")),
|
||||
message=str(error.get("message", "memory operation failed")),
|
||||
retryable=bool(error.get("retryable", False)),
|
||||
memory_id = str(
|
||||
getattr(updated, "id", "")
|
||||
or (getattr(existing, "id", "") if existing else "")
|
||||
or ""
|
||||
)
|
||||
return {"memoryId": memory_id, "forgotten": 0}
|
||||
|
||||
|
||||
async def _apply_delete_operation(
|
||||
*,
|
||||
service: Any,
|
||||
memory_type: MemoryType,
|
||||
op: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
forget_paths_raw = op.get("forget_paths")
|
||||
if not isinstance(forget_paths_raw, list) or not forget_paths_raw:
|
||||
raise ValueError("delete action requires non-empty forget_paths")
|
||||
forget_paths = [
|
||||
str(path).strip() for path in forget_paths_raw if str(path).strip()
|
||||
]
|
||||
if not forget_paths:
|
||||
raise ValueError("delete action requires non-empty forget_paths")
|
||||
|
||||
existing = await service.get_memory_model(memory_type=memory_type)
|
||||
if existing is None:
|
||||
return {"memoryId": "", "forgotten": 0}
|
||||
|
||||
if memory_type == MemoryType.USER:
|
||||
base = UserMemoryContent.model_validate(existing.content)
|
||||
updated_dict, removed = _remove_content_paths(base.model_dump(), forget_paths)
|
||||
validated = UserMemoryContent.model_validate(updated_dict)
|
||||
await service.update_user_memory(content=validated)
|
||||
else:
|
||||
base = WorkProfileContent.model_validate(existing.content)
|
||||
updated_dict, removed = _remove_content_paths(base.model_dump(), forget_paths)
|
||||
validated = WorkProfileContent.model_validate(updated_dict)
|
||||
await service.update_work_memory(content=validated)
|
||||
|
||||
return {
|
||||
"memoryId": str(getattr(existing, "id", "") or ""),
|
||||
"forgotten": len(removed),
|
||||
}
|
||||
|
||||
|
||||
def _invalid_argument(*, request: CliCommand, message: str) -> CliCommandResult:
|
||||
return CliCommandResult(
|
||||
ok=False,
|
||||
command=request.command,
|
||||
subcommand=request.subcommand,
|
||||
error=ErrorInfo(code="INVALID_ARGUMENT", message=message, retryable=False),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.agentscope.tools.cli.handler_calendar import (
|
||||
handle_calendar_create,
|
||||
handle_calendar_delete,
|
||||
handle_calendar_read,
|
||||
handle_calendar_share,
|
||||
handle_calendar_write,
|
||||
)
|
||||
from core.agentscope.tools.cli.handler_contacts import handle_contacts_lookup
|
||||
from core.agentscope.tools.cli.handler_memory import (
|
||||
handle_memory_forget,
|
||||
handle_memory_write,
|
||||
handle_calendar_update,
|
||||
)
|
||||
from core.agentscope.tools.cli.handler_contacts import handle_contacts_read
|
||||
from core.agentscope.tools.cli.handler_memory import handle_memory_update
|
||||
from core.agentscope.tools.cli.router import CommandRouter
|
||||
|
||||
|
||||
def build_router() -> CommandRouter:
|
||||
router = CommandRouter()
|
||||
router.register(command="calendar", subcommand="create", handler=handle_calendar_create)
|
||||
router.register(command="calendar", subcommand="read", handler=handle_calendar_read)
|
||||
router.register(command="calendar", subcommand="write", handler=handle_calendar_write)
|
||||
router.register(command="calendar", subcommand="update", handler=handle_calendar_update)
|
||||
router.register(command="calendar", subcommand="delete", handler=handle_calendar_delete)
|
||||
router.register(command="calendar", subcommand="share", handler=handle_calendar_share)
|
||||
router.register(command="contacts", subcommand="lookup", handler=handle_contacts_lookup)
|
||||
router.register(command="memory", subcommand="write", handler=handle_memory_write)
|
||||
router.register(command="memory", subcommand="forget", handler=handle_memory_forget)
|
||||
router.register(command="contacts", subcommand="read", handler=handle_contacts_read)
|
||||
router.register(command="memory", subcommand="update", handler=handle_memory_update)
|
||||
return router
|
||||
|
||||
@@ -31,7 +31,7 @@ def make_project_cli_wrapper(*, allowed_commands: set[str]) -> Any:
|
||||
|
||||
Args:
|
||||
command: The command to execute (calendar, contacts, memory).
|
||||
subcommand: The subcommand for the operation (read, write, lookup, etc.).
|
||||
subcommand: The subcommand for the operation (calendar: create/read/update/delete/share; contacts: read; memory: update).
|
||||
args: Arguments for the command as a JSON object.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -41,24 +41,51 @@ Call `project_cli` with:
|
||||
|
||||
Use this whenever the user asks what is scheduled, free, upcoming, or happening in a time range.
|
||||
|
||||
### Write Events
|
||||
### Create Event
|
||||
|
||||
Call `project_cli` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "calendar",
|
||||
"subcommand": "write",
|
||||
"subcommand": "create",
|
||||
"args": {
|
||||
"operations": []
|
||||
"title": "Project sync",
|
||||
"start_at": "2026-04-21T10:00:00+08:00",
|
||||
"end_at": "2026-04-21T11:00:00+08:00",
|
||||
"event_timezone": "Asia/Shanghai"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each operation object requires:
|
||||
- `action`: `create`, `update`, or `delete`
|
||||
- create requires `start_at`, `event_timezone`
|
||||
- update/delete require `event_id`
|
||||
### Update Event
|
||||
|
||||
Call `project_cli` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "calendar",
|
||||
"subcommand": "update",
|
||||
"args": {
|
||||
"event_id": "<uuid>",
|
||||
"title": "Updated title"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Event
|
||||
|
||||
Call `project_cli` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "calendar",
|
||||
"subcommand": "delete",
|
||||
"args": {
|
||||
"event_id": "<uuid>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Read first if you need to confirm the write payload shape instead of relying on memory.
|
||||
|
||||
@@ -81,14 +108,14 @@ Call `project_cli` with:
|
||||
|
||||
1. To share an event with a friend:
|
||||
- Call `view_skill_file` with `contacts/SKILL.md` if contacts instructions have not been read in this run
|
||||
- Call `project_cli` `contacts lookup` to find friend phone numbers
|
||||
- Call `project_cli` `contacts read` to find friend phone numbers
|
||||
- Call `project_cli` `calendar share` with the selected phone
|
||||
|
||||
2. To update a specific event:
|
||||
- Call `project_cli` `calendar read` to find the event_id
|
||||
- Call `project_cli` `calendar write` with action `update`
|
||||
- Call `project_cli` `calendar read` to find the event_id
|
||||
- Call `project_cli` `calendar update` with target fields
|
||||
|
||||
## Failure Recovery
|
||||
|
||||
- If `calendar write` returns partial success, report which items failed and suggest retrying only those.
|
||||
- If `calendar share` fails for a phone, suggest verifying the phone number with `contacts lookup`.
|
||||
- If `calendar create/update/delete` returns failure, report why and suggest retrying with corrected parameters.
|
||||
- If `calendar share` fails for a phone, suggest verifying the phone number with `contacts read`.
|
||||
|
||||
@@ -14,7 +14,7 @@ description: Contact lookup - find friend information including phone numbers fo
|
||||
## When to Use
|
||||
|
||||
- User wants to share something with a friend but needs their contact info
|
||||
- Agent needs phone numbers to pass to `calendar_share`
|
||||
- Agent needs phone numbers to pass to `calendar share`
|
||||
- User asks about their friend list
|
||||
|
||||
## Available Tool
|
||||
@@ -23,14 +23,14 @@ Use the single tool `project_cli`.
|
||||
|
||||
Read this file first with `view_skill_file` when contacts is the relevant skill.
|
||||
|
||||
### Lookup Contacts
|
||||
### Read Contacts
|
||||
|
||||
Call `project_cli` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "contacts",
|
||||
"subcommand": "lookup",
|
||||
"subcommand": "read",
|
||||
"args": {}
|
||||
}
|
||||
```
|
||||
@@ -43,7 +43,7 @@ Returns:
|
||||
|
||||
1. To share an event:
|
||||
- Call `view_skill_file` with `calendar/SKILL.md` if calendar instructions have not been read in this run
|
||||
- Call `project_cli` `contacts lookup` to get friend candidates
|
||||
- Call `project_cli` `contacts read` to get friend candidates
|
||||
- Match user's description to a friend
|
||||
- Call `project_cli` `calendar share` with the friend's phone
|
||||
|
||||
|
||||
@@ -24,43 +24,39 @@ Use the single tool `project_cli`.
|
||||
|
||||
Read this file first with `view_skill_file` when memory is the relevant skill.
|
||||
|
||||
### Write Memory
|
||||
### Update Memory
|
||||
|
||||
Call `project_cli` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "memory",
|
||||
"subcommand": "write",
|
||||
"subcommand": "update",
|
||||
"args": {
|
||||
"operations": []
|
||||
"operations": [
|
||||
{
|
||||
"action": "update",
|
||||
"memory_type": "user",
|
||||
"user_content": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Operation objects use `memory_type` (`user` or `work`) plus matching content.
|
||||
|
||||
### Forget Memory
|
||||
|
||||
Call `project_cli` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "memory",
|
||||
"subcommand": "forget",
|
||||
"args": {
|
||||
"operations": []
|
||||
}
|
||||
}
|
||||
```
|
||||
Operation object fields:
|
||||
- `action`: `update` or `delete`
|
||||
- `memory_type`: `user` or `work`
|
||||
- `update` requires matching content payload (`user_content` / `work_content`)
|
||||
- `delete` requires `forget_paths`
|
||||
|
||||
## Composition Patterns
|
||||
|
||||
1. When user says "remember that I prefer morning meetings":
|
||||
- Call `project_cli` `memory write` with `memory_type=user` and appropriate content
|
||||
- Call `project_cli` `memory update` with `action=update`, `memory_type=user`, and appropriate content
|
||||
|
||||
2. When user says "forget my old address":
|
||||
- Call `project_cli` `memory forget` with the specific dot-path
|
||||
- Call `project_cli` `memory update` with `action=delete` and the specific dot-path
|
||||
|
||||
## Failure Recovery
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from schemas.agent.runtime_models import ToolAgentOutput, ToolStatus
|
||||
from schemas.agent.ui_hints import UiHintIntent, UiHintsPayload, UiHintStatus
|
||||
|
||||
|
||||
def _resolve_command_key(tool_output: ToolAgentOutput) -> tuple[str, str] | None:
|
||||
@@ -28,71 +30,262 @@ def _result_data(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def _status_from_tool(tool_output: ToolAgentOutput) -> UiHintStatus:
|
||||
if tool_output.status == ToolStatus.SUCCESS:
|
||||
return UiHintStatus.SUCCESS
|
||||
if tool_output.status == ToolStatus.PARTIAL:
|
||||
return UiHintStatus.WARNING
|
||||
if tool_output.status == ToolStatus.FAILURE:
|
||||
return UiHintStatus.ERROR
|
||||
return UiHintStatus.INFO
|
||||
|
||||
|
||||
def _status_from_result_item(value: object) -> UiHintStatus:
|
||||
text = str(value or "").strip().lower()
|
||||
if text == "success":
|
||||
return UiHintStatus.SUCCESS
|
||||
if text == "partial":
|
||||
return UiHintStatus.WARNING
|
||||
if text == "failure":
|
||||
return UiHintStatus.ERROR
|
||||
return UiHintStatus.INFO
|
||||
|
||||
|
||||
def _build_status_ui_hints(
|
||||
*,
|
||||
tool_output: ToolAgentOutput,
|
||||
intent: UiHintIntent,
|
||||
title: str,
|
||||
description: str,
|
||||
items: list[dict[str, Any]],
|
||||
list_title: str,
|
||||
list_items: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
payload = UiHintsPayload.model_validate(
|
||||
{
|
||||
"intent": intent,
|
||||
"status": _status_from_tool(tool_output),
|
||||
"title": title,
|
||||
"description": description,
|
||||
"items": items,
|
||||
"sections": [{"title": list_title, "listItems": list_items}],
|
||||
}
|
||||
)
|
||||
return payload.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
|
||||
|
||||
def _results_list(data: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
raw = data.get("results")
|
||||
return [item for item in raw if isinstance(item, dict)] if isinstance(raw, list) else []
|
||||
|
||||
|
||||
def _calendar_read_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
data = _result_data(tool_output)
|
||||
if data is None:
|
||||
return None
|
||||
return {"view": "calendar_event_list", "total": data.get("total", 0)}
|
||||
|
||||
items_raw = data.get("items")
|
||||
events = [item for item in items_raw if isinstance(item, dict)] if isinstance(items_raw, list) else []
|
||||
list_items: list[dict[str, Any]] = []
|
||||
|
||||
for event in events:
|
||||
event_id = str(event.get("id") or "").strip()
|
||||
title = str(event.get("title") or "").strip()
|
||||
start_at = str(event.get("startAt") or "").strip()
|
||||
end_at = str(event.get("endAt") or "").strip()
|
||||
subtitle = f"{start_at} ~ {end_at}" if start_at and end_at else (start_at or end_at or None)
|
||||
list_items.append(
|
||||
{
|
||||
"id": event_id or None,
|
||||
"title": title or "日程",
|
||||
"subtitle": subtitle,
|
||||
"status": UiHintStatus.INFO.value,
|
||||
}
|
||||
)
|
||||
|
||||
return _build_status_ui_hints(
|
||||
tool_output=tool_output,
|
||||
intent=UiHintIntent.LIST,
|
||||
title="日程查询结果",
|
||||
description="仅展示本次查询返回的日程列表。",
|
||||
items=[
|
||||
{"key": "total", "label": "日程数量", "value": int(data.get("total") or len(events))},
|
||||
],
|
||||
list_title="日程列表",
|
||||
list_items=list_items,
|
||||
)
|
||||
|
||||
|
||||
def _calendar_write_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
def _calendar_mutation_ui_hints(
|
||||
*,
|
||||
tool_output: ToolAgentOutput,
|
||||
action_label: str,
|
||||
) -> dict[str, Any] | None:
|
||||
data = _result_data(tool_output)
|
||||
if data is None:
|
||||
return None
|
||||
return {
|
||||
"view": "calendar_batch_result",
|
||||
"status": data.get("status", tool_output.status.value),
|
||||
"results": data.get("results", []),
|
||||
}
|
||||
|
||||
success_count = int(data.get("success") or 0)
|
||||
failed_count = int(data.get("failed") or 0)
|
||||
list_items: list[dict[str, Any]] = []
|
||||
|
||||
for item in _results_list(data):
|
||||
event_id = str(item.get("eventId") or "").strip()
|
||||
status = _status_from_result_item(item.get("status")).value
|
||||
code = str(item.get("code") or "").strip()
|
||||
changed_fields = item.get("changedFields")
|
||||
field_text = (
|
||||
",".join([str(field).strip() for field in changed_fields if str(field).strip()])
|
||||
if isinstance(changed_fields, list)
|
||||
else ""
|
||||
)
|
||||
subtitle_parts: list[str] = []
|
||||
if event_id:
|
||||
subtitle_parts.append(f"event_id={event_id}")
|
||||
if field_text:
|
||||
subtitle_parts.append(f"fields={field_text}")
|
||||
if code:
|
||||
subtitle_parts.append(f"code={code}")
|
||||
list_items.append(
|
||||
{
|
||||
"id": event_id or None,
|
||||
"title": f"日程{action_label}",
|
||||
"subtitle": " / ".join(subtitle_parts) if subtitle_parts else None,
|
||||
"status": status,
|
||||
}
|
||||
)
|
||||
|
||||
return _build_status_ui_hints(
|
||||
tool_output=tool_output,
|
||||
intent=UiHintIntent.STATUS,
|
||||
title=f"日程{action_label}结果",
|
||||
description=f"仅展示本次日程{action_label}调用结果。",
|
||||
items=[
|
||||
{"key": "success", "label": "成功", "value": success_count},
|
||||
{"key": "failed", "label": "失败", "value": failed_count},
|
||||
{
|
||||
"key": "status",
|
||||
"label": "总体状态",
|
||||
"value": str(data.get("status") or tool_output.status.value),
|
||||
},
|
||||
],
|
||||
list_title="执行明细",
|
||||
list_items=list_items,
|
||||
)
|
||||
|
||||
|
||||
def _calendar_share_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
def _calendar_create_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
return _calendar_mutation_ui_hints(tool_output=tool_output, action_label="创建")
|
||||
|
||||
|
||||
def _calendar_update_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
return _calendar_mutation_ui_hints(tool_output=tool_output, action_label="更新")
|
||||
|
||||
|
||||
def _calendar_delete_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
return _calendar_mutation_ui_hints(tool_output=tool_output, action_label="删除")
|
||||
|
||||
|
||||
def _memory_update_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
data = _result_data(tool_output)
|
||||
if data is None:
|
||||
return None
|
||||
return {
|
||||
"view": "calendar_share_result",
|
||||
"status": data.get("status", tool_output.status.value),
|
||||
"results": data.get("results", []),
|
||||
}
|
||||
|
||||
success_count = int(data.get("success") or 0)
|
||||
failed_count = int(data.get("failed") or 0)
|
||||
updated_types = data.get("updated_types")
|
||||
updated = ", ".join(updated_types) if isinstance(updated_types, list) else ""
|
||||
forgotten_total = int(data.get("forgotten") or 0)
|
||||
list_items: list[dict[str, Any]] = []
|
||||
|
||||
for item in _results_list(data):
|
||||
memory_type = str(item.get("memoryType") or "memory").strip()
|
||||
memory_id = str(item.get("memoryId") or "").strip()
|
||||
action = str(item.get("action") or "update").strip()
|
||||
forgotten = int(item.get("forgotten") or 0)
|
||||
status = _status_from_result_item(item.get("status")).value
|
||||
subtitle_parts: list[str] = []
|
||||
if memory_id:
|
||||
subtitle_parts.append(f"memory_id={memory_id}")
|
||||
if action:
|
||||
subtitle_parts.append(f"action={action}")
|
||||
if forgotten:
|
||||
subtitle_parts.append(f"forgotten={forgotten}")
|
||||
list_items.append(
|
||||
{
|
||||
"id": memory_id or None,
|
||||
"title": f"{memory_type} memory",
|
||||
"subtitle": " / ".join(subtitle_parts) if subtitle_parts else None,
|
||||
"status": status,
|
||||
}
|
||||
)
|
||||
|
||||
return _build_status_ui_hints(
|
||||
tool_output=tool_output,
|
||||
intent=UiHintIntent.STATUS,
|
||||
title="记忆更新结果",
|
||||
description="仅展示本次 memory.update 的结构化状态。",
|
||||
items=[
|
||||
{"key": "success", "label": "成功", "value": success_count},
|
||||
{"key": "failed", "label": "失败", "value": failed_count},
|
||||
{
|
||||
"key": "updated_types",
|
||||
"label": "已更新类型",
|
||||
"value": updated,
|
||||
},
|
||||
{"key": "forgotten", "label": "已清理条目", "value": forgotten_total},
|
||||
],
|
||||
list_title="执行明细",
|
||||
list_items=list_items,
|
||||
)
|
||||
|
||||
|
||||
def _contacts_lookup_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
def _contacts_read_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
data = _result_data(tool_output)
|
||||
if data is None:
|
||||
return None
|
||||
return {"view": "contact_list", "friends_count": data.get("friends_count", 0)}
|
||||
|
||||
contacts_raw = data.get("friends")
|
||||
contacts = [item for item in contacts_raw if isinstance(item, dict)] if isinstance(contacts_raw, list) else []
|
||||
list_items: list[dict[str, Any]] = []
|
||||
|
||||
for item in contacts:
|
||||
user_id = str(item.get("userId") or "").strip()
|
||||
username = str(item.get("username") or "").strip()
|
||||
phone = str(item.get("phone") or "").strip()
|
||||
list_items.append(
|
||||
{
|
||||
"id": user_id or None,
|
||||
"title": username or phone or "联系人",
|
||||
"subtitle": phone or None,
|
||||
"status": UiHintStatus.INFO.value,
|
||||
}
|
||||
)
|
||||
|
||||
return _build_status_ui_hints(
|
||||
tool_output=tool_output,
|
||||
intent=UiHintIntent.LIST,
|
||||
title="联系人读取结果",
|
||||
description="仅展示当前可用联系人列表。",
|
||||
items=[
|
||||
{
|
||||
"key": "friends_count",
|
||||
"label": "联系人数量",
|
||||
"value": int(data.get("friends_count") or len(contacts)),
|
||||
}
|
||||
],
|
||||
list_title="联系人列表",
|
||||
list_items=list_items,
|
||||
)
|
||||
|
||||
|
||||
def _memory_write_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
data = _result_data(tool_output)
|
||||
if data is None:
|
||||
return None
|
||||
return {
|
||||
"view": "memory_batch_result",
|
||||
"status": data.get("status", tool_output.status.value),
|
||||
"updated_types": data.get("updated_types", []),
|
||||
}
|
||||
|
||||
|
||||
def _memory_forget_ui_hints(tool_output: ToolAgentOutput) -> dict[str, Any] | None:
|
||||
data = _result_data(tool_output)
|
||||
if data is None:
|
||||
return None
|
||||
return {
|
||||
"view": "memory_batch_result",
|
||||
"status": data.get("status", tool_output.status.value),
|
||||
"forgotten": data.get("forgotten", 0),
|
||||
}
|
||||
|
||||
|
||||
_UI_HINTS_BUILDERS: dict[tuple[str, str], Any] = {
|
||||
_UI_HINTS_BUILDERS: dict[tuple[str, str], Callable[[ToolAgentOutput], dict[str, Any] | None]] = {
|
||||
("calendar", "create"): _calendar_create_ui_hints,
|
||||
("calendar", "read"): _calendar_read_ui_hints,
|
||||
("calendar", "write"): _calendar_write_ui_hints,
|
||||
("calendar", "share"): _calendar_share_ui_hints,
|
||||
("contacts", "lookup"): _contacts_lookup_ui_hints,
|
||||
("memory", "write"): _memory_write_ui_hints,
|
||||
("memory", "forget"): _memory_forget_ui_hints,
|
||||
("calendar", "update"): _calendar_update_ui_hints,
|
||||
("calendar", "delete"): _calendar_delete_ui_hints,
|
||||
("contacts", "read"): _contacts_read_ui_hints,
|
||||
("memory", "update"): _memory_update_ui_hints,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from core.agentscope.tools.internal.project_cli import PROJECT_CLI_TOOL_NAME
|
||||
from core.agentscope.tools.internal.view_skill_file import VIEW_SKILL_FILE_TOOL_NAME
|
||||
from core.agentscope.tools.tool_middleware import register_tool_middlewares
|
||||
from core.logging import get_logger
|
||||
from schemas.agent.skill_config import SkillName
|
||||
from schemas.agent.skill_config import ProjectCliCommand, SkillName
|
||||
|
||||
_logger = get_logger("core.agentscope.tools.toolkit")
|
||||
|
||||
@@ -26,9 +26,21 @@ def _validate_enabled_skill_names(skill_names: set[str]) -> set[str]:
|
||||
return skill_names
|
||||
|
||||
|
||||
def _all_command_names() -> set[str]:
|
||||
return {command.value for command in ProjectCliCommand}
|
||||
|
||||
|
||||
def _validate_allowed_commands(command_names: set[str]) -> set[str]:
|
||||
unknown = command_names - _all_command_names()
|
||||
if unknown:
|
||||
raise ValueError(f"unknown commands in allowed_commands: {sorted(unknown)}")
|
||||
return command_names
|
||||
|
||||
|
||||
def build_toolkit(
|
||||
*,
|
||||
enabled_skill_names: set[str] | None = None,
|
||||
allowed_commands: set[str] | None = None,
|
||||
enable_hitl: bool | None = None,
|
||||
) -> Any:
|
||||
from agentscope.tool import Toolkit
|
||||
@@ -40,9 +52,14 @@ def build_toolkit(
|
||||
|
||||
toolkit = Toolkit()
|
||||
|
||||
allowed_commands = enabled_skills
|
||||
if allowed_commands is None:
|
||||
resolved_allowed_commands = _all_command_names()
|
||||
else:
|
||||
resolved_allowed_commands = _validate_allowed_commands(allowed_commands)
|
||||
|
||||
project_cli_wrapper = make_project_cli_wrapper(allowed_commands=allowed_commands)
|
||||
project_cli_wrapper = make_project_cli_wrapper(
|
||||
allowed_commands=resolved_allowed_commands
|
||||
)
|
||||
toolkit.register_tool_function(
|
||||
project_cli_wrapper,
|
||||
func_name=PROJECT_CLI_TOOL_NAME,
|
||||
|
||||
@@ -24,3 +24,4 @@ agents:
|
||||
enabled_skills:
|
||||
- calendar
|
||||
- contacts
|
||||
- memory
|
||||
|
||||
@@ -11,6 +11,12 @@ class SkillName(str, Enum):
|
||||
MEMORY = "memory"
|
||||
|
||||
|
||||
class ProjectCliCommand(str, Enum):
|
||||
CALENDAR = "calendar"
|
||||
CONTACTS = "contacts"
|
||||
MEMORY = "memory"
|
||||
|
||||
|
||||
class EnabledSkillConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from schemas.agent.skill_config import SkillName
|
||||
from schemas.agent.skill_config import ProjectCliCommand, SkillName
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ class SystemAgentLLMConfig(BaseModel):
|
||||
default_factory=ContextMessagesConfig
|
||||
)
|
||||
enabled_skills: list[SkillName] = Field(default_factory=list, max_length=32)
|
||||
allowed_commands: list[ProjectCliCommand] = Field(
|
||||
default_factory=lambda: [command for command in ProjectCliCommand],
|
||||
max_length=32,
|
||||
)
|
||||
|
||||
@field_validator("enabled_skills", mode="before")
|
||||
@classmethod
|
||||
@@ -49,3 +53,23 @@ class SystemAgentLLMConfig(BaseModel):
|
||||
if skill not in normalized:
|
||||
normalized.append(skill)
|
||||
return normalized
|
||||
|
||||
@field_validator("allowed_commands", mode="before")
|
||||
@classmethod
|
||||
def _normalize_allowed_commands(cls, value: object) -> list[ProjectCliCommand]:
|
||||
if value is None:
|
||||
return [command for command in ProjectCliCommand]
|
||||
if not isinstance(value, list):
|
||||
raise ValueError("allowed_commands must be a list")
|
||||
normalized: list[ProjectCliCommand] = []
|
||||
for item in value:
|
||||
if isinstance(item, ProjectCliCommand):
|
||||
command = item
|
||||
else:
|
||||
raw_item = str(item or "").strip()
|
||||
if not raw_item:
|
||||
continue
|
||||
command = ProjectCliCommand(raw_item)
|
||||
if command not in normalized:
|
||||
normalized.append(command)
|
||||
return normalized
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Protocol
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from schemas.agent.skill_config import SkillName
|
||||
from schemas.agent.skill_config import ProjectCliCommand, SkillName
|
||||
from schemas.enums import AutomationJobStatus, ScheduleType
|
||||
|
||||
|
||||
@@ -74,6 +74,10 @@ class RuntimeConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled_skills: list[SkillName] = Field(default_factory=list, max_length=32)
|
||||
allowed_commands: list[ProjectCliCommand] = Field(
|
||||
default_factory=lambda: [command for command in ProjectCliCommand],
|
||||
max_length=32,
|
||||
)
|
||||
context: MessageContextConfig = Field(default_factory=MessageContextConfig)
|
||||
|
||||
|
||||
@@ -81,6 +85,10 @@ class AutomationJobConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled_skills: list[SkillName] | None = Field(default=None, max_length=32)
|
||||
allowed_commands: list[ProjectCliCommand] | None = Field(
|
||||
default=None,
|
||||
max_length=32,
|
||||
)
|
||||
context: MessageContextConfig | None = None
|
||||
input_template: str | None = Field(default=None, min_length=1, max_length=4000)
|
||||
schedule: ScheduleConfig | None = None
|
||||
|
||||
@@ -159,10 +159,15 @@ class HistoryMessage(BaseModel):
|
||||
|
||||
id: str = Field(description="Message UUID")
|
||||
seq: int = Field(description="Message sequence number")
|
||||
role: Literal["user", "assistant"] = Field(
|
||||
description="Message role: user | assistant"
|
||||
role: Literal["user", "assistant", "tool"] = Field(
|
||||
description="Message role: user | assistant | tool"
|
||||
)
|
||||
content: str = Field(description="Message text content")
|
||||
suggested_actions: list[str] = Field(
|
||||
default_factory=list,
|
||||
alias="suggestedActions",
|
||||
description="Suggested follow-up prompts for assistant messages",
|
||||
)
|
||||
attachments: list[HistoryMessageAttachment] = Field(
|
||||
default_factory=list,
|
||||
description="Temporary signed URLs for user-attached images",
|
||||
|
||||
@@ -530,8 +530,6 @@ class AgentService:
|
||||
)
|
||||
for msg_dict in raw_messages:
|
||||
msg = AgentChatMessage.model_validate(msg_dict)
|
||||
if msg.role == "tool":
|
||||
continue
|
||||
|
||||
signed_urls: dict[str, str] = {}
|
||||
attachments = extract_user_message_attachments(msg.metadata)
|
||||
|
||||
@@ -16,6 +16,7 @@ from schemas.domain.automation import (
|
||||
MessageContextConfig,
|
||||
RuntimeConfig,
|
||||
)
|
||||
from schemas.agent.skill_config import ProjectCliCommand
|
||||
|
||||
|
||||
def _default_system_agents_path() -> Path:
|
||||
@@ -97,7 +98,12 @@ def build_runtime_config_from_system_agents(
|
||||
if worker_config and worker_config.enabled_skills:
|
||||
enabled_skills = list(worker_config.enabled_skills)
|
||||
|
||||
allowed_commands = [command for command in ProjectCliCommand]
|
||||
if worker_config and worker_config.allowed_commands:
|
||||
allowed_commands = list(worker_config.allowed_commands)
|
||||
|
||||
return RuntimeConfig(
|
||||
enabled_skills=enabled_skills,
|
||||
allowed_commands=allowed_commands,
|
||||
context=context_cfg,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from core.agentscope.runtime.ui_compiler import compile as compile_ui_hints
|
||||
from schemas.agent.ui_hints import UiHintsPayload
|
||||
from schemas.domain.chat_message import (
|
||||
AgentChatMessage,
|
||||
AgentChatMessageMetadata,
|
||||
@@ -28,7 +30,8 @@ def convert_message_to_history(
|
||||
|
||||
转换规则:
|
||||
- role=user: 读取 metadata.user_message_attachments,转换为 attachments[]
|
||||
- role=assistant: 仅返回文本内容,不生成 ui_schema(UI 由 tool 结果单独承载)
|
||||
- role=assistant: 返回 answer 文本 + suggested_actions
|
||||
- role=tool: 从 metadata.tool_agent_output.ui_hints 编译并返回 ui_schema
|
||||
"""
|
||||
role = message.role
|
||||
content = message.content
|
||||
@@ -43,15 +46,77 @@ def convert_message_to_history(
|
||||
"seq": message.seq,
|
||||
"role": role,
|
||||
"content": content,
|
||||
"suggestedActions": _extract_suggested_actions(metadata),
|
||||
"timestamp": message.timestamp.isoformat(),
|
||||
}
|
||||
|
||||
ui_schema = _extract_tool_ui_schema(metadata)
|
||||
if ui_schema is not None:
|
||||
result["ui_schema"] = ui_schema
|
||||
|
||||
if attachments:
|
||||
result["attachments"] = attachments
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_suggested_actions(
|
||||
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
|
||||
) -> list[str]:
|
||||
if metadata is None:
|
||||
return []
|
||||
|
||||
if isinstance(metadata, AgentChatMessageMetadata):
|
||||
output = metadata.agent_output
|
||||
if output is None:
|
||||
return []
|
||||
actions = output.suggested_actions
|
||||
elif isinstance(metadata, dict):
|
||||
output = metadata.get("agent_output")
|
||||
if not isinstance(output, dict):
|
||||
return []
|
||||
actions = output.get("suggested_actions")
|
||||
else:
|
||||
return []
|
||||
|
||||
if not isinstance(actions, list):
|
||||
return []
|
||||
normalized: list[str] = []
|
||||
for item in actions:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
text = item.strip()
|
||||
if text:
|
||||
normalized.append(text)
|
||||
return normalized
|
||||
|
||||
|
||||
def _extract_tool_ui_schema(
|
||||
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
if metadata is None:
|
||||
return None
|
||||
|
||||
raw_ui_hints: Any = None
|
||||
if isinstance(metadata, AgentChatMessageMetadata):
|
||||
tool_output = metadata.tool_agent_output
|
||||
if tool_output is not None:
|
||||
raw_ui_hints = tool_output.ui_hints
|
||||
elif isinstance(metadata, dict):
|
||||
tool_output = metadata.get("tool_agent_output")
|
||||
if isinstance(tool_output, dict):
|
||||
raw_ui_hints = tool_output.get("ui_hints")
|
||||
|
||||
if raw_ui_hints is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
ui_hints = UiHintsPayload.model_validate(raw_ui_hints)
|
||||
return compile_ui_hints(ui_hints)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _convert_user_attachments(
|
||||
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
|
||||
get_signed_url_fn: Callable[[dict[str, str]], str] | None,
|
||||
|
||||
Reference in New Issue
Block a user