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,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper utilities for tracing Python code executed within n8n Code nodes.
|
|
3
|
+
|
|
4
|
+
This module provides context managers and decorators that enable users to
|
|
5
|
+
instrument custom Python code running inside n8n Code nodes, creating
|
|
6
|
+
properly nested spans that integrate with the n8n workflow execution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Callable, Optional
|
|
14
|
+
|
|
15
|
+
from prela.core.clock import now
|
|
16
|
+
from prela.core.span import Span, SpanStatus, SpanType
|
|
17
|
+
from prela.core.tracer import Tracer, get_tracer
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PrelaN8nContext:
|
|
23
|
+
"""
|
|
24
|
+
Context manager for tracing custom Python code within n8n Code nodes.
|
|
25
|
+
|
|
26
|
+
This class creates a workflow-level span and node-level span that properly
|
|
27
|
+
integrate with Prela's tracing infrastructure. It provides helper methods
|
|
28
|
+
for logging LLM calls, tool calls, and retrieval operations.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
```python
|
|
32
|
+
# Inside n8n Code node
|
|
33
|
+
from prela.instrumentation.n8n import PrelaN8nContext
|
|
34
|
+
|
|
35
|
+
ctx = PrelaN8nContext(
|
|
36
|
+
workflow_id=$workflow.id,
|
|
37
|
+
workflow_name=$workflow.name,
|
|
38
|
+
execution_id=$execution.id,
|
|
39
|
+
node_name=$node.name
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
with ctx:
|
|
43
|
+
# Your custom code
|
|
44
|
+
response = call_my_llm(prompt)
|
|
45
|
+
ctx.log_llm_call(
|
|
46
|
+
model="gpt-4",
|
|
47
|
+
prompt=prompt,
|
|
48
|
+
response=response,
|
|
49
|
+
tokens={"prompt": 100, "completion": 50}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return [{"json": {"result": response}}]
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
workflow_id: str,
|
|
59
|
+
workflow_name: str,
|
|
60
|
+
execution_id: str,
|
|
61
|
+
node_name: str,
|
|
62
|
+
node_type: str = "n8n-nodes-base.code",
|
|
63
|
+
tracer: Optional[Tracer] = None,
|
|
64
|
+
api_key: Optional[str] = None,
|
|
65
|
+
endpoint: Optional[str] = None,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Initialize n8n Code node tracing context.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
workflow_id: Unique workflow identifier
|
|
72
|
+
workflow_name: Human-readable workflow name
|
|
73
|
+
execution_id: Unique execution identifier
|
|
74
|
+
node_name: Name of the Code node
|
|
75
|
+
node_type: Node type identifier (default: n8n-nodes-base.code)
|
|
76
|
+
tracer: Prela tracer instance (defaults to global tracer)
|
|
77
|
+
api_key: Optional API key for remote export
|
|
78
|
+
endpoint: Optional endpoint URL for remote export
|
|
79
|
+
"""
|
|
80
|
+
self.workflow_id = workflow_id
|
|
81
|
+
self.workflow_name = workflow_name
|
|
82
|
+
self.execution_id = execution_id
|
|
83
|
+
self.node_name = node_name
|
|
84
|
+
self.node_type = node_type
|
|
85
|
+
self.api_key = api_key
|
|
86
|
+
self.endpoint = endpoint
|
|
87
|
+
|
|
88
|
+
# Get or initialize tracer
|
|
89
|
+
if tracer is None:
|
|
90
|
+
self.tracer = get_tracer()
|
|
91
|
+
if self.tracer is None:
|
|
92
|
+
# Initialize default tracer if none exists
|
|
93
|
+
import prela
|
|
94
|
+
|
|
95
|
+
self.tracer = prela.init(
|
|
96
|
+
service_name="n8n-code-node",
|
|
97
|
+
exporter="console" if not endpoint else "http",
|
|
98
|
+
http_endpoint=endpoint,
|
|
99
|
+
api_key=api_key,
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
self.tracer = tracer
|
|
103
|
+
|
|
104
|
+
# Generate trace_id from execution_id
|
|
105
|
+
self.trace_id = f"n8n-{execution_id}"
|
|
106
|
+
|
|
107
|
+
# Spans
|
|
108
|
+
self.workflow_span: Optional[Span] = None
|
|
109
|
+
self.node_span: Optional[Span] = None
|
|
110
|
+
|
|
111
|
+
def __enter__(self) -> "PrelaN8nContext":
|
|
112
|
+
"""Start tracing context."""
|
|
113
|
+
try:
|
|
114
|
+
# Create workflow-level span
|
|
115
|
+
self.workflow_span = Span(
|
|
116
|
+
trace_id=self.trace_id,
|
|
117
|
+
parent_span_id=None,
|
|
118
|
+
name=f"n8n.workflow.{self.workflow_name}",
|
|
119
|
+
span_type=SpanType.AGENT,
|
|
120
|
+
started_at=now(),
|
|
121
|
+
attributes={
|
|
122
|
+
"n8n.workflow_id": self.workflow_id,
|
|
123
|
+
"n8n.workflow_name": self.workflow_name,
|
|
124
|
+
"n8n.execution_id": self.execution_id,
|
|
125
|
+
"service.name": "n8n",
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Create node-level span
|
|
130
|
+
self.node_span = Span(
|
|
131
|
+
trace_id=self.trace_id,
|
|
132
|
+
parent_span_id=self.workflow_span.span_id,
|
|
133
|
+
name=f"n8n.node.{self.node_name}",
|
|
134
|
+
span_type=SpanType.CUSTOM,
|
|
135
|
+
started_at=now(),
|
|
136
|
+
attributes={
|
|
137
|
+
"n8n.node_name": self.node_name,
|
|
138
|
+
"n8n.node_type": self.node_type,
|
|
139
|
+
"service.name": "n8n",
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.debug(
|
|
144
|
+
f"Started n8n Code node tracing: {self.workflow_name}/{self.node_name}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Failed to start n8n tracing context: {e}", exc_info=True)
|
|
149
|
+
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
153
|
+
"""End tracing context and export spans."""
|
|
154
|
+
try:
|
|
155
|
+
# Handle exceptions
|
|
156
|
+
if exc_type is not None:
|
|
157
|
+
if self.node_span:
|
|
158
|
+
self.node_span.set_status(SpanStatus.ERROR)
|
|
159
|
+
self.node_span.add_event(
|
|
160
|
+
name="exception",
|
|
161
|
+
attributes={
|
|
162
|
+
"exception.type": exc_type.__name__,
|
|
163
|
+
"exception.message": str(exc_val),
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
if self.workflow_span:
|
|
167
|
+
self.workflow_span.set_status(SpanStatus.ERROR)
|
|
168
|
+
|
|
169
|
+
# End spans
|
|
170
|
+
if self.node_span:
|
|
171
|
+
self.node_span.end()
|
|
172
|
+
if self.workflow_span:
|
|
173
|
+
self.workflow_span.end()
|
|
174
|
+
|
|
175
|
+
# Export spans if tracer has exporter
|
|
176
|
+
if self.tracer and self.tracer.exporter:
|
|
177
|
+
spans = [s for s in [self.workflow_span, self.node_span] if s]
|
|
178
|
+
for span in spans:
|
|
179
|
+
self.tracer.exporter.export([span])
|
|
180
|
+
|
|
181
|
+
logger.debug(f"Completed n8n Code node tracing: {self.node_name}")
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Failed to end n8n tracing context: {e}", exc_info=True)
|
|
185
|
+
|
|
186
|
+
# Don't suppress exceptions
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def log_llm_call(
|
|
190
|
+
self,
|
|
191
|
+
model: str,
|
|
192
|
+
prompt: str,
|
|
193
|
+
response: str,
|
|
194
|
+
tokens: Optional[dict] = None,
|
|
195
|
+
provider: Optional[str] = None,
|
|
196
|
+
temperature: Optional[float] = None,
|
|
197
|
+
**kwargs,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""
|
|
200
|
+
Log an LLM call within the Code node.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
model: Model identifier (e.g., "gpt-4", "claude-3-opus")
|
|
204
|
+
prompt: Input prompt text
|
|
205
|
+
response: Model response text
|
|
206
|
+
tokens: Token usage dict with "prompt", "completion", "total" keys
|
|
207
|
+
provider: AI provider (openai, anthropic, etc.)
|
|
208
|
+
temperature: Temperature parameter used
|
|
209
|
+
**kwargs: Additional attributes to attach to the span
|
|
210
|
+
"""
|
|
211
|
+
if not self.node_span:
|
|
212
|
+
logger.warning("Cannot log LLM call: node span not initialized")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# NEW: Replay capture if enabled
|
|
217
|
+
replay_capture = None
|
|
218
|
+
if self.tracer and getattr(self.tracer, "capture_for_replay", False):
|
|
219
|
+
from prela.core.replay import ReplayCapture
|
|
220
|
+
|
|
221
|
+
replay_capture = ReplayCapture()
|
|
222
|
+
replay_capture.set_llm_request(
|
|
223
|
+
model=model,
|
|
224
|
+
prompt=prompt,
|
|
225
|
+
temperature=temperature,
|
|
226
|
+
)
|
|
227
|
+
replay_capture.set_llm_response(
|
|
228
|
+
text=response,
|
|
229
|
+
prompt_tokens=tokens.get("prompt") if tokens else None,
|
|
230
|
+
completion_tokens=tokens.get("completion") if tokens else None,
|
|
231
|
+
)
|
|
232
|
+
if provider:
|
|
233
|
+
replay_capture.set_model_info(provider=provider)
|
|
234
|
+
|
|
235
|
+
# Create child LLM span
|
|
236
|
+
llm_span = Span(
|
|
237
|
+
trace_id=self.trace_id,
|
|
238
|
+
parent_span_id=self.node_span.span_id,
|
|
239
|
+
name=f"llm.{model}",
|
|
240
|
+
span_type=SpanType.LLM,
|
|
241
|
+
started_at=now(),
|
|
242
|
+
attributes={
|
|
243
|
+
"llm.model": model,
|
|
244
|
+
"llm.prompt": prompt[:500], # Truncate
|
|
245
|
+
"llm.response": response[:500], # Truncate
|
|
246
|
+
**kwargs,
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Add provider if specified
|
|
251
|
+
if provider:
|
|
252
|
+
llm_span.attributes["llm.provider"] = provider
|
|
253
|
+
|
|
254
|
+
# Add temperature if specified
|
|
255
|
+
if temperature is not None:
|
|
256
|
+
llm_span.attributes["llm.temperature"] = temperature
|
|
257
|
+
|
|
258
|
+
# Add token usage if available
|
|
259
|
+
if tokens:
|
|
260
|
+
if "prompt" in tokens:
|
|
261
|
+
llm_span.attributes["llm.prompt_tokens"] = tokens["prompt"]
|
|
262
|
+
if "completion" in tokens:
|
|
263
|
+
llm_span.attributes["llm.completion_tokens"] = tokens["completion"]
|
|
264
|
+
if "total" in tokens:
|
|
265
|
+
llm_span.attributes["llm.total_tokens"] = tokens["total"]
|
|
266
|
+
|
|
267
|
+
# NEW: Attach replay snapshot
|
|
268
|
+
if replay_capture:
|
|
269
|
+
try:
|
|
270
|
+
object.__setattr__(llm_span, "replay_snapshot", replay_capture.build())
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.debug(f"Failed to capture replay data: {e}")
|
|
273
|
+
|
|
274
|
+
# End span immediately (synchronous call)
|
|
275
|
+
llm_span.end()
|
|
276
|
+
|
|
277
|
+
# Export if tracer has exporter
|
|
278
|
+
if self.tracer and self.tracer.exporter:
|
|
279
|
+
self.tracer.exporter.export([llm_span])
|
|
280
|
+
|
|
281
|
+
logger.debug(f"Logged LLM call: {model}")
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Failed to log LLM call: {e}", exc_info=True)
|
|
285
|
+
|
|
286
|
+
def log_tool_call(
|
|
287
|
+
self,
|
|
288
|
+
tool_name: str,
|
|
289
|
+
input: Any,
|
|
290
|
+
output: Any,
|
|
291
|
+
error: Optional[str] = None,
|
|
292
|
+
**kwargs,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Log a tool call within the Code node.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
tool_name: Name of the tool/function called
|
|
299
|
+
input: Input parameters to the tool
|
|
300
|
+
output: Output result from the tool
|
|
301
|
+
error: Optional error message if tool failed
|
|
302
|
+
**kwargs: Additional attributes to attach to the span
|
|
303
|
+
"""
|
|
304
|
+
if not self.node_span:
|
|
305
|
+
logger.warning("Cannot log tool call: node span not initialized")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
# Create child tool span
|
|
310
|
+
tool_span = Span(
|
|
311
|
+
trace_id=self.trace_id,
|
|
312
|
+
parent_span_id=self.node_span.span_id,
|
|
313
|
+
name=f"tool.{tool_name}",
|
|
314
|
+
span_type=SpanType.TOOL,
|
|
315
|
+
started_at=now(),
|
|
316
|
+
attributes={
|
|
317
|
+
"tool.name": tool_name,
|
|
318
|
+
"tool.input": str(input)[:500], # Truncate
|
|
319
|
+
"tool.output": str(output)[:500], # Truncate
|
|
320
|
+
**kwargs,
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Add error if present
|
|
325
|
+
if error:
|
|
326
|
+
tool_span.set_status(SpanStatus.ERROR)
|
|
327
|
+
tool_span.attributes["tool.error"] = str(error)[:500]
|
|
328
|
+
|
|
329
|
+
# End span immediately
|
|
330
|
+
tool_span.end()
|
|
331
|
+
|
|
332
|
+
# Export if tracer has exporter
|
|
333
|
+
if self.tracer and self.tracer.exporter:
|
|
334
|
+
self.tracer.exporter.export([tool_span])
|
|
335
|
+
|
|
336
|
+
logger.debug(f"Logged tool call: {tool_name}")
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f"Failed to log tool call: {e}", exc_info=True)
|
|
340
|
+
|
|
341
|
+
def log_retrieval(
|
|
342
|
+
self,
|
|
343
|
+
query: str,
|
|
344
|
+
documents: list[dict],
|
|
345
|
+
retriever_type: Optional[str] = None,
|
|
346
|
+
similarity_top_k: Optional[int] = None,
|
|
347
|
+
**kwargs,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""
|
|
350
|
+
Log a retrieval/search operation within the Code node.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
query: Search query text
|
|
354
|
+
documents: Retrieved documents (list of dicts with text/score/metadata)
|
|
355
|
+
retriever_type: Type of retriever used (vector, keyword, hybrid)
|
|
356
|
+
similarity_top_k: Number of documents requested
|
|
357
|
+
**kwargs: Additional attributes to attach to the span
|
|
358
|
+
"""
|
|
359
|
+
if not self.node_span:
|
|
360
|
+
logger.warning("Cannot log retrieval: node span not initialized")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
# Create child retrieval span
|
|
365
|
+
retrieval_span = Span(
|
|
366
|
+
trace_id=self.trace_id,
|
|
367
|
+
parent_span_id=self.node_span.span_id,
|
|
368
|
+
name="retrieval",
|
|
369
|
+
span_type=SpanType.RETRIEVAL,
|
|
370
|
+
started_at=now(),
|
|
371
|
+
attributes={
|
|
372
|
+
"retrieval.query": query[:200], # Truncate
|
|
373
|
+
"retrieval.document_count": len(documents),
|
|
374
|
+
**kwargs,
|
|
375
|
+
},
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Add retriever type if specified
|
|
379
|
+
if retriever_type:
|
|
380
|
+
retrieval_span.attributes["retrieval.type"] = retriever_type
|
|
381
|
+
|
|
382
|
+
# Add similarity_top_k if specified
|
|
383
|
+
if similarity_top_k is not None:
|
|
384
|
+
retrieval_span.attributes["retrieval.similarity_top_k"] = (
|
|
385
|
+
similarity_top_k
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Add document details (limit to 5 docs)
|
|
389
|
+
for i, doc in enumerate(documents[:5]):
|
|
390
|
+
if "score" in doc:
|
|
391
|
+
retrieval_span.attributes[f"retrieval.document.{i}.score"] = doc[
|
|
392
|
+
"score"
|
|
393
|
+
]
|
|
394
|
+
if "text" in doc:
|
|
395
|
+
retrieval_span.attributes[f"retrieval.document.{i}.text"] = str(
|
|
396
|
+
doc["text"]
|
|
397
|
+
)[:200]
|
|
398
|
+
|
|
399
|
+
# End span immediately
|
|
400
|
+
retrieval_span.end()
|
|
401
|
+
|
|
402
|
+
# Export if tracer has exporter
|
|
403
|
+
if self.tracer and self.tracer.exporter:
|
|
404
|
+
self.tracer.exporter.export([retrieval_span])
|
|
405
|
+
|
|
406
|
+
logger.debug(f"Logged retrieval: {len(documents)} documents")
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.error(f"Failed to log retrieval: {e}", exc_info=True)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def trace_n8n_code(
|
|
413
|
+
items: list,
|
|
414
|
+
workflow_context: dict,
|
|
415
|
+
execution_context: dict,
|
|
416
|
+
node_context: dict,
|
|
417
|
+
tracer: Optional[Tracer] = None,
|
|
418
|
+
api_key: Optional[str] = None,
|
|
419
|
+
endpoint: Optional[str] = None,
|
|
420
|
+
) -> PrelaN8nContext:
|
|
421
|
+
"""
|
|
422
|
+
Convenience function for tracing n8n Code node execution.
|
|
423
|
+
|
|
424
|
+
This function extracts the necessary identifiers from n8n's built-in
|
|
425
|
+
context variables ($workflow, $execution, $node) and creates a
|
|
426
|
+
PrelaN8nContext for easy tracing.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
items: n8n input items (from previous node)
|
|
430
|
+
workflow_context: $workflow context from n8n
|
|
431
|
+
execution_context: $execution context from n8n
|
|
432
|
+
node_context: $node context from n8n
|
|
433
|
+
tracer: Optional Prela tracer instance
|
|
434
|
+
api_key: Optional API key for remote export
|
|
435
|
+
endpoint: Optional endpoint URL for remote export
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
PrelaN8nContext ready to use as context manager
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
```python
|
|
442
|
+
# Inside n8n Code node
|
|
443
|
+
from prela.instrumentation.n8n import trace_n8n_code
|
|
444
|
+
|
|
445
|
+
with trace_n8n_code(items, $workflow, $execution, $node) as ctx:
|
|
446
|
+
# Your code here
|
|
447
|
+
result = my_function(items[0]["json"])
|
|
448
|
+
|
|
449
|
+
# Log LLM call
|
|
450
|
+
ctx.log_llm_call(
|
|
451
|
+
model="gpt-4",
|
|
452
|
+
prompt="Hello",
|
|
453
|
+
response=result,
|
|
454
|
+
tokens={"prompt": 10, "completion": 20}
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return [{"json": {"result": result}}]
|
|
458
|
+
```
|
|
459
|
+
"""
|
|
460
|
+
return PrelaN8nContext(
|
|
461
|
+
workflow_id=workflow_context.get("id", "unknown"),
|
|
462
|
+
workflow_name=workflow_context.get("name", "Unknown Workflow"),
|
|
463
|
+
execution_id=execution_context.get("id", "unknown"),
|
|
464
|
+
node_name=node_context.get("name", "Unknown Node"),
|
|
465
|
+
node_type=node_context.get("type", "n8n-nodes-base.code"),
|
|
466
|
+
tracer=tracer,
|
|
467
|
+
api_key=api_key,
|
|
468
|
+
endpoint=endpoint,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def prela_n8n_traced(
|
|
473
|
+
func: Optional[Callable] = None,
|
|
474
|
+
*,
|
|
475
|
+
tracer: Optional[Tracer] = None,
|
|
476
|
+
api_key: Optional[str] = None,
|
|
477
|
+
endpoint: Optional[str] = None,
|
|
478
|
+
) -> Callable:
|
|
479
|
+
"""
|
|
480
|
+
Decorator for automatically tracing n8n Code node functions.
|
|
481
|
+
|
|
482
|
+
This decorator expects the decorated function to accept n8n context
|
|
483
|
+
variables as arguments and automatically wraps the execution in a
|
|
484
|
+
PrelaN8nContext.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
func: Function to decorate (optional, for @prela_n8n_traced usage)
|
|
488
|
+
tracer: Optional Prela tracer instance
|
|
489
|
+
api_key: Optional API key for remote export
|
|
490
|
+
endpoint: Optional endpoint URL for remote export
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Decorated function
|
|
494
|
+
|
|
495
|
+
Example:
|
|
496
|
+
```python
|
|
497
|
+
# Inside n8n Code node
|
|
498
|
+
from prela.instrumentation.n8n import prela_n8n_traced
|
|
499
|
+
|
|
500
|
+
@prela_n8n_traced
|
|
501
|
+
def process_items(items, workflow, execution, node):
|
|
502
|
+
# Automatically traced!
|
|
503
|
+
result = call_api(items[0]["json"])
|
|
504
|
+
return [{"json": {"result": result}}]
|
|
505
|
+
|
|
506
|
+
# Call the function with n8n contexts
|
|
507
|
+
return process_items(items, $workflow, $execution, $node)
|
|
508
|
+
```
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
def decorator(f: Callable) -> Callable:
|
|
512
|
+
@functools.wraps(f)
|
|
513
|
+
def wrapper(items, workflow, execution, node, *args, **kwargs):
|
|
514
|
+
ctx = trace_n8n_code(
|
|
515
|
+
items=items,
|
|
516
|
+
workflow_context=workflow,
|
|
517
|
+
execution_context=execution,
|
|
518
|
+
node_context=node,
|
|
519
|
+
tracer=tracer,
|
|
520
|
+
api_key=api_key,
|
|
521
|
+
endpoint=endpoint,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
with ctx:
|
|
525
|
+
result = f(items, workflow, execution, node, *args, **kwargs)
|
|
526
|
+
return result
|
|
527
|
+
|
|
528
|
+
return wrapper
|
|
529
|
+
|
|
530
|
+
# Support both @prela_n8n_traced and @prela_n8n_traced()
|
|
531
|
+
if func is None:
|
|
532
|
+
return decorator
|
|
533
|
+
else:
|
|
534
|
+
return decorator(func)
|