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.
- prela/__init__.py +394 -0
- prela/_version.py +3 -0
- prela/contrib/CLI.md +431 -0
- prela/contrib/README.md +118 -0
- prela/contrib/__init__.py +5 -0
- prela/contrib/cli.py +1063 -0
- prela/contrib/explorer.py +571 -0
- prela/core/__init__.py +64 -0
- prela/core/clock.py +98 -0
- prela/core/context.py +228 -0
- prela/core/replay.py +403 -0
- prela/core/sampler.py +178 -0
- prela/core/span.py +295 -0
- prela/core/tracer.py +498 -0
- prela/evals/__init__.py +94 -0
- prela/evals/assertions/README.md +484 -0
- prela/evals/assertions/__init__.py +78 -0
- prela/evals/assertions/base.py +90 -0
- prela/evals/assertions/multi_agent.py +625 -0
- prela/evals/assertions/semantic.py +223 -0
- prela/evals/assertions/structural.py +443 -0
- prela/evals/assertions/tool.py +380 -0
- prela/evals/case.py +370 -0
- prela/evals/n8n/__init__.py +69 -0
- prela/evals/n8n/assertions.py +450 -0
- prela/evals/n8n/runner.py +497 -0
- prela/evals/reporters/README.md +184 -0
- prela/evals/reporters/__init__.py +32 -0
- prela/evals/reporters/console.py +251 -0
- prela/evals/reporters/json.py +176 -0
- prela/evals/reporters/junit.py +278 -0
- prela/evals/runner.py +525 -0
- prela/evals/suite.py +316 -0
- prela/exporters/__init__.py +27 -0
- prela/exporters/base.py +189 -0
- prela/exporters/console.py +443 -0
- prela/exporters/file.py +322 -0
- prela/exporters/http.py +394 -0
- prela/exporters/multi.py +154 -0
- prela/exporters/otlp.py +388 -0
- prela/instrumentation/ANTHROPIC.md +297 -0
- prela/instrumentation/LANGCHAIN.md +480 -0
- prela/instrumentation/OPENAI.md +59 -0
- prela/instrumentation/__init__.py +49 -0
- prela/instrumentation/anthropic.py +1436 -0
- prela/instrumentation/auto.py +129 -0
- prela/instrumentation/base.py +436 -0
- prela/instrumentation/langchain.py +959 -0
- prela/instrumentation/llamaindex.py +719 -0
- prela/instrumentation/multi_agent/__init__.py +48 -0
- prela/instrumentation/multi_agent/autogen.py +357 -0
- prela/instrumentation/multi_agent/crewai.py +404 -0
- prela/instrumentation/multi_agent/langgraph.py +299 -0
- prela/instrumentation/multi_agent/models.py +203 -0
- prela/instrumentation/multi_agent/swarm.py +231 -0
- prela/instrumentation/n8n/__init__.py +68 -0
- prela/instrumentation/n8n/code_node.py +534 -0
- prela/instrumentation/n8n/models.py +336 -0
- prela/instrumentation/n8n/webhook.py +489 -0
- prela/instrumentation/openai.py +1198 -0
- prela/license.py +245 -0
- prela/replay/__init__.py +31 -0
- prela/replay/comparison.py +390 -0
- prela/replay/engine.py +1227 -0
- prela/replay/loader.py +231 -0
- prela/replay/result.py +196 -0
- prela-0.1.0.dist-info/METADATA +399 -0
- prela-0.1.0.dist-info/RECORD +71 -0
- prela-0.1.0.dist-info/WHEEL +4 -0
- prela-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|