prela 0.1.0__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.
Files changed (71) hide show
  1. prela/__init__.py +394 -0
  2. prela/_version.py +3 -0
  3. prela/contrib/CLI.md +431 -0
  4. prela/contrib/README.md +118 -0
  5. prela/contrib/__init__.py +5 -0
  6. prela/contrib/cli.py +1063 -0
  7. prela/contrib/explorer.py +571 -0
  8. prela/core/__init__.py +64 -0
  9. prela/core/clock.py +98 -0
  10. prela/core/context.py +228 -0
  11. prela/core/replay.py +403 -0
  12. prela/core/sampler.py +178 -0
  13. prela/core/span.py +295 -0
  14. prela/core/tracer.py +498 -0
  15. prela/evals/__init__.py +94 -0
  16. prela/evals/assertions/README.md +484 -0
  17. prela/evals/assertions/__init__.py +78 -0
  18. prela/evals/assertions/base.py +90 -0
  19. prela/evals/assertions/multi_agent.py +625 -0
  20. prela/evals/assertions/semantic.py +223 -0
  21. prela/evals/assertions/structural.py +443 -0
  22. prela/evals/assertions/tool.py +380 -0
  23. prela/evals/case.py +370 -0
  24. prela/evals/n8n/__init__.py +69 -0
  25. prela/evals/n8n/assertions.py +450 -0
  26. prela/evals/n8n/runner.py +497 -0
  27. prela/evals/reporters/README.md +184 -0
  28. prela/evals/reporters/__init__.py +32 -0
  29. prela/evals/reporters/console.py +251 -0
  30. prela/evals/reporters/json.py +176 -0
  31. prela/evals/reporters/junit.py +278 -0
  32. prela/evals/runner.py +525 -0
  33. prela/evals/suite.py +316 -0
  34. prela/exporters/__init__.py +27 -0
  35. prela/exporters/base.py +189 -0
  36. prela/exporters/console.py +443 -0
  37. prela/exporters/file.py +322 -0
  38. prela/exporters/http.py +394 -0
  39. prela/exporters/multi.py +154 -0
  40. prela/exporters/otlp.py +388 -0
  41. prela/instrumentation/ANTHROPIC.md +297 -0
  42. prela/instrumentation/LANGCHAIN.md +480 -0
  43. prela/instrumentation/OPENAI.md +59 -0
  44. prela/instrumentation/__init__.py +49 -0
  45. prela/instrumentation/anthropic.py +1436 -0
  46. prela/instrumentation/auto.py +129 -0
  47. prela/instrumentation/base.py +436 -0
  48. prela/instrumentation/langchain.py +959 -0
  49. prela/instrumentation/llamaindex.py +719 -0
  50. prela/instrumentation/multi_agent/__init__.py +48 -0
  51. prela/instrumentation/multi_agent/autogen.py +357 -0
  52. prela/instrumentation/multi_agent/crewai.py +404 -0
  53. prela/instrumentation/multi_agent/langgraph.py +299 -0
  54. prela/instrumentation/multi_agent/models.py +203 -0
  55. prela/instrumentation/multi_agent/swarm.py +231 -0
  56. prela/instrumentation/n8n/__init__.py +68 -0
  57. prela/instrumentation/n8n/code_node.py +534 -0
  58. prela/instrumentation/n8n/models.py +336 -0
  59. prela/instrumentation/n8n/webhook.py +489 -0
  60. prela/instrumentation/openai.py +1198 -0
  61. prela/license.py +245 -0
  62. prela/replay/__init__.py +31 -0
  63. prela/replay/comparison.py +390 -0
  64. prela/replay/engine.py +1227 -0
  65. prela/replay/loader.py +231 -0
  66. prela/replay/result.py +196 -0
  67. prela-0.1.0.dist-info/METADATA +399 -0
  68. prela-0.1.0.dist-info/RECORD +71 -0
  69. prela-0.1.0.dist-info/WHEEL +4 -0
  70. prela-0.1.0.dist-info/entry_points.txt +2 -0
  71. prela-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,48 @@
1
+ """Multi-agent framework instrumentation for Prela.
2
+
3
+ This module provides instrumentation for multi-agent frameworks including:
4
+ - CrewAI: Task-based multi-agent orchestration
5
+ - AutoGen: Conversational multi-agent framework
6
+ - LangGraph: Graph-based multi-agent workflows
7
+ - Swarm: OpenAI's experimental multi-agent framework
8
+
9
+ Each instrumentor captures agent definitions, inter-agent messages, task assignments,
10
+ and conversation turns to provide complete observability for multi-agent systems.
11
+ """
12
+
13
+ from prela.instrumentation.multi_agent.models import (
14
+ AgentDefinition,
15
+ AgentMessage,
16
+ AgentRole,
17
+ ConversationTurn,
18
+ CrewExecution,
19
+ MessageType,
20
+ TaskAssignment,
21
+ extract_agent_graph,
22
+ generate_agent_id,
23
+ )
24
+
25
+ # Instrumentors
26
+ from prela.instrumentation.multi_agent.autogen import AutoGenInstrumentor
27
+ from prela.instrumentation.multi_agent.crewai import CrewAIInstrumentor
28
+ from prela.instrumentation.multi_agent.langgraph import LangGraphInstrumentor
29
+ from prela.instrumentation.multi_agent.swarm import SwarmInstrumentor
30
+
31
+ __all__ = [
32
+ # Data models
33
+ "AgentDefinition",
34
+ "AgentMessage",
35
+ "TaskAssignment",
36
+ "CrewExecution",
37
+ "ConversationTurn",
38
+ "AgentRole",
39
+ "MessageType",
40
+ # Helper functions
41
+ "generate_agent_id",
42
+ "extract_agent_graph",
43
+ # Instrumentors
44
+ "AutoGenInstrumentor",
45
+ "CrewAIInstrumentor",
46
+ "LangGraphInstrumentor",
47
+ "SwarmInstrumentor",
48
+ ]
@@ -0,0 +1,357 @@
1
+ """AutoGen instrumentation for Prela.
2
+
3
+ This module provides automatic instrumentation for Microsoft AutoGen (>=0.2.0),
4
+ capturing multi-agent conversations, group chats, and agent interactions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import functools
10
+ import uuid
11
+ from datetime import datetime
12
+ from typing import Any, Optional
13
+
14
+ from prela.core.span import SpanType
15
+ from prela.core.tracer import Tracer, get_tracer
16
+ from prela.instrumentation.base import Instrumentor
17
+ from prela.instrumentation.multi_agent.models import (
18
+ AgentDefinition,
19
+ AgentMessage,
20
+ AgentRole,
21
+ ConversationTurn,
22
+ MessageType,
23
+ generate_agent_id,
24
+ )
25
+ from prela.license import require_tier
26
+
27
+
28
+ class AutoGenInstrumentor(Instrumentor):
29
+ """Instrumentor for Microsoft AutoGen multi-agent framework."""
30
+
31
+ FRAMEWORK = "autogen"
32
+
33
+ @property
34
+ def is_instrumented(self) -> bool:
35
+ """Check if AutoGen is currently instrumented."""
36
+ return self._is_instrumented
37
+
38
+ def __init__(self):
39
+ super().__init__()
40
+ self._active_conversations: dict[str, list[ConversationTurn]] = {}
41
+ self._is_instrumented = False
42
+ self._tracer: Optional[Tracer] = None
43
+
44
+ @require_tier("AutoGen instrumentation", "lunch-money")
45
+ def instrument(self, tracer: Optional[Tracer] = None) -> None:
46
+ """Patch AutoGen classes for tracing.
47
+
48
+ Args:
49
+ tracer: Optional tracer to use. If None, uses global tracer.
50
+ """
51
+ if self.is_instrumented:
52
+ return
53
+
54
+ # Try both autogen and pyautogen package names
55
+ try:
56
+ import autogen
57
+ except ImportError:
58
+ try:
59
+ import pyautogen as autogen # type: ignore
60
+ except ImportError:
61
+ return # AutoGen not installed
62
+
63
+ self._tracer = tracer or get_tracer()
64
+
65
+ # Patch ConversableAgent (base class for most agents)
66
+ if hasattr(autogen, "ConversableAgent"):
67
+ self._patch_conversable_agent(autogen.ConversableAgent)
68
+
69
+ # Patch GroupChat for multi-agent conversations
70
+ if hasattr(autogen, "GroupChat"):
71
+ self._patch_group_chat(autogen.GroupChat)
72
+
73
+ # Patch GroupChatManager
74
+ if hasattr(autogen, "GroupChatManager"):
75
+ self._patch_group_chat_manager(autogen.GroupChatManager)
76
+
77
+ self._is_instrumented = True
78
+
79
+ def uninstrument(self) -> None:
80
+ """Restore original methods."""
81
+ if not self.is_instrumented:
82
+ return
83
+
84
+ # Try both package names
85
+ try:
86
+ import autogen
87
+ except ImportError:
88
+ try:
89
+ import pyautogen as autogen # type: ignore
90
+ except ImportError:
91
+ return
92
+
93
+ # Restore all patched methods
94
+ for module_name, obj_name, method_name in [
95
+ ("autogen", "ConversableAgent", "initiate_chat"),
96
+ ("autogen", "ConversableAgent", "generate_reply"),
97
+ ("autogen", "GroupChat", "select_speaker"),
98
+ ("autogen", "GroupChatManager", "run_chat"),
99
+ ]:
100
+ try:
101
+ # Try autogen first, then pyautogen
102
+ try:
103
+ module = __import__(module_name, fromlist=[obj_name])
104
+ except ImportError:
105
+ module = __import__("py" + module_name, fromlist=[obj_name])
106
+
107
+ cls = getattr(module, obj_name, None)
108
+ if cls and hasattr(cls, f"_prela_original_{method_name}"):
109
+ original = getattr(cls, f"_prela_original_{method_name}")
110
+ setattr(cls, method_name, original)
111
+ delattr(cls, f"_prela_original_{method_name}")
112
+ except (ImportError, AttributeError):
113
+ pass
114
+
115
+ self._is_instrumented = False
116
+ self._tracer = None
117
+
118
+ def _patch_conversable_agent(self, agent_cls) -> None:
119
+ """Patch ConversableAgent for tracing."""
120
+ # Patch initiate_chat (main conversation entry point)
121
+ if hasattr(agent_cls, "initiate_chat"):
122
+ if hasattr(agent_cls, "_prela_original_initiate_chat"):
123
+ return
124
+
125
+ original_initiate_chat = agent_cls.initiate_chat
126
+ agent_cls._prela_original_initiate_chat = original_initiate_chat
127
+
128
+ instrumentor = self
129
+
130
+ @functools.wraps(original_initiate_chat)
131
+ def wrapped_initiate_chat(agent_self, recipient, *args, **kwargs):
132
+ tracer = instrumentor._tracer
133
+ if not tracer:
134
+ return original_initiate_chat(agent_self, recipient, *args, **kwargs)
135
+
136
+ conversation_id = str(uuid.uuid4())
137
+ instrumentor._active_conversations[conversation_id] = []
138
+
139
+ # Extract initial message
140
+ message = kwargs.get("message") or (args[0] if args else None)
141
+
142
+ # Create conversation attributes
143
+ conversation_attributes = {
144
+ "conversation.id": conversation_id,
145
+ "conversation.framework": instrumentor.FRAMEWORK,
146
+ "conversation.initiator": agent_self.name,
147
+ "conversation.recipient": recipient.name,
148
+ "conversation.initial_message": (
149
+ str(message)[:500] if message else None
150
+ ),
151
+ "conversation.max_turns": kwargs.get("max_turns"),
152
+ }
153
+
154
+ # NEW: Replay capture if enabled
155
+ replay_capture = None
156
+ if tracer.capture_for_replay:
157
+ from prela.core.replay import ReplayCapture
158
+
159
+ replay_capture = ReplayCapture()
160
+ # Capture agent context
161
+ replay_capture.set_agent_context(
162
+ system_prompt=getattr(agent_self, "system_message", None),
163
+ available_tools=[
164
+ {"name": name, "description": str(func)}
165
+ for name, func in getattr(agent_self, "_function_map", {}).items()
166
+ ],
167
+ memory={"messages": getattr(agent_self, "chat_messages", {})},
168
+ config={
169
+ "framework": instrumentor.FRAMEWORK,
170
+ "conversation_id": conversation_id,
171
+ "initiator": agent_self.name,
172
+ "recipient": recipient.name,
173
+ "max_turns": kwargs.get("max_turns"),
174
+ },
175
+ )
176
+
177
+ with tracer.span(
178
+ name=f"autogen.conversation.{agent_self.name}->{recipient.name}",
179
+ span_type=SpanType.AGENT,
180
+ attributes=conversation_attributes,
181
+ ) as span:
182
+ try:
183
+ result = original_initiate_chat(agent_self, recipient, *args, **kwargs)
184
+
185
+ # Add conversation statistics
186
+ turns = instrumentor._active_conversations.get(
187
+ conversation_id, []
188
+ )
189
+ span.set_attribute("conversation.total_turns", len(turns))
190
+ span.set_attribute(
191
+ "conversation.total_tokens",
192
+ sum(t.tokens_used for t in turns),
193
+ )
194
+
195
+ # NEW: Attach replay snapshot
196
+ if replay_capture:
197
+ try:
198
+ object.__setattr__(span, "replay_snapshot", replay_capture.build())
199
+ except Exception as e:
200
+ import logging
201
+ logger = logging.getLogger(__name__)
202
+ logger.debug(f"Failed to capture replay data: {e}")
203
+
204
+ return result
205
+ except Exception as e:
206
+ span.add_event(
207
+ "exception",
208
+ attributes={
209
+ "exception.type": type(e).__name__,
210
+ "exception.message": str(e),
211
+ },
212
+ )
213
+ raise
214
+ finally:
215
+ if conversation_id in instrumentor._active_conversations:
216
+ del instrumentor._active_conversations[conversation_id]
217
+
218
+ agent_cls.initiate_chat = wrapped_initiate_chat
219
+
220
+ # Patch generate_reply (individual agent responses)
221
+ if hasattr(agent_cls, "generate_reply"):
222
+ if hasattr(agent_cls, "_prela_original_generate_reply"):
223
+ return
224
+
225
+ original_generate_reply = agent_cls.generate_reply
226
+ agent_cls._prela_original_generate_reply = original_generate_reply
227
+
228
+ instrumentor = self
229
+
230
+ @functools.wraps(original_generate_reply)
231
+ def wrapped_generate_reply(
232
+ agent_self, messages=None, sender=None, *args, **kwargs
233
+ ):
234
+ tracer = instrumentor._tracer
235
+ if not tracer:
236
+ return original_generate_reply(agent_self, messages, sender, *args, **kwargs)
237
+
238
+ agent_id = generate_agent_id(instrumentor.FRAMEWORK, agent_self.name)
239
+ sender_name = sender.name if sender else "unknown"
240
+
241
+ reply_attributes = {
242
+ "agent.id": agent_id,
243
+ "agent.name": agent_self.name,
244
+ "agent.type": type(agent_self).__name__,
245
+ "agent.framework": instrumentor.FRAMEWORK,
246
+ "reply.sender": sender_name,
247
+ "reply.num_messages": len(messages) if messages else 0,
248
+ }
249
+
250
+ with tracer.span(
251
+ name=f"autogen.agent.{agent_self.name}",
252
+ span_type=SpanType.AGENT,
253
+ attributes=reply_attributes,
254
+ ) as span:
255
+ try:
256
+ result = original_generate_reply(agent_self, messages, sender, *args, **kwargs)
257
+ if result:
258
+ span.set_attribute(
259
+ "reply.content_length", len(str(result))
260
+ )
261
+ return result
262
+ except Exception as e:
263
+ span.add_event(
264
+ "exception",
265
+ attributes={
266
+ "exception.type": type(e).__name__,
267
+ "exception.message": str(e),
268
+ },
269
+ )
270
+ raise
271
+
272
+ agent_cls.generate_reply = wrapped_generate_reply
273
+
274
+ def _patch_group_chat(self, group_chat_cls) -> None:
275
+ """Patch GroupChat for multi-agent conversations."""
276
+ if hasattr(group_chat_cls, "select_speaker"):
277
+ if hasattr(group_chat_cls, "_prela_original_select_speaker"):
278
+ return
279
+
280
+ original_select_speaker = group_chat_cls.select_speaker
281
+ group_chat_cls._prela_original_select_speaker = original_select_speaker
282
+
283
+ instrumentor = self
284
+
285
+ @functools.wraps(original_select_speaker)
286
+ def wrapped_select_speaker(gc_self, *args, **kwargs):
287
+ tracer = instrumentor._tracer
288
+ result = original_select_speaker(gc_self, *args, **kwargs)
289
+
290
+ # Get current span from context
291
+ if tracer:
292
+ try:
293
+ from prela.core.context import get_current_span
294
+
295
+ current_span = get_current_span()
296
+ if current_span and result:
297
+ current_span.add_event(
298
+ "group.speaker_selected",
299
+ attributes={
300
+ "speaker.name": result.name,
301
+ "group.num_agents": len(gc_self.agents),
302
+ },
303
+ )
304
+ except Exception:
305
+ # Defensive: don't crash if span event fails
306
+ pass
307
+
308
+ return result
309
+
310
+ group_chat_cls.select_speaker = wrapped_select_speaker
311
+
312
+ def _patch_group_chat_manager(self, manager_cls) -> None:
313
+ """Patch GroupChatManager."""
314
+ if hasattr(manager_cls, "run_chat"):
315
+ if hasattr(manager_cls, "_prela_original_run_chat"):
316
+ return
317
+
318
+ original_run_chat = manager_cls.run_chat
319
+ manager_cls._prela_original_run_chat = original_run_chat
320
+
321
+ instrumentor = self
322
+
323
+ @functools.wraps(original_run_chat)
324
+ def wrapped_run_chat(manager_self, *args, **kwargs):
325
+ tracer = instrumentor._tracer
326
+ if not tracer:
327
+ return original_run_chat(manager_self, *args, **kwargs)
328
+
329
+ group_chat = manager_self.groupchat
330
+
331
+ group_attributes = {
332
+ "group.manager": manager_self.name,
333
+ "group.framework": instrumentor.FRAMEWORK,
334
+ "group.num_agents": len(group_chat.agents) if group_chat else 0,
335
+ "group.agent_names": (
336
+ [a.name for a in group_chat.agents] if group_chat else []
337
+ ),
338
+ }
339
+
340
+ with tracer.span(
341
+ name=f"autogen.group_chat.{manager_self.name}",
342
+ span_type=SpanType.AGENT,
343
+ attributes=group_attributes,
344
+ ) as span:
345
+ try:
346
+ return original_run_chat(manager_self, *args, **kwargs)
347
+ except Exception as e:
348
+ span.add_event(
349
+ "exception",
350
+ attributes={
351
+ "exception.type": type(e).__name__,
352
+ "exception.message": str(e),
353
+ },
354
+ )
355
+ raise
356
+
357
+ manager_cls.run_chat = wrapped_run_chat