opik 1.9.5__py3-none-any.whl → 1.9.39__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.
- opik/__init__.py +10 -3
- opik/anonymizer/__init__.py +5 -0
- opik/anonymizer/anonymizer.py +12 -0
- opik/anonymizer/factory.py +80 -0
- opik/anonymizer/recursive_anonymizer.py +64 -0
- opik/anonymizer/rules.py +56 -0
- opik/anonymizer/rules_anonymizer.py +35 -0
- opik/api_objects/dataset/rest_operations.py +5 -0
- opik/api_objects/experiment/experiment.py +46 -49
- opik/api_objects/experiment/helpers.py +34 -10
- opik/api_objects/local_recording.py +8 -3
- opik/api_objects/opik_client.py +230 -48
- opik/api_objects/opik_query_language.py +9 -0
- opik/api_objects/prompt/__init__.py +11 -3
- opik/api_objects/prompt/base_prompt.py +69 -0
- opik/api_objects/prompt/base_prompt_template.py +29 -0
- opik/api_objects/prompt/chat/__init__.py +1 -0
- opik/api_objects/prompt/chat/chat_prompt.py +193 -0
- opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
- opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +37 -35
- opik/api_objects/prompt/client.py +101 -30
- opik/api_objects/prompt/text/__init__.py +1 -0
- opik/api_objects/prompt/text/prompt.py +174 -0
- opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
- opik/api_objects/prompt/types.py +1 -1
- opik/cli/export.py +6 -2
- opik/cli/usage_report/charts.py +39 -10
- opik/cli/usage_report/cli.py +164 -45
- opik/cli/usage_report/pdf.py +14 -1
- opik/config.py +0 -5
- opik/decorator/base_track_decorator.py +37 -40
- opik/decorator/context_manager/span_context_manager.py +9 -0
- opik/decorator/context_manager/trace_context_manager.py +5 -0
- opik/dict_utils.py +3 -3
- opik/evaluation/__init__.py +13 -2
- opik/evaluation/engine/engine.py +195 -223
- opik/evaluation/engine/helpers.py +8 -7
- opik/evaluation/engine/metrics_evaluator.py +237 -0
- opik/evaluation/evaluation_result.py +35 -1
- opik/evaluation/evaluator.py +318 -30
- opik/evaluation/models/litellm/util.py +78 -6
- opik/evaluation/models/model_capabilities.py +33 -0
- opik/evaluation/report.py +14 -2
- opik/evaluation/rest_operations.py +36 -33
- opik/evaluation/test_case.py +2 -2
- opik/evaluation/types.py +9 -1
- opik/exceptions.py +17 -0
- opik/hooks/__init__.py +17 -1
- opik/hooks/anonymizer_hook.py +36 -0
- opik/id_helpers.py +18 -0
- opik/integrations/adk/helpers.py +16 -7
- opik/integrations/adk/legacy_opik_tracer.py +7 -4
- opik/integrations/adk/opik_tracer.py +3 -1
- opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
- opik/integrations/adk/recursive_callback_injector.py +1 -6
- opik/integrations/dspy/callback.py +1 -4
- opik/integrations/haystack/opik_connector.py +2 -2
- opik/integrations/haystack/opik_tracer.py +2 -4
- opik/integrations/langchain/opik_tracer.py +273 -82
- opik/integrations/llama_index/callback.py +110 -108
- opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
- opik/integrations/openai/opik_tracker.py +1 -1
- opik/message_processing/batching/batchers.py +11 -7
- opik/message_processing/encoder_helpers.py +79 -0
- opik/message_processing/messages.py +25 -1
- opik/message_processing/online_message_processor.py +23 -8
- opik/opik_context.py +7 -7
- opik/rest_api/__init__.py +188 -12
- opik/rest_api/client.py +3 -0
- opik/rest_api/dashboards/__init__.py +4 -0
- opik/rest_api/dashboards/client.py +462 -0
- opik/rest_api/dashboards/raw_client.py +648 -0
- opik/rest_api/datasets/client.py +893 -89
- opik/rest_api/datasets/raw_client.py +1328 -87
- opik/rest_api/experiments/client.py +30 -2
- opik/rest_api/experiments/raw_client.py +26 -0
- opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
- opik/rest_api/optimizations/client.py +302 -0
- opik/rest_api/optimizations/raw_client.py +463 -0
- opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
- opik/rest_api/prompts/__init__.py +2 -2
- opik/rest_api/prompts/client.py +34 -4
- opik/rest_api/prompts/raw_client.py +32 -2
- opik/rest_api/prompts/types/__init__.py +3 -1
- opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
- opik/rest_api/spans/__init__.py +0 -2
- opik/rest_api/spans/client.py +148 -64
- opik/rest_api/spans/raw_client.py +210 -83
- opik/rest_api/spans/types/__init__.py +0 -2
- opik/rest_api/traces/client.py +241 -73
- opik/rest_api/traces/raw_client.py +344 -90
- opik/rest_api/types/__init__.py +200 -15
- opik/rest_api/types/aggregation_data.py +1 -0
- opik/rest_api/types/alert_trigger_config_public_type.py +6 -1
- opik/rest_api/types/alert_trigger_config_type.py +6 -1
- opik/rest_api/types/alert_trigger_config_write_type.py +6 -1
- opik/rest_api/types/automation_rule_evaluator.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
- opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
- opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
- opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
- opik/rest_api/types/boolean_feedback_definition.py +25 -0
- opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
- opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
- opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
- opik/rest_api/types/boolean_feedback_detail.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
- opik/rest_api/types/dashboard_page_public.py +24 -0
- opik/rest_api/types/dashboard_public.py +30 -0
- opik/rest_api/types/dataset.py +2 -0
- opik/rest_api/types/dataset_item.py +2 -0
- opik/rest_api/types/dataset_item_compare.py +2 -0
- opik/rest_api/types/dataset_item_filter.py +23 -0
- opik/rest_api/types/dataset_item_filter_operator.py +21 -0
- opik/rest_api/types/dataset_item_page_compare.py +1 -0
- opik/rest_api/types/dataset_item_page_public.py +1 -0
- opik/rest_api/types/dataset_item_public.py +2 -0
- opik/rest_api/types/dataset_item_update.py +39 -0
- opik/rest_api/types/dataset_item_write.py +1 -0
- opik/rest_api/types/dataset_public.py +2 -0
- opik/rest_api/types/dataset_public_status.py +5 -0
- opik/rest_api/types/dataset_status.py +5 -0
- opik/rest_api/types/dataset_version_diff.py +22 -0
- opik/rest_api/types/dataset_version_diff_stats.py +24 -0
- opik/rest_api/types/dataset_version_page_public.py +23 -0
- opik/rest_api/types/dataset_version_public.py +49 -0
- opik/rest_api/types/experiment.py +2 -0
- opik/rest_api/types/experiment_public.py +2 -0
- opik/rest_api/types/experiment_score.py +20 -0
- opik/rest_api/types/experiment_score_public.py +20 -0
- opik/rest_api/types/experiment_score_write.py +20 -0
- opik/rest_api/types/feedback.py +20 -1
- opik/rest_api/types/feedback_create.py +16 -1
- opik/rest_api/types/feedback_object_public.py +22 -1
- opik/rest_api/types/feedback_public.py +20 -1
- opik/rest_api/types/feedback_score_public.py +4 -0
- opik/rest_api/types/feedback_update.py +16 -1
- opik/rest_api/types/image_url.py +20 -0
- opik/rest_api/types/image_url_public.py +20 -0
- opik/rest_api/types/image_url_write.py +20 -0
- opik/rest_api/types/llm_as_judge_message.py +5 -1
- opik/rest_api/types/llm_as_judge_message_content.py +24 -0
- opik/rest_api/types/llm_as_judge_message_content_public.py +24 -0
- opik/rest_api/types/llm_as_judge_message_content_write.py +24 -0
- opik/rest_api/types/llm_as_judge_message_public.py +5 -1
- opik/rest_api/types/llm_as_judge_message_write.py +5 -1
- opik/rest_api/types/llm_as_judge_model_parameters.py +2 -0
- opik/rest_api/types/llm_as_judge_model_parameters_public.py +2 -0
- opik/rest_api/types/llm_as_judge_model_parameters_write.py +2 -0
- opik/rest_api/types/optimization.py +2 -0
- opik/rest_api/types/optimization_public.py +2 -0
- opik/rest_api/types/optimization_public_status.py +3 -1
- opik/rest_api/types/optimization_status.py +3 -1
- opik/rest_api/types/optimization_studio_config.py +27 -0
- opik/rest_api/types/optimization_studio_config_public.py +27 -0
- opik/rest_api/types/optimization_studio_config_write.py +27 -0
- opik/rest_api/types/optimization_studio_log.py +22 -0
- opik/rest_api/types/optimization_write.py +2 -0
- opik/rest_api/types/optimization_write_status.py +3 -1
- opik/rest_api/types/prompt.py +6 -0
- opik/rest_api/types/prompt_detail.py +6 -0
- opik/rest_api/types/prompt_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_public.py +6 -0
- opik/rest_api/types/prompt_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_template_structure.py +5 -0
- opik/rest_api/types/prompt_version.py +2 -0
- opik/rest_api/types/prompt_version_detail.py +2 -0
- opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_public.py +2 -0
- opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_template_structure.py +5 -0
- opik/rest_api/types/score_name.py +1 -0
- opik/rest_api/types/service_toggles_config.py +6 -0
- opik/rest_api/types/span_enrichment_options.py +31 -0
- opik/rest_api/types/span_filter.py +23 -0
- opik/rest_api/types/span_filter_operator.py +21 -0
- opik/rest_api/types/span_filter_write.py +23 -0
- opik/rest_api/types/span_filter_write_operator.py +21 -0
- opik/rest_api/types/span_llm_as_judge_code.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
- opik/rest_api/types/span_update.py +46 -0
- opik/rest_api/types/studio_evaluation.py +20 -0
- opik/rest_api/types/studio_evaluation_public.py +20 -0
- opik/rest_api/types/studio_evaluation_write.py +20 -0
- opik/rest_api/types/studio_llm_model.py +21 -0
- opik/rest_api/types/studio_llm_model_public.py +21 -0
- opik/rest_api/types/studio_llm_model_write.py +21 -0
- opik/rest_api/types/studio_message.py +20 -0
- opik/rest_api/types/studio_message_public.py +20 -0
- opik/rest_api/types/studio_message_write.py +20 -0
- opik/rest_api/types/studio_metric.py +21 -0
- opik/rest_api/types/studio_metric_public.py +21 -0
- opik/rest_api/types/studio_metric_write.py +21 -0
- opik/rest_api/types/studio_optimizer.py +21 -0
- opik/rest_api/types/studio_optimizer_public.py +21 -0
- opik/rest_api/types/studio_optimizer_write.py +21 -0
- opik/rest_api/types/studio_prompt.py +20 -0
- opik/rest_api/types/studio_prompt_public.py +20 -0
- opik/rest_api/types/studio_prompt_write.py +20 -0
- opik/rest_api/types/trace.py +6 -0
- opik/rest_api/types/trace_public.py +6 -0
- opik/rest_api/types/trace_thread_filter_write.py +23 -0
- opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
- opik/rest_api/types/trace_thread_update.py +19 -0
- opik/rest_api/types/trace_update.py +39 -0
- opik/rest_api/types/value_entry.py +2 -0
- opik/rest_api/types/value_entry_compare.py +2 -0
- opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
- opik/rest_api/types/value_entry_public.py +2 -0
- opik/rest_api/types/video_url.py +19 -0
- opik/rest_api/types/video_url_public.py +19 -0
- opik/rest_api/types/video_url_write.py +19 -0
- opik/synchronization.py +5 -6
- opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/METADATA +5 -4
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/RECORD +246 -151
- opik/api_objects/prompt/chat_prompt_template.py +0 -164
- opik/api_objects/prompt/prompt.py +0 -131
- /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/top_level.txt +0 -0
|
@@ -9,8 +9,8 @@ from typing import (
|
|
|
9
9
|
Set,
|
|
10
10
|
TYPE_CHECKING,
|
|
11
11
|
cast,
|
|
12
|
-
Tuple,
|
|
13
12
|
Callable,
|
|
13
|
+
NamedTuple,
|
|
14
14
|
)
|
|
15
15
|
import contextvars
|
|
16
16
|
from uuid import UUID
|
|
@@ -19,8 +19,7 @@ from langchain_core import language_models
|
|
|
19
19
|
from langchain_core.tracers import BaseTracer
|
|
20
20
|
from langchain_core.tracers.schemas import Run
|
|
21
21
|
|
|
22
|
-
import
|
|
23
|
-
import opik.llm_usage as llm_usage
|
|
22
|
+
from opik import context_storage, dict_utils, llm_usage, tracing_runtime_config
|
|
24
23
|
from opik.api_objects import span, trace
|
|
25
24
|
from opik.types import DistributedTraceHeadersDict, ErrorInfoDict
|
|
26
25
|
from opik.validation import parameters_validator
|
|
@@ -32,8 +31,6 @@ from . import (
|
|
|
32
31
|
)
|
|
33
32
|
|
|
34
33
|
from ...api_objects import helpers, opik_client
|
|
35
|
-
import opik.context_storage as context_storage
|
|
36
|
-
import opik.decorator.tracing_runtime_config as tracing_runtime_config
|
|
37
34
|
|
|
38
35
|
if TYPE_CHECKING:
|
|
39
36
|
from langchain_core.runnables.graph import Graph
|
|
@@ -57,6 +54,11 @@ SkipErrorCallback = Callable[[str], bool]
|
|
|
57
54
|
ERROR_SKIPPED_OUTPUTS = {"warning": "Error output skipped by skip_error_callback."}
|
|
58
55
|
|
|
59
56
|
|
|
57
|
+
class TrackRootRunResult(NamedTuple):
|
|
58
|
+
new_trace_data: Optional[trace.TraceData]
|
|
59
|
+
new_span_data: Optional[span.SpanData]
|
|
60
|
+
|
|
61
|
+
|
|
60
62
|
def _get_span_type(run: Dict[str, Any]) -> SpanType:
|
|
61
63
|
if run.get("run_type") in ["llm", "tool"]:
|
|
62
64
|
return cast(SpanType, run.get("run_type"))
|
|
@@ -67,6 +69,14 @@ def _get_span_type(run: Dict[str, Any]) -> SpanType:
|
|
|
67
69
|
return cast(SpanType, "general")
|
|
68
70
|
|
|
69
71
|
|
|
72
|
+
def _is_root_run(run_dict: Dict[str, Any]) -> bool:
|
|
73
|
+
return run_dict.get("parent_run_id") is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_run_metadata(run_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
77
|
+
return run_dict["extra"].get("metadata", {})
|
|
78
|
+
|
|
79
|
+
|
|
70
80
|
class OpikTracer(BaseTracer):
|
|
71
81
|
"""Langchain Opik Tracer."""
|
|
72
82
|
|
|
@@ -132,6 +142,12 @@ class OpikTracer(BaseTracer):
|
|
|
132
142
|
|
|
133
143
|
self._externally_created_traces_ids: Set[str] = set()
|
|
134
144
|
|
|
145
|
+
self._skipped_langgraph_root_run_ids: Set[UUID] = set()
|
|
146
|
+
"""Set of run IDs for LangGraph root runs where we skip creating the span."""
|
|
147
|
+
|
|
148
|
+
self._langgraph_parent_span_ids: Dict[UUID, Optional[str]] = {}
|
|
149
|
+
"""Map from LangGraph root run ID to parent span ID (None if attached to trace)."""
|
|
150
|
+
|
|
135
151
|
self._project_name = project_name
|
|
136
152
|
|
|
137
153
|
self._distributed_headers = distributed_headers
|
|
@@ -157,14 +173,6 @@ class OpikTracer(BaseTracer):
|
|
|
157
173
|
)
|
|
158
174
|
|
|
159
175
|
def _persist_run(self, run: Run) -> None:
|
|
160
|
-
if run.id not in self._span_data_map:
|
|
161
|
-
LOGGER.warning(
|
|
162
|
-
f"Span data for run '{run.id}' not found in the span data map. Skipping processing of _persist_run."
|
|
163
|
-
)
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
span_data = self._span_data_map[run.id]
|
|
167
|
-
|
|
168
176
|
run_dict: Dict[str, Any] = run.dict()
|
|
169
177
|
|
|
170
178
|
error_info: Optional[ErrorInfoDict]
|
|
@@ -187,34 +195,49 @@ class OpikTracer(BaseTracer):
|
|
|
187
195
|
langchain_helpers.split_big_langgraph_outputs(outputs)
|
|
188
196
|
)
|
|
189
197
|
|
|
198
|
+
self._ensure_no_hanging_opik_tracer_spans()
|
|
199
|
+
|
|
200
|
+
span_data = self._span_data_map.get(run.id)
|
|
190
201
|
if (
|
|
191
|
-
span_data
|
|
192
|
-
|
|
202
|
+
span_data is None
|
|
203
|
+
or span_data.trace_id not in self._externally_created_traces_ids
|
|
193
204
|
):
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
205
|
+
self._finalize_trace(
|
|
206
|
+
run_id=run.id,
|
|
207
|
+
run_dict=run_dict,
|
|
208
|
+
trace_additional_metadata=trace_additional_metadata,
|
|
209
|
+
outputs=outputs,
|
|
210
|
+
error_info=error_info,
|
|
211
|
+
)
|
|
199
212
|
|
|
200
|
-
|
|
213
|
+
def _finalize_trace(
|
|
214
|
+
self,
|
|
215
|
+
run_id: UUID,
|
|
216
|
+
run_dict: Dict[str, Any],
|
|
217
|
+
trace_additional_metadata: Optional[Dict[str, Any]],
|
|
218
|
+
outputs: Optional[Dict[str, Any]],
|
|
219
|
+
error_info: Optional[ErrorInfoDict],
|
|
220
|
+
) -> None:
|
|
221
|
+
trace_data = self._created_traces_data_map.get(run_id)
|
|
222
|
+
if trace_data is None:
|
|
223
|
+
LOGGER.warning(
|
|
224
|
+
f"Trace data for run '{run_id}' not found in the traces data map. Skipping processing of _finalize_trace."
|
|
225
|
+
)
|
|
226
|
+
return
|
|
201
227
|
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
# workaround for `.astream()` method usage
|
|
229
|
+
if trace_data.input == {"input": ""}:
|
|
230
|
+
trace_data.input = run_dict["inputs"]
|
|
204
231
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
trace_data.input = run_dict["inputs"]
|
|
232
|
+
if trace_additional_metadata:
|
|
233
|
+
trace_data.update(metadata=trace_additional_metadata)
|
|
208
234
|
|
|
209
|
-
|
|
210
|
-
|
|
235
|
+
trace_data.init_end_time().update(output=outputs, error_info=error_info)
|
|
236
|
+
trace_ = self._opik_client.trace(**trace_data.as_parameters)
|
|
211
237
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
assert trace_ is not None
|
|
216
|
-
self._created_traces.append(trace_)
|
|
217
|
-
self._opik_context_storage.pop_trace_data(ensure_id=trace_data.id)
|
|
238
|
+
assert trace_ is not None
|
|
239
|
+
self._created_traces.append(trace_)
|
|
240
|
+
self._opik_context_storage.pop_trace_data(ensure_id=trace_data.id)
|
|
218
241
|
|
|
219
242
|
def _ensure_no_hanging_opik_tracer_spans(self) -> None:
|
|
220
243
|
root_run_external_parent_span_id = self._root_run_external_parent_span_id.get()
|
|
@@ -231,47 +254,71 @@ class OpikTracer(BaseTracer):
|
|
|
231
254
|
)
|
|
232
255
|
|
|
233
256
|
def _track_root_run(
|
|
234
|
-
self, run_dict: Dict[str, Any]
|
|
235
|
-
) ->
|
|
236
|
-
run_metadata = run_dict
|
|
257
|
+
self, run_dict: Dict[str, Any], allow_duplicating_root_span: bool
|
|
258
|
+
) -> TrackRootRunResult:
|
|
259
|
+
run_metadata = _get_run_metadata(run_dict)
|
|
237
260
|
root_metadata = dict_utils.deepmerge(self._trace_default_metadata, run_metadata)
|
|
238
261
|
self._update_thread_id_from_metadata(run_dict)
|
|
239
262
|
|
|
263
|
+
# Skip creating a span for root runs only when creating a new trace
|
|
264
|
+
# Keep the span when invoked from a tracked function, existing trace or distributed headers
|
|
265
|
+
|
|
240
266
|
if self._distributed_headers:
|
|
241
267
|
new_span_data = self._attach_span_to_distributed_headers(
|
|
242
268
|
run_dict=run_dict,
|
|
243
|
-
|
|
269
|
+
metadata=root_metadata,
|
|
270
|
+
)
|
|
271
|
+
return TrackRootRunResult(
|
|
272
|
+
new_trace_data=None,
|
|
273
|
+
new_span_data=new_span_data,
|
|
244
274
|
)
|
|
245
|
-
return None, new_span_data
|
|
246
275
|
|
|
247
276
|
current_span_data = self._opik_context_storage.top_span_data()
|
|
248
|
-
|
|
277
|
+
parent_span_id_when_langgraph_started = (
|
|
249
278
|
current_span_data.id if current_span_data is not None else None
|
|
250
279
|
)
|
|
280
|
+
self._root_run_external_parent_span_id.set(
|
|
281
|
+
parent_span_id_when_langgraph_started
|
|
282
|
+
)
|
|
251
283
|
if current_span_data is not None:
|
|
252
|
-
|
|
284
|
+
# When invoked from a tracked function, keep the root span
|
|
285
|
+
# and attach it to the parent span (don't skip it)
|
|
286
|
+
new_span_data = self._attach_span_to_external_span(
|
|
253
287
|
run_dict=run_dict,
|
|
254
288
|
current_span_data=current_span_data,
|
|
255
289
|
root_metadata=root_metadata,
|
|
256
290
|
)
|
|
257
|
-
return
|
|
291
|
+
return TrackRootRunResult(
|
|
292
|
+
new_trace_data=None,
|
|
293
|
+
new_span_data=new_span_data,
|
|
294
|
+
)
|
|
258
295
|
|
|
259
296
|
current_trace_data = self._opik_context_storage.get_trace_data()
|
|
260
297
|
if current_trace_data is not None:
|
|
261
|
-
|
|
298
|
+
# When invoked under an existing trace, keep the root span
|
|
299
|
+
# and attach it to the parent trace (don't skip it)
|
|
300
|
+
new_span_data = self._attach_span_to_external_trace(
|
|
262
301
|
run_dict=run_dict,
|
|
263
302
|
current_trace_data=current_trace_data,
|
|
264
303
|
root_metadata=root_metadata,
|
|
265
304
|
)
|
|
266
|
-
return
|
|
305
|
+
return TrackRootRunResult(
|
|
306
|
+
new_trace_data=None,
|
|
307
|
+
new_span_data=new_span_data,
|
|
308
|
+
)
|
|
267
309
|
|
|
268
310
|
return self._initialize_span_and_trace_from_scratch(
|
|
269
|
-
run_dict=run_dict,
|
|
311
|
+
run_dict=run_dict,
|
|
312
|
+
root_metadata=root_metadata,
|
|
313
|
+
allow_duplicating_root_span=allow_duplicating_root_span,
|
|
270
314
|
)
|
|
271
315
|
|
|
272
316
|
def _initialize_span_and_trace_from_scratch(
|
|
273
|
-
self,
|
|
274
|
-
|
|
317
|
+
self,
|
|
318
|
+
run_dict: Dict[str, Any],
|
|
319
|
+
root_metadata: Dict[str, Any],
|
|
320
|
+
allow_duplicating_root_span: bool,
|
|
321
|
+
) -> TrackRootRunResult:
|
|
275
322
|
trace_data = trace.TraceData(
|
|
276
323
|
name=run_dict["name"],
|
|
277
324
|
input=run_dict["inputs"],
|
|
@@ -280,6 +327,14 @@ class OpikTracer(BaseTracer):
|
|
|
280
327
|
project_name=self._project_name,
|
|
281
328
|
thread_id=self._thread_id,
|
|
282
329
|
)
|
|
330
|
+
|
|
331
|
+
# Skip creating a span for LangGraph root runs - children will be attached directly to trace
|
|
332
|
+
if _is_root_run(run_dict) and not allow_duplicating_root_span:
|
|
333
|
+
return TrackRootRunResult(
|
|
334
|
+
new_trace_data=trace_data,
|
|
335
|
+
new_span_data=None,
|
|
336
|
+
)
|
|
337
|
+
|
|
283
338
|
span_data = span.SpanData(
|
|
284
339
|
trace_id=trace_data.id,
|
|
285
340
|
parent_span_id=None,
|
|
@@ -290,9 +345,9 @@ class OpikTracer(BaseTracer):
|
|
|
290
345
|
tags=self._trace_default_tags,
|
|
291
346
|
project_name=self._project_name,
|
|
292
347
|
)
|
|
293
|
-
return trace_data, span_data
|
|
348
|
+
return TrackRootRunResult(new_trace_data=trace_data, new_span_data=span_data)
|
|
294
349
|
|
|
295
|
-
def
|
|
350
|
+
def _attach_span_to_external_span(
|
|
296
351
|
self,
|
|
297
352
|
run_dict: Dict[str, Any],
|
|
298
353
|
current_span_data: span.SpanData,
|
|
@@ -318,7 +373,7 @@ class OpikTracer(BaseTracer):
|
|
|
318
373
|
|
|
319
374
|
return span_data
|
|
320
375
|
|
|
321
|
-
def
|
|
376
|
+
def _attach_span_to_external_trace(
|
|
322
377
|
self,
|
|
323
378
|
run_dict: Dict[str, Any],
|
|
324
379
|
current_trace_data: trace.TraceData,
|
|
@@ -339,6 +394,8 @@ class OpikTracer(BaseTracer):
|
|
|
339
394
|
project_name=project_name,
|
|
340
395
|
type=_get_span_type(run_dict),
|
|
341
396
|
)
|
|
397
|
+
span_data.update(metadata={"created_from": "langchain"})
|
|
398
|
+
|
|
342
399
|
if not self._is_opik_trace_created_by_this_tracer(current_trace_data.id):
|
|
343
400
|
self._externally_created_traces_ids.add(current_trace_data.id)
|
|
344
401
|
return span_data
|
|
@@ -346,7 +403,7 @@ class OpikTracer(BaseTracer):
|
|
|
346
403
|
def _attach_span_to_distributed_headers(
|
|
347
404
|
self,
|
|
348
405
|
run_dict: Dict[str, Any],
|
|
349
|
-
|
|
406
|
+
metadata: Dict[str, Any],
|
|
350
407
|
) -> span.SpanData:
|
|
351
408
|
if self._distributed_headers is None:
|
|
352
409
|
raise ValueError("Distributed headers are not set")
|
|
@@ -356,7 +413,7 @@ class OpikTracer(BaseTracer):
|
|
|
356
413
|
parent_span_id=self._distributed_headers["opik_parent_span_id"],
|
|
357
414
|
name=run_dict["name"],
|
|
358
415
|
input=run_dict["inputs"],
|
|
359
|
-
metadata=
|
|
416
|
+
metadata=metadata,
|
|
360
417
|
tags=self._trace_default_tags,
|
|
361
418
|
project_name=self._project_name,
|
|
362
419
|
type=_get_span_type(run_dict),
|
|
@@ -364,43 +421,106 @@ class OpikTracer(BaseTracer):
|
|
|
364
421
|
self._externally_created_traces_ids.add(span_data.trace_id)
|
|
365
422
|
return span_data
|
|
366
423
|
|
|
367
|
-
def _process_start_span(self, run: Run) -> None:
|
|
424
|
+
def _process_start_span(self, run: Run, allow_duplicating_root_span: bool) -> None:
|
|
368
425
|
try:
|
|
369
|
-
self._process_start_span_unsafe(run)
|
|
426
|
+
self._process_start_span_unsafe(run, allow_duplicating_root_span)
|
|
370
427
|
except Exception as e:
|
|
371
428
|
LOGGER.error("Failed during _process_start_span: %s", e, exc_info=True)
|
|
372
429
|
|
|
373
|
-
def _process_start_span_unsafe(
|
|
430
|
+
def _process_start_span_unsafe(
|
|
431
|
+
self, run: Run, allow_duplicating_root_span: bool
|
|
432
|
+
) -> None:
|
|
374
433
|
run_dict: Dict[str, Any] = run.dict()
|
|
375
|
-
new_span_data: span.SpanData
|
|
376
434
|
|
|
377
435
|
if not run.parent_run_id:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
and tracing_runtime_config.is_tracing_active()
|
|
385
|
-
):
|
|
386
|
-
self._opik_client.trace(**new_trace_data.as_start_parameters)
|
|
436
|
+
self._create_root_trace_and_span(
|
|
437
|
+
run_id=run.id,
|
|
438
|
+
run_dict=run_dict,
|
|
439
|
+
allow_duplicating_root_span=allow_duplicating_root_span,
|
|
440
|
+
)
|
|
441
|
+
return
|
|
387
442
|
|
|
443
|
+
# Check if the parent is a skipped LangGraph/LangChain root run.
|
|
444
|
+
# If so, attach children directly to trace.
|
|
445
|
+
# Otherwise, attach to the parent span.
|
|
446
|
+
if run.parent_run_id in self._skipped_langgraph_root_run_ids:
|
|
447
|
+
self._attach_span_to_local_or_distributed_trace(
|
|
448
|
+
run_id=run.id,
|
|
449
|
+
parent_run_id=run.parent_run_id,
|
|
450
|
+
run_dict=run_dict,
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
self._attach_span_to_parent_span(
|
|
454
|
+
run_id=run.id, parent_run_id=run.parent_run_id, run_dict=run_dict
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def _create_root_trace_and_span(
|
|
458
|
+
self, run_id: UUID, run_dict: Dict[str, Any], allow_duplicating_root_span: bool
|
|
459
|
+
) -> None:
|
|
460
|
+
"""
|
|
461
|
+
Creates a root trace and span for a given run and stores the relevant trace and span
|
|
462
|
+
data in local storage for future reference.
|
|
463
|
+
|
|
464
|
+
The new span is only created if no new trace is created, i.e., when attached to an existing span
|
|
465
|
+
or distributed headers. If a new trace is created, the span is skipped and only the
|
|
466
|
+
trace data is stored in local storage for future reference.
|
|
467
|
+
"""
|
|
468
|
+
# This is the first run for the chain.
|
|
469
|
+
root_run_result = self._track_root_run(run_dict, allow_duplicating_root_span)
|
|
470
|
+
if root_run_result.new_trace_data is not None:
|
|
471
|
+
self._opik_context_storage.set_trace_data(root_run_result.new_trace_data)
|
|
472
|
+
if (
|
|
473
|
+
self._opik_client.config.log_start_trace_span
|
|
474
|
+
and tracing_runtime_config.is_tracing_active()
|
|
475
|
+
):
|
|
476
|
+
self._opik_client.trace(
|
|
477
|
+
**root_run_result.new_trace_data.as_start_parameters
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# If this is a LangGraph/LangChain root run under fresh trace, skip creating the span
|
|
481
|
+
if root_run_result.new_span_data is None:
|
|
482
|
+
# Mark this run as skipped and store trace data for child runs
|
|
483
|
+
self._skipped_langgraph_root_run_ids.add(run_id)
|
|
484
|
+
|
|
485
|
+
# Store parent span ID if LangGraph was attached to the existing span
|
|
486
|
+
parent_span_id = self._root_run_external_parent_span_id.get()
|
|
487
|
+
self._langgraph_parent_span_ids[run_id] = parent_span_id
|
|
488
|
+
|
|
489
|
+
# Store trace data if we created a new trace but skip span data
|
|
490
|
+
if root_run_result.new_trace_data is not None:
|
|
491
|
+
self._save_span_trace_data_to_local_maps(
|
|
492
|
+
run_id=run_id,
|
|
493
|
+
span_data=None,
|
|
494
|
+
trace_data=root_run_result.new_trace_data,
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
388
497
|
# save new span and trace data to local maps to be able to retrieve them later
|
|
389
498
|
self._save_span_trace_data_to_local_maps(
|
|
390
|
-
run_id=
|
|
391
|
-
span_data=new_span_data,
|
|
392
|
-
trace_data=new_trace_data,
|
|
499
|
+
run_id=run_id,
|
|
500
|
+
span_data=root_run_result.new_span_data,
|
|
501
|
+
trace_data=root_run_result.new_trace_data,
|
|
393
502
|
)
|
|
394
503
|
|
|
395
|
-
self._opik_context_storage.add_span_data(new_span_data)
|
|
504
|
+
self._opik_context_storage.add_span_data(root_run_result.new_span_data)
|
|
396
505
|
if (
|
|
397
506
|
self._opik_client.config.log_start_trace_span
|
|
398
507
|
and tracing_runtime_config.is_tracing_active()
|
|
399
508
|
):
|
|
400
|
-
self._opik_client.span(
|
|
401
|
-
|
|
509
|
+
self._opik_client.span(
|
|
510
|
+
**root_run_result.new_span_data.as_start_parameters
|
|
511
|
+
)
|
|
402
512
|
|
|
403
|
-
|
|
513
|
+
def _attach_span_to_parent_span(
|
|
514
|
+
self, run_id: UUID, parent_run_id: UUID, run_dict: Dict[str, Any]
|
|
515
|
+
) -> None:
|
|
516
|
+
"""
|
|
517
|
+
Attaches child span to a parent span and updates relevant context storage.
|
|
518
|
+
|
|
519
|
+
This method is responsible for creating a new span data object associated with a
|
|
520
|
+
run, linking it to the parent span data, and saving it to local and external maps.
|
|
521
|
+
Additionally, it updates the context storage and logs the span if tracing is active.
|
|
522
|
+
"""
|
|
523
|
+
parent_span_data = self._span_data_map[parent_run_id]
|
|
404
524
|
|
|
405
525
|
project_name = helpers.resolve_child_span_project_name(
|
|
406
526
|
parent_span_data.project_name,
|
|
@@ -411,22 +531,22 @@ class OpikTracer(BaseTracer):
|
|
|
411
531
|
trace_id=parent_span_data.trace_id,
|
|
412
532
|
parent_span_id=parent_span_data.id,
|
|
413
533
|
input=run_dict["inputs"],
|
|
414
|
-
metadata=run_dict
|
|
415
|
-
name=
|
|
534
|
+
metadata=_get_run_metadata(run_dict),
|
|
535
|
+
name=run_dict["name"],
|
|
416
536
|
type=_get_span_type(run_dict),
|
|
417
537
|
project_name=project_name,
|
|
418
538
|
)
|
|
419
539
|
new_span_data.update(metadata={"created_from": "langchain"})
|
|
420
540
|
|
|
421
541
|
self._save_span_trace_data_to_local_maps(
|
|
422
|
-
run_id=
|
|
542
|
+
run_id=run_id,
|
|
423
543
|
span_data=new_span_data,
|
|
424
544
|
trace_data=None,
|
|
425
545
|
)
|
|
426
546
|
|
|
427
547
|
if new_span_data.trace_id not in self._externally_created_traces_ids:
|
|
428
|
-
self._created_traces_data_map[
|
|
429
|
-
|
|
548
|
+
self._created_traces_data_map[run_id] = self._created_traces_data_map[
|
|
549
|
+
parent_run_id
|
|
430
550
|
]
|
|
431
551
|
|
|
432
552
|
self._opik_context_storage.add_span_data(new_span_data)
|
|
@@ -436,9 +556,76 @@ class OpikTracer(BaseTracer):
|
|
|
436
556
|
):
|
|
437
557
|
self._opik_client.span(**new_span_data.as_start_parameters)
|
|
438
558
|
|
|
559
|
+
def _attach_span_to_local_or_distributed_trace(
|
|
560
|
+
self, run_id: UUID, parent_run_id: UUID, run_dict: Dict[str, Any]
|
|
561
|
+
) -> None:
|
|
562
|
+
"""
|
|
563
|
+
Attaches child span directly to a trace by checking trace data or distributed
|
|
564
|
+
headers and creates new span data based on the provided run information.
|
|
565
|
+
"""
|
|
566
|
+
# Check if we have trace data (new trace) or distributed headers
|
|
567
|
+
if parent_run_id in self._created_traces_data_map:
|
|
568
|
+
# LangGraph created a new trace - attach children directly to trace
|
|
569
|
+
trace_data = self._created_traces_data_map[parent_run_id]
|
|
570
|
+
project_name = helpers.resolve_child_span_project_name(
|
|
571
|
+
trace_data.project_name,
|
|
572
|
+
self._project_name,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
new_span_data = span.SpanData(
|
|
576
|
+
trace_id=trace_data.id,
|
|
577
|
+
parent_span_id=None, # Direct child of trace
|
|
578
|
+
input=run_dict["inputs"],
|
|
579
|
+
metadata=_get_run_metadata(run_dict),
|
|
580
|
+
name=run_dict["name"],
|
|
581
|
+
type=_get_span_type(run_dict),
|
|
582
|
+
project_name=project_name,
|
|
583
|
+
)
|
|
584
|
+
if new_span_data.trace_id not in self._externally_created_traces_ids:
|
|
585
|
+
self._created_traces_data_map[run_id] = trace_data
|
|
586
|
+
|
|
587
|
+
elif self._distributed_headers:
|
|
588
|
+
# LangGraph with distributed headers - attach to distributed trace
|
|
589
|
+
new_span_data = self._attach_span_to_distributed_headers(
|
|
590
|
+
run_dict=run_dict,
|
|
591
|
+
metadata=_get_run_metadata(run_dict),
|
|
592
|
+
)
|
|
593
|
+
elif (
|
|
594
|
+
current_trace_data := self._opik_context_storage.get_trace_data()
|
|
595
|
+
) is not None:
|
|
596
|
+
# LangGraph attached to existing trace - attach children directly to trace
|
|
597
|
+
new_span_data = self._attach_span_to_external_trace(
|
|
598
|
+
run_dict=run_dict,
|
|
599
|
+
current_trace_data=current_trace_data,
|
|
600
|
+
root_metadata=_get_run_metadata(run_dict),
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
LOGGER.warning(
|
|
604
|
+
f"Cannot find trace data or distributed headers for LangGraph child run '{run_id}'"
|
|
605
|
+
)
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
new_span_data.update(metadata={"created_from": "langchain"})
|
|
609
|
+
self._save_span_trace_data_to_local_maps(
|
|
610
|
+
run_id=run_id,
|
|
611
|
+
span_data=new_span_data,
|
|
612
|
+
trace_data=None,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
self._opik_context_storage.add_span_data(new_span_data)
|
|
616
|
+
if (
|
|
617
|
+
self._opik_client.config.log_start_trace_span
|
|
618
|
+
and tracing_runtime_config.is_tracing_active()
|
|
619
|
+
):
|
|
620
|
+
self._opik_client.span(**new_span_data.as_start_parameters)
|
|
621
|
+
|
|
439
622
|
def _process_end_span(self, run: Run) -> None:
|
|
440
623
|
span_data = None
|
|
441
624
|
try:
|
|
625
|
+
# Skip processing if this is a skipped LangGraph root run
|
|
626
|
+
if run.id in self._skipped_langgraph_root_run_ids:
|
|
627
|
+
return
|
|
628
|
+
|
|
442
629
|
if run.id not in self._span_data_map:
|
|
443
630
|
LOGGER.warning(
|
|
444
631
|
f"Span data for run '{run.id}' not found in the span data map. Skipping processing of end span."
|
|
@@ -493,6 +680,10 @@ class OpikTracer(BaseTracer):
|
|
|
493
680
|
return self._skip_error_callback(error_str)
|
|
494
681
|
|
|
495
682
|
def _process_end_span_with_error(self, run: Run) -> None:
|
|
683
|
+
# Skip processing if this is a skipped LangGraph root run
|
|
684
|
+
if run.id in self._skipped_langgraph_root_run_ids:
|
|
685
|
+
return
|
|
686
|
+
|
|
496
687
|
if run.id not in self._span_data_map:
|
|
497
688
|
LOGGER.warning(
|
|
498
689
|
f"Span data for run '{run.id}' not found in the span data map. Skipping processing of _process_end_span_with_error."
|
|
@@ -531,7 +722,7 @@ class OpikTracer(BaseTracer):
|
|
|
531
722
|
def _update_thread_id_from_metadata(self, run_dict: Dict[str, Any]) -> None:
|
|
532
723
|
if not self._thread_id:
|
|
533
724
|
# We want to default to any manually set thread_id, so only update if self._thread_id is not already set
|
|
534
|
-
thread_id = run_dict
|
|
725
|
+
thread_id = _get_run_metadata(run_dict).get("thread_id")
|
|
535
726
|
|
|
536
727
|
if thread_id:
|
|
537
728
|
self._thread_id = thread_id
|
|
@@ -577,7 +768,7 @@ class OpikTracer(BaseTracer):
|
|
|
577
768
|
if self._skip_tracking():
|
|
578
769
|
return
|
|
579
770
|
|
|
580
|
-
self._process_start_span(run)
|
|
771
|
+
self._process_start_span(run, allow_duplicating_root_span=True)
|
|
581
772
|
|
|
582
773
|
def on_chat_model_start(
|
|
583
774
|
self,
|
|
@@ -637,7 +828,7 @@ class OpikTracer(BaseTracer):
|
|
|
637
828
|
if self._skip_tracking():
|
|
638
829
|
return
|
|
639
830
|
|
|
640
|
-
self._process_start_span(run)
|
|
831
|
+
self._process_start_span(run, allow_duplicating_root_span=True)
|
|
641
832
|
|
|
642
833
|
def _on_llm_end(self, run: Run) -> None:
|
|
643
834
|
"""Process the LLM Run."""
|
|
@@ -658,7 +849,7 @@ class OpikTracer(BaseTracer):
|
|
|
658
849
|
if self._skip_tracking():
|
|
659
850
|
return
|
|
660
851
|
|
|
661
|
-
self._process_start_span(run)
|
|
852
|
+
self._process_start_span(run, allow_duplicating_root_span=False)
|
|
662
853
|
|
|
663
854
|
def _on_chain_end(self, run: Run) -> None:
|
|
664
855
|
"""Process the Chain Run."""
|
|
@@ -679,7 +870,7 @@ class OpikTracer(BaseTracer):
|
|
|
679
870
|
if self._skip_tracking():
|
|
680
871
|
return
|
|
681
872
|
|
|
682
|
-
self._process_start_span(run)
|
|
873
|
+
self._process_start_span(run, allow_duplicating_root_span=True)
|
|
683
874
|
|
|
684
875
|
def _on_tool_end(self, run: Run) -> None:
|
|
685
876
|
"""Process the Tool Run."""
|