rakam-systems-core 0.1.1rc7__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.
- rakam_systems_core/__init__.py +41 -0
- rakam_systems_core/ai_core/__init__.py +68 -0
- rakam_systems_core/ai_core/base.py +142 -0
- rakam_systems_core/ai_core/config.py +12 -0
- rakam_systems_core/ai_core/config_loader.py +580 -0
- rakam_systems_core/ai_core/config_schema.py +395 -0
- rakam_systems_core/ai_core/interfaces/__init__.py +30 -0
- rakam_systems_core/ai_core/interfaces/agent.py +83 -0
- rakam_systems_core/ai_core/interfaces/chat_history.py +122 -0
- rakam_systems_core/ai_core/interfaces/chunker.py +11 -0
- rakam_systems_core/ai_core/interfaces/embedding_model.py +10 -0
- rakam_systems_core/ai_core/interfaces/indexer.py +10 -0
- rakam_systems_core/ai_core/interfaces/llm_gateway.py +139 -0
- rakam_systems_core/ai_core/interfaces/loader.py +86 -0
- rakam_systems_core/ai_core/interfaces/reranker.py +10 -0
- rakam_systems_core/ai_core/interfaces/retriever.py +11 -0
- rakam_systems_core/ai_core/interfaces/tool.py +162 -0
- rakam_systems_core/ai_core/interfaces/tool_invoker.py +260 -0
- rakam_systems_core/ai_core/interfaces/tool_loader.py +374 -0
- rakam_systems_core/ai_core/interfaces/tool_registry.py +287 -0
- rakam_systems_core/ai_core/interfaces/vectorstore.py +37 -0
- rakam_systems_core/ai_core/mcp/README.md +545 -0
- rakam_systems_core/ai_core/mcp/__init__.py +0 -0
- rakam_systems_core/ai_core/mcp/mcp_server.py +334 -0
- rakam_systems_core/ai_core/tracking.py +602 -0
- rakam_systems_core/ai_core/vs_core.py +55 -0
- rakam_systems_core/ai_utils/__init__.py +16 -0
- rakam_systems_core/ai_utils/logging.py +126 -0
- rakam_systems_core/ai_utils/metrics.py +10 -0
- rakam_systems_core/ai_utils/s3.py +480 -0
- rakam_systems_core/ai_utils/tracing.py +5 -0
- rakam_systems_core-0.1.1rc7.dist-info/METADATA +162 -0
- rakam_systems_core-0.1.1rc7.dist-info/RECORD +34 -0
- rakam_systems_core-0.1.1rc7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input/Output tracking system for agent methods.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
1. Decorator for tracking method inputs/outputs
|
|
6
|
+
2. TrackingMixin for agents to enable tracking
|
|
7
|
+
3. CSV export functionality
|
|
8
|
+
4. Session management
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
|
|
12
|
+
from functools import wraps
|
|
13
|
+
import asyncio
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
import csv
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from .config_schema import (
|
|
22
|
+
MethodInputSchema,
|
|
23
|
+
MethodOutputSchema,
|
|
24
|
+
MethodCallRecordSchema,
|
|
25
|
+
TrackingSessionSchema,
|
|
26
|
+
)
|
|
27
|
+
from .interfaces.agent import AgentInput, AgentOutput
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TrackingManager:
|
|
34
|
+
"""
|
|
35
|
+
Manages tracking of agent method calls.
|
|
36
|
+
|
|
37
|
+
Features:
|
|
38
|
+
- Track inputs and outputs
|
|
39
|
+
- Session management
|
|
40
|
+
- CSV export
|
|
41
|
+
- JSON export
|
|
42
|
+
- Query and analysis
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, output_dir: str = "./agent_tracking"):
|
|
46
|
+
self.output_dir = Path(output_dir)
|
|
47
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
self.sessions: Dict[str, TrackingSessionSchema] = {}
|
|
50
|
+
self.current_session_id: Optional[str] = None
|
|
51
|
+
self.call_records: List[MethodCallRecordSchema] = []
|
|
52
|
+
|
|
53
|
+
def start_session(self, agent_name: str, session_id: Optional[str] = None) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Start a new tracking session.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
agent_name: Name of the agent
|
|
59
|
+
session_id: Optional session ID (generates UUID if None)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Session ID
|
|
63
|
+
"""
|
|
64
|
+
session_id = session_id or str(uuid.uuid4())
|
|
65
|
+
|
|
66
|
+
session = TrackingSessionSchema(
|
|
67
|
+
session_id=session_id,
|
|
68
|
+
agent_name=agent_name,
|
|
69
|
+
started_at=datetime.now(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.sessions[session_id] = session
|
|
73
|
+
self.current_session_id = session_id
|
|
74
|
+
|
|
75
|
+
return session_id
|
|
76
|
+
|
|
77
|
+
def end_session(self, session_id: Optional[str] = None) -> None:
|
|
78
|
+
"""
|
|
79
|
+
End a tracking session.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
session_id: Session to end (uses current if None)
|
|
83
|
+
"""
|
|
84
|
+
session_id = session_id or self.current_session_id
|
|
85
|
+
if session_id and session_id in self.sessions:
|
|
86
|
+
self.sessions[session_id].end_session()
|
|
87
|
+
|
|
88
|
+
def get_session(self, session_id: Optional[str] = None) -> Optional[TrackingSessionSchema]:
|
|
89
|
+
"""Get a session by ID (current session if None)."""
|
|
90
|
+
session_id = session_id or self.current_session_id
|
|
91
|
+
return self.sessions.get(session_id) if session_id else None
|
|
92
|
+
|
|
93
|
+
def record_call(
|
|
94
|
+
self,
|
|
95
|
+
agent_name: str,
|
|
96
|
+
method_name: str,
|
|
97
|
+
input_data: MethodInputSchema,
|
|
98
|
+
output_data: MethodOutputSchema,
|
|
99
|
+
session_id: Optional[str] = None,
|
|
100
|
+
) -> MethodCallRecordSchema:
|
|
101
|
+
"""
|
|
102
|
+
Record a complete method call.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent_name: Name of the agent
|
|
106
|
+
method_name: Name of the method
|
|
107
|
+
input_data: Input data schema
|
|
108
|
+
output_data: Output data schema
|
|
109
|
+
session_id: Session to add to (uses current if None)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Created call record
|
|
113
|
+
"""
|
|
114
|
+
record = MethodCallRecordSchema(
|
|
115
|
+
call_id=input_data.call_id,
|
|
116
|
+
agent_name=agent_name,
|
|
117
|
+
method_name=method_name,
|
|
118
|
+
input_data=input_data,
|
|
119
|
+
output_data=output_data,
|
|
120
|
+
started_at=input_data.timestamp,
|
|
121
|
+
completed_at=output_data.timestamp,
|
|
122
|
+
duration_seconds=output_data.duration_seconds,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.call_records.append(record)
|
|
126
|
+
|
|
127
|
+
# Add to session if exists
|
|
128
|
+
session_id = session_id or self.current_session_id
|
|
129
|
+
if session_id and session_id in self.sessions:
|
|
130
|
+
self.sessions[session_id].add_call(record)
|
|
131
|
+
|
|
132
|
+
return record
|
|
133
|
+
|
|
134
|
+
def export_to_csv(
|
|
135
|
+
self,
|
|
136
|
+
filename: Optional[str] = None,
|
|
137
|
+
session_id: Optional[str] = None,
|
|
138
|
+
) -> Path:
|
|
139
|
+
"""
|
|
140
|
+
Export tracking data to CSV.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
filename: Output filename (auto-generates if None)
|
|
144
|
+
session_id: Session to export (all records if None)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Path to created CSV file
|
|
148
|
+
"""
|
|
149
|
+
if filename is None:
|
|
150
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
151
|
+
filename = f"tracking_{timestamp}.csv"
|
|
152
|
+
|
|
153
|
+
output_path = self.output_dir / filename
|
|
154
|
+
|
|
155
|
+
# Determine records to export
|
|
156
|
+
if session_id:
|
|
157
|
+
session = self.sessions.get(session_id)
|
|
158
|
+
records = session.calls if session else []
|
|
159
|
+
else:
|
|
160
|
+
records = self.call_records
|
|
161
|
+
|
|
162
|
+
# Write CSV
|
|
163
|
+
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
|
164
|
+
writer = csv.DictWriter(f, fieldnames=self._get_csv_fieldnames())
|
|
165
|
+
writer.writeheader()
|
|
166
|
+
|
|
167
|
+
for record in records:
|
|
168
|
+
row = self._record_to_csv_row(record)
|
|
169
|
+
writer.writerow(row)
|
|
170
|
+
|
|
171
|
+
return output_path
|
|
172
|
+
|
|
173
|
+
def _get_csv_fieldnames(self) -> List[str]:
|
|
174
|
+
"""Get CSV column names."""
|
|
175
|
+
return [
|
|
176
|
+
'call_id',
|
|
177
|
+
'agent_name',
|
|
178
|
+
'method_name',
|
|
179
|
+
'started_at',
|
|
180
|
+
'completed_at',
|
|
181
|
+
'duration_seconds',
|
|
182
|
+
'success',
|
|
183
|
+
'input_text',
|
|
184
|
+
'output_text',
|
|
185
|
+
'error',
|
|
186
|
+
'evaluation_score',
|
|
187
|
+
'evaluation_notes',
|
|
188
|
+
# Metadata fields
|
|
189
|
+
'model',
|
|
190
|
+
'temperature',
|
|
191
|
+
'max_tokens',
|
|
192
|
+
'parallel_tool_calls',
|
|
193
|
+
'tool_calls_count',
|
|
194
|
+
'usage_prompt_tokens',
|
|
195
|
+
'usage_completion_tokens',
|
|
196
|
+
'usage_total_tokens',
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
def _record_to_csv_row(self, record: MethodCallRecordSchema) -> Dict[str, Any]:
|
|
200
|
+
"""Convert a record to a CSV row."""
|
|
201
|
+
# Extract metadata
|
|
202
|
+
metadata = record.output_data.metadata or {}
|
|
203
|
+
usage_obj = metadata.get('usage')
|
|
204
|
+
|
|
205
|
+
# Extract usage data - handle both dict and object types
|
|
206
|
+
usage_prompt = ''
|
|
207
|
+
usage_completion = ''
|
|
208
|
+
usage_total = ''
|
|
209
|
+
if usage_obj:
|
|
210
|
+
if isinstance(usage_obj, dict):
|
|
211
|
+
usage_prompt = usage_obj.get('request_tokens', '') or usage_obj.get('prompt_tokens', '')
|
|
212
|
+
usage_completion = usage_obj.get('response_tokens', '') or usage_obj.get('completion_tokens', '')
|
|
213
|
+
usage_total = usage_obj.get('total_tokens', '')
|
|
214
|
+
elif hasattr(usage_obj, 'request_tokens'):
|
|
215
|
+
usage_prompt = getattr(usage_obj, 'request_tokens', '')
|
|
216
|
+
usage_completion = getattr(usage_obj, 'response_tokens', '')
|
|
217
|
+
usage_total = getattr(usage_obj, 'total_tokens', '')
|
|
218
|
+
|
|
219
|
+
# Count tool calls if messages available
|
|
220
|
+
tool_calls_count = 0
|
|
221
|
+
messages = metadata.get('messages', [])
|
|
222
|
+
if messages:
|
|
223
|
+
for msg in messages:
|
|
224
|
+
if hasattr(msg, 'parts'):
|
|
225
|
+
for part in msg.parts:
|
|
226
|
+
if hasattr(part, 'tool_name'):
|
|
227
|
+
tool_calls_count += 1
|
|
228
|
+
|
|
229
|
+
# Safely extract model settings from kwargs
|
|
230
|
+
kwargs = record.input_data.kwargs or {}
|
|
231
|
+
model_settings = kwargs.get('model_settings', {}) or {}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
'call_id': record.call_id,
|
|
235
|
+
'agent_name': record.agent_name,
|
|
236
|
+
'method_name': record.method_name,
|
|
237
|
+
'started_at': record.started_at.isoformat() if record.started_at else '',
|
|
238
|
+
'completed_at': record.completed_at.isoformat() if record.completed_at else '',
|
|
239
|
+
'duration_seconds': record.duration_seconds,
|
|
240
|
+
'success': record.output_data.success,
|
|
241
|
+
'input_text': record.input_data.input_text or '',
|
|
242
|
+
'output_text': record.output_data.output_text or '',
|
|
243
|
+
'error': record.output_data.error or '',
|
|
244
|
+
'evaluation_score': record.evaluation_score or '',
|
|
245
|
+
'evaluation_notes': record.evaluation_notes or '',
|
|
246
|
+
# Metadata
|
|
247
|
+
'model': model_settings.get('model', ''),
|
|
248
|
+
'temperature': model_settings.get('temperature', ''),
|
|
249
|
+
'max_tokens': model_settings.get('max_tokens', ''),
|
|
250
|
+
'parallel_tool_calls': model_settings.get('parallel_tool_calls', ''),
|
|
251
|
+
'tool_calls_count': tool_calls_count,
|
|
252
|
+
'usage_prompt_tokens': usage_prompt,
|
|
253
|
+
'usage_completion_tokens': usage_completion,
|
|
254
|
+
'usage_total_tokens': usage_total,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def export_to_json(
|
|
258
|
+
self,
|
|
259
|
+
filename: Optional[str] = None,
|
|
260
|
+
session_id: Optional[str] = None,
|
|
261
|
+
) -> Path:
|
|
262
|
+
"""
|
|
263
|
+
Export tracking data to JSON.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
filename: Output filename (auto-generates if None)
|
|
267
|
+
session_id: Session to export (all records if None)
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Path to created JSON file
|
|
271
|
+
"""
|
|
272
|
+
if filename is None:
|
|
273
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
274
|
+
filename = f"tracking_{timestamp}.json"
|
|
275
|
+
|
|
276
|
+
output_path = self.output_dir / filename
|
|
277
|
+
|
|
278
|
+
# Determine records to export
|
|
279
|
+
if session_id:
|
|
280
|
+
session = self.sessions.get(session_id)
|
|
281
|
+
if session:
|
|
282
|
+
data = session.dict()
|
|
283
|
+
else:
|
|
284
|
+
data = {"error": "Session not found"}
|
|
285
|
+
else:
|
|
286
|
+
data = {
|
|
287
|
+
"records": [record.dict() for record in self.call_records],
|
|
288
|
+
"total_records": len(self.call_records),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Write JSON
|
|
292
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
293
|
+
json.dump(data, f, indent=2, default=str)
|
|
294
|
+
|
|
295
|
+
return output_path
|
|
296
|
+
|
|
297
|
+
def get_statistics(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
298
|
+
"""
|
|
299
|
+
Get tracking statistics.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
session_id: Session to analyze (all records if None)
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Statistics dictionary
|
|
306
|
+
"""
|
|
307
|
+
if session_id:
|
|
308
|
+
session = self.sessions.get(session_id)
|
|
309
|
+
records = session.calls if session else []
|
|
310
|
+
else:
|
|
311
|
+
records = self.call_records
|
|
312
|
+
|
|
313
|
+
if not records:
|
|
314
|
+
return {"total_calls": 0}
|
|
315
|
+
|
|
316
|
+
successful = sum(1 for r in records if r.output_data.success)
|
|
317
|
+
failed = len(records) - successful
|
|
318
|
+
total_duration = sum(r.duration_seconds for r in records)
|
|
319
|
+
avg_duration = total_duration / len(records)
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"total_calls": len(records),
|
|
323
|
+
"successful_calls": successful,
|
|
324
|
+
"failed_calls": failed,
|
|
325
|
+
"success_rate": successful / len(records) if records else 0,
|
|
326
|
+
"total_duration_seconds": total_duration,
|
|
327
|
+
"average_duration_seconds": avg_duration,
|
|
328
|
+
"min_duration_seconds": min(r.duration_seconds for r in records),
|
|
329
|
+
"max_duration_seconds": max(r.duration_seconds for r in records),
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# Global tracking manager instance
|
|
334
|
+
_global_tracking_manager: Optional[TrackingManager] = None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_tracking_manager(output_dir: str = "./agent_tracking") -> TrackingManager:
|
|
338
|
+
"""Get or create the global tracking manager."""
|
|
339
|
+
global _global_tracking_manager
|
|
340
|
+
if _global_tracking_manager is None:
|
|
341
|
+
_global_tracking_manager = TrackingManager(output_dir)
|
|
342
|
+
return _global_tracking_manager
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def track_method(
|
|
346
|
+
method_name: Optional[str] = None,
|
|
347
|
+
track_args: bool = True,
|
|
348
|
+
track_kwargs: bool = True,
|
|
349
|
+
) -> Callable[[F], F]:
|
|
350
|
+
"""
|
|
351
|
+
Decorator to track method inputs and outputs.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
method_name: Override method name (uses actual name if None)
|
|
355
|
+
track_args: Whether to track positional arguments
|
|
356
|
+
track_kwargs: Whether to track keyword arguments
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Decorated function
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
>>> @track_method()
|
|
363
|
+
>>> async def arun(self, input_data, deps=None):
|
|
364
|
+
>>> return result
|
|
365
|
+
"""
|
|
366
|
+
def decorator(func: F) -> F:
|
|
367
|
+
actual_method_name = method_name or func.__name__
|
|
368
|
+
|
|
369
|
+
@wraps(func)
|
|
370
|
+
async def async_wrapper(self, *args, **kwargs):
|
|
371
|
+
# Check if tracking is enabled
|
|
372
|
+
if not getattr(self, '_tracking_enabled', False):
|
|
373
|
+
return await func(self, *args, **kwargs)
|
|
374
|
+
|
|
375
|
+
# Get tracking manager
|
|
376
|
+
output_dir = getattr(self, '_tracking_output_dir', './agent_tracking')
|
|
377
|
+
manager = get_tracking_manager(output_dir)
|
|
378
|
+
|
|
379
|
+
# Generate call ID
|
|
380
|
+
call_id = str(uuid.uuid4())
|
|
381
|
+
|
|
382
|
+
# Extract input text if available
|
|
383
|
+
input_text = None
|
|
384
|
+
if args:
|
|
385
|
+
if isinstance(args[0], str):
|
|
386
|
+
input_text = args[0]
|
|
387
|
+
elif isinstance(args[0], AgentInput):
|
|
388
|
+
input_text = args[0].input_text
|
|
389
|
+
|
|
390
|
+
# Create input record
|
|
391
|
+
input_data = MethodInputSchema(
|
|
392
|
+
timestamp=datetime.now(),
|
|
393
|
+
method_name=actual_method_name,
|
|
394
|
+
agent_name=self.name,
|
|
395
|
+
input_text=input_text,
|
|
396
|
+
args=list(args) if track_args else [],
|
|
397
|
+
kwargs=dict(kwargs) if track_kwargs else {},
|
|
398
|
+
call_id=call_id,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Execute method
|
|
402
|
+
start_time = time.time()
|
|
403
|
+
success = True
|
|
404
|
+
error = None
|
|
405
|
+
result = None
|
|
406
|
+
output_text = None
|
|
407
|
+
metadata = {}
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
result = await func(self, *args, **kwargs)
|
|
411
|
+
|
|
412
|
+
# Extract output text if available
|
|
413
|
+
if isinstance(result, AgentOutput):
|
|
414
|
+
output_text = result.output_text
|
|
415
|
+
metadata = result.metadata or {}
|
|
416
|
+
elif isinstance(result, str):
|
|
417
|
+
output_text = result
|
|
418
|
+
|
|
419
|
+
except Exception as e:
|
|
420
|
+
success = False
|
|
421
|
+
error = str(e)
|
|
422
|
+
raise
|
|
423
|
+
|
|
424
|
+
finally:
|
|
425
|
+
duration = time.time() - start_time
|
|
426
|
+
|
|
427
|
+
# Create output record
|
|
428
|
+
output_data = MethodOutputSchema(
|
|
429
|
+
timestamp=datetime.now(),
|
|
430
|
+
method_name=actual_method_name,
|
|
431
|
+
agent_name=self.name,
|
|
432
|
+
output_text=output_text,
|
|
433
|
+
result=result if success else None,
|
|
434
|
+
duration_seconds=duration,
|
|
435
|
+
success=success,
|
|
436
|
+
error=error,
|
|
437
|
+
metadata=metadata,
|
|
438
|
+
call_id=call_id,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Record the call
|
|
442
|
+
manager.record_call(
|
|
443
|
+
agent_name=self.name,
|
|
444
|
+
method_name=actual_method_name,
|
|
445
|
+
input_data=input_data,
|
|
446
|
+
output_data=output_data,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
@wraps(func)
|
|
452
|
+
def sync_wrapper(self, *args, **kwargs):
|
|
453
|
+
# Check if tracking is enabled
|
|
454
|
+
if not getattr(self, '_tracking_enabled', False):
|
|
455
|
+
return func(self, *args, **kwargs)
|
|
456
|
+
|
|
457
|
+
# Get tracking manager
|
|
458
|
+
output_dir = getattr(self, '_tracking_output_dir', './agent_tracking')
|
|
459
|
+
manager = get_tracking_manager(output_dir)
|
|
460
|
+
|
|
461
|
+
# Generate call ID
|
|
462
|
+
call_id = str(uuid.uuid4())
|
|
463
|
+
|
|
464
|
+
# Extract input text if available
|
|
465
|
+
input_text = None
|
|
466
|
+
if args:
|
|
467
|
+
if isinstance(args[0], str):
|
|
468
|
+
input_text = args[0]
|
|
469
|
+
elif isinstance(args[0], AgentInput):
|
|
470
|
+
input_text = args[0].input_text
|
|
471
|
+
|
|
472
|
+
# Create input record
|
|
473
|
+
input_data = MethodInputSchema(
|
|
474
|
+
timestamp=datetime.now(),
|
|
475
|
+
method_name=actual_method_name,
|
|
476
|
+
agent_name=self.name,
|
|
477
|
+
input_text=input_text,
|
|
478
|
+
args=list(args) if track_args else [],
|
|
479
|
+
kwargs=dict(kwargs) if track_kwargs else {},
|
|
480
|
+
call_id=call_id,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Execute method
|
|
484
|
+
start_time = time.time()
|
|
485
|
+
success = True
|
|
486
|
+
error = None
|
|
487
|
+
result = None
|
|
488
|
+
output_text = None
|
|
489
|
+
metadata = {}
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
result = func(self, *args, **kwargs)
|
|
493
|
+
|
|
494
|
+
# Extract output text if available
|
|
495
|
+
if isinstance(result, AgentOutput):
|
|
496
|
+
output_text = result.output_text
|
|
497
|
+
metadata = result.metadata or {}
|
|
498
|
+
elif isinstance(result, str):
|
|
499
|
+
output_text = result
|
|
500
|
+
|
|
501
|
+
except Exception as e:
|
|
502
|
+
success = False
|
|
503
|
+
error = str(e)
|
|
504
|
+
raise
|
|
505
|
+
|
|
506
|
+
finally:
|
|
507
|
+
duration = time.time() - start_time
|
|
508
|
+
|
|
509
|
+
# Create output record
|
|
510
|
+
output_data = MethodOutputSchema(
|
|
511
|
+
timestamp=datetime.now(),
|
|
512
|
+
method_name=actual_method_name,
|
|
513
|
+
agent_name=self.name,
|
|
514
|
+
output_text=output_text,
|
|
515
|
+
result=result if success else None,
|
|
516
|
+
duration_seconds=duration,
|
|
517
|
+
success=success,
|
|
518
|
+
error=error,
|
|
519
|
+
metadata=metadata,
|
|
520
|
+
call_id=call_id,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Record the call
|
|
524
|
+
manager.record_call(
|
|
525
|
+
agent_name=self.name,
|
|
526
|
+
method_name=actual_method_name,
|
|
527
|
+
input_data=input_data,
|
|
528
|
+
output_data=output_data,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return result
|
|
532
|
+
|
|
533
|
+
# Return appropriate wrapper based on whether function is async
|
|
534
|
+
if asyncio.iscoroutinefunction(func):
|
|
535
|
+
return async_wrapper # type: ignore
|
|
536
|
+
else:
|
|
537
|
+
return sync_wrapper # type: ignore
|
|
538
|
+
|
|
539
|
+
return decorator
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class TrackingMixin:
|
|
543
|
+
"""
|
|
544
|
+
Mixin class to add tracking capabilities to agents.
|
|
545
|
+
|
|
546
|
+
Usage:
|
|
547
|
+
>>> class MyAgent(TrackingMixin, BaseAgent):
|
|
548
|
+
>>> pass
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
def __init__(self, *args, **kwargs):
|
|
552
|
+
# Extract tracking-specific kwargs before passing to super
|
|
553
|
+
self._tracking_enabled = kwargs.pop('enable_tracking', False)
|
|
554
|
+
self._tracking_output_dir = kwargs.pop('tracking_output_dir', './agent_tracking')
|
|
555
|
+
self._tracking_manager: Optional[TrackingManager] = None
|
|
556
|
+
super().__init__(*args, **kwargs)
|
|
557
|
+
|
|
558
|
+
def enable_tracking(self, output_dir: Optional[str] = None) -> None:
|
|
559
|
+
"""Enable tracking for this agent."""
|
|
560
|
+
self._tracking_enabled = True
|
|
561
|
+
if output_dir:
|
|
562
|
+
self._tracking_output_dir = output_dir
|
|
563
|
+
|
|
564
|
+
def disable_tracking(self) -> None:
|
|
565
|
+
"""Disable tracking for this agent."""
|
|
566
|
+
self._tracking_enabled = False
|
|
567
|
+
|
|
568
|
+
def get_tracking_manager(self) -> TrackingManager:
|
|
569
|
+
"""Get the tracking manager for this agent."""
|
|
570
|
+
if self._tracking_manager is None:
|
|
571
|
+
self._tracking_manager = get_tracking_manager(self._tracking_output_dir)
|
|
572
|
+
return self._tracking_manager
|
|
573
|
+
|
|
574
|
+
def export_tracking_data(
|
|
575
|
+
self,
|
|
576
|
+
format: str = 'csv',
|
|
577
|
+
filename: Optional[str] = None,
|
|
578
|
+
) -> Path:
|
|
579
|
+
"""
|
|
580
|
+
Export tracking data.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
format: 'csv' or 'json'
|
|
584
|
+
filename: Output filename (auto-generates if None)
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Path to exported file
|
|
588
|
+
"""
|
|
589
|
+
manager = self.get_tracking_manager()
|
|
590
|
+
|
|
591
|
+
if format == 'csv':
|
|
592
|
+
return manager.export_to_csv(filename)
|
|
593
|
+
elif format == 'json':
|
|
594
|
+
return manager.export_to_json(filename)
|
|
595
|
+
else:
|
|
596
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
597
|
+
|
|
598
|
+
def get_tracking_statistics(self) -> Dict[str, Any]:
|
|
599
|
+
"""Get tracking statistics for this agent."""
|
|
600
|
+
manager = self.get_tracking_manager()
|
|
601
|
+
return manager.get_statistics()
|
|
602
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VSFile:
|
|
10
|
+
"""
|
|
11
|
+
A data source to be processed. Its nodes will become entries in the VectorStore.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, file_path: str) -> None:
|
|
15
|
+
self.uuid: str = uuid.uuid4()
|
|
16
|
+
self.file_path: str = file_path
|
|
17
|
+
self.file_name: str = file_path.split("/")[-1]
|
|
18
|
+
self.mime_type, _ = mimetypes.guess_type(self.file_path)
|
|
19
|
+
self.nodes: Optional[Node] = []
|
|
20
|
+
self.processed: bool = (
|
|
21
|
+
False # whether the nodes of this file have been processed
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NodeMetadata:
|
|
26
|
+
def __init__(
|
|
27
|
+
self, source_file_uuid: str, position: int, custom: dict = None
|
|
28
|
+
) -> None:
|
|
29
|
+
self.node_id: Optional[int] = None
|
|
30
|
+
self.source_file_uuid: str = source_file_uuid
|
|
31
|
+
self.position: Optional[int] = position # page_number
|
|
32
|
+
self.custom: Optional[Dict] = custom
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
custom_str = ", ".join(
|
|
36
|
+
f"{key}: {value}" for key, value in (self.custom or {}).items()
|
|
37
|
+
)
|
|
38
|
+
return (
|
|
39
|
+
f"NodeMetadata(node_id={self.node_id}, source_file_uuid='{self.source_file_uuid}', "
|
|
40
|
+
f"position={self.position}, custom={{ {custom_str} }})"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Node:
|
|
45
|
+
"""
|
|
46
|
+
A node with content and associated metadata.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, content: str, metadata: NodeMetadata) -> None:
|
|
50
|
+
self.content: str = content
|
|
51
|
+
self.metadata: Optional[NodeMetadata] = metadata
|
|
52
|
+
self.embedding: Optional[Any] = None
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return f"Node(content='{self.content[:30]}...', metadata={self.metadata})"
|