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.
@@ -0,0 +1,630 @@
1
+ """The tracing context and context manager functions for smartcomment."""
2
+
3
+ import re
4
+ import inspect
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar
7
+ from pydantic import JsonValue, PrivateAttr
8
+ from .network import ExecNetwork, none_session_id
9
+ from .operation import RuntimeOp
10
+ from .session import RuntimeSession
11
+ from .variable import RuntimeVariable
12
+ from ..schema.base import BaseMetadataModel
13
+ from ..schema.operation import OpRecord
14
+ from ..schema.session import Session
15
+ from ..logging import logger
16
+ from typing import (
17
+ Any,
18
+ Iterator,
19
+ Iterable,
20
+ )
21
+
22
+
23
+ # Module-level context variables (one per hierarchy level).
24
+ _GRAPH: ContextVar[ExecNetwork | None] = ContextVar("_GRAPH", default=None)
25
+ _SESSION: ContextVar[Session | None] = ContextVar("_SESSION", default=None)
26
+ _OP: ContextVar[OpRecord | None] = ContextVar("_OP", default=None)
27
+
28
+ # Global toggle for tracing.
29
+ _TRACING_ENABLED: bool = True
30
+
31
+
32
+ class TracingContext(BaseMetadataModel):
33
+ """Per-scope context that tracks runtime variables and propagates metadata.
34
+
35
+ A tracing context is stored in the context variable and
36
+ provides read-access to the current graph, session, and operation
37
+ via properties that delegate to the module-level context variables.
38
+ """
39
+
40
+ _runtime_variables: dict[str, RuntimeVariable[Any]] = PrivateAttr(default_factory=dict)
41
+ _full_node_id_to_names: dict[str, set[str]] = PrivateAttr(default_factory=dict)
42
+
43
+ def register_variable(
44
+ self,
45
+ name: str,
46
+ rv: RuntimeVariable[Any],
47
+ overwrite: bool = False,
48
+ ) -> None:
49
+ """Register a runtime variable by user-chosen name.
50
+
51
+ Args:
52
+ name (`str`):
53
+ Semantic name for the variable (e.g. a local variable name).
54
+ rv (`RuntimeVariable[Any]`):
55
+ The runtime handle.
56
+ overwrite (`bool`, defaults to `False`):
57
+ If it is not enabled, an error will be raised when the given name
58
+ is already registered. Otherwise, it will overwrite the existing variable.
59
+ """
60
+ if name in self._runtime_variables:
61
+ if not overwrite:
62
+ raise ValueError(
63
+ f"Variable name '{name}' is already registered in this context. "
64
+ f"Use `overwrite=True` to replace it."
65
+ )
66
+
67
+ logger.debug(
68
+ "A variable with name '{name}' is already registered in this context. "
69
+ "It will be overwritten.",
70
+ name=name,
71
+ )
72
+ # Detach the name from the previous runtime variable's alias set.
73
+ # This keeps the reverse index consistent when the old and new
74
+ # handles point to different full node identifiers.
75
+ old_rv = self._runtime_variables[name]
76
+ old_names = self._full_node_id_to_names[old_rv.full_node_id]
77
+ old_names.discard(name)
78
+ if not old_names:
79
+ self._full_node_id_to_names.pop(old_rv.full_node_id)
80
+
81
+ self._runtime_variables[name] = rv
82
+ self._full_node_id_to_names.setdefault(rv.full_node_id, set()).add(name)
83
+
84
+ def get_variable(self, name: str) -> RuntimeVariable[Any]:
85
+ """Look up a runtime variable by its registered name.
86
+
87
+ Args:
88
+ name (`str`):
89
+ The name used during registration.
90
+
91
+ Returns:
92
+ `RuntimeVariable`:
93
+ The runtime handle.
94
+
95
+ Raises:
96
+ `NameError`:
97
+ If the variable with the given name has not been declared in
98
+ this tracing context.
99
+ """
100
+ try:
101
+ return self._runtime_variables[name]
102
+ except KeyError:
103
+ raise NameError(
104
+ f"Variable name '{name}' is not declared in this tracing context."
105
+ ) from None
106
+
107
+ def get_variable_by_full_node_id(self, full_node_id: str) -> RuntimeVariable[Any]:
108
+ """Look up a runtime variable by its full (namespaced) node identifier.
109
+
110
+ Args:
111
+ full_node_id (`str`):
112
+ The full node identifier of the variable.
113
+
114
+ Returns:
115
+ `RuntimeVariable`:
116
+ The runtime handle.
117
+
118
+ Raises:
119
+ `NameError`:
120
+ If no variable with the given full node identifier is found.
121
+ """
122
+ names = self._full_node_id_to_names.get(full_node_id)
123
+ if names is None:
124
+ raise NameError(
125
+ f"No variable with full node identifier '{full_node_id}' is "
126
+ "registered in this tracing context."
127
+ )
128
+ return self._runtime_variables[next(iter(names))]
129
+
130
+ def get_names_by_full_node_id(self, full_node_id: str) -> set[str]:
131
+ """Return all names registered for the given full node identifier.
132
+
133
+ Args:
134
+ full_node_id (`str`):
135
+ The full node identifier of the variable.
136
+
137
+ Returns:
138
+ `set[str]`:
139
+ A fresh copy of the set of registered names. The returned set
140
+ is empty when no variable with this identifier is registered.
141
+ """
142
+ names = self._full_node_id_to_names.get(full_node_id)
143
+ if names is None:
144
+ return set()
145
+ return set(names)
146
+
147
+ def remove_variable(self, name: str) -> RuntimeVariable[Any]:
148
+ """Remove a runtime variable by its registered name.
149
+
150
+ The variable is removed from the tracing context only. It is not
151
+ deleted from the underlying execution graph or driver.
152
+
153
+ Args:
154
+ name (`str`):
155
+ The name used during registration.
156
+
157
+ Returns:
158
+ `RuntimeVariable[Any]`:
159
+ The removed runtime variable handle.
160
+
161
+ Raises:
162
+ `NameError`:
163
+ If no variable with the given name is registered in this
164
+ tracing context.
165
+ """
166
+ try:
167
+ rv = self._runtime_variables.pop(name)
168
+ except KeyError:
169
+ raise NameError(
170
+ f"Variable name '{name}' is not declared in this tracing context."
171
+ ) from None
172
+
173
+ names = self._full_node_id_to_names.get(rv.full_node_id)
174
+ if names is not None:
175
+ names.discard(name)
176
+ if not names:
177
+ self._full_node_id_to_names.pop(rv.full_node_id)
178
+ return rv
179
+
180
+ def query_variables(
181
+ self,
182
+ *,
183
+ name_pattern: str | None = None,
184
+ category: str | Iterable[str] | None = None,
185
+ class_name: str | None = None,
186
+ ) -> list[RuntimeVariable[Any]]:
187
+ """Query registered runtime variables by public traits.
188
+
189
+ All supplied filters are combined with logical AND. When no filters
190
+ are given, all registered variables are returned.
191
+
192
+ Args:
193
+ name_pattern (`str | None`, optional):
194
+ Regular expression matched against the registered variable
195
+ name. Plain keywords are treated as literal substrings.
196
+ category (`str | Iterable[str] | None`, optional):
197
+ A single category string or a set of category strings.
198
+ A variable matches if its category is contained in the set.
199
+ class_name (`str | None`, optional):
200
+ Exact class name to match.
201
+
202
+ Returns:
203
+ `list[RuntimeVariable[Any]]`:
204
+ Variables satisfying all filters, ordered by registration
205
+ insertion order.
206
+ """
207
+ compiled = re.compile(name_pattern) if name_pattern is not None else None
208
+ cat_set = None
209
+ if isinstance(category, str):
210
+ cat_set = {category}
211
+ elif category is not None:
212
+ cat_set = set(category)
213
+
214
+ results = []
215
+ for registered_name, rv in self._runtime_variables.items():
216
+ if compiled is not None and not compiled.search(registered_name):
217
+ continue
218
+ if cat_set is not None and rv.category not in cat_set:
219
+ continue
220
+ if class_name is not None and rv.class_name != class_name:
221
+ continue
222
+ results.append(rv)
223
+ return results
224
+
225
+ @property
226
+ def graph(self) -> ExecNetwork | None:
227
+ """Get the execution graph from the current context."""
228
+ return _GRAPH.get()
229
+
230
+ @property
231
+ def session(self) -> Session | None:
232
+ """Get the current session from the current context."""
233
+ return _SESSION.get()
234
+
235
+ @property
236
+ def op(self) -> OpRecord | None:
237
+ """Get the current operation from the current context."""
238
+ return _OP.get()
239
+
240
+
241
+ # The current tracing context.
242
+ _TRACING_CTX: ContextVar[TracingContext | None] = ContextVar("_TRACING_CTX", default=None)
243
+
244
+
245
+ def enable_tracing() -> None:
246
+ """Enable the global tracing toggle."""
247
+ global _TRACING_ENABLED
248
+ _TRACING_ENABLED = True
249
+
250
+ logger.info("Tracing is enabled.")
251
+
252
+
253
+ def disable_tracing() -> None:
254
+ """Disable the global tracing toggle.
255
+ All `comment_*` calls become no-ops and pure comments."""
256
+ global _TRACING_ENABLED
257
+ _TRACING_ENABLED = False
258
+
259
+ logger.info("Tracing is disabled.")
260
+
261
+
262
+ def is_tracing_enabled() -> bool:
263
+ """Return whether tracing is currently enabled."""
264
+ return _TRACING_ENABLED
265
+
266
+
267
+ def current_graph() -> ExecNetwork | None:
268
+ """Return the active execution graph, if any."""
269
+ return _GRAPH.get()
270
+
271
+
272
+ def current_session() -> Session | None:
273
+ """Return the active session, if any."""
274
+ return _SESSION.get()
275
+
276
+
277
+ def current_op() -> OpRecord | None:
278
+ """Return the active operation record, if any."""
279
+ return _OP.get()
280
+
281
+
282
+ def current_context() -> TracingContext | None:
283
+ """Return the active tracing context, if any."""
284
+ return _TRACING_CTX.get()
285
+
286
+
287
+ @contextmanager
288
+ def comment_graph(
289
+ *,
290
+ graph_id: str | None = None,
291
+ user_id: str | None = None,
292
+ project_id: str | None = None,
293
+ storage_path: str | None = None,
294
+ driver_type: str | None = None,
295
+ graph: ExecNetwork | None = None,
296
+ metadata: dict[str, JsonValue] | None = None,
297
+ strict: bool | None = None,
298
+ ) -> Iterator[ExecNetwork | None]:
299
+ """Create or adopt an execution network and set it as the active graph.
300
+
301
+ Args:
302
+ graph_id (`str | None`, optional):
303
+ Explicit graph identifier. It is automatically generated if omitted.
304
+ user_id (`str | None`, optional):
305
+ User identifier that owns the graph.
306
+ project_id (`str | None`, optional):
307
+ Project identifier that owns the graph.
308
+ storage_path (`str | None`, optional):
309
+ On-disk path for persistent storage.
310
+ driver_type (`str | None`, optional):
311
+ Registered driver backend name. It defaults to ``"in_memory"``
312
+ when creating a new graph.
313
+ graph (`ExecNetwork | None`, optional):
314
+ An existing graph to continue.
315
+ metadata (`dict[str, JsonValue] | None`, optional):
316
+ Extra metadata merged into the graph.
317
+ strict (`bool | None`, optional):
318
+ Enable or disable strict consistency checking for this
319
+ graph. When it is enabled, encountering a changed value for
320
+ an existing identity without an explicit ``comment_mutation`` raises
321
+ a trace consistency error. It defaults to ``False``. When an existing
322
+ graph is passed in, this parameter overrides its current setting
323
+ only if explicitly provided.
324
+
325
+ Yields:
326
+ `ExecNetwork | None`:
327
+ The active graph if tracing is enabled.
328
+ """
329
+ if not _TRACING_ENABLED:
330
+ yield None
331
+ return
332
+
333
+ created = False
334
+ if graph is None:
335
+ kwargs = {}
336
+ if graph_id is not None:
337
+ kwargs["graph_id"] = graph_id
338
+ if user_id is not None:
339
+ kwargs["user_id"] = user_id
340
+ if project_id is not None:
341
+ kwargs["project_id"] = project_id
342
+ if storage_path is not None:
343
+ kwargs["storage_path"] = storage_path
344
+ if driver_type is not None:
345
+ kwargs["driver_type"] = driver_type
346
+ if strict is not None:
347
+ kwargs["strict"] = strict
348
+ graph = ExecNetwork(**kwargs)
349
+ created = True
350
+ elif strict is not None:
351
+ graph.strict = strict
352
+
353
+ if metadata:
354
+ graph.update_metadata(metadata)
355
+
356
+ # Set up tracing context and inherit propagated attributes.
357
+ ctx = TracingContext()
358
+ parent_ctx = _TRACING_CTX.get()
359
+ if parent_ctx is not None:
360
+ ctx._runtime_variables = parent_ctx._runtime_variables.copy()
361
+ ctx._full_node_id_to_names = {
362
+ fid: names.copy()
363
+ for fid, names in parent_ctx._full_node_id_to_names.items()
364
+ }
365
+ ctx.update_metadata(parent_ctx.metadata)
366
+ # Inject propagated attributes onto the graph schema object.
367
+ if parent_ctx.metadata:
368
+ graph.update_metadata(parent_ctx.metadata)
369
+
370
+ if created:
371
+ logger.info(
372
+ "An execution graph (graph_id={}, driver_type={}, strict={}) is created.",
373
+ graph.graph_id,
374
+ graph.driver_type,
375
+ graph.strict,
376
+ )
377
+ else:
378
+ logger.debug(
379
+ "An existing execution graph (graph_id={}) is adopted.",
380
+ graph.graph_id,
381
+ )
382
+
383
+ token_graph = _GRAPH.set(graph)
384
+ token_ctx = _TRACING_CTX.set(ctx)
385
+ try:
386
+ yield graph
387
+ finally:
388
+ _GRAPH.reset(token_graph)
389
+ _TRACING_CTX.reset(token_ctx)
390
+
391
+
392
+ @contextmanager
393
+ def comment_session(
394
+ *,
395
+ session_id: str | None = None,
396
+ session_name: str | None = None,
397
+ comment: str | None = None,
398
+ category: str = "session",
399
+ metadata: dict[str, JsonValue] | None = None,
400
+ ) -> Iterator[RuntimeSession | None]:
401
+ """Create a session and set it as the active session.
402
+
403
+ Args:
404
+ session_id (`str | None`, optional):
405
+ Explicit session identifier.
406
+ session_name (`str | None`, optional):
407
+ Human-readable session name.
408
+ comment (`str | None`, optional):
409
+ Session description.
410
+ category (`str`, defaults to `"session"`):
411
+ Session category.
412
+ metadata (`dict[str, JsonValue] | None`, optional):
413
+ Extra metadata for the session schema object.
414
+
415
+ Yields:
416
+ `RuntimeSession | None`:
417
+ A read-only session handle if tracing is enabled.
418
+
419
+ Raises:
420
+ `RuntimeError`:
421
+ If no graph context is active and tracing is enabled.
422
+ `ValueError`:
423
+ If provided session identifier is the reserved NONE sentinel session identifier.
424
+ """
425
+ if not _TRACING_ENABLED:
426
+ yield None
427
+ return
428
+
429
+ graph = _GRAPH.get()
430
+ if graph is None:
431
+ raise RuntimeError(
432
+ "`comment_session` requires an active graph context. "
433
+ "Wrap your code in `with comment_graph() as graph:`."
434
+ )
435
+ none_sid = none_session_id()
436
+ if session_id == none_sid:
437
+ raise ValueError(
438
+ f"The session identifier '{none_sid}' is reserved for the built-in "
439
+ "NONE sentinel."
440
+ )
441
+
442
+ frame = inspect.stack()[2]
443
+ kwargs = {
444
+ "graph_id": graph.graph_id,
445
+ "user_id": graph.user_id,
446
+ "project_id": graph.project_id,
447
+ "category": category,
448
+ "filename": frame.filename,
449
+ "lineno": frame.lineno,
450
+ }
451
+ if session_id is not None:
452
+ kwargs["session_id"] = session_id
453
+ if session_name is not None:
454
+ kwargs["session_name"] = session_name
455
+ if comment is not None:
456
+ kwargs["comment"] = comment
457
+
458
+ session = Session(**kwargs)
459
+ if metadata:
460
+ session.update_metadata(metadata)
461
+
462
+ # Inject propagated attributes onto the session schema.
463
+ parent_ctx = _TRACING_CTX.get()
464
+ assert parent_ctx is not None, (
465
+ "The tracing context must be active when `comment_session` is called."
466
+ )
467
+ propagated = parent_ctx.metadata
468
+ if propagated:
469
+ session.update_metadata(propagated)
470
+
471
+ # Persist the session in the graph driver.
472
+ graph._driver.add_session(session)
473
+
474
+ # Set up tracing context and inherit propagated attributes.
475
+ ctx = TracingContext()
476
+ ctx._runtime_variables = parent_ctx._runtime_variables.copy()
477
+ ctx._full_node_id_to_names = {
478
+ fid: names.copy()
479
+ for fid, names in parent_ctx._full_node_id_to_names.items()
480
+ }
481
+ ctx.update_metadata(parent_ctx.metadata)
482
+
483
+ token_session = _SESSION.set(session)
484
+ token_ctx = _TRACING_CTX.set(ctx)
485
+ try:
486
+ yield RuntimeSession(session=session)
487
+ finally:
488
+ _SESSION.reset(token_session)
489
+ _TRACING_CTX.reset(token_ctx)
490
+
491
+
492
+ @contextmanager
493
+ def comment_op_scope(
494
+ *,
495
+ op_name: str | None = None,
496
+ comment: str | None = None,
497
+ category: str = "operation",
498
+ metadata: dict[str, JsonValue] | None = None,
499
+ ) -> Iterator[RuntimeOp | None]:
500
+ """Create an operation record scope and set it as the active operation.
501
+
502
+ If no session is active, an anonymous session is created automatically
503
+ for the duration of this scope.
504
+
505
+ Args:
506
+ op_name (`str | None`, optional):
507
+ Operation name.
508
+ comment (`str | None`, optional):
509
+ Operation description.
510
+ category (`str`, defaults to `"operation"`):
511
+ Operation category.
512
+ metadata (`dict[str, JsonValue] | None`, optional):
513
+ Extra metadata.
514
+
515
+ Yields:
516
+ `RuntimeOp | None`:
517
+ A read-only operation handle if tracing is enabled.
518
+
519
+ Raises:
520
+ `RuntimeError`:
521
+ If no graph context is active and tracing is enabled.
522
+ """
523
+ if not _TRACING_ENABLED:
524
+ yield None
525
+ return
526
+
527
+ graph = _GRAPH.get()
528
+ if graph is None:
529
+ raise RuntimeError(
530
+ "`comment_op_scope` requires an active graph context. "
531
+ "Wrap your code in `with comment_graph() as graph:`."
532
+ )
533
+
534
+ tracing_ctx = _TRACING_CTX.get()
535
+ assert tracing_ctx is not None, (
536
+ "The tracing context must be active when `comment_op_scope` is called."
537
+ )
538
+
539
+ session = _SESSION.get()
540
+ auto_session_token = None
541
+ if session is None:
542
+ frame_session = inspect.stack()[2]
543
+ session = Session(
544
+ graph_id=graph.graph_id,
545
+ user_id=graph.user_id,
546
+ project_id=graph.project_id,
547
+ category="session",
548
+ filename=frame_session.filename,
549
+ lineno=frame_session.lineno,
550
+ )
551
+ if tracing_ctx.metadata:
552
+ session.update_metadata(tracing_ctx.metadata)
553
+ graph._driver.add_session(session)
554
+ auto_session_token = _SESSION.set(session)
555
+
556
+ frame = inspect.stack()[2]
557
+ op = OpRecord(
558
+ graph_id=graph.graph_id,
559
+ session_id=session.session_id,
560
+ user_id=graph.user_id,
561
+ project_id=graph.project_id,
562
+ op_name=op_name,
563
+ comment=comment,
564
+ category=category,
565
+ filename=frame.filename,
566
+ lineno=frame.lineno,
567
+ )
568
+ if metadata:
569
+ op.update_metadata(metadata)
570
+
571
+ propagated = tracing_ctx.metadata
572
+ if propagated:
573
+ op.update_metadata(propagated)
574
+
575
+ # Persist the operation in the graph driver.
576
+ graph._driver.add_operation(op)
577
+
578
+ token_op = _OP.set(op)
579
+ try:
580
+ yield RuntimeOp(op=op)
581
+ finally:
582
+ _OP.reset(token_op)
583
+ if auto_session_token is not None:
584
+ _SESSION.reset(auto_session_token)
585
+
586
+
587
+ @contextmanager
588
+ def propagate_attributes(
589
+ **attrs: Any,
590
+ ) -> Iterator[None]:
591
+ """Add key-value attributes to the tracing context for propagation.
592
+
593
+ Attributes set here flow through the context inheritance chain and
594
+ are automatically injected into every execution network, session,
595
+ operation record, and variable created in nested scopes.
596
+
597
+ Example::
598
+
599
+ with propagate_attributes(run_id="exp-1", model="gpt-4"):
600
+ with comment_graph() as graph:
601
+ # `graph.metadata` contains `run_id` and `model`.
602
+ with comment_session(session_name="s1"):
603
+ rv = comment_variable("hello", id_strategy="content")
604
+ # `rv.metadata` also contains `run_id` and `model`.
605
+
606
+ Args:
607
+ **attrs (`Any`):
608
+ Arbitrary key-value pairs to propagate.
609
+ """
610
+ if not _TRACING_ENABLED:
611
+ yield
612
+ return
613
+
614
+ parent_ctx = _TRACING_CTX.get()
615
+
616
+ ctx = TracingContext()
617
+ if parent_ctx is not None:
618
+ ctx._runtime_variables = parent_ctx._runtime_variables.copy()
619
+ ctx._full_node_id_to_names = {
620
+ fid: names.copy()
621
+ for fid, names in parent_ctx._full_node_id_to_names.items()
622
+ }
623
+ ctx.update_metadata(parent_ctx.metadata)
624
+ ctx.update_metadata(attrs)
625
+
626
+ token = _TRACING_CTX.set(ctx)
627
+ try:
628
+ yield
629
+ finally:
630
+ _TRACING_CTX.reset(token)