ragaai-catalyst 2.2.4b5__py3-none-any.whl → 2.2.5b2__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.
- ragaai_catalyst/__init__.py +0 -2
- ragaai_catalyst/dataset.py +59 -1
- ragaai_catalyst/tracers/agentic_tracing/tracers/main_tracer.py +5 -285
- ragaai_catalyst/tracers/agentic_tracing/utils/__init__.py +0 -2
- ragaai_catalyst/tracers/agentic_tracing/utils/create_dataset_schema.py +1 -1
- ragaai_catalyst/tracers/exporters/__init__.py +1 -2
- ragaai_catalyst/tracers/exporters/file_span_exporter.py +0 -1
- ragaai_catalyst/tracers/exporters/ragaai_trace_exporter.py +23 -1
- ragaai_catalyst/tracers/tracer.py +6 -186
- {ragaai_catalyst-2.2.4b5.dist-info → ragaai_catalyst-2.2.5b2.dist-info}/METADATA +1 -1
- {ragaai_catalyst-2.2.4b5.dist-info → ragaai_catalyst-2.2.5b2.dist-info}/RECORD +14 -45
- ragaai_catalyst/experiment.py +0 -486
- ragaai_catalyst/tracers/agentic_tracing/tests/FinancialAnalysisSystem.ipynb +0 -536
- ragaai_catalyst/tracers/agentic_tracing/tests/GameActivityEventPlanner.ipynb +0 -134
- ragaai_catalyst/tracers/agentic_tracing/tests/TravelPlanner.ipynb +0 -563
- ragaai_catalyst/tracers/agentic_tracing/tests/__init__.py +0 -0
- ragaai_catalyst/tracers/agentic_tracing/tests/ai_travel_agent.py +0 -197
- ragaai_catalyst/tracers/agentic_tracing/tests/unique_decorator_test.py +0 -172
- ragaai_catalyst/tracers/agentic_tracing/tracers/agent_tracer.py +0 -687
- ragaai_catalyst/tracers/agentic_tracing/tracers/base.py +0 -1319
- ragaai_catalyst/tracers/agentic_tracing/tracers/custom_tracer.py +0 -347
- ragaai_catalyst/tracers/agentic_tracing/tracers/langgraph_tracer.py +0 -0
- ragaai_catalyst/tracers/agentic_tracing/tracers/llm_tracer.py +0 -1182
- ragaai_catalyst/tracers/agentic_tracing/tracers/network_tracer.py +0 -288
- ragaai_catalyst/tracers/agentic_tracing/tracers/tool_tracer.py +0 -557
- ragaai_catalyst/tracers/agentic_tracing/tracers/user_interaction_tracer.py +0 -129
- ragaai_catalyst/tracers/agentic_tracing/upload/upload_local_metric.py +0 -74
- ragaai_catalyst/tracers/agentic_tracing/utils/api_utils.py +0 -21
- ragaai_catalyst/tracers/agentic_tracing/utils/generic.py +0 -32
- ragaai_catalyst/tracers/agentic_tracing/utils/get_user_trace_metrics.py +0 -28
- ragaai_catalyst/tracers/agentic_tracing/utils/span_attributes.py +0 -133
- ragaai_catalyst/tracers/agentic_tracing/utils/supported_llm_provider.toml +0 -34
- ragaai_catalyst/tracers/exporters/raga_exporter.py +0 -467
- ragaai_catalyst/tracers/langchain_callback.py +0 -821
- ragaai_catalyst/tracers/llamaindex_callback.py +0 -361
- ragaai_catalyst/tracers/llamaindex_instrumentation.py +0 -424
- ragaai_catalyst/tracers/upload_traces.py +0 -170
- ragaai_catalyst/tracers/utils/convert_langchain_callbacks_output.py +0 -62
- ragaai_catalyst/tracers/utils/convert_llama_instru_callback.py +0 -69
- ragaai_catalyst/tracers/utils/extraction_logic_llama_index.py +0 -74
- ragaai_catalyst/tracers/utils/langchain_tracer_extraction_logic.py +0 -82
- ragaai_catalyst/tracers/utils/rag_trace_json_converter.py +0 -403
- {ragaai_catalyst-2.2.4b5.dist-info → ragaai_catalyst-2.2.5b2.dist-info}/WHEEL +0 -0
- {ragaai_catalyst-2.2.4b5.dist-info → ragaai_catalyst-2.2.5b2.dist-info}/licenses/LICENSE +0 -0
- {ragaai_catalyst-2.2.4b5.dist-info → ragaai_catalyst-2.2.5b2.dist-info}/top_level.txt +0 -0
@@ -1,1319 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import os
|
3
|
-
from datetime import datetime
|
4
|
-
from pathlib import Path
|
5
|
-
from typing import List, Any, Dict, Optional
|
6
|
-
import uuid
|
7
|
-
import sys
|
8
|
-
import tempfile
|
9
|
-
import threading
|
10
|
-
import time
|
11
|
-
|
12
|
-
from ragaai_catalyst.tracers.agentic_tracing.upload.upload_local_metric import calculate_metric
|
13
|
-
from ragaai_catalyst import RagaAICatalyst
|
14
|
-
from ragaai_catalyst.tracers.agentic_tracing.data.data_structure import (
|
15
|
-
Trace,
|
16
|
-
Metadata,
|
17
|
-
SystemInfo,
|
18
|
-
Resources,
|
19
|
-
Component,
|
20
|
-
)
|
21
|
-
from ragaai_catalyst.tracers.agentic_tracing.utils.file_name_tracker import TrackName
|
22
|
-
from ragaai_catalyst.tracers.agentic_tracing.utils.zip_list_of_unique_files import zip_list_of_unique_files
|
23
|
-
from ragaai_catalyst.tracers.agentic_tracing.utils.span_attributes import SpanAttributes
|
24
|
-
from ragaai_catalyst.tracers.agentic_tracing.utils.system_monitor import SystemMonitor
|
25
|
-
from ragaai_catalyst.tracers.agentic_tracing.upload.trace_uploader import submit_upload_task, get_task_status, ensure_uploader_running
|
26
|
-
|
27
|
-
import logging
|
28
|
-
|
29
|
-
logger = logging.getLogger(__name__)
|
30
|
-
logging_level = (
|
31
|
-
logger.setLevel(logging.DEBUG) if os.getenv("DEBUG") == "1" else logging.INFO
|
32
|
-
)
|
33
|
-
|
34
|
-
|
35
|
-
class TracerJSONEncoder(json.JSONEncoder):
|
36
|
-
def default(self, obj):
|
37
|
-
if isinstance(obj, datetime):
|
38
|
-
return obj.isoformat()
|
39
|
-
if isinstance(obj, bytes):
|
40
|
-
try:
|
41
|
-
return obj.decode("utf-8")
|
42
|
-
except UnicodeDecodeError:
|
43
|
-
return str(obj) # Fallback to string representation
|
44
|
-
if hasattr(obj, "to_dict"): # Handle objects with to_dict method
|
45
|
-
return obj.to_dict()
|
46
|
-
if hasattr(obj, "__dict__"):
|
47
|
-
# Filter out None values and handle nested serialization
|
48
|
-
return {
|
49
|
-
k: v
|
50
|
-
for k, v in obj.__dict__.items()
|
51
|
-
if v is not None and not k.startswith("_")
|
52
|
-
}
|
53
|
-
try:
|
54
|
-
# Try to convert to a basic type
|
55
|
-
return str(obj)
|
56
|
-
except:
|
57
|
-
return None # Last resort: return None instead of failing
|
58
|
-
|
59
|
-
|
60
|
-
class BaseTracer:
|
61
|
-
def __init__(self, user_details):
|
62
|
-
self.user_details = user_details
|
63
|
-
self.project_name = self.user_details["project_name"]
|
64
|
-
self.dataset_name = self.user_details["dataset_name"]
|
65
|
-
self.project_id = self.user_details["project_id"]
|
66
|
-
self.trace_name = self.user_details["trace_name"]
|
67
|
-
self.base_url = self.user_details.get("base_url", RagaAICatalyst.BASE_URL) # Get base_url from user_details or fallback to default
|
68
|
-
self.visited_metrics = []
|
69
|
-
self.trace_metrics = []
|
70
|
-
|
71
|
-
# Initialize trace data
|
72
|
-
self.trace_id = None
|
73
|
-
self.start_time = None
|
74
|
-
self.components: List[Component] = []
|
75
|
-
self.file_tracker = TrackName()
|
76
|
-
self.span_attributes_dict = {}
|
77
|
-
|
78
|
-
self.interval_time = self.user_details['interval_time']
|
79
|
-
self.memory_usage_list = []
|
80
|
-
self.cpu_usage_list = []
|
81
|
-
self.disk_usage_list = []
|
82
|
-
self.network_usage_list = []
|
83
|
-
self.tracking_thread = None
|
84
|
-
self.tracking = False
|
85
|
-
self.system_monitor = None
|
86
|
-
self.gt = None
|
87
|
-
|
88
|
-
# For post processing of tracing file before uploading
|
89
|
-
self.post_processor = None
|
90
|
-
|
91
|
-
# For upload tracking
|
92
|
-
self.upload_task_id = None
|
93
|
-
|
94
|
-
# For backward compatibility
|
95
|
-
self._upload_tasks = []
|
96
|
-
self._is_uploading = False
|
97
|
-
self._upload_completed_callback = None
|
98
|
-
self.timeout = self.user_details.get("timeout", 120)
|
99
|
-
|
100
|
-
ensure_uploader_running()
|
101
|
-
|
102
|
-
def _get_system_info(self) -> SystemInfo:
|
103
|
-
return self.system_monitor.get_system_info()
|
104
|
-
|
105
|
-
def _get_resources(self) -> Resources:
|
106
|
-
return self.system_monitor.get_resources()
|
107
|
-
|
108
|
-
def _track_memory_usage(self):
|
109
|
-
self.memory_usage_list = []
|
110
|
-
while self.tracking:
|
111
|
-
usage = self.system_monitor.track_memory_usage()
|
112
|
-
self.memory_usage_list.append(usage)
|
113
|
-
try:
|
114
|
-
time.sleep(self.interval_time)
|
115
|
-
except Exception as e:
|
116
|
-
logger.warning(f"Sleep interrupted in memory tracking: {str(e)}")
|
117
|
-
|
118
|
-
def _track_cpu_usage(self):
|
119
|
-
self.cpu_usage_list = []
|
120
|
-
while self.tracking:
|
121
|
-
usage = self.system_monitor.track_cpu_usage(self.interval_time)
|
122
|
-
self.cpu_usage_list.append(usage)
|
123
|
-
try:
|
124
|
-
time.sleep(self.interval_time)
|
125
|
-
except Exception as e:
|
126
|
-
logger.warning(f"Sleep interrupted in CPU tracking: {str(e)}")
|
127
|
-
|
128
|
-
def _track_disk_usage(self):
|
129
|
-
self.disk_usage_list = []
|
130
|
-
while self.tracking:
|
131
|
-
usage = self.system_monitor.track_disk_usage()
|
132
|
-
self.disk_usage_list.append(usage)
|
133
|
-
try:
|
134
|
-
time.sleep(self.interval_time)
|
135
|
-
except Exception as e:
|
136
|
-
logger.warning(f"Sleep interrupted in disk tracking: {str(e)}")
|
137
|
-
|
138
|
-
def _track_network_usage(self):
|
139
|
-
self.network_usage_list = []
|
140
|
-
while self.tracking:
|
141
|
-
usage = self.system_monitor.track_network_usage()
|
142
|
-
self.network_usage_list.append(usage)
|
143
|
-
try:
|
144
|
-
time.sleep(self.interval_time)
|
145
|
-
except Exception as e:
|
146
|
-
logger.warning(f"Sleep interrupted in network tracking: {str(e)}")
|
147
|
-
|
148
|
-
def register_post_processor(self, post_processor_func):
|
149
|
-
"""
|
150
|
-
Register a post-processing function that will be called after trace generation.
|
151
|
-
|
152
|
-
Args:
|
153
|
-
post_processor_func (callable): A function that takes a trace JSON file path as input
|
154
|
-
and returns a processed trace JSON file path.
|
155
|
-
The function signature should be:
|
156
|
-
def post_processor_func(original_trace_json_path: os.PathLike) -> os.PathLike
|
157
|
-
"""
|
158
|
-
if not callable(post_processor_func):
|
159
|
-
raise TypeError("post_processor_func must be a callable")
|
160
|
-
self.post_processor = post_processor_func
|
161
|
-
logger.debug("Post-processor function registered successfully in BaseTracer")
|
162
|
-
|
163
|
-
def start(self):
|
164
|
-
"""Initialize a new trace"""
|
165
|
-
self.tracking = True
|
166
|
-
self.trace_id = str(uuid.uuid4())
|
167
|
-
self.file_tracker.trace_main_file()
|
168
|
-
self.system_monitor = SystemMonitor(self.trace_id)
|
169
|
-
threading.Thread(target=self._track_memory_usage).start()
|
170
|
-
threading.Thread(target=self._track_cpu_usage).start()
|
171
|
-
threading.Thread(target=self._track_disk_usage).start()
|
172
|
-
threading.Thread(target=self._track_network_usage).start()
|
173
|
-
|
174
|
-
# Reset metrics
|
175
|
-
self.visited_metrics = []
|
176
|
-
self.trace_metrics = []
|
177
|
-
|
178
|
-
metadata = Metadata(
|
179
|
-
cost={},
|
180
|
-
tokens={},
|
181
|
-
system_info=self._get_system_info(),
|
182
|
-
resources=self._get_resources(),
|
183
|
-
)
|
184
|
-
|
185
|
-
# Get the start time
|
186
|
-
self.start_time = datetime.now().astimezone().isoformat()
|
187
|
-
|
188
|
-
self.data_key = [
|
189
|
-
{
|
190
|
-
"start_time": datetime.now().astimezone().isoformat(),
|
191
|
-
"end_time": "",
|
192
|
-
"spans": self.components,
|
193
|
-
}
|
194
|
-
]
|
195
|
-
|
196
|
-
self.trace = Trace(
|
197
|
-
id=self.trace_id,
|
198
|
-
trace_name=self.trace_name,
|
199
|
-
project_name=self.project_name,
|
200
|
-
start_time=datetime.now().astimezone().isoformat(),
|
201
|
-
end_time="", # Will be set when trace is stopped
|
202
|
-
metadata=metadata,
|
203
|
-
data=self.data_key,
|
204
|
-
replays={"source": None},
|
205
|
-
metrics=[] # Initialize empty metrics list
|
206
|
-
)
|
207
|
-
|
208
|
-
def on_upload_completed(self, callback_fn):
|
209
|
-
"""
|
210
|
-
Register a callback function to be called when all uploads are completed.
|
211
|
-
For backward compatibility - simulates the old callback mechanism.
|
212
|
-
|
213
|
-
Args:
|
214
|
-
callback_fn: A function that takes a single argument (the tracer instance)
|
215
|
-
"""
|
216
|
-
self._upload_completed_callback = callback_fn
|
217
|
-
|
218
|
-
# Check for status periodically and call callback when complete
|
219
|
-
def check_status_and_callback():
|
220
|
-
if self.upload_task_id:
|
221
|
-
status = self.get_upload_status()
|
222
|
-
if status.get("status") in ["completed", "failed"]:
|
223
|
-
self._is_uploading = False
|
224
|
-
# Execute callback
|
225
|
-
try:
|
226
|
-
if self._upload_completed_callback:
|
227
|
-
self._upload_completed_callback(self)
|
228
|
-
except Exception as e:
|
229
|
-
logger.error(f"Error in upload completion callback: {e}")
|
230
|
-
return
|
231
|
-
|
232
|
-
# Schedule next check
|
233
|
-
threading.Timer(5.0, check_status_and_callback).start()
|
234
|
-
|
235
|
-
# Start status checking if we already have a task
|
236
|
-
if self.upload_task_id:
|
237
|
-
threading.Timer(5.0, check_status_and_callback).start()
|
238
|
-
|
239
|
-
return self
|
240
|
-
|
241
|
-
def wait_for_uploads(self, timeout=None):
|
242
|
-
"""
|
243
|
-
Wait for all async uploads to complete.
|
244
|
-
This provides backward compatibility with the old API.
|
245
|
-
|
246
|
-
Args:
|
247
|
-
timeout: Maximum time to wait in seconds (None means wait indefinitely)
|
248
|
-
|
249
|
-
Returns:
|
250
|
-
True if all uploads completed successfully, False otherwise
|
251
|
-
"""
|
252
|
-
if not self.upload_task_id:
|
253
|
-
return True
|
254
|
-
|
255
|
-
start_time = time.time()
|
256
|
-
while True:
|
257
|
-
# Check if timeout expired
|
258
|
-
if timeout is not None and time.time() - start_time > timeout:
|
259
|
-
logger.warning(f"Upload wait timed out after {timeout} seconds")
|
260
|
-
return False
|
261
|
-
|
262
|
-
# Get current status
|
263
|
-
status = self.get_upload_status()
|
264
|
-
if status.get("status") == "completed":
|
265
|
-
return True
|
266
|
-
elif status.get("status") == "failed":
|
267
|
-
logger.error(f"Upload failed: {status.get('error')}")
|
268
|
-
return False
|
269
|
-
elif status.get("status") == "unknown":
|
270
|
-
logger.warning("Upload task not found, assuming completed")
|
271
|
-
return True
|
272
|
-
|
273
|
-
# Sleep before checking again
|
274
|
-
time.sleep(1.0)
|
275
|
-
|
276
|
-
def stop(self):
|
277
|
-
"""Stop the trace and save to JSON file, then submit to background uploader"""
|
278
|
-
if hasattr(self, "trace"):
|
279
|
-
# Set end times
|
280
|
-
self.trace.data[0]["end_time"] = datetime.now().astimezone().isoformat()
|
281
|
-
self.trace.end_time = datetime.now().astimezone().isoformat()
|
282
|
-
|
283
|
-
# Stop tracking metrics
|
284
|
-
self.tracking = False
|
285
|
-
|
286
|
-
# Process and aggregate metrics
|
287
|
-
self._process_resource_metrics()
|
288
|
-
|
289
|
-
# Process trace spans
|
290
|
-
self.trace = self._change_span_ids_to_int(self.trace)
|
291
|
-
self.trace = self._change_agent_input_output(self.trace)
|
292
|
-
# self.trace = self._extract_cost_tokens(self.trace)
|
293
|
-
|
294
|
-
# Create traces directory and prepare file paths
|
295
|
-
self.traces_dir = tempfile.gettempdir()
|
296
|
-
filename = self.trace.id + ".json"
|
297
|
-
filepath = f"{self.traces_dir}/{filename}"
|
298
|
-
|
299
|
-
# Process source files
|
300
|
-
list_of_unique_files = self.file_tracker.get_unique_files()
|
301
|
-
hash_id, zip_path = zip_list_of_unique_files(
|
302
|
-
list_of_unique_files, output_dir=self.traces_dir
|
303
|
-
)
|
304
|
-
self.trace.metadata.system_info.source_code = hash_id
|
305
|
-
|
306
|
-
# Prepare trace data for saving
|
307
|
-
trace_data = self.trace.to_dict()
|
308
|
-
trace_data["metrics"] = self.trace_metrics
|
309
|
-
cleaned_trace_data = self._clean_trace(trace_data)
|
310
|
-
cleaned_trace_data = self._extract_cost_tokens(cleaned_trace_data)
|
311
|
-
|
312
|
-
# Add interactions
|
313
|
-
interactions = self.format_interactions()
|
314
|
-
cleaned_trace_data["workflow"] = interactions["workflow"]
|
315
|
-
|
316
|
-
# Save trace data to file
|
317
|
-
with open(filepath, "w") as f:
|
318
|
-
json.dump(cleaned_trace_data, f, cls=TracerJSONEncoder, indent=2)
|
319
|
-
|
320
|
-
logger.info("Traces saved successfully.")
|
321
|
-
logger.debug(f"Trace saved to {filepath}")
|
322
|
-
|
323
|
-
# Apply post-processor if registered
|
324
|
-
if self.post_processor is not None:
|
325
|
-
try:
|
326
|
-
filepath = self.post_processor(filepath)
|
327
|
-
logger.debug(f"Post-processor applied successfully in BaseTracer, new path: {filepath}")
|
328
|
-
except Exception as e:
|
329
|
-
logger.error(f"Error in post-processing in BaseTracer: {e}")
|
330
|
-
|
331
|
-
# Make sure uploader process is available
|
332
|
-
ensure_uploader_running()
|
333
|
-
|
334
|
-
logger.debug("Base URL used for uploading: {}".format(self.base_url))
|
335
|
-
# Submit to background process for uploading using futures
|
336
|
-
self.upload_task_id = submit_upload_task(
|
337
|
-
filepath=filepath,
|
338
|
-
hash_id=hash_id,
|
339
|
-
zip_path=zip_path,
|
340
|
-
project_name=self.project_name,
|
341
|
-
project_id=self.project_id,
|
342
|
-
dataset_name=self.dataset_name,
|
343
|
-
user_details=self.user_details,
|
344
|
-
base_url=self.base_url,
|
345
|
-
timeout=self.timeout
|
346
|
-
)
|
347
|
-
|
348
|
-
# For backward compatibility
|
349
|
-
self._is_uploading = True
|
350
|
-
|
351
|
-
# Start checking for completion if a callback is registered
|
352
|
-
if self._upload_completed_callback:
|
353
|
-
# Start a thread to check status and call callback when complete
|
354
|
-
def check_status_and_callback():
|
355
|
-
status = self.get_upload_status()
|
356
|
-
if status.get("status") in ["completed", "failed"]:
|
357
|
-
self._is_uploading = False
|
358
|
-
# Execute callback
|
359
|
-
try:
|
360
|
-
self._upload_completed_callback(self)
|
361
|
-
except Exception as e:
|
362
|
-
logger.error(f"Error in upload completion callback: {e}")
|
363
|
-
return
|
364
|
-
|
365
|
-
# Check again after a delay
|
366
|
-
threading.Timer(5.0, check_status_and_callback).start()
|
367
|
-
|
368
|
-
# Start checking
|
369
|
-
threading.Timer(5.0, check_status_and_callback).start()
|
370
|
-
|
371
|
-
logger.info(f"Submitted upload task with ID: {self.upload_task_id}")
|
372
|
-
|
373
|
-
# Cleanup local resources
|
374
|
-
self.components = []
|
375
|
-
self.file_tracker.reset()
|
376
|
-
|
377
|
-
def get_upload_status(self):
|
378
|
-
"""
|
379
|
-
Get the status of the upload task.
|
380
|
-
|
381
|
-
Returns:
|
382
|
-
dict: Status information
|
383
|
-
"""
|
384
|
-
if not self.upload_task_id:
|
385
|
-
return {"status": "not_started", "message": "No upload has been initiated"}
|
386
|
-
|
387
|
-
return get_task_status(self.upload_task_id)
|
388
|
-
|
389
|
-
def _process_resource_metrics(self):
|
390
|
-
"""Process and aggregate all resource metrics"""
|
391
|
-
# Process memory metrics
|
392
|
-
self.trace.metadata.resources.memory.values = self.memory_usage_list
|
393
|
-
|
394
|
-
# Process CPU metrics
|
395
|
-
self.trace.metadata.resources.cpu.values = self.cpu_usage_list
|
396
|
-
|
397
|
-
# Process network and disk metrics
|
398
|
-
network_uploads, network_downloads = 0, 0
|
399
|
-
disk_read, disk_write = 0, 0
|
400
|
-
|
401
|
-
# Handle cases where lists might have different lengths
|
402
|
-
min_len = min(len(self.network_usage_list), len(self.disk_usage_list)) if self.network_usage_list and self.disk_usage_list else 0
|
403
|
-
for i in range(min_len):
|
404
|
-
network_usage = self.network_usage_list[i]
|
405
|
-
disk_usage = self.disk_usage_list[i]
|
406
|
-
|
407
|
-
# Safely get network usage values with defaults of 0
|
408
|
-
network_uploads += network_usage.get('uploads', 0) or 0
|
409
|
-
network_downloads += network_usage.get('downloads', 0) or 0
|
410
|
-
|
411
|
-
# Safely get disk usage values with defaults of 0
|
412
|
-
disk_read += disk_usage.get('disk_read', 0) or 0
|
413
|
-
disk_write += disk_usage.get('disk_write', 0) or 0
|
414
|
-
|
415
|
-
# Set aggregate values
|
416
|
-
disk_list_len = len(self.disk_usage_list)
|
417
|
-
self.trace.metadata.resources.disk.read = [disk_read / disk_list_len if disk_list_len > 0 else 0]
|
418
|
-
self.trace.metadata.resources.disk.write = [disk_write / disk_list_len if disk_list_len > 0 else 0]
|
419
|
-
|
420
|
-
network_list_len = len(self.network_usage_list)
|
421
|
-
self.trace.metadata.resources.network.uploads = [
|
422
|
-
network_uploads / network_list_len if network_list_len > 0 else 0]
|
423
|
-
self.trace.metadata.resources.network.downloads = [
|
424
|
-
network_downloads / network_list_len if network_list_len > 0 else 0]
|
425
|
-
|
426
|
-
# Set interval times
|
427
|
-
self.trace.metadata.resources.cpu.interval = float(self.interval_time)
|
428
|
-
self.trace.metadata.resources.memory.interval = float(self.interval_time)
|
429
|
-
self.trace.metadata.resources.disk.interval = float(self.interval_time)
|
430
|
-
self.trace.metadata.resources.network.interval = float(self.interval_time)
|
431
|
-
|
432
|
-
def add_component(self, component: Component):
|
433
|
-
"""Add a component to the trace"""
|
434
|
-
self.components.append(component)
|
435
|
-
|
436
|
-
def __enter__(self):
|
437
|
-
self.start()
|
438
|
-
return self
|
439
|
-
|
440
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
441
|
-
self.stop()
|
442
|
-
|
443
|
-
def _process_children(self, children_list, parent_id, current_id):
|
444
|
-
"""Helper function to process children recursively."""
|
445
|
-
for child in children_list:
|
446
|
-
child["id"] = current_id
|
447
|
-
child["parent_id"] = parent_id
|
448
|
-
current_id += 1
|
449
|
-
# Recursively process nested children if they exist
|
450
|
-
if "children" in child["data"]:
|
451
|
-
current_id = self._process_children(child["data"]["children"], child["id"], current_id)
|
452
|
-
return current_id
|
453
|
-
|
454
|
-
def _change_span_ids_to_int(self, trace):
|
455
|
-
id, parent_id = 1, 0
|
456
|
-
for span in trace.data[0]["spans"]:
|
457
|
-
span.id = id
|
458
|
-
span.parent_id = parent_id
|
459
|
-
id += 1
|
460
|
-
if span.type == "agent" and "children" in span.data:
|
461
|
-
id = self._process_children(span.data["children"], span.id, id)
|
462
|
-
return trace
|
463
|
-
|
464
|
-
def _change_agent_input_output(self, trace):
|
465
|
-
for span in trace.data[0]["spans"]:
|
466
|
-
if span.type == "agent":
|
467
|
-
childrens = span.data["children"]
|
468
|
-
span.data["input"] = None
|
469
|
-
span.data["output"] = None
|
470
|
-
if childrens:
|
471
|
-
# Find first non-null input going forward
|
472
|
-
for child in childrens:
|
473
|
-
if "data" not in child:
|
474
|
-
continue
|
475
|
-
input_data = child["data"].get("input")
|
476
|
-
|
477
|
-
if input_data:
|
478
|
-
span.data["input"] = (
|
479
|
-
input_data["args"]
|
480
|
-
if hasattr(input_data, "args")
|
481
|
-
else input_data
|
482
|
-
)
|
483
|
-
break
|
484
|
-
|
485
|
-
# Find first non-null output going backward
|
486
|
-
for child in reversed(childrens):
|
487
|
-
if "data" not in child:
|
488
|
-
continue
|
489
|
-
output_data = child["data"].get("output")
|
490
|
-
|
491
|
-
if output_data and output_data != "" and output_data != "None":
|
492
|
-
span.data["output"] = output_data
|
493
|
-
break
|
494
|
-
return trace
|
495
|
-
|
496
|
-
def _extract_cost_tokens(self, trace):
|
497
|
-
cost = {}
|
498
|
-
tokens = {}
|
499
|
-
|
500
|
-
def process_span_info(info):
|
501
|
-
if not isinstance(info, dict):
|
502
|
-
return
|
503
|
-
cost_info = info.get("cost", {})
|
504
|
-
for key, value in cost_info.items():
|
505
|
-
if key not in cost:
|
506
|
-
cost[key] = 0
|
507
|
-
cost[key] += value
|
508
|
-
token_info = info.get("tokens", {})
|
509
|
-
for key, value in token_info.items():
|
510
|
-
if key not in tokens:
|
511
|
-
tokens[key] = 0
|
512
|
-
tokens[key] += value
|
513
|
-
|
514
|
-
def process_spans(spans):
|
515
|
-
for span in spans:
|
516
|
-
# Get span type, handling both span objects and dictionaries
|
517
|
-
span_type = span.type if hasattr(span, 'type') else span.get('type')
|
518
|
-
span_info = span.info if hasattr(span, 'info') else span.get('info', {})
|
519
|
-
span_data = span.data if hasattr(span, 'data') else span.get('data', {})
|
520
|
-
|
521
|
-
# Process direct LLM spans
|
522
|
-
if span_type == "llm":
|
523
|
-
process_span_info(span_info)
|
524
|
-
# Process agent spans recursively
|
525
|
-
elif span_type == "agent":
|
526
|
-
# Process LLM children in the current agent span
|
527
|
-
children = span_data.get("children", [])
|
528
|
-
for child in children:
|
529
|
-
child_type = child.get("type")
|
530
|
-
if child_type == "llm":
|
531
|
-
process_span_info(child.get("info", {}))
|
532
|
-
# Recursively process nested agent spans
|
533
|
-
elif child_type == "agent":
|
534
|
-
process_spans([child])
|
535
|
-
|
536
|
-
process_spans(trace["data"][0]["spans"])
|
537
|
-
trace["metadata"].cost = cost
|
538
|
-
trace["metadata"].tokens = tokens
|
539
|
-
trace["metadata"].total_cost = cost.get("total_cost", 0)
|
540
|
-
trace["metadata"].total_tokens = tokens.get("total_tokens", 0)
|
541
|
-
return trace
|
542
|
-
|
543
|
-
def _clean_trace(self, trace):
|
544
|
-
# Convert span to dict if it has to_dict method
|
545
|
-
def _to_dict_if_needed(obj):
|
546
|
-
if hasattr(obj, "to_dict"):
|
547
|
-
return obj.to_dict()
|
548
|
-
return obj
|
549
|
-
|
550
|
-
def deduplicate_spans(spans):
|
551
|
-
seen_llm_spans = {} # Dictionary to track unique LLM spans
|
552
|
-
unique_spans = []
|
553
|
-
|
554
|
-
for span in spans:
|
555
|
-
# Convert span to dictionary if needed
|
556
|
-
span_dict = _to_dict_if_needed(span)
|
557
|
-
|
558
|
-
# Skip spans without hash_id
|
559
|
-
if "hash_id" not in span_dict:
|
560
|
-
continue
|
561
|
-
|
562
|
-
if span_dict.get("type") == "llm":
|
563
|
-
# Create a unique key based on hash_id, input, and output
|
564
|
-
span_key = (
|
565
|
-
span_dict.get("hash_id"),
|
566
|
-
str(span_dict.get("data", {}).get("input")),
|
567
|
-
str(span_dict.get("data", {}).get("output")),
|
568
|
-
)
|
569
|
-
|
570
|
-
# Check if we've seen this span before
|
571
|
-
if span_key not in seen_llm_spans:
|
572
|
-
seen_llm_spans[span_key] = True
|
573
|
-
unique_spans.append(span)
|
574
|
-
else:
|
575
|
-
# If we have interactions in the current span, replace the existing one
|
576
|
-
current_interactions = span_dict.get("interactions", [])
|
577
|
-
if current_interactions:
|
578
|
-
# Find and replace the existing span with this one that has interactions
|
579
|
-
for i, existing_span in enumerate(unique_spans):
|
580
|
-
existing_dict = (
|
581
|
-
existing_span
|
582
|
-
if isinstance(existing_span, dict)
|
583
|
-
else existing_span.__dict__
|
584
|
-
)
|
585
|
-
if (
|
586
|
-
existing_dict.get("hash_id")
|
587
|
-
== span_dict.get("hash_id")
|
588
|
-
and str(existing_dict.get("data", {}).get("input"))
|
589
|
-
== str(span_dict.get("data", {}).get("input"))
|
590
|
-
and str(existing_dict.get("data", {}).get("output"))
|
591
|
-
== str(span_dict.get("data", {}).get("output"))
|
592
|
-
):
|
593
|
-
unique_spans[i] = span
|
594
|
-
break
|
595
|
-
|
596
|
-
else:
|
597
|
-
# For non-LLM spans, process their children if they exist
|
598
|
-
if "data" in span_dict and "children" in span_dict["data"]:
|
599
|
-
children = span_dict["data"]["children"]
|
600
|
-
# Filter and deduplicate children
|
601
|
-
filtered_children = deduplicate_spans(children)
|
602
|
-
if isinstance(span, dict):
|
603
|
-
span["data"]["children"] = filtered_children
|
604
|
-
else:
|
605
|
-
span.data["children"] = filtered_children
|
606
|
-
unique_spans.append(span)
|
607
|
-
|
608
|
-
# Process spans to update model information for LLM spans with same name
|
609
|
-
llm_spans_by_name = {}
|
610
|
-
for i, span in enumerate(unique_spans):
|
611
|
-
span_dict = span if isinstance(span, dict) else span.__dict__
|
612
|
-
|
613
|
-
if span_dict.get('type') == 'llm':
|
614
|
-
span_name = span_dict.get('name')
|
615
|
-
if span_name:
|
616
|
-
if span_name not in llm_spans_by_name:
|
617
|
-
llm_spans_by_name[span_name] = []
|
618
|
-
llm_spans_by_name[span_name].append((i, span_dict))
|
619
|
-
|
620
|
-
# Update model information for spans with same name
|
621
|
-
for spans_with_same_name in llm_spans_by_name.values():
|
622
|
-
if len(spans_with_same_name) > 1:
|
623
|
-
# Check if any span has non-default model
|
624
|
-
has_custom_model = any(
|
625
|
-
span[1].get('info', {}).get('model') != 'default'
|
626
|
-
for span in spans_with_same_name
|
627
|
-
)
|
628
|
-
|
629
|
-
# If we have a custom model, update all default models to 'custom'
|
630
|
-
if has_custom_model:
|
631
|
-
for idx, span_dict in spans_with_same_name:
|
632
|
-
if span_dict.get('info', {}).get('model') == 'default':
|
633
|
-
if isinstance(unique_spans[idx], dict):
|
634
|
-
if 'info' not in unique_spans[idx]:
|
635
|
-
unique_spans[idx]['info'] = {}
|
636
|
-
# unique_spans[idx]['info']['model'] = 'custom'
|
637
|
-
unique_spans[idx]['type'] = 'custom'
|
638
|
-
else:
|
639
|
-
if not hasattr(unique_spans[idx], 'info'):
|
640
|
-
unique_spans[idx].info = {}
|
641
|
-
# unique_spans[idx].info['model'] = 'custom'
|
642
|
-
unique_spans[idx].type = 'custom'
|
643
|
-
|
644
|
-
return unique_spans
|
645
|
-
|
646
|
-
# Remove any spans without hash ids
|
647
|
-
for data in trace.get("data", []):
|
648
|
-
if "spans" in data:
|
649
|
-
# First filter out spans without hash_ids, then deduplicate
|
650
|
-
data["spans"] = deduplicate_spans(data["spans"])
|
651
|
-
|
652
|
-
return trace
|
653
|
-
|
654
|
-
def add_tags(self, tags: List[str]):
|
655
|
-
raise NotImplementedError
|
656
|
-
|
657
|
-
def _process_child_interactions(self, child, interaction_id, interactions):
|
658
|
-
"""
|
659
|
-
Helper method to process child interactions recursively.
|
660
|
-
|
661
|
-
Args:
|
662
|
-
child (dict): The child span to process
|
663
|
-
interaction_id (int): Current interaction ID
|
664
|
-
interactions (list): List of interactions to append to
|
665
|
-
|
666
|
-
Returns:
|
667
|
-
int: Next interaction ID to use
|
668
|
-
"""
|
669
|
-
child_type = child.get("type")
|
670
|
-
|
671
|
-
if child_type == "tool":
|
672
|
-
# Tool call start
|
673
|
-
interactions.append(
|
674
|
-
{
|
675
|
-
"id": str(interaction_id),
|
676
|
-
"span_id": child.get("id"),
|
677
|
-
"interaction_type": "tool_call_start",
|
678
|
-
"name": child.get("name"),
|
679
|
-
"content": {
|
680
|
-
"parameters": [
|
681
|
-
child.get("data", {}).get("input", {}).get("args"),
|
682
|
-
child.get("data", {}).get("input", {}).get("kwargs"),
|
683
|
-
]
|
684
|
-
},
|
685
|
-
"timestamp": child.get("start_time"),
|
686
|
-
"error": child.get("error"),
|
687
|
-
}
|
688
|
-
)
|
689
|
-
interaction_id += 1
|
690
|
-
|
691
|
-
# Tool call end
|
692
|
-
interactions.append(
|
693
|
-
{
|
694
|
-
"id": str(interaction_id),
|
695
|
-
"span_id": child.get("id"),
|
696
|
-
"interaction_type": "tool_call_end",
|
697
|
-
"name": child.get("name"),
|
698
|
-
"content": {
|
699
|
-
"returns": child.get("data", {}).get("output"),
|
700
|
-
},
|
701
|
-
"timestamp": child.get("end_time"),
|
702
|
-
"error": child.get("error"),
|
703
|
-
}
|
704
|
-
)
|
705
|
-
interaction_id += 1
|
706
|
-
|
707
|
-
elif child_type == "llm":
|
708
|
-
interactions.append(
|
709
|
-
{
|
710
|
-
"id": str(interaction_id),
|
711
|
-
"span_id": child.get("id"),
|
712
|
-
"interaction_type": "llm_call_start",
|
713
|
-
"name": child.get("name"),
|
714
|
-
"content": {
|
715
|
-
"prompt": child.get("data", {}).get("input"),
|
716
|
-
},
|
717
|
-
"timestamp": child.get("start_time"),
|
718
|
-
"error": child.get("error"),
|
719
|
-
}
|
720
|
-
)
|
721
|
-
interaction_id += 1
|
722
|
-
|
723
|
-
interactions.append(
|
724
|
-
{
|
725
|
-
"id": str(interaction_id),
|
726
|
-
"span_id": child.get("id"),
|
727
|
-
"interaction_type": "llm_call_end",
|
728
|
-
"name": child.get("name"),
|
729
|
-
"content": {"response": child.get("data", {}).get("output")},
|
730
|
-
"timestamp": child.get("end_time"),
|
731
|
-
"error": child.get("error"),
|
732
|
-
}
|
733
|
-
)
|
734
|
-
interaction_id += 1
|
735
|
-
|
736
|
-
elif child_type == "agent":
|
737
|
-
interactions.append(
|
738
|
-
{
|
739
|
-
"id": str(interaction_id),
|
740
|
-
"span_id": child.get("id"),
|
741
|
-
"interaction_type": "agent_call_start",
|
742
|
-
"name": child.get("name"),
|
743
|
-
"content": None,
|
744
|
-
"timestamp": child.get("start_time"),
|
745
|
-
"error": child.get("error"),
|
746
|
-
}
|
747
|
-
)
|
748
|
-
interaction_id += 1
|
749
|
-
|
750
|
-
# Process nested children recursively
|
751
|
-
if "children" in child.get("data", {}):
|
752
|
-
for nested_child in child["data"]["children"]:
|
753
|
-
interaction_id = self._process_child_interactions(
|
754
|
-
nested_child, interaction_id, interactions
|
755
|
-
)
|
756
|
-
|
757
|
-
interactions.append(
|
758
|
-
{
|
759
|
-
"id": str(interaction_id),
|
760
|
-
"span_id": child.get("id"),
|
761
|
-
"interaction_type": "agent_call_end",
|
762
|
-
"name": child.get("name"),
|
763
|
-
"content": child.get("data", {}).get("output"),
|
764
|
-
"timestamp": child.get("end_time"),
|
765
|
-
"error": child.get("error"),
|
766
|
-
}
|
767
|
-
)
|
768
|
-
interaction_id += 1
|
769
|
-
|
770
|
-
else:
|
771
|
-
interactions.append(
|
772
|
-
{
|
773
|
-
"id": str(interaction_id),
|
774
|
-
"span_id": child.get("id"),
|
775
|
-
"interaction_type": f"{child_type}_call_start",
|
776
|
-
"name": child.get("name"),
|
777
|
-
"content": child.get("data", {}),
|
778
|
-
"timestamp": child.get("start_time"),
|
779
|
-
"error": child.get("error"),
|
780
|
-
}
|
781
|
-
)
|
782
|
-
interaction_id += 1
|
783
|
-
|
784
|
-
interactions.append(
|
785
|
-
{
|
786
|
-
"id": str(interaction_id),
|
787
|
-
"span_id": child.get("id"),
|
788
|
-
"interaction_type": f"{child_type}_call_end",
|
789
|
-
"name": child.get("name"),
|
790
|
-
"content": child.get("data", {}),
|
791
|
-
"timestamp": child.get("end_time"),
|
792
|
-
"error": child.get("error"),
|
793
|
-
}
|
794
|
-
)
|
795
|
-
interaction_id += 1
|
796
|
-
|
797
|
-
# Process additional interactions and network calls
|
798
|
-
if "interactions" in child:
|
799
|
-
for interaction in child["interactions"]:
|
800
|
-
interaction["id"] = str(interaction_id)
|
801
|
-
interaction["span_id"] = child.get("id")
|
802
|
-
interaction["error"] = None
|
803
|
-
interactions.append(interaction)
|
804
|
-
interaction_id += 1
|
805
|
-
|
806
|
-
if "network_calls" in child:
|
807
|
-
for child_network_call in child["network_calls"]:
|
808
|
-
network_call = {}
|
809
|
-
network_call["id"] = str(interaction_id)
|
810
|
-
network_call["span_id"] = child.get("id")
|
811
|
-
network_call["interaction_type"] = "network_call"
|
812
|
-
network_call["name"] = None
|
813
|
-
network_call["content"] = {
|
814
|
-
"request": {
|
815
|
-
"url": child_network_call.get("url"),
|
816
|
-
"method": child_network_call.get("method"),
|
817
|
-
"headers": child_network_call.get("headers"),
|
818
|
-
},
|
819
|
-
"response": {
|
820
|
-
"status_code": child_network_call.get("status_code"),
|
821
|
-
"headers": child_network_call.get("response_headers"),
|
822
|
-
"body": child_network_call.get("response_body"),
|
823
|
-
},
|
824
|
-
}
|
825
|
-
network_call["timestamp"] = child_network_call.get("start_time")
|
826
|
-
network_call["error"] = child_network_call.get("error")
|
827
|
-
interactions.append(network_call)
|
828
|
-
interaction_id += 1
|
829
|
-
|
830
|
-
return interaction_id
|
831
|
-
|
832
|
-
def format_interactions(self) -> dict:
|
833
|
-
"""
|
834
|
-
Format interactions from trace data into a standardized format.
|
835
|
-
Returns a dictionary containing formatted interactions based on trace data.
|
836
|
-
|
837
|
-
The function processes spans from self.trace and formats them into interactions
|
838
|
-
of various types including: agent_start, agent_end, input, output, tool_call_start,
|
839
|
-
tool_call_end, llm_call, file_read, file_write, network_call.
|
840
|
-
|
841
|
-
Returns:
|
842
|
-
dict: A dictionary with "workflow" key containing a list of interactions
|
843
|
-
sorted by timestamp.
|
844
|
-
"""
|
845
|
-
interactions = []
|
846
|
-
interaction_id = 1
|
847
|
-
|
848
|
-
if not hasattr(self, "trace") or not self.trace.data:
|
849
|
-
return {"workflow": []}
|
850
|
-
|
851
|
-
for span in self.trace.data[0]["spans"]:
|
852
|
-
# Process agent spans
|
853
|
-
if span.type == "agent":
|
854
|
-
# Add agent_start interaction
|
855
|
-
interactions.append(
|
856
|
-
{
|
857
|
-
"id": str(interaction_id),
|
858
|
-
"span_id": span.id,
|
859
|
-
"interaction_type": "agent_call_start",
|
860
|
-
"name": span.name,
|
861
|
-
"content": None,
|
862
|
-
"timestamp": span.start_time,
|
863
|
-
"error": span.error,
|
864
|
-
}
|
865
|
-
)
|
866
|
-
interaction_id += 1
|
867
|
-
|
868
|
-
# Process children of agent recursively
|
869
|
-
if "children" in span.data:
|
870
|
-
for child in span.data["children"]:
|
871
|
-
interaction_id = self._process_child_interactions(
|
872
|
-
child, interaction_id, interactions
|
873
|
-
)
|
874
|
-
|
875
|
-
# Add agent_end interaction
|
876
|
-
interactions.append(
|
877
|
-
{
|
878
|
-
"id": str(interaction_id),
|
879
|
-
"span_id": span.id,
|
880
|
-
"interaction_type": "agent_call_end",
|
881
|
-
"name": span.name,
|
882
|
-
"content": span.data.get("output"),
|
883
|
-
"timestamp": span.end_time,
|
884
|
-
"error": span.error,
|
885
|
-
}
|
886
|
-
)
|
887
|
-
interaction_id += 1
|
888
|
-
|
889
|
-
elif span.type == "tool":
|
890
|
-
interactions.append(
|
891
|
-
{
|
892
|
-
"id": str(interaction_id),
|
893
|
-
"span_id": span.id,
|
894
|
-
"interaction_type": "tool_call_start",
|
895
|
-
"name": span.name,
|
896
|
-
"content": {
|
897
|
-
"prompt": span.data.get("input"),
|
898
|
-
"response": span.data.get("output"),
|
899
|
-
},
|
900
|
-
"timestamp": span.start_time,
|
901
|
-
"error": span.error,
|
902
|
-
}
|
903
|
-
)
|
904
|
-
interaction_id += 1
|
905
|
-
|
906
|
-
interactions.append(
|
907
|
-
{
|
908
|
-
"id": str(interaction_id),
|
909
|
-
"span_id": span.id,
|
910
|
-
"interaction_type": "tool_call_end",
|
911
|
-
"name": span.name,
|
912
|
-
"content": {
|
913
|
-
"prompt": span.data.get("input"),
|
914
|
-
"response": span.data.get("output"),
|
915
|
-
},
|
916
|
-
"timestamp": span.end_time,
|
917
|
-
"error": span.error,
|
918
|
-
}
|
919
|
-
)
|
920
|
-
interaction_id += 1
|
921
|
-
|
922
|
-
elif span.type == "llm":
|
923
|
-
interactions.append(
|
924
|
-
{
|
925
|
-
"id": str(interaction_id),
|
926
|
-
"span_id": span.id,
|
927
|
-
"interaction_type": "llm_call_start",
|
928
|
-
"name": span.name,
|
929
|
-
"content": {
|
930
|
-
"prompt": span.data.get("input"),
|
931
|
-
},
|
932
|
-
"timestamp": span.start_time,
|
933
|
-
"error": span.error,
|
934
|
-
}
|
935
|
-
)
|
936
|
-
interaction_id += 1
|
937
|
-
|
938
|
-
interactions.append(
|
939
|
-
{
|
940
|
-
"id": str(interaction_id),
|
941
|
-
"span_id": span.id,
|
942
|
-
"interaction_type": "llm_call_end",
|
943
|
-
"name": span.name,
|
944
|
-
"content": {"response": span.data.get("output")},
|
945
|
-
"timestamp": span.end_time,
|
946
|
-
"error": span.error,
|
947
|
-
}
|
948
|
-
)
|
949
|
-
interaction_id += 1
|
950
|
-
|
951
|
-
else:
|
952
|
-
interactions.append(
|
953
|
-
{
|
954
|
-
"id": str(interaction_id),
|
955
|
-
"span_id": span.id,
|
956
|
-
"interaction_type": f"{span.type}_call_start",
|
957
|
-
"name": span.name,
|
958
|
-
"content": span.data,
|
959
|
-
"timestamp": span.start_time,
|
960
|
-
"error": span.error,
|
961
|
-
}
|
962
|
-
)
|
963
|
-
interaction_id += 1
|
964
|
-
|
965
|
-
interactions.append(
|
966
|
-
{
|
967
|
-
"id": str(interaction_id),
|
968
|
-
"span_id": span.id,
|
969
|
-
"interaction_type": f"{span.type}_call_end",
|
970
|
-
"name": span.name,
|
971
|
-
"content": span.data,
|
972
|
-
"timestamp": span.end_time,
|
973
|
-
"error": span.error,
|
974
|
-
}
|
975
|
-
)
|
976
|
-
interaction_id += 1
|
977
|
-
|
978
|
-
# Process interactions from span.data if they exist
|
979
|
-
if span.interactions:
|
980
|
-
for span_interaction in span.interactions:
|
981
|
-
interaction = {}
|
982
|
-
interaction["id"] = str(interaction_id)
|
983
|
-
interaction["span_id"] = span.id
|
984
|
-
interaction["interaction_type"] = span_interaction.type
|
985
|
-
interaction["content"] = span_interaction.content
|
986
|
-
interaction["timestamp"] = span_interaction.timestamp
|
987
|
-
interaction["error"] = span.error
|
988
|
-
interactions.append(interaction)
|
989
|
-
interaction_id += 1
|
990
|
-
|
991
|
-
if span.network_calls:
|
992
|
-
for span_network_call in span.network_calls:
|
993
|
-
network_call = {}
|
994
|
-
network_call["id"] = str(interaction_id)
|
995
|
-
network_call["span_id"] = span.id
|
996
|
-
network_call["interaction_type"] = "network_call"
|
997
|
-
network_call["name"] = None
|
998
|
-
network_call["content"] = {
|
999
|
-
"request": {
|
1000
|
-
"url": span_network_call.get("url"),
|
1001
|
-
"method": span_network_call.get("method"),
|
1002
|
-
"headers": span_network_call.get("headers"),
|
1003
|
-
},
|
1004
|
-
"response": {
|
1005
|
-
"status_code": span_network_call.get("status_code"),
|
1006
|
-
"headers": span_network_call.get("response_headers"),
|
1007
|
-
"body": span_network_call.get("response_body"),
|
1008
|
-
},
|
1009
|
-
}
|
1010
|
-
network_call["timestamp"] = span_network_call.get("timestamp")
|
1011
|
-
network_call["error"] = span_network_call.get("error")
|
1012
|
-
interactions.append(network_call)
|
1013
|
-
interaction_id += 1
|
1014
|
-
|
1015
|
-
# Sort interactions by timestamp
|
1016
|
-
sorted_interactions = sorted(
|
1017
|
-
interactions, key=lambda x: x["timestamp"] if x["timestamp"] else ""
|
1018
|
-
)
|
1019
|
-
|
1020
|
-
# Reassign IDs to maintain sequential order after sorting
|
1021
|
-
for idx, interaction in enumerate(sorted_interactions, 1):
|
1022
|
-
interaction["id"] = str(idx)
|
1023
|
-
|
1024
|
-
return {"workflow": sorted_interactions}
|
1025
|
-
|
1026
|
-
# TODO: Add support for execute metrics. Maintain list of all metrics to be added for this span
|
1027
|
-
|
1028
|
-
def execute_metrics(self,
|
1029
|
-
name: str,
|
1030
|
-
model: str,
|
1031
|
-
provider: str,
|
1032
|
-
prompt: str,
|
1033
|
-
context: str,
|
1034
|
-
response: str
|
1035
|
-
):
|
1036
|
-
if not hasattr(self, 'trace'):
|
1037
|
-
logger.warning("Cannot add metrics before trace is initialized. Call start() first.")
|
1038
|
-
return
|
1039
|
-
|
1040
|
-
# Convert individual parameters to metric dict if needed
|
1041
|
-
if isinstance(name, str):
|
1042
|
-
metrics = [{
|
1043
|
-
"name": name
|
1044
|
-
}]
|
1045
|
-
else:
|
1046
|
-
# Handle dict or list input
|
1047
|
-
metrics = name if isinstance(name, list) else [name] if isinstance(name, dict) else []
|
1048
|
-
|
1049
|
-
try:
|
1050
|
-
for metric in metrics:
|
1051
|
-
if not isinstance(metric, dict):
|
1052
|
-
raise ValueError(f"Expected dict, got {type(metric)}")
|
1053
|
-
|
1054
|
-
if "name" not in metric :
|
1055
|
-
raise ValueError("Metric must contain 'name'") #score was written not required here
|
1056
|
-
|
1057
|
-
# Handle duplicate metric names on executing metric
|
1058
|
-
metric_name = metric["name"]
|
1059
|
-
if metric_name in self.visited_metrics:
|
1060
|
-
count = sum(1 for m in self.visited_metrics if m.startswith(metric_name))
|
1061
|
-
metric_name = f"{metric_name}_{count + 1}"
|
1062
|
-
self.visited_metrics.append(metric_name)
|
1063
|
-
|
1064
|
-
result = calculate_metric(project_id=self.project_id,
|
1065
|
-
metric_name=metric_name,
|
1066
|
-
model=model,
|
1067
|
-
org_domain="raga",
|
1068
|
-
provider=provider,
|
1069
|
-
user_id="1", # self.user_details['id'],
|
1070
|
-
prompt=prompt,
|
1071
|
-
context=context,
|
1072
|
-
response=response
|
1073
|
-
)
|
1074
|
-
|
1075
|
-
result = result['data']
|
1076
|
-
formatted_metric = {
|
1077
|
-
"name": metric_name,
|
1078
|
-
"score": result.get("score"),
|
1079
|
-
"reason": result.get("reason", ""),
|
1080
|
-
"source": "user",
|
1081
|
-
"cost": result.get("cost"),
|
1082
|
-
"latency": result.get("latency"),
|
1083
|
-
"mappings": [],
|
1084
|
-
"config": result.get("metric_config", {})
|
1085
|
-
}
|
1086
|
-
|
1087
|
-
logger.debug(f"Executed metric: {formatted_metric}")
|
1088
|
-
|
1089
|
-
except ValueError as e:
|
1090
|
-
logger.error(f"Validation Error: {e}")
|
1091
|
-
except Exception as e:
|
1092
|
-
logger.error(f"Error adding metric: {e}")
|
1093
|
-
|
1094
|
-
def add_metrics(
|
1095
|
-
self,
|
1096
|
-
name: str | List[Dict[str, Any]] | Dict[str, Any] = None,
|
1097
|
-
score: float | int = None,
|
1098
|
-
reasoning: str = "",
|
1099
|
-
cost: float = None,
|
1100
|
-
latency: float = None,
|
1101
|
-
metadata: Dict[str, Any] = None,
|
1102
|
-
config: Dict[str, Any] = None,
|
1103
|
-
):
|
1104
|
-
"""Add metrics at the trace level.
|
1105
|
-
|
1106
|
-
Can be called in two ways:
|
1107
|
-
1. With individual parameters:
|
1108
|
-
tracer.add_metrics(name="metric_name", score=0.9, reasoning="Good performance")
|
1109
|
-
|
1110
|
-
2. With a dictionary or list of dictionaries:
|
1111
|
-
tracer.add_metrics({"name": "metric_name", "score": 0.9})
|
1112
|
-
tracer.add_metrics([{"name": "metric1", "score": 0.9}, {"name": "metric2", "score": 0.8}])
|
1113
|
-
|
1114
|
-
Args:
|
1115
|
-
name: Either the metric name (str) or a metric dictionary/list of dictionaries
|
1116
|
-
score: Score value (float or int) when using individual parameters
|
1117
|
-
reasoning: Optional explanation for the score
|
1118
|
-
cost: Optional cost associated with the metric
|
1119
|
-
latency: Optional latency measurement
|
1120
|
-
metadata: Optional additional metadata as key-value pairs
|
1121
|
-
config: Optional configuration parameters
|
1122
|
-
"""
|
1123
|
-
if not hasattr(self, 'trace'):
|
1124
|
-
logger.warning("Cannot add metrics before trace is initialized. Call start() first.")
|
1125
|
-
return
|
1126
|
-
|
1127
|
-
# Convert individual parameters to metric dict if needed
|
1128
|
-
if isinstance(name, str):
|
1129
|
-
metrics = [{
|
1130
|
-
"name": name,
|
1131
|
-
"score": score,
|
1132
|
-
"reasoning": reasoning,
|
1133
|
-
"cost": cost,
|
1134
|
-
"latency": latency,
|
1135
|
-
"metadata": metadata or {},
|
1136
|
-
"config": config or {}
|
1137
|
-
}]
|
1138
|
-
else:
|
1139
|
-
# Handle dict or list input
|
1140
|
-
metrics = name if isinstance(name, list) else [name] if isinstance(name, dict) else []
|
1141
|
-
|
1142
|
-
try:
|
1143
|
-
for metric in metrics:
|
1144
|
-
if not isinstance(metric, dict):
|
1145
|
-
raise ValueError(f"Expected dict, got {type(metric)}")
|
1146
|
-
|
1147
|
-
if "name" not in metric or "score" not in metric:
|
1148
|
-
raise ValueError("Metric must contain 'name' and 'score' fields")
|
1149
|
-
|
1150
|
-
# Handle duplicate metric names
|
1151
|
-
metric_name = metric["name"]
|
1152
|
-
if metric_name in self.visited_metrics:
|
1153
|
-
count = sum(1 for m in self.visited_metrics if m.startswith(metric_name))
|
1154
|
-
metric_name = f"{metric_name}_{count + 1}"
|
1155
|
-
self.visited_metrics.append(metric_name)
|
1156
|
-
|
1157
|
-
formatted_metric = {
|
1158
|
-
"name": metric_name,
|
1159
|
-
"score": metric["score"],
|
1160
|
-
"reason": metric.get("reasoning", ""),
|
1161
|
-
"source": "user",
|
1162
|
-
"cost": metric.get("cost"),
|
1163
|
-
"latency": metric.get("latency"),
|
1164
|
-
"metadata": metric.get("metadata", {}),
|
1165
|
-
"mappings": [],
|
1166
|
-
"config": metric.get("config", {})
|
1167
|
-
}
|
1168
|
-
|
1169
|
-
self.trace_metrics.append(formatted_metric)
|
1170
|
-
logger.debug(f"Added trace-level metric: {formatted_metric}")
|
1171
|
-
|
1172
|
-
except ValueError as e:
|
1173
|
-
logger.error(f"Validation Error: {e}")
|
1174
|
-
except Exception as e:
|
1175
|
-
logger.error(f"Error adding metric: {e}")
|
1176
|
-
|
1177
|
-
def span(self, span_name):
|
1178
|
-
if span_name not in self.span_attributes_dict:
|
1179
|
-
self.span_attributes_dict[span_name] = SpanAttributes(span_name, self.project_id)
|
1180
|
-
return self.span_attributes_dict[span_name]
|
1181
|
-
|
1182
|
-
@staticmethod
|
1183
|
-
def get_formatted_metric(span_attributes_dict, project_id, name):
|
1184
|
-
if name in span_attributes_dict:
|
1185
|
-
local_metrics = span_attributes_dict[name].local_metrics or []
|
1186
|
-
local_metrics_results = []
|
1187
|
-
for metric in local_metrics:
|
1188
|
-
try:
|
1189
|
-
logger.info("calculating the metric, please wait....")
|
1190
|
-
|
1191
|
-
mapping = metric.get("mapping", {})
|
1192
|
-
result = calculate_metric(project_id=project_id,
|
1193
|
-
metric_name=metric.get("name"),
|
1194
|
-
model=metric.get("model"),
|
1195
|
-
provider=metric.get("provider"),
|
1196
|
-
**mapping
|
1197
|
-
)
|
1198
|
-
|
1199
|
-
result = result['data']['data'][0]
|
1200
|
-
config = result['metric_config']
|
1201
|
-
metric_config = {
|
1202
|
-
"job_id": config.get("job_id"),
|
1203
|
-
"metric_name": config.get("displayName"),
|
1204
|
-
"model": config.get("model"),
|
1205
|
-
"org_domain": config.get("orgDomain"),
|
1206
|
-
"provider": config.get("provider"),
|
1207
|
-
"reason": config.get("reason"),
|
1208
|
-
"request_id": config.get("request_id"),
|
1209
|
-
"user_id": config.get("user_id"),
|
1210
|
-
"threshold": {
|
1211
|
-
"is_editable": config.get("threshold").get("isEditable"),
|
1212
|
-
"lte": config.get("threshold").get("lte")
|
1213
|
-
}
|
1214
|
-
}
|
1215
|
-
formatted_metric = {
|
1216
|
-
"name": metric.get("displayName"),
|
1217
|
-
"displayName": metric.get("displayName"),
|
1218
|
-
"score": result.get("score"),
|
1219
|
-
"reason": result.get("reason", ""),
|
1220
|
-
"source": "user",
|
1221
|
-
"cost": result.get("cost"),
|
1222
|
-
"latency": result.get("latency"),
|
1223
|
-
"mappings": [],
|
1224
|
-
"config": metric_config
|
1225
|
-
}
|
1226
|
-
local_metrics_results.append(formatted_metric)
|
1227
|
-
except ValueError as e:
|
1228
|
-
logger.error(f"Validation Error: {e}")
|
1229
|
-
except Exception as e:
|
1230
|
-
logger.error(f"Error executing metric: {e}")
|
1231
|
-
|
1232
|
-
return local_metrics_results
|
1233
|
-
|
1234
|
-
|
1235
|
-
def upload_directly(self):
|
1236
|
-
"""Upload trace directly without using the background process"""
|
1237
|
-
# Check if we have necessary details
|
1238
|
-
if not hasattr(self, 'trace') or not self.trace_id:
|
1239
|
-
print("No trace to upload")
|
1240
|
-
return False
|
1241
|
-
|
1242
|
-
# Get the filepath from the last trace
|
1243
|
-
trace_dir = tempfile.gettempdir()
|
1244
|
-
trace_file = os.path.join(trace_dir, f"{self.trace_id}.json")
|
1245
|
-
|
1246
|
-
# If filepath wasn't saved from previous stop() call, try to find it
|
1247
|
-
if not os.path.exists(trace_file):
|
1248
|
-
print(f"Looking for trace file for {self.trace_id}")
|
1249
|
-
# Try to find the trace file by pattern
|
1250
|
-
for file in os.listdir(trace_dir):
|
1251
|
-
if file.endswith(".json") and self.trace_id in file:
|
1252
|
-
trace_file = os.path.join(trace_dir, file)
|
1253
|
-
print(f"Found trace file: {trace_file}")
|
1254
|
-
break
|
1255
|
-
|
1256
|
-
if not os.path.exists(trace_file):
|
1257
|
-
print(f"Trace file not found for ID {self.trace_id}")
|
1258
|
-
return False
|
1259
|
-
|
1260
|
-
print(f"Starting direct upload of {trace_file}")
|
1261
|
-
|
1262
|
-
try:
|
1263
|
-
# 1. Create the dataset schema
|
1264
|
-
print("Creating dataset schema...")
|
1265
|
-
from ragaai_catalyst.tracers.agentic_tracing.utils.create_dataset_schema import create_dataset_schema_with_trace
|
1266
|
-
response = create_dataset_schema_with_trace(
|
1267
|
-
dataset_name=self.dataset_name,
|
1268
|
-
project_name=self.project_name
|
1269
|
-
)
|
1270
|
-
print(f"Schema created: {response}")
|
1271
|
-
|
1272
|
-
# 2. Get code hash and zip path if available
|
1273
|
-
code_hash = None
|
1274
|
-
zip_path = None
|
1275
|
-
try:
|
1276
|
-
with open(trace_file, 'r') as f:
|
1277
|
-
data = json.load(f)
|
1278
|
-
code_hash = data.get("metadata", {}).get("system_info", {}).get("source_code")
|
1279
|
-
if code_hash:
|
1280
|
-
zip_path = os.path.join(trace_dir, f"{code_hash}.zip")
|
1281
|
-
print(f"Found code hash: {code_hash}")
|
1282
|
-
print(f"Zip path: {zip_path}")
|
1283
|
-
except Exception as e:
|
1284
|
-
print(f"Error getting code hash: {e}")
|
1285
|
-
|
1286
|
-
# 3. Upload agentic traces
|
1287
|
-
print("Uploading agentic traces...")
|
1288
|
-
from ragaai_catalyst.tracers.agentic_tracing.upload.upload_agentic_traces import UploadAgenticTraces
|
1289
|
-
from ragaai_catalyst import RagaAICatalyst
|
1290
|
-
upload_traces = UploadAgenticTraces(
|
1291
|
-
json_file_path=trace_file,
|
1292
|
-
project_name=self.project_name,
|
1293
|
-
project_id=self.project_id,
|
1294
|
-
dataset_name=self.dataset_name,
|
1295
|
-
user_detail=self.user_details,
|
1296
|
-
base_url=RagaAICatalyst.BASE_URL,
|
1297
|
-
)
|
1298
|
-
upload_traces.upload_agentic_traces()
|
1299
|
-
print("Agentic traces uploaded successfully")
|
1300
|
-
|
1301
|
-
# 4. Upload code hash if available
|
1302
|
-
if code_hash and zip_path and os.path.exists(zip_path):
|
1303
|
-
print(f"Uploading code hash: {code_hash}")
|
1304
|
-
from ragaai_catalyst.tracers.agentic_tracing.upload.upload_code import upload_code
|
1305
|
-
response = upload_code(
|
1306
|
-
hash_id=code_hash,
|
1307
|
-
zip_path=zip_path,
|
1308
|
-
project_name=self.project_name,
|
1309
|
-
dataset_name=self.dataset_name,
|
1310
|
-
)
|
1311
|
-
print(f"Code uploaded: {response}")
|
1312
|
-
|
1313
|
-
print("Upload completed successfully - check UI now")
|
1314
|
-
return True
|
1315
|
-
except Exception as e:
|
1316
|
-
print(f"Error during direct upload: {e}")
|
1317
|
-
import traceback
|
1318
|
-
traceback.print_exc()
|
1319
|
-
return False
|