315 lines
13 KiB
Python
315 lines
13 KiB
Python
"""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<number>[0-9]+|[一二两三四五六七八九十]+)(?:个)?(?:阶段|步)")
|
||
_ALL_PHASES = re.compile(r"(?:全部|所有|每个|每一|各个|各)(?:的)?阶段")
|
||
_HOLD = re.compile(
|
||
rf"(?:持续时间|保持时间|持续|保持|时长|^(?:把|将|的)?(?=调整为|调整到|设置为|设置成|设为|改为|改成|变成|变为))\s*"
|
||
rf"(?:调整为|调整到|设置为|设置成|设为|改为|改成|变成|变为)?\s*"
|
||
rf"(?P<number>{_NUMBER})\s*(?P<unit>分钟|秒钟|秒|minutes?|mins?|min|seconds?|secs?|s)"
|
||
)
|
||
_REPEAT = re.compile(
|
||
rf"(?:重复|循环)(?:次数)?\s*(?:调整为|调整到|设置为|设为|改为|改成)?\s*"
|
||
rf"(?P<number>{_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
|