Merge origin/feature/N7Web into feature/N7Web
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
This commit is contained in:
@@ -49,6 +49,16 @@ from cognitive_agent.application.direct_can_control import ( # noqa: E402
|
||||
build_direct_can_intent,
|
||||
)
|
||||
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime # noqa: E402
|
||||
from cognitive_agent.application.mode_creation_seat_policy import ( # noqa: E402
|
||||
ModeCreationSeatPolicyError,
|
||||
creation_seat_scopes,
|
||||
project_seed_operations,
|
||||
project_seed_plan,
|
||||
project_seed_schedule,
|
||||
)
|
||||
from cognitive_agent.application.mode_device_additions import ( # noqa: E402
|
||||
mode_device_additions,
|
||||
)
|
||||
from cognitive_agent.application.mode_profile import ( # noqa: E402
|
||||
ModeProfile,
|
||||
ModeProfileRegistry,
|
||||
@@ -57,6 +67,14 @@ from cognitive_agent.application.mode_profile import ( # noqa: E402
|
||||
from cognitive_agent.application.mode_scene_ingress import ( # noqa: E402
|
||||
ModeSceneIngressConsumer,
|
||||
)
|
||||
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
|
||||
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_scene_catalog import ( # noqa: E402
|
||||
DEFAULT_N7_SCENE_SERVER_ID,
|
||||
DEFAULT_N7_SCENE_SERVER_NAME,
|
||||
@@ -64,13 +82,8 @@ from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
|
||||
ModeSceneMqttBurstPublisher,
|
||||
build_mode_scene_catalog,
|
||||
)
|
||||
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
|
||||
is_mode_profile_query,
|
||||
normalize_mode_profile_name,
|
||||
parse_mode_profile_create,
|
||||
parse_mode_profile_delete,
|
||||
parse_mode_profile_update,
|
||||
)
|
||||
from cognitive_agent.application.mode_schedule import ModeSchedule, ModeScheduleRunner # noqa: E402
|
||||
from cognitive_agent.application.mode_schedule_service import ModeScheduleServiceMixin # noqa: E402
|
||||
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
|
||||
APPLY_PRESET_CAPABILITY,
|
||||
DYNAMIC_MODE_STOP_CAPABILITY,
|
||||
@@ -103,6 +116,10 @@ from cognitive_agent.application.native_can_runtime import ( # noqa: E402
|
||||
NativeCanRuntime,
|
||||
)
|
||||
from cognitive_agent.application.scene_templates import SceneTemplateRegistry # noqa: E402
|
||||
from cognitive_agent.application.schedule_intent import ( # noqa: E402
|
||||
is_schedule_stop,
|
||||
needs_mode_schedule,
|
||||
)
|
||||
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
|
||||
from cognitive_agent.application.semantic_ir import ( # noqa: E402
|
||||
apply_language_safety_guards,
|
||||
@@ -356,8 +373,9 @@ DYNAMIC_MODE_WORDS = (
|
||||
)
|
||||
|
||||
|
||||
class SeatDemoService:
|
||||
class SeatDemoService(ModeScheduleServiceMixin):
|
||||
def __init__(self, repository_root: Path = REPOSITORY_ROOT):
|
||||
self.mode_schedule_runner = ModeScheduleRunner()
|
||||
self.root = repository_root
|
||||
self.domain_root = repository_root / "domains" / "seat"
|
||||
self.contracts = ContractValidator(repository_root)
|
||||
@@ -1190,7 +1208,7 @@ class SeatDemoService:
|
||||
|
||||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||
profile = self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
|
||||
if not profile.command_flow or not profile.command_flow.get("plans"):
|
||||
if profile.schedule is None and (not profile.command_flow or not profile.command_flow.get("plans")):
|
||||
raise ValueError("该模式尚未生成可直接执行的控制指令流。")
|
||||
result = self.process(
|
||||
profile.instruction or f"进入{profile.name}",
|
||||
@@ -1205,6 +1223,8 @@ class SeatDemoService:
|
||||
control_transport=control_transport,
|
||||
_direct_mode_profile_id=profile.id,
|
||||
)
|
||||
if profile.schedule is not None:
|
||||
return result
|
||||
public_profile = profile.public_dict()
|
||||
result["provider_used"] = "saved_mode_command_flow"
|
||||
result["utterance"] = profile.name
|
||||
@@ -1323,7 +1343,7 @@ class SeatDemoService:
|
||||
):
|
||||
raise ValueError("功能模式草稿未通过安全校验,请重新生成。")
|
||||
|
||||
return {
|
||||
trial = {
|
||||
"request_id": clean_request_id,
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
@@ -1336,6 +1356,14 @@ class SeatDemoService:
|
||||
"validation": deepcopy(validation),
|
||||
"trial_source": "server_validated_preview_artifact",
|
||||
}
|
||||
schedule = self._validated_schedule_artifact(source_result)
|
||||
if schedule is not None:
|
||||
trial["schedule"] = schedule.to_dict()
|
||||
trial["schedule_execution"] = self.mode_schedule_runner.simulate(
|
||||
schedule, lambda phase: {"status": "succeeded", "phase_id": phase.id,
|
||||
"operations": deepcopy(list(phase.operations))},
|
||||
)
|
||||
return trial
|
||||
|
||||
def _persist_mode_profile_card(
|
||||
self,
|
||||
@@ -1534,6 +1562,11 @@ class SeatDemoService:
|
||||
# generation workflow.
|
||||
"execution_steps": [],
|
||||
}
|
||||
schedule = self._validated_schedule_artifact(source_result)
|
||||
if profile is not None and profile.schedule is not None and schedule is None:
|
||||
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
|
||||
if schedule is not None:
|
||||
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
|
||||
saved = self.mode_profiles.save_mode(
|
||||
clean_name,
|
||||
operations,
|
||||
@@ -1553,6 +1586,7 @@ class SeatDemoService:
|
||||
upstream_origin=(
|
||||
profile.upstream_origin if profile is not None else ""
|
||||
),
|
||||
schedule=schedule,
|
||||
expected_revision=draft_existing_mode_revision or None,
|
||||
create_only=not bool(clean_mode_id),
|
||||
)
|
||||
@@ -1753,6 +1787,7 @@ class SeatDemoService:
|
||||
self._vehicle_mqtt_transport.start()
|
||||
|
||||
def close(self) -> None:
|
||||
self.mode_schedule_runner.close()
|
||||
if self._vehicle_state_monitor is not None:
|
||||
self._vehicle_state_monitor.close()
|
||||
self._mode_scene_mqtt_publisher.close()
|
||||
@@ -2761,6 +2796,32 @@ class SeatDemoService:
|
||||
or mode_profile_query
|
||||
):
|
||||
execution_mode = "dry_run"
|
||||
schedule_requested = "模式" in utterance or _direct_mode_profile_id or is_schedule_stop(utterance)
|
||||
schedule_vehicle_id = vehicle_id or (self.vehicle_profiles.default().id if schedule_requested else "")
|
||||
if execution_mode not in {"dry_run", "simulated_hardware", "hardware"}:
|
||||
raise ValueError(f"unsupported execution mode: {execution_mode}")
|
||||
if is_schedule_stop(utterance):
|
||||
return self._cancel_mode_schedule(schedule_vehicle_id, conversation_id, utterance, execution_mode)
|
||||
schedule_profile = (self.mode_profiles.get_profile(_direct_mode_profile_id, schedule_vehicle_id)
|
||||
if _direct_mode_profile_id else None)
|
||||
if schedule_requested and schedule_profile is None and not (draft_update or mode_delete_name or mode_profile_query or parse_mode_profile_create(utterance)):
|
||||
candidates = self.mode_profiles.match_text(utterance, vehicle_id=schedule_vehicle_id)
|
||||
if len(candidates) == 1 and candidates[0].schedule is not None:
|
||||
aliases = "|".join(re.escape(name) for name in (candidates[0].name, *candidates[0].aliases))
|
||||
if re.fullmatch(
|
||||
r"(?:(?:请|麻烦|帮我|帮忙|给我|我想|我要|想要)\s*){0,4}"
|
||||
r"(?:(?:主驾|副驾|驾驶位|副驾驶|主驾驶)(?:座椅)?\s*)?"
|
||||
r"(?:进入|启动|执行|开启|启用|运行|打开|调用|应用|使用|切换到)\s*"
|
||||
rf"(?:{aliases})(?:吧|一下)?[。!!\s]*", utterance.strip(),
|
||||
):
|
||||
schedule_profile = candidates[0]
|
||||
if schedule_profile is not None and schedule_profile.schedule is not None:
|
||||
return self._run_mode_schedule(
|
||||
schedule_profile, utterance=utterance, conversation_id=conversation_id,
|
||||
execution_mode=execution_mode, approval_token=approval_token, vehicle_id=schedule_vehicle_id,
|
||||
simulation_fault=simulation_fault, hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=hardware_access_bypass, control_transport=control_transport,
|
||||
)
|
||||
if execution_mode != "hardware":
|
||||
return self._process_once(
|
||||
utterance,
|
||||
@@ -2899,6 +2960,8 @@ class SeatDemoService:
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
allow_fuzzy_mode_profile_match: bool = True,
|
||||
allow_scene_composition: bool = False,
|
||||
scene_scope_utterance: str | None = None,
|
||||
mode_creation_scopes: frozenset[str] | None = None,
|
||||
mode_profile_draft_context: dict[str, Any] | None = None,
|
||||
cloud_instruction_context: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -3158,6 +3221,8 @@ class SeatDemoService:
|
||||
mode_requests, n7_transfer_actions = (
|
||||
self._mode_requests_and_n7_transfers(direct_mode_profile)
|
||||
)
|
||||
if mode_creation_scopes is not None:
|
||||
mode_requests = project_seed_operations(mode_requests, mode_creation_scopes)
|
||||
detected_domains = list(
|
||||
dict.fromkeys(
|
||||
str(item.get("domain") or "")
|
||||
@@ -3253,9 +3318,13 @@ class SeatDemoService:
|
||||
model_parse_source=model_parse_source,
|
||||
assistant_plan=assistant_plan,
|
||||
allow_scene_composition=allow_scene_composition,
|
||||
scene_scope_utterance=scene_scope_utterance,
|
||||
cloud_instruction_context=cloud_instruction_context,
|
||||
)
|
||||
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
|
||||
scene_label_only = assistant_plan is not None and allow_scene_composition
|
||||
dynamic_mode_stop = (
|
||||
not scene_label_only and self._is_dynamic_mode_stop_request(utterance)
|
||||
)
|
||||
if dynamic_mode_stop:
|
||||
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
|
||||
# Reuse the same conservative clause segmentation for the final
|
||||
@@ -3263,7 +3332,12 @@ class SeatDemoService:
|
||||
# comma between independent controls can leak an early target
|
||||
# (for example ``副驾``) into a later HVAC clause and can also
|
||||
# hide otherwise valid window/temperature actions.
|
||||
segmented_utterance = ",".join(
|
||||
# Name-only scene plans have already been checked against exact
|
||||
# targets by _interpret. Their synthesized 进入<name> label is not
|
||||
# another atomic instruction: reparsing 座椅舒适模式 here applies
|
||||
# the unscoped-seat driver default and discards valid passenger
|
||||
# actions. Explicit user instructions never enter this branch.
|
||||
segmented_utterance = "" if scene_label_only else ",".join(
|
||||
self._split_intent_clauses(utterance)
|
||||
)
|
||||
relative_resolution = (
|
||||
@@ -3345,7 +3419,12 @@ class SeatDemoService:
|
||||
parsed,
|
||||
excluded_control_signatures,
|
||||
)
|
||||
parsed = apply_language_safety_guards(parsed, utterance)
|
||||
parsed = apply_language_safety_guards(
|
||||
parsed,
|
||||
utterance,
|
||||
scene_composition=assistant_plan is not None and allow_scene_composition,
|
||||
scene_scope_utterance=scene_scope_utterance,
|
||||
)
|
||||
parsed = self._partition_native_actions(
|
||||
parsed,
|
||||
promote_supported=(
|
||||
@@ -4413,6 +4492,16 @@ class SeatDemoService:
|
||||
) -> dict[str, Any]:
|
||||
if not matched_modes:
|
||||
return parsed
|
||||
if any(mode.schedule is not None for mode in matched_modes):
|
||||
rejected = deepcopy(parsed)
|
||||
for key in ("actions", "domain_actions", "native_can_actions", "preset_actions",
|
||||
"preset_events", "deferred_requests", "model_requests"):
|
||||
rejected[key] = []
|
||||
rejected["ambiguities"] = [{
|
||||
"field": "mode_profile",
|
||||
"question": "动态模式必须通过时间轴执行,请独立输入“进入模式名称”;不会将各阶段压平执行。",
|
||||
}]
|
||||
return rejected
|
||||
if self._contains_seat_preset_request(parsed):
|
||||
return parsed
|
||||
if len(matched_modes) > 1:
|
||||
@@ -4496,6 +4585,19 @@ class SeatDemoService:
|
||||
mode_requests, n7_transfer_actions = (
|
||||
self._mode_requests_and_n7_transfers(matched_profile)
|
||||
)
|
||||
selected_seat_scope = self._explicit_mode_seat_selection(
|
||||
matched_profile, utterance,
|
||||
)
|
||||
if selected_seat_scope is not None:
|
||||
# A cabin profile can carry independent comfort operations for both
|
||||
# front seats. An explicit single-seat invocation selects only that
|
||||
# seat; it must not authorize its sibling or mutate the stored mode.
|
||||
mode_requests = [
|
||||
request for request in mode_requests
|
||||
if request["domain"] != "seat"
|
||||
or self._normalized_mode_seat_scope(request["target_scope"])
|
||||
in selected_seat_scope
|
||||
]
|
||||
detected_domains = []
|
||||
for request in mode_requests:
|
||||
if request["domain"] not in detected_domains:
|
||||
@@ -4516,6 +4618,24 @@ class SeatDemoService:
|
||||
context,
|
||||
vehicle_profile,
|
||||
)
|
||||
# Native-only comfort capabilities are represented as deferred items by
|
||||
# the generic adapter. A verified stored mode must materialize them too;
|
||||
# otherwise a successful named-mode run silently drops its driver seat.
|
||||
mode_native_actions = []
|
||||
for item in mode_parsed.get("deferred_requests", []):
|
||||
if item.get("execution_backend") != "native_can":
|
||||
continue
|
||||
action = {
|
||||
"domain": item["domain"],
|
||||
"capability": item["capability"],
|
||||
"target": item["target_scope"],
|
||||
"parameters": deepcopy(item.get("parameters", {})),
|
||||
"operation": item.get("desired_change", "set"),
|
||||
"reason": item.get("reason", ""),
|
||||
"execution_backend": "native_can",
|
||||
}
|
||||
if self.native_can_runtime.supports(action):
|
||||
mode_native_actions.append(action)
|
||||
parsed_has_explicit_request = bool(
|
||||
parsed.get("actions")
|
||||
or parsed.get("domain_actions")
|
||||
@@ -4532,6 +4652,10 @@ class SeatDemoService:
|
||||
mode_parsed.get("domain_actions", []),
|
||||
parsed.get("domain_actions", []),
|
||||
)
|
||||
merged["native_can_actions"] = self._merge_control_actions(
|
||||
mode_native_actions,
|
||||
parsed.get("native_can_actions", []),
|
||||
)
|
||||
for key in (
|
||||
"preset_actions",
|
||||
"preset_events",
|
||||
@@ -4583,13 +4707,35 @@ class SeatDemoService:
|
||||
merged["_saved_mode_local_peripheral"] = True
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
def _explicit_mode_target_conflict(
|
||||
cls,
|
||||
profile: ModeProfile,
|
||||
utterance: str,
|
||||
) -> tuple[str, tuple[str, ...]] | None:
|
||||
"""Reject a named mode when its persisted seat scope contradicts text."""
|
||||
|
||||
requested, normalized_scopes = cls._explicit_mode_seat_scopes(profile, utterance)
|
||||
if requested is None or requested == normalized_scopes:
|
||||
return None
|
||||
if cls._explicit_mode_seat_selection(profile, utterance) is not None:
|
||||
return None
|
||||
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
|
||||
|
||||
@staticmethod
|
||||
def _normalized_mode_seat_scope(scope: str) -> str:
|
||||
return (
|
||||
"driver" if scope == "front_left" else
|
||||
"passenger" if scope == "front_right" else
|
||||
"rear" if scope in {"rear", "rear_left", "rear_right"} else scope
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _explicit_mode_seat_scopes(
|
||||
cls,
|
||||
profile: ModeProfile,
|
||||
utterance: str,
|
||||
) -> tuple[frozenset[str] | None, set[str]]:
|
||||
aliases = (profile.id, profile.name, *profile.aliases)
|
||||
mode_clause = next(
|
||||
(
|
||||
@@ -4605,23 +4751,34 @@ class SeatDemoService:
|
||||
utterance,
|
||||
)
|
||||
requested = resolve_explicit_seat_scopes(
|
||||
mode_clause,
|
||||
mode_clause.replace("主副驾", "主驾和副驾"),
|
||||
assume_seat_context=True,
|
||||
)
|
||||
if requested is None:
|
||||
return None
|
||||
|
||||
normalized_scopes = {
|
||||
"driver" if operation.target_scope == "front_left" else
|
||||
"passenger" if operation.target_scope == "front_right" else
|
||||
"rear" if operation.target_scope in {"rear", "rear_left", "rear_right"} else
|
||||
operation.target_scope
|
||||
cls._normalized_mode_seat_scope(operation.target_scope)
|
||||
for operation in profile.operations
|
||||
if operation.domain == "seat"
|
||||
}
|
||||
if requested == normalized_scopes:
|
||||
return requested, normalized_scopes
|
||||
|
||||
@classmethod
|
||||
def _explicit_mode_seat_selection(
|
||||
cls,
|
||||
profile: ModeProfile,
|
||||
utterance: str,
|
||||
) -> frozenset[str] | None:
|
||||
"""Scope a dual-seat mode invocation, never mode creation or editing."""
|
||||
requested, supported = cls._explicit_mode_seat_scopes(profile, utterance)
|
||||
if (
|
||||
requested is None
|
||||
or len(requested) != 1
|
||||
or supported != {"driver", "passenger"}
|
||||
or not requested.issubset(supported)
|
||||
or cls.is_mode_profile_configuration_request(utterance)
|
||||
or not re.search(r"进入|开启|启动|执行|启用|运行|切换|打开|调用|应用|使用", utterance)
|
||||
):
|
||||
return None
|
||||
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
|
||||
return requested
|
||||
|
||||
@staticmethod
|
||||
def _apply_implicit_mode_target_policy(
|
||||
@@ -4954,7 +5111,9 @@ class SeatDemoService:
|
||||
for issue in validation
|
||||
):
|
||||
raise ValueError("待保存功能模式草稿没有通过完整安全校验。")
|
||||
operations = self._preview_mode_operations(source_result)
|
||||
schedule = self._validated_schedule_artifact(source_result)
|
||||
operations = ([deepcopy(op) for phase in schedule.phases for op in phase.operations]
|
||||
if schedule is not None else self._preview_mode_operations(source_result))
|
||||
operation_count = draft.get("operation_count")
|
||||
if (
|
||||
not isinstance(operation_count, int)
|
||||
@@ -5035,6 +5194,17 @@ class SeatDemoService:
|
||||
"confidence": 1.0,
|
||||
}
|
||||
|
||||
def _mode_device_default_operations(
|
||||
self,
|
||||
mode_name: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return registered static-scene defaults for a bounded device append."""
|
||||
|
||||
template = self.scene_templates.resolve_exact_name(mode_name)
|
||||
if template is None or template.schedule is not None:
|
||||
return []
|
||||
return [operation.to_yaml() for operation in template.operations]
|
||||
|
||||
@staticmethod
|
||||
def _mode_update_target_is_explicit(change_text: str, target: str) -> bool:
|
||||
patterns = {
|
||||
@@ -5084,79 +5254,99 @@ class SeatDemoService:
|
||||
vehicle_id=vehicle_profile.id,
|
||||
)
|
||||
)
|
||||
if source_draft.get("schedule") is not None:
|
||||
from cognitive_agent.application.mode_schedule_edit import revise_schedule
|
||||
|
||||
return revise_schedule(
|
||||
self, ModeSchedule.from_dict(source_draft["schedule"]),
|
||||
change_text=change_text, utterance=utterance, name=mode_name,
|
||||
conversation_id=conversation_id, vehicle_profile=vehicle_profile,
|
||||
context=context, control_transport=control_transport,
|
||||
source_draft=source_draft,
|
||||
)
|
||||
base_operations = self._absolute_mode_operations(source_operations)
|
||||
if not change_text:
|
||||
raise ValueError("请说明要调整的设备和明确目标值。")
|
||||
parseable_change_text = re.sub(
|
||||
r"改为|改成|变更为|换成|变成|变为",
|
||||
"调整为",
|
||||
registered_additions = mode_device_additions(
|
||||
change_text,
|
||||
base_operations,
|
||||
self._mode_device_default_operations(mode_name),
|
||||
)
|
||||
unresolved_change_clauses = [
|
||||
clause
|
||||
for clause, _parsed, status in self._local_clause_results(
|
||||
parseable_change_text,
|
||||
context,
|
||||
if registered_additions is not None:
|
||||
normalized_changes = self._absolute_mode_operations(
|
||||
registered_additions
|
||||
)
|
||||
if status != "complete"
|
||||
]
|
||||
if unresolved_change_clauses and assistant_plan is None:
|
||||
raise ValueError(
|
||||
"调整说明包含未完整识别的内容:"
|
||||
+ ";".join(unresolved_change_clauses)
|
||||
else:
|
||||
parseable_change_text = re.sub(
|
||||
r"改为|改成|变更为|换成|变成|变为",
|
||||
"调整为",
|
||||
change_text,
|
||||
)
|
||||
change_result = self._process_once(
|
||||
parseable_change_text,
|
||||
provider,
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
approval_token,
|
||||
vehicle_profile.id,
|
||||
simulation_fault,
|
||||
control_transport=control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
allow_fuzzy_mode_profile_match=False,
|
||||
)
|
||||
change_operations = self._absolute_mode_operations(
|
||||
self._preview_mode_operations(change_result)
|
||||
)
|
||||
if (
|
||||
change_result.get("status") != "succeeded"
|
||||
or not change_operations
|
||||
or change_result.get("supplement_required") is True
|
||||
or bool(change_result.get("unresolved_clauses"))
|
||||
or bool(change_result.get("deferred_requests"))
|
||||
):
|
||||
raise ValueError("没有从调整说明中得到完整、可复用的设备目标。")
|
||||
validation = change_result.get("validation")
|
||||
if isinstance(validation, list) and any(
|
||||
isinstance(issue, dict) and issue.get("severity") == "error"
|
||||
for issue in validation
|
||||
):
|
||||
raise ValueError("调整动作没有通过车辆能力或安全校验。")
|
||||
|
||||
normalized_changes: list[dict[str, Any]] = []
|
||||
for change in change_operations:
|
||||
same_capability = [
|
||||
item
|
||||
for item in base_operations
|
||||
if item["domain"] == change["domain"]
|
||||
and item["capability"] == change["capability"]
|
||||
]
|
||||
normalized = deepcopy(change)
|
||||
if len(same_capability) == 1 and not self._mode_update_target_is_explicit(
|
||||
change_text,
|
||||
str(change["target_scope"]),
|
||||
):
|
||||
normalized["target_scope"] = same_capability[0]["target_scope"]
|
||||
elif len(same_capability) > 1 and not self._mode_update_target_is_explicit(
|
||||
change_text,
|
||||
str(change["target_scope"]),
|
||||
):
|
||||
raise ValueError(
|
||||
f"“{mode_name}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
|
||||
unresolved_change_clauses = [
|
||||
clause
|
||||
for clause, _parsed, status in self._local_clause_results(
|
||||
parseable_change_text,
|
||||
context,
|
||||
)
|
||||
normalized_changes.append(normalized)
|
||||
if status != "complete"
|
||||
]
|
||||
if unresolved_change_clauses and assistant_plan is None:
|
||||
raise ValueError(
|
||||
"调整说明包含未完整识别的内容:"
|
||||
+ ";".join(unresolved_change_clauses)
|
||||
)
|
||||
change_result = self._process_once(
|
||||
parseable_change_text,
|
||||
provider,
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
approval_token,
|
||||
vehicle_profile.id,
|
||||
simulation_fault,
|
||||
control_transport=control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
allow_fuzzy_mode_profile_match=False,
|
||||
)
|
||||
change_operations = self._absolute_mode_operations(
|
||||
self._preview_mode_operations(change_result)
|
||||
)
|
||||
if (
|
||||
change_result.get("status") != "succeeded"
|
||||
or not change_operations
|
||||
or change_result.get("supplement_required") is True
|
||||
or bool(change_result.get("unresolved_clauses"))
|
||||
or bool(change_result.get("deferred_requests"))
|
||||
):
|
||||
raise ValueError("没有从调整说明中得到完整、可复用的设备目标。")
|
||||
validation = change_result.get("validation")
|
||||
if isinstance(validation, list) and any(
|
||||
isinstance(issue, dict) and issue.get("severity") == "error"
|
||||
for issue in validation
|
||||
):
|
||||
raise ValueError("调整动作没有通过车辆能力或安全校验。")
|
||||
|
||||
normalized_changes = []
|
||||
for change in change_operations:
|
||||
same_capability = [
|
||||
item
|
||||
for item in base_operations
|
||||
if item["domain"] == change["domain"]
|
||||
and item["capability"] == change["capability"]
|
||||
]
|
||||
normalized = deepcopy(change)
|
||||
if len(same_capability) == 1 and not self._mode_update_target_is_explicit(
|
||||
change_text,
|
||||
str(change["target_scope"]),
|
||||
):
|
||||
normalized["target_scope"] = same_capability[0]["target_scope"]
|
||||
elif len(same_capability) > 1 and not self._mode_update_target_is_explicit(
|
||||
change_text,
|
||||
str(change["target_scope"]),
|
||||
):
|
||||
raise ValueError(
|
||||
f"“{mode_name}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
|
||||
)
|
||||
normalized_changes.append(normalized)
|
||||
|
||||
change_by_key = {
|
||||
(
|
||||
@@ -5433,6 +5623,14 @@ class SeatDemoService:
|
||||
if profile is None:
|
||||
self._persist(result)
|
||||
return result
|
||||
if profile.schedule is not None:
|
||||
from cognitive_agent.application.mode_schedule_edit import update_saved_schedule
|
||||
|
||||
return update_saved_schedule(
|
||||
self, profile, change_text=change_text, utterance=utterance,
|
||||
conversation_id=conversation_id, vehicle_profile=vehicle_profile,
|
||||
context=context, result=result,
|
||||
)
|
||||
if not change_text:
|
||||
result["status"] = "needs_confirmation"
|
||||
result["validation"] = [
|
||||
@@ -5446,27 +5644,64 @@ class SeatDemoService:
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
parsed, provider_used, provider_notice = self._interpret(
|
||||
registered_additions = mode_device_additions(
|
||||
change_text,
|
||||
provider,
|
||||
context,
|
||||
vehicle_profile,
|
||||
[operation.to_yaml() for operation in profile.operations],
|
||||
self._mode_device_default_operations(mode_name),
|
||||
)
|
||||
parsed = apply_language_safety_guards(parsed, change_text)
|
||||
parsed = self._partition_native_actions(parsed)
|
||||
result["provider_used"] = f"mode_profile_registry+{provider_used}"
|
||||
result["decision_summary"].extend(parsed.get("decision_summary", []))
|
||||
if provider_notice:
|
||||
result["decision_summary"].append(provider_notice)
|
||||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||||
result["model_requests"] = parsed.get("model_requests", [])
|
||||
if registered_additions is not None:
|
||||
parsed = {
|
||||
"decision_summary": [
|
||||
"已从只读场景模板载入缺失设备的登记默认动作。"
|
||||
],
|
||||
"deferred_requests": [],
|
||||
"model_requests": [],
|
||||
}
|
||||
result["provider_used"] = (
|
||||
"mode_profile_registry+registered_scene_template"
|
||||
)
|
||||
result["decision_summary"].extend(parsed["decision_summary"])
|
||||
result["deferred_requests"] = []
|
||||
result["model_requests"] = []
|
||||
mode_operations = self._absolute_mode_operations(
|
||||
registered_additions
|
||||
)
|
||||
source_actions = [
|
||||
{
|
||||
"domain": operation["domain"],
|
||||
"capability": operation["capability"],
|
||||
"target": operation["target_scope"],
|
||||
"operation": operation.get("operation", "set"),
|
||||
"parameters": deepcopy(operation.get("parameters", {})),
|
||||
"reason": operation.get(
|
||||
"reason",
|
||||
"从登记场景模板增补的设备默认动作。",
|
||||
),
|
||||
}
|
||||
for operation in mode_operations
|
||||
]
|
||||
else:
|
||||
parsed, provider_used, provider_notice = self._interpret(
|
||||
change_text,
|
||||
provider,
|
||||
context,
|
||||
vehicle_profile,
|
||||
)
|
||||
parsed = apply_language_safety_guards(parsed, change_text)
|
||||
parsed = self._partition_native_actions(parsed)
|
||||
result["provider_used"] = f"mode_profile_registry+{provider_used}"
|
||||
result["decision_summary"].extend(parsed.get("decision_summary", []))
|
||||
if provider_notice:
|
||||
result["decision_summary"].append(provider_notice)
|
||||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||||
result["model_requests"] = parsed.get("model_requests", [])
|
||||
|
||||
source_actions = [
|
||||
*parsed.get("actions", []),
|
||||
*parsed.get("domain_actions", []),
|
||||
*parsed.get("native_can_actions", []),
|
||||
]
|
||||
mode_operations = self._mode_operations_from_actions(source_actions)
|
||||
source_actions = [
|
||||
*parsed.get("actions", []),
|
||||
*parsed.get("domain_actions", []),
|
||||
*parsed.get("native_can_actions", []),
|
||||
]
|
||||
mode_operations = self._mode_operations_from_actions(source_actions)
|
||||
mode_operations, ambiguous_targets = self._inherit_mode_update_targets(
|
||||
profile,
|
||||
mode_operations,
|
||||
@@ -6100,6 +6335,25 @@ class SeatDemoService:
|
||||
result["scene_config"] = None
|
||||
result["execution_report"] = None
|
||||
|
||||
def _mode_creation_seat_failure(
|
||||
self, utterance: str, conversation_id: str, vehicle: VehicleProfile, reason: str,
|
||||
) -> dict[str, Any]:
|
||||
result = {
|
||||
"request_id": f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}",
|
||||
"utterance": utterance, "conversation_id": conversation_id,
|
||||
"vehicle": vehicle.public_dict(), "status": "validation_failed",
|
||||
"execution_mode": "dry_run", "hardware_execution_allowed": False,
|
||||
"orchestration_route": "mode_profile_create_preview", "trace_events": [],
|
||||
"validation": [{"severity": "error", "code": "MODE_SEAT_SCOPE_UNAVAILABLE",
|
||||
"path": "mode_profile_draft.seat_scope", "message": reason}],
|
||||
"mode_creation_failure": {"code": "MODE_SEAT_SCOPE_UNAVAILABLE", "reason": reason,
|
||||
"next_step": "请选择该模式实际支持的座位,或改用当前座位支持的舒适功能"},
|
||||
"decision_summary": [reason, "没有保存模式或下发控制。"],
|
||||
}
|
||||
self._scrub_failed_scene_preview(result)
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
def _process_mode_profile_create(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -6134,6 +6388,14 @@ class SeatDemoService:
|
||||
):
|
||||
raise ValueError("功能模式名称与创建请求不一致。")
|
||||
requested_instruction = create_parts[1] if create_parts is not None else ""
|
||||
selected_seats = creation_seat_scopes(utterance) if source_draft is None else None
|
||||
if selected_seats is not None and "unsupported" in selected_seats and re.search(
|
||||
r"座椅|座位|靠背|椅背|腿托|脚托|坐垫|坐盆|按摩|零重力", utterance
|
||||
):
|
||||
return self._mode_creation_seat_failure(
|
||||
utterance, conversation_id, vehicle_profile,
|
||||
"当前模式只支持明确的主驾或副驾座椅,不会将未支持的座位替换为其他座位。",
|
||||
)
|
||||
raw_plan_metadata = (
|
||||
assistant_plan.get("metadata")
|
||||
if isinstance(assistant_plan, dict)
|
||||
@@ -6160,6 +6422,7 @@ class SeatDemoService:
|
||||
or requested_instruction
|
||||
or assistant_plan.get("requests")
|
||||
!= candidate_template.plan().get("requests")
|
||||
or assistant_plan.get("schedule") != candidate_template.plan().get("schedule")
|
||||
):
|
||||
raise ValueError("内置常用模式模板已更新,请重新提交。")
|
||||
scene_template = candidate_template
|
||||
@@ -6190,6 +6453,10 @@ class SeatDemoService:
|
||||
if not requested_instruction and source_draft is None
|
||||
else None
|
||||
)
|
||||
if source_profile is None and source_draft is None and not requested_instruction and scene_template is None:
|
||||
source_name = mode_creation_source_name(mode_name)
|
||||
if source_name != mode_name:
|
||||
source_profile = self.mode_profiles.resolve_exact_name(source_name, vehicle_id=vehicle_profile.id)
|
||||
draft_mode_name = (
|
||||
str(source_draft.get("name") or mode_name).strip()
|
||||
if isinstance(source_draft, dict)
|
||||
@@ -6205,7 +6472,7 @@ class SeatDemoService:
|
||||
else "registered_mode_profile"
|
||||
if source_profile is not None
|
||||
else "codex_scene_composition"
|
||||
if assistant_plan is not None and not requested_instruction
|
||||
if assistant_plan is not None and (not requested_instruction or needs_mode_schedule(utterance))
|
||||
else "explicit_scene_requirements"
|
||||
)
|
||||
# A name-only request may already describe a repository-backed scene.
|
||||
@@ -6214,7 +6481,63 @@ class SeatDemoService:
|
||||
# being parsed as a second control clause on the assistant's local fast
|
||||
# path, turning a valid nine-action mode into an incomplete draft.
|
||||
source_utterance = requested_instruction or f"进入{draft_mode_name}"
|
||||
if requested_instruction and selected_seats is not None and len(selected_seats) == 1:
|
||||
# Qualify only explicit seat-device clauses, never an HVAC/window
|
||||
# zone or unrelated control. General one-shot controls keep their
|
||||
# existing target policy; this is scoped to new mode construction.
|
||||
label = "主驾" if selected_seats == {"driver"} else "副驾"
|
||||
instruction_clauses = self._split_intent_clauses(requested_instruction)
|
||||
qualified_clauses = [
|
||||
f"{label}{clause}" if (
|
||||
re.search(r"座椅|座位|靠背|椅背|腿托|脚托|坐垫|坐盆|按摩", clause)
|
||||
and resolve_explicit_seat_scopes(clause, assume_seat_context=True) is None
|
||||
) else clause
|
||||
for clause in instruction_clauses
|
||||
]
|
||||
# Preserve the user's punctuation and wording unless a seat really
|
||||
# needs qualification; explicit instructions need no rewriting.
|
||||
if qualified_clauses != instruction_clauses:
|
||||
source_utterance = ",".join(qualified_clauses)
|
||||
raw_schedule = (assistant_plan.get("schedule") if isinstance(assistant_plan, dict) else None)
|
||||
if source_profile is not None and source_profile.schedule is not None:
|
||||
raw_schedule = source_profile.schedule.to_dict()
|
||||
if raw_schedule is not None:
|
||||
if isinstance(assistant_plan, dict) and assistant_plan.get("ambiguities"):
|
||||
raise ValueError("动态需求仍有未解决的问题,不能生成部分成功的时间轴,请补充或调整需求。")
|
||||
if source_profile is None and source_draft is None and scene_template is None and not needs_mode_schedule(utterance):
|
||||
raise ValueError("用户未请求动态或分阶段模式,不能擅自生成定时任务。")
|
||||
schedule = ModeSchedule.from_dict(raw_schedule)
|
||||
if selected_seats is not None and (source_profile is not None or scene_template is not None):
|
||||
try:
|
||||
schedule = project_seed_schedule(schedule, selected_seats)
|
||||
except ModeCreationSeatPolicyError as exc:
|
||||
return self._mode_creation_seat_failure(utterance, conversation_id, vehicle_profile, str(exc))
|
||||
return self._preview_mode_schedule(
|
||||
schedule, name=draft_mode_name, utterance=utterance,
|
||||
conversation_id=conversation_id, vehicle_profile=vehicle_profile,
|
||||
control_transport=control_transport, generation_source=generation_source,
|
||||
source_profile=existing_profile, source_template=scene_template, source_draft=source_draft,
|
||||
scene_scope_utterance="" if source_draft is not None else utterance,
|
||||
)
|
||||
if needs_mode_schedule(utterance) and scene_template is None and not (source_profile is not None and source_profile.timeline):
|
||||
result = {
|
||||
"request_id": f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}",
|
||||
"utterance": utterance, "conversation_id": conversation_id,
|
||||
"vehicle": vehicle_profile.public_dict(), "validation": [], "trace_events": [],
|
||||
"status": "validation_failed", "execution_mode": "dry_run", "hardware_execution_allowed": False,
|
||||
"orchestration_route": "mode_profile_create_preview",
|
||||
}
|
||||
result["mode_creation_failure"] = {
|
||||
"code": "MODE_SCHEDULE_REQUIRED",
|
||||
"reason": "该需求包含时间变化,但没有生成完整的分阶段时间轴",
|
||||
"next_step": "请重试,或补充每个阶段的设备目标和持续时间;未保存或执行静态替代动作",
|
||||
}
|
||||
self._scrub_failed_scene_preview(result)
|
||||
self._persist(result)
|
||||
return result
|
||||
try:
|
||||
if scene_template is not None and selected_seats is not None:
|
||||
assistant_plan = project_seed_plan(assistant_plan, selected_seats)
|
||||
source_result = self._process_once(
|
||||
source_utterance,
|
||||
provider,
|
||||
@@ -6227,6 +6550,7 @@ class SeatDemoService:
|
||||
direct_mode_profile_id=(
|
||||
source_profile.id if source_profile is not None else None
|
||||
),
|
||||
mode_creation_scopes=selected_seats,
|
||||
assistant_plan=assistant_plan,
|
||||
# ``source_utterance`` is synthesized for a name-only, unknown
|
||||
# scene. It must not fall back to substring matching (for example
|
||||
@@ -6238,7 +6562,14 @@ class SeatDemoService:
|
||||
and source_profile is None
|
||||
and not requested_instruction
|
||||
),
|
||||
# A revision combines a server-loaded draft with changes that
|
||||
# already passed the ordinary text/target authorization path.
|
||||
# Its old display name cannot veto an explicitly requested new
|
||||
# seat. This override is internal, never accepted from payloads.
|
||||
scene_scope_utterance="" if source_draft is not None else utterance,
|
||||
)
|
||||
except ModeCreationSeatPolicyError as exc:
|
||||
return self._mode_creation_seat_failure(utterance, conversation_id, vehicle_profile, str(exc))
|
||||
except NativeCanCompileError:
|
||||
# Repository modes and model-composed scenes share the same native
|
||||
# compiler. A stale mode parameter must become a bounded failed
|
||||
@@ -8276,6 +8607,7 @@ class SeatDemoService:
|
||||
model_parse_source: str = "cloud_model",
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
allow_scene_composition: bool = False,
|
||||
scene_scope_utterance: str | None = None,
|
||||
cloud_instruction_context: dict[str, Any] | None = None,
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
@@ -8338,6 +8670,7 @@ class SeatDemoService:
|
||||
],
|
||||
trusted_local=trusted_local,
|
||||
allow_scene_composition=allow_scene_composition,
|
||||
scene_scope_utterance=scene_scope_utterance,
|
||||
)
|
||||
model = self.model_plan_adapter.adapt(
|
||||
guarded.plan,
|
||||
|
||||
Reference in New Issue
Block a user