ai-pipeline-core 0.4.7__py3-none-any.whl → 0.4.8__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.
@@ -64,7 +64,7 @@ from .prompt_manager import PromptManager
64
64
  from .settings import Settings
65
65
  from .testing import disable_run_logger, prefect_test_harness
66
66
 
67
- __version__ = "0.4.6"
67
+ __version__ = "0.4.8"
68
68
 
69
69
  __all__ = [
70
70
  "AIMessageType",
@@ -661,7 +661,10 @@ class PipelineDeployment(Generic[TOptions, TResult]):
661
661
  except Exception as e:
662
662
  logger.warning(f"Failed to initialize observability: {e}")
663
663
  with contextlib.suppress(Exception):
664
- Laminar.initialize(export_timeout_seconds=15)
664
+ # Use canonical initializer to ensure consistent Laminar setup
665
+ from ai_pipeline_core.observability import tracing
666
+
667
+ tracing._initialise_laminar()
665
668
 
666
669
  deployment = self
667
670
 
@@ -882,13 +885,34 @@ class PipelineDeployment(Generic[TOptions, TResult]):
882
885
  options: FlowOptions,
883
886
  context: DeploymentContext,
884
887
  ) -> DeploymentResult:
888
+ # Initialize observability for remote workers
889
+ try:
890
+ initialize_observability()
891
+ except Exception as e:
892
+ logger.warning(f"Failed to initialize observability: {e}")
893
+ with contextlib.suppress(Exception):
894
+ # Use canonical initializer to ensure consistent Laminar setup
895
+ from ai_pipeline_core.observability import tracing
896
+
897
+ tracing._initialise_laminar()
898
+
899
+ # Set session ID from Prefect flow run for trace grouping
900
+ flow_run_id = str(runtime.flow_run.get_id()) if runtime.flow_run else str(uuid4()) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownArgumentType]
901
+ os.environ["LMNR_SESSION_ID"] = flow_run_id
902
+
885
903
  store = create_document_store(
886
904
  settings,
887
905
  summary_generator=_build_summary_generator(),
888
906
  )
889
907
  set_document_store(store)
890
908
  try:
891
- return await deployment.run(project_name, documents, cast(Any, options), context)
909
+ # Create parent span to group all traces under a single deployment trace
910
+ with Laminar.start_as_current_span(
911
+ name=f"{deployment.name}-{project_name}",
912
+ input={"project_name": project_name, "options": options.model_dump()},
913
+ session_id=flow_run_id,
914
+ ):
915
+ return await deployment.run(project_name, documents, cast(Any, options), context)
892
916
  finally:
893
917
  store.shutdown()
894
918
  set_document_store(None)
@@ -8,7 +8,6 @@ import importlib
8
8
  from typing import Any, Protocol
9
9
  from uuid import UUID
10
10
 
11
- from lmnr import Laminar
12
11
  from opentelemetry import trace as otel_trace
13
12
  from pydantic import BaseModel, ConfigDict
14
13
 
@@ -180,10 +179,12 @@ def initialize_observability(config: ObservabilityConfig | None = None) -> None:
180
179
  if config is None:
181
180
  config = _build_config_from_settings()
182
181
 
183
- # 1. Laminar
182
+ # 1. Laminar - use canonical initializer from tracing module
184
183
  if config.has_lmnr:
185
184
  try:
186
- Laminar.initialize(project_api_key=config.lmnr_project_api_key, export_timeout_seconds=15)
185
+ from ai_pipeline_core.observability import tracing # noqa: PLC0415
186
+
187
+ tracing._initialise_laminar()
187
188
  logger.info("Laminar initialized")
188
189
  except Exception as e:
189
190
  logger.warning(f"Laminar initialization failed: {e}")
@@ -10,6 +10,7 @@ import contextlib
10
10
  import inspect
11
11
  import json
12
12
  import os
13
+ import threading
13
14
  from collections.abc import Callable
14
15
  from functools import wraps
15
16
  from typing import Any, Literal, ParamSpec, TypeVar, cast, overload
@@ -220,19 +221,42 @@ class TraceInfo(BaseModel):
220
221
  # ---------------------------------------------------------------------------
221
222
 
222
223
 
224
+ _laminar_initialized = False
225
+ _laminar_init_lock = threading.Lock()
226
+
227
+
223
228
  def _initialise_laminar() -> None:
224
- """Initialize Laminar SDK with project configuration.
229
+ """Initialize Laminar SDK with project configuration (lazy, once per process).
225
230
 
226
231
  Sets up the Laminar observability client with the project API key
227
232
  from settings. Disables automatic OpenAI instrumentation to avoid
228
233
  conflicts with our custom tracing.
229
234
 
230
- Called once per process. Multiple calls are safe (Laminar handles idempotency).
235
+ IMPORTANT: This is called lazily at first trace execution (not at decoration time)
236
+ to allow LMNR_SPAN_CONTEXT environment variable to be set before initialization.
237
+ Laminar reads LMNR_SPAN_CONTEXT during initialize() to establish parent context
238
+ for cross-process tracing.
239
+
240
+ Uses double-checked locking pattern for thread safety. The flag is set AFTER
241
+ successful initialization to prevent permanently disabled tracing on init failure.
231
242
  """
232
- if settings.lmnr_project_api_key:
233
- Laminar.initialize(
234
- project_api_key=settings.lmnr_project_api_key, disabled_instruments=[Instruments.OPENAI] if Instruments.OPENAI else [], export_timeout_seconds=15
235
- )
243
+ global _laminar_initialized # noqa: PLW0603
244
+
245
+ # Fast path: already initialized (no lock needed)
246
+ if _laminar_initialized:
247
+ return
248
+
249
+ with _laminar_init_lock:
250
+ # Double-check inside lock
251
+ if _laminar_initialized:
252
+ return
253
+
254
+ if settings.lmnr_project_api_key:
255
+ disabled = [Instruments.OPENAI] if Instruments.OPENAI else []
256
+ Laminar.initialize(project_api_key=settings.lmnr_project_api_key, disabled_instruments=disabled, export_timeout_seconds=15)
257
+
258
+ # Set flag AFTER successful initialization
259
+ _laminar_initialized = True
236
260
 
237
261
 
238
262
  # Overload for calls like @trace(name="...", level="debug")
@@ -400,7 +424,9 @@ def trace( # noqa: UP047
400
424
  return f
401
425
 
402
426
  # --- Pre-computation (done once when the function is decorated) ---
403
- _initialise_laminar()
427
+ # NOTE: _initialise_laminar() is NOT called here (at decoration/import time)
428
+ # to allow LMNR_SPAN_CONTEXT to be set before Laminar.initialize() runs.
429
+ # It's called lazily in the wrapper functions at first execution.
404
430
  sig = inspect.signature(f)
405
431
  is_coroutine = inspect.iscoroutinefunction(f)
406
432
  observe_name = name or f.__name__
@@ -550,6 +576,9 @@ def trace( # noqa: UP047
550
576
  Returns:
551
577
  The result of the wrapped function.
552
578
  """
579
+ # Lazy initialization: called at first execution, not at decoration time.
580
+ # This allows LMNR_SPAN_CONTEXT to be set before Laminar.initialize().
581
+ _initialise_laminar()
553
582
  observe_params = _prepare_and_get_observe_params(kwargs)
554
583
  observed_func = bound_observe(**observe_params)(f)
555
584
  return observed_func(*args, **kwargs)
@@ -561,6 +590,9 @@ def trace( # noqa: UP047
561
590
  Returns:
562
591
  The result of the wrapped function.
563
592
  """
593
+ # Lazy initialization: called at first execution, not at decoration time.
594
+ # This allows LMNR_SPAN_CONTEXT to be set before Laminar.initialize().
595
+ _initialise_laminar()
564
596
  observe_params = _prepare_and_get_observe_params(kwargs)
565
597
  observed_func = bound_observe(**observe_params)(f)
566
598
  return await observed_func(*args, **kwargs) # pyright: ignore[reportGeneralTypeIssues, reportUnknownVariableType]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-pipeline-core
3
- Version: 0.4.7
3
+ Version: 0.4.8
4
4
  Summary: Core utilities for AI-powered processing pipelines using prefect
5
5
  Project-URL: Homepage, https://github.com/bbarwik/ai-pipeline-core
6
6
  Project-URL: Repository, https://github.com/bbarwik/ai-pipeline-core
@@ -1,11 +1,11 @@
1
- ai_pipeline_core/__init__.py,sha256=QAQSyrKafsNov4dy9vNqTVarh2nDdrthMfYh7X-3Mcg,3270
1
+ ai_pipeline_core/__init__.py,sha256=aJwyMqt4ESan14iAS9guaHbDRk1F97PbOeHBvxShhD4,3270
2
2
  ai_pipeline_core/exceptions.py,sha256=csAl7vq6xjSFBF8-UM9WZODCbhsOdOG5zH6IbA8iteM,1280
3
3
  ai_pipeline_core/prompt_manager.py,sha256=3wFkL5rrjtUT1cLInkgyhS8hKnO4MeD1cdXAEuLhgoE,9459
4
4
  ai_pipeline_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  ai_pipeline_core/settings.py,sha256=BUz8JEFfJQrdE4rNOhQWwxnTrfekLjWkoy-3wDZQ7PY,5142
6
6
  ai_pipeline_core/testing.py,sha256=jIRrLxNvTwdamucfJoHET2qMeRhhMZV9uEJXO5vAfis,279
7
7
  ai_pipeline_core/deployment/__init__.py,sha256=wTkVK6gcEQvqBajFMTAuodRONpN25yHbR1jtcumf0WQ,900
8
- ai_pipeline_core/deployment/base.py,sha256=bGSnDdrw6cLM_TItAiwptnwApbw5wkoIGY9pnwDvOTQ,37485
8
+ ai_pipeline_core/deployment/base.py,sha256=JhaEYok3USOOGm9VrqayZTQD-i_VyOch7Gdv-jQfqsQ,38785
9
9
  ai_pipeline_core/deployment/contract.py,sha256=a1qbHhneTGB27oSOUy79CUIhOIzOoq37M63XoIMzA4Y,1952
10
10
  ai_pipeline_core/deployment/deploy.py,sha256=y5FxKMm7nGwkjzA74pTffO7A82MaDuajx6LHGTem8bI,24662
11
11
  ai_pipeline_core/deployment/helpers.py,sha256=yVtGFUs4AFXkpLkiQ_ale0nXXt5btfWSb5PAbikQHNs,3312
@@ -48,10 +48,10 @@ ai_pipeline_core/logging/logging_config.py,sha256=JnTarGSSkpi7eqR7N13TLKeuwNCvZg
48
48
  ai_pipeline_core/logging/logging_mixin.py,sha256=Jn3x0xvSwSjbAMfWELMOEfffWBB1u4IeIr7M2-55CJs,7191
49
49
  ai_pipeline_core/observability/__init__.py,sha256=km2nIiY3aYH13s2m4nR91erQG4qKnGuvQkrKDdVW3bw,720
50
50
  ai_pipeline_core/observability/_document_tracking.py,sha256=tXv6rbGIuxOYdq22aVbyn9Ve5EhYHPnrYCE-kj2NGXI,5428
51
- ai_pipeline_core/observability/_initialization.py,sha256=GfwRHpg90Og3PzmG1ZUilJVXoFx9BIWpbMgXxJ5Alqk,6747
51
+ ai_pipeline_core/observability/_initialization.py,sha256=kg5erwWLyKi-kWl21A8jXZTod05LtP-vP0VOd2zhAzo,6790
52
52
  ai_pipeline_core/observability/_logging_bridge.py,sha256=T3PpkgoI0YKN2vvBJEHzR5rFMFNHq9REHJs7PQX2VQk,2053
53
53
  ai_pipeline_core/observability/_summary.py,sha256=GAZXzXVkwUcubSiGb5DgkHfO1gGwx6pYoDz6RUJmL5k,3390
54
- ai_pipeline_core/observability/tracing.py,sha256=KhIXSl5fe39UE1Eokz9-1fe5biX6anKbwZDmXY_Z2LU,27050
54
+ ai_pipeline_core/observability/tracing.py,sha256=RZ702N392_nP4HZvxVWv6x3c6hDBZzkdNOtqPTqmFMs,28521
55
55
  ai_pipeline_core/observability/_debug/__init__.py,sha256=V8pbgdQOx-7oFKQ_sNzAZ1-oq5c73P4kVjEClZDXe8k,942
56
56
  ai_pipeline_core/observability/_debug/_auto_summary.py,sha256=LMvETvx_RPKF8srewCKwjigTiWs3KfDmQAYYSuVybIM,2687
57
57
  ai_pipeline_core/observability/_debug/_config.py,sha256=CWfnK-F3knUuOQ34y_CjmU3l67J85NIZ3siftYhevc0,3367
@@ -70,7 +70,7 @@ ai_pipeline_core/observability/_tracking/_writer.py,sha256=xZjwYyIxDzzzPxqkKjYAY
70
70
  ai_pipeline_core/pipeline/__init__.py,sha256=uMv1jwSyq8Ym8Hbn5097twBJLdwN1iMeqnVM4EWyrhA,282
71
71
  ai_pipeline_core/pipeline/decorators.py,sha256=CDJAeOjGLt5Ewc0Jc9zEuwLZwKyutOv89LSRS9dcXmI,37456
72
72
  ai_pipeline_core/pipeline/options.py,sha256=KF4FcT085-IwX8r649v0a9ua5xnApM0qG2wJHWbq39A,438
73
- ai_pipeline_core-0.4.7.dist-info/METADATA,sha256=yFjXJ9fHXFtmrF2jIFx62k5spfR1PEipR_Uekbn3bmo,29947
74
- ai_pipeline_core-0.4.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
75
- ai_pipeline_core-0.4.7.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
76
- ai_pipeline_core-0.4.7.dist-info/RECORD,,
73
+ ai_pipeline_core-0.4.8.dist-info/METADATA,sha256=Ftytzz5IBhleZK7ce8HbE4XMc8pcjdVdOe2oN-fpluA,29947
74
+ ai_pipeline_core-0.4.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
75
+ ai_pipeline_core-0.4.8.dist-info/licenses/LICENSE,sha256=kKj8mfbdWwkyG3U6n7ztB3bAZlEwShTkAsvaY657i3I,1074
76
+ ai_pipeline_core-0.4.8.dist-info/RECORD,,