datajet-compass 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.
- datajet_compass/__init__.py +0 -0
- datajet_compass/config.py +11 -0
- datajet_compass/context.py +3 -0
- datajet_compass/decorators.py +50 -0
- datajet_compass/executors.py +76 -0
- datajet_compass/file_writter.py +37 -0
- datajet_compass/log_entry.py +115 -0
- datajet_compass/trace.py +69 -0
- datajet_compass-0.1.0.dist-info/METADATA +21 -0
- datajet_compass-0.1.0.dist-info/RECORD +12 -0
- datajet_compass-0.1.0.dist-info/WHEEL +5 -0
- datajet_compass-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dotenv import load_dotenv, find_dotenv
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# find_dotenv() walks UP the directory tree from cwd until it finds a .env file.
|
|
6
|
+
# This works regardless of where the annotators library is installed or imported from.
|
|
7
|
+
# usecwd=True ensures the search starts from the caller's working directory, not this file's location.
|
|
8
|
+
load_dotenv(find_dotenv(usecwd=True), override=False)
|
|
9
|
+
|
|
10
|
+
LOG_PATH = os.getenv("LOG_PATH", None)
|
|
11
|
+
ENABLE_ONLINE_COMPASS_EVAL = True if os.getenv("ENABLE_ONLINE_COMPASS_EVAL", "false").lower() in ["true", "yes", "1"] else False
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
|
|
4
|
+
from .config import ENABLE_ONLINE_COMPASS_EVAL
|
|
5
|
+
from .trace import Trace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _make_decorator(call_type) :
|
|
9
|
+
def decorator(func):
|
|
10
|
+
|
|
11
|
+
@functools.wraps(func)
|
|
12
|
+
def _sync_wrapper(*args, **kwargs):
|
|
13
|
+
if not ENABLE_ONLINE_COMPASS_EVAL:
|
|
14
|
+
return func(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
with Trace(call_type=call_type) as trace:
|
|
17
|
+
trace.start_trace(function_name=func.__name__, request={"args": args, "kwargs": kwargs})
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
result = func(*args, **kwargs)
|
|
21
|
+
return trace.consolidate_trace(output=result, status="SUCCESS")
|
|
22
|
+
except Exception as e:
|
|
23
|
+
return trace.consolidate_trace(status="FAILED", error=e)
|
|
24
|
+
|
|
25
|
+
@functools.wraps(func)
|
|
26
|
+
async def _async_wrapper(*args, **kwargs):
|
|
27
|
+
if not ENABLE_ONLINE_COMPASS_EVAL:
|
|
28
|
+
return await func(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
with Trace(call_type=call_type) as trace:
|
|
31
|
+
trace.start_trace(function_name=func.__name__, request={"args": args, "kwargs": kwargs})
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = await func(*args, **kwargs)
|
|
35
|
+
return trace.consolidate_trace(output=result, status="SUCCESS")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return trace.consolidate_trace(status="FAILED", error=e)
|
|
38
|
+
|
|
39
|
+
return _async_wrapper if asyncio.iscoroutinefunction(func) else _sync_wrapper
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Syntactic Sugar wrappers matching standard trace requirements
|
|
45
|
+
observe = _make_decorator("function")
|
|
46
|
+
|
|
47
|
+
observe_agent = _make_decorator("agent")
|
|
48
|
+
|
|
49
|
+
observe_tool = _make_decorator("tool")
|
|
50
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import multiprocessing
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
|
|
3
|
+
import contextvars
|
|
4
|
+
|
|
5
|
+
from .context import _state_ctx
|
|
6
|
+
from .log_entry import LogEntry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextThreadPoolExecutor(ThreadPoolExecutor):
|
|
10
|
+
"""Thread pool that automatically propagates ContextVar state to worker threads."""
|
|
11
|
+
|
|
12
|
+
def submit(self, fn, *args, **kwargs):
|
|
13
|
+
ctx = contextvars.copy_context()
|
|
14
|
+
return super().submit(ctx.run, fn, *args, **kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContextProcessPoolExecutor(ProcessPoolExecutor):
|
|
18
|
+
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self._manager = multiprocessing.Manager()
|
|
22
|
+
self._child_states = self._manager.list() # each slot holds ONE CallEntry dict
|
|
23
|
+
self._invocation_counter = 0
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
self._parent_state = _state_ctx.get()
|
|
27
|
+
self._serialized_parent = self._parent_state.to_object_dict()
|
|
28
|
+
except LookupError:
|
|
29
|
+
self._parent_state = None
|
|
30
|
+
self._serialized_parent = None
|
|
31
|
+
|
|
32
|
+
def submit(self, fn, *args, **kwargs):
|
|
33
|
+
self._invocation_counter += 1
|
|
34
|
+
invocation_order = self._invocation_counter
|
|
35
|
+
self._child_states.append({})
|
|
36
|
+
|
|
37
|
+
return super().submit(
|
|
38
|
+
_run_with_context,
|
|
39
|
+
fn,
|
|
40
|
+
self._serialized_parent,
|
|
41
|
+
self._child_states,
|
|
42
|
+
invocation_order,
|
|
43
|
+
*args,
|
|
44
|
+
**kwargs
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __exit__(self, *args):
|
|
48
|
+
result = super().__exit__(*args)
|
|
49
|
+
|
|
50
|
+
# All processes finished — each slot in _child_states is ONE LogEntry dict
|
|
51
|
+
if self._parent_state is not None:
|
|
52
|
+
for call_dict in self._child_states:
|
|
53
|
+
if call_dict:
|
|
54
|
+
entry = LogEntry.from_dict(call_dict)
|
|
55
|
+
self._parent_state.functionCalls.append(entry)
|
|
56
|
+
|
|
57
|
+
self._manager.shutdown()
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_with_context(fn, serialized_state, child_states_list, invocation_order, *args, **kwargs):
|
|
62
|
+
"""Runs inside each worker process."""
|
|
63
|
+
if serialized_state:
|
|
64
|
+
state = LogEntry.from_dict(serialized_state)
|
|
65
|
+
_state_ctx.set(state)
|
|
66
|
+
|
|
67
|
+
result = fn(*args, **kwargs)
|
|
68
|
+
|
|
69
|
+
if serialized_state:
|
|
70
|
+
# Always exactly 1 top-level child — store it directly, no loop needed
|
|
71
|
+
child_call = state.functionCalls[0]
|
|
72
|
+
child_call.order = invocation_order
|
|
73
|
+
child_states_list[invocation_order - 1] = child_call.to_object_dict()
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JSONLWriter:
|
|
7
|
+
"""
|
|
8
|
+
Writes annotation records to a JSONL file.
|
|
9
|
+
Each execution record is written as ONE JSON object per line.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, file_path: str, indent: int = 2):
|
|
13
|
+
"""
|
|
14
|
+
:param file_path: Path to the .jsonl file
|
|
15
|
+
:param indent: JSON indentation (default = 2 for readable output)
|
|
16
|
+
"""
|
|
17
|
+
self.file_path = file_path
|
|
18
|
+
self.indent = indent
|
|
19
|
+
|
|
20
|
+
def _serialize(self, obj: Any) -> Any:
|
|
21
|
+
# Any object with __dict__ (custom classes)
|
|
22
|
+
if hasattr(obj, "__dict__"):
|
|
23
|
+
return obj.__dict__
|
|
24
|
+
# Fallback — convert to string
|
|
25
|
+
return str(obj)
|
|
26
|
+
|
|
27
|
+
def write(self, record: Dict[str, Any]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Write a single execution record to the JSONL file.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
|
33
|
+
with open(self.file_path, "a", encoding="utf-8") as f:
|
|
34
|
+
f.write(
|
|
35
|
+
json.dumps(record, ensure_ascii=False, indent=self.indent, default=self._serialize)
|
|
36
|
+
+ "\n"
|
|
37
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from threading import Lock
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
class LogEntry:
|
|
6
|
+
date_fmt = "%Y-%m-%d %H:%M:%S"
|
|
7
|
+
|
|
8
|
+
def __init__(self, testcase_id=None, run_id=None, request=None, type=None, function_name=None, url=None, order=1):
|
|
9
|
+
self._lock = Lock()
|
|
10
|
+
self.call_counter = 1
|
|
11
|
+
|
|
12
|
+
self.testcase_id = testcase_id
|
|
13
|
+
self.run_id = run_id
|
|
14
|
+
self.name = function_name
|
|
15
|
+
self.type = type
|
|
16
|
+
self.target = url
|
|
17
|
+
self.order = order
|
|
18
|
+
self.input = request
|
|
19
|
+
self.output = None
|
|
20
|
+
self.status = "PROCESSING"
|
|
21
|
+
self.error = None
|
|
22
|
+
self.functionCalls = []
|
|
23
|
+
self.start_time = datetime.now().strftime(LogEntry.date_fmt)
|
|
24
|
+
self.end_time = None
|
|
25
|
+
|
|
26
|
+
def get_count(self):
|
|
27
|
+
with self._lock:
|
|
28
|
+
self.call_counter += 1
|
|
29
|
+
self.functionCalls.append({})
|
|
30
|
+
|
|
31
|
+
return self.call_counter - 1
|
|
32
|
+
|
|
33
|
+
def set_response(self, output=None, status=None, error=None):
|
|
34
|
+
self.end_time = datetime.now().strftime(LogEntry.date_fmt)
|
|
35
|
+
self.output = output
|
|
36
|
+
self.status = status
|
|
37
|
+
self.error = error
|
|
38
|
+
|
|
39
|
+
def to_dict(self):
|
|
40
|
+
|
|
41
|
+
start = datetime.strptime(self.start_time, LogEntry.date_fmt)
|
|
42
|
+
end = datetime.strptime(self.end_time, LogEntry.date_fmt)
|
|
43
|
+
duration = end - start
|
|
44
|
+
|
|
45
|
+
return_dict = {
|
|
46
|
+
"testcase_id": self.testcase_id,
|
|
47
|
+
"run_id": self.run_id,
|
|
48
|
+
"name": self.name,
|
|
49
|
+
"order": self.order,
|
|
50
|
+
"execution": {
|
|
51
|
+
"type": self.type,
|
|
52
|
+
"target": self.target,
|
|
53
|
+
"status": self.status,
|
|
54
|
+
"error": self.error,
|
|
55
|
+
"execution_time": f"{duration.total_seconds()} s"
|
|
56
|
+
},
|
|
57
|
+
"input": str(self.input),
|
|
58
|
+
"output": str(self.output),
|
|
59
|
+
"functionCall": [call.to_dict() for call in self.functionCalls if call.type == "function"],
|
|
60
|
+
"agentCall": [call.to_dict() for call in self.functionCalls if call.type == "agent"],
|
|
61
|
+
"toolCall": [call.to_dict() for call in self.functionCalls if call.type == "tool"]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
keys_to_remove = [
|
|
65
|
+
('testcase_id', self.testcase_id),
|
|
66
|
+
('run_id', self.run_id),
|
|
67
|
+
('order', self.order),
|
|
68
|
+
('target', self.target)
|
|
69
|
+
]
|
|
70
|
+
for key, value in keys_to_remove:
|
|
71
|
+
if value is None:
|
|
72
|
+
return_dict.pop(key, None)
|
|
73
|
+
|
|
74
|
+
return {k: v for k, v in return_dict.items() if v != []}
|
|
75
|
+
|
|
76
|
+
def to_object_dict(self):
|
|
77
|
+
return {
|
|
78
|
+
"testcase_id": self.testcase_id,
|
|
79
|
+
"run_id": self.run_id,
|
|
80
|
+
"type": self.type,
|
|
81
|
+
"target": self.target,
|
|
82
|
+
"name": self.name,
|
|
83
|
+
"order": self.order,
|
|
84
|
+
"status": self.status,
|
|
85
|
+
"error": self.error,
|
|
86
|
+
"start_time": self.start_time,
|
|
87
|
+
"end_time": self.end_time,
|
|
88
|
+
"input": str(self.input),
|
|
89
|
+
"output": str(self.output),
|
|
90
|
+
"functionCalls": [call.to_object_dict() for call in self.functionCalls]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'LogEntry':
|
|
95
|
+
data = dict(data)
|
|
96
|
+
calls_data = data.pop("functionCalls", [])
|
|
97
|
+
|
|
98
|
+
# to_object_dict fields -> __init__ fields
|
|
99
|
+
obj = cls(
|
|
100
|
+
testcase_id=data.get("testcase_id", None),
|
|
101
|
+
run_id=data.get("run_id", None),
|
|
102
|
+
request=data.get("request", None),
|
|
103
|
+
type=data.get("type", None),
|
|
104
|
+
function_name=data.get("name", None),
|
|
105
|
+
url=data.get("target", None),
|
|
106
|
+
order=data.get("order", 1)
|
|
107
|
+
)
|
|
108
|
+
# restore live state fields
|
|
109
|
+
obj.status = data["status"]
|
|
110
|
+
obj.error = data["error"]
|
|
111
|
+
obj.output = data["output"]
|
|
112
|
+
obj.start_time = data["start_time"]
|
|
113
|
+
obj.end_time = data["end_time"]
|
|
114
|
+
obj.functionCalls = [LogEntry.from_dict(c) for c in calls_data]
|
|
115
|
+
return obj
|
datajet_compass/trace.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from .context import _state_ctx
|
|
3
|
+
from .file_writter import JSONLWriter
|
|
4
|
+
from .log_entry import LogEntry
|
|
5
|
+
from .config import LOG_PATH, ENABLE_ONLINE_COMPASS_EVAL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Trace:
|
|
9
|
+
|
|
10
|
+
def __init__(self, call_type = "api"):
|
|
11
|
+
self.call_type = call_type
|
|
12
|
+
self.token = None
|
|
13
|
+
self.parent = None
|
|
14
|
+
self.current_state = None
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def start_trace(self, function_name = None, request = None, url = None):
|
|
18
|
+
|
|
19
|
+
if not ENABLE_ONLINE_COMPASS_EVAL:
|
|
20
|
+
return None, None
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
self.parent = _state_ctx.get()
|
|
24
|
+
except Exception as e:
|
|
25
|
+
self.parent = None
|
|
26
|
+
|
|
27
|
+
with threading.Lock():
|
|
28
|
+
testcase_id = request.get("testcase_id", None) if isinstance(request, dict) else None
|
|
29
|
+
run_id = request.get("run_id", None) if isinstance(request, dict) else None
|
|
30
|
+
|
|
31
|
+
# - 1 is added because a dummy of the current call has been added to the parent's functionCalls list on get_count()
|
|
32
|
+
order_count = self.parent.get_count() if self.parent else None
|
|
33
|
+
|
|
34
|
+
state = LogEntry(testcase_id=testcase_id, run_id=run_id, request=request, type=self.call_type, function_name=function_name, url=url, order=order_count)
|
|
35
|
+
|
|
36
|
+
if self.parent is not None:
|
|
37
|
+
self.parent.functionCalls[order_count - 1] = state
|
|
38
|
+
|
|
39
|
+
self.token = _state_ctx.set(state)
|
|
40
|
+
self.current_state = state
|
|
41
|
+
|
|
42
|
+
return self.token, self.current_state
|
|
43
|
+
|
|
44
|
+
def consolidate_trace(self,output=None, status=None, error=None):
|
|
45
|
+
|
|
46
|
+
if ENABLE_ONLINE_COMPASS_EVAL:
|
|
47
|
+
self.current_state.set_response(output=output, status=status, error=error)
|
|
48
|
+
|
|
49
|
+
if self.parent is None and self.current_state.testcase_id is not None and self.current_state.run_id is not None:
|
|
50
|
+
print(self.current_state.to_dict())
|
|
51
|
+
return {"actual_response": output if error is None else error, "trace": self.current_state.to_dict()}
|
|
52
|
+
elif self.parent is None and self.current_state.testcase_id is None and self.current_state.run_id is None and LOG_PATH is not None:
|
|
53
|
+
with self._lock:
|
|
54
|
+
writer = JSONLWriter(LOG_PATH)
|
|
55
|
+
writer.write(self.current_state.to_dict())
|
|
56
|
+
|
|
57
|
+
return output if error is None else error
|
|
58
|
+
|
|
59
|
+
def __enter__(self):
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
63
|
+
if self.token:
|
|
64
|
+
_state_ctx.reset(self.token)
|
|
65
|
+
self.token = None
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datajet-compass
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Observability library for AI agents and tools
|
|
5
|
+
Author-email: Basil K <Basil.Kalathodi@ibsplc.com>, Rose Baiju Malakaran <Rose.Malakaran@ibsplc.com>, Liana S <Liana.Salim@ibsplc.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/imaginebediscovered/datajet-compass
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# DataJet Compass
|
|
15
|
+
|
|
16
|
+
Observability library for AI agents and tools.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install datajet-compass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
datajet_compass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
datajet_compass/config.py,sha256=W7CtH0ph-Rcy1CjgSsjv9XwkjlkblJMysE1ftzUkvHw,550
|
|
3
|
+
datajet_compass/context.py,sha256=x6yFXkZINmzFGBhVRX1-X7KvA5F8dLbMXu1JhGFQvlk,76
|
|
4
|
+
datajet_compass/decorators.py,sha256=UJhjsEXNSMl_f7eUR98f4mfm9pNGLlX4S_vtVYAjflU,1820
|
|
5
|
+
datajet_compass/executors.py,sha256=_OAPi8p4AuxedtuOjqxJDPDQwr6-2c0GFs3nqtIyFy8,2547
|
|
6
|
+
datajet_compass/file_writter.py,sha256=6JY5e2dWWzx2OImxgH_OhMnvB97PgpH_-W9mKU_jPNM,1183
|
|
7
|
+
datajet_compass/log_entry.py,sha256=HeNqP5w5EEcnZW0G3K0dOVZ0oXCJmIw3hYOPcQ_UvTQ,4166
|
|
8
|
+
datajet_compass/trace.py,sha256=jrMykjDqOOCjgK8rlQuK6nQGqlBmBQdNjduRQoxmHqE,2740
|
|
9
|
+
datajet_compass-0.1.0.dist-info/METADATA,sha256=DRHA08ahbwInT4fcDrndK2CG9xcFC1JGPPWMkDxou0w,693
|
|
10
|
+
datajet_compass-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
datajet_compass-0.1.0.dist-info/top_level.txt,sha256=qOGTjNmNfjeSXcmqLs4sowDpq-5fDxnIUg5ejdEhO6g,16
|
|
12
|
+
datajet_compass-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
datajet_compass
|