fred-runtime 3.4.4__tar.gz → 3.4.5__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.
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/PKG-INFO +3 -3
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/agent_app.py +120 -49
- fred_runtime-3.4.5/fred_runtime/app/chat_input_limit.py +109 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/config.py +8 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/context.py +29 -4
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/openai_compat_router.py +19 -11
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_access/capability.py +65 -13
- fred_runtime-3.4.5/fred_runtime/capabilities/document_extract/__init__.py +28 -0
- fred_runtime-3.4.5/fred_runtime/capabilities/document_extract/capability.py +197 -0
- fred_runtime-3.4.5/fred_runtime/capabilities/document_label_search/__init__.py +30 -0
- fred_runtime-3.4.5/fred_runtime/capabilities/document_label_search/capability.py +246 -0
- fred_runtime-3.4.5/fred_runtime/capabilities/document_read_common.py +216 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_summarize/capability.py +19 -11
- fred_runtime-3.4.5/fred_runtime/capabilities/document_verbatim/__init__.py +27 -0
- fred_runtime-3.4.5/fred_runtime/capabilities/document_verbatim/capability.py +128 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_base_client.py +35 -18
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_document_client.py +105 -2
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_fast_text_client.py +5 -5
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_tag_client.py +3 -2
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_workspace_client.py +7 -3
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_interceptors.py +7 -3
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_runtime.py +4 -3
- fred_runtime-3.4.5/fred_runtime/common/structures.py +109 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/graph/graph_runtime.py +4 -2
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/v2_runtime/adapters.py +229 -29
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/migrations.py +53 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/contracts.py +12 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/provider.py +1 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/resolver.py +39 -19
- fred_runtime-3.4.5/fred_runtime/react/middleware/checkpoint_hygiene.py +216 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/frame.py +7 -1
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/hitl.py +12 -8
- fred_runtime-3.4.5/fred_runtime/react/middleware/shared.py +81 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/tool_observability.py +69 -4
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_prompting.py +5 -4
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_runtime.py +48 -13
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_stream_adapter.py +10 -1
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_loop.py +33 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/__init__.py +5 -1
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/model_metadata.py +25 -5
- fred_runtime-3.4.5/fred_runtime/runtime_support/user_token_refresher.py +650 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/thinking.py +1 -1
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/tool_loop.py +150 -25
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/PKG-INFO +3 -3
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/SOURCES.txt +15 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/entry_points.txt +3 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/requires.txt +2 -2
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/pyproject.toml +13 -3
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_agent_app.py +179 -4
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_document_access_1906.py +91 -3
- fred_runtime-3.4.5/tests/test_capability_document_label_search.py +377 -0
- fred_runtime-3.4.5/tests/test_capability_document_reading.py +483 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_document_summarize.py +5 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_endpoints_1974.py +13 -9
- fred_runtime-3.4.5/tests/test_chat_input_limit.py +84 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_config_loader.py +52 -2
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_context.py +64 -1
- fred_runtime-3.4.5/tests/test_document_port_error_redaction.py +231 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_runtime_token_usage_totals.py +2 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_history.py +43 -9
- fred_runtime-3.4.5/tests/test_kf_base_client_refresh_shapes.py +129 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_kf_vector_search_toolkit.py +1 -1
- fred_runtime-3.4.5/tests/test_model_metadata_cache_tokens.py +119 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_model_reasoning_enablement.py +5 -2
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_model_routing.py +49 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_models_catalog_projection.py +135 -1
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_openai_compat_router.py +82 -4
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_loop_regressions_1972.py +259 -7
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_thinking.py +1 -1
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_token_usage_totals.py +55 -0
- fred_runtime-3.4.5/tests/test_react_tool_error_final_2244.py +261 -0
- fred_runtime-3.4.5/tests/test_tool_loop_trim.py +216 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_tool_observability_middleware.py +109 -0
- fred_runtime-3.4.5/tests/test_user_token_refresher.py +1090 -0
- fred_runtime-3.4.4/fred_runtime/common/structures.py +0 -53
- fred_runtime-3.4.4/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -119
- fred_runtime-3.4.4/fred_runtime/react/middleware/shared.py +0 -43
- fred_runtime-3.4.4/fred_runtime/runtime_support/user_token_refresher.py +0 -60
- fred_runtime-3.4.4/tests/test_tool_loop_trim.py +0 -105
- fred_runtime-3.4.4/tests/test_user_token_refresher.py +0 -134
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/README.md +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/__main__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/assembly.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/assets.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/demo.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_access/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/document_summarize/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/errors.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/mcp.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/openapi_dump.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/capabilities/registry.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/context_aware_tool.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/deep/deep_runtime.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/authz.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/model_routing.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_context.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/setup.cfg +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_agent_1973.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_chat_controls_1976.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_chat_parts_1977.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_registry_1973.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_routes_tables_1979.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_selection_1974.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_capability_tool_return_convention.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_deep_agent_middleware.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_eval_trace.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_fred_workspace_fs.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_capability_bridge.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_history_display.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_hitl_resume_langgraph_integration.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_langgraph_interrupt_id_semantics.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_mcp_config.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_model_enforcement.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_pod_client.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_message_codec_resume.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_middleware_frame.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_react_prompting.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_runtime_context_prompt_injection.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_smoke.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_spike_capability_state_1971.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_sql_checkpointer_hitl_claim.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_sql_checkpointer_owner.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_token_expiry.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_tool_carrier_middleware_naming.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_tracing_kpi_content_confinement.py +0 -0
- {fred_runtime-3.4.4 → fred_runtime-3.4.5}/tests/test_url_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-runtime
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.5
|
|
4
4
|
Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
|
|
5
5
|
Author-email: Thales <noreply@thalesgroup.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,8 +12,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: <3.13,>=3.12
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: fred-core>=3.8.
|
|
16
|
-
Requires-Dist: fred-sdk>=3.
|
|
15
|
+
Requires-Dist: fred-core>=3.8.2
|
|
16
|
+
Requires-Dist: fred-sdk>=3.4.3
|
|
17
17
|
Requires-Dist: alembic>=1.18.4
|
|
18
18
|
Requires-Dist: deepagents>=0.6.12
|
|
19
19
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -65,7 +65,6 @@ from fred_core.logs.audit_log import emit_audit_log
|
|
|
65
65
|
from fred_core.logs.log_setup import log_setup
|
|
66
66
|
from fred_core.logs.log_store_factory import build_log_store
|
|
67
67
|
from fred_core.security.models import AuthorizationError
|
|
68
|
-
from fred_core.security.oidc import get_keycloak_client_id, get_keycloak_url
|
|
69
68
|
from fred_core.security.rebac.rebac_engine import (
|
|
70
69
|
ORGANIZATION_ID,
|
|
71
70
|
OrganizationPermission,
|
|
@@ -152,7 +151,9 @@ from ..integrations.v2_runtime.adapters import (
|
|
|
152
151
|
AgentConfigAssetsAdapter,
|
|
153
152
|
CompositeToolInvoker,
|
|
154
153
|
DocumentContentAdapter,
|
|
154
|
+
DocumentExtractionAdapter,
|
|
155
155
|
DocumentFolderAdapter,
|
|
156
|
+
DocumentMarkdownAdapter,
|
|
156
157
|
DocumentSearchAdapter,
|
|
157
158
|
DocumentSummarizeAdapter,
|
|
158
159
|
DocumentTreeAdapter,
|
|
@@ -160,6 +161,7 @@ from ..integrations.v2_runtime.adapters import (
|
|
|
160
161
|
FredMcpToolProvider,
|
|
161
162
|
FredWorkspaceFs,
|
|
162
163
|
KPIWriterMetricsAdapter,
|
|
164
|
+
_refresh_runtime_context_access_token,
|
|
163
165
|
build_default_tracer,
|
|
164
166
|
)
|
|
165
167
|
from ..runtime_context import (
|
|
@@ -168,7 +170,8 @@ from ..runtime_context import (
|
|
|
168
170
|
set_runtime_context,
|
|
169
171
|
)
|
|
170
172
|
from ..runtime_context import RuntimeContext as FredRuntimeContext
|
|
171
|
-
from ..runtime_support import
|
|
173
|
+
from ..runtime_support import aclose_token_refresh_client
|
|
174
|
+
from .chat_input_limit import validate_runtime_request
|
|
172
175
|
from .config import AgentPodConfig
|
|
173
176
|
from .container import build_pod_container
|
|
174
177
|
from .context import AuditEventRecord, KpiTurnRecord, PodApplicationContext
|
|
@@ -349,6 +352,7 @@ def _execute_response_adapter() -> TypeAdapter[Any]:
|
|
|
349
352
|
cache = _execute_response_adapter_cache
|
|
350
353
|
if cache is None or cache[0] is not union_token:
|
|
351
354
|
cache = (union_token, TypeAdapter(RuntimeEvent | _RuntimeErrorPayload))
|
|
355
|
+
# codeql[py/unused-global-variable]
|
|
352
356
|
_execute_response_adapter_cache = cache
|
|
353
357
|
return cache[1]
|
|
354
358
|
|
|
@@ -455,58 +459,26 @@ class _MediaClientAgentAdapter:
|
|
|
455
459
|
self.runtime_context = binding.runtime_context
|
|
456
460
|
self.agent_settings: AgentSettingsLike = settings
|
|
457
461
|
|
|
458
|
-
def refresh_user_access_token(self) -> str:
|
|
462
|
+
async def refresh_user_access_token(self) -> str:
|
|
459
463
|
"""
|
|
460
464
|
Refresh the user access token for media downloads.
|
|
461
465
|
|
|
462
466
|
Why this exists:
|
|
463
467
|
- media fetch retries need the same Keycloak refresh path as other
|
|
464
|
-
runtime adapters
|
|
468
|
+
runtime adapters. It delegates rather than reimplementing: this used
|
|
469
|
+
to be a ~40-line copy of `_refresh_runtime_context_access_token`
|
|
470
|
+
which had already drifted — the copy never wrote
|
|
471
|
+
`access_token_expires_at`, so any consumer gating on expiry saw stale
|
|
472
|
+
data after a media-path refresh.
|
|
465
473
|
|
|
466
474
|
How to use it:
|
|
467
|
-
-
|
|
475
|
+
- awaited by `KfMarkdownMediaClient` when the current token is expired
|
|
468
476
|
|
|
469
477
|
Example:
|
|
470
|
-
- `token = adapter.refresh_user_access_token()`
|
|
478
|
+
- `token = await adapter.refresh_user_access_token()`
|
|
471
479
|
"""
|
|
472
480
|
|
|
473
|
-
|
|
474
|
-
if not refresh_token:
|
|
475
|
-
raise RuntimeError(
|
|
476
|
-
"Cannot refresh user access token: refresh_token missing from runtime context."
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
keycloak_url = get_keycloak_url()
|
|
480
|
-
client_id = get_keycloak_client_id()
|
|
481
|
-
if not keycloak_url:
|
|
482
|
-
raise RuntimeError(
|
|
483
|
-
"User security realm_url is not configured for Keycloak."
|
|
484
|
-
)
|
|
485
|
-
if not client_id:
|
|
486
|
-
raise RuntimeError(
|
|
487
|
-
"User security client_id is not configured for Keycloak."
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
payload = refresh_user_access_token_from_keycloak(
|
|
491
|
-
keycloak_url=keycloak_url,
|
|
492
|
-
client_id=client_id,
|
|
493
|
-
refresh_token=refresh_token,
|
|
494
|
-
)
|
|
495
|
-
new_access_token = payload.get("access_token")
|
|
496
|
-
raw_refresh = payload.get("refresh_token")
|
|
497
|
-
new_refresh_token: str = (
|
|
498
|
-
raw_refresh
|
|
499
|
-
if isinstance(raw_refresh, str) and raw_refresh
|
|
500
|
-
else refresh_token
|
|
501
|
-
)
|
|
502
|
-
if not isinstance(new_access_token, str) or not new_access_token:
|
|
503
|
-
raise RuntimeError(
|
|
504
|
-
"Keycloak refresh response did not include a valid access_token."
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
self.runtime_context.access_token = new_access_token
|
|
508
|
-
self.runtime_context.refresh_token = new_refresh_token
|
|
509
|
-
return new_access_token
|
|
481
|
+
return await _refresh_runtime_context_access_token(self.runtime_context)
|
|
510
482
|
|
|
511
483
|
|
|
512
484
|
def _definition_to_agent_tuning(
|
|
@@ -752,6 +724,18 @@ def _build_runtime_services(
|
|
|
752
724
|
binding=binding,
|
|
753
725
|
settings=settings,
|
|
754
726
|
)
|
|
727
|
+
# Paginated full-markdown read (DOCREAD-01): powers document_verbatim,
|
|
728
|
+
# same private-binding doctrine.
|
|
729
|
+
document_markdown = DocumentMarkdownAdapter(
|
|
730
|
+
binding=binding,
|
|
731
|
+
settings=settings,
|
|
732
|
+
)
|
|
733
|
+
# Server-side exhaustive extraction (DOCREAD-01 Phase 2): powers
|
|
734
|
+
# document_extract's single-call path.
|
|
735
|
+
document_extraction = DocumentExtractionAdapter(
|
|
736
|
+
binding=binding,
|
|
737
|
+
settings=settings,
|
|
738
|
+
)
|
|
755
739
|
tool_provider = FredMcpToolProvider(
|
|
756
740
|
binding=binding,
|
|
757
741
|
settings=settings,
|
|
@@ -811,6 +795,8 @@ def _build_runtime_services(
|
|
|
811
795
|
document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
|
|
812
796
|
document_tree=document_tree,
|
|
813
797
|
document_summarize=document_summarize,
|
|
798
|
+
document_markdown=document_markdown,
|
|
799
|
+
document_extraction=document_extraction,
|
|
814
800
|
)
|
|
815
801
|
|
|
816
802
|
|
|
@@ -960,6 +946,9 @@ class _AgentTemplateSummary(BaseModel):
|
|
|
960
946
|
# for); this field is the one control-plane reads to resolve what a
|
|
961
947
|
# `selected_capability_ids = None` instance activates (#1980).
|
|
962
948
|
default_capability_ids: list[str] = Field(default_factory=list)
|
|
949
|
+
# Pod-scoped deployment policy mirrored per template so control-plane can
|
|
950
|
+
# publish it to the managed-chat composer without another runtime endpoint.
|
|
951
|
+
max_chat_input_chars: int
|
|
963
952
|
|
|
964
953
|
|
|
965
954
|
class _McpCatalogEntry(BaseModel):
|
|
@@ -1001,6 +990,19 @@ class _ModelCatalogEntry(BaseModel):
|
|
|
1001
990
|
shows no reasoning control at all — aptitude is not a choice, an
|
|
1002
991
|
administrator cannot make a model reason. Non-empty means the row carries
|
|
1003
992
|
the toggle. Same established join as `profile_ids` above."""
|
|
993
|
+
display_name: str | None = None
|
|
994
|
+
"""The ops-authored `model_display_name` of this model's first profile that
|
|
995
|
+
declares one — the label the composer shows instead of splitting the
|
|
996
|
+
capability id apart. None means no profile named this model and the
|
|
997
|
+
frontend falls back to that heuristic. First-declared wins, same
|
|
998
|
+
first-seen rule as `description` above."""
|
|
999
|
+
reasoning_effort: str | None = None
|
|
1000
|
+
"""The ops-authored `settings.reasoning_effort` of this model's first
|
|
1001
|
+
thinking profile — DERIVED from the settings (never declared twice; the
|
|
1002
|
+
settings key is the single source of truth per review 2026-08-12). The
|
|
1003
|
+
composer's reasoning menu displays it as the level a reasoning turn will
|
|
1004
|
+
actually run with ("Élevé" for Mistral small); None when no thinking
|
|
1005
|
+
profile ships the key (the menu then falls back to a generic On label)."""
|
|
1004
1006
|
|
|
1005
1007
|
|
|
1006
1008
|
class _ModelCatalogResponse(BaseModel):
|
|
@@ -1040,15 +1042,23 @@ def _project_model_catalog_entries(catalog: Any) -> list[_ModelCatalogEntry]:
|
|
|
1040
1042
|
provider=provider,
|
|
1041
1043
|
name=name,
|
|
1042
1044
|
description=profile.description,
|
|
1045
|
+
display_name=profile.model_display_name,
|
|
1043
1046
|
profile_ids=[profile.profile_id],
|
|
1044
1047
|
)
|
|
1045
1048
|
seen[key] = existing
|
|
1046
1049
|
else:
|
|
1047
1050
|
existing.profile_ids.append(profile.profile_id)
|
|
1051
|
+
# First declaring profile wins; a later one only fills a gap.
|
|
1052
|
+
if existing.display_name is None:
|
|
1053
|
+
existing.display_name = profile.model_display_name
|
|
1048
1054
|
# REASON-01 §5.3: aptitude is per profile, the toggle is per model —
|
|
1049
1055
|
# this one condition is the whole projection between them.
|
|
1050
1056
|
if profile.supports_thinking:
|
|
1051
1057
|
existing.thinking_profile_ids.append(profile.profile_id)
|
|
1058
|
+
if existing.reasoning_effort is None:
|
|
1059
|
+
existing.reasoning_effort = (profile.model.settings or {}).get(
|
|
1060
|
+
"reasoning_effort"
|
|
1061
|
+
)
|
|
1052
1062
|
return list(seen.values())
|
|
1053
1063
|
|
|
1054
1064
|
|
|
@@ -2167,11 +2177,15 @@ async def _write_turn_history(
|
|
|
2167
2177
|
)
|
|
2168
2178
|
|
|
2169
2179
|
elif kind == "awaiting_human":
|
|
2170
|
-
# Store the full HITL gate definition — question
|
|
2171
|
-
#
|
|
2180
|
+
# Store the full HITL gate definition — question, all choices, and
|
|
2181
|
+
# the resume identity (interrupt_id/checkpoint_id/pending_calls) —
|
|
2182
|
+
# so audit logs and UI replay have the complete structured record
|
|
2183
|
+
# AND a reload while the gate is still open can reconstruct a
|
|
2184
|
+
# working (not just readable) prompt.
|
|
2172
2185
|
req = payload.get("request", {})
|
|
2173
2186
|
question = req.get("question") or req.get("title") or "HITL pause"
|
|
2174
2187
|
raw_choices = req.get("choices") or []
|
|
2188
|
+
raw_pending_calls = req.get("pending_calls") or []
|
|
2175
2189
|
messages.append(
|
|
2176
2190
|
make_hitl_request(
|
|
2177
2191
|
session_id,
|
|
@@ -2188,6 +2202,18 @@ async def _write_turn_history(
|
|
|
2188
2202
|
],
|
|
2189
2203
|
stage=req.get("stage"),
|
|
2190
2204
|
title=req.get("title"),
|
|
2205
|
+
free_text=bool(req.get("free_text")),
|
|
2206
|
+
interrupt_id=req.get("interrupt_id"),
|
|
2207
|
+
checkpoint_id=req.get("checkpoint_id"),
|
|
2208
|
+
pending_calls=[
|
|
2209
|
+
{
|
|
2210
|
+
"tool_call_id": c.get("tool_call_id", ""),
|
|
2211
|
+
"tool_name": c.get("tool_name", ""),
|
|
2212
|
+
"args_preview": c.get("args_preview", ""),
|
|
2213
|
+
}
|
|
2214
|
+
for c in raw_pending_calls
|
|
2215
|
+
if isinstance(c, dict)
|
|
2216
|
+
],
|
|
2191
2217
|
)
|
|
2192
2218
|
)
|
|
2193
2219
|
rank += 1
|
|
@@ -2220,6 +2246,8 @@ async def _write_turn_history(
|
|
|
2220
2246
|
input_tokens=tu.get("input_tokens", 0),
|
|
2221
2247
|
output_tokens=tu.get("output_tokens", 0),
|
|
2222
2248
|
total_tokens=tu.get("total_tokens", 0),
|
|
2249
|
+
cache_read_tokens=tu.get("cache_read_tokens", 0),
|
|
2250
|
+
cache_creation_tokens=tu.get("cache_creation_tokens", 0),
|
|
2223
2251
|
)
|
|
2224
2252
|
final_model = payload.get("model_name")
|
|
2225
2253
|
final_finish_reason = payload.get("finish_reason")
|
|
@@ -2276,6 +2304,7 @@ class _TurnOutcome:
|
|
|
2276
2304
|
token_usage: dict[str, Any] | None
|
|
2277
2305
|
input_tokens: int | None
|
|
2278
2306
|
output_tokens: int | None
|
|
2307
|
+
cache_read_tokens: int | None
|
|
2279
2308
|
tool_count: int
|
|
2280
2309
|
is_error: bool
|
|
2281
2310
|
total_ms: int
|
|
@@ -2299,6 +2328,7 @@ def _parse_turn_outcome(
|
|
|
2299
2328
|
token_usage=token_usage,
|
|
2300
2329
|
input_tokens=token_usage.get("input_tokens") if token_usage else None,
|
|
2301
2330
|
output_tokens=token_usage.get("output_tokens") if token_usage else None,
|
|
2331
|
+
cache_read_tokens=token_usage.get("cache_read_tokens") if token_usage else None,
|
|
2302
2332
|
tool_count=tool_count,
|
|
2303
2333
|
is_error=is_error,
|
|
2304
2334
|
total_ms=total_ms,
|
|
@@ -2444,6 +2474,7 @@ def _emit_turn_completed(
|
|
|
2444
2474
|
"tool_count": outcome.tool_count,
|
|
2445
2475
|
"input_tokens": outcome.input_tokens,
|
|
2446
2476
|
"output_tokens": outcome.output_tokens,
|
|
2477
|
+
"cache_read_tokens": outcome.cache_read_tokens,
|
|
2447
2478
|
},
|
|
2448
2479
|
actor=KPIActor(type="human", user_id=user_id),
|
|
2449
2480
|
)
|
|
@@ -3409,6 +3440,7 @@ def _capability_route_base_url(base_url: str, capability_id: str) -> str:
|
|
|
3409
3440
|
def _build_agent_router(
|
|
3410
3441
|
registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
|
|
3411
3442
|
security_enabled: bool,
|
|
3443
|
+
max_chat_input_chars: int,
|
|
3412
3444
|
base_url: str = "",
|
|
3413
3445
|
) -> APIRouter:
|
|
3414
3446
|
"""
|
|
@@ -3433,6 +3465,14 @@ def _build_agent_router(
|
|
|
3433
3465
|
# Returns None in local-dev mode so dev pods start without Keycloak.
|
|
3434
3466
|
_authenticated_user = _make_user_dependency(get_current_user, security_enabled)
|
|
3435
3467
|
|
|
3468
|
+
def _enforce_chat_input_limit(request: RuntimeExecuteRequest) -> None:
|
|
3469
|
+
violation = validate_runtime_request(request, max_chat_input_chars)
|
|
3470
|
+
if violation is not None:
|
|
3471
|
+
raise HTTPException(
|
|
3472
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
3473
|
+
detail=violation.native_detail(),
|
|
3474
|
+
)
|
|
3475
|
+
|
|
3436
3476
|
@router.get("")
|
|
3437
3477
|
async def list_agents() -> list[str]:
|
|
3438
3478
|
"""Return the agent IDs registered in this pod."""
|
|
@@ -3567,6 +3607,7 @@ def _build_agent_router(
|
|
|
3567
3607
|
default_capability_ids=[
|
|
3568
3608
|
ref.id for ref in definition.default_mcp_servers
|
|
3569
3609
|
],
|
|
3610
|
+
max_chat_input_chars=max_chat_input_chars,
|
|
3570
3611
|
)
|
|
3571
3612
|
for definition in registry.values()
|
|
3572
3613
|
if include_non_public or getattr(definition, "public", True)
|
|
@@ -4262,6 +4303,7 @@ def _build_agent_router(
|
|
|
4262
4303
|
- This endpoint does not implement pod discovery or routing.
|
|
4263
4304
|
Those concerns belong to Kubernetes Service, Ingress, and Argo CD.
|
|
4264
4305
|
"""
|
|
4306
|
+
_enforce_chat_input_limit(request)
|
|
4265
4307
|
auth = http_request.headers.get("Authorization", "")
|
|
4266
4308
|
access_token = auth.removeprefix("Bearer ").strip() or None
|
|
4267
4309
|
|
|
@@ -4347,6 +4389,7 @@ def _build_agent_router(
|
|
|
4347
4389
|
Intended for evaluation harnesses (DeepEval, Promptfoo) that need
|
|
4348
4390
|
input, output, retrieval_context, tools_called, and steps in one response.
|
|
4349
4391
|
"""
|
|
4392
|
+
_enforce_chat_input_limit(request)
|
|
4350
4393
|
auth = http_request.headers.get("Authorization", "")
|
|
4351
4394
|
access_token = auth.removeprefix("Bearer ").strip() or None
|
|
4352
4395
|
|
|
@@ -4458,6 +4501,7 @@ def _build_agent_router(
|
|
|
4458
4501
|
- This endpoint does not implement pod discovery or routing.
|
|
4459
4502
|
Those concerns belong to Kubernetes Service, Ingress, and Argo CD.
|
|
4460
4503
|
"""
|
|
4504
|
+
_enforce_chat_input_limit(request)
|
|
4461
4505
|
auth = http_request.headers.get("Authorization", "")
|
|
4462
4506
|
access_token = auth.removeprefix("Bearer ").strip() or None
|
|
4463
4507
|
|
|
@@ -4682,9 +4726,32 @@ def create_agent_app(
|
|
|
4682
4726
|
"prometheus" if config.observability.kpi.prometheus.enabled else "logging",
|
|
4683
4727
|
list(registry.keys()),
|
|
4684
4728
|
)
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4729
|
+
try:
|
|
4730
|
+
yield
|
|
4731
|
+
finally:
|
|
4732
|
+
# Shutdown must RUN and must COMPLETE.
|
|
4733
|
+
# - `finally`: a plain sequence after `yield` is skipped entirely
|
|
4734
|
+
# when an exception propagates out of the app through the
|
|
4735
|
+
# generator, which is exactly the case where releasing sockets
|
|
4736
|
+
# and stopping tasks matters most.
|
|
4737
|
+
# - per-step guard: these three release INDEPENDENT resources, so a
|
|
4738
|
+
# raising `container.shutdown()` must not strand the Keycloak
|
|
4739
|
+
# refresh pool behind it. The pod is going away either way;
|
|
4740
|
+
# losing a connection pool because an unrelated subsystem failed
|
|
4741
|
+
# is strictly worse than logging both failures.
|
|
4742
|
+
# `except Exception` deliberately lets `CancelledError` through —
|
|
4743
|
+
# a shutdown being cancelled is not a step failing.
|
|
4744
|
+
for label, close in (
|
|
4745
|
+
("gc diagnostics", gc_diagnostics.stop),
|
|
4746
|
+
("pod container", container.shutdown),
|
|
4747
|
+
# Release the Keycloak refresh pool deterministically rather
|
|
4748
|
+
# than leaving it to GC, as the control-plane client above is.
|
|
4749
|
+
("keycloak refresh pool", aclose_token_refresh_client),
|
|
4750
|
+
):
|
|
4751
|
+
try:
|
|
4752
|
+
await close()
|
|
4753
|
+
except Exception:
|
|
4754
|
+
logger.exception("[fred-runtime] shutdown step failed: %s", label)
|
|
4688
4755
|
|
|
4689
4756
|
app = FastAPI(
|
|
4690
4757
|
title=config.app.name,
|
|
@@ -4717,7 +4784,10 @@ def create_agent_app(
|
|
|
4717
4784
|
api_router = APIRouter(prefix=base_url)
|
|
4718
4785
|
api_router.include_router(
|
|
4719
4786
|
_build_agent_router(
|
|
4720
|
-
registry,
|
|
4787
|
+
registry,
|
|
4788
|
+
security_enabled=security_enabled,
|
|
4789
|
+
max_chat_input_chars=config.app.max_chat_input_chars,
|
|
4790
|
+
base_url=base_url,
|
|
4721
4791
|
)
|
|
4722
4792
|
)
|
|
4723
4793
|
|
|
@@ -4747,6 +4817,7 @@ def create_agent_app(
|
|
|
4747
4817
|
openai_router = create_openai_compat_router(
|
|
4748
4818
|
registry,
|
|
4749
4819
|
security_enabled=security_enabled,
|
|
4820
|
+
max_chat_input_chars=config.app.max_chat_input_chars,
|
|
4750
4821
|
)
|
|
4751
4822
|
app.include_router(openai_router, prefix="/v1")
|
|
4752
4823
|
logger.info("[fred-runtime] OpenAI-compat endpoints enabled at /v1")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright Thales 2026
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Field-aware chat-input length validation shared by runtime HTTP surfaces."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from fred_sdk.contracts.execution import RuntimeExecuteRequest
|
|
23
|
+
|
|
24
|
+
CHAT_INPUT_TOO_LONG_CODE = "chat_input_too_long"
|
|
25
|
+
_HITL_TEXT_FIELDS = ("choice_id", "answer", "text")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class ChatInputTooLong:
|
|
30
|
+
"""Safe error data for an oversized submitted message.
|
|
31
|
+
|
|
32
|
+
The rejected text is deliberately not retained on this object, so logging
|
|
33
|
+
or serializing the validation result cannot accidentally echo user content.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
limit_chars: int
|
|
37
|
+
actual_chars: int
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def message(self) -> str:
|
|
41
|
+
return f"Your message exceeds the {self.limit_chars:,}-character limit."
|
|
42
|
+
|
|
43
|
+
def native_detail(self) -> dict[str, str | int]:
|
|
44
|
+
"""Return the Fred-native ``HTTPException.detail`` payload."""
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"code": CHAT_INPUT_TOO_LONG_CODE,
|
|
48
|
+
"message": self.message,
|
|
49
|
+
"limit_chars": self.limit_chars,
|
|
50
|
+
"actual_chars": self.actual_chars,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def openai_error(self) -> dict[str, str | int]:
|
|
54
|
+
"""Return the OpenAI-compatible ``error`` object."""
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"message": self.message,
|
|
58
|
+
"type": "invalid_request_error",
|
|
59
|
+
"param": "messages",
|
|
60
|
+
"code": CHAT_INPUT_TOO_LONG_CODE,
|
|
61
|
+
"limit_chars": self.limit_chars,
|
|
62
|
+
"actual_chars": self.actual_chars,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate_chat_text(text: str, max_chars: int) -> ChatInputTooLong | None:
|
|
67
|
+
"""Validate one string using Python's Unicode-code-point semantics."""
|
|
68
|
+
|
|
69
|
+
actual_chars = len(text)
|
|
70
|
+
if actual_chars <= max_chars:
|
|
71
|
+
return None
|
|
72
|
+
return ChatInputTooLong(limit_chars=max_chars, actual_chars=actual_chars)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def runtime_request_chat_char_count(request: RuntimeExecuteRequest) -> int:
|
|
76
|
+
"""Count the user-authored chat text that this runtime request submits.
|
|
77
|
+
|
|
78
|
+
Ordinary turns count ``input``. A HITL resume counts a bare string or the
|
|
79
|
+
combined string values of the canonical ``choice_id``, ``answer``, and
|
|
80
|
+
``text`` fields. Values are summed per field without deduplication: callers
|
|
81
|
+
may use either ``choice_id`` or ``answer`` for a selection, and managed chat
|
|
82
|
+
currently mirrors the identifier in both. Arbitrary JSON keys are
|
|
83
|
+
intentionally not traversed.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
resume_payload: Any = request.resume_payload
|
|
87
|
+
if resume_payload is None:
|
|
88
|
+
return len(request.input)
|
|
89
|
+
if isinstance(resume_payload, str):
|
|
90
|
+
return len(resume_payload)
|
|
91
|
+
if isinstance(resume_payload, Mapping):
|
|
92
|
+
return sum(
|
|
93
|
+
len(value)
|
|
94
|
+
for key in _HITL_TEXT_FIELDS
|
|
95
|
+
if isinstance((value := resume_payload.get(key)), str)
|
|
96
|
+
)
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_runtime_request(
|
|
101
|
+
request: RuntimeExecuteRequest,
|
|
102
|
+
max_chars: int,
|
|
103
|
+
) -> ChatInputTooLong | None:
|
|
104
|
+
"""Validate the effective ordinary-turn or HITL text in one request."""
|
|
105
|
+
|
|
106
|
+
actual_chars = runtime_request_chat_char_count(request)
|
|
107
|
+
if actual_chars <= max_chars:
|
|
108
|
+
return None
|
|
109
|
+
return ChatInputTooLong(limit_chars=max_chars, actual_chars=actual_chars)
|
|
@@ -120,6 +120,14 @@ class PodAppConfig(BaseModel):
|
|
|
120
120
|
"limit."
|
|
121
121
|
),
|
|
122
122
|
)
|
|
123
|
+
max_chat_input_chars: int = Field(
|
|
124
|
+
default=5_000,
|
|
125
|
+
ge=1,
|
|
126
|
+
description=(
|
|
127
|
+
"Maximum number of Unicode code points accepted in one submitted "
|
|
128
|
+
"chat message, including supported HITL resume text fields."
|
|
129
|
+
),
|
|
130
|
+
)
|
|
123
131
|
gcu_version: str | None = None
|
|
124
132
|
openai_compat: bool = False
|
|
125
133
|
"""
|
|
@@ -52,6 +52,12 @@ _CONTROL_PLANE_CLIENT_LIMITS = httpx.Limits(
|
|
|
52
52
|
# attempt hang well past what a readiness-driven operator expects.
|
|
53
53
|
_SQL_BOOT_PING_TIMEOUT_S = 5.0
|
|
54
54
|
|
|
55
|
+
# Schema ownership: fred-runtime's Alembic tree creates `session_history` and
|
|
56
|
+
# stamps `alembic_version_runtime`; no store creates tables at runtime (#2290).
|
|
57
|
+
# This is the exact command the Helm migration hook and `make db-upgrade` run.
|
|
58
|
+
_MIGRATE_COMMAND = "python -m fred_runtime migrate"
|
|
59
|
+
_RUNTIME_VERSION_TABLE = "alembic_version_runtime"
|
|
60
|
+
|
|
55
61
|
|
|
56
62
|
# ---------------------------------------------------------------------------
|
|
57
63
|
# TypedDicts for ring buffer entries
|
|
@@ -151,13 +157,15 @@ class PodApplicationContext:
|
|
|
151
157
|
Durable storage is mandatory for every fred-runtime pod — dev uses a
|
|
152
158
|
local SQLite file (`sqlite_path`), production uses Postgres, but both
|
|
153
159
|
are "durable", never "no storage". Any failure here (bad config,
|
|
154
|
-
unreachable/misconfigured Postgres) must abort pod
|
|
155
|
-
FastAPI lifespan never completes and Kubernetes never
|
|
156
|
-
to this replica — see RUNTIME-EXECUTION-CONTRACT.md §8
|
|
157
|
-
for the
|
|
160
|
+
unreachable/misconfigured Postgres, unmigrated schema) must abort pod
|
|
161
|
+
startup so the FastAPI lifespan never completes and Kubernetes never
|
|
162
|
+
routes traffic to this replica — see RUNTIME-EXECUTION-CONTRACT.md §8
|
|
163
|
+
(dated entries) for the incidents this closes.
|
|
158
164
|
"""
|
|
165
|
+
from fred_core.history.history_models import SessionHistoryRow
|
|
159
166
|
from fred_core.history.postgres_history_store import PostgresHistoryStore
|
|
160
167
|
from fred_core.sql.base_sql import create_async_engine_from_config
|
|
168
|
+
from fred_core.sql.schema_guard import require_tables
|
|
161
169
|
from fred_core.users.store.postgres_user_store import init_user_store
|
|
162
170
|
from sqlalchemy import text
|
|
163
171
|
|
|
@@ -194,6 +202,23 @@ class PodApplicationContext:
|
|
|
194
202
|
await engine.dispose()
|
|
195
203
|
raise
|
|
196
204
|
|
|
205
|
+
try:
|
|
206
|
+
# Startup-only (never on a read/write path): the history store no
|
|
207
|
+
# longer creates its own tables — Alembic owns that DDL (#2290) —
|
|
208
|
+
# so an install that skipped its migration job must be caught here,
|
|
209
|
+
# not hours later as an UndefinedTableError mid-turn (#2137).
|
|
210
|
+
await require_tables(
|
|
211
|
+
engine,
|
|
212
|
+
[SessionHistoryRow.__tablename__],
|
|
213
|
+
component="fred-runtime",
|
|
214
|
+
migrate_command=_MIGRATE_COMMAND,
|
|
215
|
+
version_table=_RUNTIME_VERSION_TABLE,
|
|
216
|
+
)
|
|
217
|
+
except Exception:
|
|
218
|
+
# Same leak-on-failure reasoning as the ping path above.
|
|
219
|
+
await engine.dispose()
|
|
220
|
+
raise
|
|
221
|
+
|
|
197
222
|
init_user_store(engine)
|
|
198
223
|
checkpointer = FredSqlCheckpointer(engine, kpi=self.get_kpi_writer())
|
|
199
224
|
history_store = PostgresHistoryStore(engine, kpi=self.get_kpi_writer())
|
|
@@ -49,8 +49,8 @@ from collections.abc import AsyncIterator, Mapping
|
|
|
49
49
|
from typing import Any, Callable
|
|
50
50
|
from uuid import uuid4
|
|
51
51
|
|
|
52
|
-
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
53
|
-
from fastapi.responses import StreamingResponse
|
|
52
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
|
53
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
54
54
|
from fred_core import AuthorizationError, KeycloakUser, TeamPermission
|
|
55
55
|
from fred_sdk.contracts.models import GraphAgentDefinition, ReActAgentDefinition
|
|
56
56
|
from fred_sdk.contracts.openai_compat import (
|
|
@@ -67,6 +67,7 @@ from .agent_app import (
|
|
|
67
67
|
_iterate_runtime_event_payloads,
|
|
68
68
|
_resolve_agent_instance,
|
|
69
69
|
)
|
|
70
|
+
from .chat_input_limit import validate_chat_text
|
|
70
71
|
from .dependencies import get_pod_container_from_app
|
|
71
72
|
|
|
72
73
|
logger = logging.getLogger(__name__)
|
|
@@ -75,6 +76,7 @@ logger = logging.getLogger(__name__)
|
|
|
75
76
|
def create_openai_compat_router(
|
|
76
77
|
registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
|
|
77
78
|
security_enabled: bool,
|
|
79
|
+
max_chat_input_chars: int,
|
|
78
80
|
authenticated_user_dep: Callable[..., Any] | None = None,
|
|
79
81
|
) -> APIRouter:
|
|
80
82
|
"""
|
|
@@ -141,7 +143,7 @@ def create_openai_compat_router(
|
|
|
141
143
|
request: OpenAIChatRequest,
|
|
142
144
|
http_request: Request,
|
|
143
145
|
authenticated_user: KeycloakUser | None = Depends(user_dep),
|
|
144
|
-
) ->
|
|
146
|
+
) -> Response:
|
|
145
147
|
"""
|
|
146
148
|
Execute one agent turn over the OpenAI chat completions SSE protocol.
|
|
147
149
|
|
|
@@ -164,6 +166,20 @@ def create_openai_compat_router(
|
|
|
164
166
|
- `fred.node_error`: graph node error message (finish_reason="stop")
|
|
165
167
|
- `fred.token_usage`: input/output token counts (on the final chunk)
|
|
166
168
|
"""
|
|
169
|
+
user_messages = [m for m in request.messages if m.role == "user"]
|
|
170
|
+
if not user_messages:
|
|
171
|
+
raise HTTPException(
|
|
172
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
173
|
+
detail="Request must contain at least one user message.",
|
|
174
|
+
)
|
|
175
|
+
message = user_messages[-1].content
|
|
176
|
+
violation = validate_chat_text(message, max_chat_input_chars)
|
|
177
|
+
if violation is not None:
|
|
178
|
+
return JSONResponse(
|
|
179
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
180
|
+
content={"error": violation.openai_error()},
|
|
181
|
+
)
|
|
182
|
+
|
|
167
183
|
if request.model not in registry:
|
|
168
184
|
raise HTTPException(
|
|
169
185
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -173,14 +189,6 @@ def create_openai_compat_router(
|
|
|
173
189
|
),
|
|
174
190
|
)
|
|
175
191
|
|
|
176
|
-
user_messages = [m for m in request.messages if m.role == "user"]
|
|
177
|
-
if not user_messages:
|
|
178
|
-
raise HTTPException(
|
|
179
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
180
|
-
detail="Request must contain at least one user message.",
|
|
181
|
-
)
|
|
182
|
-
message = user_messages[-1].content
|
|
183
|
-
|
|
184
192
|
session_id = http_request.headers.get("X-Fred-Session-Id") or uuid4().hex
|
|
185
193
|
team_id = http_request.headers.get("X-Fred-Team-Id")
|
|
186
194
|
auth = http_request.headers.get("Authorization", "")
|