Files
Cognitive_Agent/src/cognitive_agent/application/mode_schedule_edit.py
T

315 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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