|
@@ -3,13 +3,14 @@ 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 dataclasses import replace
|
|
|
|
|
|
|
+from dataclasses import dataclass, replace
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
from jsonschema.exceptions import ValidationError
|
|
from jsonschema.exceptions import ValidationError
|
|
|
|
|
|
|
|
from agent_lab.application.events.models import (
|
|
from agent_lab.application.events.models import (
|
|
|
EventDefinition,
|
|
EventDefinition,
|
|
|
|
|
+ EventArgumentResolution,
|
|
|
EventExecutionContext,
|
|
EventExecutionContext,
|
|
|
EventRequest,
|
|
EventRequest,
|
|
|
EventResult,
|
|
EventResult,
|
|
@@ -27,6 +28,13 @@ ArgumentFallback = Callable[
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class _ValidationResult:
|
|
|
|
|
+ status: EventStatus | None = None
|
|
|
|
|
+ error: str | None = None
|
|
|
|
|
+ missing_required: bool = False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class EventKernel:
|
|
class EventKernel:
|
|
|
def __init__(
|
|
def __init__(
|
|
|
self,
|
|
self,
|
|
@@ -49,19 +57,27 @@ class EventKernel:
|
|
|
assert definition is not None
|
|
assert definition is not None
|
|
|
resolved_context = context or EventExecutionContext()
|
|
resolved_context = context or EventExecutionContext()
|
|
|
|
|
|
|
|
- resolved, resolution_error = self._resolve_deterministic(
|
|
|
|
|
- definition,
|
|
|
|
|
- request,
|
|
|
|
|
- resolved_context,
|
|
|
|
|
|
|
+ resolved, resolution_complete, resolution_error = (
|
|
|
|
|
+ self._resolve_deterministic(
|
|
|
|
|
+ definition,
|
|
|
|
|
+ request,
|
|
|
|
|
+ resolved_context,
|
|
|
|
|
+ )
|
|
|
)
|
|
)
|
|
|
if resolution_error is not None:
|
|
if resolution_error is not None:
|
|
|
return resolution_error
|
|
return resolution_error
|
|
|
assert resolved is not None
|
|
assert resolved is not None
|
|
|
|
|
|
|
|
- validation_error = self._validation_error(definition, resolved.arguments)
|
|
|
|
|
|
|
+ validation = self._validate(definition, resolved.arguments)
|
|
|
|
|
+ if not resolution_complete and validation.status is None:
|
|
|
|
|
+ validation = _ValidationResult(
|
|
|
|
|
+ status=EventStatus.INVALID_ARGUMENTS,
|
|
|
|
|
+ error="event arguments incomplete",
|
|
|
|
|
+ )
|
|
|
used_fallback = False
|
|
used_fallback = False
|
|
|
if (
|
|
if (
|
|
|
- validation_error is not None
|
|
|
|
|
|
|
+ validation.status is not EventStatus.DEFINITION_ERROR
|
|
|
|
|
+ and (validation.missing_required or not resolution_complete)
|
|
|
and request.source is not EventSource.PROVIDER_RESOLVED
|
|
and request.source is not EventSource.PROVIDER_RESOLVED
|
|
|
and definition.fallback_allowed
|
|
and definition.fallback_allowed
|
|
|
and self.argument_fallback is not None
|
|
and self.argument_fallback is not None
|
|
@@ -97,18 +113,18 @@ class EventKernel:
|
|
|
if fallback_error is not None:
|
|
if fallback_error is not None:
|
|
|
return fallback_error
|
|
return fallback_error
|
|
|
assert resolved is not None
|
|
assert resolved is not None
|
|
|
- validation_error = self._validation_error(
|
|
|
|
|
- definition,
|
|
|
|
|
- resolved.arguments,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ validation = self._validate(
|
|
|
|
|
+ definition,
|
|
|
|
|
+ resolved.arguments,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- if validation_error is not None:
|
|
|
|
|
|
|
+ if validation.status is not None:
|
|
|
return self._result(
|
|
return self._result(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
|
- EventStatus.INVALID_ARGUMENTS,
|
|
|
|
|
|
|
+ validation.status,
|
|
|
resolved=resolved,
|
|
resolved=resolved,
|
|
|
- error=validation_error,
|
|
|
|
|
|
|
+ error=validation.error,
|
|
|
used_fallback=used_fallback,
|
|
used_fallback=used_fallback,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -159,7 +175,7 @@ class EventKernel:
|
|
|
if early_result is not None:
|
|
if early_result is not None:
|
|
|
return early_result
|
|
return early_result
|
|
|
assert definition is not None
|
|
assert definition is not None
|
|
|
- resolved, resolution_error = self._resolve_deterministic(
|
|
|
|
|
|
|
+ resolved, _, resolution_error = self._resolve_deterministic(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
|
context or EventExecutionContext(),
|
|
context or EventExecutionContext(),
|
|
@@ -167,14 +183,14 @@ class EventKernel:
|
|
|
if resolution_error is not None:
|
|
if resolution_error is not None:
|
|
|
return resolution_error
|
|
return resolution_error
|
|
|
assert resolved is not None
|
|
assert resolved is not None
|
|
|
- validation_error = self._validation_error(definition, resolved.arguments)
|
|
|
|
|
- if validation_error is not None:
|
|
|
|
|
|
|
+ validation = self._validate(definition, resolved.arguments)
|
|
|
|
|
+ if validation.status is not None:
|
|
|
return self._result(
|
|
return self._result(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
|
- EventStatus.INVALID_ARGUMENTS,
|
|
|
|
|
|
|
+ validation.status,
|
|
|
resolved=resolved,
|
|
resolved=resolved,
|
|
|
- error=validation_error,
|
|
|
|
|
|
|
+ error=validation.error,
|
|
|
)
|
|
)
|
|
|
if inspect.iscoroutinefunction(definition.handler):
|
|
if inspect.iscoroutinefunction(definition.handler):
|
|
|
return self._result(
|
|
return self._result(
|
|
@@ -260,7 +276,7 @@ class EventKernel:
|
|
|
definition: EventDefinition,
|
|
definition: EventDefinition,
|
|
|
request: EventRequest,
|
|
request: EventRequest,
|
|
|
context: EventExecutionContext,
|
|
context: EventExecutionContext,
|
|
|
- ) -> tuple[ResolvedEventArguments | None, EventResult | None]:
|
|
|
|
|
|
|
+ ) -> tuple[ResolvedEventArguments | None, bool, EventResult | None]:
|
|
|
if request.source is EventSource.PROVIDER_RESOLVED:
|
|
if request.source is EventSource.PROVIDER_RESOLVED:
|
|
|
return (
|
|
return (
|
|
|
ResolvedEventArguments(
|
|
ResolvedEventArguments(
|
|
@@ -268,22 +284,29 @@ class EventKernel:
|
|
|
arguments=dict(request.arguments),
|
|
arguments=dict(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
),
|
|
|
|
|
+ True,
|
|
|
None,
|
|
None,
|
|
|
)
|
|
)
|
|
|
try:
|
|
try:
|
|
|
- arguments = (
|
|
|
|
|
|
|
+ value = (
|
|
|
definition.resolver(request, context)
|
|
definition.resolver(request, context)
|
|
|
if definition.resolver is not None
|
|
if definition.resolver is not None
|
|
|
else dict(request.arguments)
|
|
else dict(request.arguments)
|
|
|
)
|
|
)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
- return None, self._resolution_error(
|
|
|
|
|
|
|
+ return None, False, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
|
f"event argument resolver failed: {exc}",
|
|
f"event argument resolver failed: {exc}",
|
|
|
)
|
|
)
|
|
|
- if not isinstance(arguments, dict):
|
|
|
|
|
- return None, self._resolution_error(
|
|
|
|
|
|
|
+ if isinstance(value, EventArgumentResolution):
|
|
|
|
|
+ arguments = value.arguments
|
|
|
|
|
+ complete = value.complete
|
|
|
|
|
+ else:
|
|
|
|
|
+ arguments = value
|
|
|
|
|
+ complete = True
|
|
|
|
|
+ if not isinstance(arguments, dict) or not isinstance(complete, bool):
|
|
|
|
|
+ return None, False, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
|
"event argument resolver returned invalid payload",
|
|
"event argument resolver returned invalid payload",
|
|
@@ -291,7 +314,7 @@ class EventKernel:
|
|
|
try:
|
|
try:
|
|
|
raw_arguments = self._json_arguments(arguments)
|
|
raw_arguments = self._json_arguments(arguments)
|
|
|
except (TypeError, ValueError) as exc:
|
|
except (TypeError, ValueError) as exc:
|
|
|
- return None, self._resolution_error(
|
|
|
|
|
|
|
+ return None, False, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
|
f"event argument resolver failed to serialize: {exc}",
|
|
f"event argument resolver failed to serialize: {exc}",
|
|
@@ -302,6 +325,7 @@ class EventKernel:
|
|
|
arguments=dict(arguments),
|
|
arguments=dict(arguments),
|
|
|
raw_arguments=raw_arguments,
|
|
raw_arguments=raw_arguments,
|
|
|
),
|
|
),
|
|
|
|
|
+ complete,
|
|
|
None,
|
|
None,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -334,8 +358,11 @@ class EventKernel:
|
|
|
used_fallback=True,
|
|
used_fallback=True,
|
|
|
)
|
|
)
|
|
|
try:
|
|
try:
|
|
|
- raw_arguments = json.loads(value.raw_arguments)
|
|
|
|
|
- except (TypeError, json.JSONDecodeError):
|
|
|
|
|
|
|
+ raw_arguments = json.loads(
|
|
|
|
|
+ value.raw_arguments,
|
|
|
|
|
+ parse_constant=_reject_json_constant,
|
|
|
|
|
+ )
|
|
|
|
|
+ except (TypeError, ValueError, json.JSONDecodeError):
|
|
|
return None, self._resolution_error(
|
|
return None, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
@@ -343,7 +370,17 @@ class EventKernel:
|
|
|
resolved=value,
|
|
resolved=value,
|
|
|
used_fallback=True,
|
|
used_fallback=True,
|
|
|
)
|
|
)
|
|
|
- if raw_arguments != value.arguments:
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ self._json_arguments(value.arguments)
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return None, self._resolution_error(
|
|
|
|
|
+ definition,
|
|
|
|
|
+ request,
|
|
|
|
|
+ "fallback parsed arguments are not valid JSON",
|
|
|
|
|
+ resolved=value,
|
|
|
|
|
+ used_fallback=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ if not _json_values_equal(raw_arguments, value.arguments):
|
|
|
return None, self._resolution_error(
|
|
return None, self._resolution_error(
|
|
|
definition,
|
|
definition,
|
|
|
request,
|
|
request,
|
|
@@ -353,32 +390,44 @@ class EventKernel:
|
|
|
)
|
|
)
|
|
|
return value, None
|
|
return value, None
|
|
|
|
|
|
|
|
- def _validation_error(
|
|
|
|
|
|
|
+ def _validate(
|
|
|
self,
|
|
self,
|
|
|
definition: EventDefinition,
|
|
definition: EventDefinition,
|
|
|
arguments: dict[str, Any],
|
|
arguments: dict[str, Any],
|
|
|
- ) -> str | None:
|
|
|
|
|
|
|
+ ) -> _ValidationResult:
|
|
|
validator = self.registry.validator(definition.name)
|
|
validator = self.registry.validator(definition.name)
|
|
|
assert validator is not None
|
|
assert validator is not None
|
|
|
try:
|
|
try:
|
|
|
- validator.validate(arguments)
|
|
|
|
|
- except ValidationError as exc:
|
|
|
|
|
- if exc.validator == "required":
|
|
|
|
|
- missing = [
|
|
|
|
|
- name
|
|
|
|
|
- for name in exc.validator_value
|
|
|
|
|
- if name not in exc.instance
|
|
|
|
|
- ]
|
|
|
|
|
- return f"missing required arguments: {', '.join(missing)}"
|
|
|
|
|
- if exc.validator == "type" and exc.path:
|
|
|
|
|
- return (
|
|
|
|
|
- f"invalid argument type for {exc.path[-1]}: "
|
|
|
|
|
- f"expected {exc.validator_value}"
|
|
|
|
|
- )
|
|
|
|
|
- return f"invalid event arguments: {exc.message}"
|
|
|
|
|
|
|
+ errors = list(validator.iter_errors(arguments))
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
- return f"event argument validation failed: {exc}"
|
|
|
|
|
- return None
|
|
|
|
|
|
|
+ return _ValidationResult(
|
|
|
|
|
+ status=EventStatus.DEFINITION_ERROR,
|
|
|
|
|
+ error=f"event argument validation failed: {exc}",
|
|
|
|
|
+ )
|
|
|
|
|
+ if not errors:
|
|
|
|
|
+ return _ValidationResult()
|
|
|
|
|
+ missing_errors = [error for error in errors if error.validator == "required"]
|
|
|
|
|
+ error = missing_errors[0] if missing_errors else errors[0]
|
|
|
|
|
+ return _ValidationResult(
|
|
|
|
|
+ status=EventStatus.INVALID_ARGUMENTS,
|
|
|
|
|
+ error=self._format_validation_error(error),
|
|
|
|
|
+ missing_required=bool(missing_errors),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _format_validation_error(self, error: ValidationError) -> str:
|
|
|
|
|
+ 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)}"
|
|
|
|
|
+ if error.validator == "type" and error.path:
|
|
|
|
|
+ return (
|
|
|
|
|
+ f"invalid argument type for {error.path[-1]}: "
|
|
|
|
|
+ f"expected {error.validator_value}"
|
|
|
|
|
+ )
|
|
|
|
|
+ return f"invalid event arguments: {error.message}"
|
|
|
|
|
|
|
|
def _resolution_error(
|
|
def _resolution_error(
|
|
|
self,
|
|
self,
|
|
@@ -435,4 +484,28 @@ class EventKernel:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
def _json_arguments(self, arguments: dict[str, Any]) -> str:
|
|
def _json_arguments(self, arguments: dict[str, Any]) -> str:
|
|
|
- return json.dumps(arguments, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ arguments,
|
|
|
|
|
+ ensure_ascii=False,
|
|
|
|
|
+ separators=(",", ":"),
|
|
|
|
|
+ allow_nan=False,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _reject_json_constant(value: str) -> None:
|
|
|
|
|
+ raise ValueError(f"non-standard JSON constant: {value}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _json_values_equal(left: Any, right: Any) -> bool:
|
|
|
|
|
+ if type(left) is not type(right):
|
|
|
|
|
+ return False
|
|
|
|
|
+ if isinstance(left, dict):
|
|
|
|
|
+ return left.keys() == right.keys() and all(
|
|
|
|
|
+ _json_values_equal(left[key], right[key]) for key in left
|
|
|
|
|
+ )
|
|
|
|
|
+ if isinstance(left, list):
|
|
|
|
|
+ return len(left) == len(right) and all(
|
|
|
|
|
+ _json_values_equal(left_item, right_item)
|
|
|
|
|
+ for left_item, right_item in zip(left, right, strict=True)
|
|
|
|
|
+ )
|
|
|
|
|
+ return left == right
|