|
@@ -3,12 +3,9 @@ from __future__ import annotations
|
|
|
import inspect
|
|
import inspect
|
|
|
import json
|
|
import json
|
|
|
from collections.abc import Awaitable, Callable, Iterable
|
|
from collections.abc import Awaitable, Callable, Iterable
|
|
|
-from copy import deepcopy
|
|
|
|
|
from dataclasses import dataclass, replace
|
|
from dataclasses import dataclass, replace
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
-from jsonschema.exceptions import ValidationError
|
|
|
|
|
-
|
|
|
|
|
from agent_lab.application.events.models import (
|
|
from agent_lab.application.events.models import (
|
|
|
EventDefinition,
|
|
EventDefinition,
|
|
|
EventArgumentResolution,
|
|
EventArgumentResolution,
|
|
@@ -19,7 +16,7 @@ from agent_lab.application.events.models import (
|
|
|
EventStatus,
|
|
EventStatus,
|
|
|
ResolvedEventArguments,
|
|
ResolvedEventArguments,
|
|
|
)
|
|
)
|
|
|
-from agent_lab.application.events.registry import EventRegistry
|
|
|
|
|
|
|
+from agent_lab.application.events.registry import EventRegistry, ValidationIssue
|
|
|
|
|
|
|
|
|
|
|
|
|
ArgumentFallbackResult = ResolvedEventArguments | None
|
|
ArgumentFallbackResult = ResolvedEventArguments | None
|
|
@@ -86,7 +83,7 @@ class EventKernel:
|
|
|
used_fallback = True
|
|
used_fallback = True
|
|
|
fallback_request = replace(
|
|
fallback_request = replace(
|
|
|
request,
|
|
request,
|
|
|
- arguments=resolved.arguments,
|
|
|
|
|
|
|
+ arguments=self._strict_json_copy(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
)
|
|
)
|
|
|
try:
|
|
try:
|
|
@@ -131,7 +128,7 @@ class EventKernel:
|
|
|
|
|
|
|
|
resolved_request = replace(
|
|
resolved_request = replace(
|
|
|
request,
|
|
request,
|
|
|
- arguments=deepcopy(resolved.arguments),
|
|
|
|
|
|
|
+ arguments=self._strict_json_copy(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
)
|
|
)
|
|
|
try:
|
|
try:
|
|
@@ -219,7 +216,7 @@ class EventKernel:
|
|
|
)
|
|
)
|
|
|
resolved_request = replace(
|
|
resolved_request = replace(
|
|
|
request,
|
|
request,
|
|
|
- arguments=deepcopy(resolved.arguments),
|
|
|
|
|
|
|
+ arguments=self._strict_json_copy(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
)
|
|
)
|
|
|
try:
|
|
try:
|
|
@@ -280,7 +277,7 @@ class EventKernel:
|
|
|
event_name=request.name,
|
|
event_name=request.name,
|
|
|
status=EventStatus.UNKNOWN,
|
|
status=EventStatus.UNKNOWN,
|
|
|
source=request.source,
|
|
source=request.source,
|
|
|
- arguments=deepcopy(request.arguments),
|
|
|
|
|
|
|
+ arguments=self._safe_json_object_snapshot(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
raw_arguments=request.raw_arguments,
|
|
|
error="unknown event",
|
|
error="unknown event",
|
|
|
)
|
|
)
|
|
@@ -291,7 +288,7 @@ class EventKernel:
|
|
|
EventStatus.DISABLED,
|
|
EventStatus.DISABLED,
|
|
|
resolved=ResolvedEventArguments(
|
|
resolved=ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
event_name=request.name,
|
|
|
- arguments=deepcopy(request.arguments),
|
|
|
|
|
|
|
+ arguments=self._safe_json_object_snapshot(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
),
|
|
|
error="event disabled",
|
|
error="event disabled",
|
|
@@ -305,10 +302,24 @@ class EventKernel:
|
|
|
context: EventExecutionContext,
|
|
context: EventExecutionContext,
|
|
|
) -> tuple[ResolvedEventArguments | None, bool, EventResult | None]:
|
|
) -> tuple[ResolvedEventArguments | None, bool, EventResult | None]:
|
|
|
if request.source is EventSource.PROVIDER_RESOLVED:
|
|
if request.source is EventSource.PROVIDER_RESOLVED:
|
|
|
|
|
+ try:
|
|
|
|
|
+ arguments = self._strict_json_copy(request.arguments)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ return None, False, self._result(
|
|
|
|
|
+ definition,
|
|
|
|
|
+ request,
|
|
|
|
|
+ EventStatus.INVALID_ARGUMENTS,
|
|
|
|
|
+ resolved=ResolvedEventArguments(
|
|
|
|
|
+ event_name=request.name,
|
|
|
|
|
+ arguments={},
|
|
|
|
|
+ raw_arguments=request.raw_arguments,
|
|
|
|
|
+ ),
|
|
|
|
|
+ error="provider-resolved event arguments are not valid JSON",
|
|
|
|
|
+ )
|
|
|
return (
|
|
return (
|
|
|
ResolvedEventArguments(
|
|
ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
event_name=request.name,
|
|
|
- arguments=deepcopy(request.arguments),
|
|
|
|
|
|
|
+ arguments=arguments,
|
|
|
raw_arguments=request.raw_arguments,
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
),
|
|
|
True,
|
|
True,
|
|
@@ -338,10 +349,10 @@ class EventKernel:
|
|
|
request,
|
|
request,
|
|
|
"event argument resolver returned invalid payload",
|
|
"event argument resolver returned invalid payload",
|
|
|
)
|
|
)
|
|
|
- copied_arguments = deepcopy(arguments)
|
|
|
|
|
try:
|
|
try:
|
|
|
|
|
+ copied_arguments = self._strict_json_copy(arguments)
|
|
|
raw_arguments = self._json_arguments(copied_arguments)
|
|
raw_arguments = self._json_arguments(copied_arguments)
|
|
|
- except (TypeError, ValueError) as exc:
|
|
|
|
|
|
|
+ except Exception as exc:
|
|
|
return None, False, self._resolution_error(
|
|
return None, False, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
@@ -399,8 +410,8 @@ class EventKernel:
|
|
|
used_fallback=True,
|
|
used_fallback=True,
|
|
|
)
|
|
)
|
|
|
try:
|
|
try:
|
|
|
- self._json_arguments(value.arguments)
|
|
|
|
|
- except (TypeError, ValueError):
|
|
|
|
|
|
|
+ copied_arguments = self._strict_json_copy(value.arguments)
|
|
|
|
|
+ except Exception:
|
|
|
return None, self._resolution_error(
|
|
return None, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
@@ -419,7 +430,7 @@ class EventKernel:
|
|
|
return (
|
|
return (
|
|
|
ResolvedEventArguments(
|
|
ResolvedEventArguments(
|
|
|
event_name=value.event_name,
|
|
event_name=value.event_name,
|
|
|
- arguments=deepcopy(value.arguments),
|
|
|
|
|
|
|
+ arguments=copied_arguments,
|
|
|
raw_arguments=value.raw_arguments,
|
|
raw_arguments=value.raw_arguments,
|
|
|
),
|
|
),
|
|
|
None,
|
|
None,
|
|
@@ -444,30 +455,32 @@ class EventKernel:
|
|
|
)
|
|
)
|
|
|
if not errors:
|
|
if not errors:
|
|
|
return _ValidationResult()
|
|
return _ValidationResult()
|
|
|
- missing_errors = [
|
|
|
|
|
- required_error
|
|
|
|
|
- for error in errors
|
|
|
|
|
- for required_error in _required_errors(error)
|
|
|
|
|
- ]
|
|
|
|
|
- error = missing_errors[0] if missing_errors else errors[0]
|
|
|
|
|
|
|
+ missing_error = next(
|
|
|
|
|
+ (
|
|
|
|
|
+ required_error
|
|
|
|
|
+ for error in errors
|
|
|
|
|
+ if (required_error := _fallback_required_error(error))
|
|
|
|
|
+ is not None
|
|
|
|
|
+ ),
|
|
|
|
|
+ None,
|
|
|
|
|
+ )
|
|
|
|
|
+ error = missing_error or errors[0]
|
|
|
return _ValidationResult(
|
|
return _ValidationResult(
|
|
|
status=EventStatus.INVALID_ARGUMENTS,
|
|
status=EventStatus.INVALID_ARGUMENTS,
|
|
|
error=self._format_validation_error(error),
|
|
error=self._format_validation_error(error),
|
|
|
- missing_required=bool(missing_errors),
|
|
|
|
|
|
|
+ missing_required=missing_error is not None,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- def _format_validation_error(self, error: ValidationError) -> str:
|
|
|
|
|
|
|
+ def _format_validation_error(self, error: ValidationIssue) -> str:
|
|
|
if error.validator == "required":
|
|
if error.validator == "required":
|
|
|
- missing = [
|
|
|
|
|
- name
|
|
|
|
|
- for name in error.validator_value
|
|
|
|
|
- if name not in error.instance
|
|
|
|
|
- ]
|
|
|
|
|
- return f"missing required arguments: {', '.join(missing)}"
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ "missing required arguments: "
|
|
|
|
|
+ f"{', '.join(error.missing_required)}"
|
|
|
|
|
+ )
|
|
|
if error.validator == "type" and error.path:
|
|
if error.validator == "type" and error.path:
|
|
|
return (
|
|
return (
|
|
|
f"invalid argument type for {error.path[-1]}: "
|
|
f"invalid argument type for {error.path[-1]}: "
|
|
|
- f"expected {error.validator_value}"
|
|
|
|
|
|
|
+ f"expected {error.expected}"
|
|
|
)
|
|
)
|
|
|
return f"invalid event arguments: {error.message}"
|
|
return f"invalid event arguments: {error.message}"
|
|
|
|
|
|
|
@@ -487,7 +500,7 @@ class EventKernel:
|
|
|
resolved=resolved
|
|
resolved=resolved
|
|
|
or ResolvedEventArguments(
|
|
or ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
event_name=request.name,
|
|
|
- arguments=deepcopy(request.arguments),
|
|
|
|
|
|
|
+ arguments=self._safe_json_object_snapshot(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
),
|
|
|
error=error,
|
|
error=error,
|
|
@@ -510,9 +523,9 @@ class EventKernel:
|
|
|
event_name=request.name,
|
|
event_name=request.name,
|
|
|
status=status,
|
|
status=status,
|
|
|
source=request.source,
|
|
source=request.source,
|
|
|
- arguments=deepcopy(resolved.arguments),
|
|
|
|
|
|
|
+ arguments=self._safe_json_object_snapshot(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
- payload=deepcopy(payload or {}),
|
|
|
|
|
|
|
+ payload=self._safe_json_object_snapshot(payload or {}),
|
|
|
error=error,
|
|
error=error,
|
|
|
used_fallback=used_fallback,
|
|
used_fallback=used_fallback,
|
|
|
result_policy=definition.result_policy,
|
|
result_policy=definition.result_policy,
|
|
@@ -542,16 +555,61 @@ class EventKernel:
|
|
|
raise ValueError("JSON round-trip changed payload")
|
|
raise ValueError("JSON round-trip changed payload")
|
|
|
return copied
|
|
return copied
|
|
|
|
|
|
|
|
|
|
+ def _safe_json_object_snapshot(self, value: Any) -> dict[str, Any]:
|
|
|
|
|
+ if not isinstance(value, dict):
|
|
|
|
|
+ return {}
|
|
|
|
|
+ try:
|
|
|
|
|
+ return self._strict_json_copy(value)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ return {}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def _reject_json_constant(value: str) -> None:
|
|
def _reject_json_constant(value: str) -> None:
|
|
|
raise ValueError(f"non-standard JSON constant: {value}")
|
|
raise ValueError(f"non-standard JSON constant: {value}")
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _required_errors(error: ValidationError) -> Iterable[ValidationError]:
|
|
|
|
|
|
|
+def _fallback_required_error(error: ValidationIssue) -> ValidationIssue | None:
|
|
|
if error.validator == "required":
|
|
if error.validator == "required":
|
|
|
- yield error
|
|
|
|
|
|
|
+ return error
|
|
|
|
|
+ if error.validator not in {"anyOf", "oneOf"}:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ branches: dict[int, list[ValidationIssue]] = {}
|
|
|
for nested_error in error.context:
|
|
for nested_error in error.context:
|
|
|
- yield from _required_errors(nested_error)
|
|
|
|
|
|
|
+ branch = _branch_index(nested_error)
|
|
|
|
|
+ if branch is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ branches.setdefault(branch, []).append(nested_error)
|
|
|
|
|
+
|
|
|
|
|
+ viable: list[ValidationIssue] = []
|
|
|
|
|
+ for branch_errors in branches.values():
|
|
|
|
|
+ required_errors: list[ValidationIssue] = []
|
|
|
|
|
+ substantive = False
|
|
|
|
|
+ for branch_error in branch_errors:
|
|
|
|
|
+ required_error = _missing_only_required_error(branch_error)
|
|
|
|
|
+ if required_error is None:
|
|
|
|
|
+ substantive = True
|
|
|
|
|
+ break
|
|
|
|
|
+ required_errors.append(required_error)
|
|
|
|
|
+ if required_errors and not substantive:
|
|
|
|
|
+ viable.append(required_errors[0])
|
|
|
|
|
+ return viable[0] if len(viable) == 1 else None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _missing_only_required_error(
|
|
|
|
|
+ error: ValidationIssue,
|
|
|
|
|
+) -> ValidationIssue | None:
|
|
|
|
|
+ if error.validator == "required":
|
|
|
|
|
+ return error
|
|
|
|
|
+ if error.validator in {"anyOf", "oneOf"}:
|
|
|
|
|
+ return _fallback_required_error(error)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _branch_index(error: ValidationIssue) -> int | None:
|
|
|
|
|
+ if error.schema_path and isinstance(error.schema_path[0], int):
|
|
|
|
|
+ return error.schema_path[0]
|
|
|
|
|
+ return None
|
|
|
|
|
|
|
|
|
|
|
|
|
def _json_values_equal(left: Any, right: Any) -> bool:
|
|
def _json_values_equal(left: Any, right: Any) -> bool:
|