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.
@@ -838,14 +838,55 @@ class OpenAIClient:
838
838
  )
839
839
  else:
840
840
  # Process as regular non-streaming response
841
- final_response = self.create_completion(
842
- messages=messages,
843
- model=model,
844
- temperature=temperature,
845
- tools=formatted_tools,
846
- stream=False,
847
- **kwargs
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
- final_response = await self.acreate_completion(
973
- messages=messages,
974
- model=model,
975
- temperature=temperature,
976
- tools=formatted_tools,
977
- stream=False,
978
- **kwargs
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
@@ -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.debug(f"Embedding input text: {text}")
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.debug(f"Received embedding of length: {len(embedding)}")
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.debug(f"Embedding input text: {text}")
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.debug(f"Received embedding of length: {len(embedding)}")
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
- # Add section header
1310
- if lines:
1311
- lines.append("") # Space before new section
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
- # Add formatted content with bullet points
1317
- for content in formatted_hits:
1318
- lines.append(f" {content}")
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 []
@@ -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
- context_results.append(
406
- f"Result of previous task {context_item.name if context_item.name else context_item.description}:\n{context_item.result.raw}"
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
- context_results.append(
410
- f"Previous task {context_item.name if context_item.name else context_item.description} has no result yet."
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 Minimal Telemetry Module
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
- telemetry.track_agent_execution(agent.name, success=True)
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
- telemetry.track_agent_execution(agent.name, success=False)
51
- telemetry.track_error(type(e).__name__)
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
- telemetry.track_agent_execution(agent.name, success=True)
63
- return result
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
- telemetry.track_agent_execution(agent.name, success=False)
66
- telemetry.track_error(type(e).__name__)
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
- telemetry.track_agent_execution(agent.name, success=True)
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
- telemetry.track_agent_execution(agent.name, success=False)
81
- telemetry.track_error(type(e).__name__)
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