jaf-py 2.5.3__py3-none-any.whl → 2.5.5__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 +1 -1
- jaf/core/engine.py +159 -117
- jaf/core/regeneration.py +392 -0
- jaf/core/tracing.py +1 -1
- jaf/core/types.py +115 -2
- jaf/memory/providers/in_memory.py +174 -1
- jaf/memory/providers/postgres.py +211 -1
- jaf/memory/providers/redis.py +189 -1
- jaf/memory/types.py +35 -1
- jaf/memory/utils.py +2 -0
- jaf/server/server.py +163 -0
- jaf/server/types.py +49 -1
- {jaf_py-2.5.3.dist-info → jaf_py-2.5.5.dist-info}/METADATA +2 -2
- {jaf_py-2.5.3.dist-info → jaf_py-2.5.5.dist-info}/RECORD +18 -17
- {jaf_py-2.5.3.dist-info → jaf_py-2.5.5.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.3.dist-info → jaf_py-2.5.5.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.3.dist-info → jaf_py-2.5.5.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.3.dist-info → jaf_py-2.5.5.dist-info}/top_level.txt +0 -0
jaf/__init__.py
CHANGED
|
@@ -191,7 +191,7 @@ def generate_run_id() -> RunId:
|
|
|
191
191
|
"""Generate a new run ID."""
|
|
192
192
|
return create_run_id(str(uuid.uuid4()))
|
|
193
193
|
|
|
194
|
-
__version__ = "2.5.
|
|
194
|
+
__version__ = "2.5.5"
|
|
195
195
|
__all__ = [
|
|
196
196
|
# Core types and functions
|
|
197
197
|
"TraceId", "RunId", "ValidationResult", "Message", "ModelConfig",
|
jaf/core/engine.py
CHANGED
|
@@ -293,15 +293,7 @@ async def _load_conversation_history(state: RunState[Ctx], config: RunConfig[Ctx
|
|
|
293
293
|
# For HITL scenarios, append new messages to memory messages
|
|
294
294
|
# This prevents duplication when resuming from interruptions
|
|
295
295
|
if memory_messages:
|
|
296
|
-
combined_messages = memory_messages +
|
|
297
|
-
msg for msg in state.messages
|
|
298
|
-
if not any(
|
|
299
|
-
mem_msg.role == msg.role and
|
|
300
|
-
mem_msg.content == msg.content and
|
|
301
|
-
getattr(mem_msg, 'tool_calls', None) == getattr(msg, 'tool_calls', None)
|
|
302
|
-
for mem_msg in memory_messages
|
|
303
|
-
)
|
|
304
|
-
]
|
|
296
|
+
combined_messages = memory_messages + list(state.messages)
|
|
305
297
|
else:
|
|
306
298
|
combined_messages = list(state.messages)
|
|
307
299
|
|
|
@@ -535,6 +527,17 @@ async def _run_internal(
|
|
|
535
527
|
"gpt-4o"
|
|
536
528
|
)
|
|
537
529
|
|
|
530
|
+
# Apply before_llm_call callback if provided
|
|
531
|
+
if config.before_llm_call:
|
|
532
|
+
if asyncio.iscoroutinefunction(config.before_llm_call):
|
|
533
|
+
state = await config.before_llm_call(state, current_agent)
|
|
534
|
+
else:
|
|
535
|
+
result = config.before_llm_call(state, current_agent)
|
|
536
|
+
if asyncio.iscoroutine(result):
|
|
537
|
+
state = await result
|
|
538
|
+
else:
|
|
539
|
+
state = result
|
|
540
|
+
|
|
538
541
|
# Emit LLM call start event
|
|
539
542
|
if config.on_event:
|
|
540
543
|
config.on_event(LLMCallStartEvent(data=to_event_data(LLMCallStartEventData(
|
|
@@ -546,121 +549,157 @@ async def _run_internal(
|
|
|
546
549
|
messages=state.messages
|
|
547
550
|
))))
|
|
548
551
|
|
|
549
|
-
#
|
|
552
|
+
# Retry logic for empty LLM responses
|
|
550
553
|
llm_response: Dict[str, Any]
|
|
551
554
|
assistant_event_streamed = False
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
})
|
|
577
|
-
target = partial_tool_calls[idx]
|
|
578
|
-
# id
|
|
579
|
-
tc_id = getattr(tcd, "id", None)
|
|
580
|
-
if tc_id:
|
|
581
|
-
target["id"] = tc_id
|
|
582
|
-
# function fields
|
|
583
|
-
fn = getattr(tcd, "function", None)
|
|
584
|
-
if fn is not None:
|
|
585
|
-
fn_name = getattr(fn, "name", None)
|
|
586
|
-
if fn_name:
|
|
587
|
-
target["function"]["name"] = fn_name
|
|
588
|
-
args_delta = getattr(fn, "arguments_delta", None)
|
|
589
|
-
if args_delta:
|
|
590
|
-
target["function"]["arguments"] += args_delta
|
|
591
|
-
|
|
592
|
-
# Emit partial assistant message when something changed
|
|
593
|
-
if delta_text or tcd is not None:
|
|
594
|
-
assistant_event_streamed = True
|
|
595
|
-
# Normalize tool_calls for message
|
|
596
|
-
message_tool_calls = None
|
|
597
|
-
if len(partial_tool_calls) > 0:
|
|
598
|
-
message_tool_calls = []
|
|
599
|
-
for i, tc in enumerate(partial_tool_calls):
|
|
600
|
-
arguments = tc["function"]["arguments"]
|
|
601
|
-
if isinstance(arguments, str):
|
|
602
|
-
arguments = _normalize_tool_call_arguments(arguments)
|
|
603
|
-
message_tool_calls.append({
|
|
604
|
-
"id": tc["id"] or f"call_{i}",
|
|
555
|
+
|
|
556
|
+
for retry_attempt in range(config.max_empty_response_retries + 1):
|
|
557
|
+
# Get completion from model provider, prefer streaming if available
|
|
558
|
+
get_stream = getattr(config.model_provider, "get_completion_stream", None)
|
|
559
|
+
if callable(get_stream):
|
|
560
|
+
try:
|
|
561
|
+
aggregated_text = ""
|
|
562
|
+
# Working array of partial tool calls
|
|
563
|
+
partial_tool_calls: List[Dict[str, Any]] = []
|
|
564
|
+
|
|
565
|
+
async for chunk in get_stream(state, current_agent, config): # type: ignore[arg-type]
|
|
566
|
+
# Text deltas
|
|
567
|
+
delta_text = getattr(chunk, "delta", None)
|
|
568
|
+
if delta_text:
|
|
569
|
+
aggregated_text += delta_text
|
|
570
|
+
|
|
571
|
+
# Tool call deltas
|
|
572
|
+
tcd = getattr(chunk, "tool_call_delta", None)
|
|
573
|
+
if tcd is not None:
|
|
574
|
+
idx = getattr(tcd, "index", 0) or 0
|
|
575
|
+
# Ensure slot exists
|
|
576
|
+
while len(partial_tool_calls) <= idx:
|
|
577
|
+
partial_tool_calls.append({
|
|
578
|
+
"id": None,
|
|
605
579
|
"type": "function",
|
|
606
|
-
"function": {
|
|
607
|
-
"name": tc["function"]["name"] or "",
|
|
608
|
-
"arguments": arguments
|
|
609
|
-
}
|
|
580
|
+
"function": {"name": None, "arguments": ""}
|
|
610
581
|
})
|
|
582
|
+
target = partial_tool_calls[idx]
|
|
583
|
+
# id
|
|
584
|
+
tc_id = getattr(tcd, "id", None)
|
|
585
|
+
if tc_id:
|
|
586
|
+
target["id"] = tc_id
|
|
587
|
+
# function fields
|
|
588
|
+
fn = getattr(tcd, "function", None)
|
|
589
|
+
if fn is not None:
|
|
590
|
+
fn_name = getattr(fn, "name", None)
|
|
591
|
+
if fn_name:
|
|
592
|
+
target["function"]["name"] = fn_name
|
|
593
|
+
args_delta = getattr(fn, "arguments_delta", None)
|
|
594
|
+
if args_delta:
|
|
595
|
+
target["function"]["arguments"] += args_delta
|
|
596
|
+
|
|
597
|
+
# Emit partial assistant message when something changed
|
|
598
|
+
if delta_text or tcd is not None:
|
|
599
|
+
assistant_event_streamed = True
|
|
600
|
+
# Normalize tool_calls for message
|
|
601
|
+
message_tool_calls = None
|
|
602
|
+
if len(partial_tool_calls) > 0:
|
|
603
|
+
message_tool_calls = []
|
|
604
|
+
for i, tc in enumerate(partial_tool_calls):
|
|
605
|
+
arguments = tc["function"]["arguments"]
|
|
606
|
+
if isinstance(arguments, str):
|
|
607
|
+
arguments = _normalize_tool_call_arguments(arguments)
|
|
608
|
+
message_tool_calls.append({
|
|
609
|
+
"id": tc["id"] or f"call_{i}",
|
|
610
|
+
"type": "function",
|
|
611
|
+
"function": {
|
|
612
|
+
"name": tc["function"]["name"] or "",
|
|
613
|
+
"arguments": arguments
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
partial_msg = Message(
|
|
618
|
+
role=ContentRole.ASSISTANT,
|
|
619
|
+
content=aggregated_text or "",
|
|
620
|
+
tool_calls=None if not message_tool_calls else [
|
|
621
|
+
ToolCall(
|
|
622
|
+
id=mc["id"],
|
|
623
|
+
type="function",
|
|
624
|
+
function=ToolCallFunction(
|
|
625
|
+
name=mc["function"]["name"],
|
|
626
|
+
arguments=_normalize_tool_call_arguments(mc["function"]["arguments"])
|
|
627
|
+
),
|
|
628
|
+
) for mc in message_tool_calls
|
|
629
|
+
],
|
|
630
|
+
)
|
|
631
|
+
try:
|
|
632
|
+
if config.on_event:
|
|
633
|
+
config.on_event(AssistantMessageEvent(data=to_event_data(
|
|
634
|
+
AssistantMessageEventData(message=partial_msg)
|
|
635
|
+
)))
|
|
636
|
+
except Exception as _e:
|
|
637
|
+
# Do not fail the run on callback errors
|
|
638
|
+
pass
|
|
639
|
+
|
|
640
|
+
# Build final response object compatible with downstream logic
|
|
641
|
+
final_tool_calls = None
|
|
642
|
+
if len(partial_tool_calls) > 0:
|
|
643
|
+
final_tool_calls = []
|
|
644
|
+
for i, tc in enumerate(partial_tool_calls):
|
|
645
|
+
arguments = tc["function"]["arguments"]
|
|
646
|
+
if isinstance(arguments, str):
|
|
647
|
+
arguments = _normalize_tool_call_arguments(arguments)
|
|
648
|
+
final_tool_calls.append({
|
|
649
|
+
"id": tc["id"] or f"call_{i}",
|
|
650
|
+
"type": "function",
|
|
651
|
+
"function": {
|
|
652
|
+
"name": tc["function"]["name"] or "",
|
|
653
|
+
"arguments": arguments
|
|
654
|
+
}
|
|
655
|
+
})
|
|
611
656
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
content
|
|
615
|
-
tool_calls
|
|
616
|
-
|
|
617
|
-
id=mc["id"],
|
|
618
|
-
type="function",
|
|
619
|
-
function=ToolCallFunction(
|
|
620
|
-
name=mc["function"]["name"],
|
|
621
|
-
arguments=_normalize_tool_call_arguments(mc["function"]["arguments"])
|
|
622
|
-
),
|
|
623
|
-
) for mc in message_tool_calls
|
|
624
|
-
],
|
|
625
|
-
)
|
|
626
|
-
try:
|
|
627
|
-
if config.on_event:
|
|
628
|
-
config.on_event(AssistantMessageEvent(data=to_event_data(
|
|
629
|
-
AssistantMessageEventData(message=partial_msg)
|
|
630
|
-
)))
|
|
631
|
-
except Exception as _e:
|
|
632
|
-
# Do not fail the run on callback errors
|
|
633
|
-
pass
|
|
634
|
-
|
|
635
|
-
# Build final response object compatible with downstream logic
|
|
636
|
-
final_tool_calls = None
|
|
637
|
-
if len(partial_tool_calls) > 0:
|
|
638
|
-
final_tool_calls = []
|
|
639
|
-
for i, tc in enumerate(partial_tool_calls):
|
|
640
|
-
arguments = tc["function"]["arguments"]
|
|
641
|
-
if isinstance(arguments, str):
|
|
642
|
-
arguments = _normalize_tool_call_arguments(arguments)
|
|
643
|
-
final_tool_calls.append({
|
|
644
|
-
"id": tc["id"] or f"call_{i}",
|
|
645
|
-
"type": "function",
|
|
646
|
-
"function": {
|
|
647
|
-
"name": tc["function"]["name"] or "",
|
|
648
|
-
"arguments": arguments
|
|
649
|
-
}
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
llm_response = {
|
|
653
|
-
"message": {
|
|
654
|
-
"content": aggregated_text or None,
|
|
655
|
-
"tool_calls": final_tool_calls
|
|
657
|
+
llm_response = {
|
|
658
|
+
"message": {
|
|
659
|
+
"content": aggregated_text or None,
|
|
660
|
+
"tool_calls": final_tool_calls
|
|
661
|
+
}
|
|
656
662
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
663
|
+
except Exception:
|
|
664
|
+
# Fallback to non-streaming on error
|
|
665
|
+
assistant_event_streamed = False
|
|
666
|
+
llm_response = await config.model_provider.get_completion(state, current_agent, config)
|
|
667
|
+
else:
|
|
661
668
|
llm_response = await config.model_provider.get_completion(state, current_agent, config)
|
|
662
|
-
|
|
663
|
-
|
|
669
|
+
|
|
670
|
+
# Check if response has meaningful content
|
|
671
|
+
has_content = llm_response.get('message', {}).get('content')
|
|
672
|
+
has_tool_calls = llm_response.get('message', {}).get('tool_calls')
|
|
673
|
+
|
|
674
|
+
# If we got a valid response, break out of retry loop
|
|
675
|
+
if has_content or has_tool_calls:
|
|
676
|
+
break
|
|
677
|
+
|
|
678
|
+
# If this is not the last attempt, retry with exponential backoff
|
|
679
|
+
if retry_attempt < config.max_empty_response_retries:
|
|
680
|
+
delay = config.empty_response_retry_delay * (2 ** retry_attempt)
|
|
681
|
+
if config.log_empty_responses:
|
|
682
|
+
print(f"[JAF:ENGINE] Empty LLM response on attempt {retry_attempt + 1}/{config.max_empty_response_retries + 1}, retrying in {delay:.1f}s...")
|
|
683
|
+
print(f"[JAF:ENGINE] Response had message: {bool(llm_response.get('message'))}, content: {bool(has_content)}, tool_calls: {bool(has_tool_calls)}")
|
|
684
|
+
await asyncio.sleep(delay)
|
|
685
|
+
else:
|
|
686
|
+
# Last attempt failed, log detailed diagnostic info
|
|
687
|
+
if config.log_empty_responses:
|
|
688
|
+
print(f"[JAF:ENGINE] Empty LLM response after {config.max_empty_response_retries + 1} attempts")
|
|
689
|
+
print(f"[JAF:ENGINE] Agent: {current_agent.name}, Model: {model}")
|
|
690
|
+
print(f"[JAF:ENGINE] Message count: {len(state.messages)}, Turn: {state.turn_count}")
|
|
691
|
+
print(f"[JAF:ENGINE] Response structure: {json.dumps(llm_response, indent=2)[:1000]}")
|
|
692
|
+
|
|
693
|
+
# Apply after_llm_call callback if provided
|
|
694
|
+
if config.after_llm_call:
|
|
695
|
+
if asyncio.iscoroutinefunction(config.after_llm_call):
|
|
696
|
+
llm_response = await config.after_llm_call(state, llm_response)
|
|
697
|
+
else:
|
|
698
|
+
result = config.after_llm_call(state, llm_response)
|
|
699
|
+
if asyncio.iscoroutine(result):
|
|
700
|
+
llm_response = await result
|
|
701
|
+
else:
|
|
702
|
+
llm_response = result
|
|
664
703
|
|
|
665
704
|
# Emit LLM call end event
|
|
666
705
|
if config.on_event:
|
|
@@ -673,6 +712,9 @@ async def _run_internal(
|
|
|
673
712
|
|
|
674
713
|
# Check if response has message
|
|
675
714
|
if not llm_response.get('message'):
|
|
715
|
+
if config.log_empty_responses:
|
|
716
|
+
print(f"[JAF:ENGINE] ERROR: No message in LLM response")
|
|
717
|
+
print(f"[JAF:ENGINE] Response structure: {json.dumps(llm_response, indent=2)[:500]}")
|
|
676
718
|
return RunResult(
|
|
677
719
|
final_state=state,
|
|
678
720
|
outcome=ErrorOutcome(error=ModelBehaviorError(
|