"""Phase-aware natural-language edits, always revalidated before persistence.""" from __future__ import annotations import json import re from collections.abc import Callable from copy import deepcopy from typing import Any from cognitive_agent.application.mode_schedule import ModeSchedule _NUMBER = r"[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+" _PHASE = re.compile(r"第(?P[0-9]+|[一二两三四五六七八九十]+)(?:个)?(?:阶段|步)") _ALL_PHASES = re.compile(r"(?:全部|所有|每个|每一|各个|各)(?:的)?阶段") _HOLD = re.compile( rf"(?:持续时间|保持时间|持续|保持|时长|^(?:把|将|的)?(?=调整为|调整到|设置为|设置成|设为|改为|改成|变成|变为))\s*" rf"(?:调整为|调整到|设置为|设置成|设为|改为|改成|变成|变为)?\s*" rf"(?P{_NUMBER})\s*(?P分钟|秒钟|秒|minutes?|mins?|min|seconds?|secs?|s)" ) _REPEAT = re.compile( rf"(?:重复|循环)(?:次数)?\s*(?:调整为|调整到|设置为|设为|改为|改成)?\s*" rf"(?P{_NUMBER})\s*次" ) def _number(value: str) -> float: if re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", value): return float(value) digits = {char: index for index, char in enumerate("零一二三四五六七八九")} if value == "两": return 2 if "十" in value: tens, ones = value.split("十", 1) if (tens and tens not in digits) or (ones and ones not in digits): raise ValueError("请使用有效的整数编号或次数。") return (digits[tens] if tens else 1) * 10 + (digits[ones] if ones else 0) if value in digits: return digits[value] raise ValueError("请使用明确的阶段编号、持续时间或重复次数。") def edit_schedule( schedule: ModeSchedule, change_text: str, *, parse_operations: Callable[[str], list[dict[str, Any]]], target_is_explicit: Callable[[str, str], bool], ) -> ModeSchedule: """Edit one/all phases; an unscoped differing trajectory needs clarification.""" text = change_text.strip() if not text: raise ValueError("请说明要修改的阶段、动作、时长或重复次数。") if re.search(r"(?:秒钟|分钟|秒|s)(?:之后|以后|后|再)", text): raise ValueError( "阶段时间表示动作执行后的保持时长,请明确阶段动作和持续时间,不使用延时后执行的表述。" ) source = schedule.to_dict() selected: set[int] | None = None phase_matches = list(_PHASE.finditer(text)) all_match = _ALL_PHASES.search(text) if all_match: if phase_matches: raise ValueError("请分别修改单个阶段或全部阶段,不要混用阶段范围。") selected = set(range(len(schedule.phases))) text = _ALL_PHASES.sub("", text) elif phase_matches: indexes = {int(_number(match.group("number"))) - 1 for match in phase_matches} if len(indexes) != 1 or any( index < 0 or index >= len(schedule.phases) for index in indexes ): raise ValueError(f"当前只有{len(schedule.phases)}个阶段,请明确一次修改哪一个阶段。") selected = indexes text = _PHASE.sub("", text) else: named = {index for index, phase in enumerate(schedule.phases) if phase.name in text} if len(named) > 1: raise ValueError("请一次修改一个阶段,或明确指定全部阶段。") if named: selected = named phase_name = schedule.phases[next(iter(named))].name text = text.replace(phase_name + "阶段", "").replace(phase_name, "") repeats = list(_REPEAT.finditer(text)) if repeats: if len(repeats) != 1: raise ValueError("请提供一个明确的整条时间轴重复次数。") count = _number(repeats[0].group("number")) if count != int(count): raise ValueError("重复次数必须为整数。") source["repeat_count"] = int(count) text = _REPEAT.sub("", text) holds = list(_HOLD.finditer(text)) if holds: if len(holds) != 1 or selected is None: raise ValueError("修改持续时间需要明确第几个阶段或全部阶段。") seconds = _number(holds[0].group("number")) if holds[0].group("unit") in {"分钟", "minute", "minutes", "min", "mins"}: seconds *= 60 milliseconds = seconds * 1000 if milliseconds != int(milliseconds): raise ValueError("阶段持续时间最多精确到毫秒。") for index in selected: source["phases"][index]["duration_ms"] = int(milliseconds) text = _HOLD.sub("", text) if re.search(rf"(?:{_NUMBER})\s*(?:分钟|秒钟|秒|minutes?|mins?|seconds?|secs?|s)", text): raise ValueError("动态阶段只支持明确绝对设备目标;时长请单独表述为阶段保持时间,不支持按秒运动。") text = re.sub(r"改为|改成|变更为|换成|变成|变为", "调整为", text) text = re.sub( r"^(?:把|将|的|中|里|阶段)+|(?:然后|并且|同时|并|,|,|;|;|。|\s)+$", "", text ).strip("的中里,,;;。 ") changes = parse_operations(text) if text else [] if not changes and not holds and not repeats: raise ValueError("没有识别到可执行的阶段调整,请说明设备目标、阶段时长或重复次数。") for raw_change in changes: change = deepcopy(raw_change) indexes = selected if selected is not None else set(range(len(schedule.phases))) candidates = [ (index, operation) for index in indexes for operation in source["phases"][index]["operations"] if operation["domain"] == change["domain"] and operation["capability"] == change["capability"] ] if not target_is_explicit(text, change["target_scope"]): targets = {operation["target_scope"] for _, operation in candidates} if len(targets) > 1: raise ValueError("该阶段的设备有多个区域,请明确主驾、副驾或具体目标区域。") if targets: change["target_scope"] = next(iter(targets)) candidates = [ (index, operation) for index, operation in candidates if operation["target_scope"] == change["target_scope"] ] if selected is None: values = { json.dumps(operation["parameters"], sort_keys=True, ensure_ascii=False) for _, operation in candidates } if len(values) > 1: raise ValueError("该设备在多个阶段使用不同目标,请明确第几个阶段,或说“全部阶段”。") if not candidates: raise ValueError("新增设备动作需要明确放入第几个阶段,或明确全部阶段。") indexes = {index for index, _ in candidates} key = (change["capability"], change["target_scope"]) for index in indexes: operations = source["phases"][index]["operations"] found = False for position, operation in enumerate(operations): if (operation["capability"], operation["target_scope"]) == key: operations[position] = { **deepcopy(operation), "parameters": deepcopy(change["parameters"]), } found = True if not found: operations.append(deepcopy(change)) return ModeSchedule.from_dict(source) def revise_schedule( service, schedule: ModeSchedule, *, change_text: str, utterance: str, name: str, conversation_id: str, vehicle_profile, context: dict[str, Any], control_transport: str, source_draft=None, source_profile=None, ) -> dict[str, Any]: """Parse changed atoms locally, then recompile every phase before success.""" def parse_changes(text): if any( status != "complete" for _clause, _parsed, status in service._local_clause_results(text, context) ): raise ValueError("阶段调整说明不完整,请使用设备和明确目标值;未修改原时间轴。") result = service._process_once( text, "local", conversation_id, "dry_run", None, vehicle_profile.id, control_transport=control_transport, allow_fuzzy_mode_profile_match=False, ) if ( result.get("status") != "succeeded" or result.get("deferred_requests") or result.get("unresolved_clauses") or result.get("supplement_required") or any(issue.get("severity") == "error" for issue in result.get("validation", [])) ): raise ValueError("阶段动作未通过完整的设备能力或安全校验。") operations = service._absolute_mode_operations(service._preview_mode_operations(result)) if not operations: raise ValueError("未识别到可复用的绝对设备目标。") return operations revised = edit_schedule( schedule, change_text, parse_operations=parse_changes, target_is_explicit=service._mode_update_target_is_explicit, ) preview = service._preview_mode_schedule( revised, name=name, utterance=utterance, conversation_id=conversation_id, vehicle_profile=vehicle_profile, control_transport=control_transport, generation_source="draft_revision" if source_draft else "registered_mode_profile", source_draft=source_draft, source_profile=source_profile, ) if preview.get("status") == "succeeded" and source_draft: draft = preview["mode_profile_draft"] draft["revises_source_request_id"] = source_draft["source_request_id"] for key in ("source_template_id", "source_template_revision"): if key in source_draft: draft[key] = source_draft[key] service._persist(preview) return preview def update_saved_schedule( service, profile, *, change_text: str, utterance: str, conversation_id: str, vehicle_profile, context: dict[str, Any], result: dict[str, Any], ) -> dict[str, Any]: try: preview = revise_schedule( service, profile.schedule, change_text=change_text, utterance=utterance, name=profile.name, conversation_id=conversation_id, vehicle_profile=vehicle_profile, context=context, control_transport="wired_can", source_profile=profile, ) if preview.get("status") != "succeeded": return preview draft = preview["mode_profile_draft"] saved = service.save_mode_profile_card( profile.id, draft["name"], draft["instruction"], draft["source_request_id"], vehicle_profile.id, ) updated = service.mode_profiles.get_profile(profile.id, vehicle_profile.id) report = service._mode_profile_control_report( result["request_id"], "update_mode_profile", updated, f"已更新“{profile.name}”的阶段时间轴;未向车辆下发控制。", {"can_command_emitted": False, "phase_count": len(updated.schedule.phases)}, ) result.update( status="succeeded", execution_mode="dry_run", hardware_execution_allowed=False, orchestration_route="mode_profile_update", provider_used="mode_schedule_editor", mode_profile=saved["mode_profile"], matched_modes=[saved["mode_profile"]], schedule=updated.schedule.to_dict(), execution_report=report, validation=service.contracts.validate(report), mode_profile_changes=[ deepcopy(op) for phase in updated.schedule.phases for op in phase.operations ], decision_summary=[ f"已更新“{profile.name}”的阶段时间轴,并完整重新校验全部阶段;未下发车辆控制。" ], ) except ValueError as exc: result.update( status="needs_confirmation", execution_mode="dry_run", hardware_execution_allowed=False, orchestration_route="mode_profile_update", validation=[ { "severity": "error", "code": "MODE_SCHEDULE_UPDATE_REJECTED", "path": "schedule", "message": str(exc), } ], decision_summary=[f"未修改“{profile.name}”的时间轴:{exc}"], ) service._persist(result) return result