|
@@ -1,5 +1,6 @@
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
+import json
|
|
|
import re
|
|
import re
|
|
|
from collections.abc import Awaitable, Callable
|
|
from collections.abc import Awaitable, Callable
|
|
|
from copy import deepcopy
|
|
from copy import deepcopy
|
|
@@ -78,7 +79,7 @@ class WebSearchPort(Protocol):
|
|
|
|
|
|
|
|
class InMemorySessionTerminationAdapter:
|
|
class InMemorySessionTerminationAdapter:
|
|
|
def __init__(self) -> None:
|
|
def __init__(self) -> None:
|
|
|
- self._results: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
+ self._results: dict[tuple[str, str], dict[str, Any]] = {}
|
|
|
|
|
|
|
|
def terminate(
|
|
def terminate(
|
|
|
self,
|
|
self,
|
|
@@ -86,19 +87,21 @@ class InMemorySessionTerminationAdapter:
|
|
|
*,
|
|
*,
|
|
|
reason: str | None = None,
|
|
reason: str | None = None,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
- if event_id not in self._results:
|
|
|
|
|
- self._results[event_id] = {
|
|
|
|
|
|
|
+ arguments = {} if reason is None else {"reason": reason}
|
|
|
|
|
+ cache_key = _adapter_cache_key(event_id, arguments)
|
|
|
|
|
+ if cache_key not in self._results:
|
|
|
|
|
+ self._results[cache_key] = {
|
|
|
"tool": "session.terminate",
|
|
"tool": "session.terminate",
|
|
|
"status": "terminated",
|
|
"status": "terminated",
|
|
|
"event_id": event_id,
|
|
"event_id": event_id,
|
|
|
"reason": reason,
|
|
"reason": reason,
|
|
|
}
|
|
}
|
|
|
- return deepcopy(self._results[event_id])
|
|
|
|
|
|
|
+ return deepcopy(self._results[cache_key])
|
|
|
|
|
|
|
|
|
|
|
|
|
class InMemoryDeviceVolumeAdapter:
|
|
class InMemoryDeviceVolumeAdapter:
|
|
|
def __init__(self) -> None:
|
|
def __init__(self) -> None:
|
|
|
- self._results: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
+ self._results: dict[tuple[str, str], dict[str, Any]] = {}
|
|
|
|
|
|
|
|
def adjust(
|
|
def adjust(
|
|
|
self,
|
|
self,
|
|
@@ -108,7 +111,13 @@ class InMemoryDeviceVolumeAdapter:
|
|
|
value: int | None = None,
|
|
value: int | None = None,
|
|
|
delta: int | None = None,
|
|
delta: int | None = None,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
- if event_id not in self._results:
|
|
|
|
|
|
|
+ arguments: dict[str, Any] = {"mode": mode}
|
|
|
|
|
+ if value is not None:
|
|
|
|
|
+ arguments["value"] = value
|
|
|
|
|
+ if delta is not None:
|
|
|
|
|
+ arguments["delta"] = delta
|
|
|
|
|
+ cache_key = _adapter_cache_key(event_id, arguments)
|
|
|
|
|
+ if cache_key not in self._results:
|
|
|
payload: dict[str, Any] = {
|
|
payload: dict[str, Any] = {
|
|
|
"tool": "device.volume.adjust",
|
|
"tool": "device.volume.adjust",
|
|
|
"status": "applied",
|
|
"status": "applied",
|
|
@@ -119,13 +128,13 @@ class InMemoryDeviceVolumeAdapter:
|
|
|
payload["value"] = value
|
|
payload["value"] = value
|
|
|
if delta is not None:
|
|
if delta is not None:
|
|
|
payload["delta"] = delta
|
|
payload["delta"] = delta
|
|
|
- self._results[event_id] = payload
|
|
|
|
|
- return deepcopy(self._results[event_id])
|
|
|
|
|
|
|
+ self._results[cache_key] = payload
|
|
|
|
|
+ return deepcopy(self._results[cache_key])
|
|
|
|
|
|
|
|
|
|
|
|
|
class InMemoryCalendarScheduleAdapter:
|
|
class InMemoryCalendarScheduleAdapter:
|
|
|
def __init__(self) -> None:
|
|
def __init__(self) -> None:
|
|
|
- self._results: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
+ self._results: dict[tuple[str, str], dict[str, Any]] = {}
|
|
|
|
|
|
|
|
def create(
|
|
def create(
|
|
|
self,
|
|
self,
|
|
@@ -137,23 +146,24 @@ class InMemoryCalendarScheduleAdapter:
|
|
|
recurrence: str | None = None,
|
|
recurrence: str | None = None,
|
|
|
reminder_minutes: int | None = None,
|
|
reminder_minutes: int | None = None,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
- if event_id not in self._results:
|
|
|
|
|
- schedule: dict[str, Any] = {
|
|
|
|
|
- "title": title,
|
|
|
|
|
- "start_at": start_at,
|
|
|
|
|
- "timezone": timezone,
|
|
|
|
|
- }
|
|
|
|
|
- if recurrence is not None:
|
|
|
|
|
- schedule["recurrence"] = recurrence
|
|
|
|
|
- if reminder_minutes is not None:
|
|
|
|
|
- schedule["reminder_minutes"] = reminder_minutes
|
|
|
|
|
- self._results[event_id] = {
|
|
|
|
|
|
|
+ schedule: dict[str, Any] = {
|
|
|
|
|
+ "title": title,
|
|
|
|
|
+ "start_at": start_at,
|
|
|
|
|
+ "timezone": timezone,
|
|
|
|
|
+ }
|
|
|
|
|
+ if recurrence is not None:
|
|
|
|
|
+ schedule["recurrence"] = recurrence
|
|
|
|
|
+ if reminder_minutes is not None:
|
|
|
|
|
+ schedule["reminder_minutes"] = reminder_minutes
|
|
|
|
|
+ cache_key = _adapter_cache_key(event_id, schedule)
|
|
|
|
|
+ if cache_key not in self._results:
|
|
|
|
|
+ self._results[cache_key] = {
|
|
|
"tool": "calendar.schedule.create",
|
|
"tool": "calendar.schedule.create",
|
|
|
"status": "created",
|
|
"status": "created",
|
|
|
"event_id": event_id,
|
|
"event_id": event_id,
|
|
|
"schedule": schedule,
|
|
"schedule": schedule,
|
|
|
}
|
|
}
|
|
|
- return deepcopy(self._results[event_id])
|
|
|
|
|
|
|
+ return deepcopy(self._results[cache_key])
|
|
|
|
|
|
|
|
|
|
|
|
|
class InMemoryWebSearchAdapter:
|
|
class InMemoryWebSearchAdapter:
|
|
@@ -292,6 +302,7 @@ def _device_volume_definition(port: DeviceVolumePort) -> EventDefinition:
|
|
|
},
|
|
},
|
|
|
handler=handler,
|
|
handler=handler,
|
|
|
resolver=_resolve_device_volume,
|
|
resolver=_resolve_device_volume,
|
|
|
|
|
+ normalizer=_normalize_device_volume,
|
|
|
fallback_allowed=True,
|
|
fallback_allowed=True,
|
|
|
result_policy=ResultPolicy.SILENT_SUCCESS,
|
|
result_policy=ResultPolicy.SILENT_SUCCESS,
|
|
|
risk_level=RiskLevel.MEDIUM,
|
|
risk_level=RiskLevel.MEDIUM,
|
|
@@ -319,14 +330,14 @@ def _calendar_schedule_definition(port: CalendarSchedulePort) -> EventDefinition
|
|
|
parameters={
|
|
parameters={
|
|
|
"type": "object",
|
|
"type": "object",
|
|
|
"properties": {
|
|
"properties": {
|
|
|
- "title": {"type": "string", "minLength": 1},
|
|
|
|
|
|
|
+ "title": {"type": "string", "pattern": r"\S"},
|
|
|
"start_at": {
|
|
"start_at": {
|
|
|
"type": "string",
|
|
"type": "string",
|
|
|
- "pattern": f"^{_RFC3339_PATTERN}$",
|
|
|
|
|
|
|
+ "format": "date-time",
|
|
|
},
|
|
},
|
|
|
"timezone": {
|
|
"timezone": {
|
|
|
"type": "string",
|
|
"type": "string",
|
|
|
- "pattern": f"^{_TIMEZONE_PATTERN}$",
|
|
|
|
|
|
|
+ "format": "iana-timezone",
|
|
|
},
|
|
},
|
|
|
"recurrence": {"type": "string", "minLength": 1},
|
|
"recurrence": {"type": "string", "minLength": 1},
|
|
|
"reminder_minutes": {"type": "integer", "minimum": 0},
|
|
"reminder_minutes": {"type": "integer", "minimum": 0},
|
|
@@ -360,7 +371,7 @@ def _knowledge_search_definition(port: WebSearchPort) -> EventDefinition:
|
|
|
parameters={
|
|
parameters={
|
|
|
"type": "object",
|
|
"type": "object",
|
|
|
"properties": {
|
|
"properties": {
|
|
|
- "query": {"type": "string", "minLength": 1},
|
|
|
|
|
|
|
+ "query": {"type": "string", "pattern": r"\S"},
|
|
|
"max_results": {
|
|
"max_results": {
|
|
|
"type": "integer",
|
|
"type": "integer",
|
|
|
"minimum": 1,
|
|
"minimum": 1,
|
|
@@ -372,6 +383,7 @@ def _knowledge_search_definition(port: WebSearchPort) -> EventDefinition:
|
|
|
},
|
|
},
|
|
|
handler=handler,
|
|
handler=handler,
|
|
|
resolver=_resolve_knowledge_search,
|
|
resolver=_resolve_knowledge_search,
|
|
|
|
|
+ normalizer=_normalize_knowledge_search,
|
|
|
fallback_allowed=False,
|
|
fallback_allowed=False,
|
|
|
result_policy=ResultPolicy.LLM_FOLLOW_UP,
|
|
result_policy=ResultPolicy.LLM_FOLLOW_UP,
|
|
|
risk_level=RiskLevel.LOW,
|
|
risk_level=RiskLevel.LOW,
|
|
@@ -396,8 +408,7 @@ def _resolve_device_volume(
|
|
|
arguments = deepcopy(request.arguments)
|
|
arguments = deepcopy(request.arguments)
|
|
|
mode = arguments.get("mode")
|
|
mode = arguments.get("mode")
|
|
|
if isinstance(mode, str):
|
|
if isinstance(mode, str):
|
|
|
- arguments["mode"] = mode.strip().lower()
|
|
|
|
|
- mode = arguments["mode"]
|
|
|
|
|
|
|
+ mode = mode.strip().lower()
|
|
|
complete = mode in {"mute", "unmute"} or (
|
|
complete = mode in {"mute", "unmute"} or (
|
|
|
mode == "absolute" and "value" in arguments
|
|
mode == "absolute" and "value" in arguments
|
|
|
) or (mode == "relative" and "delta" in arguments)
|
|
) or (mode == "relative" and "delta" in arguments)
|
|
@@ -410,6 +421,11 @@ def _resolve_device_volume(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
content = _latest_user_content(context).lower()
|
|
content = _latest_user_content(context).lower()
|
|
|
|
|
+ if re.search(
|
|
|
|
|
+ r"\bdon['’]t\b|\bdo\s+not\b|\bnot\b|\bnever\b|不要|别|不许|禁止",
|
|
|
|
|
+ content,
|
|
|
|
|
+ ):
|
|
|
|
|
+ return EventArgumentResolution(arguments={}, complete=False)
|
|
|
unmute = bool(re.search(r"\bunmute\b|取消静音|解除静音|恢复声音", content))
|
|
unmute = bool(re.search(r"\bunmute\b|取消静音|解除静音|恢复声音", content))
|
|
|
mute = bool(re.search(r"\bmute\b|(?<!取消)(?<!解除)静音", content))
|
|
mute = bool(re.search(r"\bmute\b|(?<!取消)(?<!解除)静音", content))
|
|
|
if mute and unmute:
|
|
if mute and unmute:
|
|
@@ -423,32 +439,32 @@ def _resolve_device_volume(
|
|
|
|
|
|
|
|
absolute_matches = [
|
|
absolute_matches = [
|
|
|
re.search(
|
|
re.search(
|
|
|
- r"音量.{0,6}?(?:调到|调至|设置为|设为|到)\s*(\d{1,3})",
|
|
|
|
|
|
|
+ r"音量.{0,6}?(?:调到|调至|设置为|设为|到)\s*(\d+)",
|
|
|
content,
|
|
content,
|
|
|
),
|
|
),
|
|
|
re.search(
|
|
re.search(
|
|
|
- r"(?:set|change)\s+(?:the\s+)?volume\s+(?:to|at)\s*(\d{1,3})",
|
|
|
|
|
|
|
+ r"(?:set|change)\s+(?:the\s+)?volume\s+(?:to|at)\s*(\d+)",
|
|
|
content,
|
|
content,
|
|
|
),
|
|
),
|
|
|
- re.search(r"\bvolume\s*(?:to|at|=)\s*(\d{1,3})", content),
|
|
|
|
|
|
|
+ re.search(r"\bvolume\s*(?:to|at|=)\s*(\d+)", content),
|
|
|
]
|
|
]
|
|
|
positive_matches = [
|
|
positive_matches = [
|
|
|
- re.search(r"音量.{0,4}?(?:增加|调高|提高|加)\s*(\d{1,3})", content),
|
|
|
|
|
|
|
+ re.search(r"音量.{0,4}?(?:增加|调高|提高|加)\s*(\d+)", content),
|
|
|
re.search(
|
|
re.search(
|
|
|
r"(?:increase|raise|turn\s+up)\s+(?:the\s+)?volume"
|
|
r"(?:increase|raise|turn\s+up)\s+(?:the\s+)?volume"
|
|
|
- r"(?:\s+by)?\s*(\d{1,3})",
|
|
|
|
|
|
|
+ r"(?:\s+by)?\s*(\d+)",
|
|
|
content,
|
|
content,
|
|
|
),
|
|
),
|
|
|
]
|
|
]
|
|
|
negative_matches = [
|
|
negative_matches = [
|
|
|
- re.search(r"音量.{0,4}?(?:降低|调低|减少|减)\s*(\d{1,3})", content),
|
|
|
|
|
|
|
+ re.search(r"音量.{0,4}?(?:降低|调低|减少|减)\s*(\d+)", content),
|
|
|
re.search(
|
|
re.search(
|
|
|
r"(?:decrease|lower|turn\s+down)\s+(?:the\s+)?volume"
|
|
r"(?:decrease|lower|turn\s+down)\s+(?:the\s+)?volume"
|
|
|
- r"(?:\s+by)?\s*(\d{1,3})",
|
|
|
|
|
|
|
+ r"(?:\s+by)?\s*(\d+)",
|
|
|
content,
|
|
content,
|
|
|
),
|
|
),
|
|
|
]
|
|
]
|
|
|
- signed = re.search(r"(?:音量|\bvolume\b)\s*([+-])\s*(\d{1,3})", content)
|
|
|
|
|
|
|
+ signed = re.search(r"(?:音量|\bvolume\b)\s*([+-])\s*(\d+)", content)
|
|
|
absolute = next((match for match in absolute_matches if match), None)
|
|
absolute = next((match for match in absolute_matches if match), None)
|
|
|
positive = next((match for match in positive_matches if match), None)
|
|
positive = next((match for match in positive_matches if match), None)
|
|
|
negative = next((match for match in negative_matches if match), None)
|
|
negative = next((match for match in negative_matches if match), None)
|
|
@@ -526,6 +542,14 @@ def _resolve_calendar_schedule(
|
|
|
return EventArgumentResolution(arguments=arguments, complete=False)
|
|
return EventArgumentResolution(arguments=arguments, complete=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _normalize_device_volume(arguments: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
+ normalized = deepcopy(arguments)
|
|
|
|
|
+ mode = normalized.get("mode")
|
|
|
|
|
+ if isinstance(mode, str):
|
|
|
|
|
+ normalized["mode"] = mode.strip().lower()
|
|
|
|
|
+ return normalized
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _resolve_knowledge_search(
|
|
def _resolve_knowledge_search(
|
|
|
request: EventRequest,
|
|
request: EventRequest,
|
|
|
context: EventExecutionContext,
|
|
context: EventExecutionContext,
|
|
@@ -540,6 +564,14 @@ def _resolve_knowledge_search(
|
|
|
return EventArgumentResolution(arguments=arguments, complete=False)
|
|
return EventArgumentResolution(arguments=arguments, complete=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _normalize_knowledge_search(arguments: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
+ normalized = deepcopy(arguments)
|
|
|
|
|
+ query = normalized.get("query")
|
|
|
|
|
+ if isinstance(query, str):
|
|
|
|
|
+ normalized["query"] = query.strip()
|
|
|
|
|
+ return normalized
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _latest_user_content(context: EventExecutionContext) -> str:
|
|
def _latest_user_content(context: EventExecutionContext) -> str:
|
|
|
for message in reversed(context.history):
|
|
for message in reversed(context.history):
|
|
|
if message.role == "user" and message.content.strip():
|
|
if message.role == "user" and message.content.strip():
|
|
@@ -558,3 +590,18 @@ def _format_retrieved_at(value: datetime | str) -> str:
|
|
|
value = value.replace(tzinfo=datetime_timezone.utc)
|
|
value = value.replace(tzinfo=datetime_timezone.utc)
|
|
|
rendered = value.astimezone(datetime_timezone.utc).isoformat(timespec="seconds")
|
|
rendered = value.astimezone(datetime_timezone.utc).isoformat(timespec="seconds")
|
|
|
return rendered.replace("+00:00", "Z")
|
|
return rendered.replace("+00:00", "Z")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _adapter_cache_key(
|
|
|
|
|
+ event_id: str,
|
|
|
|
|
+ arguments: dict[str, Any],
|
|
|
|
|
+) -> tuple[str, str]:
|
|
|
|
|
+ return (
|
|
|
|
|
+ event_id,
|
|
|
|
|
+ json.dumps(
|
|
|
|
|
+ arguments,
|
|
|
|
|
+ ensure_ascii=False,
|
|
|
|
|
+ sort_keys=True,
|
|
|
|
|
+ separators=(",", ":"),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|