activegraph 1.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- activegraph/__init__.py +222 -0
- activegraph/__main__.py +5 -0
- activegraph/behaviors/__init__.py +1 -0
- activegraph/behaviors/base.py +153 -0
- activegraph/behaviors/decorators.py +218 -0
- activegraph/cli/__init__.py +17 -0
- activegraph/cli/main.py +793 -0
- activegraph/cli/quickstart.py +556 -0
- activegraph/core/__init__.py +1 -0
- activegraph/core/clock.py +46 -0
- activegraph/core/event.py +33 -0
- activegraph/core/graph.py +846 -0
- activegraph/core/ids.py +135 -0
- activegraph/core/patch.py +47 -0
- activegraph/core/view.py +59 -0
- activegraph/errors.py +342 -0
- activegraph/frame.py +15 -0
- activegraph/llm/__init__.py +51 -0
- activegraph/llm/anthropic.py +339 -0
- activegraph/llm/cache.py +180 -0
- activegraph/llm/errors.py +257 -0
- activegraph/llm/prompt.py +420 -0
- activegraph/llm/provider.py +66 -0
- activegraph/llm/recorded.py +270 -0
- activegraph/llm/types.py +130 -0
- activegraph/observability/__init__.py +61 -0
- activegraph/observability/logging.py +205 -0
- activegraph/observability/metrics.py +219 -0
- activegraph/observability/migration.py +404 -0
- activegraph/observability/prometheus.py +101 -0
- activegraph/observability/status.py +83 -0
- activegraph/packs/__init__.py +999 -0
- activegraph/packs/diligence/__init__.py +86 -0
- activegraph/packs/diligence/behaviors.py +447 -0
- activegraph/packs/diligence/fixtures/__init__.py +364 -0
- activegraph/packs/diligence/fixtures/companies.py +511 -0
- activegraph/packs/diligence/object_types.py +163 -0
- activegraph/packs/diligence/settings.py +60 -0
- activegraph/packs/diligence/tools.py +124 -0
- activegraph/packs/loader.py +709 -0
- activegraph/packs/scaffold.py +317 -0
- activegraph/policy.py +20 -0
- activegraph/runtime/__init__.py +1 -0
- activegraph/runtime/behavior_graph.py +151 -0
- activegraph/runtime/budget.py +120 -0
- activegraph/runtime/config_errors.py +124 -0
- activegraph/runtime/diff.py +145 -0
- activegraph/runtime/errors.py +216 -0
- activegraph/runtime/exec_errors.py +232 -0
- activegraph/runtime/patterns.py +946 -0
- activegraph/runtime/queue.py +27 -0
- activegraph/runtime/registration_errors.py +291 -0
- activegraph/runtime/registry.py +111 -0
- activegraph/runtime/runtime.py +2441 -0
- activegraph/runtime/scheduler.py +206 -0
- activegraph/runtime/view_builder.py +65 -0
- activegraph/store/__init__.py +41 -0
- activegraph/store/base.py +66 -0
- activegraph/store/conformance.py +161 -0
- activegraph/store/errors.py +84 -0
- activegraph/store/memory.py +118 -0
- activegraph/store/postgres.py +609 -0
- activegraph/store/serde.py +202 -0
- activegraph/store/sqlite.py +446 -0
- activegraph/store/url.py +230 -0
- activegraph/tools/__init__.py +64 -0
- activegraph/tools/base.py +57 -0
- activegraph/tools/cache.py +157 -0
- activegraph/tools/context.py +48 -0
- activegraph/tools/decorators.py +70 -0
- activegraph/tools/errors.py +320 -0
- activegraph/tools/graph_query.py +94 -0
- activegraph/tools/recorded.py +205 -0
- activegraph/tools/web_fetch.py +80 -0
- activegraph/trace/__init__.py +1 -0
- activegraph/trace/causal.py +123 -0
- activegraph/trace/printer.py +495 -0
- activegraph-1.0.0rc2.dist-info/METADATA +228 -0
- activegraph-1.0.0rc2.dist-info/RECORD +82 -0
- activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
- activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
- activegraph-1.0.0rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Single in-process FIFO queue. CONTRACT #10: no priority, no async."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from activegraph.core.event import Event
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventQueue:
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self._q: deque[Event] = deque()
|
|
14
|
+
|
|
15
|
+
def push(self, event: Event) -> None:
|
|
16
|
+
self._q.append(event)
|
|
17
|
+
|
|
18
|
+
def pop(self) -> Optional[Event]:
|
|
19
|
+
if not self._q:
|
|
20
|
+
return None
|
|
21
|
+
return self._q.popleft()
|
|
22
|
+
|
|
23
|
+
def __len__(self) -> int:
|
|
24
|
+
return len(self._q)
|
|
25
|
+
|
|
26
|
+
def __bool__(self) -> bool:
|
|
27
|
+
return bool(self._q)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Runtime-side registration error leaves. v1.0 PR-E.
|
|
2
|
+
|
|
3
|
+
These leaves fire at registration time (or at registration-adjacent
|
|
4
|
+
lookup time) and are grouped here rather than scattered across
|
|
5
|
+
runtime.py / scheduler.py / behaviors/. Topic-module raise sites
|
|
6
|
+
import from here; consolidation makes the category audit trivial.
|
|
7
|
+
|
|
8
|
+
Pack registration errors live in :mod:`activegraph.packs.__init__`
|
|
9
|
+
(alongside the other Pack* classes for back-compat). LLM-side
|
|
10
|
+
:class:`MissingProviderError` and tool-side :class:`MissingToolError`
|
|
11
|
+
stay in their topic modules and re-parent in place.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from activegraph.errors import RegistrationError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BehaviorNotFoundError(RegistrationError, LookupError):
|
|
22
|
+
"""``runtime.get_behavior(name)`` could not resolve the name to a
|
|
23
|
+
registered behavior.
|
|
24
|
+
|
|
25
|
+
Multi-inherits :class:`LookupError` so user code that catches the
|
|
26
|
+
builtin around behavior lookups continues to work.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_doc_slug = "behavior-not-found-error"
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
name: str,
|
|
34
|
+
*,
|
|
35
|
+
registered: Optional[tuple[str, ...]] = None,
|
|
36
|
+
pack_state: bool = False,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.name = name
|
|
39
|
+
self.registered = registered or ()
|
|
40
|
+
ctx: dict[str, Any] = {"name": name, "pack_state": pack_state}
|
|
41
|
+
if self.registered:
|
|
42
|
+
ctx["registered"] = list(self.registered)
|
|
43
|
+
sample = ""
|
|
44
|
+
if self.registered:
|
|
45
|
+
preview = ", ".join(repr(n) for n in list(self.registered)[:6])
|
|
46
|
+
extra = f" (+{len(self.registered) - 6} more)" if len(self.registered) > 6 else ""
|
|
47
|
+
sample = f"\n registered: {preview}{extra}"
|
|
48
|
+
RegistrationError.__init__(
|
|
49
|
+
self,
|
|
50
|
+
f"no behavior named {name!r} is loaded",
|
|
51
|
+
what_failed=(
|
|
52
|
+
f"runtime.get_behavior({name!r}) could not resolve the name "
|
|
53
|
+
f"to a registered behavior.{sample}"
|
|
54
|
+
),
|
|
55
|
+
why=(
|
|
56
|
+
"Behaviors are addressable by their declared name. The "
|
|
57
|
+
"lookup is strict — the runtime refuses to fall back to a "
|
|
58
|
+
"fuzzy match or a no-op because a wrong-behavior dispatch "
|
|
59
|
+
"would silently corrupt the audit trail. Behaviors live "
|
|
60
|
+
"either in the global registry (decorated with `@behavior` "
|
|
61
|
+
"or `@llm_behavior` at module load) or in a loaded pack."
|
|
62
|
+
),
|
|
63
|
+
how_to_fix=(
|
|
64
|
+
"Check the spelling against the registered behaviors above. "
|
|
65
|
+
"If the behavior comes from a pack, confirm the pack is "
|
|
66
|
+
"loaded:\n"
|
|
67
|
+
" rt.load_pack(my_pack)\n"
|
|
68
|
+
"If the behavior is defined in user code, confirm the "
|
|
69
|
+
"decorator ran (the module is imported) before the runtime "
|
|
70
|
+
"is constructed.\n"
|
|
71
|
+
"\n"
|
|
72
|
+
"For canonical lookups across packs, use the fully-qualified "
|
|
73
|
+
"form `'pack_name.behavior_name'`."
|
|
74
|
+
),
|
|
75
|
+
context=ctx,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AmbiguousBehaviorError(RegistrationError, ValueError):
|
|
80
|
+
"""A short behavior name resolves to more than one loaded pack.
|
|
81
|
+
|
|
82
|
+
Fires only when both packs declare a behavior under the same short
|
|
83
|
+
name. The user is asked to disambiguate by using the canonical
|
|
84
|
+
`pack_name.behavior_name` form. CONTRACT v0.9 #8.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_doc_slug = "ambiguous-behavior-error"
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
name: str,
|
|
92
|
+
*,
|
|
93
|
+
packs: Optional[tuple[str, ...]] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self.name = name
|
|
96
|
+
self.packs = packs or ()
|
|
97
|
+
ctx: dict[str, Any] = {"name": name}
|
|
98
|
+
if self.packs:
|
|
99
|
+
ctx["packs"] = list(self.packs)
|
|
100
|
+
pack_list = (
|
|
101
|
+
", ".join(f"{p!r}" for p in self.packs)
|
|
102
|
+
if self.packs
|
|
103
|
+
else "(multiple packs)"
|
|
104
|
+
)
|
|
105
|
+
example = (
|
|
106
|
+
f"{self.packs[0]}.{name}" if self.packs else f"pack_name.{name}"
|
|
107
|
+
)
|
|
108
|
+
RegistrationError.__init__(
|
|
109
|
+
self,
|
|
110
|
+
f"behavior name {name!r} is ambiguous across loaded packs",
|
|
111
|
+
what_failed=(
|
|
112
|
+
f"The short behavior name {name!r} resolves to behaviors in "
|
|
113
|
+
f"more than one loaded pack: {pack_list}. The runtime cannot "
|
|
114
|
+
f"pick one without an explicit choice."
|
|
115
|
+
),
|
|
116
|
+
why=(
|
|
117
|
+
"Pack-prefixed names are canonical; short names are a "
|
|
118
|
+
"convenience for the common single-pack case (CONTRACT "
|
|
119
|
+
"v0.9 #8). When multiple packs declare the same short "
|
|
120
|
+
"name, the convenience would have to either pick one "
|
|
121
|
+
"silently (which would change behavior on pack-load order) "
|
|
122
|
+
"or pick neither — both produce surprises. The runtime "
|
|
123
|
+
"refuses the lookup and asks for the canonical name."
|
|
124
|
+
),
|
|
125
|
+
how_to_fix=(
|
|
126
|
+
f"Use the canonical form:\n"
|
|
127
|
+
f" rt.get_behavior({example!r})\n"
|
|
128
|
+
f"\n"
|
|
129
|
+
f"If you wanted both packs' behaviors to fire together "
|
|
130
|
+
f"under the same trigger, they are registered separately "
|
|
131
|
+
f"in the runtime — both will fire on a matching event "
|
|
132
|
+
f"regardless of which one your lookup names. The lookup "
|
|
133
|
+
f"is for explicit by-name access, not for trigger "
|
|
134
|
+
f"dispatch."
|
|
135
|
+
),
|
|
136
|
+
context=ctx,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ToolNotFoundError(RegistrationError, LookupError):
|
|
141
|
+
"""``runtime.get_tool(name)`` could not resolve the name to a
|
|
142
|
+
registered tool.
|
|
143
|
+
|
|
144
|
+
Symmetric with :class:`BehaviorNotFoundError`. Multi-inherits
|
|
145
|
+
:class:`LookupError` for back-compat.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
_doc_slug = "tool-not-found-error"
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
name: str,
|
|
153
|
+
*,
|
|
154
|
+
registered: Optional[tuple[str, ...]] = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
self.name = name
|
|
157
|
+
self.registered = registered or ()
|
|
158
|
+
ctx: dict[str, Any] = {"name": name}
|
|
159
|
+
if self.registered:
|
|
160
|
+
ctx["registered"] = list(self.registered)
|
|
161
|
+
sample = ""
|
|
162
|
+
if self.registered:
|
|
163
|
+
preview = ", ".join(repr(n) for n in list(self.registered)[:6])
|
|
164
|
+
extra = f" (+{len(self.registered) - 6} more)" if len(self.registered) > 6 else ""
|
|
165
|
+
sample = f"\n registered: {preview}{extra}"
|
|
166
|
+
RegistrationError.__init__(
|
|
167
|
+
self,
|
|
168
|
+
f"no tool named {name!r} is loaded",
|
|
169
|
+
what_failed=(
|
|
170
|
+
f"runtime.get_tool({name!r}) could not resolve the name to a "
|
|
171
|
+
f"registered tool.{sample}"
|
|
172
|
+
),
|
|
173
|
+
why=(
|
|
174
|
+
"Tools are addressable by their declared name. The runtime "
|
|
175
|
+
"refuses to fall back to a fuzzy match because the tool's "
|
|
176
|
+
"input/output schema is part of the contract — invoking the "
|
|
177
|
+
"wrong tool with the right name would silently produce "
|
|
178
|
+
"wrong-shape data."
|
|
179
|
+
),
|
|
180
|
+
how_to_fix=(
|
|
181
|
+
"Check the spelling against the registered tools above. If "
|
|
182
|
+
"the tool comes from a pack, confirm the pack is loaded. If "
|
|
183
|
+
"the tool is in user code, confirm the @tool decorator ran "
|
|
184
|
+
"(module imported) before the runtime is constructed.\n"
|
|
185
|
+
"\n"
|
|
186
|
+
"For canonical lookups across packs, use "
|
|
187
|
+
"`'pack_name.tool_name'`."
|
|
188
|
+
),
|
|
189
|
+
context=ctx,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class AmbiguousToolError(RegistrationError, ValueError):
|
|
194
|
+
"""A short tool name resolves to more than one loaded pack.
|
|
195
|
+
|
|
196
|
+
Symmetric with :class:`AmbiguousBehaviorError`. CONTRACT v0.9 #9.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
_doc_slug = "ambiguous-tool-error"
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
name: str,
|
|
204
|
+
*,
|
|
205
|
+
packs: Optional[tuple[str, ...]] = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
self.name = name
|
|
208
|
+
self.packs = packs or ()
|
|
209
|
+
ctx: dict[str, Any] = {"name": name}
|
|
210
|
+
if self.packs:
|
|
211
|
+
ctx["packs"] = list(self.packs)
|
|
212
|
+
pack_list = (
|
|
213
|
+
", ".join(f"{p!r}" for p in self.packs)
|
|
214
|
+
if self.packs
|
|
215
|
+
else "(multiple packs)"
|
|
216
|
+
)
|
|
217
|
+
example = (
|
|
218
|
+
f"{self.packs[0]}.{name}" if self.packs else f"pack_name.{name}"
|
|
219
|
+
)
|
|
220
|
+
RegistrationError.__init__(
|
|
221
|
+
self,
|
|
222
|
+
f"tool name {name!r} is ambiguous across loaded packs",
|
|
223
|
+
what_failed=(
|
|
224
|
+
f"The short tool name {name!r} resolves to tools in more "
|
|
225
|
+
f"than one loaded pack: {pack_list}. The runtime cannot pick "
|
|
226
|
+
f"one without an explicit choice."
|
|
227
|
+
),
|
|
228
|
+
why=(
|
|
229
|
+
"Same rule as behavior names (CONTRACT v0.9 #9): "
|
|
230
|
+
"pack-prefixed names are canonical, short names are a "
|
|
231
|
+
"convenience for the single-pack case. Multi-pack "
|
|
232
|
+
"ambiguity is refused because picking one would silently "
|
|
233
|
+
"swap which tool's schema validates the LLM's call."
|
|
234
|
+
),
|
|
235
|
+
how_to_fix=(
|
|
236
|
+
f"Use the canonical form:\n"
|
|
237
|
+
f" rt.get_tool({example!r})\n"
|
|
238
|
+
f"\n"
|
|
239
|
+
f"If you want the @llm_behavior to be able to choose "
|
|
240
|
+
f"either pack's version, list both canonical names in "
|
|
241
|
+
f"its `tools=[...]` argument."
|
|
242
|
+
),
|
|
243
|
+
context=ctx,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class InvalidToolRegistration(RegistrationError, TypeError):
|
|
248
|
+
"""A value passed to ``Runtime(tools=[...])`` is not a Tool instance.
|
|
249
|
+
|
|
250
|
+
Common cause: the developer passed a bare function instead of one
|
|
251
|
+
decorated with ``@tool``. Multi-inherits :class:`TypeError` for
|
|
252
|
+
back-compat.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
_doc_slug = "invalid-tool-registration"
|
|
256
|
+
|
|
257
|
+
def __init__(self, value: Any) -> None:
|
|
258
|
+
self.value = value
|
|
259
|
+
type_name = type(value).__name__
|
|
260
|
+
repr_short = repr(value)
|
|
261
|
+
if len(repr_short) > 80:
|
|
262
|
+
repr_short = repr_short[:77] + "..."
|
|
263
|
+
RegistrationError.__init__(
|
|
264
|
+
self,
|
|
265
|
+
f"tool registration value is not a Tool instance (got {type_name})",
|
|
266
|
+
what_failed=(
|
|
267
|
+
f"Runtime(tools=[...]) was given a value that isn't a Tool "
|
|
268
|
+
f"instance:\n value: {repr_short}\n type: {type_name}"
|
|
269
|
+
),
|
|
270
|
+
why=(
|
|
271
|
+
"The Tool wrapper carries the tool's declared name, input "
|
|
272
|
+
"schema, output schema, timeout, and deterministic flag. "
|
|
273
|
+
"Registering a bare function would skip those declarations "
|
|
274
|
+
"and the runtime could not validate calls into the tool — "
|
|
275
|
+
"schema-violating calls would reach the body and produce "
|
|
276
|
+
"wrong-shape data. The check fails fast at construction."
|
|
277
|
+
),
|
|
278
|
+
how_to_fix=(
|
|
279
|
+
"Decorate the function with @tool, and pass the decorated "
|
|
280
|
+
"object:\n"
|
|
281
|
+
" @tool(name='my_tool', input_schema=..., output_schema=...)\n"
|
|
282
|
+
" def my_tool(args, ctx): ...\n"
|
|
283
|
+
"\n"
|
|
284
|
+
" rt = Runtime(graph, tools=[my_tool])\n"
|
|
285
|
+
"\n"
|
|
286
|
+
"If the function was already decorated, confirm you're "
|
|
287
|
+
"passing the decorator's return value (the wrapped Tool), "
|
|
288
|
+
"not the original function."
|
|
289
|
+
),
|
|
290
|
+
context={"type": type_name, "repr": repr_short},
|
|
291
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Match events to behaviors. CONTRACT #10: registration order for ties.
|
|
2
|
+
|
|
3
|
+
v0.7 (CONTRACT v0.7 #11): a behavior with both `on=[...]` and
|
|
4
|
+
`pattern=...` requires BOTH conditions — the event type matches AND
|
|
5
|
+
the pattern matches against the post-event graph state. A behavior
|
|
6
|
+
with only `pattern=...` (empty `on`) matches on every non-lifecycle
|
|
7
|
+
event.
|
|
8
|
+
|
|
9
|
+
`match()` returns `(behavior, relations, matches)` triples. The
|
|
10
|
+
`matches` list is the pattern matcher's bindings (empty for
|
|
11
|
+
behaviors without `pattern=`). The runtime forwards `matches` as
|
|
12
|
+
`ctx.matches`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Iterable, Union
|
|
18
|
+
|
|
19
|
+
from activegraph.behaviors.base import Behavior, RelationBehavior
|
|
20
|
+
from activegraph.core.event import Event
|
|
21
|
+
from activegraph.core.graph import Graph, Relation, evaluate_where
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
BehaviorLike = Union[Behavior, RelationBehavior]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Registry:
|
|
28
|
+
def __init__(self, behaviors: Iterable[BehaviorLike]) -> None:
|
|
29
|
+
self._behaviors: list[BehaviorLike] = list(behaviors)
|
|
30
|
+
|
|
31
|
+
def all(self) -> list[BehaviorLike]:
|
|
32
|
+
return list(self._behaviors)
|
|
33
|
+
|
|
34
|
+
def index_of(self, behavior: BehaviorLike) -> int:
|
|
35
|
+
for i, b in enumerate(self._behaviors):
|
|
36
|
+
if b is behavior:
|
|
37
|
+
return i
|
|
38
|
+
return -1
|
|
39
|
+
|
|
40
|
+
def match(
|
|
41
|
+
self, event: Event, graph: Graph
|
|
42
|
+
) -> list[tuple[BehaviorLike, list[Relation], list[Any]]]:
|
|
43
|
+
"""Return (behavior, matching_relations, pattern_matches) triples
|
|
44
|
+
in registration order. Pattern matches are empty for behaviors
|
|
45
|
+
without `pattern=`.
|
|
46
|
+
"""
|
|
47
|
+
out: list[tuple[BehaviorLike, list[Relation], list[Any]]] = []
|
|
48
|
+
for b in self._behaviors:
|
|
49
|
+
# Event-type filter: required only when `on=` is non-empty.
|
|
50
|
+
# Pattern-only behaviors (empty `on`) skip this gate.
|
|
51
|
+
if b.on and event.type not in b.on:
|
|
52
|
+
continue
|
|
53
|
+
# Suppress lifecycle events for pattern-only behaviors so a
|
|
54
|
+
# pattern doesn't fire on behavior.started, etc.
|
|
55
|
+
if not b.on and _is_lifecycle(event):
|
|
56
|
+
continue
|
|
57
|
+
pattern_matches: list[Any] = []
|
|
58
|
+
if b.pattern_matcher is not None:
|
|
59
|
+
pattern_matches = b.pattern_matcher.matches(event, graph)
|
|
60
|
+
if not pattern_matches:
|
|
61
|
+
continue
|
|
62
|
+
if isinstance(b, RelationBehavior):
|
|
63
|
+
rels = _matching_relations(b, event, graph)
|
|
64
|
+
if rels:
|
|
65
|
+
out.append((b, rels, pattern_matches))
|
|
66
|
+
else:
|
|
67
|
+
if b.where and not evaluate_where(b.where, event.payload):
|
|
68
|
+
continue
|
|
69
|
+
out.append((b, [], pattern_matches))
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_lifecycle(event: Event) -> bool:
|
|
74
|
+
return (
|
|
75
|
+
event.type.startswith("behavior.")
|
|
76
|
+
or event.type.startswith("relation_behavior.")
|
|
77
|
+
or event.type.startswith("runtime.")
|
|
78
|
+
or event.type.startswith("llm.")
|
|
79
|
+
or event.type.startswith("tool.")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _matching_relations(
|
|
84
|
+
rb: RelationBehavior, event: Event, graph: Graph
|
|
85
|
+
) -> list[Relation]:
|
|
86
|
+
candidates = [r for r in graph.all_relations() if r.type == rb.relation_type]
|
|
87
|
+
referenced = _collect_string_values(event.payload)
|
|
88
|
+
out: list[Relation] = []
|
|
89
|
+
for r in candidates:
|
|
90
|
+
if r.source in referenced or r.target in referenced:
|
|
91
|
+
if rb.where and not evaluate_where(rb.where, event.payload):
|
|
92
|
+
continue
|
|
93
|
+
out.append(r)
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _collect_string_values(obj) -> set[str]:
|
|
98
|
+
out: set[str] = set()
|
|
99
|
+
_walk(obj, out)
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _walk(obj, out: set[str]) -> None:
|
|
104
|
+
if isinstance(obj, str):
|
|
105
|
+
out.add(obj)
|
|
106
|
+
elif isinstance(obj, dict):
|
|
107
|
+
for v in obj.values():
|
|
108
|
+
_walk(v, out)
|
|
109
|
+
elif isinstance(obj, list):
|
|
110
|
+
for v in obj:
|
|
111
|
+
_walk(v, out)
|