feat: add seat-scoped static and dynamic assistant modes
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user