jaf-py 2.5.10__py3-none-any.whl → 2.5.11__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.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +360 -279
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.10.dist-info/RECORD +0 -96
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
jaf/core/tracing.py
CHANGED
|
@@ -4,7 +4,9 @@ Tracing and observability for the JAF framework.
|
|
|
4
4
|
This module provides tracing capabilities to monitor agent execution,
|
|
5
5
|
tool calls, and performance metrics.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import os
|
|
9
|
+
|
|
8
10
|
os.environ["LANGFUSE_ENABLE_OTEL"] = "false"
|
|
9
11
|
import json
|
|
10
12
|
import logging
|
|
@@ -32,12 +34,13 @@ from .types import TraceEvent, TraceId
|
|
|
32
34
|
provider = None
|
|
33
35
|
tracer = None
|
|
34
36
|
|
|
37
|
+
|
|
35
38
|
def setup_otel_tracing(
|
|
36
39
|
service_name: str = "jaf-agent",
|
|
37
40
|
collector_url: Optional[str] = None,
|
|
38
41
|
proxy: Optional[str] = None,
|
|
39
42
|
session: Optional[Any] = None,
|
|
40
|
-
timeout: Optional[int] = None
|
|
43
|
+
timeout: Optional[int] = None,
|
|
41
44
|
) -> None:
|
|
42
45
|
"""Configure OpenTelemetry tracing.
|
|
43
46
|
|
|
@@ -55,9 +58,7 @@ def setup_otel_tracing(
|
|
|
55
58
|
if not collector_url:
|
|
56
59
|
return
|
|
57
60
|
|
|
58
|
-
provider = TracerProvider(
|
|
59
|
-
resource=Resource.create({"service.name": service_name})
|
|
60
|
-
)
|
|
61
|
+
provider = TracerProvider(resource=Resource.create({"service.name": service_name}))
|
|
61
62
|
|
|
62
63
|
# Configure session with proxy if needed
|
|
63
64
|
effective_session = session
|
|
@@ -68,8 +69,8 @@ def setup_otel_tracing(
|
|
|
68
69
|
if effective_proxy:
|
|
69
70
|
effective_session = requests.Session()
|
|
70
71
|
effective_session.proxies = {
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
"http": effective_proxy,
|
|
73
|
+
"https": effective_proxy,
|
|
73
74
|
}
|
|
74
75
|
print(f"[OTEL] Configuring proxy: {effective_proxy}")
|
|
75
76
|
elif effective_session is None and requests is None and (proxy or os.environ.get("OTEL_PROXY")):
|
|
@@ -77,8 +78,8 @@ def setup_otel_tracing(
|
|
|
77
78
|
if effective_proxy:
|
|
78
79
|
effective_session = requests.Session()
|
|
79
80
|
effective_session.proxies = {
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
"http": effective_proxy,
|
|
82
|
+
"https": effective_proxy,
|
|
82
83
|
}
|
|
83
84
|
print(f"[OTEL] Configuring proxy: {effective_proxy}")
|
|
84
85
|
|
|
@@ -166,6 +167,7 @@ class OtelTraceCollector:
|
|
|
166
167
|
"""Not implemented for OTEL."""
|
|
167
168
|
pass
|
|
168
169
|
|
|
170
|
+
|
|
169
171
|
class TraceCollector(Protocol):
|
|
170
172
|
"""Protocol for trace collectors."""
|
|
171
173
|
|
|
@@ -185,6 +187,7 @@ class TraceCollector(Protocol):
|
|
|
185
187
|
"""Clear traces."""
|
|
186
188
|
...
|
|
187
189
|
|
|
190
|
+
|
|
188
191
|
class InMemoryTraceCollector:
|
|
189
192
|
"""In-memory trace collector that organizes events by trace ID."""
|
|
190
193
|
|
|
@@ -196,17 +199,17 @@ class InMemoryTraceCollector:
|
|
|
196
199
|
trace_id: Optional[TraceId] = None
|
|
197
200
|
|
|
198
201
|
# Extract trace ID from event data - handle both snake_case and camelCase
|
|
199
|
-
if hasattr(event,
|
|
202
|
+
if hasattr(event, "data") and isinstance(event.data, dict):
|
|
200
203
|
# Try snake_case first (Python convention)
|
|
201
|
-
if
|
|
202
|
-
trace_id = event.data[
|
|
203
|
-
elif
|
|
204
|
-
trace_id = TraceId(event.data[
|
|
204
|
+
if "trace_id" in event.data:
|
|
205
|
+
trace_id = event.data["trace_id"]
|
|
206
|
+
elif "run_id" in event.data:
|
|
207
|
+
trace_id = TraceId(event.data["run_id"])
|
|
205
208
|
# Fallback to camelCase (for compatibility)
|
|
206
|
-
elif
|
|
207
|
-
trace_id = event.data[
|
|
208
|
-
elif
|
|
209
|
-
trace_id = TraceId(event.data[
|
|
209
|
+
elif "traceId" in event.data:
|
|
210
|
+
trace_id = event.data["traceId"]
|
|
211
|
+
elif "runId" in event.data:
|
|
212
|
+
trace_id = TraceId(event.data["runId"])
|
|
210
213
|
|
|
211
214
|
if not trace_id:
|
|
212
215
|
return
|
|
@@ -231,6 +234,7 @@ class InMemoryTraceCollector:
|
|
|
231
234
|
else:
|
|
232
235
|
self.traces.clear()
|
|
233
236
|
|
|
237
|
+
|
|
234
238
|
class ConsoleTraceCollector:
|
|
235
239
|
"""Console trace collector with detailed logging."""
|
|
236
240
|
|
|
@@ -245,66 +249,70 @@ class ConsoleTraceCollector:
|
|
|
245
249
|
timestamp = datetime.now().isoformat()
|
|
246
250
|
prefix = f"[{timestamp}] JAF:{event.type}"
|
|
247
251
|
|
|
248
|
-
if event.type ==
|
|
252
|
+
if event.type == "run_start":
|
|
249
253
|
data = event.data
|
|
250
|
-
run_id = data.get(
|
|
251
|
-
trace_id = data.get(
|
|
254
|
+
run_id = data.get("run_id") or data.get("runId")
|
|
255
|
+
trace_id = data.get("trace_id") or data.get("traceId")
|
|
252
256
|
# Track start time for this run
|
|
253
257
|
if run_id:
|
|
254
258
|
self.run_start_times[run_id] = time.time()
|
|
255
259
|
print(f"{prefix} Starting run {run_id} (trace: {trace_id})")
|
|
256
260
|
|
|
257
|
-
elif event.type ==
|
|
261
|
+
elif event.type == "llm_call_start":
|
|
258
262
|
data = event.data
|
|
259
|
-
model = data.get(
|
|
260
|
-
agent_name = data.get(
|
|
263
|
+
model = data.get("model")
|
|
264
|
+
agent_name = data.get("agent_name") or data.get("agentName")
|
|
261
265
|
print(f"{prefix} Calling {model} for agent {agent_name}")
|
|
262
266
|
|
|
263
|
-
elif event.type ==
|
|
267
|
+
elif event.type == "llm_call_end":
|
|
264
268
|
data = event.data
|
|
265
|
-
choice = data.get(
|
|
266
|
-
message = choice.get(
|
|
269
|
+
choice = data.get("choice", {})
|
|
270
|
+
message = choice.get("message", {}) if isinstance(choice, dict) else {}
|
|
267
271
|
|
|
268
272
|
# Check for tool_calls with both naming conventions
|
|
269
|
-
tool_calls = message.get(
|
|
273
|
+
tool_calls = message.get("tool_calls") or message.get("toolCalls")
|
|
270
274
|
has_tools = bool(tool_calls and len(tool_calls) > 0)
|
|
271
|
-
has_content = bool(message.get(
|
|
275
|
+
has_content = bool(message.get("content"))
|
|
272
276
|
|
|
273
277
|
if has_tools:
|
|
274
|
-
response_type =
|
|
278
|
+
response_type = "tool calls"
|
|
275
279
|
elif has_content:
|
|
276
|
-
response_type =
|
|
280
|
+
response_type = "content"
|
|
277
281
|
else:
|
|
278
|
-
response_type =
|
|
282
|
+
response_type = "empty response"
|
|
279
283
|
|
|
280
284
|
print(f"{prefix} LLM responded with {response_type}")
|
|
281
285
|
|
|
282
|
-
elif event.type ==
|
|
286
|
+
elif event.type == "tool_call_start":
|
|
283
287
|
data = event.data
|
|
284
|
-
tool_name = data.get(
|
|
285
|
-
args = data.get(
|
|
288
|
+
tool_name = data.get("tool_name") or data.get("toolName")
|
|
289
|
+
args = data.get("args")
|
|
286
290
|
print(f"{prefix} Executing tool {tool_name} with args:", args)
|
|
287
291
|
|
|
288
|
-
elif event.type ==
|
|
292
|
+
elif event.type == "tool_call_end":
|
|
289
293
|
data = event.data
|
|
290
|
-
tool_name = data.get(
|
|
294
|
+
tool_name = data.get("tool_name") or data.get("toolName")
|
|
291
295
|
print(f"{prefix} Tool {tool_name} completed")
|
|
292
296
|
|
|
293
|
-
elif event.type ==
|
|
297
|
+
elif event.type == "handoff":
|
|
294
298
|
data = event.data
|
|
295
|
-
from_agent = data.get(
|
|
296
|
-
to_agent = data.get(
|
|
299
|
+
from_agent = data.get("from")
|
|
300
|
+
to_agent = data.get("to")
|
|
297
301
|
print(f"{prefix} Agent handoff: {from_agent} → {to_agent}")
|
|
298
302
|
|
|
299
|
-
elif event.type ==
|
|
303
|
+
elif event.type == "run_end":
|
|
300
304
|
data = event.data
|
|
301
|
-
outcome = data.get(
|
|
305
|
+
outcome = data.get("outcome")
|
|
302
306
|
|
|
303
307
|
# Calculate elapsed time if we have a start time
|
|
304
308
|
elapsed = None
|
|
305
309
|
# Try to get run_id from outcome or use a fallback
|
|
306
310
|
run_id = None
|
|
307
|
-
if
|
|
311
|
+
if (
|
|
312
|
+
outcome
|
|
313
|
+
and hasattr(outcome, "final_state")
|
|
314
|
+
and hasattr(outcome.final_state, "run_id")
|
|
315
|
+
):
|
|
308
316
|
run_id = outcome.final_state.run_id
|
|
309
317
|
|
|
310
318
|
if run_id and run_id in self.run_start_times:
|
|
@@ -312,15 +320,19 @@ class ConsoleTraceCollector:
|
|
|
312
320
|
# Clean up the start time
|
|
313
321
|
del self.run_start_times[run_id]
|
|
314
322
|
|
|
315
|
-
if outcome and hasattr(outcome,
|
|
323
|
+
if outcome and hasattr(outcome, "status"):
|
|
316
324
|
status = outcome.status
|
|
317
325
|
|
|
318
|
-
if status ==
|
|
326
|
+
if status == "completed":
|
|
319
327
|
elapsed_str = f" in {elapsed:.2f}s" if elapsed else ""
|
|
320
328
|
print(f"{prefix} Run completed successfully{elapsed_str}")
|
|
321
329
|
else:
|
|
322
|
-
error = outcome.error if hasattr(outcome,
|
|
323
|
-
error_type =
|
|
330
|
+
error = outcome.error if hasattr(outcome, "error") else None
|
|
331
|
+
error_type = (
|
|
332
|
+
getattr(error, "_tag", "unknown")
|
|
333
|
+
if error and hasattr(error, "_tag")
|
|
334
|
+
else "unknown"
|
|
335
|
+
)
|
|
324
336
|
elapsed_str = f" in {elapsed:.2f}s" if elapsed else ""
|
|
325
337
|
print(f"{prefix} Run failed with {error_type}{elapsed_str}")
|
|
326
338
|
else:
|
|
@@ -339,6 +351,7 @@ class ConsoleTraceCollector:
|
|
|
339
351
|
"""Clear traces."""
|
|
340
352
|
self.in_memory.clear(trace_id)
|
|
341
353
|
|
|
354
|
+
|
|
342
355
|
class FileTraceCollector:
|
|
343
356
|
"""File trace collector that writes events to a file."""
|
|
344
357
|
|
|
@@ -351,9 +364,9 @@ class FileTraceCollector:
|
|
|
351
364
|
self.in_memory.collect(event)
|
|
352
365
|
|
|
353
366
|
log_entry = {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
367
|
+
"timestamp": datetime.now().isoformat(),
|
|
368
|
+
"type": event.type,
|
|
369
|
+
"data": event.data,
|
|
357
370
|
}
|
|
358
371
|
|
|
359
372
|
try:
|
|
@@ -362,8 +375,8 @@ class FileTraceCollector:
|
|
|
362
375
|
if dir_path:
|
|
363
376
|
os.makedirs(dir_path, exist_ok=True)
|
|
364
377
|
|
|
365
|
-
with open(self.file_path,
|
|
366
|
-
f.write(json.dumps(log_entry, default=str) +
|
|
378
|
+
with open(self.file_path, "a", encoding="utf-8") as f:
|
|
379
|
+
f.write(json.dumps(log_entry, default=str) + "\n")
|
|
367
380
|
except Exception as error:
|
|
368
381
|
print(f"Failed to write trace to file: {error}")
|
|
369
382
|
|
|
@@ -379,6 +392,7 @@ class FileTraceCollector:
|
|
|
379
392
|
"""Clear traces."""
|
|
380
393
|
self.in_memory.clear(trace_id)
|
|
381
394
|
|
|
395
|
+
|
|
382
396
|
class LangfuseTraceCollector:
|
|
383
397
|
"""Langfuse trace collector using v2 SDK.
|
|
384
398
|
|
|
@@ -392,7 +406,7 @@ class LangfuseTraceCollector:
|
|
|
392
406
|
self,
|
|
393
407
|
httpx_client: Optional[httpx.Client] = None,
|
|
394
408
|
proxy: Optional[str] = None,
|
|
395
|
-
timeout: Optional[int] = None
|
|
409
|
+
timeout: Optional[int] = None,
|
|
396
410
|
):
|
|
397
411
|
"""Initialize Langfuse trace collector.
|
|
398
412
|
|
|
@@ -409,8 +423,16 @@ class LangfuseTraceCollector:
|
|
|
409
423
|
host = os.environ.get("LANGFUSE_HOST")
|
|
410
424
|
|
|
411
425
|
print(f"[LANGFUSE] Initializing with host: {host}")
|
|
412
|
-
print(
|
|
413
|
-
|
|
426
|
+
print(
|
|
427
|
+
f"[LANGFUSE] Public key: {public_key[:10]}..."
|
|
428
|
+
if public_key
|
|
429
|
+
else "[LANGFUSE] No public key set"
|
|
430
|
+
)
|
|
431
|
+
print(
|
|
432
|
+
f"[LANGFUSE] Secret key: {secret_key[:10]}..."
|
|
433
|
+
if secret_key
|
|
434
|
+
else "[LANGFUSE] No secret key set"
|
|
435
|
+
)
|
|
414
436
|
|
|
415
437
|
# Track if we own the client for cleanup
|
|
416
438
|
self._owns_httpx_client = False
|
|
@@ -433,7 +455,9 @@ class LangfuseTraceCollector:
|
|
|
433
455
|
raise
|
|
434
456
|
except Exception as e:
|
|
435
457
|
logger = logging.getLogger(__name__)
|
|
436
|
-
logger.error(
|
|
458
|
+
logger.error(
|
|
459
|
+
f"[LANGFUSE] Failed to create httpx.Client with proxy '{effective_proxy}': {e}"
|
|
460
|
+
)
|
|
437
461
|
raise
|
|
438
462
|
# If no proxy specified, httpx will still respect HTTP_PROXY/HTTPS_PROXY env vars
|
|
439
463
|
elif proxy:
|
|
@@ -443,13 +467,13 @@ class LangfuseTraceCollector:
|
|
|
443
467
|
public_key=public_key,
|
|
444
468
|
secret_key=secret_key,
|
|
445
469
|
host=host,
|
|
446
|
-
release="jaf-py-v2.5.
|
|
447
|
-
httpx_client=client
|
|
470
|
+
release="jaf-py-v2.5.11",
|
|
471
|
+
httpx_client=client,
|
|
448
472
|
)
|
|
449
473
|
self._httpx_client = client
|
|
450
474
|
|
|
451
475
|
# Detect Langfuse version (v2 has trace() method, v3 does not)
|
|
452
|
-
self._is_langfuse_v3 = not hasattr(self.langfuse,
|
|
476
|
+
self._is_langfuse_v3 = not hasattr(self.langfuse, "trace")
|
|
453
477
|
if self._is_langfuse_v3:
|
|
454
478
|
print("[LANGFUSE] Detected Langfuse v3.x - using OpenTelemetry-based API")
|
|
455
479
|
else:
|
|
@@ -475,7 +499,7 @@ class LangfuseTraceCollector:
|
|
|
475
499
|
|
|
476
500
|
def _get_event_data(self, event: TraceEvent, key: str, default: Any = None) -> Any:
|
|
477
501
|
"""Extract data from event, handling both dict and dataclass."""
|
|
478
|
-
if not hasattr(event,
|
|
502
|
+
if not hasattr(event, "data"):
|
|
479
503
|
return default
|
|
480
504
|
|
|
481
505
|
# Handle dict
|
|
@@ -490,36 +514,32 @@ class LangfuseTraceCollector:
|
|
|
490
514
|
if self._is_langfuse_v3:
|
|
491
515
|
# Langfuse v3: Use start_span() to create a root span (creates trace implicitly)
|
|
492
516
|
# Extract parameters for v3 API
|
|
493
|
-
name = kwargs.get(
|
|
494
|
-
input_data = kwargs.get(
|
|
495
|
-
metadata = kwargs.get(
|
|
496
|
-
user_id = kwargs.get(
|
|
497
|
-
session_id = kwargs.get(
|
|
498
|
-
tags = kwargs.get(
|
|
517
|
+
name = kwargs.get("name", "trace")
|
|
518
|
+
input_data = kwargs.get("input")
|
|
519
|
+
metadata = kwargs.get("metadata", {})
|
|
520
|
+
user_id = kwargs.get("user_id")
|
|
521
|
+
session_id = kwargs.get("session_id")
|
|
522
|
+
tags = kwargs.get("tags", [])
|
|
499
523
|
|
|
500
524
|
# Add user_id, session_id, and tags to metadata for v3
|
|
501
525
|
if user_id:
|
|
502
|
-
metadata[
|
|
526
|
+
metadata["user_id"] = user_id
|
|
503
527
|
if session_id:
|
|
504
|
-
metadata[
|
|
528
|
+
metadata["session_id"] = session_id
|
|
505
529
|
if tags:
|
|
506
|
-
metadata[
|
|
530
|
+
metadata["tags"] = tags
|
|
507
531
|
|
|
508
532
|
# Create root span
|
|
509
|
-
trace = self.langfuse.start_span(
|
|
510
|
-
name=name,
|
|
511
|
-
input=input_data,
|
|
512
|
-
metadata=metadata
|
|
513
|
-
)
|
|
533
|
+
trace = self.langfuse.start_span(name=name, input=input_data, metadata=metadata)
|
|
514
534
|
|
|
515
535
|
# Update trace properties using update_trace()
|
|
516
536
|
update_params = {}
|
|
517
537
|
if user_id:
|
|
518
|
-
update_params[
|
|
538
|
+
update_params["user_id"] = user_id
|
|
519
539
|
if session_id:
|
|
520
|
-
update_params[
|
|
540
|
+
update_params["session_id"] = session_id
|
|
521
541
|
if tags:
|
|
522
|
-
update_params[
|
|
542
|
+
update_params["tags"] = tags
|
|
523
543
|
|
|
524
544
|
if update_params:
|
|
525
545
|
trace.update_trace(**update_params)
|
|
@@ -565,9 +585,9 @@ class LangfuseTraceCollector:
|
|
|
565
585
|
|
|
566
586
|
# Separate parameters for update() vs end()
|
|
567
587
|
for key, value in kwargs.items():
|
|
568
|
-
if key in [
|
|
588
|
+
if key in ["output", "metadata", "model", "usage"]:
|
|
569
589
|
update_params[key] = value
|
|
570
|
-
elif key ==
|
|
590
|
+
elif key == "end_time":
|
|
571
591
|
end_params[key] = value
|
|
572
592
|
|
|
573
593
|
# Update first if there are parameters
|
|
@@ -593,67 +613,73 @@ class LangfuseTraceCollector:
|
|
|
593
613
|
if event.type == "run_start":
|
|
594
614
|
# Start a new trace for the entire run
|
|
595
615
|
print(f"[LANGFUSE] Starting trace for run: {trace_id}")
|
|
596
|
-
|
|
616
|
+
|
|
597
617
|
# Initialize tracking for this trace
|
|
598
618
|
self.trace_tool_calls[trace_id] = []
|
|
599
619
|
self.trace_tool_results[trace_id] = []
|
|
600
|
-
|
|
620
|
+
|
|
601
621
|
# Extract user query from the run_start data
|
|
602
622
|
user_query = None
|
|
603
623
|
user_id = None
|
|
604
624
|
conversation_history = []
|
|
605
|
-
|
|
625
|
+
|
|
606
626
|
# Debug: Print the event data structure to understand what we're working with
|
|
607
627
|
if self._get_event_data(event, "context"):
|
|
608
628
|
context = self._get_event_data(event, "context")
|
|
609
629
|
print(f"[LANGFUSE DEBUG] Context type: {type(context)}")
|
|
610
|
-
print(
|
|
611
|
-
|
|
630
|
+
print(
|
|
631
|
+
f"[LANGFUSE DEBUG] Context attributes: {dir(context) if hasattr(context, '__dict__') else 'Not an object'}"
|
|
632
|
+
)
|
|
633
|
+
if hasattr(context, "__dict__"):
|
|
612
634
|
print(f"[LANGFUSE DEBUG] Context dict: {context.__dict__}")
|
|
613
|
-
|
|
635
|
+
|
|
614
636
|
# Try to extract from context first
|
|
615
637
|
context = self._get_event_data(event, "context")
|
|
616
638
|
if context:
|
|
617
639
|
# Try direct attribute access
|
|
618
|
-
if hasattr(context,
|
|
640
|
+
if hasattr(context, "query"):
|
|
619
641
|
user_query = context.query
|
|
620
642
|
print(f"[LANGFUSE DEBUG] Found user_query from context.query: {user_query}")
|
|
621
|
-
|
|
643
|
+
|
|
622
644
|
# Try to extract from combined_history
|
|
623
|
-
if hasattr(context,
|
|
645
|
+
if hasattr(context, "combined_history") and context.combined_history:
|
|
624
646
|
history = context.combined_history
|
|
625
|
-
print(
|
|
647
|
+
print(
|
|
648
|
+
f"[LANGFUSE DEBUG] Found combined_history with {len(history)} messages"
|
|
649
|
+
)
|
|
626
650
|
for i, msg in enumerate(reversed(history)):
|
|
627
651
|
print(f"[LANGFUSE DEBUG] History message {i}: {msg}")
|
|
628
652
|
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
629
653
|
user_query = msg.get("content", "")
|
|
630
|
-
print(
|
|
654
|
+
print(
|
|
655
|
+
f"[LANGFUSE DEBUG] Found user_query from history: {user_query}"
|
|
656
|
+
)
|
|
631
657
|
break
|
|
632
|
-
|
|
658
|
+
|
|
633
659
|
# Try to extract user_id from user_info
|
|
634
|
-
if hasattr(context,
|
|
660
|
+
if hasattr(context, "user_info"):
|
|
635
661
|
user_info = context.user_info
|
|
636
662
|
print(f"[LANGFUSE DEBUG] Found user_info: {type(user_info)}")
|
|
637
663
|
if isinstance(user_info, dict):
|
|
638
664
|
user_id = user_info.get("email") or user_info.get("username")
|
|
639
665
|
print(f"[LANGFUSE DEBUG] Extracted user_id: {user_id}")
|
|
640
|
-
elif hasattr(user_info,
|
|
666
|
+
elif hasattr(user_info, "email"):
|
|
641
667
|
user_id = user_info.email
|
|
642
668
|
print(f"[LANGFUSE DEBUG] Extracted user_id from attr: {user_id}")
|
|
643
|
-
|
|
669
|
+
|
|
644
670
|
# Extract conversation history and current user query from messages
|
|
645
671
|
messages = self._get_event_data(event, "messages", [])
|
|
646
672
|
if messages:
|
|
647
673
|
print(f"[LANGFUSE DEBUG] Processing {len(messages)} messages")
|
|
648
|
-
|
|
674
|
+
|
|
649
675
|
# Find the last user message (current query) and extract conversation history (excluding current)
|
|
650
676
|
current_user_message_found = False
|
|
651
677
|
for i in range(len(messages) - 1, -1, -1):
|
|
652
678
|
msg = messages[i]
|
|
653
|
-
|
|
679
|
+
|
|
654
680
|
# Extract message data comprehensively
|
|
655
681
|
msg_data = {}
|
|
656
|
-
|
|
682
|
+
|
|
657
683
|
if isinstance(msg, dict):
|
|
658
684
|
role = msg.get("role")
|
|
659
685
|
content = msg.get("content", "")
|
|
@@ -665,15 +691,15 @@ class LangfuseTraceCollector:
|
|
|
665
691
|
"tool_call_id": msg.get("tool_call_id"),
|
|
666
692
|
"name": msg.get("name"),
|
|
667
693
|
"function_call": msg.get("function_call"),
|
|
668
|
-
"timestamp": msg.get("timestamp", datetime.now().isoformat())
|
|
694
|
+
"timestamp": msg.get("timestamp", datetime.now().isoformat()),
|
|
669
695
|
}
|
|
670
|
-
elif hasattr(msg,
|
|
671
|
-
role = getattr(msg,
|
|
672
|
-
content = getattr(msg,
|
|
696
|
+
elif hasattr(msg, "role"):
|
|
697
|
+
role = getattr(msg, "role", None)
|
|
698
|
+
content = getattr(msg, "content", "")
|
|
673
699
|
# Handle both string content and complex content structures
|
|
674
700
|
if not isinstance(content, str):
|
|
675
701
|
# Try to extract text from complex content
|
|
676
|
-
if hasattr(content,
|
|
702
|
+
if hasattr(content, "__iter__") and not isinstance(content, str):
|
|
677
703
|
try:
|
|
678
704
|
# If it's a list, try to join text parts
|
|
679
705
|
content = " ".join(str(item) for item in content if item)
|
|
@@ -681,27 +707,29 @@ class LangfuseTraceCollector:
|
|
|
681
707
|
content = str(content)
|
|
682
708
|
else:
|
|
683
709
|
content = str(content)
|
|
684
|
-
|
|
710
|
+
|
|
685
711
|
# Capture all additional fields from object messages
|
|
686
712
|
msg_data = {
|
|
687
713
|
"role": role,
|
|
688
714
|
"content": content,
|
|
689
|
-
"tool_calls": getattr(msg,
|
|
690
|
-
"tool_call_id": getattr(msg,
|
|
691
|
-
"name": getattr(msg,
|
|
692
|
-
"function_call": getattr(msg,
|
|
693
|
-
"timestamp": getattr(msg,
|
|
715
|
+
"tool_calls": getattr(msg, "tool_calls", None),
|
|
716
|
+
"tool_call_id": getattr(msg, "tool_call_id", None),
|
|
717
|
+
"name": getattr(msg, "name", None),
|
|
718
|
+
"function_call": getattr(msg, "function_call", None),
|
|
719
|
+
"timestamp": getattr(msg, "timestamp", datetime.now().isoformat()),
|
|
694
720
|
}
|
|
695
721
|
else:
|
|
696
722
|
# Handle messages that don't have expected structure
|
|
697
|
-
print(
|
|
723
|
+
print(
|
|
724
|
+
f"[LANGFUSE DEBUG] Skipping message with unexpected structure: {type(msg)}"
|
|
725
|
+
)
|
|
698
726
|
continue
|
|
699
|
-
|
|
727
|
+
|
|
700
728
|
# Clean up None values from msg_data
|
|
701
729
|
msg_data = {k: v for k, v in msg_data.items() if v is not None}
|
|
702
|
-
|
|
730
|
+
|
|
703
731
|
# If we haven't found the current user message yet and this is a user message
|
|
704
|
-
if not current_user_message_found and (role == "user" or role ==
|
|
732
|
+
if not current_user_message_found and (role == "user" or role == "user"):
|
|
705
733
|
user_query = content
|
|
706
734
|
current_user_message_found = True
|
|
707
735
|
print(f"[LANGFUSE DEBUG] Found current user query: {user_query}")
|
|
@@ -709,11 +737,15 @@ class LangfuseTraceCollector:
|
|
|
709
737
|
# Add to conversation history (excluding the current user message)
|
|
710
738
|
# Include ALL message types: assistant, tool, system, function, etc.
|
|
711
739
|
conversation_history.insert(0, msg_data)
|
|
712
|
-
print(
|
|
713
|
-
|
|
714
|
-
|
|
740
|
+
print(
|
|
741
|
+
f"[LANGFUSE DEBUG] Added to conversation history: role={role}, content_length={len(str(content))}, has_tool_calls={bool(msg_data.get('tool_calls'))}"
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
print(
|
|
745
|
+
f"[LANGFUSE DEBUG] Final extracted - user_query: {user_query}, user_id: {user_id}"
|
|
746
|
+
)
|
|
715
747
|
print(f"[LANGFUSE DEBUG] Conversation history length: {len(conversation_history)}")
|
|
716
|
-
|
|
748
|
+
|
|
717
749
|
# Debug: Log the roles and types captured in conversation history
|
|
718
750
|
if conversation_history:
|
|
719
751
|
roles_summary = {}
|
|
@@ -721,15 +753,17 @@ class LangfuseTraceCollector:
|
|
|
721
753
|
role = msg.get("role", "unknown")
|
|
722
754
|
roles_summary[role] = roles_summary.get(role, 0) + 1
|
|
723
755
|
print(f"[LANGFUSE DEBUG] Conversation history roles breakdown: {roles_summary}")
|
|
724
|
-
|
|
756
|
+
|
|
725
757
|
# Log first few messages for verification
|
|
726
758
|
for i, msg in enumerate(conversation_history[:3]):
|
|
727
759
|
role = msg.get("role", "unknown")
|
|
728
760
|
content_preview = str(msg.get("content", ""))[:100]
|
|
729
761
|
has_tool_calls = bool(msg.get("tool_calls"))
|
|
730
762
|
has_tool_call_id = bool(msg.get("tool_call_id"))
|
|
731
|
-
print(
|
|
732
|
-
|
|
763
|
+
print(
|
|
764
|
+
f"[LANGFUSE DEBUG] History msg {i}: role={role}, content='{content_preview}...', tool_calls={has_tool_calls}, tool_call_id={has_tool_call_id}"
|
|
765
|
+
)
|
|
766
|
+
|
|
733
767
|
# Create comprehensive input data for the trace
|
|
734
768
|
trace_input = {
|
|
735
769
|
"user_query": user_query,
|
|
@@ -737,8 +771,8 @@ class LangfuseTraceCollector:
|
|
|
737
771
|
"agent_name": self._get_event_data(event, "agent_name", "analytics_agent_jaf"),
|
|
738
772
|
"session_info": {
|
|
739
773
|
"session_id": self._get_event_data(event, "session_id"),
|
|
740
|
-
"user_id": user_id or self._get_event_data(event, "user_id")
|
|
741
|
-
}
|
|
774
|
+
"user_id": user_id or self._get_event_data(event, "user_id"),
|
|
775
|
+
},
|
|
742
776
|
}
|
|
743
777
|
|
|
744
778
|
# Extract agent_name for tagging
|
|
@@ -762,45 +796,51 @@ class LangfuseTraceCollector:
|
|
|
762
796
|
"conversation_history": conversation_history,
|
|
763
797
|
"tool_calls": [],
|
|
764
798
|
"tool_results": [],
|
|
765
|
-
"user_info": self._get_event_data(event, "context").user_info
|
|
766
|
-
|
|
799
|
+
"user_info": self._get_event_data(event, "context").user_info
|
|
800
|
+
if self._get_event_data(event, "context")
|
|
801
|
+
and hasattr(self._get_event_data(event, "context"), "user_info")
|
|
802
|
+
else None,
|
|
803
|
+
},
|
|
767
804
|
)
|
|
768
805
|
self.trace_spans[trace_id] = trace
|
|
769
806
|
# Store user_id, user_query, and conversation_history for later use
|
|
770
807
|
trace._user_id = user_id or self._get_event_data(event, "user_id")
|
|
771
808
|
trace._user_query = user_query
|
|
772
809
|
trace._conversation_history = conversation_history
|
|
773
|
-
print(
|
|
774
|
-
|
|
810
|
+
print(
|
|
811
|
+
f"[LANGFUSE] Created trace with user query: {user_query[:100] if user_query else 'None'}..."
|
|
812
|
+
)
|
|
813
|
+
|
|
775
814
|
elif event.type == "run_end":
|
|
776
815
|
if trace_id in self.trace_spans:
|
|
777
816
|
print(f"[LANGFUSE] Ending trace for run: {trace_id}")
|
|
778
|
-
|
|
817
|
+
|
|
779
818
|
# Update the trace metadata with final tool calls and results
|
|
780
|
-
conversation_history = getattr(
|
|
819
|
+
conversation_history = getattr(
|
|
820
|
+
self.trace_spans[trace_id], "_conversation_history", []
|
|
821
|
+
)
|
|
781
822
|
final_metadata = {
|
|
782
823
|
"framework": "jaf",
|
|
783
824
|
"event_type": "run_end",
|
|
784
825
|
"trace_id": str(trace_id),
|
|
785
|
-
"user_query": getattr(self.trace_spans[trace_id],
|
|
786
|
-
"user_id": getattr(self.trace_spans[trace_id],
|
|
787
|
-
"agent_name": self._get_event_data(
|
|
826
|
+
"user_query": getattr(self.trace_spans[trace_id], "_user_query", None),
|
|
827
|
+
"user_id": getattr(self.trace_spans[trace_id], "_user_id", None),
|
|
828
|
+
"agent_name": self._get_event_data(
|
|
829
|
+
event, "agent_name", "analytics_agent_jaf"
|
|
830
|
+
),
|
|
788
831
|
"conversation_history": conversation_history,
|
|
789
832
|
"tool_calls": self.trace_tool_calls.get(trace_id, []),
|
|
790
|
-
"tool_results": self.trace_tool_results.get(trace_id, [])
|
|
833
|
+
"tool_results": self.trace_tool_results.get(trace_id, []),
|
|
791
834
|
}
|
|
792
|
-
|
|
835
|
+
|
|
793
836
|
# End the trace with updated metadata
|
|
794
|
-
self.trace_spans[trace_id].update(
|
|
795
|
-
|
|
796
|
-
metadata=final_metadata
|
|
797
|
-
)
|
|
798
|
-
|
|
837
|
+
self.trace_spans[trace_id].update(output=event.data, metadata=final_metadata)
|
|
838
|
+
|
|
799
839
|
# Flush to ensure data is sent
|
|
800
840
|
print(f"[LANGFUSE] Flushing data to Langfuse...")
|
|
801
841
|
self.langfuse.flush()
|
|
802
842
|
print(f"[LANGFUSE] Flush completed")
|
|
803
|
-
|
|
843
|
+
|
|
804
844
|
# Clean up
|
|
805
845
|
del self.trace_spans[trace_id]
|
|
806
846
|
if trace_id in self.trace_tool_calls:
|
|
@@ -809,17 +849,17 @@ class LangfuseTraceCollector:
|
|
|
809
849
|
del self.trace_tool_results[trace_id]
|
|
810
850
|
else:
|
|
811
851
|
print(f"[LANGFUSE] No trace found for run_end: {trace_id}")
|
|
812
|
-
|
|
852
|
+
|
|
813
853
|
elif event.type == "llm_call_start":
|
|
814
854
|
# Start a generation for LLM calls
|
|
815
855
|
model = self._get_event_data(event, "model", "unknown")
|
|
816
856
|
print(f"[LANGFUSE] Starting generation for LLM call with model: {model}")
|
|
817
|
-
|
|
857
|
+
|
|
818
858
|
# Get stored user information from the trace
|
|
819
859
|
trace = self.trace_spans[trace_id]
|
|
820
|
-
user_id = getattr(trace,
|
|
821
|
-
user_query = getattr(trace,
|
|
822
|
-
|
|
860
|
+
user_id = getattr(trace, "_user_id", None)
|
|
861
|
+
user_query = getattr(trace, "_user_query", None)
|
|
862
|
+
|
|
823
863
|
# Use compatibility layer to create generation (works with both v2 and v3)
|
|
824
864
|
generation = self._create_generation(
|
|
825
865
|
parent_span=trace,
|
|
@@ -829,13 +869,13 @@ class LangfuseTraceCollector:
|
|
|
829
869
|
"agent_name": self._get_event_data(event, "agent_name"),
|
|
830
870
|
"model": model,
|
|
831
871
|
"user_id": user_id,
|
|
832
|
-
"user_query": user_query
|
|
833
|
-
}
|
|
872
|
+
"user_query": user_query,
|
|
873
|
+
},
|
|
834
874
|
)
|
|
835
875
|
span_id = self._get_span_id(event)
|
|
836
876
|
self.active_spans[span_id] = generation
|
|
837
877
|
print(f"[LANGFUSE] Created generation: {generation}")
|
|
838
|
-
|
|
878
|
+
|
|
839
879
|
elif event.type == "llm_call_end":
|
|
840
880
|
span_id = self._get_span_id(event)
|
|
841
881
|
if span_id in self.active_spans:
|
|
@@ -846,29 +886,31 @@ class LangfuseTraceCollector:
|
|
|
846
886
|
|
|
847
887
|
# Extract usage from the event data
|
|
848
888
|
usage = self._get_event_data(event, "usage", {})
|
|
849
|
-
|
|
889
|
+
|
|
850
890
|
# Extract model information from choice data or event data
|
|
851
891
|
model = choice.get("model", "unknown")
|
|
852
892
|
if model == "unknown":
|
|
853
893
|
# Try to get model from the choice response structure
|
|
854
894
|
if isinstance(choice, dict):
|
|
855
895
|
model = choice.get("model") or choice.get("id", "unknown")
|
|
856
|
-
|
|
896
|
+
|
|
857
897
|
# Convert to Langfuse v2 format - let Langfuse handle cost calculation automatically
|
|
858
898
|
langfuse_usage = None
|
|
859
899
|
if usage:
|
|
860
900
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
861
901
|
completion_tokens = usage.get("completion_tokens", 0)
|
|
862
902
|
total_tokens = usage.get("total_tokens", 0)
|
|
863
|
-
|
|
903
|
+
|
|
864
904
|
langfuse_usage = {
|
|
865
905
|
"input": prompt_tokens,
|
|
866
906
|
"output": completion_tokens,
|
|
867
907
|
"total": total_tokens,
|
|
868
|
-
"unit": "TOKENS"
|
|
908
|
+
"unit": "TOKENS",
|
|
869
909
|
}
|
|
870
|
-
|
|
871
|
-
print(
|
|
910
|
+
|
|
911
|
+
print(
|
|
912
|
+
f"[LANGFUSE] Usage data for automatic cost calculation: {langfuse_usage}"
|
|
913
|
+
)
|
|
872
914
|
|
|
873
915
|
# Include model information in the generation end - Langfuse will calculate costs automatically
|
|
874
916
|
# Use compatibility wrapper for ending spans/generations
|
|
@@ -881,8 +923,8 @@ class LangfuseTraceCollector:
|
|
|
881
923
|
"model": model,
|
|
882
924
|
"system_fingerprint": choice.get("system_fingerprint"),
|
|
883
925
|
"created": choice.get("created"),
|
|
884
|
-
"response_id": choice.get("id")
|
|
885
|
-
}
|
|
926
|
+
"response_id": choice.get("id"),
|
|
927
|
+
},
|
|
886
928
|
)
|
|
887
929
|
|
|
888
930
|
# Clean up the span reference
|
|
@@ -890,10 +932,10 @@ class LangfuseTraceCollector:
|
|
|
890
932
|
print(f"[LANGFUSE] Generation ended with cost tracking")
|
|
891
933
|
else:
|
|
892
934
|
print(f"[LANGFUSE] No generation found for llm_call_end: {span_id}")
|
|
893
|
-
|
|
935
|
+
|
|
894
936
|
elif event.type == "tool_call_start":
|
|
895
937
|
# Start a span for tool calls with detailed input information
|
|
896
|
-
tool_name = self._get_event_data(event,
|
|
938
|
+
tool_name = self._get_event_data(event, "tool_name", "unknown")
|
|
897
939
|
tool_args = self._get_event_data(event, "args", {})
|
|
898
940
|
call_id = self._get_event_data(event, "call_id")
|
|
899
941
|
if not call_id:
|
|
@@ -903,31 +945,31 @@ class LangfuseTraceCollector:
|
|
|
903
945
|
except TypeError:
|
|
904
946
|
# event.data may be immutable; log and rely on synthetic ID tracking downstream
|
|
905
947
|
print(f"[LANGFUSE] Generated synthetic call_id for tool start: {call_id}")
|
|
906
|
-
|
|
948
|
+
|
|
907
949
|
print(f"[LANGFUSE] Starting span for tool call: {tool_name} ({call_id})")
|
|
908
|
-
|
|
950
|
+
|
|
909
951
|
# Track this tool call for the trace
|
|
910
952
|
tool_call_data = {
|
|
911
953
|
"tool_name": tool_name,
|
|
912
954
|
"arguments": tool_args,
|
|
913
955
|
"call_id": call_id,
|
|
914
|
-
"timestamp": datetime.now().isoformat()
|
|
956
|
+
"timestamp": datetime.now().isoformat(),
|
|
915
957
|
}
|
|
916
|
-
|
|
958
|
+
|
|
917
959
|
# Ensure trace_id exists in tracking
|
|
918
960
|
if trace_id not in self.trace_tool_calls:
|
|
919
961
|
self.trace_tool_calls[trace_id] = []
|
|
920
962
|
|
|
921
963
|
self.trace_tool_calls[trace_id].append(tool_call_data)
|
|
922
|
-
|
|
964
|
+
|
|
923
965
|
# Create comprehensive input data for the tool call
|
|
924
966
|
tool_input = {
|
|
925
967
|
"tool_name": tool_name,
|
|
926
968
|
"arguments": tool_args,
|
|
927
969
|
"call_id": call_id,
|
|
928
|
-
"timestamp": datetime.now().isoformat()
|
|
970
|
+
"timestamp": datetime.now().isoformat(),
|
|
929
971
|
}
|
|
930
|
-
|
|
972
|
+
|
|
931
973
|
# Use compatibility layer to create span (works with both v2 and v3)
|
|
932
974
|
span = self._create_span(
|
|
933
975
|
parent_span=self.trace_spans[trace_id],
|
|
@@ -937,48 +979,58 @@ class LangfuseTraceCollector:
|
|
|
937
979
|
"tool_name": tool_name,
|
|
938
980
|
"call_id": call_id,
|
|
939
981
|
"framework": "jaf",
|
|
940
|
-
"event_type": "tool_call"
|
|
941
|
-
}
|
|
982
|
+
"event_type": "tool_call",
|
|
983
|
+
},
|
|
942
984
|
)
|
|
943
985
|
span_id = self._get_span_id(event)
|
|
944
986
|
self.active_spans[span_id] = span
|
|
945
|
-
print(
|
|
946
|
-
|
|
987
|
+
print(
|
|
988
|
+
f"[LANGFUSE] Created tool span for {tool_name} with args: {str(tool_args)[:100]}..."
|
|
989
|
+
)
|
|
990
|
+
|
|
947
991
|
elif event.type == "tool_call_end":
|
|
948
992
|
span_id = self._get_span_id(event)
|
|
949
993
|
if span_id in self.active_spans:
|
|
950
|
-
tool_name = self._get_event_data(event,
|
|
994
|
+
tool_name = self._get_event_data(event, "tool_name", "unknown")
|
|
951
995
|
tool_result = self._get_event_data(event, "result")
|
|
952
996
|
call_id = self._get_event_data(event, "call_id")
|
|
953
|
-
|
|
997
|
+
|
|
954
998
|
print(f"[LANGFUSE] Ending span for tool call: {tool_name} ({call_id})")
|
|
955
|
-
|
|
999
|
+
|
|
956
1000
|
# Track this tool result for the trace
|
|
957
1001
|
tool_result_data = {
|
|
958
1002
|
"tool_name": tool_name,
|
|
959
1003
|
"result": tool_result,
|
|
960
1004
|
"call_id": call_id,
|
|
961
1005
|
"timestamp": datetime.now().isoformat(),
|
|
962
|
-
"execution_status": self._get_event_data(
|
|
963
|
-
|
|
964
|
-
|
|
1006
|
+
"execution_status": self._get_event_data(
|
|
1007
|
+
event, "execution_status", "completed"
|
|
1008
|
+
),
|
|
1009
|
+
"status": self._get_event_data(
|
|
1010
|
+
event, "execution_status", "completed"
|
|
1011
|
+
), # DEPRECATED: backward compatibility
|
|
1012
|
+
"tool_result": self._get_event_data(event, "tool_result"),
|
|
965
1013
|
}
|
|
966
|
-
|
|
1014
|
+
|
|
967
1015
|
if trace_id not in self.trace_tool_results:
|
|
968
1016
|
self.trace_tool_results[trace_id] = []
|
|
969
|
-
|
|
1017
|
+
|
|
970
1018
|
self.trace_tool_results[trace_id].append(tool_result_data)
|
|
971
|
-
|
|
1019
|
+
|
|
972
1020
|
# Create comprehensive output data for the tool call
|
|
973
1021
|
tool_output = {
|
|
974
1022
|
"tool_name": tool_name,
|
|
975
1023
|
"result": tool_result,
|
|
976
1024
|
"call_id": call_id,
|
|
977
1025
|
"timestamp": datetime.now().isoformat(),
|
|
978
|
-
"execution_status": self._get_event_data(
|
|
979
|
-
|
|
1026
|
+
"execution_status": self._get_event_data(
|
|
1027
|
+
event, "execution_status", "completed"
|
|
1028
|
+
),
|
|
1029
|
+
"status": self._get_event_data(
|
|
1030
|
+
event, "execution_status", "completed"
|
|
1031
|
+
), # DEPRECATED: backward compatibility
|
|
980
1032
|
}
|
|
981
|
-
|
|
1033
|
+
|
|
982
1034
|
# End the span with detailed output
|
|
983
1035
|
# Use compatibility wrapper for ending spans/generations
|
|
984
1036
|
span = self.active_spans[span_id]
|
|
@@ -990,16 +1042,18 @@ class LangfuseTraceCollector:
|
|
|
990
1042
|
"call_id": call_id,
|
|
991
1043
|
"result_length": len(str(tool_result)) if tool_result else 0,
|
|
992
1044
|
"framework": "jaf",
|
|
993
|
-
"event_type": "tool_call_end"
|
|
994
|
-
}
|
|
1045
|
+
"event_type": "tool_call_end",
|
|
1046
|
+
},
|
|
995
1047
|
)
|
|
996
|
-
|
|
1048
|
+
|
|
997
1049
|
# Clean up the span reference
|
|
998
1050
|
del self.active_spans[span_id]
|
|
999
|
-
print(
|
|
1051
|
+
print(
|
|
1052
|
+
f"[LANGFUSE] Tool span ended for {tool_name} with result length: {len(str(tool_result)) if tool_result else 0}"
|
|
1053
|
+
)
|
|
1000
1054
|
else:
|
|
1001
1055
|
print(f"[LANGFUSE] No tool span found for tool_call_end: {span_id}")
|
|
1002
|
-
|
|
1056
|
+
|
|
1003
1057
|
elif event.type == "handoff":
|
|
1004
1058
|
# Create an event for handoffs
|
|
1005
1059
|
print(f"[LANGFUSE] Creating event for handoff")
|
|
@@ -1007,8 +1061,11 @@ class LangfuseTraceCollector:
|
|
|
1007
1061
|
self._create_event(
|
|
1008
1062
|
parent_span=self.trace_spans[trace_id],
|
|
1009
1063
|
name="agent-handoff",
|
|
1010
|
-
input={
|
|
1011
|
-
|
|
1064
|
+
input={
|
|
1065
|
+
"from": self._get_event_data(event, "from"),
|
|
1066
|
+
"to": self._get_event_data(event, "to"),
|
|
1067
|
+
},
|
|
1068
|
+
metadata=event.data,
|
|
1012
1069
|
)
|
|
1013
1070
|
print(f"[LANGFUSE] Handoff event created")
|
|
1014
1071
|
|
|
@@ -1020,36 +1077,37 @@ class LangfuseTraceCollector:
|
|
|
1020
1077
|
parent_span=self.trace_spans[trace_id],
|
|
1021
1078
|
name=event.type,
|
|
1022
1079
|
input=event.data,
|
|
1023
|
-
metadata={"framework": "jaf", "event_type": event.type}
|
|
1080
|
+
metadata={"framework": "jaf", "event_type": event.type},
|
|
1024
1081
|
)
|
|
1025
1082
|
print(f"[LANGFUSE] Generic event created")
|
|
1026
|
-
|
|
1083
|
+
|
|
1027
1084
|
except Exception as e:
|
|
1028
1085
|
# Log error but don't break the application
|
|
1029
1086
|
print(f"[LANGFUSE] ERROR: Trace collection failed: {e}")
|
|
1030
1087
|
import traceback
|
|
1088
|
+
|
|
1031
1089
|
traceback.print_exc()
|
|
1032
1090
|
|
|
1033
1091
|
def _get_trace_id(self, event: TraceEvent) -> Optional[TraceId]:
|
|
1034
1092
|
"""Extract trace ID from event data, handling both dict and dataclass."""
|
|
1035
|
-
if not hasattr(event,
|
|
1093
|
+
if not hasattr(event, "data"):
|
|
1036
1094
|
return None
|
|
1037
1095
|
|
|
1038
1096
|
# Try snake_case first (Python convention)
|
|
1039
|
-
trace_id = self._get_event_data(event,
|
|
1097
|
+
trace_id = self._get_event_data(event, "trace_id")
|
|
1040
1098
|
if trace_id:
|
|
1041
1099
|
return trace_id
|
|
1042
1100
|
|
|
1043
|
-
run_id = self._get_event_data(event,
|
|
1101
|
+
run_id = self._get_event_data(event, "run_id")
|
|
1044
1102
|
if run_id:
|
|
1045
1103
|
return TraceId(run_id)
|
|
1046
1104
|
|
|
1047
1105
|
# Fallback to camelCase (for compatibility)
|
|
1048
|
-
trace_id = self._get_event_data(event,
|
|
1106
|
+
trace_id = self._get_event_data(event, "traceId")
|
|
1049
1107
|
if trace_id:
|
|
1050
1108
|
return trace_id
|
|
1051
1109
|
|
|
1052
|
-
run_id = self._get_event_data(event,
|
|
1110
|
+
run_id = self._get_event_data(event, "runId")
|
|
1053
1111
|
if run_id:
|
|
1054
1112
|
return TraceId(run_id)
|
|
1055
1113
|
|
|
@@ -1058,18 +1116,24 @@ class LangfuseTraceCollector:
|
|
|
1058
1116
|
def _get_span_id(self, event: TraceEvent) -> str:
|
|
1059
1117
|
"""Generate a unique span ID for the event."""
|
|
1060
1118
|
trace_id = self._get_trace_id(event)
|
|
1061
|
-
|
|
1119
|
+
|
|
1062
1120
|
# Use consistent identifiers that don't depend on timestamp
|
|
1063
|
-
if event.type.startswith(
|
|
1064
|
-
call_id = self._get_event_data(event,
|
|
1121
|
+
if event.type.startswith("tool_call"):
|
|
1122
|
+
call_id = self._get_event_data(event, "call_id") or self._get_event_data(
|
|
1123
|
+
event, "tool_call_id"
|
|
1124
|
+
)
|
|
1065
1125
|
if call_id:
|
|
1066
1126
|
return f"tool-{trace_id}-{call_id}"
|
|
1067
|
-
tool_name = self._get_event_data(event,
|
|
1127
|
+
tool_name = self._get_event_data(event, "tool_name") or self._get_event_data(
|
|
1128
|
+
event, "toolName", "unknown"
|
|
1129
|
+
)
|
|
1068
1130
|
return f"tool-{tool_name}-{trace_id}"
|
|
1069
|
-
elif event.type.startswith(
|
|
1131
|
+
elif event.type.startswith("llm_call"):
|
|
1070
1132
|
# For LLM calls, use a simpler consistent ID that matches between start and end
|
|
1071
1133
|
# Get run_id for more consistent matching
|
|
1072
|
-
run_id = self._get_event_data(event,
|
|
1134
|
+
run_id = self._get_event_data(event, "run_id") or self._get_event_data(
|
|
1135
|
+
event, "runId", trace_id
|
|
1136
|
+
)
|
|
1073
1137
|
return f"llm-{run_id}"
|
|
1074
1138
|
else:
|
|
1075
1139
|
return f"{event.type}-{trace_id}"
|
|
@@ -1092,7 +1156,7 @@ def create_composite_trace_collector(
|
|
|
1092
1156
|
httpx_client: Optional[httpx.Client] = None,
|
|
1093
1157
|
otel_session: Optional[Any] = None,
|
|
1094
1158
|
proxy: Optional[str] = None,
|
|
1095
|
-
timeout: Optional[int] = None
|
|
1159
|
+
timeout: Optional[int] = None,
|
|
1096
1160
|
) -> TraceCollector:
|
|
1097
1161
|
"""Create a composite trace collector that forwards events to multiple collectors.
|
|
1098
1162
|
|
|
@@ -1116,10 +1180,7 @@ def create_composite_trace_collector(
|
|
|
1116
1180
|
if collector_url:
|
|
1117
1181
|
# Pass proxy and timeout to OTEL setup
|
|
1118
1182
|
setup_otel_tracing(
|
|
1119
|
-
collector_url=collector_url,
|
|
1120
|
-
proxy=proxy,
|
|
1121
|
-
session=otel_session,
|
|
1122
|
-
timeout=timeout
|
|
1183
|
+
collector_url=collector_url, proxy=proxy, session=otel_session, timeout=timeout
|
|
1123
1184
|
)
|
|
1124
1185
|
otel_collector = OtelTraceCollector()
|
|
1125
1186
|
collector_list.append(otel_collector)
|
|
@@ -1127,9 +1188,7 @@ def create_composite_trace_collector(
|
|
|
1127
1188
|
# Automatically add Langfuse collector if keys are configured
|
|
1128
1189
|
if os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY"):
|
|
1129
1190
|
langfuse_collector = LangfuseTraceCollector(
|
|
1130
|
-
httpx_client=httpx_client,
|
|
1131
|
-
proxy=proxy,
|
|
1132
|
-
timeout=timeout
|
|
1191
|
+
httpx_client=httpx_client, proxy=proxy, timeout=timeout
|
|
1133
1192
|
)
|
|
1134
1193
|
collector_list.append(langfuse_collector)
|
|
1135
1194
|
|
|
@@ -1164,19 +1223,19 @@ def create_composite_trace_collector(
|
|
|
1164
1223
|
collector.clear(trace_id)
|
|
1165
1224
|
except Exception as e:
|
|
1166
1225
|
print(f"Warning: Failed to clear trace collector: {e}")
|
|
1167
|
-
|
|
1226
|
+
|
|
1168
1227
|
def close(self) -> None:
|
|
1169
1228
|
"""Close all collectors that support cleanup."""
|
|
1170
1229
|
for collector in self.collectors:
|
|
1171
|
-
if hasattr(collector,
|
|
1230
|
+
if hasattr(collector, "close"):
|
|
1172
1231
|
try:
|
|
1173
1232
|
collector.close()
|
|
1174
1233
|
except Exception as e:
|
|
1175
1234
|
print(f"Warning: Failed to close trace collector: {e}")
|
|
1176
|
-
|
|
1235
|
+
|
|
1177
1236
|
def __enter__(self):
|
|
1178
1237
|
return self
|
|
1179
|
-
|
|
1238
|
+
|
|
1180
1239
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1181
1240
|
self.close()
|
|
1182
1241
|
return False
|