jaf-py 2.2.4__py3-none-any.whl → 2.3.1__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/core/engine.py CHANGED
@@ -94,7 +94,9 @@ async def run(
94
94
  config.on_event(RunStartEvent(data=to_event_data(RunStartEventData(
95
95
  run_id=initial_state.run_id,
96
96
  trace_id=initial_state.trace_id,
97
- session_id=config.conversation_id
97
+ session_id=config.conversation_id,
98
+ context=initial_state.context,
99
+ messages=initial_state.messages
98
100
  ))))
99
101
 
100
102
  state_with_memory = await _load_conversation_history(initial_state, config)
@@ -293,7 +295,9 @@ async def _run_internal(
293
295
  agent_name=current_agent.name,
294
296
  model=model,
295
297
  trace_id=state.trace_id,
296
- run_id=state.run_id
298
+ run_id=state.run_id,
299
+ context=state.context,
300
+ messages=state.messages
297
301
  ))))
298
302
 
299
303
  # Get completion from model provider
jaf/core/tracing.py CHANGED
@@ -358,15 +358,98 @@ class LangfuseTraceCollector:
358
358
  if event.type == "run_start":
359
359
  # Start a new trace for the entire run
360
360
  print(f"[LANGFUSE] Starting trace for run: {trace_id}")
361
+
362
+ # Extract user query from the run_start data
363
+ user_query = None
364
+ user_id = None
365
+
366
+ # Debug: Print the event data structure to understand what we're working with
367
+ if event.data.get("context"):
368
+ context = event.data["context"]
369
+ print(f"[LANGFUSE DEBUG] Context type: {type(context)}")
370
+ print(f"[LANGFUSE DEBUG] Context attributes: {dir(context) if hasattr(context, '__dict__') else 'Not an object'}")
371
+ if hasattr(context, '__dict__'):
372
+ print(f"[LANGFUSE DEBUG] Context dict: {context.__dict__}")
373
+
374
+ # Try to extract from context first
375
+ context = event.data.get("context")
376
+ if context:
377
+ # Try direct attribute access
378
+ if hasattr(context, 'query'):
379
+ user_query = context.query
380
+ print(f"[LANGFUSE DEBUG] Found user_query from context.query: {user_query}")
381
+
382
+ # Try to extract from combined_history
383
+ if hasattr(context, 'combined_history') and context.combined_history:
384
+ history = context.combined_history
385
+ print(f"[LANGFUSE DEBUG] Found combined_history with {len(history)} messages")
386
+ for i, msg in enumerate(reversed(history)):
387
+ print(f"[LANGFUSE DEBUG] History message {i}: {msg}")
388
+ if isinstance(msg, dict) and msg.get("role") == "user":
389
+ user_query = msg.get("content", "")
390
+ print(f"[LANGFUSE DEBUG] Found user_query from history: {user_query}")
391
+ break
392
+
393
+ # Try to extract user_id from token_response
394
+ if hasattr(context, 'token_response'):
395
+ token_response = context.token_response
396
+ print(f"[LANGFUSE DEBUG] Found token_response: {type(token_response)}")
397
+ if isinstance(token_response, dict):
398
+ user_id = token_response.get("email") or token_response.get("username")
399
+ print(f"[LANGFUSE DEBUG] Extracted user_id: {user_id}")
400
+ elif hasattr(token_response, 'email'):
401
+ user_id = token_response.email
402
+ print(f"[LANGFUSE DEBUG] Extracted user_id from attr: {user_id}")
403
+
404
+ # Fallback: try to extract from messages if context didn't work
405
+ if not user_query and event.data.get("messages"):
406
+ print(f"[LANGFUSE DEBUG] Trying fallback from messages")
407
+ messages = event.data["messages"]
408
+ print(f"[LANGFUSE DEBUG] Found {len(messages)} messages")
409
+ # Find the last user message which should be the current query
410
+ for i, msg in enumerate(reversed(messages)):
411
+ print(f"[LANGFUSE DEBUG] Message {i}: {msg}")
412
+ if isinstance(msg, dict) and msg.get("role") == "user":
413
+ user_query = msg.get("content", "")
414
+ print(f"[LANGFUSE DEBUG] Found user_query from messages: {user_query}")
415
+ break
416
+ elif hasattr(msg, 'role') and msg.role == 'user':
417
+ user_query = msg.content
418
+ print(f"[LANGFUSE DEBUG] Found user_query from message attr: {user_query}")
419
+ break
420
+
421
+ print(f"[LANGFUSE DEBUG] Final extracted - user_query: {user_query}, user_id: {user_id}")
422
+
423
+ # Create comprehensive input data for the trace
424
+ trace_input = {
425
+ "user_query": user_query,
426
+ "run_id": str(trace_id),
427
+ "agent_name": event.data.get("agent_name", "analytics_agent_jaf"),
428
+ "session_info": {
429
+ "session_id": event.data.get("session_id"),
430
+ "user_id": user_id or event.data.get("user_id")
431
+ }
432
+ }
433
+
361
434
  trace = self.langfuse.trace(
362
435
  name=f"jaf-run-{trace_id}",
363
- user_id=event.data.get("user_id"),
436
+ user_id=user_id or event.data.get("user_id"),
364
437
  session_id=event.data.get("session_id"),
365
- input=event.data,
366
- metadata={"framework": "jaf", "event_type": "run_start", "trace_id": str(trace_id)}
438
+ input=trace_input,
439
+ metadata={
440
+ "framework": "jaf",
441
+ "event_type": "run_start",
442
+ "trace_id": str(trace_id),
443
+ "user_query": user_query,
444
+ "user_id": user_id or event.data.get("user_id"),
445
+ "agent_name": event.data.get("agent_name", "analytics_agent_jaf")
446
+ }
367
447
  )
368
448
  self.trace_spans[trace_id] = trace
369
- print(f"[LANGFUSE] Created trace: {trace}")
449
+ # Store user_id and user_query for later use in generations
450
+ trace._user_id = user_id or event.data.get("user_id")
451
+ trace._user_query = user_query
452
+ print(f"[LANGFUSE] Created trace with user query: {user_query[:100] if user_query else 'None'}...")
370
453
 
371
454
  elif event.type == "run_end":
372
455
  if trace_id in self.trace_spans:
@@ -386,10 +469,21 @@ class LangfuseTraceCollector:
386
469
  # Start a generation for LLM calls
387
470
  model = event.data.get("model", "unknown")
388
471
  print(f"[LANGFUSE] Starting generation for LLM call with model: {model}")
389
- generation = self.trace_spans[trace_id].generation(
472
+
473
+ # Get stored user information from the trace
474
+ trace = self.trace_spans[trace_id]
475
+ user_id = getattr(trace, '_user_id', None)
476
+ user_query = getattr(trace, '_user_query', None)
477
+
478
+ generation = trace.generation(
390
479
  name=f"llm-call-{model}",
391
480
  input=event.data.get("messages"),
392
- metadata={"agent_name": event.data.get("agent_name"), "model": model}
481
+ metadata={
482
+ "agent_name": event.data.get("agent_name"),
483
+ "model": model,
484
+ "user_id": user_id,
485
+ "user_query": user_query
486
+ }
393
487
  )
394
488
  span_id = self._get_span_id(event)
395
489
  self.active_spans[span_id] = generation
@@ -405,47 +499,111 @@ class LangfuseTraceCollector:
405
499
 
406
500
  # Extract usage from the event data
407
501
  usage = event.data.get("usage", {})
408
-
409
- # Convert to Langfuse v2 format
502
+
503
+ # Extract model information from choice data or event data
504
+ model = choice.get("model", "unknown")
505
+ if model == "unknown":
506
+ # Try to get model from the choice response structure
507
+ if isinstance(choice, dict):
508
+ model = choice.get("model") or choice.get("id", "unknown")
509
+
510
+ # Convert to Langfuse v2 format - let Langfuse handle cost calculation automatically
410
511
  langfuse_usage = None
411
512
  if usage:
513
+ prompt_tokens = usage.get("prompt_tokens", 0)
514
+ completion_tokens = usage.get("completion_tokens", 0)
515
+ total_tokens = usage.get("total_tokens", 0)
516
+
412
517
  langfuse_usage = {
413
- "input": usage.get("prompt_tokens", 0),
414
- "output": usage.get("completion_tokens", 0),
415
- "total": usage.get("total_tokens", 0),
518
+ "input": prompt_tokens,
519
+ "output": completion_tokens,
520
+ "total": total_tokens,
416
521
  "unit": "TOKENS"
417
522
  }
418
-
419
- generation.end(output=choice, usage=langfuse_usage)
523
+
524
+ print(f"[LANGFUSE] Usage data for automatic cost calculation: {langfuse_usage}")
525
+
526
+ # Include model information in the generation end - Langfuse will calculate costs automatically
527
+ generation.end(
528
+ output=choice,
529
+ usage=langfuse_usage,
530
+ model=model, # Pass model directly for automatic cost calculation
531
+ metadata={
532
+ "model": model,
533
+ "system_fingerprint": choice.get("system_fingerprint"),
534
+ "created": choice.get("created"),
535
+ "response_id": choice.get("id")
536
+ }
537
+ )
420
538
 
421
539
  # Clean up the span reference
422
540
  del self.active_spans[span_id]
423
- print(f"[LANGFUSE] Generation ended")
541
+ print(f"[LANGFUSE] Generation ended with cost tracking")
424
542
  else:
425
543
  print(f"[LANGFUSE] No generation found for llm_call_end: {span_id}")
426
544
 
427
545
  elif event.type == "tool_call_start":
428
- # Start a span for tool calls
429
- print(f"[LANGFUSE] Starting span for tool call: {event.data.get('tool_name')}")
546
+ # Start a span for tool calls with detailed input information
547
+ tool_name = event.data.get('tool_name', 'unknown')
548
+ tool_args = event.data.get("args", {})
549
+
550
+ print(f"[LANGFUSE] Starting span for tool call: {tool_name}")
551
+
552
+ # Create comprehensive input data for the tool call
553
+ tool_input = {
554
+ "tool_name": tool_name,
555
+ "arguments": tool_args,
556
+ "call_id": event.data.get("call_id"),
557
+ "timestamp": datetime.now().isoformat()
558
+ }
559
+
430
560
  span = self.trace_spans[trace_id].span(
431
- name=f"tool-{event.data.get('tool_name', 'unknown')}",
432
- input=event.data.get("args"),
433
- metadata={"tool_name": event.data.get("tool_name")}
561
+ name=f"tool-{tool_name}",
562
+ input=tool_input,
563
+ metadata={
564
+ "tool_name": tool_name,
565
+ "call_id": event.data.get("call_id"),
566
+ "framework": "jaf",
567
+ "event_type": "tool_call"
568
+ }
434
569
  )
435
570
  span_id = self._get_span_id(event)
436
571
  self.active_spans[span_id] = span
437
- print(f"[LANGFUSE] Created tool span: {span}")
572
+ print(f"[LANGFUSE] Created tool span for {tool_name} with args: {str(tool_args)[:100]}...")
438
573
 
439
574
  elif event.type == "tool_call_end":
440
575
  span_id = self._get_span_id(event)
441
576
  if span_id in self.active_spans:
442
- print(f"[LANGFUSE] Ending span for tool call")
443
- # End the span
577
+ tool_name = event.data.get('tool_name', 'unknown')
578
+ tool_result = event.data.get("result")
579
+
580
+ print(f"[LANGFUSE] Ending span for tool call: {tool_name}")
581
+
582
+ # Create comprehensive output data for the tool call
583
+ tool_output = {
584
+ "tool_name": tool_name,
585
+ "result": tool_result,
586
+ "call_id": event.data.get("call_id"),
587
+ "timestamp": datetime.now().isoformat(),
588
+ "status": "completed"
589
+ }
590
+
591
+ # End the span with detailed output
444
592
  span = self.active_spans[span_id]
445
- span.end(output=event.data.get("result"))
593
+ span.end(
594
+ output=tool_output,
595
+ metadata={
596
+ "tool_name": tool_name,
597
+ "call_id": event.data.get("call_id"),
598
+ "result_length": len(str(tool_result)) if tool_result else 0,
599
+ "framework": "jaf",
600
+ "event_type": "tool_call_end"
601
+ }
602
+ )
603
+
446
604
  # Clean up the span reference
447
605
  del self.active_spans[span_id]
448
- print(f"[LANGFUSE] Tool span ended")
606
+ print(f"[LANGFUSE] Tool span ended for {tool_name} with result length: {len(str(tool_result)) if tool_result else 0}")
449
607
  else:
450
608
  print(f"[LANGFUSE] No tool span found for tool_call_end: {span_id}")
451
609
 
@@ -490,7 +648,6 @@ class LangfuseTraceCollector:
490
648
  return TraceId(event.data['runId'])
491
649
 
492
650
  # Debug: print what's actually in the event data
493
- print(f"[LANGFUSE] Event data keys: {list(event.data.keys()) if hasattr(event, 'data') and event.data else 'No data'}")
494
651
  return None
495
652
 
496
653
  def _get_span_id(self, event: TraceEvent) -> str:
@@ -502,19 +659,10 @@ class LangfuseTraceCollector:
502
659
  tool_name = event.data.get('tool_name') or event.data.get('toolName', 'unknown')
503
660
  return f"tool-{tool_name}-{trace_id}"
504
661
  elif event.type.startswith('llm_call'):
505
- # For LLM calls, get the model name from the event data
506
- model = event.data.get('model')
507
-
508
- # For llm_call_end events, the model might be in the choice object
509
- if not model and event.type == 'llm_call_end':
510
- choice = event.data.get('choice', {})
511
- if isinstance(choice, dict):
512
- model = choice.get('model')
513
-
514
- # Handle case where model might be empty or None
515
- if not model or model == '':
516
- model = 'unknown'
517
- return f"llm-{model}-{trace_id}"
662
+ # For LLM calls, use a simpler consistent ID that matches between start and end
663
+ # Get run_id for more consistent matching
664
+ run_id = event.data.get('run_id') or event.data.get('runId', trace_id)
665
+ return f"llm-{run_id}"
518
666
  else:
519
667
  return f"{event.type}-{trace_id}"
520
668
 
jaf/core/types.py CHANGED
@@ -379,6 +379,8 @@ class RunStartEventData:
379
379
  run_id: RunId
380
380
  trace_id: TraceId
381
381
  session_id: Optional[str] = None
382
+ context: Optional[Any] = None
383
+ messages: Optional[List[Message]] = None
382
384
 
383
385
  @dataclass(frozen=True)
384
386
  class RunStartEvent:
@@ -392,6 +394,8 @@ class LLMCallStartEventData:
392
394
  model: str
393
395
  trace_id: TraceId
394
396
  run_id: RunId
397
+ context: Optional[Any] = None
398
+ messages: Optional[List[Message]] = None
395
399
 
396
400
  @dataclass(frozen=True)
397
401
  class LLMCallStartEvent:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaf-py
3
- Version: 2.2.4
3
+ Version: 2.3.1
4
4
  Summary: A purely functional agent framework with immutable state and composable tools - Python implementation
5
5
  Author: JAF Contributors
6
6
  Maintainer: JAF Contributors
@@ -42,7 +42,7 @@ jaf/core/__init__.py,sha256=rBvP_7TGbJICDJnA7a3qyX8yQErCDWaGAn5WzpyH4gU,1339
42
42
  jaf/core/agent_tool.py,sha256=8TcBuSxGmDTW5F_GhBU_m5S43nYqkjO4qTrNERraAig,11656
43
43
  jaf/core/analytics.py,sha256=NrUfOLLTDIhOzdfc65ZqS9AJ4ZAP9BtNtga69q0YdYw,23265
44
44
  jaf/core/composition.py,sha256=IVxRO1Q9nK7JRH32qQ4p8WMIUu66BhqPNrlTNMGFVwE,26317
45
- jaf/core/engine.py,sha256=ydlF2m9GWyHXWAP6dng8sOwxEWcaaH8etkImorfY0aY,26954
45
+ jaf/core/engine.py,sha256=v6yXp-zJF9Cj_9qlaW1rmCz7_N3YuDU02xxlzl-howM,27122
46
46
  jaf/core/errors.py,sha256=5fwTNhkojKRQ4wZj3lZlgDnAsrYyjYOwXJkIr5EGNUc,5539
47
47
  jaf/core/performance.py,sha256=jedQmTEkrKMD6_Aw1h8PdG-5TsdYSFFT7Or6k5dmN2g,9974
48
48
  jaf/core/proxy.py,sha256=_WM3cpRlSQLYpgSBrnY30UPMe2iZtlqDQ65kppE-WY0,4609
@@ -50,8 +50,8 @@ jaf/core/proxy_helpers.py,sha256=i7a5fAX9rLmO4FMBX51-yRkTFwfWedzQNgnLmeLUd_A,437
50
50
  jaf/core/streaming.py,sha256=c5o9iqpjoYV2LrUpG6qLWCYrWcP-DCcZsvMbyqKunp8,16089
51
51
  jaf/core/tool_results.py,sha256=-bTOqOX02lMyslp5Z4Dmuhx0cLd5o7kgR88qK2HO_sw,11323
52
52
  jaf/core/tools.py,sha256=SbJRRr4y_xxNYNTulZg6OiyNaHBlo_qXWYY510jxQEs,16489
53
- jaf/core/tracing.py,sha256=r96LCi4dV7BSoqVuFMXI7j72G9jOwpbEjnhYqubuQzc,23455
54
- jaf/core/types.py,sha256=fF7_UMFTPUzGQQxZ9tGoWdjF5LSI02RHoJ9vvSe3W8I,16905
53
+ jaf/core/tracing.py,sha256=3ByTbpYMWHJku-NEasK5ncyMOdv7vHwmG6ybJP19pxE,31659
54
+ jaf/core/types.py,sha256=9UXrPkepw7opgv1VGbPAC1Zx80RP4-ouRxtb6kVTA-A,17063
55
55
  jaf/core/workflows.py,sha256=Ul-82gzjIXtkhnSMSPv-8igikjkMtW1EBo9yrfodtvI,26294
56
56
  jaf/memory/__init__.py,sha256=-L98xlvihurGAzF0DnXtkueDVvO_wV2XxxEwAWdAj50,1400
57
57
  jaf/memory/factory.py,sha256=Fh6JyvQtCKe38DZV5-NnC9vPRCvzBgSSPFIGaX7Nt5E,2958
@@ -79,9 +79,9 @@ jaf/visualization/functional_core.py,sha256=zedMDZbvjuOugWwnh6SJ2stvRNQX1Hlkb9Ab
79
79
  jaf/visualization/graphviz.py,sha256=WTOM6UP72-lVKwI4_SAr5-GCC3ouckxHv88ypCDQWJ0,12056
80
80
  jaf/visualization/imperative_shell.py,sha256=GpMrAlMnLo2IQgyB2nardCz09vMvAzaYI46MyrvJ0i4,2593
81
81
  jaf/visualization/types.py,sha256=QQcbVeQJLuAOXk8ynd08DXIS-PVCnv3R-XVE9iAcglw,1389
82
- jaf_py-2.2.4.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
83
- jaf_py-2.2.4.dist-info/METADATA,sha256=LRWLY4HHpn57BSrHesTA1i5gXSP95zA7Wt1vjIx5P7w,27613
84
- jaf_py-2.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
- jaf_py-2.2.4.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
86
- jaf_py-2.2.4.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
87
- jaf_py-2.2.4.dist-info/RECORD,,
82
+ jaf_py-2.3.1.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
83
+ jaf_py-2.3.1.dist-info/METADATA,sha256=0o3VxXPHt1FO6lHDttZyFGNDk-WPH5nxaX9zXoAvcLg,27613
84
+ jaf_py-2.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ jaf_py-2.3.1.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
86
+ jaf_py-2.3.1.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
87
+ jaf_py-2.3.1.dist-info/RECORD,,
File without changes