modaic 0.10.4__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,259 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from functools import wraps
5
+ from typing import Any, Callable, Dict, Optional, TypeVar, cast
6
+
7
+ import dspy
8
+ import opik
9
+ from opik import Opik, config
10
+ from opik.integrations.dspy.callback import OpikCallback
11
+ from typing_extensions import Concatenate, ParamSpec
12
+
13
+ from .utils import validate_project_name
14
+
15
+ P = ParamSpec("P") # params of the function
16
+ R = TypeVar("R") # return type of the function
17
+ T = TypeVar("T", bound="Trackable") # an instance of a class that inherits from Trackable
18
+
19
+
20
+ @dataclass
21
+ class ModaicSettings:
22
+ """Global settings for Modaic observability."""
23
+
24
+ tracing: bool = False
25
+ project: Optional[str] = None
26
+ base_url: str = "https://api.modaic.dev"
27
+ modaic_token: Optional[str] = None
28
+ default_tags: Dict[str, str] = field(default_factory=dict)
29
+ log_inputs: bool = True
30
+ log_outputs: bool = True
31
+ max_input_size: int = 10000
32
+ max_output_size: int = 10000
33
+
34
+
35
+ # global settings instance
36
+ _settings = ModaicSettings()
37
+ _opik_client: Optional[Opik] = None
38
+ _configured = False
39
+
40
+
41
+ def configure(
42
+ project: str,
43
+ tracing: bool = True,
44
+ base_url: str = "https://api.modaic.dev",
45
+ modaic_token: Optional[str] = None,
46
+ default_tags: Optional[Dict[str, str]] = None,
47
+ log_inputs: bool = True,
48
+ log_outputs: bool = True,
49
+ max_input_size: int = 10000,
50
+ max_output_size: int = 10000,
51
+ **opik_kwargs,
52
+ ) -> None:
53
+ """Configure Modaic observability settings globally.
54
+
55
+ Args:
56
+ tracing: Whether observability is enabled
57
+ project: Default project name
58
+ base_url: Opik server URL
59
+ modaic_token: Authentication token for Opik
60
+ default_tags: Default tags to apply to all traces
61
+ log_inputs: Whether to log function inputs
62
+ log_outputs: Whether to log function outputs
63
+ max_input_size: Maximum size of logged inputs
64
+ max_output_size: Maximum size of logged outputs
65
+ **opik_kwargs: Additional arguments passed to opik.configure()
66
+ """
67
+ global _settings, _opik_client, _configured
68
+
69
+ # update global settings
70
+ _settings.tracing = tracing
71
+ _settings.project = project
72
+ _settings.base_url = base_url
73
+ _settings.modaic_token = modaic_token
74
+ _settings.default_tags = default_tags or {}
75
+ _settings.log_inputs = log_inputs
76
+ _settings.log_outputs = log_outputs
77
+ _settings.max_input_size = max_input_size
78
+ _settings.max_output_size = max_output_size
79
+
80
+ if tracing:
81
+ # configure Opik
82
+ opik_config = {"use_local": True, "url": base_url, "force": True, "automatic_approvals": True, **opik_kwargs}
83
+
84
+ opik.configure(**opik_config)
85
+
86
+ _opik_client = Opik(host=base_url, project_name=project)
87
+ opik_callback = OpikCallback(project_name=project, log_graph=False)
88
+ dspy.configure(callbacks=[opik_callback])
89
+
90
+ config.update_session_config("track_disable", not tracing)
91
+
92
+ _configured = True
93
+
94
+
95
+ def _get_effective_settings(project: Optional[str] = None, tags: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
96
+ """Get effective settings by merging global and local parameters."""
97
+ effective_project = project if project else _settings.project
98
+
99
+ # validate project name if provided
100
+ if effective_project:
101
+ validate_project_name(effective_project)
102
+
103
+ # merge tags
104
+ effective_tags = {**_settings.default_tags}
105
+ if tags:
106
+ effective_tags.update(tags)
107
+
108
+ return {"project": effective_project, "tags": effective_tags}
109
+
110
+
111
+ def _truncate_data(data: Any, max_size: int) -> Any:
112
+ """Truncate data if it exceeds max_size when serialized."""
113
+ try:
114
+ import json
115
+
116
+ serialized = json.dumps(data, default=str)
117
+ if len(serialized) > max_size:
118
+ return f"<Data truncated: {len(serialized)} chars>"
119
+ return data
120
+ except Exception:
121
+ # if serialization fails, convert to string and truncate
122
+ str_data = str(data)
123
+ if len(str_data) > max_size:
124
+ return str_data[:max_size] + "..."
125
+ return str_data
126
+
127
+
128
+ def track( # noqa: ANN201
129
+ name: Optional[str] = None,
130
+ project: Optional[str] = None,
131
+ tags: Optional[Dict[str, str]] = None,
132
+ span_type: str = "general",
133
+ capture_input: Optional[bool] = None,
134
+ capture_output: Optional[bool] = None,
135
+ metadata: Optional[Dict[str, Any]] = None,
136
+ **opik_kwargs,
137
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
138
+ """Decorator to track function calls with Opik.
139
+
140
+ Args:
141
+ name: Custom name for the tracked operation
142
+ project: Project name (overrides global setting)
143
+ tags: Additional tags for this operation
144
+ span_type: Type of span ('general', 'tool', 'llm', 'guardrail')
145
+ capture_input: Whether to capture input (overrides global setting)
146
+ capture_output: Whether to capture output (overrides global setting)
147
+ metadata: Additional metadata
148
+ **opik_kwargs: Additional arguments passed to opik.track
149
+ """
150
+
151
+ def decorator(func: Callable) -> Callable:
152
+ if not _settings.tracing:
153
+ return func
154
+
155
+ # get effective settings
156
+ settings = _get_effective_settings(project, tags)
157
+
158
+ # determine capture settings
159
+ should_capture_input = capture_input if capture_input is not None else _settings.log_inputs
160
+ should_capture_output = capture_output if capture_output is not None else _settings.log_outputs
161
+
162
+ # build opik.track arguments
163
+ track_args: Dict[str, Any] = {
164
+ "type": span_type,
165
+ "capture_input": should_capture_input,
166
+ "capture_output": should_capture_output,
167
+ **opik_kwargs,
168
+ }
169
+
170
+ # add project if available
171
+ if settings["project"]:
172
+ track_args["project_name"] = settings["project"]
173
+
174
+ if name:
175
+ track_args["name"] = name
176
+
177
+ # add tags and metadata
178
+ if settings["tags"] or metadata:
179
+ combined_metadata = {**(metadata or {})}
180
+ if settings["tags"]:
181
+ combined_metadata["tags"] = settings["tags"]
182
+ track_args["metadata"] = combined_metadata
183
+
184
+ # apply opik.track decorator
185
+ # Return function with type annotations persisted for static type checking
186
+ return cast(Callable[P, R], opik.track(**track_args)(func))
187
+
188
+ return decorator
189
+
190
+
191
+ class Trackable:
192
+ """Base class for objects that support automatic tracking.
193
+
194
+ Manages the attributes project, and commit for classes that subclass it.
195
+ All Modaic classes except PrecompiledProgram should inherit from this class.
196
+ """
197
+
198
+ def __init__(
199
+ self,
200
+ project: Optional[str] = None,
201
+ commit: Optional[str] = None,
202
+ trace: bool = False,
203
+ ):
204
+ self.project = project
205
+ self.commit = commit
206
+ self.trace = trace
207
+
208
+ def set_project(self, project: Optional[str] = None, trace: bool = True):
209
+ """Update the project for this trackable object."""
210
+ self.project = project
211
+ self.trace = trace
212
+
213
+
214
+ MethodDecorator = Callable[
215
+ [Callable[Concatenate[T, P], R]],
216
+ Callable[Concatenate[T, P], R],
217
+ ]
218
+
219
+
220
+ def track_modaic_obj(func: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
221
+ """Method decorator for Trackable objects to automatically track method calls.
222
+
223
+ Uses self.project to automatically set project
224
+ for modaic.track, then wraps the function with modaic.track.
225
+
226
+ Usage:
227
+ class Retriever(Trackable):
228
+ @track_modaic_obj
229
+ def retrieve(self, query: str):
230
+ ...
231
+ """
232
+
233
+ @wraps(func)
234
+ def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
235
+ # self should be a Trackable instance
236
+ # TODO: may want to get rid of this type check for hot paths
237
+ if not isinstance(self, Trackable):
238
+ raise ValueError("@track_modaic_obj can only be used on methods of Trackable subclasses")
239
+
240
+ # get project from self
241
+ project = getattr(self, "project", None)
242
+
243
+ # check if tracking is enabled both globally and for this object
244
+ if not _settings.tracing or not self.trace:
245
+ # binds the method to self so it can be called with args and kwars, also type cast's it to callable with type vars for static type checking
246
+ bound = cast(Callable[P, R], func.__get__(self, type(self)))
247
+ return bound(*args, **kwargs)
248
+
249
+ # create tracking decorator with automatic name generation
250
+ tracker = track(name=f"{self.__class__.__name__}.{func.__name__}", project=project, span_type="general")
251
+
252
+ # apply tracking and call method
253
+ # type casts the 'track' decorator static type checking
254
+ tracked_func = cast(MethodDecorator, tracker)(func)
255
+ # binds the method to self so it can be called with args and kwars, also type cast's it to callable with type vars for static type checking
256
+ bound_tracked = cast(Callable[P, R], tracked_func.__get__(self, type(self)))
257
+ return bound_tracked(*args, **kwargs)
258
+
259
+ return cast(Callable[Concatenate[T, P], R], wrapper)