smartcomment 0.1.0__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.
- smartcomment/__init__.py +49 -0
- smartcomment/analysis/__init__.py +1 -0
- smartcomment/analysis/visualization/__init__.py +43 -0
- smartcomment/analysis/visualization/_utils.py +103 -0
- smartcomment/analysis/visualization/graphviz.py +201 -0
- smartcomment/analysis/visualization/pyvis.py +155 -0
- smartcomment/api/__init__.py +18 -0
- smartcomment/api/_helpers.py +440 -0
- smartcomment/api/_mutation.py +318 -0
- smartcomment/api/public.py +971 -0
- smartcomment/debugging.py +69 -0
- smartcomment/drivers/__init__.py +12 -0
- smartcomment/drivers/base.py +309 -0
- smartcomment/drivers/in_memory.py +323 -0
- smartcomment/identity/__init__.py +6 -0
- smartcomment/identity/registry.py +214 -0
- smartcomment/logging.py +80 -0
- smartcomment/runtime/__init__.py +44 -0
- smartcomment/runtime/context.py +630 -0
- smartcomment/runtime/errors.py +117 -0
- smartcomment/runtime/graph.py +520 -0
- smartcomment/runtime/network.py +1841 -0
- smartcomment/runtime/operation.py +274 -0
- smartcomment/runtime/session.py +133 -0
- smartcomment/runtime/variable.py +237 -0
- smartcomment/schema/__init__.py +1 -0
- smartcomment/schema/base.py +134 -0
- smartcomment/schema/operation.py +96 -0
- smartcomment/schema/session.py +42 -0
- smartcomment/schema/variable.py +126 -0
- smartcomment-0.1.0.dist-info/METADATA +29 -0
- smartcomment-0.1.0.dist-info/RECORD +34 -0
- smartcomment-0.1.0.dist-info/WHEEL +5 -0
- smartcomment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
"""Public API for smartcomment, the ``comment_*`` family of functions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import inspect
|
|
6
|
+
from pydantic import BaseModel, JsonValue
|
|
7
|
+
from ..runtime import (
|
|
8
|
+
none_full_node_id,
|
|
9
|
+
none_op_id,
|
|
10
|
+
none_session_id,
|
|
11
|
+
current_graph,
|
|
12
|
+
current_context,
|
|
13
|
+
current_op,
|
|
14
|
+
current_session,
|
|
15
|
+
is_tracing_enabled,
|
|
16
|
+
)
|
|
17
|
+
from ..runtime.context import _OP
|
|
18
|
+
from ..runtime.operation import RuntimeEdge, RuntimeOp
|
|
19
|
+
from ..runtime.variable import RuntimeVariable
|
|
20
|
+
from ..schema.operation import OpEdge, OpRecord
|
|
21
|
+
from ..logging import logger
|
|
22
|
+
from ._mutation import _MutationScope
|
|
23
|
+
from ._helpers import (
|
|
24
|
+
_resolve_call_context,
|
|
25
|
+
_ensure_variable,
|
|
26
|
+
_resolve_item,
|
|
27
|
+
)
|
|
28
|
+
from typing import (
|
|
29
|
+
Any,
|
|
30
|
+
Callable,
|
|
31
|
+
Literal,
|
|
32
|
+
TypeVar,
|
|
33
|
+
overload,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
def comment_variable(
|
|
42
|
+
value: T,
|
|
43
|
+
*,
|
|
44
|
+
to_runtime: Literal[True],
|
|
45
|
+
variable_name: str | None = ...,
|
|
46
|
+
encoding_fn: Callable[[T], str] | None = ...,
|
|
47
|
+
decoding_fn: Callable[[str], T] | None = ...,
|
|
48
|
+
schema: type[BaseModel] | None = ...,
|
|
49
|
+
id_strategy: Callable[[T], str] | str | None = ...,
|
|
50
|
+
comment: str | None = ...,
|
|
51
|
+
category: str = ...,
|
|
52
|
+
class_name: str | None = ...,
|
|
53
|
+
auto_class_name: bool = ...,
|
|
54
|
+
identity_only: bool = ...,
|
|
55
|
+
metadata: dict[str, JsonValue] | None = ...,
|
|
56
|
+
) -> RuntimeVariable[T]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def comment_variable(
|
|
61
|
+
value: T,
|
|
62
|
+
*,
|
|
63
|
+
to_runtime: Literal[False] = ...,
|
|
64
|
+
variable_name: str | None = ...,
|
|
65
|
+
encoding_fn: Callable[[T], str] | None = ...,
|
|
66
|
+
decoding_fn: Callable[[str], T] | None = ...,
|
|
67
|
+
schema: type[BaseModel] | None = ...,
|
|
68
|
+
id_strategy: Callable[[T], str] | str | None = ...,
|
|
69
|
+
comment: str | None = ...,
|
|
70
|
+
category: str = ...,
|
|
71
|
+
class_name: str | None = ...,
|
|
72
|
+
auto_class_name: bool = ...,
|
|
73
|
+
identity_only: bool = ...,
|
|
74
|
+
metadata: dict[str, JsonValue] | None = ...,
|
|
75
|
+
) -> T: ...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def comment_variable(
|
|
79
|
+
value: T,
|
|
80
|
+
*,
|
|
81
|
+
to_runtime: bool = False,
|
|
82
|
+
variable_name: str | None = None,
|
|
83
|
+
encoding_fn: Callable[[T], str] | None = None,
|
|
84
|
+
decoding_fn: Callable[[str], T] | None = None,
|
|
85
|
+
schema: type[BaseModel] | None = None,
|
|
86
|
+
id_strategy: Callable[[T], str] | str | None = None,
|
|
87
|
+
comment: str | None = None,
|
|
88
|
+
category: str = "variable",
|
|
89
|
+
class_name: str | None = None,
|
|
90
|
+
auto_class_name: bool = False,
|
|
91
|
+
identity_only: bool = False,
|
|
92
|
+
metadata: dict[str, JsonValue] | None = None,
|
|
93
|
+
) -> RuntimeVariable[T] | T:
|
|
94
|
+
"""Register a value as a tracked variable in the execution graph.
|
|
95
|
+
|
|
96
|
+
Use this for starting variables (no parent operation) or when you
|
|
97
|
+
need to store the handle in the current tracing context for cross-scope
|
|
98
|
+
access.
|
|
99
|
+
|
|
100
|
+
When no session is active the variable is placed under the reserved
|
|
101
|
+
NONE sentinel session.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
value (`T`):
|
|
105
|
+
The Python value to track.
|
|
106
|
+
to_runtime (`bool`, defaults to `False`):
|
|
107
|
+
When enabled, a runtime variable handle is returned.
|
|
108
|
+
Otherwise, the original variable is returned after it has been
|
|
109
|
+
recorded in the graph.
|
|
110
|
+
variable_name (`str | None`, optional):
|
|
111
|
+
If provided, it registers the handle in the current tracing context
|
|
112
|
+
so the variable can be retrieved by name in inner scopes.
|
|
113
|
+
encoding_fn (`Callable[[T], str] | None`, optional):
|
|
114
|
+
Custom encoder from Python value to string.
|
|
115
|
+
decoding_fn (`Callable[[str], T] | None`, optional):
|
|
116
|
+
Custom decoder from string back to Python value.
|
|
117
|
+
schema (`type[BaseModel] | None`, optional):
|
|
118
|
+
Pydantic schema for auto encoding and decoding.
|
|
119
|
+
id_strategy (`Callable[[T], str] | str | None`, optional):
|
|
120
|
+
An identity strategy. It can be a callable, a registered name.
|
|
121
|
+
If not provided, it will be looked up by type.
|
|
122
|
+
comment (`str | None`, optional):
|
|
123
|
+
A comment for the variable.
|
|
124
|
+
category (`str`, defaults to `"variable"`):
|
|
125
|
+
Variable category.
|
|
126
|
+
class_name (`str | None`, optional):
|
|
127
|
+
Namespace from the Python class of the value.
|
|
128
|
+
auto_class_name (`bool`, defaults to `False`):
|
|
129
|
+
If enabled, it will auto-detect the class name from the value.
|
|
130
|
+
identity_only (`bool`, defaults to `False`):
|
|
131
|
+
When enabled, the snapshot consistency check is bypassed for
|
|
132
|
+
this variable. If a node with the same identity already exists
|
|
133
|
+
in the graph but has a different encoded value, the existing
|
|
134
|
+
node is returned instead of raising an error or creating a new
|
|
135
|
+
version. This is useful when the caller only holds a lightweight
|
|
136
|
+
representation (e.g. an identifier) of a variable that was
|
|
137
|
+
previously recorded with a richer snapshot.
|
|
138
|
+
metadata (`dict[str, JsonValue] | None`, optional):
|
|
139
|
+
Extra metadata for the variable.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
`RuntimeVariable[T] | T`:
|
|
143
|
+
The runtime handle when ``to_runtime`` is enabled, or the
|
|
144
|
+
original variable otherwise. It always returns the original
|
|
145
|
+
variable when the tracing is disabled.
|
|
146
|
+
"""
|
|
147
|
+
if not is_tracing_enabled():
|
|
148
|
+
return value # type: ignore[return-value]
|
|
149
|
+
|
|
150
|
+
if isinstance(value, RuntimeVariable):
|
|
151
|
+
if variable_name is not None:
|
|
152
|
+
ctx = current_context()
|
|
153
|
+
assert ctx is not None, (
|
|
154
|
+
"The current tracing context is not active."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
logger.debug(
|
|
158
|
+
"`comment_variable` receives a runtime variable handle as the value. "
|
|
159
|
+
"It will be registered in the current tracing context "
|
|
160
|
+
"under the name '{variable_name}'.",
|
|
161
|
+
variable_name=variable_name,
|
|
162
|
+
)
|
|
163
|
+
ctx.register_variable(variable_name, value, overwrite=True)
|
|
164
|
+
return value # type: ignore[return-value]
|
|
165
|
+
|
|
166
|
+
graph, session_id, filename, lineno = _resolve_call_context(
|
|
167
|
+
caller_name="comment_variable",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
rv = _ensure_variable(
|
|
171
|
+
value,
|
|
172
|
+
id_strategy=id_strategy,
|
|
173
|
+
encoding_fn=encoding_fn,
|
|
174
|
+
decoding_fn=decoding_fn,
|
|
175
|
+
schema=schema,
|
|
176
|
+
comment=comment,
|
|
177
|
+
category=category,
|
|
178
|
+
class_name=class_name,
|
|
179
|
+
auto_class_name=auto_class_name,
|
|
180
|
+
variable_name=variable_name,
|
|
181
|
+
metadata=metadata,
|
|
182
|
+
identity_only=identity_only,
|
|
183
|
+
graph=graph,
|
|
184
|
+
session_id=session_id,
|
|
185
|
+
filename=filename,
|
|
186
|
+
lineno=lineno,
|
|
187
|
+
force_new_version=False,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if to_runtime:
|
|
191
|
+
return rv
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def comment_op(
|
|
196
|
+
*,
|
|
197
|
+
inputs: list[Any],
|
|
198
|
+
outputs: list[Any],
|
|
199
|
+
op_name: str | None = None,
|
|
200
|
+
comment: str | None = None,
|
|
201
|
+
category: str = "operation",
|
|
202
|
+
id_strategy: Callable[[Any], str] | str | None = None,
|
|
203
|
+
encoding_fn: Callable[[Any], str] | None = None,
|
|
204
|
+
decoding_fn: Callable[[str], Any] | None = None,
|
|
205
|
+
schema: type[BaseModel] | None = None,
|
|
206
|
+
auto_class_name: bool = False,
|
|
207
|
+
metadata: dict[str, JsonValue] | None = None,
|
|
208
|
+
reuse_op: bool = False,
|
|
209
|
+
) -> RuntimeOp | None:
|
|
210
|
+
"""Record an operation that links input variables to output variables.
|
|
211
|
+
|
|
212
|
+
Each element in ``inputs`` and ``outputs`` is interpreted in one of three
|
|
213
|
+
ways. If the entry is already a runtime variable handle, that handle is
|
|
214
|
+
wired into the graph as given. If it is an ordinary Python value, a
|
|
215
|
+
variable node is created or reused using the same encoding and identity
|
|
216
|
+
rules as ``comment_variable``, using the shared parameters of this call
|
|
217
|
+
(``id_strategy``, ``encoding_fn``, and so on) as defaults. If it is a
|
|
218
|
+
value-option tuple where the first element is the value and the second is
|
|
219
|
+
a dictionary of per-item options, those keys in each option dictionary override
|
|
220
|
+
the shared defaults. For available options for each variable, please
|
|
221
|
+
refer to ``comment_variable``.
|
|
222
|
+
|
|
223
|
+
When ``inputs`` or ``outputs`` is empty, the NONE sentinel is
|
|
224
|
+
auto-injected so that graph edges always exist.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
inputs (`list[Any]`):
|
|
228
|
+
Input items.
|
|
229
|
+
outputs (`list[Any]`):
|
|
230
|
+
Output items.
|
|
231
|
+
op_name (`str | None`, optional):
|
|
232
|
+
Canonical operation name. When not provided, an active
|
|
233
|
+
operation context is required and its operation is reused.
|
|
234
|
+
comment (`str | None`, optional):
|
|
235
|
+
A comment for the operation.
|
|
236
|
+
category (`str`, defaults to `"operation"`):
|
|
237
|
+
Operation category.
|
|
238
|
+
id_strategy (`Callable[[Any], str] | str | None`, optional):
|
|
239
|
+
Shared identity strategy for auto-created variables.
|
|
240
|
+
encoding_fn (`Callable[[Any], str] | None`, optional):
|
|
241
|
+
Shared encoder for auto-created variables.
|
|
242
|
+
decoding_fn (`Callable[[str], Any] | None`, optional):
|
|
243
|
+
Shared decoder for auto-created variables.
|
|
244
|
+
schema (`type[BaseModel] | None`, optional):
|
|
245
|
+
Shared Pydantic schema for auto encoding and decoding.
|
|
246
|
+
auto_class_name (`bool`, defaults to `False`):
|
|
247
|
+
Auto-detect class name for auto-created variables.
|
|
248
|
+
metadata (`dict[str, JsonValue] | None`, optional):
|
|
249
|
+
Extra metadata merged into the operation record and corresponding
|
|
250
|
+
operation edges. It is also used as the default for auto-created variables.
|
|
251
|
+
To attach metadata to a specific variable instead of sharing this
|
|
252
|
+
dictionary, pass that slot as ``(value, {"metadata": ...})``
|
|
253
|
+
inside ``inputs``.
|
|
254
|
+
reuse_op (`bool`, defaults to `False`):
|
|
255
|
+
When enabled and an active operation context exists, edges are
|
|
256
|
+
attributed to the existing operation instead of creating a new
|
|
257
|
+
one. The ``category``, ``comment``, and ``metadata`` are applied
|
|
258
|
+
to the edges only.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
`RuntimeOp | None`:
|
|
262
|
+
A runtime view of the operation, or ``None`` if tracing is
|
|
263
|
+
disabled.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
`RuntimeError`:
|
|
267
|
+
If the operation name is not provided and no active operation context
|
|
268
|
+
exists.
|
|
269
|
+
"""
|
|
270
|
+
if not is_tracing_enabled():
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
graph, session_id, filename, lineno = _resolve_call_context(
|
|
274
|
+
caller_name="comment_op",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Resolve the operation before creating any variable nodes so that a
|
|
278
|
+
# failure does not leave orphan nodes in the driver.
|
|
279
|
+
ctx = current_context()
|
|
280
|
+
assert ctx is not None, (
|
|
281
|
+
"The current tracing context is not active."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
propagated = ctx.metadata
|
|
285
|
+
|
|
286
|
+
should_try_reuse = op_name is None or reuse_op
|
|
287
|
+
active_op = current_op() if should_try_reuse else None
|
|
288
|
+
|
|
289
|
+
if active_op is not None:
|
|
290
|
+
op = active_op
|
|
291
|
+
elif op_name is None:
|
|
292
|
+
raise RuntimeError(
|
|
293
|
+
"`comment_op` requires either an explicit operation name or an "
|
|
294
|
+
"active operation context. When the operation name is not provided, "
|
|
295
|
+
"please wrap the call in `with comment_op_scope(op_name=...) as op:`."
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
op = OpRecord(
|
|
299
|
+
graph_id=graph.graph_id,
|
|
300
|
+
session_id=session_id,
|
|
301
|
+
user_id=graph.user_id,
|
|
302
|
+
project_id=graph.project_id,
|
|
303
|
+
op_name=op_name,
|
|
304
|
+
comment=comment,
|
|
305
|
+
category=category,
|
|
306
|
+
filename=filename,
|
|
307
|
+
lineno=lineno,
|
|
308
|
+
)
|
|
309
|
+
if metadata:
|
|
310
|
+
op.update_metadata(metadata)
|
|
311
|
+
if propagated:
|
|
312
|
+
op.update_metadata(propagated)
|
|
313
|
+
graph._driver.add_operation(op)
|
|
314
|
+
|
|
315
|
+
resolve_kwargs = dict(
|
|
316
|
+
shared_id_strategy=id_strategy,
|
|
317
|
+
shared_encoding_fn=encoding_fn,
|
|
318
|
+
shared_decoding_fn=decoding_fn,
|
|
319
|
+
shared_schema=schema,
|
|
320
|
+
shared_auto_class_name=auto_class_name,
|
|
321
|
+
shared_category="variable",
|
|
322
|
+
shared_metadata=metadata,
|
|
323
|
+
graph=graph,
|
|
324
|
+
session_id=session_id,
|
|
325
|
+
filename=filename,
|
|
326
|
+
lineno=lineno,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Resolve input items first so we can detect self-loops in outputs.
|
|
330
|
+
input_rvs = [_resolve_item(item, **resolve_kwargs) for item in inputs]
|
|
331
|
+
input_node_ids = {rv.full_node_id for rv in input_rvs}
|
|
332
|
+
|
|
333
|
+
# Resolve output items.
|
|
334
|
+
# If an output resolves to the same node as an input,
|
|
335
|
+
# force a new version to prevent self-loop edges.
|
|
336
|
+
output_rvs = []
|
|
337
|
+
for item in outputs:
|
|
338
|
+
# Resolve each output to a runtime variable. When an output would resolve to
|
|
339
|
+
# the same graph node as an already-resolved input (same identity and encoded
|
|
340
|
+
# value), the first resolution returns that existing node.
|
|
341
|
+
rv = _resolve_item(item, **resolve_kwargs)
|
|
342
|
+
if rv.full_node_id in input_node_ids:
|
|
343
|
+
logger.warning(
|
|
344
|
+
"Output collides with an input node whose full name is '{!r}' "
|
|
345
|
+
"in the operation named '{}'. "
|
|
346
|
+
"A new variable version is created and edges are added. This is valid "
|
|
347
|
+
"but non-idiomatic and increases graph complexity.",
|
|
348
|
+
rv.full_name,
|
|
349
|
+
op_name,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# We then call `_resolve_item` again with `force_new_version=True`
|
|
353
|
+
# so a new variable version is created, avoiding a self-loop edge
|
|
354
|
+
# from the node to itself while still recording edges from inputs to this output.
|
|
355
|
+
rv = _resolve_item(item, force_new_version=True, **resolve_kwargs)
|
|
356
|
+
output_rvs.append(rv)
|
|
357
|
+
|
|
358
|
+
runtime_op = RuntimeOp(op=op)
|
|
359
|
+
|
|
360
|
+
if not input_rvs and not output_rvs:
|
|
361
|
+
logger.debug(
|
|
362
|
+
"No input or output variables are provided for the operation named '{}'.",
|
|
363
|
+
op_name,
|
|
364
|
+
)
|
|
365
|
+
return runtime_op
|
|
366
|
+
|
|
367
|
+
# NONE sentinel injection.
|
|
368
|
+
if not input_rvs or not output_rvs:
|
|
369
|
+
graph._ensure_none_node()
|
|
370
|
+
|
|
371
|
+
# Build two lists of source and target full node identifiers.
|
|
372
|
+
_none = none_full_node_id()
|
|
373
|
+
source_ids = [rv.full_node_id for rv in input_rvs] if input_rvs else [_none]
|
|
374
|
+
target_ids = [rv.full_node_id for rv in output_rvs] if output_rvs else [_none]
|
|
375
|
+
|
|
376
|
+
# Pre-compute merged metadata for edges.
|
|
377
|
+
edge_metadata = {}
|
|
378
|
+
if metadata:
|
|
379
|
+
edge_metadata.update(metadata)
|
|
380
|
+
if propagated:
|
|
381
|
+
edge_metadata.update(propagated)
|
|
382
|
+
|
|
383
|
+
# Create edges (cartesian product).
|
|
384
|
+
for src in source_ids:
|
|
385
|
+
for tgt in target_ids:
|
|
386
|
+
edge = OpEdge(
|
|
387
|
+
graph_id=graph.graph_id,
|
|
388
|
+
session_id=session_id,
|
|
389
|
+
user_id=graph.user_id,
|
|
390
|
+
project_id=graph.project_id,
|
|
391
|
+
op_id=op.op_id,
|
|
392
|
+
category=category,
|
|
393
|
+
source_full_node_id=src,
|
|
394
|
+
target_full_node_id=tgt,
|
|
395
|
+
comment=comment,
|
|
396
|
+
filename=filename,
|
|
397
|
+
lineno=lineno,
|
|
398
|
+
)
|
|
399
|
+
if edge_metadata:
|
|
400
|
+
edge.update_metadata(edge_metadata)
|
|
401
|
+
graph._driver.add_edge(edge)
|
|
402
|
+
|
|
403
|
+
return runtime_op
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def comment_mutation(
|
|
407
|
+
*,
|
|
408
|
+
target: T,
|
|
409
|
+
inputs: list[Any] | None = None,
|
|
410
|
+
comment: str | None = None,
|
|
411
|
+
category: str = "variable",
|
|
412
|
+
class_name: str | None = None,
|
|
413
|
+
auto_class_name: bool = False,
|
|
414
|
+
id_strategy: Callable[[Any], str] | str | None = None,
|
|
415
|
+
encoding_fn: Callable[[Any], str] | None = None,
|
|
416
|
+
decoding_fn: Callable[[str], Any] | None = None,
|
|
417
|
+
schema: type[BaseModel] | None = None,
|
|
418
|
+
mutation_name: str | None = None,
|
|
419
|
+
mutation_comment: str | None = None,
|
|
420
|
+
mutation_category: str = "mutation",
|
|
421
|
+
metadata: dict[str, JsonValue] | None = None,
|
|
422
|
+
mutation_metadata: dict[str, JsonValue] | None = None,
|
|
423
|
+
reuse_op: bool = False,
|
|
424
|
+
) -> _MutationScope[T]:
|
|
425
|
+
"""Context manager that records an in-place mutation as a new version.
|
|
426
|
+
|
|
427
|
+
It wraps the code that mutates a target variable inside the ``with``
|
|
428
|
+
block. On entry, the current state of target is snapshotted. On
|
|
429
|
+
normal exit, the target is re-encoded and a new version node, an
|
|
430
|
+
operation record, and connecting edges are written to the graph.
|
|
431
|
+
|
|
432
|
+
``target`` must be a mutable Python object. Passing a
|
|
433
|
+
runtime variable raises an error because it is a read-only
|
|
434
|
+
view and mutating it would violate tracing invariants.
|
|
435
|
+
|
|
436
|
+
Example::
|
|
437
|
+
|
|
438
|
+
with comment_mutation(target=my_list, mutation_name="append") as scope:
|
|
439
|
+
my_list.append("new_item")
|
|
440
|
+
new_rv = scope.result # RuntimeVariable for the new version
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
target (`T`):
|
|
444
|
+
The mutable Python object to track.
|
|
445
|
+
inputs (`list[Any] | None`, optional):
|
|
446
|
+
Additional input variables involved in the mutation. Each
|
|
447
|
+
element may be a raw value, a runtime variable, or a
|
|
448
|
+
value-option tuple whose dict keys override the
|
|
449
|
+
shared defaults (``id_strategy``, ``encoding_fn``, etc.).
|
|
450
|
+
comment (`str | None`, optional):
|
|
451
|
+
Comment attached to the target variable (the new version
|
|
452
|
+
node). For the operation comment, use `mutation_comment`.
|
|
453
|
+
category (`str`, defaults to `"variable"`):
|
|
454
|
+
Category for the target variable node. Also used as the
|
|
455
|
+
shared default for input variables that do not override it.
|
|
456
|
+
class_name (`str | None`, optional):
|
|
457
|
+
Namespace prefix for the target variable's full name.
|
|
458
|
+
auto_class_name (`bool`, defaults to `False`):
|
|
459
|
+
Auto-detect `class_name` from the target's Python type.
|
|
460
|
+
id_strategy (`Callable[[Any], str] | str | None`, optional):
|
|
461
|
+
Identity strategy for the target variable. Also used as the
|
|
462
|
+
shared default for input variables that do not override it.
|
|
463
|
+
encoding_fn (`Callable[[Any], str] | None`, optional):
|
|
464
|
+
Encoder for the target and input variables (shared default).
|
|
465
|
+
decoding_fn (`Callable[[str], Any] | None`, optional):
|
|
466
|
+
Decoder paired with the encoding function (shared default).
|
|
467
|
+
schema (`type[BaseModel] | None`, optional):
|
|
468
|
+
Pydantic schema for auto encoding and decoding (shared default).
|
|
469
|
+
mutation_name (`str | None`, optional):
|
|
470
|
+
Canonical operation name. If not provided, it will be auto-generated
|
|
471
|
+
from the target variable's full name.
|
|
472
|
+
mutation_comment (`str | None`, optional):
|
|
473
|
+
Comment attached to the operation record and propagated
|
|
474
|
+
to the version edge and input edges. If not provided, the
|
|
475
|
+
version edge will use the default comment.
|
|
476
|
+
mutation_category (`str`, defaults to `"mutation"`):
|
|
477
|
+
Category for the operation record and edges.
|
|
478
|
+
metadata (`dict[str, JsonValue] | None`, optional):
|
|
479
|
+
Extra metadata attached to the target variable (the new
|
|
480
|
+
version node).
|
|
481
|
+
mutation_metadata (`dict[str, JsonValue] | None`, optional):
|
|
482
|
+
Extra metadata attached to the operation record. Edges
|
|
483
|
+
also receive a copy.
|
|
484
|
+
reuse_op (`bool`, defaults to `False`):
|
|
485
|
+
When enabled and an active operation context exists, edges
|
|
486
|
+
are attributed to the existing operation instead of creating a
|
|
487
|
+
new mutation operation. The ``mutation_comment`` and
|
|
488
|
+
``mutation_category`` are still applied to the edges. The
|
|
489
|
+
version bump (new variable node) always happens regardless
|
|
490
|
+
of this flag. When no active operation context is found,
|
|
491
|
+
a new mutation operation is created as usual.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
`_MutationScope[T]`:
|
|
495
|
+
A context manager for the in-place mutation operation.
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
`ValueError`:
|
|
499
|
+
If target variable is a runtime variable or an input collides
|
|
500
|
+
with the target's full node identifier.
|
|
501
|
+
`RuntimeError`:
|
|
502
|
+
If no graph context is active.
|
|
503
|
+
"""
|
|
504
|
+
if isinstance(target, RuntimeVariable):
|
|
505
|
+
raise ValueError(
|
|
506
|
+
"`comment_mutation` does not accept a runtime variable as "
|
|
507
|
+
"target because it is a read-only view. Pass the mutable "
|
|
508
|
+
"Python object directly."
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return _MutationScope(
|
|
512
|
+
target=target,
|
|
513
|
+
inputs=inputs,
|
|
514
|
+
comment=comment,
|
|
515
|
+
category=category,
|
|
516
|
+
class_name=class_name,
|
|
517
|
+
auto_class_name=auto_class_name,
|
|
518
|
+
id_strategy=id_strategy,
|
|
519
|
+
encoding_fn=encoding_fn,
|
|
520
|
+
decoding_fn=decoding_fn,
|
|
521
|
+
schema=schema,
|
|
522
|
+
mutation_name=mutation_name,
|
|
523
|
+
mutation_comment=mutation_comment,
|
|
524
|
+
mutation_category=mutation_category,
|
|
525
|
+
metadata=metadata,
|
|
526
|
+
mutation_metadata=mutation_metadata,
|
|
527
|
+
reuse_op=reuse_op,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def comment_fn(
|
|
532
|
+
*,
|
|
533
|
+
op_name: str | None = None,
|
|
534
|
+
comment: str | None = None,
|
|
535
|
+
category: str = "operation",
|
|
536
|
+
id_strategy: Callable[[Any], str] | str | None = None,
|
|
537
|
+
encoding_fn: Callable[[Any], str] | None = None,
|
|
538
|
+
decoding_fn: Callable[[str], Any] | None = None,
|
|
539
|
+
schema: type[BaseModel] | None = None,
|
|
540
|
+
auto_class_name: bool = False,
|
|
541
|
+
op_metadata: dict[str, JsonValue] | None = None,
|
|
542
|
+
metadata: dict[str, JsonValue] | None = None,
|
|
543
|
+
param_options: dict[str, dict[str, Any]] | None = None,
|
|
544
|
+
include_source: bool = False,
|
|
545
|
+
) -> Callable:
|
|
546
|
+
"""Decorator for automatically tracing function-level calls.
|
|
547
|
+
|
|
548
|
+
It wraps a synchronous or asynchronous function so that every call records
|
|
549
|
+
an operation with the function arguments as inputs and the return value as
|
|
550
|
+
a single output.
|
|
551
|
+
|
|
552
|
+
When an active operation context is present, the edges created by this
|
|
553
|
+
decorator are attributed to that existing operation (the ``category`` and
|
|
554
|
+
``comment`` are applied to each edge only). When no operation context is
|
|
555
|
+
active, a new operation record is created and installed as the active
|
|
556
|
+
operation context for the duration of the function body, so that inner calls to
|
|
557
|
+
``comment_link`` can reference it.
|
|
558
|
+
|
|
559
|
+
Per-parameter options are configured via ``param_options``, a dictionary
|
|
560
|
+
mapping parameter names to option dictionaries. Unmentioned parameters
|
|
561
|
+
use the shared defaults supplied to this decorator. To configure the output,
|
|
562
|
+
you can pass the key ``"-o"`` together with its option dictionary.
|
|
563
|
+
|
|
564
|
+
Example::
|
|
565
|
+
|
|
566
|
+
@comment_fn(
|
|
567
|
+
category="extraction",
|
|
568
|
+
param_options={
|
|
569
|
+
"query": {"id_strategy": "content"},
|
|
570
|
+
"entries": {"auto_class_name": True},
|
|
571
|
+
},
|
|
572
|
+
)
|
|
573
|
+
def extract(query: str, entries: list) -> list:
|
|
574
|
+
...
|
|
575
|
+
|
|
576
|
+
@comment_fn(op_name="async_op")
|
|
577
|
+
async def fetch(url: str) -> str:
|
|
578
|
+
...
|
|
579
|
+
|
|
580
|
+
# Reusing an outer operation scope:
|
|
581
|
+
with comment_op_scope(op_name="pipeline") as op:
|
|
582
|
+
result = extract(query, entries)
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
op_name (`str | None`, optional):
|
|
586
|
+
Canonical operation name recorded in the graph. When not
|
|
587
|
+
provided, the decorated function's qualified name is used.
|
|
588
|
+
comment (`str | None`, optional):
|
|
589
|
+
Static operation description. When not provided, the decorated
|
|
590
|
+
function's docstring is used. If the function has no docstring,
|
|
591
|
+
its qualified name is used as a fallback.
|
|
592
|
+
category (`str`, defaults to `"operation"`):
|
|
593
|
+
Operation category. It is applied to the operation record when a new
|
|
594
|
+
one is created, and always applied to every edge.
|
|
595
|
+
id_strategy (`Callable[[Any], str] | str | None`, optional):
|
|
596
|
+
Shared identity strategy for auto-created input and output variables.
|
|
597
|
+
encoding_fn (`Callable[[Any], str] | None`, optional):
|
|
598
|
+
Shared encoder for auto-created variables.
|
|
599
|
+
decoding_fn (`Callable[[str], Any] | None`, optional):
|
|
600
|
+
Shared decoder for auto-created variables.
|
|
601
|
+
schema (`type[BaseModel] | None`, optional):
|
|
602
|
+
Shared Pydantic schema for auto encoding and decoding.
|
|
603
|
+
auto_class_name (`bool`, defaults to ``False``):
|
|
604
|
+
Auto-detect class name for auto-created variables.
|
|
605
|
+
op_metadata (`dict[str, JsonValue] | None`, optional):
|
|
606
|
+
Extra metadata attached to the operation record (when created)
|
|
607
|
+
and its edges.
|
|
608
|
+
metadata (`dict[str, JsonValue] | None`, optional):
|
|
609
|
+
Shared default metadata for auto-created variable nodes.
|
|
610
|
+
param_options (`dict[str, dict[str, Any]] | None`, optional):
|
|
611
|
+
Per-parameter option overrides. Keys are parameter names, values
|
|
612
|
+
are option dicts whose keys override the shared defaults for that
|
|
613
|
+
parameter only. If you want to configure the output, you can
|
|
614
|
+
pass the key ``"-o"`` together with its option dict, which is
|
|
615
|
+
applied to the output variable instead of an input parameter.
|
|
616
|
+
include_source (`bool`, defaults to ``False``):
|
|
617
|
+
When enabled, the decorated function's source code is captured
|
|
618
|
+
and attached as ``"source_code"`` in every edge's metadata.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
`Callable`:
|
|
622
|
+
The decorated function (sync or async, matching the original).
|
|
623
|
+
"""
|
|
624
|
+
opts_map = param_options or {}
|
|
625
|
+
|
|
626
|
+
def decorator(fn: Callable) -> Callable:
|
|
627
|
+
sig = inspect.signature(fn)
|
|
628
|
+
|
|
629
|
+
resolved_op_name = op_name if op_name is not None else fn.__qualname__
|
|
630
|
+
if comment is not None:
|
|
631
|
+
auto_comment = comment
|
|
632
|
+
else:
|
|
633
|
+
# If the user writes a docstring, we use it as the comment.
|
|
634
|
+
docstring = inspect.getdoc(fn)
|
|
635
|
+
if docstring:
|
|
636
|
+
auto_comment = f"[{fn.__qualname__}] {inspect.cleandoc(docstring)}"
|
|
637
|
+
else:
|
|
638
|
+
# Otherwise, we use the qualified name as the comment.
|
|
639
|
+
auto_comment = fn.__qualname__
|
|
640
|
+
|
|
641
|
+
fn_source = None
|
|
642
|
+
if include_source:
|
|
643
|
+
try:
|
|
644
|
+
fn_source = inspect.getsource(fn)
|
|
645
|
+
except Exception as e:
|
|
646
|
+
logger.warning(
|
|
647
|
+
"It is unable to get the source code for {}.\n"
|
|
648
|
+
"Below is the error information:\n\n"
|
|
649
|
+
" {}\n",
|
|
650
|
+
fn.__qualname__,
|
|
651
|
+
e,
|
|
652
|
+
)
|
|
653
|
+
fn_source = None
|
|
654
|
+
|
|
655
|
+
fn_filename = fn.__code__.co_filename
|
|
656
|
+
fn_lineno = fn.__code__.co_firstlineno
|
|
657
|
+
|
|
658
|
+
def _resolve_session_id(graph):
|
|
659
|
+
"""Return the active session identifier or fall back to the NONE sentinel."""
|
|
660
|
+
session = current_session()
|
|
661
|
+
if session is not None:
|
|
662
|
+
return session.session_id
|
|
663
|
+
graph._ensure_none_session()
|
|
664
|
+
return none_session_id()
|
|
665
|
+
|
|
666
|
+
def _setup_op(graph, session_id):
|
|
667
|
+
"""Create or reuse an operation.
|
|
668
|
+
|
|
669
|
+
When an active operation context is present, its operation is
|
|
670
|
+
reused. Otherwise a fresh operation record is created, persisted during
|
|
671
|
+
the function body.
|
|
672
|
+
"""
|
|
673
|
+
active_op = current_op()
|
|
674
|
+
if active_op is not None:
|
|
675
|
+
return active_op, None, True
|
|
676
|
+
|
|
677
|
+
fn_ctx = current_context()
|
|
678
|
+
assert fn_ctx is not None, (
|
|
679
|
+
"The current tracing context is not active."
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
op = OpRecord(
|
|
683
|
+
graph_id=graph.graph_id,
|
|
684
|
+
session_id=session_id,
|
|
685
|
+
user_id=graph.user_id,
|
|
686
|
+
project_id=graph.project_id,
|
|
687
|
+
op_name=resolved_op_name,
|
|
688
|
+
comment=auto_comment,
|
|
689
|
+
category=category,
|
|
690
|
+
filename=fn_filename,
|
|
691
|
+
lineno=fn_lineno,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
merged = {}
|
|
695
|
+
if op_metadata:
|
|
696
|
+
merged.update(op_metadata)
|
|
697
|
+
propagated = fn_ctx.metadata
|
|
698
|
+
if propagated:
|
|
699
|
+
merged.update(propagated)
|
|
700
|
+
if merged:
|
|
701
|
+
op.update_metadata(merged)
|
|
702
|
+
|
|
703
|
+
graph._driver.add_operation(op)
|
|
704
|
+
token = _OP.set(op)
|
|
705
|
+
return op, token, False
|
|
706
|
+
|
|
707
|
+
def _trace_edges(result, bound, op, graph, session_id):
|
|
708
|
+
"""Resolve input, output variables, and write edges."""
|
|
709
|
+
input_items = []
|
|
710
|
+
|
|
711
|
+
# We iterate over the arguments and create a list of input variables.
|
|
712
|
+
for name, value in bound.arguments.items():
|
|
713
|
+
per_param = opts_map.get(name)
|
|
714
|
+
if per_param is not None:
|
|
715
|
+
input_items.append((value, per_param))
|
|
716
|
+
else:
|
|
717
|
+
input_items.append(value)
|
|
718
|
+
|
|
719
|
+
# The reserved `"-o"` key configures the output variable.
|
|
720
|
+
output_items = []
|
|
721
|
+
if result is not None:
|
|
722
|
+
output_opts = opts_map.get("-o")
|
|
723
|
+
if output_opts is not None:
|
|
724
|
+
output_items.append((result, output_opts))
|
|
725
|
+
else:
|
|
726
|
+
output_items.append(result)
|
|
727
|
+
|
|
728
|
+
resolve_kwargs = dict(
|
|
729
|
+
shared_id_strategy=id_strategy,
|
|
730
|
+
shared_encoding_fn=encoding_fn,
|
|
731
|
+
shared_decoding_fn=decoding_fn,
|
|
732
|
+
shared_schema=schema,
|
|
733
|
+
shared_auto_class_name=auto_class_name,
|
|
734
|
+
shared_category="variable",
|
|
735
|
+
shared_metadata=metadata,
|
|
736
|
+
graph=graph,
|
|
737
|
+
session_id=session_id,
|
|
738
|
+
filename=fn_filename,
|
|
739
|
+
lineno=fn_lineno,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
input_rvs = [_resolve_item(item, **resolve_kwargs) for item in input_items]
|
|
743
|
+
output_rvs = [_resolve_item(item, **resolve_kwargs) for item in output_items]
|
|
744
|
+
|
|
745
|
+
if not input_rvs and not output_rvs:
|
|
746
|
+
logger.debug(
|
|
747
|
+
"No input or output variables for operation '{}'.",
|
|
748
|
+
resolved_op_name,
|
|
749
|
+
)
|
|
750
|
+
return
|
|
751
|
+
|
|
752
|
+
if not input_rvs or not output_rvs:
|
|
753
|
+
graph._ensure_none_node()
|
|
754
|
+
|
|
755
|
+
fn_ctx = current_context()
|
|
756
|
+
assert fn_ctx is not None, (
|
|
757
|
+
"The current tracing context is not active."
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
edge_meta = {}
|
|
761
|
+
if op_metadata:
|
|
762
|
+
edge_meta.update(op_metadata)
|
|
763
|
+
if fn_source is not None:
|
|
764
|
+
edge_meta["source_code"] = fn_source
|
|
765
|
+
propagated = fn_ctx.metadata
|
|
766
|
+
if propagated:
|
|
767
|
+
edge_meta.update(propagated)
|
|
768
|
+
|
|
769
|
+
_none = none_full_node_id()
|
|
770
|
+
source_ids = [rv.full_node_id for rv in input_rvs] if input_rvs else [_none]
|
|
771
|
+
target_ids = [rv.full_node_id for rv in output_rvs] if output_rvs else [_none]
|
|
772
|
+
|
|
773
|
+
for src in source_ids:
|
|
774
|
+
for tgt in target_ids:
|
|
775
|
+
edge = OpEdge(
|
|
776
|
+
graph_id=graph.graph_id,
|
|
777
|
+
session_id=session_id,
|
|
778
|
+
user_id=graph.user_id,
|
|
779
|
+
project_id=graph.project_id,
|
|
780
|
+
op_id=op.op_id,
|
|
781
|
+
category=category,
|
|
782
|
+
source_full_node_id=src,
|
|
783
|
+
target_full_node_id=tgt,
|
|
784
|
+
comment=auto_comment,
|
|
785
|
+
filename=fn_filename,
|
|
786
|
+
lineno=fn_lineno,
|
|
787
|
+
)
|
|
788
|
+
if edge_meta:
|
|
789
|
+
edge.update_metadata(edge_meta)
|
|
790
|
+
graph._driver.add_edge(edge)
|
|
791
|
+
|
|
792
|
+
if asyncio.iscoroutinefunction(fn):
|
|
793
|
+
@functools.wraps(fn)
|
|
794
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
795
|
+
if not is_tracing_enabled():
|
|
796
|
+
return await fn(*args, **kwargs)
|
|
797
|
+
graph = current_graph()
|
|
798
|
+
if graph is None:
|
|
799
|
+
return await fn(*args, **kwargs)
|
|
800
|
+
|
|
801
|
+
session_id = _resolve_session_id(graph)
|
|
802
|
+
op, token, _reused = _setup_op(graph, session_id)
|
|
803
|
+
try:
|
|
804
|
+
result = await fn(*args, **kwargs)
|
|
805
|
+
finally:
|
|
806
|
+
if token is not None:
|
|
807
|
+
_OP.reset(token)
|
|
808
|
+
|
|
809
|
+
bound = sig.bind(*args, **kwargs)
|
|
810
|
+
bound.apply_defaults()
|
|
811
|
+
_trace_edges(result, bound, op, graph, session_id)
|
|
812
|
+
return result
|
|
813
|
+
return async_wrapper
|
|
814
|
+
else:
|
|
815
|
+
@functools.wraps(fn)
|
|
816
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
817
|
+
if not is_tracing_enabled():
|
|
818
|
+
return fn(*args, **kwargs)
|
|
819
|
+
graph = current_graph()
|
|
820
|
+
if graph is None:
|
|
821
|
+
return fn(*args, **kwargs)
|
|
822
|
+
|
|
823
|
+
session_id = _resolve_session_id(graph)
|
|
824
|
+
op, token, _reused = _setup_op(graph, session_id)
|
|
825
|
+
try:
|
|
826
|
+
result = fn(*args, **kwargs)
|
|
827
|
+
finally:
|
|
828
|
+
if token is not None:
|
|
829
|
+
_OP.reset(token)
|
|
830
|
+
|
|
831
|
+
bound = sig.bind(*args, **kwargs)
|
|
832
|
+
bound.apply_defaults()
|
|
833
|
+
_trace_edges(result, bound, op, graph, session_id)
|
|
834
|
+
return result
|
|
835
|
+
return sync_wrapper
|
|
836
|
+
|
|
837
|
+
return decorator
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def comment_link(
|
|
841
|
+
*,
|
|
842
|
+
source: Any,
|
|
843
|
+
target: Any,
|
|
844
|
+
comment: str | None = None,
|
|
845
|
+
category: str | None = None,
|
|
846
|
+
id_strategy: Callable[[Any], str] | str | None = None,
|
|
847
|
+
encoding_fn: Callable[[Any], str] | None = None,
|
|
848
|
+
decoding_fn: Callable[[str], Any] | None = None,
|
|
849
|
+
schema: type[BaseModel] | None = None,
|
|
850
|
+
auto_class_name: bool = False,
|
|
851
|
+
metadata: dict[str, JsonValue] | None = None,
|
|
852
|
+
edge_metadata: dict[str, JsonValue] | None = None,
|
|
853
|
+
) -> RuntimeEdge | None:
|
|
854
|
+
"""Record a single directed edge between two variables.
|
|
855
|
+
|
|
856
|
+
Unlike ``comment_op`` which creates a Cartesian product of edges
|
|
857
|
+
between all inputs and all outputs, ``comment_link`` creates exactly
|
|
858
|
+
one edge from one source variable to one target variable. This allows
|
|
859
|
+
fine-grained control over graph topology within the same operation scope.
|
|
860
|
+
|
|
861
|
+
If an active operation context exists, the edge is associated with that
|
|
862
|
+
operation and inherits its category and comment as defaults.
|
|
863
|
+
When no operation context is active, a per-graph NONE sentinel operation
|
|
864
|
+
is lazily created.
|
|
865
|
+
|
|
866
|
+
Note that if you want to specify the category and comment for the variable,
|
|
867
|
+
you should pass these as options to the variable or use ``comment_variable``.
|
|
868
|
+
|
|
869
|
+
Example::
|
|
870
|
+
|
|
871
|
+
with comment_op_scope(op_name="custom_links") as op:
|
|
872
|
+
comment_link(source=query, target=result_a, category="primary")
|
|
873
|
+
comment_link(source=query, target=result_b, category="secondary")
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
source (`Any`):
|
|
877
|
+
The upstream variable. It can be a raw Python value, a runtime variable,
|
|
878
|
+
or a value-option tuple.
|
|
879
|
+
target (`Any`):
|
|
880
|
+
The downstream variable. It can be a raw Python value, a runtime variable,
|
|
881
|
+
or a value-option tuple.
|
|
882
|
+
comment (`str | None`, optional):
|
|
883
|
+
Edge annotation. When not provided, it falls back to the active
|
|
884
|
+
operation's comment as default.
|
|
885
|
+
category (`str | None`, optional):
|
|
886
|
+
Edge category. When not provided, it falls back to the active
|
|
887
|
+
operation's category as default, or `"link"` when no
|
|
888
|
+
operation context is active.
|
|
889
|
+
id_strategy (`Callable[[Any], str] | str | None`, optional):
|
|
890
|
+
Shared identity strategy for auto-created variable nodes.
|
|
891
|
+
encoding_fn (`Callable[[Any], str] | None`, optional):
|
|
892
|
+
Shared encoder for auto-created variables.
|
|
893
|
+
decoding_fn (`Callable[[str], Any] | None`, optional):
|
|
894
|
+
Shared decoder for auto-created variables.
|
|
895
|
+
schema (`type[BaseModel] | None`, optional):
|
|
896
|
+
Shared Pydantic schema for auto encoding and decoding.
|
|
897
|
+
auto_class_name (`bool`, defaults to `False`):
|
|
898
|
+
Auto-detect class name for auto-created variables.
|
|
899
|
+
metadata (`dict[str, JsonValue] | None`, optional):
|
|
900
|
+
Metadata used as the default for auto-created variable nodes.
|
|
901
|
+
edge_metadata (`dict[str, JsonValue] | None`, optional):
|
|
902
|
+
Extra metadata attached to the created edge.
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
`RuntimeEdge | None`:
|
|
906
|
+
A runtime view of the created edge, or ``None`` if tracing is
|
|
907
|
+
disabled.
|
|
908
|
+
"""
|
|
909
|
+
if not is_tracing_enabled():
|
|
910
|
+
return None
|
|
911
|
+
|
|
912
|
+
graph, session_id, filename, lineno = _resolve_call_context(
|
|
913
|
+
caller_name="comment_link",
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
resolve_kwargs = dict(
|
|
917
|
+
shared_id_strategy=id_strategy,
|
|
918
|
+
shared_encoding_fn=encoding_fn,
|
|
919
|
+
shared_decoding_fn=decoding_fn,
|
|
920
|
+
shared_schema=schema,
|
|
921
|
+
shared_auto_class_name=auto_class_name,
|
|
922
|
+
shared_category="variable",
|
|
923
|
+
shared_metadata=metadata,
|
|
924
|
+
graph=graph,
|
|
925
|
+
session_id=session_id,
|
|
926
|
+
filename=filename,
|
|
927
|
+
lineno=lineno,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
source_rv = _resolve_item(source, **resolve_kwargs)
|
|
931
|
+
target_rv = _resolve_item(target, **resolve_kwargs)
|
|
932
|
+
|
|
933
|
+
active_op = current_op()
|
|
934
|
+
if active_op is not None:
|
|
935
|
+
op_id = active_op.op_id
|
|
936
|
+
resolved_category = category if category is not None else active_op.category
|
|
937
|
+
resolved_comment = comment if comment is not None else active_op.comment
|
|
938
|
+
else:
|
|
939
|
+
graph._ensure_none_op()
|
|
940
|
+
op_id = none_op_id()
|
|
941
|
+
resolved_category = category if category is not None else "link"
|
|
942
|
+
resolved_comment = comment
|
|
943
|
+
|
|
944
|
+
ctx = current_context()
|
|
945
|
+
assert ctx is not None, "The current tracing context is not active."
|
|
946
|
+
|
|
947
|
+
merged_edge_metadata = {}
|
|
948
|
+
if edge_metadata:
|
|
949
|
+
merged_edge_metadata.update(edge_metadata)
|
|
950
|
+
propagated = ctx.metadata
|
|
951
|
+
if propagated:
|
|
952
|
+
merged_edge_metadata.update(propagated)
|
|
953
|
+
|
|
954
|
+
edge = OpEdge(
|
|
955
|
+
graph_id=graph.graph_id,
|
|
956
|
+
session_id=session_id,
|
|
957
|
+
user_id=graph.user_id,
|
|
958
|
+
project_id=graph.project_id,
|
|
959
|
+
op_id=op_id,
|
|
960
|
+
category=resolved_category,
|
|
961
|
+
source_full_node_id=source_rv.full_node_id,
|
|
962
|
+
target_full_node_id=target_rv.full_node_id,
|
|
963
|
+
comment=resolved_comment,
|
|
964
|
+
filename=filename,
|
|
965
|
+
lineno=lineno,
|
|
966
|
+
)
|
|
967
|
+
if merged_edge_metadata:
|
|
968
|
+
edge.update_metadata(merged_edge_metadata)
|
|
969
|
+
graph._driver.add_edge(edge)
|
|
970
|
+
|
|
971
|
+
return RuntimeEdge(edge=edge)
|