modaic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of modaic might be problematic. Click here for more details.
- modaic/__init__.py +25 -0
- modaic/agents/rag_agent.py +33 -0
- modaic/agents/registry.py +84 -0
- modaic/auto_agent.py +228 -0
- modaic/context/__init__.py +34 -0
- modaic/context/base.py +1064 -0
- modaic/context/dtype_mapping.py +25 -0
- modaic/context/table.py +585 -0
- modaic/context/text.py +94 -0
- modaic/databases/__init__.py +35 -0
- modaic/databases/graph_database.py +269 -0
- modaic/databases/sql_database.py +355 -0
- modaic/databases/vector_database/__init__.py +12 -0
- modaic/databases/vector_database/benchmarks/baseline.py +123 -0
- modaic/databases/vector_database/benchmarks/common.py +48 -0
- modaic/databases/vector_database/benchmarks/fork.py +132 -0
- modaic/databases/vector_database/benchmarks/threaded.py +119 -0
- modaic/databases/vector_database/vector_database.py +722 -0
- modaic/databases/vector_database/vendors/milvus.py +408 -0
- modaic/databases/vector_database/vendors/mongodb.py +0 -0
- modaic/databases/vector_database/vendors/pinecone.py +0 -0
- modaic/databases/vector_database/vendors/qdrant.py +1 -0
- modaic/exceptions.py +38 -0
- modaic/hub.py +305 -0
- modaic/indexing.py +127 -0
- modaic/module_utils.py +341 -0
- modaic/observability.py +275 -0
- modaic/precompiled.py +429 -0
- modaic/query_language.py +321 -0
- modaic/storage/__init__.py +3 -0
- modaic/storage/file_store.py +239 -0
- modaic/storage/pickle_store.py +25 -0
- modaic/types.py +287 -0
- modaic/utils.py +21 -0
- modaic-0.1.0.dist-info/METADATA +281 -0
- modaic-0.1.0.dist-info/RECORD +39 -0
- modaic-0.1.0.dist-info/WHEEL +5 -0
- modaic-0.1.0.dist-info/licenses/LICENSE +31 -0
- modaic-0.1.0.dist-info/top_level.txt +1 -0
modaic/module_utils.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import sysconfig
|
|
7
|
+
import warnings
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
import tomlkit as tomlk
|
|
13
|
+
|
|
14
|
+
from .utils import compute_cache_dir
|
|
15
|
+
|
|
16
|
+
MODAIC_CACHE = compute_cache_dir()
|
|
17
|
+
AGENTS_CACHE = Path(MODAIC_CACHE) / "agents"
|
|
18
|
+
EDITABLE_MODE = os.getenv("EDITABLE_MODE", "false").lower() == "true"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_builtin(module_name: str) -> bool:
|
|
22
|
+
"""Check whether a module name refers to a built-in module.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
module_name: The fully qualified module name.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool: True if the module is a Python built-in.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
return module_name in sys.builtin_module_names
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_stdlib(module_name: str) -> bool:
|
|
35
|
+
"""Check whether a module belongs to the Python standard library.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
module_name: The fully qualified module name.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
bool: True if the module is part of the stdlib (including built-ins).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
spec = importlib.util.find_spec(module_name)
|
|
46
|
+
except ValueError:
|
|
47
|
+
return False
|
|
48
|
+
except Exception:
|
|
49
|
+
return False
|
|
50
|
+
if not spec:
|
|
51
|
+
return False
|
|
52
|
+
if spec.origin == "built-in":
|
|
53
|
+
return True
|
|
54
|
+
origin = spec.origin or ""
|
|
55
|
+
stdlib_dir = Path(sysconfig.get_paths()["stdlib"]).resolve()
|
|
56
|
+
try:
|
|
57
|
+
origin_path = Path(origin).resolve()
|
|
58
|
+
except OSError:
|
|
59
|
+
return False
|
|
60
|
+
return stdlib_dir in origin_path.parents or origin_path == stdlib_dir
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_builtin_or_frozen(mod: ModuleType) -> bool:
|
|
64
|
+
"""Check whether a module object is built-in or frozen.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
mod: The module object.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
bool: True if the module is built-in or frozen.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
spec = getattr(mod, "__spec__", None)
|
|
74
|
+
origin = getattr(spec, "origin", None)
|
|
75
|
+
name = getattr(mod, "__name__", None)
|
|
76
|
+
return (name in sys.builtin_module_names) or (origin in ("built-in", "frozen"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_internal_imports() -> Dict[str, ModuleType]:
|
|
80
|
+
"""Return only internal modules currently loaded in sys.modules.
|
|
81
|
+
|
|
82
|
+
Internal modules are defined as those not installed in site/dist packages
|
|
83
|
+
(covers virtualenv `.venv` cases as well).
|
|
84
|
+
|
|
85
|
+
If the environment variable `EDITABLE_MODE` is set to "true" (case-insensitive),
|
|
86
|
+
modules located under `src/modaic/` are also excluded.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
None
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dict[str, ModuleType]: Mapping of module names to module objects that are
|
|
93
|
+
not located under any "site-packages" or "dist-packages" directory.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
internal: Dict[str, ModuleType] = {}
|
|
97
|
+
|
|
98
|
+
seen: set[int] = set()
|
|
99
|
+
for name, module in list(sys.modules.items()):
|
|
100
|
+
if module is None:
|
|
101
|
+
continue
|
|
102
|
+
module_id = id(module)
|
|
103
|
+
if module_id in seen:
|
|
104
|
+
continue
|
|
105
|
+
seen.add(module_id)
|
|
106
|
+
|
|
107
|
+
if is_builtin_or_frozen(module):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
module_file = getattr(module, "__file__", None)
|
|
111
|
+
if not module_file:
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
module_path = Path(module_file).resolve()
|
|
115
|
+
except OSError:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if is_builtin(name) or is_stdlib(name):
|
|
119
|
+
continue
|
|
120
|
+
if is_external_package(module_path):
|
|
121
|
+
continue
|
|
122
|
+
if EDITABLE_MODE:
|
|
123
|
+
posix_path = module_path.as_posix().lower()
|
|
124
|
+
if "src/modaic" in posix_path:
|
|
125
|
+
continue
|
|
126
|
+
normalized_name = name
|
|
127
|
+
|
|
128
|
+
internal[normalized_name] = module
|
|
129
|
+
|
|
130
|
+
return internal
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def resolve_project_root() -> Path:
|
|
134
|
+
"""
|
|
135
|
+
Return the project root directory, must be a directory containing a pyproject.toml file.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
FileNotFoundError: If pyproject.toml is not found in the current directory.
|
|
139
|
+
"""
|
|
140
|
+
pyproject_path = Path("pyproject.toml")
|
|
141
|
+
if not pyproject_path.exists():
|
|
142
|
+
raise FileNotFoundError("pyproject.toml not found in current directory")
|
|
143
|
+
return pyproject_path.resolve().parent
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_path_ignored(target_path: Path, ignored_paths: list[Path]) -> bool:
|
|
147
|
+
"""Return True if target_path matches or is contained within any ignored path."""
|
|
148
|
+
try:
|
|
149
|
+
absolute_target = target_path.resolve()
|
|
150
|
+
except OSError:
|
|
151
|
+
return False
|
|
152
|
+
for ignored in ignored_paths:
|
|
153
|
+
if absolute_target == ignored:
|
|
154
|
+
return True
|
|
155
|
+
try:
|
|
156
|
+
absolute_target.relative_to(ignored)
|
|
157
|
+
return True
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def copy_module_layout(base_dir: Path, name_parts: list[str]) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Create ancestor package directories and ensure each contains an __init__.py file.
|
|
166
|
+
Example:
|
|
167
|
+
Given a base_dir of "/tmp/modaic" and name_parts of ["agent","indexer"],
|
|
168
|
+
creates the following layout:
|
|
169
|
+
| /tmp/modaic/
|
|
170
|
+
| | agent/
|
|
171
|
+
| | | __init__.py
|
|
172
|
+
| | indexer/
|
|
173
|
+
| | | __init__.py
|
|
174
|
+
"""
|
|
175
|
+
current = base_dir
|
|
176
|
+
for part in name_parts:
|
|
177
|
+
current = current / part
|
|
178
|
+
current.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
init_file = current / "__init__.py"
|
|
180
|
+
if not init_file.exists():
|
|
181
|
+
init_file.touch()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_external_package(path: Path) -> bool:
|
|
185
|
+
"""Return True if the path is under site-packages or dist-packages."""
|
|
186
|
+
parts = {p.lower() for p in path.parts}
|
|
187
|
+
return "site-packages" in parts or "dist-packages" in parts
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def init_agent_repo(repo_path: str, with_code: bool = True) -> Path:
|
|
191
|
+
"""Create a local repository staging directory for agent modules and files, excluding ignored files and folders."""
|
|
192
|
+
repo_dir = Path(AGENTS_CACHE) / repo_path
|
|
193
|
+
repo_dir.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
|
|
195
|
+
internal_imports = get_internal_imports()
|
|
196
|
+
ignored_paths = get_ignored_files()
|
|
197
|
+
|
|
198
|
+
seen_files: set[Path] = set()
|
|
199
|
+
|
|
200
|
+
readme_src = Path("README.md")
|
|
201
|
+
if readme_src.exists() and not is_path_ignored(readme_src, ignored_paths):
|
|
202
|
+
readme_dest = repo_dir / "README.md"
|
|
203
|
+
shutil.copy2(readme_src, readme_dest)
|
|
204
|
+
else:
|
|
205
|
+
warnings.warn("README.md not found in current directory. Please add one when pushing to the hub.", stacklevel=4)
|
|
206
|
+
|
|
207
|
+
if not with_code:
|
|
208
|
+
return repo_dir
|
|
209
|
+
|
|
210
|
+
for module_name, module in internal_imports.items():
|
|
211
|
+
module_file = getattr(module, "__file__", None)
|
|
212
|
+
if not module_file:
|
|
213
|
+
continue
|
|
214
|
+
try:
|
|
215
|
+
src_path = Path(module_file).resolve()
|
|
216
|
+
except OSError:
|
|
217
|
+
continue
|
|
218
|
+
if src_path.suffix != ".py":
|
|
219
|
+
continue
|
|
220
|
+
if is_path_ignored(src_path, ignored_paths):
|
|
221
|
+
continue
|
|
222
|
+
if src_path in seen_files:
|
|
223
|
+
continue
|
|
224
|
+
seen_files.add(src_path)
|
|
225
|
+
|
|
226
|
+
# Split modul_name to get the relative path
|
|
227
|
+
name_parts = module_name.split(".")
|
|
228
|
+
if src_path.name == "__init__.py":
|
|
229
|
+
copy_module_layout(repo_dir, name_parts)
|
|
230
|
+
dest_path = repo_dir.joinpath(*name_parts) / "__init__.py"
|
|
231
|
+
else:
|
|
232
|
+
if len(name_parts) > 1:
|
|
233
|
+
copy_module_layout(repo_dir, name_parts[:-1])
|
|
234
|
+
else:
|
|
235
|
+
repo_dir.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
# use the file name to name the file
|
|
237
|
+
dest_path = repo_dir.joinpath(*name_parts[:-1]) / src_path.name
|
|
238
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
shutil.copy2(src_path, dest_path)
|
|
240
|
+
return repo_dir
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def create_agent_repo(repo_path: str, with_code: bool = True) -> Path:
|
|
244
|
+
"""
|
|
245
|
+
Create a temporary directory inside the Modaic cache. Containing everything that will be pushed to the hub. This function adds the following files:
|
|
246
|
+
- All internal modules used to run the agent
|
|
247
|
+
- The pyproject.toml
|
|
248
|
+
- The README.md
|
|
249
|
+
"""
|
|
250
|
+
package_name = repo_path.split("/")[-1]
|
|
251
|
+
repo_dir = init_agent_repo(repo_path, with_code=with_code)
|
|
252
|
+
if with_code:
|
|
253
|
+
create_pyproject_toml(repo_dir, package_name)
|
|
254
|
+
|
|
255
|
+
return repo_dir
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_ignored_files() -> list[Path]:
|
|
259
|
+
"""Return a list of absolute Paths that should be excluded from staging."""
|
|
260
|
+
project_root = resolve_project_root()
|
|
261
|
+
pyproject_path = Path("pyproject.toml")
|
|
262
|
+
doc = tomlk.parse(pyproject_path.read_text(encoding="utf-8"))
|
|
263
|
+
|
|
264
|
+
# Safely get [tool.modaic.ignore]
|
|
265
|
+
ignore_table = (
|
|
266
|
+
doc.get("tool", {}) # [tool]
|
|
267
|
+
.get("modaic", {}) # [tool.modaic]
|
|
268
|
+
.get("ignore") # [tool.modaic.ignore]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if ignore_table is None or "files" not in ignore_table:
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
ignored: list[Path] = []
|
|
275
|
+
for entry in ignore_table["files"]:
|
|
276
|
+
try:
|
|
277
|
+
ignored.append((project_root / entry).resolve())
|
|
278
|
+
except OSError:
|
|
279
|
+
continue
|
|
280
|
+
return ignored
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def create_pyproject_toml(repo_dir: Path, package_name: str):
|
|
284
|
+
"""
|
|
285
|
+
Create a new pyproject.toml for the bundled agent in the temp directory.
|
|
286
|
+
"""
|
|
287
|
+
old = Path("pyproject.toml").read_text(encoding="utf-8")
|
|
288
|
+
new = repo_dir / "pyproject.toml"
|
|
289
|
+
|
|
290
|
+
doc_old = tomlk.parse(old)
|
|
291
|
+
doc_new = tomlk.document()
|
|
292
|
+
|
|
293
|
+
if "project" not in doc_old:
|
|
294
|
+
raise KeyError("No [project] table in old TOML")
|
|
295
|
+
doc_new["project"] = doc_old["project"]
|
|
296
|
+
doc_new["project"]["dependencies"] = get_filtered_dependencies(doc_old["project"]["dependencies"])
|
|
297
|
+
if "tool" in doc_old and "uv" in doc_old["tool"] and "sources" in doc_old["tool"]["uv"]:
|
|
298
|
+
doc_new["tool"] = {"uv": {"sources": doc_old["tool"]["uv"]["sources"]}}
|
|
299
|
+
warn_if_local(doc_new["tool"]["uv"]["sources"])
|
|
300
|
+
|
|
301
|
+
doc_new["project"]["name"] = package_name
|
|
302
|
+
|
|
303
|
+
with open(new, "w") as fp:
|
|
304
|
+
tomlk.dump(doc_new, fp)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_filtered_dependencies(dependencies: list[str]) -> list[str]:
|
|
308
|
+
"""
|
|
309
|
+
Get the dependencies that should be included in the bundled agent.
|
|
310
|
+
"""
|
|
311
|
+
pyproject_path = Path("pyproject.toml")
|
|
312
|
+
doc = tomlk.parse(pyproject_path.read_text(encoding="utf-8"))
|
|
313
|
+
|
|
314
|
+
# Safely get [tool.modaic.ignore]
|
|
315
|
+
ignore_table = (
|
|
316
|
+
doc.get("tool", {}) # [tool]
|
|
317
|
+
.get("modaic", {}) # [tool.modaic]
|
|
318
|
+
.get("ignore", {}) # [tool.modaic.ignore]
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if "dependencies" not in ignore_table:
|
|
322
|
+
return dependencies
|
|
323
|
+
|
|
324
|
+
ignored_dependencies = ignore_table["dependencies"]
|
|
325
|
+
if not ignored_dependencies:
|
|
326
|
+
return dependencies
|
|
327
|
+
pattern = re.compile(r"\b(" + "|".join(map(re.escape, ignored_dependencies)) + r")\b")
|
|
328
|
+
filtered_dependencies = [pkg for pkg in dependencies if not pattern.search(pkg)]
|
|
329
|
+
return filtered_dependencies
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def warn_if_local(sources: dict[str, dict]):
|
|
333
|
+
"""
|
|
334
|
+
Warn if the agent is bundled with a local package.
|
|
335
|
+
"""
|
|
336
|
+
for source, config in sources.items():
|
|
337
|
+
if "path" in config:
|
|
338
|
+
warnings.warn(
|
|
339
|
+
f"Bundling agent with local package {source} installed from {config['path']}. This is not recommended.",
|
|
340
|
+
stacklevel=5,
|
|
341
|
+
)
|
modaic/observability.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
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 opik
|
|
8
|
+
from opik import Opik, config
|
|
9
|
+
from typing_extensions import Concatenate, ParamSpec
|
|
10
|
+
|
|
11
|
+
from .utils import validate_project_name
|
|
12
|
+
|
|
13
|
+
P = ParamSpec("P") # params of the function
|
|
14
|
+
R = TypeVar("R") # return type of the function
|
|
15
|
+
T = TypeVar("T", bound="Trackable") # an instance of a class that inherits from Trackable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ModaicSettings:
|
|
20
|
+
"""Global settings for Modaic observability."""
|
|
21
|
+
|
|
22
|
+
tracing: bool = False
|
|
23
|
+
repo: Optional[str] = None
|
|
24
|
+
project: Optional[str] = None
|
|
25
|
+
base_url: str = "https://api.modaic.dev"
|
|
26
|
+
modaic_token: Optional[str] = None
|
|
27
|
+
default_tags: Dict[str, str] = field(default_factory=dict)
|
|
28
|
+
log_inputs: bool = True
|
|
29
|
+
log_outputs: bool = True
|
|
30
|
+
max_input_size: int = 10000
|
|
31
|
+
max_output_size: int = 10000
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# global settings instance
|
|
35
|
+
_settings = ModaicSettings()
|
|
36
|
+
_opik_client: Optional[Opik] = None
|
|
37
|
+
_configured = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def configure(
|
|
41
|
+
tracing: bool = True,
|
|
42
|
+
repo: Optional[str] = None,
|
|
43
|
+
project: Optional[str] = None,
|
|
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
|
+
repo: Default repository name (e.g., 'user/repo')
|
|
58
|
+
project: Default project name
|
|
59
|
+
base_url: Opik server URL
|
|
60
|
+
modaic_token: Authentication token for Opik
|
|
61
|
+
default_tags: Default tags to apply to all traces
|
|
62
|
+
log_inputs: Whether to log function inputs
|
|
63
|
+
log_outputs: Whether to log function outputs
|
|
64
|
+
max_input_size: Maximum size of logged inputs
|
|
65
|
+
max_output_size: Maximum size of logged outputs
|
|
66
|
+
**opik_kwargs: Additional arguments passed to opik.configure()
|
|
67
|
+
"""
|
|
68
|
+
global _settings, _opik_client, _configured
|
|
69
|
+
if project and not repo:
|
|
70
|
+
raise ValueError("You cannot specify a project without a repo")
|
|
71
|
+
|
|
72
|
+
# update global settings
|
|
73
|
+
_settings.tracing = tracing
|
|
74
|
+
_settings.repo = repo
|
|
75
|
+
_settings.project = project
|
|
76
|
+
_settings.base_url = base_url
|
|
77
|
+
_settings.modaic_token = modaic_token
|
|
78
|
+
_settings.default_tags = default_tags or {}
|
|
79
|
+
_settings.log_inputs = log_inputs
|
|
80
|
+
_settings.log_outputs = log_outputs
|
|
81
|
+
_settings.max_input_size = max_input_size
|
|
82
|
+
_settings.max_output_size = max_output_size
|
|
83
|
+
|
|
84
|
+
if tracing:
|
|
85
|
+
# configure Opik
|
|
86
|
+
opik_config = {"use_local": True, "url": base_url, "force": True, "automatic_approvals": True, **opik_kwargs}
|
|
87
|
+
|
|
88
|
+
opik.configure(**opik_config)
|
|
89
|
+
|
|
90
|
+
# create client with project if specified
|
|
91
|
+
project_name = f"{repo}-{project}" if project else repo
|
|
92
|
+
if project_name:
|
|
93
|
+
_opik_client = Opik(host=base_url, project_name=project_name)
|
|
94
|
+
|
|
95
|
+
config.update_session_config("track_disable", not tracing)
|
|
96
|
+
|
|
97
|
+
_configured = True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_effective_settings(
|
|
101
|
+
repo: Optional[str] = None, project: Optional[str] = None, tags: Optional[Dict[str, str]] = None
|
|
102
|
+
) -> Dict[str, Any]:
|
|
103
|
+
"""Get effective settings by merging global and local parameters."""
|
|
104
|
+
effective_repo = repo or _settings.repo
|
|
105
|
+
effective_project = (f"{repo}-{project}" if project else effective_repo) or _settings.project
|
|
106
|
+
|
|
107
|
+
# validate project name if provided
|
|
108
|
+
if effective_project:
|
|
109
|
+
validate_project_name(effective_project)
|
|
110
|
+
|
|
111
|
+
# merge tags
|
|
112
|
+
effective_tags = {**_settings.default_tags}
|
|
113
|
+
if tags:
|
|
114
|
+
effective_tags.update(tags)
|
|
115
|
+
|
|
116
|
+
return {"repo": effective_repo, "project": effective_project, "tags": effective_tags}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _truncate_data(data: Any, max_size: int) -> Any:
|
|
120
|
+
"""Truncate data if it exceeds max_size when serialized."""
|
|
121
|
+
try:
|
|
122
|
+
import json
|
|
123
|
+
|
|
124
|
+
serialized = json.dumps(data, default=str)
|
|
125
|
+
if len(serialized) > max_size:
|
|
126
|
+
return f"<Data truncated: {len(serialized)} chars>"
|
|
127
|
+
return data
|
|
128
|
+
except Exception:
|
|
129
|
+
# if serialization fails, convert to string and truncate
|
|
130
|
+
str_data = str(data)
|
|
131
|
+
if len(str_data) > max_size:
|
|
132
|
+
return str_data[:max_size] + "..."
|
|
133
|
+
return str_data
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def track( # noqa: ANN201
|
|
137
|
+
name: Optional[str] = None,
|
|
138
|
+
repo: Optional[str] = None,
|
|
139
|
+
project: Optional[str] = None,
|
|
140
|
+
tags: Optional[Dict[str, str]] = None,
|
|
141
|
+
span_type: str = "general",
|
|
142
|
+
capture_input: Optional[bool] = None,
|
|
143
|
+
capture_output: Optional[bool] = None,
|
|
144
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
145
|
+
**opik_kwargs,
|
|
146
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
147
|
+
"""Decorator to track function calls with Opik.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Custom name for the tracked operation
|
|
151
|
+
repo: Repository name (overrides global setting)
|
|
152
|
+
project: Project name (overrides global setting)
|
|
153
|
+
tags: Additional tags for this operation
|
|
154
|
+
span_type: Type of span ('general', 'tool', 'llm', 'guardrail')
|
|
155
|
+
capture_input: Whether to capture input (overrides global setting)
|
|
156
|
+
capture_output: Whether to capture output (overrides global setting)
|
|
157
|
+
metadata: Additional metadata
|
|
158
|
+
**opik_kwargs: Additional arguments passed to opik.track
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def decorator(func: Callable) -> Callable:
|
|
162
|
+
if not _settings.tracing:
|
|
163
|
+
return func
|
|
164
|
+
|
|
165
|
+
# get effective settings
|
|
166
|
+
settings = _get_effective_settings(repo, project, tags)
|
|
167
|
+
|
|
168
|
+
# determine capture settings
|
|
169
|
+
should_capture_input = capture_input if capture_input is not None else _settings.log_inputs
|
|
170
|
+
should_capture_output = capture_output if capture_output is not None else _settings.log_outputs
|
|
171
|
+
|
|
172
|
+
# build opik.track arguments
|
|
173
|
+
track_args: Dict[str, Any] = {
|
|
174
|
+
"type": span_type,
|
|
175
|
+
"capture_input": should_capture_input,
|
|
176
|
+
"capture_output": should_capture_output,
|
|
177
|
+
**opik_kwargs,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# add project if available
|
|
181
|
+
if settings["project"]:
|
|
182
|
+
track_args["project_name"] = settings["project"]
|
|
183
|
+
|
|
184
|
+
if name:
|
|
185
|
+
track_args["name"] = name
|
|
186
|
+
|
|
187
|
+
# add tags and metadata
|
|
188
|
+
if settings["tags"] or metadata:
|
|
189
|
+
combined_metadata = {**(metadata or {})}
|
|
190
|
+
if settings["tags"]:
|
|
191
|
+
combined_metadata["tags"] = settings["tags"]
|
|
192
|
+
if settings["repo"]:
|
|
193
|
+
combined_metadata["repo"] = settings["repo"]
|
|
194
|
+
track_args["metadata"] = combined_metadata
|
|
195
|
+
|
|
196
|
+
# apply opik.track decorator
|
|
197
|
+
# Return function with type annotations persisted for static type checking
|
|
198
|
+
return cast(Callable[P, R], opik.track(**track_args)(func))
|
|
199
|
+
|
|
200
|
+
return decorator
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class Trackable:
|
|
204
|
+
"""Base class for objects that support automatic tracking.
|
|
205
|
+
|
|
206
|
+
Manages the attributes repo, project, and commit for classes that subclass it.
|
|
207
|
+
All Modaic classes except PrecompiledAgent should inherit from this class.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, repo: Optional[str] = None, project: Optional[str] = None, commit: Optional[str] = None):
|
|
211
|
+
self.repo = repo
|
|
212
|
+
self.project = project
|
|
213
|
+
self.commit = commit
|
|
214
|
+
|
|
215
|
+
def set_repo_project(self, repo: Optional[str] = None, project: Optional[str] = None, trace: bool = True):
|
|
216
|
+
"""Update the repo and project for this trackable object."""
|
|
217
|
+
if repo is not None:
|
|
218
|
+
self.repo = repo
|
|
219
|
+
|
|
220
|
+
self.project = f"{self.repo}-{project}" if project else self.repo
|
|
221
|
+
|
|
222
|
+
# configure global tracing
|
|
223
|
+
if trace and (repo or project):
|
|
224
|
+
configure(tracing=trace, repo=repo or self.repo, project=project or self.project)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
MethodDecorator = Callable[
|
|
228
|
+
[Callable[Concatenate[T, P], R]],
|
|
229
|
+
Callable[Concatenate[T, P], R],
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def track_modaic_obj(func: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
234
|
+
"""Method decorator for Trackable objects to automatically track method calls.
|
|
235
|
+
|
|
236
|
+
Uses self.repo and self.project to automatically set repository and project
|
|
237
|
+
for modaic.track, then wraps the function with modaic.track.
|
|
238
|
+
|
|
239
|
+
Usage:
|
|
240
|
+
class Retriever(Trackable):
|
|
241
|
+
@track_modaic_obj
|
|
242
|
+
def retrieve(self, query: str):
|
|
243
|
+
...
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
@wraps(func)
|
|
247
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
248
|
+
# self should be a Trackable instance
|
|
249
|
+
# TODO: may want to get rid of this type check for hot paths
|
|
250
|
+
if not isinstance(self, Trackable):
|
|
251
|
+
raise ValueError("@track_modaic_obj can only be used on methods of Trackable subclasses")
|
|
252
|
+
|
|
253
|
+
# get repo and project from self
|
|
254
|
+
repo = getattr(self, "repo", None)
|
|
255
|
+
project = getattr(self, "project", None)
|
|
256
|
+
|
|
257
|
+
# check if tracking is enabled globally
|
|
258
|
+
if not _settings.tracing:
|
|
259
|
+
# 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
|
|
260
|
+
bound = cast(Callable[P, R], func.__get__(self, type(self)))
|
|
261
|
+
return bound(*args, **kwargs)
|
|
262
|
+
|
|
263
|
+
# create tracking decorator with automatic name generation
|
|
264
|
+
tracker = track(
|
|
265
|
+
name=f"{self.__class__.__name__}.{func.__name__}", repo=repo, project=project, span_type="general"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# apply tracking and call method
|
|
269
|
+
# type casts the 'track' decorator static type checking
|
|
270
|
+
tracked_func = cast(MethodDecorator, tracker)(func)
|
|
271
|
+
# 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
|
|
272
|
+
bound_tracked = cast(Callable[P, R], tracked_func.__get__(self, type(self)))
|
|
273
|
+
return bound_tracked(*args, **kwargs)
|
|
274
|
+
|
|
275
|
+
return cast(Callable[Concatenate[T, P], R], wrapper)
|