jaf-py 2.5.11__tar.gz → 2.5.13__tar.gz
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_py-2.5.11 → jaf_py-2.5.13}/PKG-INFO +2 -2
- {jaf_py-2.5.11 → jaf_py-2.5.13}/README.md +1 -1
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/__init__.py +1 -1
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/agent_tool.py +2 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/tracing.py +42 -1
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/types.py +32 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/providers/model.py +115 -4
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/PKG-INFO +2 -2
- {jaf_py-2.5.11 → jaf_py-2.5.13}/pyproject.toml +1 -1
- {jaf_py-2.5.11 → jaf_py-2.5.13}/LICENSE +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/agent.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/agent_card.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/client.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/client_example.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/integration_example.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/server_demo/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/examples/server_example.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/cleanup.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/factory.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/composite.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/in_memory.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/postgres.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/providers/redis.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/serialization.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_serialization.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/memory/types.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/protocol.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/server.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/standalone_client.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/run_tests.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_agent.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_client.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_integration.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_protocol.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/tests/test_types.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/a2a/types.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/cli.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/analytics.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/checkpoint.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/composition.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/engine.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/errors.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/guardrails.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/handoff.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/parallel_agents.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/performance.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/proxy.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/proxy_helpers.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/regeneration.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/state.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/streaming.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/tool_results.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/tools.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/core/workflows.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/exceptions.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/approval_storage.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/factory.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/in_memory.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/postgres.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/providers/redis.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/types.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/memory/utils.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/plugins/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/plugins/base.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/policies/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/policies/handoff.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/policies/validation.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/providers/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/providers/mcp.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/main.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/server.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/server/types.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/utils/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/utils/attachments.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/utils/document_processor.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/__init__.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/example.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/functional_core.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/graphviz.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/imperative_shell.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf/visualization/types.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/SOURCES.txt +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/dependency_links.txt +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/entry_points.txt +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/requires.txt +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/jaf_py.egg-info/top_level.txt +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/setup.cfg +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/setup.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_a2a_deep.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_a2a_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_api_reference_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_attachments.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_callback_system_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_coffee_tool.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_conversation_id_fix.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_deployment_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_docs_code_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_engine.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_engine_manual.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_error_handling_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_getting_started_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_manual.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_math_tool.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_comprehensive.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_docs.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_real_functionality.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_mcp_transports.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_memory_system_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_model_providers_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_property_based.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_proxy_simple.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_redis_fixes.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_redis_memory.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_server_api_examples.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_session_continuity.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_streamable_http_mcp_example.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_timeout_functionality.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_tool_integration.py +0 -0
- {jaf_py-2.5.11 → jaf_py-2.5.13}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaf-py
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.13
|
|
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
|
|
@@ -82,7 +82,7 @@ Dynamic: license-file
|
|
|
82
82
|
|
|
83
83
|
<!--  -->
|
|
84
84
|
|
|
85
|
-
[](https://github.com/xynehq/jaf-py)
|
|
86
86
|
[](https://www.python.org/)
|
|
87
87
|
[](https://xynehq.github.io/jaf-py/)
|
|
88
88
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!--  -->
|
|
4
4
|
|
|
5
|
-
[](https://github.com/xynehq/jaf-py)
|
|
6
6
|
[](https://www.python.org/)
|
|
7
7
|
[](https://xynehq.github.io/jaf-py/)
|
|
8
8
|
|
|
@@ -189,6 +189,8 @@ def create_agent_tool(
|
|
|
189
189
|
initial_input_guardrails=parent_config.initial_input_guardrails,
|
|
190
190
|
final_output_guardrails=parent_config.final_output_guardrails,
|
|
191
191
|
on_event=parent_config.on_event,
|
|
192
|
+
before_llm_call=parent_config.before_llm_call,
|
|
193
|
+
after_llm_call=parent_config.after_llm_call,
|
|
192
194
|
memory=parent_config.memory if preserve_session else None,
|
|
193
195
|
conversation_id=parent_config.conversation_id if preserve_session else None,
|
|
194
196
|
default_tool_timeout=parent_config.default_tool_timeout,
|
|
@@ -467,7 +467,7 @@ class LangfuseTraceCollector:
|
|
|
467
467
|
public_key=public_key,
|
|
468
468
|
secret_key=secret_key,
|
|
469
469
|
host=host,
|
|
470
|
-
release="jaf-py-v2.5.
|
|
470
|
+
release="jaf-py-v2.5.13",
|
|
471
471
|
httpx_client=client,
|
|
472
472
|
)
|
|
473
473
|
self._httpx_client = client
|
|
@@ -1069,6 +1069,47 @@ class LangfuseTraceCollector:
|
|
|
1069
1069
|
)
|
|
1070
1070
|
print(f"[LANGFUSE] Handoff event created")
|
|
1071
1071
|
|
|
1072
|
+
elif event.type == "retry":
|
|
1073
|
+
# Create an event for retry attempts
|
|
1074
|
+
attempt = self._get_event_data(event, "attempt", 1)
|
|
1075
|
+
max_retries = self._get_event_data(event, "max_retries", 3)
|
|
1076
|
+
reason = self._get_event_data(event, "reason", "Unknown")
|
|
1077
|
+
operation = self._get_event_data(event, "operation", "llm_call")
|
|
1078
|
+
delay = self._get_event_data(event, "delay")
|
|
1079
|
+
error_details = self._get_event_data(event, "error_details", {})
|
|
1080
|
+
|
|
1081
|
+
print(
|
|
1082
|
+
f"[LANGFUSE] Recording retry event: attempt {attempt}/{max_retries} for {operation}, reason: {reason}"
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
# Create comprehensive retry event data
|
|
1086
|
+
retry_input = {
|
|
1087
|
+
"attempt": attempt,
|
|
1088
|
+
"max_retries": max_retries,
|
|
1089
|
+
"reason": reason,
|
|
1090
|
+
"operation": operation,
|
|
1091
|
+
"delay_seconds": delay,
|
|
1092
|
+
"error_details": error_details,
|
|
1093
|
+
"timestamp": datetime.now().isoformat(),
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
# Use compatibility layer to create event (works with both v2 and v3)
|
|
1097
|
+
self._create_event(
|
|
1098
|
+
parent_span=self.trace_spans[trace_id],
|
|
1099
|
+
name=f"retry-{operation}",
|
|
1100
|
+
input=retry_input,
|
|
1101
|
+
metadata={
|
|
1102
|
+
"framework": "jaf",
|
|
1103
|
+
"event_type": "retry",
|
|
1104
|
+
"retry_attempt": attempt,
|
|
1105
|
+
"max_retries": max_retries,
|
|
1106
|
+
"operation": operation,
|
|
1107
|
+
"reason": reason,
|
|
1108
|
+
"is_final_retry": attempt >= max_retries,
|
|
1109
|
+
},
|
|
1110
|
+
)
|
|
1111
|
+
print(f"[LANGFUSE] Retry event created for attempt {attempt}/{max_retries}")
|
|
1112
|
+
|
|
1072
1113
|
else:
|
|
1073
1114
|
# Create a generic event for other event types
|
|
1074
1115
|
print(f"[LANGFUSE] Creating generic event for: {event.type}")
|
|
@@ -978,6 +978,37 @@ class OutputParseEvent:
|
|
|
978
978
|
data: OutputParseEventData = field(default_factory=lambda: OutputParseEventData("", "start"))
|
|
979
979
|
|
|
980
980
|
|
|
981
|
+
@dataclass(frozen=True)
|
|
982
|
+
class RetryEventData:
|
|
983
|
+
"""Data for retry events."""
|
|
984
|
+
|
|
985
|
+
attempt: int # Current retry attempt (1-indexed)
|
|
986
|
+
max_retries: int # Maximum number of retries configured
|
|
987
|
+
reason: str # Reason for retry (e.g., "HTTP 429 - Rate Limit", "HTTP 500 - Server Error")
|
|
988
|
+
operation: Literal["llm_call", "tool_call", "workflow_step"] # What operation is being retried
|
|
989
|
+
trace_id: TraceId
|
|
990
|
+
run_id: RunId
|
|
991
|
+
delay: Optional[float] = None # Backoff delay in seconds before next retry
|
|
992
|
+
error_details: Optional[Dict[str, Any]] = None # Additional error context
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
@dataclass(frozen=True)
|
|
996
|
+
class RetryEvent:
|
|
997
|
+
"""Event emitted when a retry occurs."""
|
|
998
|
+
|
|
999
|
+
type: Literal["retry"] = "retry"
|
|
1000
|
+
data: RetryEventData = field(
|
|
1001
|
+
default_factory=lambda: RetryEventData(
|
|
1002
|
+
attempt=1,
|
|
1003
|
+
max_retries=3,
|
|
1004
|
+
reason="",
|
|
1005
|
+
operation="llm_call",
|
|
1006
|
+
trace_id=TraceId(""),
|
|
1007
|
+
run_id=RunId(""),
|
|
1008
|
+
)
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
|
|
981
1012
|
# Union type for all trace events
|
|
982
1013
|
TraceEvent = Union[
|
|
983
1014
|
RunStartEvent,
|
|
@@ -992,6 +1023,7 @@ TraceEvent = Union[
|
|
|
992
1023
|
ToolCallEndEvent,
|
|
993
1024
|
HandoffEvent,
|
|
994
1025
|
RunEndEvent,
|
|
1026
|
+
RetryEvent,
|
|
995
1027
|
]
|
|
996
1028
|
|
|
997
1029
|
|
|
@@ -10,6 +10,7 @@ import httpx
|
|
|
10
10
|
import time
|
|
11
11
|
import os
|
|
12
12
|
import base64
|
|
13
|
+
import asyncio
|
|
13
14
|
|
|
14
15
|
from openai import AsyncOpenAI
|
|
15
16
|
from pydantic import BaseModel
|
|
@@ -27,6 +28,8 @@ from ..core.types import (
|
|
|
27
28
|
ToolCallFunctionDelta,
|
|
28
29
|
MessageContentPart,
|
|
29
30
|
get_text_content,
|
|
31
|
+
RetryEvent,
|
|
32
|
+
RetryEventData,
|
|
30
33
|
)
|
|
31
34
|
from ..core.proxy import ProxyConfig
|
|
32
35
|
from ..utils.document_processor import (
|
|
@@ -110,6 +113,102 @@ async def _is_vision_model(model: str, base_url: str) -> bool:
|
|
|
110
113
|
return is_known_vision_model
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
async def _retry_with_events(
|
|
117
|
+
operation_func,
|
|
118
|
+
state: RunState,
|
|
119
|
+
config: RunConfig,
|
|
120
|
+
operation_name: str = "llm_call",
|
|
121
|
+
max_retries: int = 3,
|
|
122
|
+
backoff_factor: float = 1.0,
|
|
123
|
+
):
|
|
124
|
+
"""
|
|
125
|
+
Wrapper that retries an async operation and emits retry events.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
operation_func: Async function to execute (should accept no arguments)
|
|
129
|
+
state: Current run state
|
|
130
|
+
config: Run configuration with event handler
|
|
131
|
+
operation_name: Name of the operation for logging
|
|
132
|
+
max_retries: Maximum number of retry attempts
|
|
133
|
+
backoff_factor: Exponential backoff multiplier
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Result from operation_func
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
Last exception if all retries are exhausted
|
|
140
|
+
"""
|
|
141
|
+
last_exception = None
|
|
142
|
+
|
|
143
|
+
for attempt in range(max_retries + 1):
|
|
144
|
+
try:
|
|
145
|
+
return await operation_func()
|
|
146
|
+
except Exception as e:
|
|
147
|
+
last_exception = e
|
|
148
|
+
|
|
149
|
+
# Check if this is a retryable HTTP error
|
|
150
|
+
is_retryable = False
|
|
151
|
+
reason = str(e)
|
|
152
|
+
error_details = {"error_type": type(e).__name__, "error_message": str(e)}
|
|
153
|
+
|
|
154
|
+
# Check for HTTP errors (common in OpenAI/LiteLLM)
|
|
155
|
+
if hasattr(e, "status_code"):
|
|
156
|
+
status_code = e.status_code
|
|
157
|
+
error_details["status_code"] = status_code
|
|
158
|
+
|
|
159
|
+
# Retry on rate limits (429) and server errors (5xx)
|
|
160
|
+
if status_code == 429:
|
|
161
|
+
is_retryable = True
|
|
162
|
+
reason = f"HTTP {status_code} - Rate Limit"
|
|
163
|
+
elif 500 <= status_code < 600:
|
|
164
|
+
is_retryable = True
|
|
165
|
+
reason = f"HTTP {status_code} - Server Error"
|
|
166
|
+
else:
|
|
167
|
+
reason = f"HTTP {status_code}"
|
|
168
|
+
|
|
169
|
+
# Check for common exception names
|
|
170
|
+
elif "RateLimitError" in type(e).__name__:
|
|
171
|
+
is_retryable = True
|
|
172
|
+
reason = "Rate Limit Error"
|
|
173
|
+
elif "ServiceUnavailableError" in type(e).__name__ or "APIError" in type(e).__name__:
|
|
174
|
+
is_retryable = True
|
|
175
|
+
reason = "API Error"
|
|
176
|
+
elif "Timeout" in type(e).__name__:
|
|
177
|
+
is_retryable = True
|
|
178
|
+
reason = "Timeout"
|
|
179
|
+
|
|
180
|
+
# If not last attempt and is retryable, retry with backoff
|
|
181
|
+
if attempt < max_retries and is_retryable:
|
|
182
|
+
delay = backoff_factor * (2**attempt) # Exponential backoff
|
|
183
|
+
|
|
184
|
+
# Emit retry event
|
|
185
|
+
if config.on_event:
|
|
186
|
+
retry_event = RetryEvent(
|
|
187
|
+
data=RetryEventData(
|
|
188
|
+
attempt=attempt + 1,
|
|
189
|
+
max_retries=max_retries,
|
|
190
|
+
reason=reason,
|
|
191
|
+
operation=operation_name,
|
|
192
|
+
trace_id=state.trace_id,
|
|
193
|
+
run_id=state.run_id,
|
|
194
|
+
delay=delay,
|
|
195
|
+
error_details=error_details,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
config.on_event(retry_event)
|
|
199
|
+
|
|
200
|
+
print(
|
|
201
|
+
f"[JAF:RETRY] Attempt {attempt + 1}/{max_retries} failed: {reason}. Retrying in {delay}s..."
|
|
202
|
+
)
|
|
203
|
+
await asyncio.sleep(delay)
|
|
204
|
+
else:
|
|
205
|
+
# Not retryable or last attempt, re-raise
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
# Should never reach here, but just in case
|
|
209
|
+
raise last_exception
|
|
210
|
+
|
|
211
|
+
|
|
113
212
|
def make_litellm_provider(
|
|
114
213
|
base_url: str,
|
|
115
214
|
api_key: str = "anything",
|
|
@@ -248,8 +347,14 @@ def make_litellm_provider(
|
|
|
248
347
|
if agent.output_codec:
|
|
249
348
|
request_params["response_format"] = {"type": "json_object"}
|
|
250
349
|
|
|
251
|
-
# Make the API call
|
|
252
|
-
|
|
350
|
+
# Make the API call with retry handling
|
|
351
|
+
async def _api_call():
|
|
352
|
+
return await self.client.chat.completions.create(**request_params)
|
|
353
|
+
|
|
354
|
+
# Use retry wrapper to track retries in Langfuse
|
|
355
|
+
response = await _retry_with_events(
|
|
356
|
+
_api_call, state, config, operation_name="llm_call", max_retries=3, backoff_factor=1.0
|
|
357
|
+
)
|
|
253
358
|
|
|
254
359
|
# Return in the expected format that the engine expects
|
|
255
360
|
choice = response.choices[0]
|
|
@@ -577,8 +682,14 @@ def make_litellm_sdk_provider(
|
|
|
577
682
|
if self.base_url:
|
|
578
683
|
request_params["api_base"] = self.base_url
|
|
579
684
|
|
|
580
|
-
# Make the API call using litellm
|
|
581
|
-
|
|
685
|
+
# Make the API call using litellm with retry handling
|
|
686
|
+
async def _api_call():
|
|
687
|
+
return await litellm.acompletion(**request_params)
|
|
688
|
+
|
|
689
|
+
# Use retry wrapper to track retries in Langfuse
|
|
690
|
+
response = await _retry_with_events(
|
|
691
|
+
_api_call, state, config, operation_name="llm_call", max_retries=3, backoff_factor=1.0
|
|
692
|
+
)
|
|
582
693
|
|
|
583
694
|
# Return in the expected format that the engine expects
|
|
584
695
|
choice = response.choices[0]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaf-py
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.13
|
|
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
|
|
@@ -82,7 +82,7 @@ Dynamic: license-file
|
|
|
82
82
|
|
|
83
83
|
<!--  -->
|
|
84
84
|
|
|
85
|
-
[](https://github.com/xynehq/jaf-py)
|
|
86
86
|
[](https://www.python.org/)
|
|
87
87
|
[](https://xynehq.github.io/jaf-py/)
|
|
88
88
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jaf-py"
|
|
7
|
-
version = "2.5.
|
|
7
|
+
version = "2.5.13"
|
|
8
8
|
description = "A purely functional agent framework with immutable state and composable tools - Python implementation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|