langchain-agentx-python 0.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.
- langchain_agentx/__init__.py +46 -0
- langchain_agentx/command/__init__.py +28 -0
- langchain_agentx/command/builtin/__init__.py +25 -0
- langchain_agentx/command/builtin/clear.py +33 -0
- langchain_agentx/command/builtin/compact.py +33 -0
- langchain_agentx/command/builtin/memory.py +37 -0
- langchain_agentx/command/builtin/reload_plugins.py +42 -0
- langchain_agentx/command/context.py +30 -0
- langchain_agentx/command/dispatcher.py +183 -0
- langchain_agentx/command/registry.py +110 -0
- langchain_agentx/command/result.py +25 -0
- langchain_agentx/command/types.py +41 -0
- langchain_agentx/config/__init__.py +14 -0
- langchain_agentx/loop/__init__.py +47 -0
- langchain_agentx/loop/config/__init__.py +20 -0
- langchain_agentx/loop/config/agent_config.py +66 -0
- langchain_agentx/loop/config/agent_loop_config.py +72 -0
- langchain_agentx/loop/config/model_context_resolver.py +105 -0
- langchain_agentx/loop/config/runtime_settings.py +50 -0
- langchain_agentx/loop/config/token_estimator.py +133 -0
- langchain_agentx/loop/context/__init__.py +66 -0
- langchain_agentx/loop/context/blocking_guard.py +97 -0
- langchain_agentx/loop/context/compaction_service.py +60 -0
- langchain_agentx/loop/context/message_utils.py +56 -0
- langchain_agentx/loop/context/pipeline.py +127 -0
- langchain_agentx/loop/context/settings.py +103 -0
- langchain_agentx/loop/context/stages/__init__.py +29 -0
- langchain_agentx/loop/context/stages/autocompact.py +140 -0
- langchain_agentx/loop/context/stages/base.py +32 -0
- langchain_agentx/loop/context/stages/collapse.py +76 -0
- langchain_agentx/loop/context/stages/microcompact.py +76 -0
- langchain_agentx/loop/context/stages/noop.py +33 -0
- langchain_agentx/loop/context/stages/snip.py +71 -0
- langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
- langchain_agentx/loop/context/types.py +79 -0
- langchain_agentx/loop/exit/__init__.py +1 -0
- langchain_agentx/loop/exit/exit_logic.py +320 -0
- langchain_agentx/loop/exit/reason_codes.py +39 -0
- langchain_agentx/loop/graph/__init__.py +5 -0
- langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
- langchain_agentx/loop/graph/factory.py +1409 -0
- langchain_agentx/loop/graph/graph_edges.py +820 -0
- langchain_agentx/loop/hook/__init__.py +48 -0
- langchain_agentx/loop/hook/async_hook_runner.py +62 -0
- langchain_agentx/loop/hook/config.py +280 -0
- langchain_agentx/loop/hook/engine.py +321 -0
- langchain_agentx/loop/hook/executors/__init__.py +9 -0
- langchain_agentx/loop/hook/executors/agent.py +107 -0
- langchain_agentx/loop/hook/executors/command.py +230 -0
- langchain_agentx/loop/hook/executors/http.py +114 -0
- langchain_agentx/loop/hook/executors/prompt.py +92 -0
- langchain_agentx/loop/hook/graph_wiring.py +134 -0
- langchain_agentx/loop/hook/registry.py +262 -0
- langchain_agentx/loop/hook/trust.py +43 -0
- langchain_agentx/loop/hook/types.py +110 -0
- langchain_agentx/loop/injection/__init__.py +13 -0
- langchain_agentx/loop/injection/dedup.py +74 -0
- langchain_agentx/loop/loop_abort.py +36 -0
- langchain_agentx/loop/model/__init__.py +1 -0
- langchain_agentx/loop/model/model_node.py +648 -0
- langchain_agentx/loop/model/model_nodes.py +661 -0
- langchain_agentx/loop/model/orphan_tool_results.py +38 -0
- langchain_agentx/loop/model/retrier.py +307 -0
- langchain_agentx/loop/model/retry_bridge.py +58 -0
- langchain_agentx/loop/model/retry_events.py +35 -0
- langchain_agentx/loop/model/retry_policy.py +56 -0
- langchain_agentx/loop/model/schema_and_format.py +153 -0
- langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
- langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
- langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
- langchain_agentx/loop/prompt/__init__.py +95 -0
- langchain_agentx/loop/prompt/builder.py +61 -0
- langchain_agentx/loop/prompt/builtin.py +218 -0
- langchain_agentx/loop/prompt/compact.py +408 -0
- langchain_agentx/loop/prompt/sections.py +120 -0
- langchain_agentx/loop/runtime/__init__.py +19 -0
- langchain_agentx/loop/runtime/context.py +34 -0
- langchain_agentx/loop/runtime/context_factory.py +107 -0
- langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
- langchain_agentx/loop/subagent/__init__.py +53 -0
- langchain_agentx/loop/subagent/async_runner.py +215 -0
- langchain_agentx/loop/subagent/context.py +209 -0
- langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
- langchain_agentx/loop/subagent/graph.py +72 -0
- langchain_agentx/loop/subagent/orchestrator.py +391 -0
- langchain_agentx/loop/subagent/progress.py +30 -0
- langchain_agentx/loop/subagent/prompt.py +52 -0
- langchain_agentx/loop/subagent/runner.py +504 -0
- langchain_agentx/loop/subagent/transcript.py +172 -0
- langchain_agentx/memory/__init__.py +2 -0
- langchain_agentx/memory/instruction/__init__.py +12 -0
- langchain_agentx/memory/instruction/loader.py +325 -0
- langchain_agentx/memory/instruction/resolver.py +24 -0
- langchain_agentx/memory/instruction/runtime.py +83 -0
- langchain_agentx/memory/instruction/sections.py +83 -0
- langchain_agentx/memory/instruction/types.py +59 -0
- langchain_agentx/memory/memdir/__init__.py +77 -0
- langchain_agentx/memory/memdir/age.py +36 -0
- langchain_agentx/memory/memdir/agent_memory.py +380 -0
- langchain_agentx/memory/memdir/extractor.py +309 -0
- langchain_agentx/memory/memdir/loader.py +187 -0
- langchain_agentx/memory/memdir/paths.py +63 -0
- langchain_agentx/memory/memdir/recall.py +45 -0
- langchain_agentx/memory/memdir/runtime.py +43 -0
- langchain_agentx/memory/memdir/scan.py +135 -0
- langchain_agentx/memory/memdir/types.py +104 -0
- langchain_agentx/memory/session/__init__.py +76 -0
- langchain_agentx/memory/session/compact_bridge.py +208 -0
- langchain_agentx/memory/session/prompts.py +172 -0
- langchain_agentx/memory/session/session_memory.py +282 -0
- langchain_agentx/observability/__init__.py +67 -0
- langchain_agentx/observability/evaluation/__init__.py +17 -0
- langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
- langchain_agentx/observability/evaluation/checkers/base.py +34 -0
- langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
- langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
- langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
- langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
- langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
- langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
- langchain_agentx/observability/evaluation/service.py +102 -0
- langchain_agentx/observability/evaluation/state.py +32 -0
- langchain_agentx/observability/evaluation/store.py +258 -0
- langchain_agentx/observability/events/__init__.py +15 -0
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
- langchain_agentx/observability/logging/__init__.py +15 -0
- langchain_agentx/observability/logging/debug_burst.py +95 -0
- langchain_agentx/observability/logging/logging_config.py +178 -0
- langchain_agentx/observability/logging/logging_contract.py +65 -0
- langchain_agentx/observability/replay/__init__.py +35 -0
- langchain_agentx/observability/replay/cli.py +91 -0
- langchain_agentx/observability/replay/service.py +83 -0
- langchain_agentx/observability/replay/store.py +278 -0
- langchain_agentx/observability/replay/ui.py +47 -0
- langchain_agentx/observability/trace/__init__.py +25 -0
- langchain_agentx/observability/trace/collector.py +560 -0
- langchain_agentx/observability/trace/event_emitter.py +183 -0
- langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
- langchain_agentx/observability/trace/models.py +144 -0
- langchain_agentx/observability/trace/sqlite_store.py +873 -0
- langchain_agentx/observability/trace/trace_callback.py +295 -0
- langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
- langchain_agentx/plugin/__init__.py +26 -0
- langchain_agentx/plugin/builtin.py +53 -0
- langchain_agentx/plugin/config.py +113 -0
- langchain_agentx/plugin/loader.py +386 -0
- langchain_agentx/plugin/manifest.py +154 -0
- langchain_agentx/plugin/registries.py +211 -0
- langchain_agentx/plugin/types.py +142 -0
- langchain_agentx/provider/__init__.py +27 -0
- langchain_agentx/provider/anthropic.py +121 -0
- langchain_agentx/provider/compatible_chat_openai.py +86 -0
- langchain_agentx/provider/env.py +45 -0
- langchain_agentx/provider/model_profile.py +156 -0
- langchain_agentx/provider/openai.py +89 -0
- langchain_agentx/session/__init__.py +17 -0
- langchain_agentx/session/agent_session.py +320 -0
- langchain_agentx/session/conversation_factory.py +87 -0
- langchain_agentx/session/conversation_recovery.py +156 -0
- langchain_agentx/session/conversation_session.py +198 -0
- langchain_agentx/session/factory.py +143 -0
- langchain_agentx/session/protocol.py +25 -0
- langchain_agentx/task_runtime/__init__.py +113 -0
- langchain_agentx/task_runtime/core/__init__.py +51 -0
- langchain_agentx/task_runtime/core/ids.py +33 -0
- langchain_agentx/task_runtime/core/interfaces.py +115 -0
- langchain_agentx/task_runtime/core/notification_priority.py +19 -0
- langchain_agentx/task_runtime/core/types.py +136 -0
- langchain_agentx/task_runtime/integrations/__init__.py +33 -0
- langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
- langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
- langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
- langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
- langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
- langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
- langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
- langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
- langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
- langchain_agentx/task_runtime/output/__init__.py +5 -0
- langchain_agentx/task_runtime/output/sink.py +64 -0
- langchain_agentx/task_runtime/policy/__init__.py +11 -0
- langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
- langchain_agentx/task_runtime/queue/__init__.py +5 -0
- langchain_agentx/task_runtime/queue/in_memory.py +55 -0
- langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
- langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
- langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
- langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
- langchain_agentx/task_runtime/store/__init__.py +6 -0
- langchain_agentx/task_runtime/store/in_memory.py +81 -0
- langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
- langchain_agentx/task_runtime/tasks/__init__.py +76 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
- langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
- langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
- langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
- langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
- langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
- langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
- langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
- langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
- langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
- langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
- langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
- langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
- langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
- langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
- langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
- langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
- langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
- langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
- langchain_agentx/tool_runtime/__init__.py +90 -0
- langchain_agentx/tool_runtime/adapter.py +365 -0
- langchain_agentx/tool_runtime/base.py +319 -0
- langchain_agentx/tool_runtime/errors.py +190 -0
- langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
- langchain_agentx/tool_runtime/loader.py +195 -0
- langchain_agentx/tool_runtime/models.py +260 -0
- langchain_agentx/tool_runtime/permission_context.py +78 -0
- langchain_agentx/tool_runtime/pipeline.py +621 -0
- langchain_agentx/tool_runtime/policy.py +447 -0
- langchain_agentx/tool_runtime/registry.py +81 -0
- langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
- langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
- langchain_agentx/tool_runtime/resolvers/background.py +32 -0
- langchain_agentx/tool_runtime/resolvers/base.py +20 -0
- langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
- langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
- langchain_agentx/tool_runtime/session_store.py +132 -0
- langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
- langchain_agentx/tool_runtime/state_bridge.py +164 -0
- langchain_agentx/tools/__init__.py +26 -0
- langchain_agentx/tools/agent/__init__.py +9 -0
- langchain_agentx/tools/agent/backend.py +53 -0
- langchain_agentx/tools/agent/built_in/__init__.py +19 -0
- langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
- langchain_agentx/tools/agent/built_in/explore.py +80 -0
- langchain_agentx/tools/agent/built_in/general.py +57 -0
- langchain_agentx/tools/agent/built_in/plan.py +89 -0
- langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
- langchain_agentx/tools/agent/built_in/verification.py +120 -0
- langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
- langchain_agentx/tools/agent/cwd_resolution.py +119 -0
- langchain_agentx/tools/agent/limits.py +26 -0
- langchain_agentx/tools/agent/loader.py +270 -0
- langchain_agentx/tools/agent/models.py +85 -0
- langchain_agentx/tools/agent/prompt.py +120 -0
- langchain_agentx/tools/agent/registry/__init__.py +18 -0
- langchain_agentx/tools/agent/registry/config.py +29 -0
- langchain_agentx/tools/agent/registry/registry.py +47 -0
- langchain_agentx/tools/agent/scope.py +137 -0
- langchain_agentx/tools/agent/tool.py +256 -0
- langchain_agentx/tools/bash/__init__.py +9 -0
- langchain_agentx/tools/bash/ast_security.py +571 -0
- langchain_agentx/tools/bash/backend.py +1447 -0
- langchain_agentx/tools/bash/bash_hardening.py +734 -0
- langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
- langchain_agentx/tools/bash/cwd_reporter.py +95 -0
- langchain_agentx/tools/bash/limits.py +71 -0
- langchain_agentx/tools/bash/mode_validation.py +282 -0
- langchain_agentx/tools/bash/models.py +131 -0
- langchain_agentx/tools/bash/observability.py +148 -0
- langchain_agentx/tools/bash/output_utils.py +200 -0
- langchain_agentx/tools/bash/path_security.py +2429 -0
- langchain_agentx/tools/bash/prompt.py +68 -0
- langchain_agentx/tools/bash/read_only_validation.py +589 -0
- langchain_agentx/tools/bash/result_presenter.py +324 -0
- langchain_agentx/tools/bash/sandbox_decision.py +133 -0
- langchain_agentx/tools/bash/security.py +311 -0
- langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
- langchain_agentx/tools/bash/sed_validation.py +163 -0
- langchain_agentx/tools/bash/semantics.py +111 -0
- langchain_agentx/tools/bash/session_manager.py +205 -0
- langchain_agentx/tools/bash/session_runtime.py +290 -0
- langchain_agentx/tools/bash/shell_locator.py +191 -0
- langchain_agentx/tools/bash/task_runtime.py +91 -0
- langchain_agentx/tools/bash/tool.py +939 -0
- langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
- langchain_agentx/tools/glob/__init__.py +9 -0
- langchain_agentx/tools/glob/models.py +57 -0
- langchain_agentx/tools/glob/pagination.py +30 -0
- langchain_agentx/tools/glob/prompt.py +24 -0
- langchain_agentx/tools/glob/rg_list_backend.py +139 -0
- langchain_agentx/tools/glob/rg_pattern.py +44 -0
- langchain_agentx/tools/glob/tool.py +327 -0
- langchain_agentx/tools/grep/__init__.py +7 -0
- langchain_agentx/tools/grep/backend.py +375 -0
- langchain_agentx/tools/grep/models.py +127 -0
- langchain_agentx/tools/grep/prompt.py +30 -0
- langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
- langchain_agentx/tools/grep/tool.py +475 -0
- langchain_agentx/tools/read/__init__.py +9 -0
- langchain_agentx/tools/read/backend.py +415 -0
- langchain_agentx/tools/read/limits.py +67 -0
- langchain_agentx/tools/read/models.py +156 -0
- langchain_agentx/tools/read/prompt.py +73 -0
- langchain_agentx/tools/read/tool.py +494 -0
- langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
- langchain_agentx/tools/skill/__init__.py +4 -0
- langchain_agentx/tools/skill/argument_substitution.py +80 -0
- langchain_agentx/tools/skill/loader.py +196 -0
- langchain_agentx/tools/skill/models.py +88 -0
- langchain_agentx/tools/skill/policy.py +80 -0
- langchain_agentx/tools/skill/prompt.py +35 -0
- langchain_agentx/tools/skill/tool.py +222 -0
- langchain_agentx/utils/__init__.py +0 -0
- langchain_agentx/utils/cwd.py +124 -0
- langchain_agentx/utils/host_platform.py +112 -0
- langchain_agentx/utils/path_hierarchy.py +48 -0
- langchain_agentx/utils/path_user_input.py +66 -0
- langchain_agentx/utils/rg_executable.py +18 -0
- langchain_agentx/utils/subprocess_text.py +101 -0
- langchain_agentx/utils/temp_paths.py +77 -0
- langchain_agentx/utils/unc_path.py +25 -0
- langchain_agentx/utils/win_reserved_paths.py +51 -0
- langchain_agentx/workflow/__init__.py +7 -0
- langchain_agentx/workflow/base.py +97 -0
- langchain_agentx/workflow/batch.py +55 -0
- langchain_agentx/workflow/dag.py +54 -0
- langchain_agentx/workspace/__init__.py +13 -0
- langchain_agentx/workspace/config.py +140 -0
- langchain_agentx/workspace/path_key_normalizer.py +30 -0
- langchain_agentx/workspace/resolver.py +74 -0
- langchain_agentx/workspace/validators.py +41 -0
- langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
- langchain_agentx_python-0.1.dist-info/METADATA +513 -0
- langchain_agentx_python-0.1.dist-info/RECORD +354 -0
- langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
- langchain_agentx_python-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loop model 调用重试器。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 以 OOP 方式编排重试循环与等待策略
|
|
6
|
+
- 产出对用户可见的重试事件(scheduled/heartbeat)
|
|
7
|
+
- 复用默认异常分类(retry_on)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Awaitable, Callable, Generic, Literal, Protocol, TypeVar
|
|
15
|
+
|
|
16
|
+
from .retry_events import ModelRetryEvent
|
|
17
|
+
from .retry_policy import RetryDelayPolicy, RetryHeartbeatPolicy
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
RetryScope = Literal["foreground", "background"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class RetryContext:
|
|
25
|
+
scope: RetryScope = "foreground"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_retry_on(exc: Exception) -> bool:
|
|
29
|
+
"""判断异常是否应该重试。排除用户取消;覆盖常见限流/连接/服务端错误。"""
|
|
30
|
+
if isinstance(exc, (KeyboardInterrupt, SystemExit)):
|
|
31
|
+
return False
|
|
32
|
+
try:
|
|
33
|
+
if isinstance(exc, asyncio.CancelledError):
|
|
34
|
+
return False
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
status = getattr(exc, "status_code", None) or getattr(exc, "status", None)
|
|
39
|
+
if isinstance(status, int) and status in (429, 500, 502, 503, 504, 529):
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from openai import APIConnectionError, InternalServerError, RateLimitError
|
|
44
|
+
|
|
45
|
+
if isinstance(exc, (RateLimitError, APIConnectionError, InternalServerError)):
|
|
46
|
+
return True
|
|
47
|
+
except ImportError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
import httpx
|
|
52
|
+
|
|
53
|
+
if isinstance(exc, (httpx.ConnectError, httpx.TimeoutException, httpx.RemoteProtocolError)):
|
|
54
|
+
return True
|
|
55
|
+
except ImportError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_status_code(exc: Exception) -> int | None:
|
|
62
|
+
status = getattr(exc, "status_code", None) or getattr(exc, "status", None)
|
|
63
|
+
return status if isinstance(status, int) else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AuthRecoveryHandler(Protocol):
|
|
67
|
+
def recover(self, exc: Exception) -> bool: ...
|
|
68
|
+
|
|
69
|
+
async def arecover(self, exc: Exception) -> bool: ...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True)
|
|
73
|
+
class RetryClassifier:
|
|
74
|
+
retry_on: Callable[[Exception], bool]
|
|
75
|
+
allow_background_529_retry: bool = False
|
|
76
|
+
has_auth_recovery_handler: bool = False
|
|
77
|
+
|
|
78
|
+
def should_retry(self, exc: Exception, ctx: RetryContext) -> bool:
|
|
79
|
+
status = _extract_status_code(exc)
|
|
80
|
+
if status == 529 and ctx.scope == "background" and not self.allow_background_529_retry:
|
|
81
|
+
return False
|
|
82
|
+
if status in (401, 403):
|
|
83
|
+
return self.has_auth_recovery_handler
|
|
84
|
+
if status in (408, 409):
|
|
85
|
+
return True
|
|
86
|
+
return self.retry_on(exc)
|
|
87
|
+
|
|
88
|
+
def should_attempt_auth_recovery(self, exc: Exception) -> bool:
|
|
89
|
+
return _extract_status_code(exc) in (401, 403)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(slots=True)
|
|
93
|
+
class ModelRetryConfig:
|
|
94
|
+
max_retries: int = 10
|
|
95
|
+
initial_delay: float = 0.5
|
|
96
|
+
backoff_factor: float = 2.0
|
|
97
|
+
max_delay: float = 32.0
|
|
98
|
+
jitter: bool = True
|
|
99
|
+
heartbeat_threshold_seconds: float = 30.0
|
|
100
|
+
heartbeat_interval_seconds: float = 30.0
|
|
101
|
+
retry_on: Callable[[Exception], bool] = default_retry_on
|
|
102
|
+
allow_background_529_retry: bool = False
|
|
103
|
+
auth_recovery_handler: AuthRecoveryHandler | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(slots=True)
|
|
107
|
+
class RetryExecutionResult(Generic[T]):
|
|
108
|
+
value: T
|
|
109
|
+
events: list[ModelRetryEvent]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(slots=True)
|
|
113
|
+
class ModelCallRetrier:
|
|
114
|
+
config: ModelRetryConfig
|
|
115
|
+
|
|
116
|
+
def invoke(
|
|
117
|
+
self,
|
|
118
|
+
fn: Callable[[], T],
|
|
119
|
+
*,
|
|
120
|
+
context: RetryContext | None = None,
|
|
121
|
+
) -> RetryExecutionResult[T]:
|
|
122
|
+
events: list[ModelRetryEvent] = []
|
|
123
|
+
delay_policy = self._delay_policy()
|
|
124
|
+
hb_policy = self._heartbeat_policy()
|
|
125
|
+
classifier = self._classifier()
|
|
126
|
+
ctx = context or RetryContext()
|
|
127
|
+
attempt = 0
|
|
128
|
+
auth_recovery_attempted = False
|
|
129
|
+
while True:
|
|
130
|
+
try:
|
|
131
|
+
return RetryExecutionResult(value=fn(), events=events)
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
if (not auth_recovery_attempted) and self._recover_auth_sync(exc, classifier):
|
|
134
|
+
auth_recovery_attempted = True
|
|
135
|
+
continue
|
|
136
|
+
if attempt >= self.config.max_retries or not classifier.should_retry(exc, ctx):
|
|
137
|
+
raise
|
|
138
|
+
self._sleep_before_next(
|
|
139
|
+
attempt=attempt,
|
|
140
|
+
exc=exc,
|
|
141
|
+
events=events,
|
|
142
|
+
delay_policy=delay_policy,
|
|
143
|
+
heartbeat_policy=hb_policy,
|
|
144
|
+
)
|
|
145
|
+
auth_recovery_attempted = False
|
|
146
|
+
attempt += 1
|
|
147
|
+
|
|
148
|
+
async def ainvoke(
|
|
149
|
+
self,
|
|
150
|
+
fn: Callable[[], Awaitable[T]],
|
|
151
|
+
*,
|
|
152
|
+
context: RetryContext | None = None,
|
|
153
|
+
) -> RetryExecutionResult[T]:
|
|
154
|
+
events: list[ModelRetryEvent] = []
|
|
155
|
+
delay_policy = self._delay_policy()
|
|
156
|
+
hb_policy = self._heartbeat_policy()
|
|
157
|
+
classifier = self._classifier()
|
|
158
|
+
ctx = context or RetryContext()
|
|
159
|
+
attempt = 0
|
|
160
|
+
auth_recovery_attempted = False
|
|
161
|
+
while True:
|
|
162
|
+
try:
|
|
163
|
+
return RetryExecutionResult(value=await fn(), events=events)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
if (not auth_recovery_attempted) and await self._recover_auth_async(exc, classifier):
|
|
166
|
+
auth_recovery_attempted = True
|
|
167
|
+
continue
|
|
168
|
+
if attempt >= self.config.max_retries or not classifier.should_retry(exc, ctx):
|
|
169
|
+
raise
|
|
170
|
+
await self._asleep_before_next(
|
|
171
|
+
attempt=attempt,
|
|
172
|
+
exc=exc,
|
|
173
|
+
events=events,
|
|
174
|
+
delay_policy=delay_policy,
|
|
175
|
+
heartbeat_policy=hb_policy,
|
|
176
|
+
)
|
|
177
|
+
auth_recovery_attempted = False
|
|
178
|
+
attempt += 1
|
|
179
|
+
|
|
180
|
+
def _sleep_before_next(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
attempt: int,
|
|
184
|
+
exc: Exception,
|
|
185
|
+
events: list[ModelRetryEvent],
|
|
186
|
+
delay_policy: RetryDelayPolicy,
|
|
187
|
+
heartbeat_policy: RetryHeartbeatPolicy,
|
|
188
|
+
) -> None:
|
|
189
|
+
import time
|
|
190
|
+
|
|
191
|
+
delay = delay_policy.compute_delay_seconds(attempt=attempt, exc=exc)
|
|
192
|
+
self._record_wait_events(
|
|
193
|
+
events=events,
|
|
194
|
+
attempt=attempt,
|
|
195
|
+
exc=exc,
|
|
196
|
+
delay_seconds=delay,
|
|
197
|
+
heartbeat_policy=heartbeat_policy,
|
|
198
|
+
)
|
|
199
|
+
time.sleep(delay)
|
|
200
|
+
|
|
201
|
+
async def _asleep_before_next(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
attempt: int,
|
|
205
|
+
exc: Exception,
|
|
206
|
+
events: list[ModelRetryEvent],
|
|
207
|
+
delay_policy: RetryDelayPolicy,
|
|
208
|
+
heartbeat_policy: RetryHeartbeatPolicy,
|
|
209
|
+
) -> None:
|
|
210
|
+
delay = delay_policy.compute_delay_seconds(attempt=attempt, exc=exc)
|
|
211
|
+
self._record_wait_events(
|
|
212
|
+
events=events,
|
|
213
|
+
attempt=attempt,
|
|
214
|
+
exc=exc,
|
|
215
|
+
delay_seconds=delay,
|
|
216
|
+
heartbeat_policy=heartbeat_policy,
|
|
217
|
+
)
|
|
218
|
+
await asyncio.sleep(delay)
|
|
219
|
+
|
|
220
|
+
def _record_wait_events(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
events: list[ModelRetryEvent],
|
|
224
|
+
attempt: int,
|
|
225
|
+
exc: Exception,
|
|
226
|
+
delay_seconds: float,
|
|
227
|
+
heartbeat_policy: RetryHeartbeatPolicy,
|
|
228
|
+
) -> None:
|
|
229
|
+
total_ms = max(0, int(delay_seconds * 1000))
|
|
230
|
+
attempt_no = attempt + 1
|
|
231
|
+
events.append(
|
|
232
|
+
ModelRetryEvent(
|
|
233
|
+
event_type="scheduled",
|
|
234
|
+
retry_in_ms=total_ms,
|
|
235
|
+
attempt=attempt_no,
|
|
236
|
+
max_retries=self.config.max_retries,
|
|
237
|
+
error_type=type(exc).__name__,
|
|
238
|
+
error_message=str(exc),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
if not heartbeat_policy.should_emit_heartbeat(delay_seconds):
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
interval_ms = max(1000, int(heartbeat_policy.interval_seconds * 1000))
|
|
245
|
+
remaining = total_ms - interval_ms
|
|
246
|
+
while remaining > 0:
|
|
247
|
+
events.append(
|
|
248
|
+
ModelRetryEvent(
|
|
249
|
+
event_type="heartbeat",
|
|
250
|
+
retry_in_ms=remaining,
|
|
251
|
+
attempt=attempt_no,
|
|
252
|
+
max_retries=self.config.max_retries,
|
|
253
|
+
error_type=type(exc).__name__,
|
|
254
|
+
error_message=str(exc),
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
remaining -= interval_ms
|
|
258
|
+
|
|
259
|
+
def _delay_policy(self) -> RetryDelayPolicy:
|
|
260
|
+
return RetryDelayPolicy(
|
|
261
|
+
initial_delay=self.config.initial_delay,
|
|
262
|
+
backoff_factor=self.config.backoff_factor,
|
|
263
|
+
max_delay=self.config.max_delay,
|
|
264
|
+
jitter=self.config.jitter,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _heartbeat_policy(self) -> RetryHeartbeatPolicy:
|
|
268
|
+
return RetryHeartbeatPolicy(
|
|
269
|
+
threshold_seconds=self.config.heartbeat_threshold_seconds,
|
|
270
|
+
interval_seconds=self.config.heartbeat_interval_seconds,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _classifier(self) -> RetryClassifier:
|
|
274
|
+
return RetryClassifier(
|
|
275
|
+
retry_on=self.config.retry_on,
|
|
276
|
+
allow_background_529_retry=self.config.allow_background_529_retry,
|
|
277
|
+
has_auth_recovery_handler=self.config.auth_recovery_handler is not None,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def _recover_auth_sync(self, exc: Exception, classifier: RetryClassifier) -> bool:
|
|
281
|
+
handler = self.config.auth_recovery_handler
|
|
282
|
+
if handler is None or not classifier.should_attempt_auth_recovery(exc):
|
|
283
|
+
return False
|
|
284
|
+
try:
|
|
285
|
+
return bool(handler.recover(exc))
|
|
286
|
+
except Exception:
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
async def _recover_auth_async(self, exc: Exception, classifier: RetryClassifier) -> bool:
|
|
290
|
+
handler = self.config.auth_recovery_handler
|
|
291
|
+
if handler is None or not classifier.should_attempt_auth_recovery(exc):
|
|
292
|
+
return False
|
|
293
|
+
try:
|
|
294
|
+
return bool(await handler.arecover(exc))
|
|
295
|
+
except Exception:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
__all__ = [
|
|
300
|
+
"ModelRetryConfig",
|
|
301
|
+
"ModelCallRetrier",
|
|
302
|
+
"RetryContext",
|
|
303
|
+
"RetryClassifier",
|
|
304
|
+
"AuthRecoveryHandler",
|
|
305
|
+
"RetryExecutionResult",
|
|
306
|
+
"default_retry_on",
|
|
307
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
模型重试事件到 loop message 的桥接。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 将 retrier 产出的事件转为 SystemMessage
|
|
6
|
+
- 由 model_node 统一并入 Command(update={"messages": ...})
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Iterable
|
|
12
|
+
|
|
13
|
+
from langchain_core.messages import SystemMessage
|
|
14
|
+
from langgraph.types import Command
|
|
15
|
+
|
|
16
|
+
from .retry_events import (
|
|
17
|
+
RETRY_EVENT_METADATA_FLAG,
|
|
18
|
+
RETRY_EVENT_SUBTYPE,
|
|
19
|
+
ModelRetryEvent,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _format_retry_event_message(event: ModelRetryEvent) -> str:
|
|
24
|
+
seconds = max(0, int(round(event.retry_in_ms / 1000)))
|
|
25
|
+
if event.event_type == "heartbeat":
|
|
26
|
+
prefix = "Retry still pending"
|
|
27
|
+
else:
|
|
28
|
+
prefix = "Retry scheduled"
|
|
29
|
+
return (
|
|
30
|
+
f"[{prefix}] in {seconds}s "
|
|
31
|
+
f"(attempt {event.attempt}/{event.max_retries}) "
|
|
32
|
+
f"{event.error_type}: {event.error_message}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_retry_event_commands(events: Iterable[ModelRetryEvent]) -> list[Command]:
|
|
37
|
+
commands: list[Command] = []
|
|
38
|
+
for event in events:
|
|
39
|
+
msg = SystemMessage(
|
|
40
|
+
content=_format_retry_event_message(event),
|
|
41
|
+
response_metadata={
|
|
42
|
+
RETRY_EVENT_METADATA_FLAG: True,
|
|
43
|
+
"subtype": RETRY_EVENT_SUBTYPE,
|
|
44
|
+
"event_type": event.event_type,
|
|
45
|
+
"retry_in_ms": event.retry_in_ms,
|
|
46
|
+
"attempt": event.attempt,
|
|
47
|
+
"max_retries": event.max_retries,
|
|
48
|
+
"error": {
|
|
49
|
+
"type": event.error_type,
|
|
50
|
+
"message": event.error_message,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
commands.append(Command(update={"messages": [msg]}))
|
|
55
|
+
return commands
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["build_retry_event_commands"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
模型重试事件类型定义。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 提供 retrier 与 loop node 间的事件契约
|
|
6
|
+
- 约束对用户可见的重试提示字段(attempt/retry_in_ms/error)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
RetryEventType = Literal["scheduled", "heartbeat"]
|
|
16
|
+
RETRY_EVENT_METADATA_FLAG = "langchain_agentx_retry_event"
|
|
17
|
+
RETRY_EVENT_SUBTYPE = "api_retry"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class ModelRetryEvent:
|
|
22
|
+
event_type: RetryEventType
|
|
23
|
+
retry_in_ms: int
|
|
24
|
+
attempt: int
|
|
25
|
+
max_retries: int
|
|
26
|
+
error_type: str
|
|
27
|
+
error_message: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ModelRetryEvent",
|
|
32
|
+
"RetryEventType",
|
|
33
|
+
"RETRY_EVENT_METADATA_FLAG",
|
|
34
|
+
"RETRY_EVENT_SUBTYPE",
|
|
35
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
模型重试策略对象。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- 计算每次重试等待时长(优先 Retry-After)
|
|
6
|
+
- 管理 heartbeat 分段策略(长等待期间每 N 秒提示)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _extract_retry_after_seconds(exc: Exception) -> int | None:
|
|
16
|
+
headers = getattr(exc, "headers", None)
|
|
17
|
+
if headers is None:
|
|
18
|
+
return None
|
|
19
|
+
retry_after = None
|
|
20
|
+
if hasattr(headers, "get"):
|
|
21
|
+
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
|
22
|
+
if retry_after is None:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return max(0, int(str(retry_after).strip()))
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class RetryDelayPolicy:
|
|
32
|
+
initial_delay: float = 0.5
|
|
33
|
+
backoff_factor: float = 2.0
|
|
34
|
+
max_delay: float = 32.0
|
|
35
|
+
jitter: bool = True
|
|
36
|
+
|
|
37
|
+
def compute_delay_seconds(self, *, attempt: int, exc: Exception) -> float:
|
|
38
|
+
retry_after = _extract_retry_after_seconds(exc)
|
|
39
|
+
if retry_after is not None:
|
|
40
|
+
return float(retry_after)
|
|
41
|
+
base = min(self.initial_delay * (self.backoff_factor ** attempt), self.max_delay)
|
|
42
|
+
if not self.jitter:
|
|
43
|
+
return base
|
|
44
|
+
return base * random.uniform(0.75, 1.25)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class RetryHeartbeatPolicy:
|
|
49
|
+
threshold_seconds: float = 30.0
|
|
50
|
+
interval_seconds: float = 30.0
|
|
51
|
+
|
|
52
|
+
def should_emit_heartbeat(self, delay_seconds: float) -> bool:
|
|
53
|
+
return delay_seconds > self.threshold_seconds
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["RetryDelayPolicy", "RetryHeartbeatPolicy"]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain AgentX agent 的 schema 合并与响应格式工具。
|
|
3
|
+
|
|
4
|
+
本模块从 `factory.py` 中拆分出以下与图结构无关的纯工具逻辑:
|
|
5
|
+
- 基于多份 TypedDict schema 合并出 state / input / output schema
|
|
6
|
+
- 处理 `OmitFromSchema` / `Annotated` 等元数据,控制字段是否出现在输入或输出中
|
|
7
|
+
- 探测当前模型是否支持 provider 侧结构化输出(provider strategy)
|
|
8
|
+
- 处理 structured output 错误并决定是否重试,以及给模型的错误提示内容
|
|
9
|
+
|
|
10
|
+
这些工具保持与 LangChain 官方语义一致,可在不同 agent 工厂中复用。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, get_args, get_origin, get_type_hints
|
|
16
|
+
|
|
17
|
+
from langchain.agents.middleware.types import OmitFromSchema
|
|
18
|
+
from langchain.agents.structured_output import (
|
|
19
|
+
AutoStrategy,
|
|
20
|
+
MultipleStructuredOutputsError,
|
|
21
|
+
OutputToolBinding,
|
|
22
|
+
ProviderStrategy,
|
|
23
|
+
ProviderStrategyBinding,
|
|
24
|
+
ResponseFormat,
|
|
25
|
+
StructuredOutputError,
|
|
26
|
+
StructuredOutputValidationError,
|
|
27
|
+
ToolStrategy,
|
|
28
|
+
)
|
|
29
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
30
|
+
from langchain_core.tools import BaseTool
|
|
31
|
+
from typing_extensions import Annotated, NotRequired, Required, TypedDict
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
|
|
35
|
+
|
|
36
|
+
FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [
|
|
37
|
+
# if model profile data are not available, these models are assumed to support
|
|
38
|
+
# structured output
|
|
39
|
+
"grok",
|
|
40
|
+
"gpt-5",
|
|
41
|
+
"gpt-4.1",
|
|
42
|
+
"gpt-4o",
|
|
43
|
+
"gpt-oss",
|
|
44
|
+
"o3-pro",
|
|
45
|
+
"o3-mini",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_schema(schemas: set[type], schema_name: str, omit_flag: str | None = None) -> type:
|
|
50
|
+
"""Resolve schema by merging schemas and optionally respecting `OmitFromSchema` annotations."""
|
|
51
|
+
all_annotations: dict[str, Any] = {}
|
|
52
|
+
|
|
53
|
+
for schema in schemas:
|
|
54
|
+
hints = get_type_hints(schema, include_extras=True)
|
|
55
|
+
|
|
56
|
+
for field_name, field_type in hints.items():
|
|
57
|
+
should_omit = False
|
|
58
|
+
|
|
59
|
+
if omit_flag:
|
|
60
|
+
metadata = _extract_metadata(field_type)
|
|
61
|
+
for meta in metadata:
|
|
62
|
+
if isinstance(meta, OmitFromSchema) and getattr(meta, omit_flag) is True:
|
|
63
|
+
should_omit = True
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if not should_omit:
|
|
67
|
+
all_annotations[field_name] = field_type
|
|
68
|
+
|
|
69
|
+
return TypedDict(schema_name, all_annotations) # type: ignore[operator]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_metadata(type_: type) -> list[Any]:
|
|
73
|
+
"""Extract metadata from a field type, handling `Required`/`NotRequired` and `Annotated` wrappers."""
|
|
74
|
+
if get_origin(type_) in {Required, NotRequired}:
|
|
75
|
+
inner_type = get_args(type_)[0]
|
|
76
|
+
if get_origin(inner_type) is Annotated:
|
|
77
|
+
return list(get_args(inner_type)[1:])
|
|
78
|
+
|
|
79
|
+
if get_origin(type_) is Annotated:
|
|
80
|
+
return list(get_args(type_)[1:])
|
|
81
|
+
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _supports_provider_strategy(
|
|
86
|
+
model: str | BaseChatModel, tools: list[BaseTool | dict[str, Any]] | None = None
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""Check if a model supports provider-specific structured output."""
|
|
89
|
+
model_name: str | None = None
|
|
90
|
+
if isinstance(model, str):
|
|
91
|
+
model_name = model
|
|
92
|
+
elif isinstance(model, BaseChatModel):
|
|
93
|
+
model_name = (
|
|
94
|
+
getattr(model, "model_name", None)
|
|
95
|
+
or getattr(model, "model", None)
|
|
96
|
+
or getattr(model, "model_id", "")
|
|
97
|
+
)
|
|
98
|
+
model_profile = model.profile
|
|
99
|
+
if (
|
|
100
|
+
model_profile is not None
|
|
101
|
+
and model_profile.get("structured_output")
|
|
102
|
+
and not (
|
|
103
|
+
tools
|
|
104
|
+
and isinstance(model_name, str)
|
|
105
|
+
and "gemini" in model_name.lower()
|
|
106
|
+
and "gemini-3" not in model_name.lower()
|
|
107
|
+
)
|
|
108
|
+
):
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
any(part in model_name.lower() for part in FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT)
|
|
113
|
+
if model_name
|
|
114
|
+
else False
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _handle_structured_output_error(
|
|
119
|
+
exception: Exception,
|
|
120
|
+
response_format: ResponseFormat[Any],
|
|
121
|
+
) -> tuple[bool, str]:
|
|
122
|
+
"""Handle structured output error. Returns `(should_retry, retry_tool_message)`."""
|
|
123
|
+
if not isinstance(response_format, ToolStrategy):
|
|
124
|
+
return False, ""
|
|
125
|
+
|
|
126
|
+
handle_errors = response_format.handle_errors
|
|
127
|
+
|
|
128
|
+
if handle_errors is False:
|
|
129
|
+
return False, ""
|
|
130
|
+
if handle_errors is True:
|
|
131
|
+
return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
|
|
132
|
+
if isinstance(handle_errors, str):
|
|
133
|
+
return True, handle_errors
|
|
134
|
+
if isinstance(handle_errors, type):
|
|
135
|
+
if issubclass(handle_errors, Exception) and isinstance(exception, handle_errors):
|
|
136
|
+
return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
|
|
137
|
+
return False, ""
|
|
138
|
+
if isinstance(handle_errors, tuple):
|
|
139
|
+
if any(isinstance(exception, exc_type) for exc_type in handle_errors):
|
|
140
|
+
return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
|
|
141
|
+
return False, ""
|
|
142
|
+
return True, handle_errors(exception)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
__all__ = [
|
|
146
|
+
"STRUCTURED_OUTPUT_ERROR_TEMPLATE",
|
|
147
|
+
"FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT",
|
|
148
|
+
"_resolve_schema",
|
|
149
|
+
"_extract_metadata",
|
|
150
|
+
"_supports_provider_strategy",
|
|
151
|
+
"_handle_structured_output_error",
|
|
152
|
+
]
|
|
153
|
+
|