praisonaiagents 0.0.144__py3-none-any.whl → 0.0.146__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.
- praisonaiagents/__init__.py +71 -7
- praisonaiagents/agent/__init__.py +2 -1
- praisonaiagents/agent/agent.py +358 -48
- praisonaiagents/agent/context_agent.py +2315 -0
- praisonaiagents/agents/agents.py +30 -12
- praisonaiagents/knowledge/knowledge.py +9 -1
- praisonaiagents/llm/__init__.py +40 -14
- praisonaiagents/llm/llm.py +485 -59
- praisonaiagents/llm/openai_client.py +98 -16
- praisonaiagents/memory/memory.py +84 -15
- praisonaiagents/task/task.py +7 -6
- praisonaiagents/telemetry/__init__.py +63 -3
- praisonaiagents/telemetry/integration.py +78 -10
- praisonaiagents/telemetry/performance_cli.py +397 -0
- praisonaiagents/telemetry/performance_monitor.py +573 -0
- praisonaiagents/telemetry/performance_utils.py +571 -0
- praisonaiagents/telemetry/telemetry.py +35 -11
- {praisonaiagents-0.0.144.dist-info → praisonaiagents-0.0.146.dist-info}/METADATA +9 -3
- {praisonaiagents-0.0.144.dist-info → praisonaiagents-0.0.146.dist-info}/RECORD +21 -17
- {praisonaiagents-0.0.144.dist-info → praisonaiagents-0.0.146.dist-info}/WHEEL +0 -0
- {praisonaiagents-0.0.144.dist-info → praisonaiagents-0.0.146.dist-info}/top_level.txt +0 -0
@@ -838,14 +838,55 @@ class OpenAIClient:
|
|
838
838
|
)
|
839
839
|
else:
|
840
840
|
# Process as regular non-streaming response
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
841
|
+
if display_fn and console:
|
842
|
+
# When verbose (display_fn provided), use streaming for better UX
|
843
|
+
try:
|
844
|
+
with Live(display_fn("", start_time), console=console, refresh_per_second=4, transient=True) as live:
|
845
|
+
# Use streaming when display_fn is provided for progressive display
|
846
|
+
response_stream = self.create_completion(
|
847
|
+
messages=messages,
|
848
|
+
model=model,
|
849
|
+
temperature=temperature,
|
850
|
+
tools=formatted_tools,
|
851
|
+
stream=True, # Always stream when verbose/display_fn
|
852
|
+
**kwargs
|
853
|
+
)
|
854
|
+
|
855
|
+
full_response_text = ""
|
856
|
+
chunks = []
|
857
|
+
|
858
|
+
# Process streaming response
|
859
|
+
for chunk in response_stream:
|
860
|
+
chunks.append(chunk)
|
861
|
+
if chunk.choices[0].delta.content:
|
862
|
+
full_response_text += chunk.choices[0].delta.content
|
863
|
+
live.update(display_fn(full_response_text, start_time))
|
864
|
+
|
865
|
+
# Process final response from chunks
|
866
|
+
final_response = process_stream_chunks(chunks)
|
867
|
+
|
868
|
+
# Clear the last generating display with a blank line
|
869
|
+
console.print()
|
870
|
+
except Exception as e:
|
871
|
+
self.logger.error(f"Error in Live display for non-streaming: {e}")
|
872
|
+
# Fallback to regular completion without display
|
873
|
+
final_response = self.create_completion(
|
874
|
+
messages=messages,
|
875
|
+
model=model,
|
876
|
+
temperature=temperature,
|
877
|
+
tools=formatted_tools,
|
878
|
+
stream=False,
|
879
|
+
**kwargs
|
880
|
+
)
|
881
|
+
else:
|
882
|
+
final_response = self.create_completion(
|
883
|
+
messages=messages,
|
884
|
+
model=model,
|
885
|
+
temperature=temperature,
|
886
|
+
tools=formatted_tools,
|
887
|
+
stream=False,
|
888
|
+
**kwargs
|
889
|
+
)
|
849
890
|
|
850
891
|
if not final_response:
|
851
892
|
return None
|
@@ -969,14 +1010,55 @@ class OpenAIClient:
|
|
969
1010
|
)
|
970
1011
|
else:
|
971
1012
|
# Process as regular non-streaming response
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
1013
|
+
if display_fn and console:
|
1014
|
+
# When verbose (display_fn provided), use streaming for better UX
|
1015
|
+
try:
|
1016
|
+
with Live(display_fn("", start_time), console=console, refresh_per_second=4, transient=True) as live:
|
1017
|
+
# Use streaming when display_fn is provided for progressive display
|
1018
|
+
response_stream = await self.acreate_completion(
|
1019
|
+
messages=messages,
|
1020
|
+
model=model,
|
1021
|
+
temperature=temperature,
|
1022
|
+
tools=formatted_tools,
|
1023
|
+
stream=True, # Always stream when verbose/display_fn
|
1024
|
+
**kwargs
|
1025
|
+
)
|
1026
|
+
|
1027
|
+
full_response_text = ""
|
1028
|
+
chunks = []
|
1029
|
+
|
1030
|
+
# Process streaming response
|
1031
|
+
async for chunk in response_stream:
|
1032
|
+
chunks.append(chunk)
|
1033
|
+
if chunk.choices[0].delta.content:
|
1034
|
+
full_response_text += chunk.choices[0].delta.content
|
1035
|
+
live.update(display_fn(full_response_text, start_time))
|
1036
|
+
|
1037
|
+
# Process final response from chunks
|
1038
|
+
final_response = process_stream_chunks(chunks)
|
1039
|
+
|
1040
|
+
# Clear the last generating display with a blank line
|
1041
|
+
console.print()
|
1042
|
+
except Exception as e:
|
1043
|
+
self.logger.error(f"Error in Live display for async non-streaming: {e}")
|
1044
|
+
# Fallback to regular completion without display
|
1045
|
+
final_response = await self.acreate_completion(
|
1046
|
+
messages=messages,
|
1047
|
+
model=model,
|
1048
|
+
temperature=temperature,
|
1049
|
+
tools=formatted_tools,
|
1050
|
+
stream=False,
|
1051
|
+
**kwargs
|
1052
|
+
)
|
1053
|
+
else:
|
1054
|
+
final_response = await self.acreate_completion(
|
1055
|
+
messages=messages,
|
1056
|
+
model=model,
|
1057
|
+
temperature=temperature,
|
1058
|
+
tools=formatted_tools,
|
1059
|
+
stream=False,
|
1060
|
+
**kwargs
|
1061
|
+
)
|
980
1062
|
|
981
1063
|
if not final_response:
|
982
1064
|
return None
|
praisonaiagents/memory/memory.py
CHANGED
@@ -10,9 +10,19 @@ from datetime import datetime
|
|
10
10
|
# Disable litellm telemetry before any imports
|
11
11
|
os.environ["LITELLM_TELEMETRY"] = "False"
|
12
12
|
|
13
|
-
# Set up logger
|
13
|
+
# Set up logger with custom TRACE level
|
14
14
|
logger = logging.getLogger(__name__)
|
15
15
|
|
16
|
+
# Add custom TRACE level (below DEBUG)
|
17
|
+
TRACE_LEVEL = 5
|
18
|
+
logging.addLevelName(TRACE_LEVEL, 'TRACE')
|
19
|
+
|
20
|
+
def trace(self, message, *args, **kwargs):
|
21
|
+
if self.isEnabledFor(TRACE_LEVEL):
|
22
|
+
self._log(TRACE_LEVEL, message, args, **kwargs)
|
23
|
+
|
24
|
+
logging.Logger.trace = trace
|
25
|
+
|
16
26
|
try:
|
17
27
|
import chromadb
|
18
28
|
from chromadb.config import Settings as ChromaSettings
|
@@ -128,6 +138,8 @@ class Memory:
|
|
128
138
|
logging.getLogger('httpx').setLevel(logging.WARNING)
|
129
139
|
logging.getLogger('httpcore').setLevel(logging.WARNING)
|
130
140
|
logging.getLogger('chromadb.segment.impl.vector.local_persistent_hnsw').setLevel(logging.ERROR)
|
141
|
+
logging.getLogger('utils').setLevel(logging.WARNING)
|
142
|
+
logging.getLogger('litellm.utils').setLevel(logging.WARNING)
|
131
143
|
|
132
144
|
self.provider = self.cfg.get("provider", "rag")
|
133
145
|
self.use_mem0 = (self.provider.lower() == "mem0") and MEM0_AVAILABLE
|
@@ -770,7 +782,7 @@ class Memory:
|
|
770
782
|
import litellm
|
771
783
|
|
772
784
|
logger.info("Getting embeddings from LiteLLM...")
|
773
|
-
logger.
|
785
|
+
logger.trace(f"Embedding input text: {text}")
|
774
786
|
|
775
787
|
response = litellm.embedding(
|
776
788
|
model=self.embedding_model,
|
@@ -778,7 +790,7 @@ class Memory:
|
|
778
790
|
)
|
779
791
|
embedding = response.data[0]["embedding"]
|
780
792
|
logger.info("Successfully got embeddings from LiteLLM")
|
781
|
-
logger.
|
793
|
+
logger.trace(f"Received embedding of length: {len(embedding)}")
|
782
794
|
|
783
795
|
elif OPENAI_AVAILABLE:
|
784
796
|
# Fallback to OpenAI client
|
@@ -786,7 +798,7 @@ class Memory:
|
|
786
798
|
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
787
799
|
|
788
800
|
logger.info("Getting embeddings from OpenAI...")
|
789
|
-
logger.
|
801
|
+
logger.trace(f"Embedding input text: {text}")
|
790
802
|
|
791
803
|
response = client.embeddings.create(
|
792
804
|
input=text,
|
@@ -794,7 +806,7 @@ class Memory:
|
|
794
806
|
)
|
795
807
|
embedding = response.data[0].embedding
|
796
808
|
logger.info("Successfully got embeddings from OpenAI")
|
797
|
-
logger.
|
809
|
+
logger.trace(f"Received embedding of length: {len(embedding)}")
|
798
810
|
else:
|
799
811
|
logger.warning("Neither litellm nor openai available for embeddings")
|
800
812
|
return
|
@@ -1243,12 +1255,22 @@ class Memory:
|
|
1243
1255
|
task_descr: str,
|
1244
1256
|
user_id: Optional[str] = None,
|
1245
1257
|
additional: str = "",
|
1246
|
-
max_items: int = 3
|
1258
|
+
max_items: int = 3,
|
1259
|
+
include_in_output: Optional[bool] = None
|
1247
1260
|
) -> str:
|
1248
1261
|
"""
|
1249
1262
|
Merges relevant short-term, long-term, entity, user memories
|
1250
1263
|
into a single text block with deduplication and clean formatting.
|
1264
|
+
|
1265
|
+
Args:
|
1266
|
+
include_in_output: If None, memory content is only included when debug logging is enabled.
|
1267
|
+
If True, memory content is always included.
|
1268
|
+
If False, memory content is never included (only logged for debugging).
|
1251
1269
|
"""
|
1270
|
+
# Determine whether to include memory content in output based on logging level
|
1271
|
+
if include_in_output is None:
|
1272
|
+
include_in_output = logging.getLogger().getEffectiveLevel() == logging.DEBUG
|
1273
|
+
|
1252
1274
|
q = (task_descr + " " + additional).strip()
|
1253
1275
|
lines = []
|
1254
1276
|
seen_contents = set() # Track unique contents
|
@@ -1306,16 +1328,20 @@ class Memory:
|
|
1306
1328
|
formatted_hits.append(formatted)
|
1307
1329
|
|
1308
1330
|
if formatted_hits:
|
1309
|
-
#
|
1310
|
-
|
1311
|
-
|
1312
|
-
lines.append(title)
|
1313
|
-
lines.append("=" * len(title)) # Underline the title
|
1314
|
-
lines.append("") # Space after title
|
1331
|
+
# Log detailed memory content for debugging including section headers
|
1332
|
+
brief_title = title.replace(" Context", "").replace("Memory ", "")
|
1333
|
+
logger.debug(f"Memory section '{brief_title}' ({len(formatted_hits)} items): {formatted_hits}")
|
1315
1334
|
|
1316
|
-
#
|
1317
|
-
|
1318
|
-
|
1335
|
+
# Only include memory content in output when specified (controlled by log level or explicit parameter)
|
1336
|
+
if include_in_output:
|
1337
|
+
# Add only the actual memory content for AI agent use (no headers)
|
1338
|
+
if lines:
|
1339
|
+
lines.append("") # Space before new section
|
1340
|
+
|
1341
|
+
# Include actual memory content without verbose section headers
|
1342
|
+
for hit in formatted_hits:
|
1343
|
+
lines.append(f"• {hit}")
|
1344
|
+
lines.append("") # Space after content
|
1319
1345
|
|
1320
1346
|
# Add each section
|
1321
1347
|
# First get all results
|
@@ -1522,3 +1548,46 @@ class Memory:
|
|
1522
1548
|
logger.info(f"After quality filter: {len(filtered)} results")
|
1523
1549
|
|
1524
1550
|
return filtered
|
1551
|
+
|
1552
|
+
def get_all_memories(self) -> List[Dict[str, Any]]:
|
1553
|
+
"""Get all memories from both short-term and long-term storage"""
|
1554
|
+
all_memories = []
|
1555
|
+
|
1556
|
+
try:
|
1557
|
+
# Get short-term memories
|
1558
|
+
conn = sqlite3.connect(self.short_db)
|
1559
|
+
c = conn.cursor()
|
1560
|
+
rows = c.execute("SELECT id, content, meta, created_at FROM short_mem").fetchall()
|
1561
|
+
conn.close()
|
1562
|
+
|
1563
|
+
for row in rows:
|
1564
|
+
meta = json.loads(row[2] or "{}")
|
1565
|
+
all_memories.append({
|
1566
|
+
"id": row[0],
|
1567
|
+
"text": row[1],
|
1568
|
+
"metadata": meta,
|
1569
|
+
"created_at": row[3],
|
1570
|
+
"type": "short_term"
|
1571
|
+
})
|
1572
|
+
|
1573
|
+
# Get long-term memories
|
1574
|
+
conn = sqlite3.connect(self.long_db)
|
1575
|
+
c = conn.cursor()
|
1576
|
+
rows = c.execute("SELECT id, content, meta, created_at FROM long_mem").fetchall()
|
1577
|
+
conn.close()
|
1578
|
+
|
1579
|
+
for row in rows:
|
1580
|
+
meta = json.loads(row[2] or "{}")
|
1581
|
+
all_memories.append({
|
1582
|
+
"id": row[0],
|
1583
|
+
"text": row[1],
|
1584
|
+
"metadata": meta,
|
1585
|
+
"created_at": row[3],
|
1586
|
+
"type": "long_term"
|
1587
|
+
})
|
1588
|
+
|
1589
|
+
return all_memories
|
1590
|
+
|
1591
|
+
except Exception as e:
|
1592
|
+
self._log_verbose(f"Error getting all memories: {e}", logging.ERROR)
|
1593
|
+
return []
|
praisonaiagents/task/task.py
CHANGED
@@ -402,13 +402,14 @@ Expected Output: {self.expected_output}.
|
|
402
402
|
context_results.append(f"Input Content: {' '.join(str(x) for x in context_item)}")
|
403
403
|
elif hasattr(context_item, 'result'): # Task object
|
404
404
|
if context_item.result:
|
405
|
-
|
406
|
-
|
407
|
-
)
|
405
|
+
task_name = context_item.name if context_item.name else context_item.description
|
406
|
+
# Log detailed result for debugging
|
407
|
+
logger.debug(f"Previous task '{task_name}' result: {context_item.result.raw}")
|
408
|
+
# Include actual result content without verbose labels (essential for task chaining)
|
409
|
+
context_results.append(context_item.result.raw)
|
408
410
|
else:
|
409
|
-
|
410
|
-
|
411
|
-
)
|
411
|
+
# Task has no result yet, don't include verbose status message
|
412
|
+
pass
|
412
413
|
|
413
414
|
# Join unique context results
|
414
415
|
unique_contexts = list(dict.fromkeys(context_results)) # Remove duplicates
|
@@ -1,13 +1,24 @@
|
|
1
1
|
"""
|
2
|
-
PraisonAI Agents
|
2
|
+
PraisonAI Agents Telemetry & Performance Monitoring Module
|
3
|
+
|
4
|
+
This module provides:
|
5
|
+
1. Anonymous usage tracking with privacy-first design
|
6
|
+
2. User-friendly performance monitoring and analysis tools
|
3
7
|
|
4
|
-
This module provides anonymous usage tracking with privacy-first design.
|
5
8
|
Telemetry is opt-out and can be disabled via environment variables:
|
6
9
|
- PRAISONAI_TELEMETRY_DISABLED=true
|
7
10
|
- PRAISONAI_DISABLE_TELEMETRY=true
|
8
11
|
- DO_NOT_TRACK=true
|
9
12
|
|
10
13
|
No personal data, prompts, or responses are collected.
|
14
|
+
|
15
|
+
Performance Monitoring Features:
|
16
|
+
- Function performance tracking with detailed statistics
|
17
|
+
- API call monitoring and analysis
|
18
|
+
- Function execution flow visualization
|
19
|
+
- Performance bottleneck identification
|
20
|
+
- Real-time performance reporting
|
21
|
+
- CLI interface for easy access
|
11
22
|
"""
|
12
23
|
|
13
24
|
import os
|
@@ -20,15 +31,64 @@ if TYPE_CHECKING:
|
|
20
31
|
# Import the classes for real (not just type checking)
|
21
32
|
from .telemetry import MinimalTelemetry, TelemetryCollector
|
22
33
|
|
34
|
+
# Import performance monitoring tools
|
35
|
+
try:
|
36
|
+
from .performance_monitor import (
|
37
|
+
PerformanceMonitor, performance_monitor,
|
38
|
+
monitor_function, track_api_call, get_performance_report,
|
39
|
+
get_function_stats, get_api_stats, get_slowest_functions,
|
40
|
+
get_slowest_apis, clear_performance_data
|
41
|
+
)
|
42
|
+
from .performance_utils import (
|
43
|
+
FunctionFlowAnalyzer, PerformanceAnalyzer,
|
44
|
+
flow_analyzer, performance_analyzer,
|
45
|
+
analyze_function_flow, visualize_execution_flow,
|
46
|
+
analyze_performance_trends, generate_comprehensive_report
|
47
|
+
)
|
48
|
+
from .performance_cli import PerformanceCLI
|
49
|
+
PERFORMANCE_MONITORING_AVAILABLE = True
|
50
|
+
except ImportError:
|
51
|
+
PERFORMANCE_MONITORING_AVAILABLE = False
|
52
|
+
|
23
53
|
__all__ = [
|
54
|
+
# Core telemetry
|
24
55
|
'get_telemetry',
|
25
56
|
'enable_telemetry',
|
26
|
-
'disable_telemetry',
|
57
|
+
'disable_telemetry',
|
27
58
|
'force_shutdown_telemetry',
|
28
59
|
'MinimalTelemetry',
|
29
60
|
'TelemetryCollector', # For backward compatibility
|
30
61
|
]
|
31
62
|
|
63
|
+
# Add performance monitoring to __all__ if available
|
64
|
+
if PERFORMANCE_MONITORING_AVAILABLE:
|
65
|
+
__all__.extend([
|
66
|
+
# Performance monitoring classes
|
67
|
+
'PerformanceMonitor',
|
68
|
+
'FunctionFlowAnalyzer',
|
69
|
+
'PerformanceAnalyzer',
|
70
|
+
'PerformanceCLI',
|
71
|
+
# Global instances
|
72
|
+
'performance_monitor',
|
73
|
+
'flow_analyzer',
|
74
|
+
'performance_analyzer',
|
75
|
+
# Convenience functions
|
76
|
+
'monitor_function',
|
77
|
+
'track_api_call',
|
78
|
+
'get_performance_report',
|
79
|
+
'get_function_stats',
|
80
|
+
'get_api_stats',
|
81
|
+
'get_slowest_functions',
|
82
|
+
'get_slowest_apis',
|
83
|
+
'clear_performance_data',
|
84
|
+
'analyze_function_flow',
|
85
|
+
'visualize_execution_flow',
|
86
|
+
'analyze_performance_trends',
|
87
|
+
'generate_comprehensive_report',
|
88
|
+
# Availability flag
|
89
|
+
'PERFORMANCE_MONITORING_AVAILABLE'
|
90
|
+
])
|
91
|
+
|
32
92
|
|
33
93
|
def get_telemetry() -> 'MinimalTelemetry':
|
34
94
|
"""Get the global telemetry instance."""
|
@@ -42,13 +42,27 @@ def instrument_agent(agent: 'Agent', telemetry: Optional['MinimalTelemetry'] = N
|
|
42
42
|
if original_chat:
|
43
43
|
@wraps(original_chat)
|
44
44
|
def instrumented_chat(*args, **kwargs):
|
45
|
+
import threading
|
46
|
+
|
45
47
|
try:
|
46
48
|
result = original_chat(*args, **kwargs)
|
47
|
-
|
49
|
+
# Track success asynchronously to prevent blocking
|
50
|
+
def track_async():
|
51
|
+
try:
|
52
|
+
telemetry.track_agent_execution(agent.name, success=True)
|
53
|
+
except:
|
54
|
+
pass # Ignore telemetry errors
|
55
|
+
threading.Thread(target=track_async, daemon=True).start()
|
48
56
|
return result
|
49
57
|
except Exception as e:
|
50
|
-
|
51
|
-
|
58
|
+
# Track error asynchronously
|
59
|
+
def track_error_async():
|
60
|
+
try:
|
61
|
+
telemetry.track_agent_execution(agent.name, success=False)
|
62
|
+
telemetry.track_error(type(e).__name__)
|
63
|
+
except:
|
64
|
+
pass # Ignore telemetry errors
|
65
|
+
threading.Thread(target=track_error_async, daemon=True).start()
|
52
66
|
raise
|
53
67
|
|
54
68
|
agent.chat = instrumented_chat
|
@@ -57,13 +71,53 @@ def instrument_agent(agent: 'Agent', telemetry: Optional['MinimalTelemetry'] = N
|
|
57
71
|
if original_start:
|
58
72
|
@wraps(original_start)
|
59
73
|
def instrumented_start(*args, **kwargs):
|
74
|
+
import types
|
75
|
+
import threading
|
76
|
+
|
60
77
|
try:
|
61
78
|
result = original_start(*args, **kwargs)
|
62
|
-
|
63
|
-
|
79
|
+
|
80
|
+
# Check if result is a generator (streaming mode)
|
81
|
+
if isinstance(result, types.GeneratorType):
|
82
|
+
# For streaming, defer telemetry tracking to avoid blocking
|
83
|
+
def streaming_wrapper():
|
84
|
+
try:
|
85
|
+
for chunk in result:
|
86
|
+
yield chunk
|
87
|
+
# Track success only after streaming completes
|
88
|
+
# Use a separate thread to make it truly non-blocking
|
89
|
+
def track_async():
|
90
|
+
try:
|
91
|
+
telemetry.track_agent_execution(agent.name, success=True)
|
92
|
+
except:
|
93
|
+
pass # Ignore telemetry errors
|
94
|
+
threading.Thread(target=track_async, daemon=True).start()
|
95
|
+
except Exception as e:
|
96
|
+
# Track error immediately
|
97
|
+
threading.Thread(target=lambda: telemetry.track_agent_execution(agent.name, success=False), daemon=True).start()
|
98
|
+
threading.Thread(target=lambda: telemetry.track_error(type(e).__name__), daemon=True).start()
|
99
|
+
raise
|
100
|
+
|
101
|
+
return streaming_wrapper()
|
102
|
+
else:
|
103
|
+
# For non-streaming, track immediately but asynchronously
|
104
|
+
def track_async():
|
105
|
+
try:
|
106
|
+
telemetry.track_agent_execution(agent.name, success=True)
|
107
|
+
except:
|
108
|
+
pass # Ignore telemetry errors
|
109
|
+
threading.Thread(target=track_async, daemon=True).start()
|
110
|
+
return result
|
111
|
+
|
64
112
|
except Exception as e:
|
65
|
-
|
66
|
-
|
113
|
+
# Track error immediately but asynchronously
|
114
|
+
def track_error_async():
|
115
|
+
try:
|
116
|
+
telemetry.track_agent_execution(agent.name, success=False)
|
117
|
+
telemetry.track_error(type(e).__name__)
|
118
|
+
except:
|
119
|
+
pass # Ignore telemetry errors
|
120
|
+
threading.Thread(target=track_error_async, daemon=True).start()
|
67
121
|
raise
|
68
122
|
|
69
123
|
agent.start = instrumented_start
|
@@ -72,13 +126,27 @@ def instrument_agent(agent: 'Agent', telemetry: Optional['MinimalTelemetry'] = N
|
|
72
126
|
if original_run:
|
73
127
|
@wraps(original_run)
|
74
128
|
def instrumented_run(*args, **kwargs):
|
129
|
+
import threading
|
130
|
+
|
75
131
|
try:
|
76
132
|
result = original_run(*args, **kwargs)
|
77
|
-
|
133
|
+
# Track success asynchronously to prevent blocking
|
134
|
+
def track_async():
|
135
|
+
try:
|
136
|
+
telemetry.track_agent_execution(agent.name, success=True)
|
137
|
+
except:
|
138
|
+
pass # Ignore telemetry errors
|
139
|
+
threading.Thread(target=track_async, daemon=True).start()
|
78
140
|
return result
|
79
141
|
except Exception as e:
|
80
|
-
|
81
|
-
|
142
|
+
# Track error asynchronously
|
143
|
+
def track_error_async():
|
144
|
+
try:
|
145
|
+
telemetry.track_agent_execution(agent.name, success=False)
|
146
|
+
telemetry.track_error(type(e).__name__)
|
147
|
+
except:
|
148
|
+
pass # Ignore telemetry errors
|
149
|
+
threading.Thread(target=track_error_async, daemon=True).start()
|
82
150
|
raise
|
83
151
|
|
84
152
|
agent.run = instrumented_run
|