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.
Files changed (92) hide show
  1. jaf/__init__.py +154 -57
  2. jaf/a2a/__init__.py +42 -21
  3. jaf/a2a/agent.py +79 -126
  4. jaf/a2a/agent_card.py +87 -78
  5. jaf/a2a/client.py +30 -66
  6. jaf/a2a/examples/client_example.py +12 -12
  7. jaf/a2a/examples/integration_example.py +38 -47
  8. jaf/a2a/examples/server_example.py +56 -53
  9. jaf/a2a/memory/__init__.py +0 -4
  10. jaf/a2a/memory/cleanup.py +28 -21
  11. jaf/a2a/memory/factory.py +155 -133
  12. jaf/a2a/memory/providers/composite.py +21 -26
  13. jaf/a2a/memory/providers/in_memory.py +89 -83
  14. jaf/a2a/memory/providers/postgres.py +117 -115
  15. jaf/a2a/memory/providers/redis.py +128 -121
  16. jaf/a2a/memory/serialization.py +77 -87
  17. jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
  18. jaf/a2a/memory/tests/test_cleanup.py +211 -94
  19. jaf/a2a/memory/tests/test_serialization.py +73 -68
  20. jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
  21. jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
  22. jaf/a2a/memory/types.py +91 -53
  23. jaf/a2a/protocol.py +95 -125
  24. jaf/a2a/server.py +90 -118
  25. jaf/a2a/standalone_client.py +30 -43
  26. jaf/a2a/tests/__init__.py +16 -33
  27. jaf/a2a/tests/run_tests.py +17 -53
  28. jaf/a2a/tests/test_agent.py +40 -140
  29. jaf/a2a/tests/test_client.py +54 -117
  30. jaf/a2a/tests/test_integration.py +28 -82
  31. jaf/a2a/tests/test_protocol.py +54 -139
  32. jaf/a2a/tests/test_types.py +50 -136
  33. jaf/a2a/types.py +58 -34
  34. jaf/cli.py +21 -41
  35. jaf/core/__init__.py +7 -1
  36. jaf/core/agent_tool.py +93 -72
  37. jaf/core/analytics.py +257 -207
  38. jaf/core/checkpoint.py +223 -0
  39. jaf/core/composition.py +249 -235
  40. jaf/core/engine.py +817 -519
  41. jaf/core/errors.py +55 -42
  42. jaf/core/guardrails.py +276 -202
  43. jaf/core/handoff.py +47 -31
  44. jaf/core/parallel_agents.py +69 -75
  45. jaf/core/performance.py +75 -73
  46. jaf/core/proxy.py +43 -44
  47. jaf/core/proxy_helpers.py +24 -27
  48. jaf/core/regeneration.py +220 -129
  49. jaf/core/state.py +68 -66
  50. jaf/core/streaming.py +115 -108
  51. jaf/core/tool_results.py +111 -101
  52. jaf/core/tools.py +114 -116
  53. jaf/core/tracing.py +269 -210
  54. jaf/core/types.py +371 -151
  55. jaf/core/workflows.py +209 -168
  56. jaf/exceptions.py +46 -38
  57. jaf/memory/__init__.py +1 -6
  58. jaf/memory/approval_storage.py +54 -77
  59. jaf/memory/factory.py +4 -4
  60. jaf/memory/providers/in_memory.py +216 -180
  61. jaf/memory/providers/postgres.py +216 -146
  62. jaf/memory/providers/redis.py +173 -116
  63. jaf/memory/types.py +70 -51
  64. jaf/memory/utils.py +36 -34
  65. jaf/plugins/__init__.py +12 -12
  66. jaf/plugins/base.py +105 -96
  67. jaf/policies/__init__.py +0 -1
  68. jaf/policies/handoff.py +37 -46
  69. jaf/policies/validation.py +76 -52
  70. jaf/providers/__init__.py +6 -3
  71. jaf/providers/mcp.py +97 -51
  72. jaf/providers/model.py +360 -279
  73. jaf/server/__init__.py +1 -1
  74. jaf/server/main.py +7 -11
  75. jaf/server/server.py +514 -359
  76. jaf/server/types.py +208 -52
  77. jaf/utils/__init__.py +17 -18
  78. jaf/utils/attachments.py +111 -116
  79. jaf/utils/document_processor.py +175 -174
  80. jaf/visualization/__init__.py +1 -1
  81. jaf/visualization/example.py +111 -110
  82. jaf/visualization/functional_core.py +46 -71
  83. jaf/visualization/graphviz.py +154 -189
  84. jaf/visualization/imperative_shell.py +7 -16
  85. jaf/visualization/types.py +8 -4
  86. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
  87. jaf_py-2.5.11.dist-info/RECORD +97 -0
  88. jaf_py-2.5.10.dist-info/RECORD +0 -96
  89. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
  90. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
  91. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
  92. {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
- 'http': effective_proxy,
72
- 'https': effective_proxy,
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
- 'http': effective_proxy,
81
- 'https': effective_proxy,
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, 'data') and isinstance(event.data, dict):
202
+ if hasattr(event, "data") and isinstance(event.data, dict):
200
203
  # Try snake_case first (Python convention)
201
- if 'trace_id' in event.data:
202
- trace_id = event.data['trace_id']
203
- elif 'run_id' in event.data:
204
- trace_id = TraceId(event.data['run_id'])
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 'traceId' in event.data:
207
- trace_id = event.data['traceId']
208
- elif 'runId' in event.data:
209
- trace_id = TraceId(event.data['runId'])
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 == 'run_start':
252
+ if event.type == "run_start":
249
253
  data = event.data
250
- run_id = data.get('run_id') or data.get('runId')
251
- trace_id = data.get('trace_id') or data.get('traceId')
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 == 'llm_call_start':
261
+ elif event.type == "llm_call_start":
258
262
  data = event.data
259
- model = data.get('model')
260
- agent_name = data.get('agent_name') or data.get('agentName')
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 == 'llm_call_end':
267
+ elif event.type == "llm_call_end":
264
268
  data = event.data
265
- choice = data.get('choice', {})
266
- message = choice.get('message', {}) if isinstance(choice, dict) else {}
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('tool_calls') or message.get('toolCalls')
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('content'))
275
+ has_content = bool(message.get("content"))
272
276
 
273
277
  if has_tools:
274
- response_type = 'tool calls'
278
+ response_type = "tool calls"
275
279
  elif has_content:
276
- response_type = 'content'
280
+ response_type = "content"
277
281
  else:
278
- response_type = 'empty response'
282
+ response_type = "empty response"
279
283
 
280
284
  print(f"{prefix} LLM responded with {response_type}")
281
285
 
282
- elif event.type == 'tool_call_start':
286
+ elif event.type == "tool_call_start":
283
287
  data = event.data
284
- tool_name = data.get('tool_name') or data.get('toolName')
285
- args = data.get('args')
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 == 'tool_call_end':
292
+ elif event.type == "tool_call_end":
289
293
  data = event.data
290
- tool_name = data.get('tool_name') or data.get('toolName')
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 == 'handoff':
297
+ elif event.type == "handoff":
294
298
  data = event.data
295
- from_agent = data.get('from')
296
- to_agent = data.get('to')
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 == 'run_end':
303
+ elif event.type == "run_end":
300
304
  data = event.data
301
- outcome = data.get('outcome')
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 outcome and hasattr(outcome, 'final_state') and hasattr(outcome.final_state, 'run_id'):
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, 'status'):
323
+ if outcome and hasattr(outcome, "status"):
316
324
  status = outcome.status
317
325
 
318
- if status == 'completed':
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, 'error') else None
323
- error_type = getattr(error, '_tag', 'unknown') if error and hasattr(error, '_tag') else 'unknown'
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
- 'timestamp': datetime.now().isoformat(),
355
- 'type': event.type,
356
- 'data': event.data
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, 'a', encoding='utf-8') as f:
366
- f.write(json.dumps(log_entry, default=str) + '\n')
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(f"[LANGFUSE] Public key: {public_key[:10]}..." if public_key else "[LANGFUSE] No public key set")
413
- print(f"[LANGFUSE] Secret key: {secret_key[:10]}..." if secret_key else "[LANGFUSE] No secret key set")
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(f"[LANGFUSE] Failed to create httpx.Client with proxy '{effective_proxy}': {e}")
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.10",
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, 'trace')
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, 'data'):
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('name', 'trace')
494
- input_data = kwargs.get('input')
495
- metadata = kwargs.get('metadata', {})
496
- user_id = kwargs.get('user_id')
497
- session_id = kwargs.get('session_id')
498
- tags = kwargs.get('tags', [])
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['user_id'] = user_id
526
+ metadata["user_id"] = user_id
503
527
  if session_id:
504
- metadata['session_id'] = session_id
528
+ metadata["session_id"] = session_id
505
529
  if tags:
506
- metadata['tags'] = tags
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['user_id'] = user_id
538
+ update_params["user_id"] = user_id
519
539
  if session_id:
520
- update_params['session_id'] = session_id
540
+ update_params["session_id"] = session_id
521
541
  if tags:
522
- update_params['tags'] = tags
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 ['output', 'metadata', 'model', 'usage']:
588
+ if key in ["output", "metadata", "model", "usage"]:
569
589
  update_params[key] = value
570
- elif key == 'end_time':
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(f"[LANGFUSE DEBUG] Context attributes: {dir(context) if hasattr(context, '__dict__') else 'Not an object'}")
611
- if hasattr(context, '__dict__'):
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, 'query'):
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, 'combined_history') and context.combined_history:
645
+ if hasattr(context, "combined_history") and context.combined_history:
624
646
  history = context.combined_history
625
- print(f"[LANGFUSE DEBUG] Found combined_history with {len(history)} messages")
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(f"[LANGFUSE DEBUG] Found user_query from history: {user_query}")
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, 'user_info'):
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, 'email'):
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, 'role'):
671
- role = getattr(msg, 'role', None)
672
- content = getattr(msg, 'content', "")
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, '__iter__') and not isinstance(content, str):
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, 'tool_calls', None),
690
- "tool_call_id": getattr(msg, 'tool_call_id', None),
691
- "name": getattr(msg, 'name', None),
692
- "function_call": getattr(msg, 'function_call', None),
693
- "timestamp": getattr(msg, 'timestamp', datetime.now().isoformat())
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(f"[LANGFUSE DEBUG] Skipping message with unexpected structure: {type(msg)}")
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 == 'user'):
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(f"[LANGFUSE DEBUG] Added to conversation history: role={role}, content_length={len(str(content))}, has_tool_calls={bool(msg_data.get('tool_calls'))}")
713
-
714
- print(f"[LANGFUSE DEBUG] Final extracted - user_query: {user_query}, user_id: {user_id}")
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(f"[LANGFUSE DEBUG] History msg {i}: role={role}, content='{content_preview}...', tool_calls={has_tool_calls}, tool_call_id={has_tool_call_id}")
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 if self._get_event_data(event, "context") and hasattr(self._get_event_data(event, "context"), 'user_info') else None
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(f"[LANGFUSE] Created trace with user query: {user_query[:100] if user_query else 'None'}...")
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(self.trace_spans[trace_id], '_conversation_history', [])
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], '_user_query', None),
786
- "user_id": getattr(self.trace_spans[trace_id], '_user_id', None),
787
- "agent_name": self._get_event_data(event, "agent_name", "analytics_agent_jaf"),
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
- output=event.data,
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, '_user_id', None)
821
- user_query = getattr(trace, '_user_query', None)
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(f"[LANGFUSE] Usage data for automatic cost calculation: {langfuse_usage}")
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, 'tool_name', 'unknown')
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(f"[LANGFUSE] Created tool span for {tool_name} with args: {str(tool_args)[:100]}...")
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, 'tool_name', 'unknown')
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(event, "execution_status", "completed"),
963
- "status": self._get_event_data(event, "execution_status", "completed"), # DEPRECATED: backward compatibility
964
- "tool_result": self._get_event_data(event, "tool_result")
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(event, "execution_status", "completed"),
979
- "status": self._get_event_data(event, "execution_status", "completed") # DEPRECATED: backward compatibility
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(f"[LANGFUSE] Tool span ended for {tool_name} with result length: {len(str(tool_result)) if tool_result else 0}")
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={"from": self._get_event_data(event, "from"), "to": self._get_event_data(event, "to")},
1011
- metadata=event.data
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, 'data'):
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, 'trace_id')
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, 'run_id')
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, 'traceId')
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, 'runId')
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('tool_call'):
1064
- call_id = self._get_event_data(event, 'call_id') or self._get_event_data(event, 'tool_call_id')
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, 'tool_name') or self._get_event_data(event, 'toolName', 'unknown')
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('llm_call'):
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, 'run_id') or self._get_event_data(event, 'runId', trace_id)
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, 'close'):
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