praisonaiagents 0.0.151__tar.gz → 0.0.153__tar.gz

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.
Files changed (97) hide show
  1. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/PKG-INFO +1 -1
  2. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/__init__.py +57 -7
  3. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agent/agent.py +4 -0
  4. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agents/agents.py +116 -1
  5. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/llm/llm.py +78 -0
  6. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/main.py +7 -0
  7. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/mcp/mcp_http_stream.py +42 -2
  8. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/telemetry/__init__.py +31 -3
  9. praisonaiagents-0.0.153/praisonaiagents/telemetry/integration.py +611 -0
  10. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/telemetry/performance_monitor.py +162 -1
  11. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/telemetry/performance_utils.py +35 -7
  12. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/telemetry/telemetry.py +145 -42
  13. praisonaiagents-0.0.153/praisonaiagents/telemetry/token_collector.py +170 -0
  14. praisonaiagents-0.0.153/praisonaiagents/telemetry/token_telemetry.py +89 -0
  15. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents.egg-info/PKG-INFO +1 -1
  16. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents.egg-info/SOURCES.txt +3 -0
  17. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/pyproject.toml +1 -1
  18. praisonaiagents-0.0.153/tests/test_basic_agents_demo.py +51 -0
  19. praisonaiagents-0.0.151/praisonaiagents/telemetry/integration.py +0 -310
  20. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/README.md +0 -0
  21. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/_logging.py +0 -0
  22. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/_warning_patch.py +0 -0
  23. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agent/__init__.py +0 -0
  24. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agent/context_agent.py +0 -0
  25. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agent/handoff.py +0 -0
  26. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agent/image_agent.py +0 -0
  27. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agent/router_agent.py +0 -0
  28. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agents/__init__.py +0 -0
  29. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/agents/autoagents.py +0 -0
  30. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/approval.py +0 -0
  31. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/flow_display.py +0 -0
  32. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/guardrails/__init__.py +0 -0
  33. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/guardrails/guardrail_result.py +0 -0
  34. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/guardrails/llm_guardrail.py +0 -0
  35. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/knowledge/__init__.py +0 -0
  36. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/knowledge/chunking.py +0 -0
  37. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/knowledge/knowledge.py +0 -0
  38. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/llm/__init__.py +0 -0
  39. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/llm/model_capabilities.py +0 -0
  40. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/llm/model_router.py +0 -0
  41. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/llm/openai_client.py +0 -0
  42. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/mcp/__init__.py +0 -0
  43. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/mcp/mcp.py +0 -0
  44. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/mcp/mcp_sse.py +0 -0
  45. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/memory/__init__.py +0 -0
  46. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/memory/memory.py +0 -0
  47. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/process/__init__.py +0 -0
  48. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/process/process.py +0 -0
  49. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/session.py +0 -0
  50. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/task/__init__.py +0 -0
  51. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/task/task.py +0 -0
  52. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/telemetry/performance_cli.py +0 -0
  53. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/README.md +0 -0
  54. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/__init__.py +0 -0
  55. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/arxiv_tools.py +0 -0
  56. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/calculator_tools.py +0 -0
  57. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/csv_tools.py +0 -0
  58. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/duckdb_tools.py +0 -0
  59. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/duckduckgo_tools.py +0 -0
  60. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/excel_tools.py +0 -0
  61. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/file_tools.py +0 -0
  62. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/json_tools.py +0 -0
  63. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/mongodb_tools.py +0 -0
  64. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/newspaper_tools.py +0 -0
  65. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/pandas_tools.py +0 -0
  66. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/python_tools.py +0 -0
  67. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/searxng_tools.py +0 -0
  68. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/shell_tools.py +0 -0
  69. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/spider_tools.py +0 -0
  70. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/test.py +0 -0
  71. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/tools.py +0 -0
  72. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/train/data/generatecot.py +0 -0
  73. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/wikipedia_tools.py +0 -0
  74. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/xml_tools.py +0 -0
  75. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/yaml_tools.py +0 -0
  76. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents/tools/yfinance_tools.py +0 -0
  77. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents.egg-info/dependency_links.txt +0 -0
  78. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents.egg-info/requires.txt +0 -0
  79. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/praisonaiagents.egg-info/top_level.txt +0 -0
  80. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/setup.cfg +0 -0
  81. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test-graph-memory.py +0 -0
  82. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test.py +0 -0
  83. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_context_agent.py +0 -0
  84. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_embedding_logging.py +0 -0
  85. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_fix_comprehensive.py +0 -0
  86. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_gemini_streaming_fix.py +0 -0
  87. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_handoff_compatibility.py +0 -0
  88. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_http_stream_basic.py +0 -0
  89. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_llm_self_reflection_direct.py +0 -0
  90. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_ollama_async_fix.py +0 -0
  91. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_ollama_fix.py +0 -0
  92. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_ollama_sequential_fix.py +0 -0
  93. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_posthog_fixed.py +0 -0
  94. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_self_reflection_comprehensive.py +0 -0
  95. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_self_reflection_fix_simple.py +0 -0
  96. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_self_reflection_fix_verification.py +0 -0
  97. {praisonaiagents-0.0.151 → praisonaiagents-0.0.153}/tests/test_validation_feedback.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praisonaiagents
3
- Version: 0.0.151
3
+ Version: 0.0.153
4
4
  Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
5
5
  Author: Mervin Praison
6
6
  Requires-Python: >=3.10
@@ -60,6 +60,9 @@ try:
60
60
  get_telemetry,
61
61
  enable_telemetry,
62
62
  disable_telemetry,
63
+ enable_performance_mode,
64
+ disable_performance_mode,
65
+ cleanup_telemetry_resources,
63
66
  MinimalTelemetry,
64
67
  TelemetryCollector
65
68
  )
@@ -80,22 +83,66 @@ except ImportError:
80
83
  def disable_telemetry():
81
84
  pass
82
85
 
86
+ def enable_performance_mode():
87
+ pass
88
+
89
+ def disable_performance_mode():
90
+ pass
91
+
92
+ def cleanup_telemetry_resources():
93
+ pass
94
+
83
95
  MinimalTelemetry = None
84
96
  TelemetryCollector = None
85
97
 
86
98
  # Add Agents as an alias for PraisonAIAgents
87
99
  Agents = PraisonAIAgents
88
100
 
89
- # Apply telemetry auto-instrumentation after all imports
101
+ # Enable PostHog telemetry by default with actual event posting
102
+ # PostHog events are posted by default unless explicitly disabled
103
+ # Users can:
104
+ # - Disable completely: PRAISONAI_DISABLE_TELEMETRY=true (or DO_NOT_TRACK=true)
105
+ # - Enable performance mode: PRAISONAI_PERFORMANCE_MODE=true (minimal overhead, limited events)
106
+ # - Enable full telemetry: PRAISONAI_FULL_TELEMETRY=true (detailed tracking)
107
+ # - Legacy opt-in mode: PRAISONAI_AUTO_INSTRUMENT=true
90
108
  if _telemetry_available:
91
109
  try:
92
- # Only instrument if telemetry is enabled
93
- _telemetry = get_telemetry()
94
- if _telemetry and _telemetry.enabled:
95
- from .telemetry.integration import auto_instrument_all
96
- auto_instrument_all(_telemetry)
110
+ import os
111
+
112
+ # Check for explicit disable (respects DO_NOT_TRACK and other disable flags)
113
+ telemetry_disabled = any([
114
+ os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
115
+ os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
116
+ os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
117
+ ])
118
+
119
+ # Check for performance mode (minimal overhead with limited events)
120
+ performance_mode = os.environ.get('PRAISONAI_PERFORMANCE_MODE', '').lower() in ('true', '1', 'yes')
121
+
122
+ # Check for full telemetry mode (more detailed tracking)
123
+ full_telemetry = os.environ.get('PRAISONAI_FULL_TELEMETRY', '').lower() in ('true', '1', 'yes')
124
+
125
+ # Legacy explicit auto-instrument option
126
+ explicit_auto_instrument = os.environ.get('PRAISONAI_AUTO_INSTRUMENT', '').lower() in ('true', '1', 'yes')
127
+
128
+ # Enable PostHog by default unless explicitly disabled
129
+ if not telemetry_disabled:
130
+ _telemetry = get_telemetry()
131
+ if _telemetry and _telemetry.enabled:
132
+ from .telemetry.integration import auto_instrument_all
133
+
134
+ # Default: PostHog telemetry is enabled and events are posted
135
+ # Performance mode can be explicitly enabled for minimal overhead
136
+ use_performance_mode = performance_mode and not (full_telemetry or explicit_auto_instrument)
137
+ auto_instrument_all(_telemetry, performance_mode=use_performance_mode)
138
+
139
+ # Track package import for basic usage analytics
140
+ try:
141
+ _telemetry.track_feature_usage("package_import")
142
+ except Exception:
143
+ pass
97
144
  except Exception:
98
- # Silently fail if there are any issues
145
+ # Silently fail if there are any issues - never break user applications
99
146
  pass
100
147
 
101
148
  __all__ = [
@@ -135,6 +182,9 @@ __all__ = [
135
182
  'get_telemetry',
136
183
  'enable_telemetry',
137
184
  'disable_telemetry',
185
+ 'enable_performance_mode',
186
+ 'disable_performance_mode',
187
+ 'cleanup_telemetry_resources',
138
188
  'MinimalTelemetry',
139
189
  'TelemetryCollector'
140
190
  ]
@@ -207,6 +207,7 @@ class Agent:
207
207
  use_system_prompt: Optional[bool] = True,
208
208
  markdown: bool = True,
209
209
  stream: bool = False,
210
+ metrics: bool = False,
210
211
  self_reflect: bool = False,
211
212
  max_reflect: int = 3,
212
213
  min_reflect: int = 1,
@@ -283,6 +284,8 @@ class Agent:
283
284
  readability and structure. Defaults to True.
284
285
  stream (bool, optional): Enable streaming responses from the language model for real-time
285
286
  output when using Agent.start() method. Defaults to False for backward compatibility.
287
+ metrics (bool, optional): Enable automatic token usage tracking and display summary
288
+ when tasks complete. Simplifies token monitoring for cost optimization. Defaults to False.
286
289
  self_reflect (bool, optional): Enable self-reflection capabilities where the agent
287
290
  evaluates and improves its own responses. Defaults to False.
288
291
  max_reflect (int, optional): Maximum number of self-reflection iterations to prevent
@@ -465,6 +468,7 @@ class Agent:
465
468
  self.chat_history = []
466
469
  self.markdown = markdown
467
470
  self.stream = stream
471
+ self.metrics = metrics
468
472
  self.max_reflect = max_reflect
469
473
  self.min_reflect = min_reflect
470
474
  self.reflect_prompt = reflect_prompt
@@ -15,6 +15,12 @@ import asyncio
15
15
  import uuid
16
16
  from enum import Enum
17
17
 
18
+ # Import token tracking
19
+ try:
20
+ from ..telemetry.token_collector import _token_collector
21
+ except ImportError:
22
+ _token_collector = None
23
+
18
24
  # Task status constants
19
25
  class TaskStatus(Enum):
20
26
  """Enumeration for task status values to ensure consistency"""
@@ -305,6 +311,11 @@ class PraisonAIAgents:
305
311
  task.status = "in progress"
306
312
 
307
313
  executor_agent = task.agent
314
+
315
+ # Set current agent for token tracking
316
+ llm = getattr(executor_agent, 'llm', None) or getattr(executor_agent, 'llm_instance', None)
317
+ if llm and hasattr(llm, 'set_current_agent'):
318
+ llm.set_current_agent(executor_agent.name)
308
319
 
309
320
  # Ensure tools are available from both task and agent
310
321
  tools = task.tools or []
@@ -401,6 +412,12 @@ Context:
401
412
  agent=executor_agent.name,
402
413
  output_format="RAW"
403
414
  )
415
+
416
+ # Add token metrics if available
417
+ if llm and hasattr(llm, 'last_token_metrics'):
418
+ token_metrics = llm.last_token_metrics
419
+ if token_metrics:
420
+ task_output.token_metrics = token_metrics
404
421
 
405
422
  if task.output_json:
406
423
  cleaned = self.clean_json_output(agent_output)
@@ -633,6 +650,11 @@ Context:
633
650
  task.status = "in progress"
634
651
 
635
652
  executor_agent = task.agent
653
+
654
+ # Set current agent for token tracking
655
+ llm = getattr(executor_agent, 'llm', None) or getattr(executor_agent, 'llm_instance', None)
656
+ if llm and hasattr(llm, 'set_current_agent'):
657
+ llm.set_current_agent(executor_agent.name)
636
658
 
637
659
  task_prompt = f"""
638
660
  You need to do the following task: {task.description}.
@@ -749,6 +771,12 @@ Context:
749
771
  agent=executor_agent.name,
750
772
  output_format="RAW"
751
773
  )
774
+
775
+ # Add token metrics if available
776
+ if llm and hasattr(llm, 'last_token_metrics'):
777
+ token_metrics = llm.last_token_metrics
778
+ if token_metrics:
779
+ task_output.token_metrics = token_metrics
752
780
 
753
781
  if task.output_json:
754
782
  cleaned = self.clean_json_output(agent_output)
@@ -905,6 +933,18 @@ Context:
905
933
  # Run tasks as before
906
934
  self.run_all_tasks()
907
935
 
936
+ # Auto-display token metrics if any agent has metrics=True
937
+ metrics_enabled = any(getattr(agent, 'metrics', False) for agent in self.agents)
938
+ if metrics_enabled:
939
+ try:
940
+ self.display_token_usage()
941
+ except (ImportError, AttributeError) as e:
942
+ # Token tracking not available or not properly configured
943
+ logging.debug(f"Could not auto-display token usage: {e}")
944
+ except Exception as e:
945
+ # Log unexpected errors for debugging
946
+ logging.debug(f"Unexpected error in token metrics display: {e}")
947
+
908
948
  # Get results
909
949
  results = {
910
950
  "task_status": self.get_all_tasks_status(),
@@ -924,6 +964,10 @@ Context:
924
964
  # Return full results dict if return_dict is True or if no final result was found
925
965
  return results
926
966
 
967
+ def run(self, content=None, return_dict=False, **kwargs):
968
+ """Alias for start() method to provide consistent API with Agent class"""
969
+ return self.start(content=content, return_dict=return_dict, **kwargs)
970
+
927
971
  def set_state(self, key: str, value: Any) -> None:
928
972
  """Set a state value"""
929
973
  self._state[key] = value
@@ -1038,6 +1082,77 @@ Context:
1038
1082
  return True
1039
1083
 
1040
1084
  return False
1085
+
1086
+ def get_token_usage_summary(self) -> Dict[str, Any]:
1087
+ """Get a summary of token usage across all agents and tasks."""
1088
+ if not _token_collector:
1089
+ return {"error": "Token tracking not available"}
1090
+
1091
+ return _token_collector.get_session_summary()
1092
+
1093
+ def get_detailed_token_report(self) -> Dict[str, Any]:
1094
+ """Get a detailed token usage report."""
1095
+ if not _token_collector:
1096
+ return {"error": "Token tracking not available"}
1097
+
1098
+ summary = _token_collector.get_session_summary()
1099
+ recent = _token_collector.get_recent_interactions(limit=20)
1100
+
1101
+ # Calculate cost estimates (example rates)
1102
+ cost_per_1k_input = 0.0005 # $0.0005 per 1K input tokens
1103
+ cost_per_1k_output = 0.0015 # $0.0015 per 1K output tokens
1104
+
1105
+ total_metrics = summary.get("total_metrics", {})
1106
+ input_cost = (total_metrics.get("input_tokens", 0) / 1000) * cost_per_1k_input
1107
+ output_cost = (total_metrics.get("output_tokens", 0) / 1000) * cost_per_1k_output
1108
+ total_cost = input_cost + output_cost
1109
+
1110
+ return {
1111
+ "summary": summary,
1112
+ "recent_interactions": recent,
1113
+ "cost_estimate": {
1114
+ "input_cost": f"${input_cost:.4f}",
1115
+ "output_cost": f"${output_cost:.4f}",
1116
+ "total_cost": f"${total_cost:.4f}",
1117
+ "note": "Cost estimates based on example rates"
1118
+ }
1119
+ }
1120
+
1121
+ def display_token_usage(self):
1122
+ """Display token usage in a formatted table."""
1123
+ if not _token_collector:
1124
+ print("Token tracking not available")
1125
+ return
1126
+
1127
+ summary = _token_collector.get_session_summary()
1128
+
1129
+ print("\n" + "="*50)
1130
+ print("TOKEN USAGE SUMMARY")
1131
+ print("="*50)
1132
+
1133
+ total_metrics = summary.get("total_metrics", {})
1134
+ print(f"\nTotal Interactions: {summary.get('total_interactions', 0)}")
1135
+ print(f"Total Tokens: {total_metrics.get('total_tokens', 0):,}")
1136
+ print(f" - Input Tokens: {total_metrics.get('input_tokens', 0):,}")
1137
+ print(f" - Output Tokens: {total_metrics.get('output_tokens', 0):,}")
1138
+ print(f" - Cached Tokens: {total_metrics.get('cached_tokens', 0):,}")
1139
+ print(f" - Reasoning Tokens: {total_metrics.get('reasoning_tokens', 0):,}")
1140
+
1141
+ # By model
1142
+ by_model = summary.get("by_model", {})
1143
+ if by_model:
1144
+ print("\nUsage by Model:")
1145
+ for model, metrics in by_model.items():
1146
+ print(f" {model}: {metrics.get('total_tokens', 0):,} tokens")
1147
+
1148
+ # By agent
1149
+ by_agent = summary.get("by_agent", {})
1150
+ if by_agent:
1151
+ print("\nUsage by Agent:")
1152
+ for agent, metrics in by_agent.items():
1153
+ print(f" {agent}: {metrics.get('total_tokens', 0):,} tokens")
1154
+
1155
+ print("="*50 + "\n")
1041
1156
 
1042
1157
  def launch(self, path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = "http"):
1043
1158
  """
@@ -1416,4 +1531,4 @@ Context:
1416
1531
  return None
1417
1532
  else:
1418
1533
  display_error(f"Invalid protocol: {protocol}. Choose 'http' or 'mcp'.")
1419
- return None
1534
+ return None
@@ -20,6 +20,13 @@ from ..main import (
20
20
  from rich.console import Console
21
21
  from rich.live import Live
22
22
 
23
+ # Import token tracking
24
+ try:
25
+ from ..telemetry.token_collector import TokenMetrics, _token_collector
26
+ except ImportError:
27
+ TokenMetrics = None
28
+ _token_collector = None
29
+
23
30
  # Logging is already configured in _logging.py via __init__.py
24
31
 
25
32
  # TODO: Include in-build tool calling in LLM class
@@ -254,6 +261,11 @@ class LLM:
254
261
  self.min_reflect = extra_settings.get('min_reflect', 1)
255
262
  self.reasoning_steps = extra_settings.get('reasoning_steps', False)
256
263
 
264
+ # Token tracking
265
+ self.last_token_metrics: Optional[TokenMetrics] = None
266
+ self.session_token_metrics: Optional[TokenMetrics] = None
267
+ self.current_agent_name: Optional[str] = None
268
+
257
269
  # Enable error dropping for cleaner output
258
270
  litellm.drop_params = True
259
271
  # Enable parameter modification for providers like Anthropic
@@ -941,6 +953,9 @@ class LLM:
941
953
  response_text = resp["choices"][0]["message"]["content"]
942
954
  final_response = resp
943
955
 
956
+ # Track token usage
957
+ self._track_token_usage(final_response, model)
958
+
944
959
  # Execute callbacks and display based on verbose setting
945
960
  generation_time_val = time.time() - current_time
946
961
  response_content = f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}" if reasoning_content else response_text
@@ -1118,6 +1133,9 @@ class LLM:
1118
1133
  # Handle None content from Gemini
1119
1134
  response_content = final_response["choices"][0]["message"].get("content")
1120
1135
  response_text = response_content if response_content is not None else ""
1136
+
1137
+ # Track token usage
1138
+ self._track_token_usage(final_response, self.model)
1121
1139
 
1122
1140
  # Execute callbacks and display based on verbose setting
1123
1141
  if verbose and not interaction_displayed:
@@ -1264,6 +1282,9 @@ class LLM:
1264
1282
  # Handle None content from Gemini
1265
1283
  response_content = final_response["choices"][0]["message"].get("content")
1266
1284
  response_text = response_content if response_content is not None else ""
1285
+
1286
+ # Track token usage
1287
+ self._track_token_usage(final_response, self.model)
1267
1288
 
1268
1289
  # Execute callbacks and display based on verbose setting
1269
1290
  if verbose and not interaction_displayed:
@@ -1456,6 +1477,8 @@ class LLM:
1456
1477
  )
1457
1478
  if should_break:
1458
1479
  final_response_text = tool_summary_text
1480
+ # Reset interaction_displayed to ensure final summary is shown
1481
+ interaction_displayed = False
1459
1482
  break
1460
1483
  elif tool_summary_text is None and iteration_count > self.OLLAMA_SUMMARY_ITERATION_THRESHOLD:
1461
1484
  # Continue iteration after adding final answer prompt
@@ -1485,6 +1508,8 @@ class LLM:
1485
1508
  tool_summary = self._generate_ollama_tool_summary(accumulated_tool_results, response_text)
1486
1509
  if tool_summary:
1487
1510
  final_response_text = tool_summary
1511
+ # Reset interaction_displayed to ensure final summary is shown
1512
+ interaction_displayed = False
1488
1513
  break
1489
1514
 
1490
1515
  # If we've executed tools in previous iterations, this response contains the final answer
@@ -2567,6 +2592,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
2567
2592
  )
2568
2593
  if should_break:
2569
2594
  final_response_text = tool_summary_text
2595
+ # Reset interaction_displayed to ensure final summary is shown
2596
+ interaction_displayed = False
2570
2597
  break
2571
2598
  elif tool_summary_text is None and iteration_count > self.OLLAMA_SUMMARY_ITERATION_THRESHOLD:
2572
2599
  # Continue iteration after adding final answer prompt
@@ -2594,6 +2621,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
2594
2621
  tool_summary = self._generate_ollama_tool_summary(accumulated_tool_results, response_text)
2595
2622
  if tool_summary:
2596
2623
  final_response_text = tool_summary
2624
+ # Reset interaction_displayed to ensure final summary is shown
2625
+ interaction_displayed = False
2597
2626
  break
2598
2627
 
2599
2628
  # If we've executed tools in previous iterations, this response contains the final answer
@@ -2853,6 +2882,55 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
2853
2882
 
2854
2883
  litellm.callbacks = events
2855
2884
 
2885
+ def _track_token_usage(self, response: Dict[str, Any], model: str) -> Optional[TokenMetrics]:
2886
+ """Extract and track token usage from LLM response."""
2887
+ if not TokenMetrics or not _token_collector:
2888
+ return None
2889
+
2890
+ try:
2891
+ usage = response.get("usage", {})
2892
+ if not usage:
2893
+ return None
2894
+
2895
+ # Extract token counts
2896
+ metrics = TokenMetrics(
2897
+ input_tokens=usage.get("prompt_tokens", 0),
2898
+ output_tokens=usage.get("completion_tokens", 0),
2899
+ cached_tokens=usage.get("cached_tokens", 0),
2900
+ reasoning_tokens=usage.get("reasoning_tokens", 0),
2901
+ audio_input_tokens=usage.get("audio_input_tokens", 0),
2902
+ audio_output_tokens=usage.get("audio_output_tokens", 0)
2903
+ )
2904
+
2905
+ # Store metrics
2906
+ self.last_token_metrics = metrics
2907
+
2908
+ # Update session metrics
2909
+ if not self.session_token_metrics:
2910
+ self.session_token_metrics = TokenMetrics()
2911
+ self.session_token_metrics = self.session_token_metrics + metrics
2912
+
2913
+ # Track in global collector
2914
+ _token_collector.track_tokens(
2915
+ model=model,
2916
+ agent=self.current_agent_name,
2917
+ metrics=metrics,
2918
+ metadata={
2919
+ "provider": self.provider,
2920
+ "stream": False
2921
+ }
2922
+ )
2923
+
2924
+ return metrics
2925
+
2926
+ except Exception as e:
2927
+ if self.verbose:
2928
+ logging.warning(f"Failed to track token usage: {e}")
2929
+ return None
2930
+
2931
+ def set_current_agent(self, agent_name: Optional[str]):
2932
+ """Set the current agent name for token tracking."""
2933
+ self.current_agent_name = agent_name
2856
2934
 
2857
2935
  def _build_completion_params(self, **override_params) -> Dict[str, Any]:
2858
2936
  """Build parameters for litellm completion calls with all necessary config"""
@@ -12,6 +12,12 @@ from rich.markdown import Markdown
12
12
  from rich.live import Live
13
13
  import asyncio
14
14
 
15
+ # Import token metrics if available
16
+ try:
17
+ from .telemetry.token_collector import TokenMetrics
18
+ except ImportError:
19
+ TokenMetrics = None
20
+
15
21
  # Logging is already configured in _logging.py via __init__.py
16
22
 
17
23
  # Global list to store error logs
@@ -415,6 +421,7 @@ class TaskOutput(BaseModel):
415
421
  json_dict: Optional[Dict[str, Any]] = None
416
422
  agent: str
417
423
  output_format: Literal["RAW", "JSON", "Pydantic"] = "RAW"
424
+ token_metrics: Optional['TokenMetrics'] = None # Add token metrics field
418
425
 
419
426
  def json(self) -> Optional[str]:
420
427
  if self.output_format == "JSON" and self.json_dict:
@@ -192,14 +192,20 @@ class HTTPStreamTransport:
192
192
  self._message_queue = asyncio.Queue()
193
193
  self._pending_requests = {}
194
194
  self._closing = False
195
+ self._closed = False
195
196
 
196
197
  async def __aenter__(self):
197
198
  self._session = aiohttp.ClientSession()
198
199
  return self
199
200
 
200
201
  async def __aexit__(self, exc_type, exc_val, exc_tb):
202
+ # Prevent double closing
203
+ if self._closed:
204
+ return
205
+
201
206
  # Set closing flag to stop listener gracefully
202
207
  self._closing = True
208
+ self._closed = True
203
209
 
204
210
  if self._sse_task:
205
211
  self._sse_task.cancel()
@@ -210,6 +216,13 @@ class HTTPStreamTransport:
210
216
  if self._session:
211
217
  await self._session.close()
212
218
 
219
+ def __del__(self):
220
+ """Lightweight cleanup during garbage collection."""
221
+ # Note: We cannot safely run async cleanup in __del__
222
+ # The best practice is to use async context managers or explicit close() calls
223
+ pass
224
+
225
+
213
226
  async def send_request(self, request: Dict[str, Any]) -> Union[Dict[str, Any], None]:
214
227
  """Send a request to the HTTP Stream endpoint."""
215
228
  if not self._session:
@@ -460,7 +473,34 @@ class HTTPStreamMCPClient:
460
473
 
461
474
  async def __aexit__(self, exc_type, exc_val, exc_tb):
462
475
  """Async context manager exit."""
476
+ await self.aclose()
477
+
478
+ async def aclose(self):
479
+ """Async cleanup method to close all resources."""
463
480
  if self.transport:
464
- await self.transport.__aexit__(exc_type, exc_val, exc_tb)
481
+ await self.transport.__aexit__(None, None, None)
465
482
  if hasattr(self, '_session_context') and self._session_context:
466
- await self._session_context.__aexit__(exc_type, exc_val, exc_tb)
483
+ await self._session_context.__aexit__(None, None, None)
484
+
485
+ def close(self):
486
+ """Synchronous cleanup method to close all resources."""
487
+ if hasattr(self, 'transport') and self.transport and not getattr(self.transport, '_closed', False):
488
+ try:
489
+ # Use the global event loop for non-blocking cleanup
490
+ loop = get_event_loop()
491
+ if not loop.is_closed():
492
+ # Schedule cleanup without blocking
493
+ asyncio.run_coroutine_threadsafe(self.aclose(), loop)
494
+ except Exception:
495
+ # Silently ignore cleanup errors to avoid impacting performance
496
+ pass
497
+
498
+ def __del__(self):
499
+ """Cleanup when object is garbage collected."""
500
+ try:
501
+ # Simple, lightweight cleanup
502
+ if hasattr(self, 'transport') and self.transport:
503
+ self.close()
504
+ except Exception:
505
+ # Never raise exceptions in __del__
506
+ pass
@@ -5,19 +5,24 @@ This module provides:
5
5
  1. Anonymous usage tracking with privacy-first design
6
6
  2. User-friendly performance monitoring and analysis tools
7
7
 
8
- Telemetry is opt-out and can be disabled via environment variables:
8
+ Telemetry can be disabled via environment variables:
9
9
  - PRAISONAI_TELEMETRY_DISABLED=true
10
10
  - PRAISONAI_DISABLE_TELEMETRY=true
11
11
  - DO_NOT_TRACK=true
12
12
 
13
+ Performance monitoring can be optimized via environment variables:
14
+ - PRAISONAI_PERFORMANCE_DISABLED=true (disables performance monitoring overhead)
15
+ - PRAISONAI_FLOW_ANALYSIS_ENABLED=true (enables expensive flow analysis - opt-in only)
16
+
13
17
  No personal data, prompts, or responses are collected.
14
18
 
15
19
  Performance Monitoring Features:
16
20
  - Function performance tracking with detailed statistics
17
21
  - API call monitoring and analysis
18
- - Function execution flow visualization
22
+ - Function execution flow visualization (opt-in)
19
23
  - Performance bottleneck identification
20
24
  - Real-time performance reporting
25
+ - External APM metrics export (DataDog, New Relic compatible)
21
26
  - CLI interface for easy access
22
27
  """
23
28
 
@@ -37,7 +42,7 @@ try:
37
42
  PerformanceMonitor, performance_monitor,
38
43
  monitor_function, track_api_call, get_performance_report,
39
44
  get_function_stats, get_api_stats, get_slowest_functions,
40
- get_slowest_apis, clear_performance_data
45
+ get_slowest_apis, clear_performance_data, export_external_apm_metrics
41
46
  )
42
47
  from .performance_utils import (
43
48
  FunctionFlowAnalyzer, PerformanceAnalyzer,
@@ -58,6 +63,10 @@ __all__ = [
58
63
  'force_shutdown_telemetry',
59
64
  'MinimalTelemetry',
60
65
  'TelemetryCollector', # For backward compatibility
66
+ # Performance optimizations
67
+ 'enable_performance_mode',
68
+ 'disable_performance_mode',
69
+ 'cleanup_telemetry_resources',
61
70
  ]
62
71
 
63
72
  # Add performance monitoring to __all__ if available
@@ -81,6 +90,7 @@ if PERFORMANCE_MONITORING_AVAILABLE:
81
90
  'get_slowest_functions',
82
91
  'get_slowest_apis',
83
92
  'clear_performance_data',
93
+ 'export_external_apm_metrics',
84
94
  'analyze_function_flow',
85
95
  'visualize_execution_flow',
86
96
  'analyze_performance_trends',
@@ -114,6 +124,24 @@ def force_shutdown_telemetry():
114
124
  _force_shutdown_telemetry()
115
125
 
116
126
 
127
+ def enable_performance_mode():
128
+ """Enable performance mode for minimal telemetry overhead."""
129
+ from .integration import enable_performance_mode as _enable_performance_mode
130
+ _enable_performance_mode()
131
+
132
+
133
+ def disable_performance_mode():
134
+ """Disable performance mode to resume full telemetry tracking."""
135
+ from .integration import disable_performance_mode as _disable_performance_mode
136
+ _disable_performance_mode()
137
+
138
+
139
+ def cleanup_telemetry_resources():
140
+ """Clean up telemetry resources including thread pools and queues."""
141
+ from .integration import cleanup_telemetry_resources as _cleanup_telemetry_resources
142
+ _cleanup_telemetry_resources()
143
+
144
+
117
145
  # Auto-instrumentation and cleanup setup
118
146
  _initialized = False
119
147
  _atexit_registered = False