feat: add seat-scoped static and dynamic assistant modes

This commit is contained in:
vee
2026-09-12 14:56:30 +08:00
parent d09b5967f8
commit d8dad22ec0
60 changed files with 5847 additions and 149 deletions
@@ -20,19 +20,25 @@ from typing import Protocol
from cognitive_agent.application.mode_profile_intent import (
is_mode_profile_query,
mode_creation_source_name,
normalize_mode_profile_name,
parse_mode_profile_create,
parse_mode_profile_delete,
parse_mode_profile_update,
)
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.mode_schedule_service import public_schedule_status
from cognitive_agent.application.scene_composer import (
SCENE_COMPOSER_INSTRUCTIONS,
SCENE_PLANNER_VERSION,
SCHEDULE_COMPOSER_INSTRUCTIONS,
SceneCompositionError,
build_scene_output_schema,
normalize_scene_output,
scene_context_for_mode_creation,
)
from cognitive_agent.application.scene_templates import normalize_scene_template_name
from cognitive_agent.application.schedule_intent import is_schedule_stop, needs_mode_schedule
LOGGER = logging.getLogger(__name__)
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
@@ -374,6 +380,9 @@ def _catalog_exact_mode_match_count(
}:
matched_ids.add(str(raw_mode.get("id") or ""))
matched_ids.discard("")
subject = mode_creation_source_name(requested_name)
if not matched_ids and normalize_mode_profile_name(subject) != needle:
return _catalog_exact_mode_match_count(catalog, subject)
return len(matched_ids)
@@ -475,6 +484,10 @@ def _catalog_exact_scene_template_plan(
template_id = str(raw_template.get("id") or "").strip()
if template_id:
matches[template_id] = raw_template
if not matches:
subject = mode_creation_source_name(requested_name)
if normalize_scene_template_name(subject) != needle:
return _catalog_exact_scene_template_plan(catalog, subject)
if len(matches) != 1:
return None
template = next(iter(matches.values()))
@@ -499,7 +512,12 @@ def _catalog_exact_scene_template_plan(
def _vehicle_result_succeeded(result: Mapping[str, object]) -> bool:
"""Only a terminal, fully successful service result completes a tool call."""
return str(result.get("status") or "").lower() == "succeeded"
schedule = result.get("schedule_execution")
return str(result.get("status") or "").lower() == "succeeded" or (
result.get("status") == "running"
and result.get("orchestration_route") == "mode_schedule_execute"
and isinstance(schedule, Mapping) and schedule.get("status") == "running"
)
def _mode_profile_result_succeeded(result: Mapping[str, object]) -> bool:
@@ -594,6 +612,10 @@ def _vehicle_action_state(result: Mapping[str, object]) -> tuple[str, bool, str]
if not _vehicle_result_succeeded(result):
return "failed", False, "车辆控制未完成"
if result.get("orchestration_route") == "mode_schedule_stop":
return ("simulated" if result.get("execution_mode") != "hardware" else "cancelled"), True, "动态模式停止请求已处理"
if result.get("status") == "running":
return "running", True, "时间轴已启动,尚未完成所有阶段"
execution_mode = str(result.get("execution_mode") or "").lower()
if execution_mode == "dry_run":
return "simulated", True, "车辆控制模拟编排已完成"
@@ -621,6 +643,8 @@ def _authorize_vehicle_request(
message=message,
)
normalized = "".join(str(message).lower().split())
if is_schedule_stop(message):
return _VehicleAuthorization(message, "explicit_schedule_stop")
mode_profile_create = _mode_profile_create_parts(message)
mode_profile_update = _mode_profile_update_parts(message)
mode_profile_delete = parse_mode_profile_delete(message)
@@ -899,6 +923,9 @@ class CodexAssistantService:
"explicit_mode_profile_draft_update",
}
mode_parts = _mode_profile_create_parts(message) if mode_profile_preview else None
if mode_parts:
scene_context = scene_context_for_mode_creation(scene_context, message)
model_context = scene_context_for_mode_creation(model_context, message)
exact_mode_matches = (
_catalog_exact_mode_match_count(catalog, mode_parts[0])
if mode_parts
@@ -909,10 +936,11 @@ class CodexAssistantService:
if mode_parts and not mode_parts[1] and exact_mode_matches == 0
else None
)
dynamic_scene_request = bool(mode_parts and needs_mode_schedule(message))
direct_mode_profile_preview = bool(
mode_parts
and (
mode_parts[1]
(mode_parts[1] and not dynamic_scene_request)
# Ambiguous or unavailable mode catalogs fail closed in the
# local builder. Only a known-zero match is safe to hand to
# semantic scene composition.
@@ -929,7 +957,7 @@ class CodexAssistantService:
}.get(authorization.reason)
scene_composition_preview = bool(
mode_parts
and not mode_parts[1]
and (not mode_parts[1] or dynamic_scene_request)
and exact_mode_matches == 0
and scene_template_plan is None
and scene_context.get("composition_revision")
@@ -941,6 +969,7 @@ class CodexAssistantService:
# vehicle service still limits it to dry-run and requires confirmation.
direct_vehicle_control = (
fast_comfort_control
or authorization.reason == "explicit_schedule_stop"
or exact_quick_control
or saved_mode_execution_id is not None
or direct_mode_profile_preview
@@ -1248,11 +1277,17 @@ class CodexAssistantService:
revision = str(scene_context.get("composition_revision") or "")
if not revision:
raise _ScenePlannerFallback("scene capability revision is unavailable")
scope_policy = scene_context.get("seat_scope_policy")
selected_scopes = (
scope_policy.get("selected_scopes", [])
if isinstance(scope_policy, Mapping) else []
)
scope_key = ",".join(sorted(str(scope) for scope in selected_scopes))
return (
vehicle_id,
revision,
normalize_mode_profile_name(mode_name),
SCENE_PLANNER_VERSION,
f"{SCENE_PLANNER_VERSION}:seats={scope_key}",
)
def _begin_scene_plan(
@@ -1493,7 +1528,8 @@ class CodexAssistantService:
raise _ScenePlannerFallback("structured turns are not supported")
planning_started_at = time.monotonic()
output_schema = build_scene_output_schema(scene_context)
dynamic = needs_mode_schedule(message)
output_schema = build_scene_output_schema(scene_context, dynamic=dynamic)
context_bytes = len(
json.dumps(
dict(scene_context),
@@ -1522,7 +1558,7 @@ class CodexAssistantService:
try:
try:
thread_id = self.client.start_thread(
developer_instructions=SCENE_COMPOSER_INSTRUCTIONS,
developer_instructions=SCHEDULE_COMPOSER_INSTRUCTIONS if dynamic else SCENE_COMPOSER_INSTRUCTIONS,
ephemeral=True,
replace_developer_instructions=True,
timeout=self._remaining_scene_time(deadline),
@@ -1652,7 +1688,7 @@ class CodexAssistantService:
raise SceneCompositionError("scene composer output was not valid JSON") from exc
if not isinstance(raw_output, Mapping):
raise SceneCompositionError("scene composer output must be an object")
normalized = normalize_scene_output(raw_output, scene_context)
normalized = normalize_scene_output(raw_output, scene_context, dynamic=dynamic)
finished_at = time.monotonic()
LOGGER.info(
"Scene Composer model completed context_bytes=%d schema_bytes=%d "
@@ -1887,7 +1923,9 @@ class CodexAssistantService:
"success": successful,
"state": action_state,
"display_text": (
"功能模式预览已生成"
completion_text
if action_state == "running" or result.get("orchestration_route") == "mode_schedule_stop"
else "功能模式预览已生成"
if mode_profile_preview and successful
else "功能模式预览生成失败"
if mode_profile_preview
@@ -2047,6 +2085,13 @@ class CodexAssistantService:
if isinstance(draft, Mapping)
else "新功能模式"
)
if isinstance(draft, Mapping) and isinstance(draft.get("schedule"), dict):
schedule = ModeSchedule.from_dict(draft["schedule"])
return (f"已为你生成“{name}”的分阶段时间轴:{len(schedule.phases)}个阶段,"
f"每轮{schedule.operation_count}项动作,重复{schedule.repeat_count}次,"
f"总时长{schedule.total_duration_ms // 1000}秒。"
"每个阶段的设备目标和持续时间见卡片。当前未保存或执行;确认后保存,进入模式才开始计时。"
"执行中可输入“停止动态模式”取消后续阶段,已下发目标不会自动回滚。")
if (
isinstance(draft, Mapping)
and draft.get("generation_source") == "draft_revision"
@@ -2251,6 +2296,18 @@ class CodexAssistantService:
result: Mapping[str, object],
successful: bool,
) -> str:
if result.get("orchestration_route") == "mode_schedule_stop":
if result.get("execution_mode") == "hardware" and (result.get("schedule_execution") or {}).get("status") != "cancelled":
return "当前没有正在执行的动态时间轴。"
return ("已停止动态模式后续阶段。已下发的设备目标保持,不自动回滚。"
if result.get("execution_mode") == "hardware"
else "已模拟停止动态模式,未改变正在运行的车辆任务。")
if result.get("orchestration_route") == "mode_schedule_execute":
if not successful:
return "时间轴未能完成,后续阶段已停止。请检查车辆状态后重试。"
return ("动态模式时间轴已启动,将按卡片中的时长逐阶段调节。尚未完成所有阶段;输入“停止动态模式”可取消后续阶段。"
if result.get("status") == "running"
else "已按虚拟时钟模拟验证动态模式的全部阶段,未向实际车辆下发操作。")
if reason in {"exact_quick_control", "explicit_saved_mode_execute"}:
instruction = re.sub(
r"\s+",
@@ -3011,7 +3068,7 @@ class CodexAssistantService:
if isinstance(intent_ir, dict):
spec = intent_ir.get("spec")
if isinstance(spec, dict) and isinstance(spec.get("actions"), list):
for item in spec["actions"][:12]:
for item in spec["actions"][:96 if result.get("schedule") else 12]:
if not isinstance(item, dict):
continue
actions.append(
@@ -3125,6 +3182,22 @@ class CodexAssistantService:
)
if key in mode_profile_draft
}
for key in ("mode_profile_draft", "mode_profile"):
source = result.get(key)
if isinstance(source, dict) and isinstance(source.get("schedule"), dict):
try:
safe[key]["schedule"] = ModeSchedule.from_dict(source["schedule"]).to_dict()
except ValueError:
pass
if isinstance(result.get("schedule"), dict):
try:
safe["schedule"] = ModeSchedule.from_dict(result["schedule"]).to_dict()
except ValueError:
pass
if isinstance(result.get("schedule_execution"), dict):
safe["schedule_execution"] = public_schedule_status(result["schedule_execution"])
if isinstance(result.get("vehicle"), dict):
safe["vehicle"] = {"id": str(result["vehicle"].get("id") or "")[:128]}
failure = result.get("mode_creation_failure")
if isinstance(failure, Mapping):
reason, next_step = CodexAssistantService._mode_profile_failure_feedback(