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