jaf-py 2.2.4__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,99 @@ 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
+ print(f"[LANGFUSE DEBUG] Event data keys: {list(event.data.keys()) if event.data else 'No data'}")
368
+ if event.data.get("context"):
369
+ context = event.data["context"]
370
+ print(f"[LANGFUSE DEBUG] Context type: {type(context)}")
371
+ print(f"[LANGFUSE DEBUG] Context attributes: {dir(context) if hasattr(context, '__dict__') else 'Not an object'}")
372
+ if hasattr(context, '__dict__'):
373
+ print(f"[LANGFUSE DEBUG] Context dict: {context.__dict__}")
374
+
375
+ # Try to extract from context first
376
+ context = event.data.get("context")
377
+ if context:
378
+ # Try direct attribute access
379
+ if hasattr(context, 'query'):
380
+ user_query = context.query
381
+ print(f"[LANGFUSE DEBUG] Found user_query from context.query: {user_query}")
382
+
383
+ # Try to extract from combined_history
384
+ if hasattr(context, 'combined_history') and context.combined_history:
385
+ history = context.combined_history
386
+ print(f"[LANGFUSE DEBUG] Found combined_history with {len(history)} messages")
387
+ for i, msg in enumerate(reversed(history)):
388
+ print(f"[LANGFUSE DEBUG] History message {i}: {msg}")
389
+ if isinstance(msg, dict) and msg.get("role") == "user":
390
+ user_query = msg.get("content", "")
391
+ print(f"[LANGFUSE DEBUG] Found user_query from history: {user_query}")
392
+ break
393
+
394
+ # Try to extract user_id from token_response
395
+ if hasattr(context, 'token_response'):
396
+ token_response = context.token_response
397
+ print(f"[LANGFUSE DEBUG] Found token_response: {type(token_response)}")
398
+ if isinstance(token_response, dict):
399
+ user_id = token_response.get("email") or token_response.get("username")
400
+ print(f"[LANGFUSE DEBUG] Extracted user_id: {user_id}")
401
+ elif hasattr(token_response, 'email'):
402
+ user_id = token_response.email
403
+ print(f"[LANGFUSE DEBUG] Extracted user_id from attr: {user_id}")
404
+
405
+ # Fallback: try to extract from messages if context didn't work
406
+ if not user_query and event.data.get("messages"):
407
+ print(f"[LANGFUSE DEBUG] Trying fallback from messages")
408
+ messages = event.data["messages"]
409
+ print(f"[LANGFUSE DEBUG] Found {len(messages)} messages")
410
+ # Find the last user message which should be the current query
411
+ for i, msg in enumerate(reversed(messages)):
412
+ print(f"[LANGFUSE DEBUG] Message {i}: {msg}")
413
+ if isinstance(msg, dict) and msg.get("role") == "user":
414
+ user_query = msg.get("content", "")
415
+ print(f"[LANGFUSE DEBUG] Found user_query from messages: {user_query}")
416
+ break
417
+ elif hasattr(msg, 'role') and msg.role == 'user':
418
+ user_query = msg.content
419
+ print(f"[LANGFUSE DEBUG] Found user_query from message attr: {user_query}")
420
+ break
421
+
422
+ print(f"[LANGFUSE DEBUG] Final extracted - user_query: {user_query}, user_id: {user_id}")
423
+
424
+ # Create comprehensive input data for the trace
425
+ trace_input = {
426
+ "user_query": user_query,
427
+ "run_id": str(trace_id),
428
+ "agent_name": event.data.get("agent_name", "analytics_agent_jaf"),
429
+ "session_info": {
430
+ "session_id": event.data.get("session_id"),
431
+ "user_id": user_id or event.data.get("user_id")
432
+ }
433
+ }
434
+
361
435
  trace = self.langfuse.trace(
362
436
  name=f"jaf-run-{trace_id}",
363
- user_id=event.data.get("user_id"),
437
+ user_id=user_id or event.data.get("user_id"),
364
438
  session_id=event.data.get("session_id"),
365
- input=event.data,
366
- metadata={"framework": "jaf", "event_type": "run_start", "trace_id": str(trace_id)}
439
+ input=trace_input,
440
+ metadata={
441
+ "framework": "jaf",
442
+ "event_type": "run_start",
443
+ "trace_id": str(trace_id),
444
+ "user_query": user_query,
445
+ "user_id": user_id or event.data.get("user_id"),
446
+ "agent_name": event.data.get("agent_name", "analytics_agent_jaf")
447
+ }
367
448
  )
368
449
  self.trace_spans[trace_id] = trace
369
- print(f"[LANGFUSE] Created trace: {trace}")
450
+ # Store user_id and user_query for later use in generations
451
+ trace._user_id = user_id or event.data.get("user_id")
452
+ trace._user_query = user_query
453
+ print(f"[LANGFUSE] Created trace with user query: {user_query[:100] if user_query else 'None'}...")
370
454
 
371
455
  elif event.type == "run_end":
372
456
  if trace_id in self.trace_spans:
@@ -386,10 +470,21 @@ class LangfuseTraceCollector:
386
470
  # Start a generation for LLM calls
387
471
  model = event.data.get("model", "unknown")
388
472
  print(f"[LANGFUSE] Starting generation for LLM call with model: {model}")
389
- generation = self.trace_spans[trace_id].generation(
473
+
474
+ # Get stored user information from the trace
475
+ trace = self.trace_spans[trace_id]
476
+ user_id = getattr(trace, '_user_id', None)
477
+ user_query = getattr(trace, '_user_query', None)
478
+
479
+ generation = trace.generation(
390
480
  name=f"llm-call-{model}",
391
481
  input=event.data.get("messages"),
392
- metadata={"agent_name": event.data.get("agent_name"), "model": model}
482
+ metadata={
483
+ "agent_name": event.data.get("agent_name"),
484
+ "model": model,
485
+ "user_id": user_id,
486
+ "user_query": user_query
487
+ }
393
488
  )
394
489
  span_id = self._get_span_id(event)
395
490
  self.active_spans[span_id] = generation
@@ -405,47 +500,111 @@ class LangfuseTraceCollector:
405
500
 
406
501
  # Extract usage from the event data
407
502
  usage = event.data.get("usage", {})
408
-
409
- # Convert to Langfuse v2 format
503
+
504
+ # Extract model information from choice data or event data
505
+ model = choice.get("model", "unknown")
506
+ if model == "unknown":
507
+ # Try to get model from the choice response structure
508
+ if isinstance(choice, dict):
509
+ model = choice.get("model") or choice.get("id", "unknown")
510
+
511
+ # Convert to Langfuse v2 format - let Langfuse handle cost calculation automatically
410
512
  langfuse_usage = None
411
513
  if usage:
514
+ prompt_tokens = usage.get("prompt_tokens", 0)
515
+ completion_tokens = usage.get("completion_tokens", 0)
516
+ total_tokens = usage.get("total_tokens", 0)
517
+
412
518
  langfuse_usage = {
413
- "input": usage.get("prompt_tokens", 0),
414
- "output": usage.get("completion_tokens", 0),
415
- "total": usage.get("total_tokens", 0),
519
+ "input": prompt_tokens,
520
+ "output": completion_tokens,
521
+ "total": total_tokens,
416
522
  "unit": "TOKENS"
417
523
  }
418
-
419
- generation.end(output=choice, usage=langfuse_usage)
524
+
525
+ print(f"[LANGFUSE] Usage data for automatic cost calculation: {langfuse_usage}")
526
+
527
+ # Include model information in the generation end - Langfuse will calculate costs automatically
528
+ generation.end(
529
+ output=choice,
530
+ usage=langfuse_usage,
531
+ model=model, # Pass model directly for automatic cost calculation
532
+ metadata={
533
+ "model": model,
534
+ "system_fingerprint": choice.get("system_fingerprint"),
535
+ "created": choice.get("created"),
536
+ "response_id": choice.get("id")
537
+ }
538
+ )
420
539
 
421
540
  # Clean up the span reference
422
541
  del self.active_spans[span_id]
423
- print(f"[LANGFUSE] Generation ended")
542
+ print(f"[LANGFUSE] Generation ended with cost tracking")
424
543
  else:
425
544
  print(f"[LANGFUSE] No generation found for llm_call_end: {span_id}")
426
545
 
427
546
  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')}")
547
+ # Start a span for tool calls with detailed input information
548
+ tool_name = event.data.get('tool_name', 'unknown')
549
+ tool_args = event.data.get("args", {})
550
+
551
+ print(f"[LANGFUSE] Starting span for tool call: {tool_name}")
552
+
553
+ # Create comprehensive input data for the tool call
554
+ tool_input = {
555
+ "tool_name": tool_name,
556
+ "arguments": tool_args,
557
+ "call_id": event.data.get("call_id"),
558
+ "timestamp": datetime.now().isoformat()
559
+ }
560
+
430
561
  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")}
562
+ name=f"tool-{tool_name}",
563
+ input=tool_input,
564
+ metadata={
565
+ "tool_name": tool_name,
566
+ "call_id": event.data.get("call_id"),
567
+ "framework": "jaf",
568
+ "event_type": "tool_call"
569
+ }
434
570
  )
435
571
  span_id = self._get_span_id(event)
436
572
  self.active_spans[span_id] = span
437
- print(f"[LANGFUSE] Created tool span: {span}")
573
+ print(f"[LANGFUSE] Created tool span for {tool_name} with args: {str(tool_args)[:100]}...")
438
574
 
439
575
  elif event.type == "tool_call_end":
440
576
  span_id = self._get_span_id(event)
441
577
  if span_id in self.active_spans:
442
- print(f"[LANGFUSE] Ending span for tool call")
443
- # End the span
578
+ tool_name = event.data.get('tool_name', 'unknown')
579
+ tool_result = event.data.get("result")
580
+
581
+ print(f"[LANGFUSE] Ending span for tool call: {tool_name}")
582
+
583
+ # Create comprehensive output data for the tool call
584
+ tool_output = {
585
+ "tool_name": tool_name,
586
+ "result": tool_result,
587
+ "call_id": event.data.get("call_id"),
588
+ "timestamp": datetime.now().isoformat(),
589
+ "status": "completed"
590
+ }
591
+
592
+ # End the span with detailed output
444
593
  span = self.active_spans[span_id]
445
- span.end(output=event.data.get("result"))
594
+ span.end(
595
+ output=tool_output,
596
+ metadata={
597
+ "tool_name": tool_name,
598
+ "call_id": event.data.get("call_id"),
599
+ "result_length": len(str(tool_result)) if tool_result else 0,
600
+ "framework": "jaf",
601
+ "event_type": "tool_call_end"
602
+ }
603
+ )
604
+
446
605
  # Clean up the span reference
447
606
  del self.active_spans[span_id]
448
- print(f"[LANGFUSE] Tool span ended")
607
+ print(f"[LANGFUSE] Tool span ended for {tool_name} with result length: {len(str(tool_result)) if tool_result else 0}")
449
608
  else:
450
609
  print(f"[LANGFUSE] No tool span found for tool_call_end: {span_id}")
451
610
 
@@ -502,19 +661,10 @@ class LangfuseTraceCollector:
502
661
  tool_name = event.data.get('tool_name') or event.data.get('toolName', 'unknown')
503
662
  return f"tool-{tool_name}-{trace_id}"
504
663
  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}"
664
+ # For LLM calls, use a simpler consistent ID that matches between start and end
665
+ # Get run_id for more consistent matching
666
+ run_id = event.data.get('run_id') or event.data.get('runId', trace_id)
667
+ return f"llm-{run_id}"
518
668
  else:
519
669
  return f"{event.type}-{trace_id}"
520
670
 
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.0
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=1g5n-jRkSVSuQNONIgip0j3pHOfOnu2CBzoYhiknGo8,31902
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.0.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
83
+ jaf_py-2.3.0.dist-info/METADATA,sha256=_MyYA624bGLW5kwGHSXg4jNrbD_PJEd7UGwr2_JTkdA,27613
84
+ jaf_py-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ jaf_py-2.3.0.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
86
+ jaf_py-2.3.0.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
87
+ jaf_py-2.3.0.dist-info/RECORD,,
File without changes