praisonaiagents 0.0.101__py3-none-any.whl → 0.0.103__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.
@@ -12,6 +12,7 @@ from .knowledge.knowledge import Knowledge
12
12
  from .knowledge.chunking import Chunking
13
13
  from .mcp.mcp import MCP
14
14
  from .session import Session
15
+ from .memory.memory import Memory
15
16
  from .guardrails import GuardrailResult, LLMGuardrail
16
17
  from .main import (
17
18
  TaskOutput,
@@ -29,9 +30,50 @@ from .main import (
29
30
  async_display_callbacks,
30
31
  )
31
32
 
33
+ # Telemetry support (lazy loaded)
34
+ try:
35
+ from .telemetry import (
36
+ get_telemetry,
37
+ enable_telemetry,
38
+ disable_telemetry,
39
+ MinimalTelemetry,
40
+ TelemetryCollector
41
+ )
42
+ _telemetry_available = True
43
+ except ImportError:
44
+ # Telemetry not available - provide stub functions
45
+ _telemetry_available = False
46
+ def get_telemetry():
47
+ return None
48
+
49
+ def enable_telemetry(*args, **kwargs):
50
+ import logging
51
+ logging.warning(
52
+ "Telemetry not available. Install with: pip install praisonaiagents[telemetry]"
53
+ )
54
+ return None
55
+
56
+ def disable_telemetry():
57
+ pass
58
+
59
+ MinimalTelemetry = None
60
+ TelemetryCollector = None
61
+
32
62
  # Add Agents as an alias for PraisonAIAgents
33
63
  Agents = PraisonAIAgents
34
64
 
65
+ # Apply telemetry auto-instrumentation after all imports
66
+ if _telemetry_available:
67
+ try:
68
+ # Only instrument if telemetry is enabled
69
+ _telemetry = get_telemetry()
70
+ if _telemetry and _telemetry.enabled:
71
+ from .telemetry.integration import auto_instrument_all
72
+ auto_instrument_all(_telemetry)
73
+ except Exception:
74
+ # Silently fail if there are any issues
75
+ pass
76
+
35
77
  __all__ = [
36
78
  'Agent',
37
79
  'ImageAgent',
@@ -43,6 +85,7 @@ __all__ = [
43
85
  'ReflectionOutput',
44
86
  'AutoAgents',
45
87
  'Session',
88
+ 'Memory',
46
89
  'display_interaction',
47
90
  'display_self_reflection',
48
91
  'display_instruction',
@@ -58,5 +101,10 @@ __all__ = [
58
101
  'Chunking',
59
102
  'MCP',
60
103
  'GuardrailResult',
61
- 'LLMGuardrail'
104
+ 'LLMGuardrail',
105
+ 'get_telemetry',
106
+ 'enable_telemetry',
107
+ 'disable_telemetry',
108
+ 'MinimalTelemetry',
109
+ 'TelemetryCollector'
62
110
  ]
@@ -1,5 +1,9 @@
1
1
  import logging
2
2
  import warnings
3
+ import os
4
+
5
+ # Disable litellm telemetry before any imports
6
+ os.environ["LITELLM_TELEMETRY"] = "False"
3
7
 
4
8
  # Suppress all relevant logs at module level
5
9
  logging.getLogger("litellm").setLevel(logging.ERROR)
@@ -17,4 +21,11 @@ logging.basicConfig(level=logging.WARNING)
17
21
  # Import after suppressing warnings
18
22
  from .llm import LLM, LLMContextLengthExceededException
19
23
 
24
+ # Ensure telemetry is disabled after import as well
25
+ try:
26
+ import litellm
27
+ litellm.telemetry = False
28
+ except ImportError:
29
+ pass
30
+
20
31
  __all__ = ["LLM", "LLMContextLengthExceededException"]
@@ -17,6 +17,9 @@ from ..main import (
17
17
  from rich.console import Console
18
18
  from rich.live import Live
19
19
 
20
+ # Disable litellm telemetry before any imports
21
+ os.environ["LITELLM_TELEMETRY"] = "False"
22
+
20
23
  # TODO: Include in-build tool calling in LLM class
21
24
  # TODO: Restructure so that duplicate calls are not made (Sync with agent.py)
22
25
  class LLMContextLengthExceededException(Exception):
@@ -108,6 +111,9 @@ class LLM:
108
111
  ):
109
112
  try:
110
113
  import litellm
114
+ # Disable telemetry
115
+ litellm.telemetry = False
116
+
111
117
  # Set litellm options globally
112
118
  litellm.set_verbose = False
113
119
  litellm.success_callback = []
@@ -6,6 +6,9 @@ import shutil
6
6
  from typing import Any, Dict, List, Optional, Union, Literal
7
7
  import logging
8
8
 
9
+ # Disable litellm telemetry before any imports
10
+ os.environ["LITELLM_TELEMETRY"] = "False"
11
+
9
12
  # Set up logger
10
13
  logger = logging.getLogger(__name__)
11
14
 
@@ -29,6 +32,13 @@ try:
29
32
  except ImportError:
30
33
  OPENAI_AVAILABLE = False
31
34
 
35
+ try:
36
+ import litellm
37
+ litellm.telemetry = False # Disable telemetry
38
+ LITELLM_AVAILABLE = True
39
+ except ImportError:
40
+ LITELLM_AVAILABLE = False
41
+
32
42
 
33
43
 
34
44
 
@@ -340,14 +350,28 @@ class Memory:
340
350
 
341
351
  elif self.use_rag and hasattr(self, "chroma_col"):
342
352
  try:
343
- from openai import OpenAI
344
- client = OpenAI()
345
-
346
- response = client.embeddings.create(
347
- input=query,
348
- model="text-embedding-3-small"
349
- )
350
- query_embedding = response.data[0].embedding
353
+ if LITELLM_AVAILABLE:
354
+ # Use LiteLLM for consistency with the rest of the codebase
355
+ import litellm
356
+
357
+ response = litellm.embedding(
358
+ model="text-embedding-3-small",
359
+ input=query
360
+ )
361
+ query_embedding = response.data[0]["embedding"]
362
+ elif OPENAI_AVAILABLE:
363
+ # Fallback to OpenAI client
364
+ from openai import OpenAI
365
+ client = OpenAI()
366
+
367
+ response = client.embeddings.create(
368
+ input=query,
369
+ model="text-embedding-3-small"
370
+ )
371
+ query_embedding = response.data[0].embedding
372
+ else:
373
+ self._log_verbose("Neither litellm nor openai available for embeddings", logging.WARNING)
374
+ return []
351
375
 
352
376
  resp = self.chroma_col.query(
353
377
  query_embeddings=[query_embedding],
@@ -464,19 +488,39 @@ class Memory:
464
488
  # Store in vector database if enabled
465
489
  if self.use_rag and hasattr(self, "chroma_col"):
466
490
  try:
467
- from openai import OpenAI
468
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # Ensure API key is correctly set
469
-
470
- logger.info("Getting embeddings from OpenAI...")
471
- logger.debug(f"Embedding input text: {text}") # Log the input text
472
-
473
- response = client.embeddings.create(
474
- input=text,
475
- model="text-embedding-3-small"
476
- )
477
- embedding = response.data[0].embedding
478
- logger.info("Successfully got embeddings")
479
- logger.debug(f"Received embedding of length: {len(embedding)}") # Log embedding details
491
+ if LITELLM_AVAILABLE:
492
+ # Use LiteLLM for consistency with the rest of the codebase
493
+ import litellm
494
+
495
+ logger.info("Getting embeddings from LiteLLM...")
496
+ logger.debug(f"Embedding input text: {text}")
497
+
498
+ response = litellm.embedding(
499
+ model="text-embedding-3-small",
500
+ input=text
501
+ )
502
+ embedding = response.data[0]["embedding"]
503
+ logger.info("Successfully got embeddings from LiteLLM")
504
+ logger.debug(f"Received embedding of length: {len(embedding)}")
505
+
506
+ elif OPENAI_AVAILABLE:
507
+ # Fallback to OpenAI client
508
+ from openai import OpenAI
509
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
510
+
511
+ logger.info("Getting embeddings from OpenAI...")
512
+ logger.debug(f"Embedding input text: {text}")
513
+
514
+ response = client.embeddings.create(
515
+ input=text,
516
+ model="text-embedding-3-small"
517
+ )
518
+ embedding = response.data[0].embedding
519
+ logger.info("Successfully got embeddings from OpenAI")
520
+ logger.debug(f"Received embedding of length: {len(embedding)}")
521
+ else:
522
+ logger.warning("Neither litellm nor openai available for embeddings")
523
+ return
480
524
 
481
525
  # Sanitize metadata for ChromaDB
482
526
  sanitized_metadata = self._sanitize_metadata(metadata)
@@ -527,15 +571,28 @@ class Memory:
527
571
 
528
572
  elif self.use_rag and hasattr(self, "chroma_col"):
529
573
  try:
530
- from openai import OpenAI
531
- client = OpenAI()
532
-
533
- # Get query embedding
534
- response = client.embeddings.create(
535
- input=query,
536
- model="text-embedding-3-small" # Using consistent model
537
- )
538
- query_embedding = response.data[0].embedding
574
+ if LITELLM_AVAILABLE:
575
+ # Use LiteLLM for consistency with the rest of the codebase
576
+ import litellm
577
+
578
+ response = litellm.embedding(
579
+ model="text-embedding-3-small",
580
+ input=query
581
+ )
582
+ query_embedding = response.data[0]["embedding"]
583
+ elif OPENAI_AVAILABLE:
584
+ # Fallback to OpenAI client
585
+ from openai import OpenAI
586
+ client = OpenAI()
587
+
588
+ response = client.embeddings.create(
589
+ input=query,
590
+ model="text-embedding-3-small"
591
+ )
592
+ query_embedding = response.data[0].embedding
593
+ else:
594
+ self._log_verbose("Neither litellm nor openai available for embeddings", logging.WARNING)
595
+ return []
539
596
 
540
597
  # Search ChromaDB with embedding
541
598
  resp = self.chroma_col.query(
@@ -910,21 +967,44 @@ class Memory:
910
967
  """
911
968
 
912
969
  try:
913
- # Use LiteLLM for consistency with the rest of the codebase
914
- import litellm
915
-
916
- # Convert model name if it's in litellm format
917
- model_name = llm or "gpt-4o-mini"
918
-
919
- response = litellm.completion(
920
- model=model_name,
921
- messages=[{
922
- "role": "user",
923
- "content": custom_prompt or default_prompt
924
- }],
925
- response_format={"type": "json_object"},
926
- temperature=0.3
927
- )
970
+ if LITELLM_AVAILABLE:
971
+ # Use LiteLLM for consistency with the rest of the codebase
972
+ import litellm
973
+
974
+ # Convert model name if it's in litellm format
975
+ model_name = llm or "gpt-4o-mini"
976
+
977
+ response = litellm.completion(
978
+ model=model_name,
979
+ messages=[{
980
+ "role": "user",
981
+ "content": custom_prompt or default_prompt
982
+ }],
983
+ response_format={"type": "json_object"},
984
+ temperature=0.3
985
+ )
986
+ elif OPENAI_AVAILABLE:
987
+ # Fallback to OpenAI client
988
+ from openai import OpenAI
989
+ client = OpenAI()
990
+
991
+ response = client.chat.completions.create(
992
+ model=llm or "gpt-4o-mini",
993
+ messages=[{
994
+ "role": "user",
995
+ "content": custom_prompt or default_prompt
996
+ }],
997
+ response_format={"type": "json_object"},
998
+ temperature=0.3
999
+ )
1000
+ else:
1001
+ logger.error("Neither litellm nor openai available for quality calculation")
1002
+ return {
1003
+ "completeness": 0.0,
1004
+ "relevance": 0.0,
1005
+ "clarity": 0.0,
1006
+ "accuracy": 0.0
1007
+ }
928
1008
 
929
1009
  metrics = json.loads(response.choices[0].message.content)
930
1010
 
@@ -0,0 +1,102 @@
1
+ """
2
+ PraisonAI Agents Minimal Telemetry Module
3
+
4
+ This module provides anonymous usage tracking with privacy-first design.
5
+ Telemetry is opt-out and can be disabled via environment variables:
6
+ - PRAISONAI_TELEMETRY_DISABLED=true
7
+ - PRAISONAI_DISABLE_TELEMETRY=true
8
+ - DO_NOT_TRACK=true
9
+
10
+ No personal data, prompts, or responses are collected.
11
+ """
12
+
13
+ import os
14
+ import atexit
15
+ from typing import Optional, TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from .telemetry import MinimalTelemetry, TelemetryCollector
19
+
20
+ # Import the classes for real (not just type checking)
21
+ from .telemetry import MinimalTelemetry, TelemetryCollector
22
+
23
+ __all__ = [
24
+ 'get_telemetry',
25
+ 'enable_telemetry',
26
+ 'disable_telemetry',
27
+ 'MinimalTelemetry',
28
+ 'TelemetryCollector', # For backward compatibility
29
+ ]
30
+
31
+
32
+ def get_telemetry() -> 'MinimalTelemetry':
33
+ """Get the global telemetry instance."""
34
+ from .telemetry import get_telemetry as _get_telemetry
35
+ return _get_telemetry()
36
+
37
+
38
+ def enable_telemetry():
39
+ """Enable telemetry (if not disabled by environment)."""
40
+ from .telemetry import enable_telemetry as _enable_telemetry
41
+ _enable_telemetry()
42
+
43
+
44
+ def disable_telemetry():
45
+ """Disable telemetry."""
46
+ from .telemetry import disable_telemetry as _disable_telemetry
47
+ _disable_telemetry()
48
+
49
+
50
+ # Auto-instrumentation and cleanup setup
51
+ _initialized = False
52
+ _atexit_registered = False
53
+
54
+ def _ensure_atexit():
55
+ """Ensure atexit handler is registered."""
56
+ global _atexit_registered
57
+ if _atexit_registered:
58
+ return
59
+
60
+ # Check if telemetry should be disabled
61
+ telemetry_disabled = any([
62
+ os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
63
+ os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
64
+ os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
65
+ ])
66
+
67
+ if not telemetry_disabled:
68
+ # Register atexit handler to flush telemetry on exit
69
+ atexit.register(lambda: get_telemetry().flush())
70
+ _atexit_registered = True
71
+
72
+ def _initialize_telemetry():
73
+ """Initialize telemetry with auto-instrumentation and cleanup."""
74
+ global _initialized
75
+ if _initialized:
76
+ return
77
+
78
+ # Ensure atexit is registered
79
+ _ensure_atexit()
80
+
81
+ # Check if telemetry should be disabled
82
+ telemetry_disabled = any([
83
+ os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
84
+ os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
85
+ os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
86
+ ])
87
+
88
+ if not telemetry_disabled:
89
+ try:
90
+ # Defer the actual instrumentation to avoid circular imports
91
+ # This will be called when get_telemetry() is first accessed
92
+ _initialized = True
93
+ except Exception:
94
+ # Silently fail if there are any issues
95
+ pass
96
+
97
+
98
+ # No need for lazy auto-instrumentation here since main __init__.py handles it
99
+
100
+
101
+ # Initialize atexit handler early
102
+ _ensure_atexit()
@@ -0,0 +1,242 @@
1
+ """
2
+ Simplified integration module for adding telemetry to core PraisonAI components.
3
+ """
4
+
5
+ from typing import Any, Optional, TYPE_CHECKING
6
+ from functools import wraps
7
+ import time
8
+
9
+ if TYPE_CHECKING:
10
+ from .telemetry import MinimalTelemetry
11
+ from ..agent.agent import Agent
12
+ from ..task.task import Task
13
+ from ..agents.agents import PraisonAIAgents
14
+
15
+
16
+ def instrument_agent(agent: 'Agent', telemetry: Optional['MinimalTelemetry'] = None):
17
+ """
18
+ Instrument an Agent instance with minimal telemetry.
19
+
20
+ Args:
21
+ agent: The Agent instance to instrument
22
+ telemetry: Optional telemetry instance (uses global if not provided)
23
+ """
24
+ if not telemetry:
25
+ from .telemetry import get_telemetry
26
+ telemetry = get_telemetry()
27
+
28
+ if not telemetry.enabled:
29
+ return agent
30
+
31
+ # Check if agent is already instrumented to avoid double-counting
32
+ if hasattr(agent, '_telemetry_instrumented'):
33
+ return agent
34
+
35
+ # Store original methods
36
+ original_chat = agent.chat if hasattr(agent, 'chat') else None
37
+ original_start = agent.start if hasattr(agent, 'start') else None
38
+ original_run = agent.run if hasattr(agent, 'run') else None
39
+ original_execute_tool = agent.execute_tool if hasattr(agent, 'execute_tool') else None
40
+
41
+ # Wrap chat method if it exists (this is the main method called by workflow)
42
+ if original_chat:
43
+ @wraps(original_chat)
44
+ def instrumented_chat(*args, **kwargs):
45
+ try:
46
+ result = original_chat(*args, **kwargs)
47
+ telemetry.track_agent_execution(agent.name, success=True)
48
+ return result
49
+ except Exception as e:
50
+ telemetry.track_agent_execution(agent.name, success=False)
51
+ telemetry.track_error(type(e).__name__)
52
+ raise
53
+
54
+ agent.chat = instrumented_chat
55
+
56
+ # Wrap start method if it exists
57
+ if original_start:
58
+ @wraps(original_start)
59
+ def instrumented_start(*args, **kwargs):
60
+ try:
61
+ result = original_start(*args, **kwargs)
62
+ telemetry.track_agent_execution(agent.name, success=True)
63
+ return result
64
+ except Exception as e:
65
+ telemetry.track_agent_execution(agent.name, success=False)
66
+ telemetry.track_error(type(e).__name__)
67
+ raise
68
+
69
+ agent.start = instrumented_start
70
+
71
+ # Wrap run method if it exists
72
+ if original_run:
73
+ @wraps(original_run)
74
+ def instrumented_run(*args, **kwargs):
75
+ try:
76
+ result = original_run(*args, **kwargs)
77
+ telemetry.track_agent_execution(agent.name, success=True)
78
+ return result
79
+ except Exception as e:
80
+ telemetry.track_agent_execution(agent.name, success=False)
81
+ telemetry.track_error(type(e).__name__)
82
+ raise
83
+
84
+ agent.run = instrumented_run
85
+
86
+ # Wrap execute_tool method
87
+ if original_execute_tool:
88
+ @wraps(original_execute_tool)
89
+ def instrumented_execute_tool(tool_name: str, *args, **kwargs):
90
+ try:
91
+ result = original_execute_tool(tool_name, *args, **kwargs)
92
+ telemetry.track_tool_usage(tool_name, success=True)
93
+ return result
94
+ except Exception as e:
95
+ telemetry.track_tool_usage(tool_name, success=False)
96
+ telemetry.track_error(type(e).__name__)
97
+ raise
98
+
99
+ agent.execute_tool = instrumented_execute_tool
100
+
101
+ # Mark agent as instrumented to avoid double instrumentation
102
+ agent._telemetry_instrumented = True
103
+
104
+ return agent
105
+
106
+
107
+ def instrument_workflow(workflow: 'PraisonAIAgents', telemetry: Optional['MinimalTelemetry'] = None):
108
+ """
109
+ Instrument a PraisonAIAgents workflow with minimal telemetry.
110
+
111
+ Args:
112
+ workflow: The PraisonAIAgents instance to instrument
113
+ telemetry: Optional telemetry instance (uses global if not provided)
114
+ """
115
+ if not telemetry:
116
+ from .telemetry import get_telemetry
117
+ telemetry = get_telemetry()
118
+
119
+ if not telemetry.enabled:
120
+ return workflow
121
+
122
+ # Check if workflow is already instrumented to avoid double-counting
123
+ if hasattr(workflow, '_telemetry_instrumented'):
124
+ return workflow
125
+
126
+ # Track feature usage
127
+ telemetry.track_feature_usage(f"workflow_{workflow.process}" if hasattr(workflow, 'process') else "workflow")
128
+
129
+ # Instrument all agents in the workflow
130
+ if hasattr(workflow, 'agents') and workflow.agents:
131
+ for agent in workflow.agents:
132
+ instrument_agent(agent, telemetry)
133
+
134
+ # Wrap the execute_task method to track task completions
135
+ if hasattr(workflow, 'execute_task'):
136
+ original_execute_task = workflow.execute_task
137
+
138
+ @wraps(original_execute_task)
139
+ def instrumented_execute_task(task_id, *args, **kwargs):
140
+ task = None
141
+ try:
142
+ # Get task info
143
+ if hasattr(workflow, 'tasks') and task_id < len(workflow.tasks):
144
+ task = workflow.tasks[task_id]
145
+
146
+ result = original_execute_task(task_id, *args, **kwargs)
147
+
148
+ # Track task completion
149
+ task_name = task.name if task and hasattr(task, 'name') else f"task_{task_id}"
150
+ telemetry.track_task_completion(task_name, success=True)
151
+
152
+ return result
153
+ except Exception as e:
154
+ telemetry.track_error(type(e).__name__)
155
+ if task:
156
+ task_name = task.name if hasattr(task, 'name') else f"task_{task_id}"
157
+ telemetry.track_task_completion(task_name, success=False)
158
+ raise
159
+
160
+ workflow.execute_task = instrumented_execute_task
161
+
162
+ # Wrap the start method
163
+ original_start = workflow.start
164
+
165
+ @wraps(original_start)
166
+ def instrumented_start(*args, **kwargs):
167
+ try:
168
+ result = original_start(*args, **kwargs)
169
+ # Don't double-track here since agent.chat already tracks execution
170
+ return result
171
+ except Exception as e:
172
+ telemetry.track_error(type(e).__name__)
173
+ raise
174
+
175
+ workflow.start = instrumented_start
176
+
177
+ # Also wrap astart if it exists (async version)
178
+ if hasattr(workflow, 'astart'):
179
+ original_astart = workflow.astart
180
+
181
+ @wraps(original_astart)
182
+ async def instrumented_astart(*args, **kwargs):
183
+ try:
184
+ result = await original_astart(*args, **kwargs)
185
+ # Don't double-track here since agent.chat already tracks execution
186
+ return result
187
+ except Exception as e:
188
+ telemetry.track_error(type(e).__name__)
189
+ raise
190
+
191
+ workflow.astart = instrumented_astart
192
+
193
+ # Mark workflow as instrumented to avoid double instrumentation
194
+ workflow._telemetry_instrumented = True
195
+
196
+ return workflow
197
+
198
+
199
+ # Auto-instrumentation helper
200
+ def auto_instrument_all(telemetry: Optional['MinimalTelemetry'] = None):
201
+ """
202
+ Automatically instrument all new instances of Agent and PraisonAIAgents.
203
+ This should be called after enabling telemetry.
204
+
205
+ Args:
206
+ telemetry: Optional telemetry instance (uses global if not provided)
207
+ """
208
+ if not telemetry:
209
+ from .telemetry import get_telemetry
210
+ telemetry = get_telemetry()
211
+
212
+ if not telemetry.enabled:
213
+ return
214
+
215
+ try:
216
+ # Import the classes
217
+ from ..agent.agent import Agent
218
+ from ..agents.agents import PraisonAIAgents
219
+
220
+ # Store original __init__ methods
221
+ original_agent_init = Agent.__init__
222
+ original_workflow_init = PraisonAIAgents.__init__
223
+
224
+ # Wrap Agent.__init__
225
+ @wraps(original_agent_init)
226
+ def agent_init_wrapper(self, *args, **kwargs):
227
+ original_agent_init(self, *args, **kwargs)
228
+ instrument_agent(self, telemetry)
229
+
230
+ # Wrap PraisonAIAgents.__init__
231
+ @wraps(original_workflow_init)
232
+ def workflow_init_wrapper(self, *args, **kwargs):
233
+ original_workflow_init(self, *args, **kwargs)
234
+ instrument_workflow(self, telemetry)
235
+
236
+ # Apply wrapped constructors
237
+ Agent.__init__ = agent_init_wrapper
238
+ PraisonAIAgents.__init__ = workflow_init_wrapper
239
+
240
+ except ImportError:
241
+ # Classes not available, skip auto-instrumentation
242
+ pass
@@ -0,0 +1,350 @@
1
+ """
2
+ Minimal telemetry implementation for PraisonAI Agents.
3
+
4
+ This module provides anonymous usage tracking with privacy-first design.
5
+ All telemetry is opt-out via environment variables.
6
+ """
7
+
8
+ import os
9
+ import time
10
+ import platform
11
+ import hashlib
12
+ from typing import Dict, Any, Optional
13
+ from datetime import datetime
14
+ import logging
15
+
16
+ # Try to import PostHog
17
+ try:
18
+ from posthog import Posthog
19
+ POSTHOG_AVAILABLE = True
20
+ except ImportError:
21
+ POSTHOG_AVAILABLE = False
22
+
23
+ # Check for opt-out environment variables
24
+ _TELEMETRY_DISABLED = any([
25
+ os.environ.get('PRAISONAI_TELEMETRY_DISABLED', '').lower() in ('true', '1', 'yes'),
26
+ os.environ.get('PRAISONAI_DISABLE_TELEMETRY', '').lower() in ('true', '1', 'yes'),
27
+ os.environ.get('DO_NOT_TRACK', '').lower() in ('true', '1', 'yes'),
28
+ ])
29
+
30
+
31
+ class MinimalTelemetry:
32
+ """
33
+ Minimal telemetry collector for anonymous usage tracking.
34
+
35
+ Privacy guarantees:
36
+ - No personal data is collected
37
+ - No prompts, responses, or user content is tracked
38
+ - Only anonymous metrics about feature usage
39
+ - Respects DO_NOT_TRACK standard
40
+ - Can be disabled via environment variables
41
+ """
42
+
43
+ def __init__(self, enabled: bool = None):
44
+ """
45
+ Initialize the minimal telemetry collector.
46
+
47
+ Args:
48
+ enabled: Override the environment-based enable/disable setting
49
+ """
50
+ # Respect explicit enabled parameter, otherwise check environment
51
+ if enabled is not None:
52
+ self.enabled = enabled
53
+ else:
54
+ self.enabled = not _TELEMETRY_DISABLED
55
+
56
+ self.logger = logging.getLogger(__name__)
57
+
58
+ if not self.enabled:
59
+ self.logger.debug("Telemetry is disabled")
60
+ return
61
+
62
+ # Generate anonymous session ID (not user ID)
63
+ session_data = f"{datetime.now().isoformat()}-{os.getpid()}-{time.time()}"
64
+ self.session_id = hashlib.sha256(session_data.encode()).hexdigest()[:16]
65
+
66
+ # Basic metrics storage
67
+ self._metrics = {
68
+ "agent_executions": 0,
69
+ "task_completions": 0,
70
+ "tool_calls": 0,
71
+ "errors": 0,
72
+ }
73
+
74
+ # Collect basic environment info (anonymous)
75
+ self._environment = {
76
+ "python_version": platform.python_version(),
77
+ "os_type": platform.system(),
78
+ "framework_version": self._get_framework_version(),
79
+ }
80
+
81
+ self.logger.debug(f"Telemetry enabled with session {self.session_id}")
82
+
83
+ # Initialize PostHog if available
84
+ if POSTHOG_AVAILABLE:
85
+ try:
86
+ self._posthog = Posthog(
87
+ project_api_key='phc_skZpl3eFLQJ4iYjsERNMbCO6jfeSJi2vyZlPahKgxZ7',
88
+ host='https://eu.i.posthog.com',
89
+ disable_geoip=True
90
+ )
91
+ except:
92
+ self._posthog = None
93
+ else:
94
+ self._posthog = None
95
+
96
+ def _get_framework_version(self) -> str:
97
+ """Get the PraisonAI Agents version."""
98
+ try:
99
+ from .. import __version__
100
+ return __version__
101
+ except ImportError:
102
+ return "unknown"
103
+
104
+ def track_agent_execution(self, agent_name: str = None, success: bool = True):
105
+ """
106
+ Track an agent execution event.
107
+
108
+ Args:
109
+ agent_name: Name of the agent (not logged, just for counting)
110
+ success: Whether the execution was successful
111
+ """
112
+ if not self.enabled:
113
+ return
114
+
115
+ self._metrics["agent_executions"] += 1
116
+
117
+ # In a real implementation, this would send to a backend
118
+ # For now, just log at debug level
119
+ self.logger.debug(f"Agent execution tracked: success={success}")
120
+
121
+ def track_task_completion(self, task_name: str = None, success: bool = True):
122
+ """
123
+ Track a task completion event.
124
+
125
+ Args:
126
+ task_name: Name of the task (not logged, just for counting)
127
+ success: Whether the task completed successfully
128
+ """
129
+ if not self.enabled:
130
+ return
131
+
132
+ self._metrics["task_completions"] += 1
133
+
134
+ self.logger.debug(f"Task completion tracked: success={success}")
135
+
136
+ def track_tool_usage(self, tool_name: str, success: bool = True):
137
+ """
138
+ Track tool usage event.
139
+
140
+ Args:
141
+ tool_name: Name of the tool being used
142
+ success: Whether the tool call was successful
143
+ """
144
+ if not self.enabled:
145
+ return
146
+
147
+ self._metrics["tool_calls"] += 1
148
+
149
+ # Only track tool name, not arguments or results
150
+ self.logger.debug(f"Tool usage tracked: {tool_name}, success={success}")
151
+
152
+ def track_error(self, error_type: str = None):
153
+ """
154
+ Track an error event.
155
+
156
+ Args:
157
+ error_type: Type of error (not the full message)
158
+ """
159
+ if not self.enabled:
160
+ return
161
+
162
+ self._metrics["errors"] += 1
163
+
164
+ # Only track error type, not full error messages
165
+ self.logger.debug(f"Error tracked: type={error_type or 'unknown'}")
166
+
167
+ def track_feature_usage(self, feature_name: str):
168
+ """
169
+ Track usage of a specific feature.
170
+
171
+ Args:
172
+ feature_name: Name of the feature being used
173
+ """
174
+ if not self.enabled:
175
+ return
176
+
177
+ # Track which features are being used
178
+ self.logger.debug(f"Feature usage tracked: {feature_name}")
179
+
180
+ def get_metrics(self) -> Dict[str, Any]:
181
+ """
182
+ Get current metrics summary.
183
+
184
+ Returns:
185
+ Dictionary of current metrics
186
+ """
187
+ if not self.enabled:
188
+ return {"enabled": False}
189
+
190
+ return {
191
+ "enabled": True,
192
+ "session_id": self.session_id,
193
+ "metrics": self._metrics.copy(),
194
+ "environment": self._environment.copy(),
195
+ }
196
+
197
+ def flush(self):
198
+ """
199
+ Flush any pending telemetry data.
200
+
201
+ In a real implementation, this would send data to a backend.
202
+ """
203
+ if not self.enabled:
204
+ return
205
+
206
+ metrics = self.get_metrics()
207
+ self.logger.debug(f"Telemetry flush: {metrics}")
208
+
209
+ # Send to PostHog if available
210
+ if hasattr(self, '_posthog') and self._posthog:
211
+
212
+ try:
213
+ self._posthog.capture(
214
+ distinct_id='anonymous',
215
+ event='sdk_used',
216
+ properties={
217
+ 'version': self._environment['framework_version'],
218
+ 'os': platform.system(),
219
+ '$process_person_profile': False,
220
+ '$geoip_disable': True
221
+ }
222
+ )
223
+ except:
224
+ pass
225
+
226
+ # Reset counters
227
+ for key in self._metrics:
228
+ if isinstance(self._metrics[key], int):
229
+ self._metrics[key] = 0
230
+
231
+
232
+ # Global telemetry instance
233
+ _telemetry_instance = None
234
+
235
+
236
+ def get_telemetry() -> MinimalTelemetry:
237
+ """
238
+ Get the global telemetry instance.
239
+
240
+ Returns:
241
+ The global MinimalTelemetry instance
242
+ """
243
+ global _telemetry_instance
244
+ if _telemetry_instance is None:
245
+ _telemetry_instance = MinimalTelemetry()
246
+ return _telemetry_instance
247
+
248
+
249
+ def disable_telemetry():
250
+ """Programmatically disable telemetry."""
251
+ global _telemetry_instance
252
+ if _telemetry_instance:
253
+ _telemetry_instance.enabled = False
254
+ else:
255
+ _telemetry_instance = MinimalTelemetry(enabled=False)
256
+
257
+
258
+ def enable_telemetry():
259
+ """Programmatically enable telemetry (if not disabled by environment)."""
260
+ global _telemetry_instance
261
+ if not _TELEMETRY_DISABLED:
262
+ if _telemetry_instance:
263
+ _telemetry_instance.enabled = True
264
+ else:
265
+ _telemetry_instance = MinimalTelemetry(enabled=True)
266
+
267
+
268
+ # For backward compatibility with existing code
269
+ class TelemetryCollector:
270
+ """Backward compatibility wrapper for the old TelemetryCollector interface."""
271
+
272
+ def __init__(self, backend: str = "minimal", service_name: str = "praisonai-agents", **kwargs):
273
+ self.telemetry = get_telemetry()
274
+
275
+ def start(self):
276
+ """Start telemetry collection."""
277
+ # No-op for minimal implementation
278
+ pass
279
+
280
+ def stop(self):
281
+ """Stop telemetry collection and flush data."""
282
+ self.telemetry.flush()
283
+
284
+ def trace_agent_execution(self, agent_name: str, **attributes):
285
+ """Compatibility method for agent execution tracking."""
286
+ from contextlib import contextmanager
287
+
288
+ @contextmanager
289
+ def _trace():
290
+ try:
291
+ yield None
292
+ self.telemetry.track_agent_execution(agent_name, success=True)
293
+ except Exception:
294
+ self.telemetry.track_agent_execution(agent_name, success=False)
295
+ raise
296
+
297
+ return _trace()
298
+
299
+ def trace_task_execution(self, task_name: str, agent_name: str = None, **attributes):
300
+ """Compatibility method for task execution tracking."""
301
+ from contextlib import contextmanager
302
+
303
+ @contextmanager
304
+ def _trace():
305
+ try:
306
+ yield None
307
+ self.telemetry.track_task_completion(task_name, success=True)
308
+ except Exception:
309
+ self.telemetry.track_task_completion(task_name, success=False)
310
+ raise
311
+
312
+ return _trace()
313
+
314
+ def trace_tool_call(self, tool_name: str, **attributes):
315
+ """Compatibility method for tool call tracking."""
316
+ from contextlib import contextmanager
317
+
318
+ @contextmanager
319
+ def _trace():
320
+ try:
321
+ yield None
322
+ self.telemetry.track_tool_usage(tool_name, success=True)
323
+ except Exception:
324
+ self.telemetry.track_tool_usage(tool_name, success=False)
325
+ raise
326
+
327
+ return _trace()
328
+
329
+ def trace_llm_call(self, model: str = None, **attributes):
330
+ """Compatibility method for LLM call tracking."""
331
+ from contextlib import contextmanager
332
+
333
+ @contextmanager
334
+ def _trace():
335
+ # We don't track LLM calls in minimal telemetry
336
+ yield None
337
+
338
+ return _trace()
339
+
340
+ def record_tokens(self, prompt_tokens: int, completion_tokens: int, model: str = None):
341
+ """Compatibility method - we don't track token usage."""
342
+ pass
343
+
344
+ def record_cost(self, cost: float, model: str = None):
345
+ """Compatibility method - we don't track costs."""
346
+ pass
347
+
348
+ def get_metrics(self) -> Dict[str, Any]:
349
+ """Get current metrics."""
350
+ return self.telemetry.get_metrics()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praisonaiagents
3
- Version: 0.0.101
3
+ Version: 0.0.103
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
@@ -8,13 +8,14 @@ Requires-Dist: pydantic
8
8
  Requires-Dist: rich
9
9
  Requires-Dist: openai
10
10
  Requires-Dist: mcp>=1.6.0
11
+ Requires-Dist: posthog>=4.0.0
11
12
  Provides-Extra: mcp
12
13
  Requires-Dist: mcp>=1.6.0; extra == "mcp"
13
14
  Requires-Dist: fastapi>=0.115.0; extra == "mcp"
14
15
  Requires-Dist: uvicorn>=0.34.0; extra == "mcp"
15
16
  Provides-Extra: memory
16
17
  Requires-Dist: chromadb>=1.0.0; extra == "memory"
17
- Requires-Dist: litellm>=1.50.0; extra == "memory"
18
+ Requires-Dist: litellm>=1.72.0; extra == "memory"
18
19
  Provides-Extra: knowledge
19
20
  Requires-Dist: mem0ai>=0.1.0; extra == "knowledge"
20
21
  Requires-Dist: chromadb>=1.0.0; extra == "knowledge"
@@ -24,11 +25,13 @@ Provides-Extra: graph
24
25
  Requires-Dist: mem0ai[graph]>=0.1.0; extra == "graph"
25
26
  Requires-Dist: chromadb>=1.0.0; extra == "graph"
26
27
  Provides-Extra: llm
27
- Requires-Dist: litellm>=1.50.0; extra == "llm"
28
+ Requires-Dist: litellm>=1.72.0; extra == "llm"
28
29
  Requires-Dist: pydantic>=2.4.2; extra == "llm"
29
30
  Provides-Extra: api
30
31
  Requires-Dist: fastapi>=0.115.0; extra == "api"
31
32
  Requires-Dist: uvicorn>=0.34.0; extra == "api"
33
+ Provides-Extra: telemetry
34
+ Requires-Dist: posthog>=4.0.0; extra == "telemetry"
32
35
  Provides-Extra: all
33
36
  Requires-Dist: praisonaiagents[memory]; extra == "all"
34
37
  Requires-Dist: praisonaiagents[knowledge]; extra == "all"
@@ -36,3 +39,4 @@ Requires-Dist: praisonaiagents[graph]; extra == "all"
36
39
  Requires-Dist: praisonaiagents[llm]; extra == "all"
37
40
  Requires-Dist: praisonaiagents[mcp]; extra == "all"
38
41
  Requires-Dist: praisonaiagents[api]; extra == "all"
42
+ Requires-Dist: praisonaiagents[telemetry]; extra == "all"
@@ -1,4 +1,4 @@
1
- praisonaiagents/__init__.py,sha256=GmTiMNta4iwmfarh_6cTUpry50hpqFE8YqolrYfZ_7U,1465
1
+ praisonaiagents/__init__.py,sha256=10wExtoVkZ31OwxoKjj9gtWq2uvC05dYBommV2Eii4M,2769
2
2
  praisonaiagents/approval.py,sha256=UJ4OhfihpFGR5CAaMphqpSvqdZCHi5w2MGw1MByZ1FQ,9813
3
3
  praisonaiagents/main.py,sha256=_-XE7_Y7ChvtLQMivfNFrrnAhv4wSSDhH9WJMWlkS0w,16315
4
4
  praisonaiagents/session.py,sha256=CI-ffCiOfmgB-1zFFik9daKCB5Sm41Q9ZOaq1-oSLW8,9250
@@ -14,17 +14,20 @@ praisonaiagents/guardrails/llm_guardrail.py,sha256=MTTqmYDdZX-18QN9T17T5P_6H2qnV
14
14
  praisonaiagents/knowledge/__init__.py,sha256=xL1Eh-a3xsHyIcU4foOWF-JdWYIYBALJH9bge0Ujuto,246
15
15
  praisonaiagents/knowledge/chunking.py,sha256=G6wyHa7_8V0_7VpnrrUXbEmUmptlT16ISJYaxmkSgmU,7678
16
16
  praisonaiagents/knowledge/knowledge.py,sha256=OKPar-XGyAp1ndmbOOdCgqFnTCqpOThYVSIZRxZyP58,15683
17
- praisonaiagents/llm/__init__.py,sha256=ttPQQJQq6Tah-0updoEXDZFKWtJAM93rBWRoIgxRWO8,689
18
- praisonaiagents/llm/llm.py,sha256=hoIxHzo9aNygeOiw9RtoPhpuSCVTUrKPe3OPvsT5qLc,98212
17
+ praisonaiagents/llm/__init__.py,sha256=bSywIHBHH0YUf4hSx-FmFXkRv2g1Rlhuk-gjoImE8j8,925
18
+ praisonaiagents/llm/llm.py,sha256=4AyXTgcolemKf4kHQOwQAIE4MJ0z-CuGs9F7UkHbyfE,98385
19
19
  praisonaiagents/mcp/__init__.py,sha256=ibbqe3_7XB7VrIcUcetkZiUZS1fTVvyMy_AqCSFG8qc,240
20
20
  praisonaiagents/mcp/mcp.py,sha256=_gfp8hrSVT9aPqEDDfU8MiCdg0-3dVQpEQUE6AbrJlo,17243
21
21
  praisonaiagents/mcp/mcp_sse.py,sha256=DLh3F_aoVRM1X-7hgIOWOw4FQ1nGmn9YNbQTesykzn4,6792
22
22
  praisonaiagents/memory/__init__.py,sha256=aEFdhgtTqDdMhc_JCWM-f4XI9cZIj7Wz5g_MUa-0amg,397
23
- praisonaiagents/memory/memory.py,sha256=x6CEMYhgzvlJH6SGKHPLRDt6kF0DVFFSUQbgr1OK3JM,38729
23
+ praisonaiagents/memory/memory.py,sha256=eYXVvuXrvt4LaEJ-AAbAiwpFUCuS5LH5F7Z0cBW5_gQ,42186
24
24
  praisonaiagents/process/__init__.py,sha256=lkYbL7Hn5a0ldvJtkdH23vfIIZLIcanK-65C0MwaorY,52
25
25
  praisonaiagents/process/process.py,sha256=gxhMXG3s4CzaREyuwE5zxCMx2Wp_b_Wd53tDfkj8Qk8,66567
26
26
  praisonaiagents/task/__init__.py,sha256=VL5hXVmyGjINb34AalxpBMl-YW9m5EDcRkMTKkSSl7c,80
27
27
  praisonaiagents/task/task.py,sha256=imqJ8wzZzVyUSym2EyF2tC-vAsV1UdfI_P3YM5mqAiw,20786
28
+ praisonaiagents/telemetry/__init__.py,sha256=5iAOrj_N_cKMmh2ltWGYs3PfOYt_jcwUoElW8fTAIsc,3062
29
+ praisonaiagents/telemetry/integration.py,sha256=36vvYac8tW92YzQYbBeKWKM8JC9IiizlxhUy3AFqPlA,8667
30
+ praisonaiagents/telemetry/telemetry.py,sha256=T2Mv_iOXYbL-C3CZW5EEEs7N0dUk1S2xrD1FQjVaxmc,11064
28
31
  praisonaiagents/tools/README.md,sha256=bIQGTSqQbC8l_UvTAnKbnh1TxrybSFGbCqxnhvDwkE4,4450
29
32
  praisonaiagents/tools/__init__.py,sha256=Rrgi7_3-yLHpfBB81WUi0-wD_wb_BsukwHVdjDYAF-0,9316
30
33
  praisonaiagents/tools/arxiv_tools.py,sha256=1stb31zTjLTon4jCnpZG5de9rKc9QWgC0leLegvPXWo,10528
@@ -48,7 +51,7 @@ praisonaiagents/tools/xml_tools.py,sha256=iYTMBEk5l3L3ryQ1fkUnNVYK-Nnua2Kx2S0dxN
48
51
  praisonaiagents/tools/yaml_tools.py,sha256=uogAZrhXV9O7xvspAtcTfpKSQYL2nlOTvCQXN94-G9A,14215
49
52
  praisonaiagents/tools/yfinance_tools.py,sha256=s2PBj_1v7oQnOobo2fDbQBACEHl61ftG4beG6Z979ZE,8529
50
53
  praisonaiagents/tools/train/data/generatecot.py,sha256=H6bNh-E2hqL5MW6kX3hqZ05g9ETKN2-kudSjiuU_SD8,19403
51
- praisonaiagents-0.0.101.dist-info/METADATA,sha256=E7LFE0liQmrpsCnURK80ynkYPxMKuNTTSJM4YKUS57o,1503
52
- praisonaiagents-0.0.101.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- praisonaiagents-0.0.101.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
54
- praisonaiagents-0.0.101.dist-info/RECORD,,
54
+ praisonaiagents-0.0.103.dist-info/METADATA,sha256=QPwe9pWnpP519hGH179MF5Vt_Jt0uD-9F-O5D5IhK84,1669
55
+ praisonaiagents-0.0.103.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ praisonaiagents-0.0.103.dist-info/top_level.txt,sha256=_HsRddrJ23iDx5TTqVUVvXG2HeHBL5voshncAMDGjtA,16
57
+ praisonaiagents-0.0.103.dist-info/RECORD,,