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
opik/__init__.py
CHANGED
|
@@ -6,18 +6,23 @@ from .api_objects.experiment.experiment_item import (
|
|
|
6
6
|
ExperimentItemReferences,
|
|
7
7
|
)
|
|
8
8
|
from .api_objects.opik_client import Opik
|
|
9
|
-
from .api_objects.prompt import Prompt
|
|
9
|
+
from .api_objects.prompt import Prompt, ChatPrompt
|
|
10
10
|
from .api_objects.prompt.types import PromptType
|
|
11
11
|
from .api_objects.span import Span
|
|
12
12
|
from .api_objects.trace import Trace
|
|
13
13
|
from .configurator.configure import configure
|
|
14
14
|
from .decorator.tracker import flush_tracker, track
|
|
15
|
-
from .evaluation import
|
|
15
|
+
from .evaluation import (
|
|
16
|
+
evaluate,
|
|
17
|
+
evaluate_experiment,
|
|
18
|
+
evaluate_on_dict_items,
|
|
19
|
+
evaluate_prompt,
|
|
20
|
+
)
|
|
16
21
|
from .integrations.sagemaker import auth as sagemaker_auth
|
|
17
22
|
from .plugins.pytest.decorator import llm_unit
|
|
18
23
|
from .types import LLMProvider
|
|
19
24
|
from . import opik_context
|
|
20
|
-
from .
|
|
25
|
+
from .tracing_runtime_config import (
|
|
21
26
|
is_tracing_active,
|
|
22
27
|
reset_tracing_to_config_default,
|
|
23
28
|
set_tracing_active,
|
|
@@ -37,6 +42,7 @@ __all__ = [
|
|
|
37
42
|
"evaluate",
|
|
38
43
|
"evaluate_prompt",
|
|
39
44
|
"evaluate_experiment",
|
|
45
|
+
"evaluate_on_dict_items",
|
|
40
46
|
"ExperimentItemContent",
|
|
41
47
|
"ExperimentItemReferences",
|
|
42
48
|
"track",
|
|
@@ -49,6 +55,7 @@ __all__ = [
|
|
|
49
55
|
"llm_unit",
|
|
50
56
|
"configure",
|
|
51
57
|
"Prompt",
|
|
58
|
+
"ChatPrompt",
|
|
52
59
|
"PromptType",
|
|
53
60
|
"LLMProvider",
|
|
54
61
|
"reset_tracing_to_config_default",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Dict, Any, Union, List
|
|
3
|
+
|
|
4
|
+
AnonymizerDataType = Union[Dict[str, Any], str, List[Any]]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Anonymizer(abc.ABC):
|
|
8
|
+
"""Abstract base class for anonymizing sensitive data in various data structures."""
|
|
9
|
+
|
|
10
|
+
@abc.abstractmethod
|
|
11
|
+
def anonymize(self, data: AnonymizerDataType, **kwargs: Any) -> AnonymizerDataType:
|
|
12
|
+
pass
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Union, List, Dict, Callable, Tuple
|
|
2
|
+
|
|
3
|
+
from . import anonymizer, rules_anonymizer, rules
|
|
4
|
+
|
|
5
|
+
RulesType = Union[
|
|
6
|
+
List[Dict[str, str]],
|
|
7
|
+
List[Tuple[str, str]],
|
|
8
|
+
List[Callable[[str], str]],
|
|
9
|
+
List[Union[Dict[str, str], Tuple[str, str], Callable[[str], str]]],
|
|
10
|
+
Dict[str, str],
|
|
11
|
+
Tuple[str, str],
|
|
12
|
+
Callable[[str], str],
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_anonymizer(
|
|
17
|
+
anonymizer_rules: RulesType, max_depth: int = 10
|
|
18
|
+
) -> anonymizer.Anonymizer:
|
|
19
|
+
"""Create an anonymizer with the specified rules.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
anonymizer_rules: Anonymizer rules specification in various formats:
|
|
23
|
+
- Dict with "regex" and "replace" keys for a single regex rule
|
|
24
|
+
- Tuple with (regex, replacement) for a single regex rule
|
|
25
|
+
- Callable that takes a string and returns anonymized string
|
|
26
|
+
- List of any of the above for multiple rules
|
|
27
|
+
max_depth: Maximum recursion depth for nested data structures.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
An Anonymizer instance configured with the specified rules.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If a rule format is invalid.
|
|
34
|
+
"""
|
|
35
|
+
rule_objects: List[rules.Rule] = []
|
|
36
|
+
|
|
37
|
+
if callable(anonymizer_rules):
|
|
38
|
+
# Single function rule
|
|
39
|
+
rule_objects.append(rules.FunctionRule(anonymizer_rules))
|
|
40
|
+
elif isinstance(anonymizer_rules, dict):
|
|
41
|
+
# Single dictionary rule
|
|
42
|
+
_check_dictionary_rule(anonymizer_rules)
|
|
43
|
+
rule_objects.append(
|
|
44
|
+
rules.RegexRule(anonymizer_rules["regex"], anonymizer_rules["replace"])
|
|
45
|
+
)
|
|
46
|
+
elif isinstance(anonymizer_rules, tuple):
|
|
47
|
+
# Single tuple rule
|
|
48
|
+
_check_tuple_rule(anonymizer_rules)
|
|
49
|
+
regex_pattern, replacement = anonymizer_rules
|
|
50
|
+
rule_objects.append(rules.RegexRule(regex_pattern, replacement))
|
|
51
|
+
elif isinstance(anonymizer_rules, list):
|
|
52
|
+
# List of rules
|
|
53
|
+
for rule in anonymizer_rules:
|
|
54
|
+
if callable(rule) and not isinstance(rule, (dict, tuple)):
|
|
55
|
+
rule_objects.append(rules.FunctionRule(rule))
|
|
56
|
+
elif isinstance(rule, dict):
|
|
57
|
+
_check_dictionary_rule(rule)
|
|
58
|
+
rule_objects.append(rules.RegexRule(rule["regex"], rule["replace"]))
|
|
59
|
+
elif isinstance(rule, tuple):
|
|
60
|
+
_check_tuple_rule(rule)
|
|
61
|
+
regex_pattern, replacement = rule
|
|
62
|
+
rule_objects.append(rules.RegexRule(regex_pattern, replacement))
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(f"Unsupported rule type in list: {type(rule)}")
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"Unsupported rules type: {type(anonymizer_rules)}")
|
|
67
|
+
|
|
68
|
+
return rules_anonymizer.RulesAnonymizer(rule_objects, max_depth=max_depth)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _check_dictionary_rule(rule: Dict[str, str]) -> None:
|
|
72
|
+
if "regex" not in rule or "replace" not in rule:
|
|
73
|
+
raise ValueError("Dictionary rule must have 'regex' and 'replace' keys")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check_tuple_rule(rule: Tuple[str, str]) -> None:
|
|
77
|
+
if len(rule) != 2:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Tuple rule must have exactly 2 elements: (regex, replacement)"
|
|
80
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from . import anonymizer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RecursiveAnonymizer(anonymizer.Anonymizer):
|
|
8
|
+
"""Abstract base class for anonymizing sensitive data in various data structures.
|
|
9
|
+
|
|
10
|
+
This class provides a framework for recursively anonymizing text content within
|
|
11
|
+
nested data structures such as dictionaries, lists, and strings. Subclasses must
|
|
12
|
+
implement the anonymize_text() method to define the specific anonymization logic.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, max_depth: int = 10):
|
|
16
|
+
"""Initialize the Anonymizer with depth limiting.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
max_depth: Maximum recursion depth to prevent infinite loops when
|
|
20
|
+
processing deeply nested or circular data structures.
|
|
21
|
+
Defaults to 10.
|
|
22
|
+
"""
|
|
23
|
+
self.max_depth = max_depth
|
|
24
|
+
|
|
25
|
+
def anonymize(
|
|
26
|
+
self, data: anonymizer.AnonymizerDataType, **kwargs: Any
|
|
27
|
+
) -> anonymizer.AnonymizerDataType:
|
|
28
|
+
return self._recursive_anonymize(data, **kwargs)
|
|
29
|
+
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def _recursive_anonymize(
|
|
35
|
+
self,
|
|
36
|
+
data: anonymizer.AnonymizerDataType,
|
|
37
|
+
depth: int = 0,
|
|
38
|
+
field_name: Optional[str] = None,
|
|
39
|
+
**kwargs: Any,
|
|
40
|
+
) -> anonymizer.AnonymizerDataType:
|
|
41
|
+
if depth >= self.max_depth:
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
if field_name is None:
|
|
45
|
+
field_name = ""
|
|
46
|
+
|
|
47
|
+
if isinstance(data, str):
|
|
48
|
+
return self.anonymize_text(data, field_name=field_name, **kwargs)
|
|
49
|
+
elif isinstance(data, dict):
|
|
50
|
+
return {
|
|
51
|
+
key: self._recursive_anonymize(
|
|
52
|
+
value, depth + 1, field_name=f"{field_name}.{key}", **kwargs
|
|
53
|
+
)
|
|
54
|
+
for key, value in data.items()
|
|
55
|
+
}
|
|
56
|
+
elif isinstance(data, list):
|
|
57
|
+
return [
|
|
58
|
+
self._recursive_anonymize(
|
|
59
|
+
item, depth + 1, field_name=f"{field_name}.{i}", **kwargs
|
|
60
|
+
)
|
|
61
|
+
for i, item in enumerate(data)
|
|
62
|
+
]
|
|
63
|
+
else:
|
|
64
|
+
return data
|
opik/anonymizer/rules.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import re
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Rule(abc.ABC):
|
|
7
|
+
"""Abstract base class for text anonymization rules.
|
|
8
|
+
|
|
9
|
+
Rules define specific patterns or conditions for anonymizing sensitive
|
|
10
|
+
information in text. Subclasses must implement the apply() method to
|
|
11
|
+
define the anonymization logic.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def apply(self, text: str) -> str:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RegexRule(Rule):
|
|
20
|
+
"""A rule that uses regular expressions to find and replace patterns in text.
|
|
21
|
+
|
|
22
|
+
This rule compiles a regular expression pattern and applies it to input text,
|
|
23
|
+
replacing all matches with a specified replacement string.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, regex: str, replacement: str):
|
|
27
|
+
"""Initialize the regex rule with a pattern and replacement.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
regex: Regular expression pattern to match sensitive data.
|
|
31
|
+
replacement: String to replace matched patterns with.
|
|
32
|
+
"""
|
|
33
|
+
self.pattern = re.compile(regex)
|
|
34
|
+
self.replacement = replacement
|
|
35
|
+
|
|
36
|
+
def apply(self, text: str) -> str:
|
|
37
|
+
return self.pattern.sub(self.replacement, text)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FunctionRule(Rule):
|
|
41
|
+
"""A rule that applies a custom function to anonymize text.
|
|
42
|
+
|
|
43
|
+
This rule allows for flexible anonymization by accepting any callable
|
|
44
|
+
that takes a string as input and returns an anonymized string.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, func: Callable[[str], str]):
|
|
48
|
+
"""Initialize the function rule with a custom anonymization function.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
func: A callable that takes a string and returns an anonymized version.
|
|
52
|
+
"""
|
|
53
|
+
self.func = func
|
|
54
|
+
|
|
55
|
+
def apply(self, text: str) -> str:
|
|
56
|
+
return self.func(text)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import List, Any
|
|
2
|
+
|
|
3
|
+
from . import recursive_anonymizer, rules
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RulesAnonymizer(recursive_anonymizer.RecursiveAnonymizer):
|
|
7
|
+
"""An anonymizer that applies a list of rules sequentially to text data.
|
|
8
|
+
|
|
9
|
+
This class takes a list of Rule objects and applies them to
|
|
10
|
+
anonymize sensitive information in text.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, anonymizer_rules: List[rules.Rule], max_depth: int = 10):
|
|
14
|
+
"""Initialize the RulesAnonymizer with a list of rules.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
anonymizer_rules: List of Rule objects to apply for anonymization.
|
|
18
|
+
max_depth: Maximum recursion depth for nested data structures.
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(max_depth)
|
|
21
|
+
self.rules = anonymizer_rules
|
|
22
|
+
|
|
23
|
+
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
24
|
+
"""Apply all rules sequentially to the input text.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
data: The text to anonymize.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The anonymized text after applying all rules.
|
|
31
|
+
"""
|
|
32
|
+
result = data
|
|
33
|
+
for rule in self.rules:
|
|
34
|
+
result = rule.apply(result)
|
|
35
|
+
return result
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from typing import List
|
|
2
4
|
from opik.rest_api import OpikApi
|
|
3
5
|
import opik.exceptions as exceptions
|
|
4
6
|
from opik.message_processing import streamer
|
|
5
7
|
from . import dataset
|
|
6
8
|
from .. import experiment
|
|
9
|
+
from ..experiment import experiments_client
|
|
7
10
|
from ...rest_api.core.api_error import ApiError
|
|
8
11
|
|
|
9
12
|
|
|
@@ -60,6 +63,7 @@ def get_dataset_experiments(
|
|
|
60
63
|
dataset_id: str,
|
|
61
64
|
max_results: int,
|
|
62
65
|
streamer: streamer.Streamer,
|
|
66
|
+
experiments_client: experiments_client.ExperimentsClient,
|
|
63
67
|
) -> List[experiment.Experiment]:
|
|
64
68
|
page_size = 100
|
|
65
69
|
experiments: List[experiment.Experiment] = []
|
|
@@ -83,6 +87,7 @@ def get_dataset_experiments(
|
|
|
83
87
|
dataset_name=experiment_.dataset_name,
|
|
84
88
|
rest_client=rest_client,
|
|
85
89
|
streamer=streamer,
|
|
90
|
+
experiments_client=experiments_client,
|
|
86
91
|
)
|
|
87
92
|
)
|
|
88
93
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import logging
|
|
3
|
-
from typing import List, Optional
|
|
3
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
import opik.rest_api
|
|
6
5
|
from opik.message_processing.batching import sequence_splitter
|
|
7
6
|
from opik.message_processing import messages, streamer
|
|
8
7
|
from opik.rest_api import client as rest_api_client
|
|
9
|
-
from opik.rest_api
|
|
10
|
-
from . import experiment_item
|
|
11
|
-
from .. import constants, helpers
|
|
12
|
-
from ...api_objects.prompt import
|
|
8
|
+
from opik.rest_api import types as rest_api_types
|
|
9
|
+
from . import experiment_item, experiments_client
|
|
10
|
+
from .. import constants, helpers
|
|
11
|
+
from ...api_objects.prompt import base_prompt
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from opik.evaluation.metrics import score_result
|
|
13
15
|
|
|
14
16
|
LOGGER = logging.getLogger(__name__)
|
|
15
17
|
|
|
@@ -22,7 +24,8 @@ class Experiment:
|
|
|
22
24
|
dataset_name: str,
|
|
23
25
|
rest_client: rest_api_client.OpikApi,
|
|
24
26
|
streamer: streamer.Streamer,
|
|
25
|
-
|
|
27
|
+
experiments_client: experiments_client.ExperimentsClient,
|
|
28
|
+
prompts: Optional[List[base_prompt.BasePrompt]] = None,
|
|
26
29
|
) -> None:
|
|
27
30
|
self._id = id
|
|
28
31
|
self._name = name
|
|
@@ -30,6 +33,7 @@ class Experiment:
|
|
|
30
33
|
self._rest_client = rest_client
|
|
31
34
|
self._prompts = prompts
|
|
32
35
|
self._streamer = streamer
|
|
36
|
+
self._experiments_client = experiments_client
|
|
33
37
|
|
|
34
38
|
@property
|
|
35
39
|
def id(self) -> str:
|
|
@@ -59,7 +63,7 @@ class Experiment:
|
|
|
59
63
|
def experiments_rest_client(self) -> rest_api_client.ExperimentsClient:
|
|
60
64
|
return self._rest_client.experiments
|
|
61
65
|
|
|
62
|
-
def get_experiment_data(self) -> experiment_public.ExperimentPublic:
|
|
66
|
+
def get_experiment_data(self) -> rest_api_types.experiment_public.ExperimentPublic:
|
|
63
67
|
return self._rest_client.experiments.get_experiment_by_id(id=self.id)
|
|
64
68
|
|
|
65
69
|
def insert(
|
|
@@ -100,55 +104,48 @@ class Experiment:
|
|
|
100
104
|
|
|
101
105
|
def get_items(
|
|
102
106
|
self,
|
|
103
|
-
max_results: Optional[int] =
|
|
107
|
+
max_results: Optional[int] = 10000,
|
|
104
108
|
truncate: bool = False,
|
|
105
109
|
) -> List[experiment_item.ExperimentItemContent]:
|
|
106
110
|
"""
|
|
107
|
-
Retrieves and returns a list of experiment items
|
|
108
|
-
truncate the results for each batch.
|
|
109
|
-
|
|
110
|
-
This method streams experiment items from a backend service in chunks up to the specified `max_results`
|
|
111
|
-
or until the available items are exhausted. It handles batch-wise retrieval and parsing, ensuring the client
|
|
112
|
-
receives a list of `ExperimentItemContent` objects, while respecting the constraints on maximum retrieval size
|
|
113
|
-
from the backend. If truncation is enabled, the backend may return truncated details for each item.
|
|
111
|
+
Retrieves and returns a list of experiment items for this experiment.
|
|
114
112
|
|
|
115
113
|
Args:
|
|
116
|
-
max_results: Maximum number of experiment items to retrieve.
|
|
117
|
-
truncate: Whether to truncate the items returned by the backend.
|
|
114
|
+
max_results: Maximum number of experiment items to retrieve. Defaults to 10000 if not specified.
|
|
115
|
+
truncate: Whether to truncate the items returned by the backend. Defaults to False.
|
|
118
116
|
|
|
117
|
+
Returns:
|
|
118
|
+
List of ExperimentItemContent objects for this experiment.
|
|
119
119
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
items_stream = self._rest_client.experiments.stream_experiment_items(
|
|
132
|
-
experiment_name=self.name,
|
|
133
|
-
limit=current_batch_size,
|
|
134
|
-
last_retrieved_id=result[-1].id if len(result) > 0 else None,
|
|
135
|
-
truncate=truncate,
|
|
136
|
-
)
|
|
120
|
+
if max_results is None:
|
|
121
|
+
max_results = 10000 # TODO: remove this once we have a proper way to get all experiment items
|
|
122
|
+
|
|
123
|
+
return self._experiments_client.find_experiment_items_for_dataset(
|
|
124
|
+
dataset_name=self.dataset_name,
|
|
125
|
+
experiment_ids=[self.id],
|
|
126
|
+
truncate=truncate,
|
|
127
|
+
max_results=max_results,
|
|
128
|
+
)
|
|
137
129
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
130
|
+
def log_experiment_scores(
|
|
131
|
+
self,
|
|
132
|
+
score_results: List["score_result.ScoreResult"],
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Log experiment-level scores to the backend."""
|
|
135
|
+
experiment_scores: List[rest_api_types.ExperimentScore] = []
|
|
144
136
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
result.append(converted_item)
|
|
137
|
+
for score_result_ in score_results:
|
|
138
|
+
if score_result_.scoring_failed:
|
|
139
|
+
continue
|
|
150
140
|
|
|
151
|
-
|
|
152
|
-
|
|
141
|
+
experiment_score = rest_api_types.ExperimentScore(
|
|
142
|
+
name=score_result_.name,
|
|
143
|
+
value=score_result_.value,
|
|
144
|
+
)
|
|
145
|
+
experiment_scores.append(experiment_score)
|
|
153
146
|
|
|
154
|
-
|
|
147
|
+
if experiment_scores:
|
|
148
|
+
self._rest_client.experiments.update_experiment(
|
|
149
|
+
id=self.id,
|
|
150
|
+
experiment_scores=experiment_scores,
|
|
151
|
+
)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import logging
|
|
2
|
-
import opik.jsonable_encoder as jsonable_encoder
|
|
3
3
|
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
4
|
-
|
|
5
|
-
from
|
|
4
|
+
|
|
5
|
+
from opik import id_helpers
|
|
6
|
+
import opik.jsonable_encoder as jsonable_encoder
|
|
7
|
+
|
|
8
|
+
from ..prompt import base_prompt
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
LOGGER = logging.getLogger(__name__)
|
|
8
12
|
|
|
@@ -11,7 +15,7 @@ PromptVersion = Dict[str, str]
|
|
|
11
15
|
|
|
12
16
|
def build_metadata_and_prompt_versions(
|
|
13
17
|
experiment_config: Optional[Dict[str, Any]],
|
|
14
|
-
prompts: Optional[List[
|
|
18
|
+
prompts: Optional[List[base_prompt.BasePrompt]],
|
|
15
19
|
) -> Tuple[Optional[Dict[str, Any]], Optional[List[PromptVersion]]]:
|
|
16
20
|
prompt_versions: Optional[List[PromptVersion]] = None
|
|
17
21
|
|
|
@@ -37,9 +41,20 @@ def build_metadata_and_prompt_versions(
|
|
|
37
41
|
prompt_versions = []
|
|
38
42
|
experiment_config["prompts"] = {}
|
|
39
43
|
|
|
40
|
-
for
|
|
41
|
-
prompt_versions.append({"id":
|
|
42
|
-
|
|
44
|
+
for prompt_obj in prompts:
|
|
45
|
+
prompt_versions.append({"id": prompt_obj.__internal_api__version_id__})
|
|
46
|
+
# Use __internal_api__to_info_dict__() to get the prompt content in a consistent way
|
|
47
|
+
prompt_info = prompt_obj.__internal_api__to_info_dict__()
|
|
48
|
+
# Extract the template/messages from the version dict
|
|
49
|
+
if "version" in prompt_info:
|
|
50
|
+
if "template" in prompt_info["version"]:
|
|
51
|
+
experiment_config["prompts"][prompt_obj.name] = prompt_info[
|
|
52
|
+
"version"
|
|
53
|
+
]["template"]
|
|
54
|
+
elif "messages" in prompt_info["version"]:
|
|
55
|
+
experiment_config["prompts"][prompt_obj.name] = prompt_info[
|
|
56
|
+
"version"
|
|
57
|
+
]["messages"]
|
|
43
58
|
|
|
44
59
|
if experiment_config == {}:
|
|
45
60
|
return None, None
|
|
@@ -50,9 +65,9 @@ def build_metadata_and_prompt_versions(
|
|
|
50
65
|
|
|
51
66
|
|
|
52
67
|
def handle_prompt_args(
|
|
53
|
-
prompt: Optional[
|
|
54
|
-
prompts: Optional[List[
|
|
55
|
-
) -> Optional[List[
|
|
68
|
+
prompt: Optional[base_prompt.BasePrompt] = None,
|
|
69
|
+
prompts: Optional[List[base_prompt.BasePrompt]] = None,
|
|
70
|
+
) -> Optional[List[base_prompt.BasePrompt]]:
|
|
56
71
|
if prompts is not None and len(prompts) > 0 and prompt is not None:
|
|
57
72
|
LOGGER.warning(
|
|
58
73
|
"Arguments `prompt` and `prompts` are mutually exclusive, `prompts` will be used`."
|
|
@@ -63,3 +78,12 @@ def handle_prompt_args(
|
|
|
63
78
|
prompts = None
|
|
64
79
|
|
|
65
80
|
return prompts
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def generate_unique_experiment_name(experiment_name_prefix: Optional[str]) -> str:
|
|
84
|
+
if experiment_name_prefix is None:
|
|
85
|
+
return id_helpers.generate_random_alphanumeric_string(12)
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
f"{experiment_name_prefix}-{id_helpers.generate_random_alphanumeric_string(6)}"
|
|
89
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
from typing import Iterator, List
|
|
3
|
-
|
|
3
|
+
from typing import Optional
|
|
4
4
|
from . import opik_client
|
|
5
5
|
from ..message_processing import message_processors_chain
|
|
6
6
|
from ..message_processing.emulation import local_emulator_message_processor, models
|
|
@@ -33,9 +33,13 @@ class _LocalRecordingHandle:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@contextlib.contextmanager
|
|
36
|
-
def record_traces_locally(
|
|
36
|
+
def record_traces_locally(
|
|
37
|
+
client: Optional[opik_client.Opik] = None,
|
|
38
|
+
) -> Iterator[_LocalRecordingHandle]:
|
|
37
39
|
"""Enable local recording of traces/spans within the context.
|
|
38
40
|
|
|
41
|
+
Args:
|
|
42
|
+
client: Optional Opik client to use for recording. If not provided, the default session client will be used.
|
|
39
43
|
Usage:
|
|
40
44
|
with opik.record_traces_locally() as storage:
|
|
41
45
|
# your code that creates traces/spans
|
|
@@ -44,7 +48,8 @@ def record_traces_locally() -> Iterator[_LocalRecordingHandle]:
|
|
|
44
48
|
Yields a handle with `span_trees` and `trace_trees` properties that flush
|
|
45
49
|
the client before reading, ensuring all events are captured.
|
|
46
50
|
"""
|
|
47
|
-
client
|
|
51
|
+
if client is None:
|
|
52
|
+
client = opik_client.get_client_cached()
|
|
48
53
|
|
|
49
54
|
# Disallow nested/local concurrent recordings in the same process
|
|
50
55
|
existing_local = message_processors_chain.get_local_emulator_message_processor(
|