ciris-agent 1.7.7__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.
- ciris_adapters/README.md +113 -0
- ciris_adapters/__init__.py +30 -0
- ciris_adapters/ciris_covenant_metrics/README.md +144 -0
- ciris_adapters/ciris_covenant_metrics/__init__.py +36 -0
- ciris_adapters/ciris_covenant_metrics/adapter.py +249 -0
- ciris_adapters/ciris_covenant_metrics/manifest.json +152 -0
- ciris_adapters/ciris_covenant_metrics/services.py +403 -0
- ciris_adapters/ciris_hosted_tools/__init__.py +24 -0
- ciris_adapters/ciris_hosted_tools/adapter.py +169 -0
- ciris_adapters/ciris_hosted_tools/manifest.json +94 -0
- ciris_adapters/ciris_hosted_tools/services.py +744 -0
- ciris_adapters/external_data_sql/README.md +559 -0
- ciris_adapters/external_data_sql/__init__.py +43 -0
- ciris_adapters/external_data_sql/adapter.py +144 -0
- ciris_adapters/external_data_sql/configurable.py +315 -0
- ciris_adapters/external_data_sql/dialects/__init__.py +37 -0
- ciris_adapters/external_data_sql/dialects/base.py +133 -0
- ciris_adapters/external_data_sql/dialects/mysql.py +63 -0
- ciris_adapters/external_data_sql/dialects/postgresql.py +59 -0
- ciris_adapters/external_data_sql/dialects/sqlite.py +62 -0
- ciris_adapters/external_data_sql/example_config.json +88 -0
- ciris_adapters/external_data_sql/example_privacy_schema.yaml +127 -0
- ciris_adapters/external_data_sql/manifest.json +195 -0
- ciris_adapters/external_data_sql/privacy_schema_loader.py +189 -0
- ciris_adapters/external_data_sql/protocol.py +101 -0
- ciris_adapters/external_data_sql/schemas.py +146 -0
- ciris_adapters/external_data_sql/service.py +1547 -0
- ciris_adapters/external_data_sql/service_old.py +492 -0
- ciris_adapters/home_assistant/__init__.py +63 -0
- ciris_adapters/home_assistant/adapter.py +201 -0
- ciris_adapters/home_assistant/communication_service.py +347 -0
- ciris_adapters/home_assistant/configurable.py +667 -0
- ciris_adapters/home_assistant/manifest.json +203 -0
- ciris_adapters/home_assistant/schemas.py +129 -0
- ciris_adapters/home_assistant/service.py +751 -0
- ciris_adapters/home_assistant/tool_service.py +441 -0
- ciris_adapters/mcp_client/__init__.py +82 -0
- ciris_adapters/mcp_client/adapter.py +847 -0
- ciris_adapters/mcp_client/config.py +280 -0
- ciris_adapters/mcp_client/configurable.py +422 -0
- ciris_adapters/mcp_client/manifest.json +185 -0
- ciris_adapters/mcp_client/mcp_communication_service.py +393 -0
- ciris_adapters/mcp_client/mcp_tool_service.py +463 -0
- ciris_adapters/mcp_client/mcp_wise_service.py +394 -0
- ciris_adapters/mcp_client/schemas.py +149 -0
- ciris_adapters/mcp_client/security.py +592 -0
- ciris_adapters/mcp_common/__init__.py +44 -0
- ciris_adapters/mcp_common/manifest.json +25 -0
- ciris_adapters/mcp_common/protocol.py +315 -0
- ciris_adapters/mcp_common/schemas.py +225 -0
- ciris_adapters/mcp_server/__init__.py +47 -0
- ciris_adapters/mcp_server/adapter.py +581 -0
- ciris_adapters/mcp_server/config.py +260 -0
- ciris_adapters/mcp_server/configurable.py +393 -0
- ciris_adapters/mcp_server/handlers.py +663 -0
- ciris_adapters/mcp_server/manifest.json +211 -0
- ciris_adapters/mcp_server/security.py +500 -0
- ciris_adapters/mock_llm/README.md +117 -0
- ciris_adapters/mock_llm/__init__.py +21 -0
- ciris_adapters/mock_llm/adapter.py +131 -0
- ciris_adapters/mock_llm/configurable.py +237 -0
- ciris_adapters/mock_llm/manifest.json +106 -0
- ciris_adapters/mock_llm/protocol.py +37 -0
- ciris_adapters/mock_llm/responses.py +520 -0
- ciris_adapters/mock_llm/responses_action_selection.py +1041 -0
- ciris_adapters/mock_llm/responses_epistemic.py +17 -0
- ciris_adapters/mock_llm/responses_feedback.py +27 -0
- ciris_adapters/mock_llm/schemas.py +35 -0
- ciris_adapters/mock_llm/service.py +294 -0
- ciris_adapters/navigation/__init__.py +21 -0
- ciris_adapters/navigation/adapter.py +129 -0
- ciris_adapters/navigation/configurable.py +239 -0
- ciris_adapters/navigation/manifest.json +104 -0
- ciris_adapters/navigation/service.py +487 -0
- ciris_adapters/reddit/README.md +132 -0
- ciris_adapters/reddit/REDDIT_ADAPTER_ANALYSIS.md +715 -0
- ciris_adapters/reddit/REDDIT_ADAPTER_SUMMARY.txt +278 -0
- ciris_adapters/reddit/REDDIT_ANALYSIS_INDEX.md +307 -0
- ciris_adapters/reddit/REDDIT_PRODUCTION_READINESS_PLAN.md +518 -0
- ciris_adapters/reddit/__init__.py +15 -0
- ciris_adapters/reddit/adapter.py +189 -0
- ciris_adapters/reddit/configurable.py +274 -0
- ciris_adapters/reddit/error_handler.py +307 -0
- ciris_adapters/reddit/manifest.json +218 -0
- ciris_adapters/reddit/observer.py +532 -0
- ciris_adapters/reddit/protocol.py +34 -0
- ciris_adapters/reddit/schemas.py +433 -0
- ciris_adapters/reddit/service.py +1471 -0
- ciris_adapters/sample_adapter/README.md +474 -0
- ciris_adapters/sample_adapter/__init__.py +45 -0
- ciris_adapters/sample_adapter/adapter.py +208 -0
- ciris_adapters/sample_adapter/configurable.py +469 -0
- ciris_adapters/sample_adapter/manifest.json +247 -0
- ciris_adapters/sample_adapter/services.py +486 -0
- ciris_adapters/weather/__init__.py +16 -0
- ciris_adapters/weather/adapter.py +130 -0
- ciris_adapters/weather/configurable.py +240 -0
- ciris_adapters/weather/manifest.json +156 -0
- ciris_adapters/weather/service.py +600 -0
- ciris_agent-1.7.7.dist-info/METADATA +284 -0
- ciris_agent-1.7.7.dist-info/RECORD +986 -0
- ciris_agent-1.7.7.dist-info/WHEEL +5 -0
- ciris_agent-1.7.7.dist-info/entry_points.txt +15 -0
- ciris_agent-1.7.7.dist-info/licenses/LICENSE +205 -0
- ciris_agent-1.7.7.dist-info/licenses/NOTICE +82 -0
- ciris_agent-1.7.7.dist-info/top_level.txt +4 -0
- ciris_engine/__init__.py +15 -0
- ciris_engine/ciris_templates/ally.yaml +632 -0
- ciris_engine/ciris_templates/default.yaml +411 -0
- ciris_engine/ciris_templates/echo-core.yaml +629 -0
- ciris_engine/ciris_templates/echo-speculative.yaml +764 -0
- ciris_engine/ciris_templates/echo.yaml +647 -0
- ciris_engine/ciris_templates/sage.yaml +332 -0
- ciris_engine/ciris_templates/scout.yaml +338 -0
- ciris_engine/ciris_templates/test.yaml +168 -0
- ciris_engine/cli.py +42 -0
- ciris_engine/config/CIRIS_SERVICES.json +19 -0
- ciris_engine/config/MODEL_CAPABILITIES.json +419 -0
- ciris_engine/config/PRICING_DATA.json +179 -0
- ciris_engine/config/__init__.py +50 -0
- ciris_engine/config/ciris_services.py +113 -0
- ciris_engine/config/model_capabilities.py +388 -0
- ciris_engine/config/pricing_models.py +276 -0
- ciris_engine/constants.py +35 -0
- ciris_engine/data/__init__.py +1 -0
- ciris_engine/data/covenant_1.0b.txt +978 -0
- ciris_engine/gui_static/11steps.svg +107 -0
- ciris_engine/gui_static/2x-schematics.png +0 -0
- ciris_engine/gui_static/404/index.html +1 -0
- ciris_engine/gui_static/404.html +1 -0
- ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_buildManifest.js +1 -0
- ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_ssgManifest.js +1 -0
- ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_buildManifest.js +1 -0
- ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_ssgManifest.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/3297-60e86ba0f8a7b040.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/3835-2aad4b7f5f8e4643.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4499-99a0bc47de0b8975.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4534-af88cd4ba6e99bff.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4541-84b455f9e0dc4cfe.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/4789-61412711484754bb.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/6539-c6398bc9d7018430.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/704-8e827b26cc8c2d32.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/704-fb45d630f3192c6f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8072-de4952a2e6d2b33f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8315-b91d03a3949db0af.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8386-f93a83ccbd789bd9.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/87c73c54-781a7f35148d5433.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/8903-fefea3339a02d41b.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/9090-e66485adf8d9d990.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/_not-found/page-a67d9808462c23b1.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-2d7ee1583bbbd02e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-6a3c2bae6fe92b7b.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-2ed3a035136bc4e8.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-b2f5c91844a32422.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/page-25b90f89af3ea58c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/page-b65d16c94ecaf69c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-675b6d05c8f9184f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-cbee2e1c8ab52145.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-0f44da06697cf9f0.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-563420253577edbf.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/adapters/page-1854631018bc32be.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/agents/page-8353752c176a7c70.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/agents/page-f61a529f110a6040.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-7f19b9d20d39be28.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-d1063938f249b8bd.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/audit/page-321b6728b8fff0bb.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/audit/page-ebac35ca961a1277.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/billing/page-6f3dc3bd02924f8e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/billing/page-fa4a469f814c821a.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/comms/page-0d4f734269addd8f.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/comms/page-79227d426050089c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/config/page-018d21d683b6e5bc.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/config/page-2aa5a5363ca2a371.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/consent/page-198373205fd316e2.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/consent/page-f2ca39e7713b13f8.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-1dd5a196f643c60d.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-530a04d3abbb8cda.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/docs/page-3193b06d094ab654.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/docs/page-330e996dedb87aba.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/layout-0a70f5fc460298b1.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/layout-21f2f99dd5b336e9.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/login/page-33240e6c6034a49d.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/login/page-68ffab6d54a7fdcd.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/logs/page-8a6167aecc4a475c.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/memory/page-9ca8c5d0056de3ff.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/memory/page-e961226941c18f81.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/page-6fdb065a787a4974.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/page-89f87d431be6064a.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/runtime/page-2e728b9c43aa164d.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/runtime/page-c7dd033dc40a72f0.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/services/page-ae9f0bdf11d01a95.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/services/page-b10feb79ca5d75e5.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/sessions/page-13ebe7ef1c16ae11.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/sessions/page-e6c82b16d617f785.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/setup/page-0beb5f5b5a5c20fc.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/setup/page-2595e729eae30c0e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-1037c987aecc3653.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-2ffd147f6d3162ff.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/system/page-2c5798d58cafcd91.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/system/page-505b1ba4eceb01c3.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-b0cad31d5cb1b2fa.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-f3ecd7a8012df230.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-login/page-f35117fdc4105801.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-login/page-fb583a7924114906.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-50f116fd76935563.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-c37d8aa5ba623a44.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/tools/page-429aec7a707777ef.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/tools/page-5f705aad60e0c04e.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/users/page-13476b8b0f3808cc.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/users/page-7e500d154ed5bba4.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/wa/page-cc4a9d8a5cb44d08.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/app/wa/page-ec3e429efbc79230.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/framework-9d29490f5ba089ba.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/main-1f554952e47a82c4.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/main-app-26fa8aed029082e5.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/main-app-97b0486ef6bcef25.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/pages/_app-6ce685456e616eb2.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/pages/_error-d4bce98d93fe21e7.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- ciris_engine/gui_static/_next/static/chunks/webpack-fcebd240b7f8477d.js +1 -0
- ciris_engine/gui_static/_next/static/css/16b94b1fe0cc6e37.css +3 -0
- ciris_engine/gui_static/_next/static/css/77a24ceaae86deff.css +3 -0
- ciris_engine/gui_static/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/747892c23ea88013-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/8d697b304b401681-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/9610d9e46709d722-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- ciris_engine/gui_static/_next/static/media/d8298875641ec7d4-s.p.woff2 +0 -0
- ciris_engine/gui_static/account/api-keys/index.html +1 -0
- ciris_engine/gui_static/account/api-keys/index.txt +27 -0
- ciris_engine/gui_static/account/consent/index.html +1 -0
- ciris_engine/gui_static/account/consent/index.txt +27 -0
- ciris_engine/gui_static/account/index.html +1 -0
- ciris_engine/gui_static/account/index.txt +27 -0
- ciris_engine/gui_static/account/privacy/index.html +1 -0
- ciris_engine/gui_static/account/privacy/index.txt +27 -0
- ciris_engine/gui_static/account/settings/index.html +1 -0
- ciris_engine/gui_static/account/settings/index.txt +27 -0
- ciris_engine/gui_static/adapters/index.html +1 -0
- ciris_engine/gui_static/adapters/index.txt +27 -0
- ciris_engine/gui_static/agents/index.html +1 -0
- ciris_engine/gui_static/agents/index.txt +27 -0
- ciris_engine/gui_static/andrew-roberts-euBRXcx57T4-unsplash.jpg +0 -0
- ciris_engine/gui_static/api-demo/index.html +1 -0
- ciris_engine/gui_static/api-demo/index.txt +27 -0
- ciris_engine/gui_static/audit/index.html +1 -0
- ciris_engine/gui_static/audit/index.txt +27 -0
- ciris_engine/gui_static/billing/index.html +1 -0
- ciris_engine/gui_static/billing/index.txt +27 -0
- ciris_engine/gui_static/blurryinfo.png +0 -0
- ciris_engine/gui_static/chip-vincent-PkQDwfl9Flc-unsplash.jpg +0 -0
- ciris_engine/gui_static/ciris-architecture.svg +338 -0
- ciris_engine/gui_static/comms/index.html +1 -0
- ciris_engine/gui_static/comms/index.txt +27 -0
- ciris_engine/gui_static/config/index.html +1 -0
- ciris_engine/gui_static/config/index.txt +27 -0
- ciris_engine/gui_static/consent/index.html +1 -0
- ciris_engine/gui_static/consent/index.txt +27 -0
- ciris_engine/gui_static/dashboard/index.html +1 -0
- ciris_engine/gui_static/dashboard/index.txt +27 -0
- ciris_engine/gui_static/docs/index.html +1 -0
- ciris_engine/gui_static/docs/index.txt +27 -0
- ciris_engine/gui_static/eric.png +0 -0
- ciris_engine/gui_static/file.svg +1 -0
- ciris_engine/gui_static/globe.svg +1 -0
- ciris_engine/gui_static/index.html +1 -0
- ciris_engine/gui_static/index.txt +27 -0
- ciris_engine/gui_static/infogfx-1@2x.png +0 -0
- ciris_engine/gui_static/infogfx-2.png +0 -0
- ciris_engine/gui_static/infogfx-dark-1.png +0 -0
- ciris_engine/gui_static/kelly-vohs-soSTXmIxTDU-unsplash.jpg +0 -0
- ciris_engine/gui_static/login/index.html +1 -0
- ciris_engine/gui_static/login/index.txt +27 -0
- ciris_engine/gui_static/logs/index.html +1 -0
- ciris_engine/gui_static/logs/index.txt +27 -0
- ciris_engine/gui_static/memory/index.html +1 -0
- ciris_engine/gui_static/memory/index.txt +27 -0
- ciris_engine/gui_static/nathan-farrish-ArcTfEoBgzs-unsplash.jpg +0 -0
- ciris_engine/gui_static/next.svg +1 -0
- ciris_engine/gui_static/overview.svg +512 -0
- ciris_engine/gui_static/overview1.svg +407 -0
- ciris_engine/gui_static/overview2.svg +370 -0
- ciris_engine/gui_static/pipeline-visualization.svg +278 -0
- ciris_engine/gui_static/privacy-policy.html +160 -0
- ciris_engine/gui_static/runtime/index.html +8 -0
- ciris_engine/gui_static/runtime/index.txt +27 -0
- ciris_engine/gui_static/services/index.html +1 -0
- ciris_engine/gui_static/services/index.txt +27 -0
- ciris_engine/gui_static/sessions/index.html +1 -0
- ciris_engine/gui_static/sessions/index.txt +27 -0
- ciris_engine/gui_static/setup/index.html +1 -0
- ciris_engine/gui_static/setup/index.txt +27 -0
- ciris_engine/gui_static/status-dashboard/index.html +1 -0
- ciris_engine/gui_static/status-dashboard/index.txt +27 -0
- ciris_engine/gui_static/system/index.html +1 -0
- ciris_engine/gui_static/system/index.txt +27 -0
- ciris_engine/gui_static/terms-of-service.html +174 -0
- ciris_engine/gui_static/test-auth/index.html +1 -0
- ciris_engine/gui_static/test-auth/index.txt +27 -0
- ciris_engine/gui_static/test-login/index.html +1 -0
- ciris_engine/gui_static/test-login/index.txt +27 -0
- ciris_engine/gui_static/test-sdk/index.html +1 -0
- ciris_engine/gui_static/test-sdk/index.txt +27 -0
- ciris_engine/gui_static/tools/index.html +1 -0
- ciris_engine/gui_static/tools/index.txt +27 -0
- ciris_engine/gui_static/users/index.html +1 -0
- ciris_engine/gui_static/users/index.txt +27 -0
- ciris_engine/gui_static/vercel.svg +1 -0
- ciris_engine/gui_static/videos/video1.mp4 +0 -0
- ciris_engine/gui_static/videos/video3.mp4 +0 -0
- ciris_engine/gui_static/wa/index.html +1 -0
- ciris_engine/gui_static/wa/index.txt +27 -0
- ciris_engine/gui_static/window.svg +1 -0
- ciris_engine/logic/__init__.py +8 -0
- ciris_engine/logic/adapters/__init__.py +74 -0
- ciris_engine/logic/adapters/api/__init__.py +5 -0
- ciris_engine/logic/adapters/api/adapter.py +1037 -0
- ciris_engine/logic/adapters/api/api_communication.py +370 -0
- ciris_engine/logic/adapters/api/api_document.py +330 -0
- ciris_engine/logic/adapters/api/api_observer.py +24 -0
- ciris_engine/logic/adapters/api/api_runtime_control.py +388 -0
- ciris_engine/logic/adapters/api/api_tools.py +299 -0
- ciris_engine/logic/adapters/api/api_vision.py +215 -0
- ciris_engine/logic/adapters/api/app.py +272 -0
- ciris_engine/logic/adapters/api/auth.py +159 -0
- ciris_engine/logic/adapters/api/config.py +101 -0
- ciris_engine/logic/adapters/api/constants.py +55 -0
- ciris_engine/logic/adapters/api/dependencies/__init__.py +1 -0
- ciris_engine/logic/adapters/api/dependencies/auth.py +260 -0
- ciris_engine/logic/adapters/api/endpoints/__init__.py +1 -0
- ciris_engine/logic/adapters/api/endpoints/emergency.py +86 -0
- ciris_engine/logic/adapters/api/middleware/__init__.py +1 -0
- ciris_engine/logic/adapters/api/middleware/rate_limiter.py +302 -0
- ciris_engine/logic/adapters/api/models.py +29 -0
- ciris_engine/logic/adapters/api/routes/__init__.py +52 -0
- ciris_engine/logic/adapters/api/routes/agent.py +1762 -0
- ciris_engine/logic/adapters/api/routes/audit.py +707 -0
- ciris_engine/logic/adapters/api/routes/auth.py +1745 -0
- ciris_engine/logic/adapters/api/routes/billing.py +895 -0
- ciris_engine/logic/adapters/api/routes/config.py +329 -0
- ciris_engine/logic/adapters/api/routes/connectors.py +534 -0
- ciris_engine/logic/adapters/api/routes/consent.py +637 -0
- ciris_engine/logic/adapters/api/routes/dsar.py +637 -0
- ciris_engine/logic/adapters/api/routes/dsar_multi_source.py +484 -0
- ciris_engine/logic/adapters/api/routes/emergency.py +302 -0
- ciris_engine/logic/adapters/api/routes/memory.py +733 -0
- ciris_engine/logic/adapters/api/routes/memory_filters.py +230 -0
- ciris_engine/logic/adapters/api/routes/memory_models.py +112 -0
- ciris_engine/logic/adapters/api/routes/memory_queries.py +236 -0
- ciris_engine/logic/adapters/api/routes/memory_query_helpers.py +394 -0
- ciris_engine/logic/adapters/api/routes/memory_visualization.py +359 -0
- ciris_engine/logic/adapters/api/routes/memory_visualization_helpers.py +110 -0
- ciris_engine/logic/adapters/api/routes/partnership.py +541 -0
- ciris_engine/logic/adapters/api/routes/setup.py +1374 -0
- ciris_engine/logic/adapters/api/routes/system.py +3049 -0
- ciris_engine/logic/adapters/api/routes/system_extensions.py +952 -0
- ciris_engine/logic/adapters/api/routes/telemetry.py +1987 -0
- ciris_engine/logic/adapters/api/routes/telemetry_converters.py +141 -0
- ciris_engine/logic/adapters/api/routes/telemetry_helpers.py +111 -0
- ciris_engine/logic/adapters/api/routes/telemetry_logs_reader.py +280 -0
- ciris_engine/logic/adapters/api/routes/telemetry_metrics.py +131 -0
- ciris_engine/logic/adapters/api/routes/telemetry_models.py +190 -0
- ciris_engine/logic/adapters/api/routes/telemetry_otlp.py +878 -0
- ciris_engine/logic/adapters/api/routes/telemetry_resource_helpers.py +191 -0
- ciris_engine/logic/adapters/api/routes/tickets.py +541 -0
- ciris_engine/logic/adapters/api/routes/tools.py +556 -0
- ciris_engine/logic/adapters/api/routes/transparency.py +281 -0
- ciris_engine/logic/adapters/api/routes/users.py +981 -0
- ciris_engine/logic/adapters/api/routes/verification.py +373 -0
- ciris_engine/logic/adapters/api/routes/wa.py +369 -0
- ciris_engine/logic/adapters/api/service_configuration.py +177 -0
- ciris_engine/logic/adapters/api/services/__init__.py +1 -0
- ciris_engine/logic/adapters/api/services/auth_service.py +1417 -0
- ciris_engine/logic/adapters/api/services/oauth_security.py +68 -0
- ciris_engine/logic/adapters/base.py +141 -0
- ciris_engine/logic/adapters/base_adapter.py +73 -0
- ciris_engine/logic/adapters/base_observer.py +1141 -0
- ciris_engine/logic/adapters/base_vision.py +312 -0
- ciris_engine/logic/adapters/cirisnode_client.py +307 -0
- ciris_engine/logic/adapters/cli/__init__.py +3 -0
- ciris_engine/logic/adapters/cli/adapter.py +207 -0
- ciris_engine/logic/adapters/cli/cli_adapter.py +902 -0
- ciris_engine/logic/adapters/cli/cli_observer.py +268 -0
- ciris_engine/logic/adapters/cli/cli_tools.py +427 -0
- ciris_engine/logic/adapters/cli/cli_wa_service.py +134 -0
- ciris_engine/logic/adapters/cli/config.py +73 -0
- ciris_engine/logic/adapters/discord/__init__.py +3 -0
- ciris_engine/logic/adapters/discord/adapter.py +783 -0
- ciris_engine/logic/adapters/discord/ciris_discord_client.py +159 -0
- ciris_engine/logic/adapters/discord/config.py +177 -0
- ciris_engine/logic/adapters/discord/constants.py +185 -0
- ciris_engine/logic/adapters/discord/discord-stubs.pyi +50 -0
- ciris_engine/logic/adapters/discord/discord_adapter.py +1584 -0
- ciris_engine/logic/adapters/discord/discord_audit.py +150 -0
- ciris_engine/logic/adapters/discord/discord_channel_manager.py +351 -0
- ciris_engine/logic/adapters/discord/discord_connection_manager.py +313 -0
- ciris_engine/logic/adapters/discord/discord_embed_formatter.py +369 -0
- ciris_engine/logic/adapters/discord/discord_error_classifier.py +302 -0
- ciris_engine/logic/adapters/discord/discord_error_handler.py +316 -0
- ciris_engine/logic/adapters/discord/discord_guidance_handler.py +460 -0
- ciris_engine/logic/adapters/discord/discord_message_handler.py +207 -0
- ciris_engine/logic/adapters/discord/discord_observer.py +670 -0
- ciris_engine/logic/adapters/discord/discord_rate_limiter.py +249 -0
- ciris_engine/logic/adapters/discord/discord_reaction_handler.py +278 -0
- ciris_engine/logic/adapters/discord/discord_tool_handler.py +465 -0
- ciris_engine/logic/adapters/discord/discord_tool_service.py +790 -0
- ciris_engine/logic/adapters/discord/discord_tools.py +90 -0
- ciris_engine/logic/adapters/discord/discord_vision_helper.py +148 -0
- ciris_engine/logic/adapters/discord/py.typed +0 -0
- ciris_engine/logic/adapters/document_parser.py +320 -0
- ciris_engine/logic/audit/__init__.py +10 -0
- ciris_engine/logic/audit/hash_chain.py +313 -0
- ciris_engine/logic/audit/signature_manager.py +352 -0
- ciris_engine/logic/audit/verifier.py +408 -0
- ciris_engine/logic/buses/__init__.py +21 -0
- ciris_engine/logic/buses/base_bus.py +178 -0
- ciris_engine/logic/buses/bus_manager.py +121 -0
- ciris_engine/logic/buses/communication_bus.py +387 -0
- ciris_engine/logic/buses/llm_bus.py +722 -0
- ciris_engine/logic/buses/memory_bus.py +577 -0
- ciris_engine/logic/buses/prohibitions.py +502 -0
- ciris_engine/logic/buses/runtime_control_bus.py +539 -0
- ciris_engine/logic/buses/tool_bus.py +482 -0
- ciris_engine/logic/buses/wise_bus.py +684 -0
- ciris_engine/logic/config/__init__.py +25 -0
- ciris_engine/logic/config/bootstrap.py +255 -0
- ciris_engine/logic/config/config_accessor.py +202 -0
- ciris_engine/logic/config/db_paths.py +194 -0
- ciris_engine/logic/config/env_utils.py +39 -0
- ciris_engine/logic/conscience/__init__.py +16 -0
- ciris_engine/logic/conscience/build_deferral_package.py +0 -0
- ciris_engine/logic/conscience/core.py +688 -0
- ciris_engine/logic/conscience/interface.py +33 -0
- ciris_engine/logic/conscience/registry.py +76 -0
- ciris_engine/logic/conscience/thought_depth_guardrail.py +231 -0
- ciris_engine/logic/conscience/updated_status_conscience.py +156 -0
- ciris_engine/logic/context/__init__.py +10 -0
- ciris_engine/logic/context/batch_context.py +550 -0
- ciris_engine/logic/context/builder.py +149 -0
- ciris_engine/logic/context/channel_resolution.py +136 -0
- ciris_engine/logic/context/secrets_snapshot.py +52 -0
- ciris_engine/logic/context/system_snapshot.py +116 -0
- ciris_engine/logic/context/system_snapshot_helpers.py +1651 -0
- ciris_engine/logic/covenant/__init__.py +33 -0
- ciris_engine/logic/covenant/executor.py +303 -0
- ciris_engine/logic/covenant/extractor.py +382 -0
- ciris_engine/logic/covenant/handler.py +241 -0
- ciris_engine/logic/covenant/verifier.py +383 -0
- ciris_engine/logic/dma/__init__.py +15 -0
- ciris_engine/logic/dma/action_selection/__init__.py +11 -0
- ciris_engine/logic/dma/action_selection/action_instruction_generator.py +444 -0
- ciris_engine/logic/dma/action_selection/context_builder.py +508 -0
- ciris_engine/logic/dma/action_selection/faculty_integration.py +193 -0
- ciris_engine/logic/dma/action_selection/special_cases.py +132 -0
- ciris_engine/logic/dma/action_selection_pdma.py +365 -0
- ciris_engine/logic/dma/base_dma.py +335 -0
- ciris_engine/logic/dma/csdma.py +239 -0
- ciris_engine/logic/dma/dma_executor.py +575 -0
- ciris_engine/logic/dma/dsdma_base.py +410 -0
- ciris_engine/logic/dma/exceptions.py +4 -0
- ciris_engine/logic/dma/factory.py +150 -0
- ciris_engine/logic/dma/pdma.py +120 -0
- ciris_engine/logic/dma/prompt_loader.py +189 -0
- ciris_engine/logic/dma/prompts/action_selection_pdma.yml +58 -0
- ciris_engine/logic/dma/prompts/csdma_common_sense.yml +28 -0
- ciris_engine/logic/dma/prompts/dsdma_base.yml +17 -0
- ciris_engine/logic/dma/prompts/pdma_ethical.yml +42 -0
- ciris_engine/logic/formatters/__init__.py +26 -0
- ciris_engine/logic/formatters/crisis_resources.py +80 -0
- ciris_engine/logic/formatters/escalation.py +21 -0
- ciris_engine/logic/formatters/identity.py +224 -0
- ciris_engine/logic/formatters/prompt_blocks.py +64 -0
- ciris_engine/logic/formatters/system_snapshot.py +193 -0
- ciris_engine/logic/formatters/user_profiles.py +108 -0
- ciris_engine/logic/handlers/__init__.py +1 -0
- ciris_engine/logic/handlers/control/__init__.py +1 -0
- ciris_engine/logic/handlers/control/defer_handler.py +195 -0
- ciris_engine/logic/handlers/control/ponder_handler.py +154 -0
- ciris_engine/logic/handlers/control/reject_handler.py +81 -0
- ciris_engine/logic/handlers/external/__init__.py +1 -0
- ciris_engine/logic/handlers/external/observe_handler.py +154 -0
- ciris_engine/logic/handlers/external/speak_handler.py +250 -0
- ciris_engine/logic/handlers/external/tool_handler.py +148 -0
- ciris_engine/logic/handlers/memory/__init__.py +1 -0
- ciris_engine/logic/handlers/memory/forget_handler.py +107 -0
- ciris_engine/logic/handlers/memory/memorize_handler.py +391 -0
- ciris_engine/logic/handlers/memory/recall_handler.py +213 -0
- ciris_engine/logic/handlers/terminal/__init__.py +1 -0
- ciris_engine/logic/handlers/terminal/task_complete_handler.py +299 -0
- ciris_engine/logic/infrastructure/__init__.py +1 -0
- ciris_engine/logic/infrastructure/handlers/__init__.py +8 -0
- ciris_engine/logic/infrastructure/handlers/action_dispatcher.py +382 -0
- ciris_engine/logic/infrastructure/handlers/base_handler.py +450 -0
- ciris_engine/logic/infrastructure/handlers/exceptions.py +2 -0
- ciris_engine/logic/infrastructure/handlers/handler_registry.py +59 -0
- ciris_engine/logic/infrastructure/handlers/helpers.py +55 -0
- ciris_engine/logic/infrastructure/step_streaming.py +149 -0
- ciris_engine/logic/infrastructure/sub_services/__init__.py +1 -0
- ciris_engine/logic/infrastructure/sub_services/identity_variance_monitor.py +1035 -0
- ciris_engine/logic/infrastructure/sub_services/pattern_analysis_loop.py +758 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_bootstrap.py +229 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_display.py +176 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_oauth.py +404 -0
- ciris_engine/logic/infrastructure/sub_services/wa_cli_wizard.py +181 -0
- ciris_engine/logic/persistence/__init__.py +130 -0
- ciris_engine/logic/persistence/analytics.py +97 -0
- ciris_engine/logic/persistence/db/__init__.py +28 -0
- ciris_engine/logic/persistence/db/core.py +520 -0
- ciris_engine/logic/persistence/db/dialect.py +380 -0
- ciris_engine/logic/persistence/db/execution_helpers.py +216 -0
- ciris_engine/logic/persistence/db/migration_runner.py +191 -0
- ciris_engine/logic/persistence/db/operations.py +313 -0
- ciris_engine/logic/persistence/db/query_builder.py +232 -0
- ciris_engine/logic/persistence/db/retry.py +154 -0
- ciris_engine/logic/persistence/db/setup.py +18 -0
- ciris_engine/logic/persistence/migrations/postgres/001_initial_schema.sql +4 -0
- ciris_engine/logic/persistence/migrations/postgres/002_add_retry_status.sql +3 -0
- ciris_engine/logic/persistence/migrations/postgres/003_add_task_update_tracking.sql +8 -0
- ciris_engine/logic/persistence/migrations/postgres/004_add_occurrence_id.sql +54 -0
- ciris_engine/logic/persistence/migrations/postgres/005_add_consolidation_locks.sql +22 -0
- ciris_engine/logic/persistence/migrations/postgres/006_add_correlation_id_unique_index.sql +16 -0
- ciris_engine/logic/persistence/migrations/postgres/007_add_dsar_tickets.sql +39 -0
- ciris_engine/logic/persistence/migrations/postgres/008_rename_to_tickets_add_sop.sql +123 -0
- ciris_engine/logic/persistence/migrations/postgres/009_add_ticket_status_columns.sql +39 -0
- ciris_engine/logic/persistence/migrations/postgres/010_add_images_to_tasks.sql +5 -0
- ciris_engine/logic/persistence/migrations/sqlite/001_initial_schema.sql +357 -0
- ciris_engine/logic/persistence/migrations/sqlite/002_add_retry_status.sql +3 -0
- ciris_engine/logic/persistence/migrations/sqlite/003_add_task_update_tracking.sql +8 -0
- ciris_engine/logic/persistence/migrations/sqlite/004_add_occurrence_id.sql +45 -0
- ciris_engine/logic/persistence/migrations/sqlite/005_add_consolidation_locks.sql +22 -0
- ciris_engine/logic/persistence/migrations/sqlite/006_add_correlation_id_unique_index.sql +16 -0
- ciris_engine/logic/persistence/migrations/sqlite/007_add_dsar_tickets.sql +39 -0
- ciris_engine/logic/persistence/migrations/sqlite/008_rename_to_tickets_add_sop.sql +120 -0
- ciris_engine/logic/persistence/migrations/sqlite/009_add_ticket_status_columns.sql +129 -0
- ciris_engine/logic/persistence/migrations/sqlite/010_add_images_to_tasks.sql +17 -0
- ciris_engine/logic/persistence/models/__init__.py +141 -0
- ciris_engine/logic/persistence/models/correlations.py +881 -0
- ciris_engine/logic/persistence/models/deferral.py +68 -0
- ciris_engine/logic/persistence/models/dsar.py +286 -0
- ciris_engine/logic/persistence/models/graph.py +362 -0
- ciris_engine/logic/persistence/models/identity.py +264 -0
- ciris_engine/logic/persistence/models/queue_status.py +139 -0
- ciris_engine/logic/persistence/models/tasks.py +1043 -0
- ciris_engine/logic/persistence/models/thoughts.py +400 -0
- ciris_engine/logic/persistence/models/tickets.py +518 -0
- ciris_engine/logic/persistence/stores/__init__.py +13 -0
- ciris_engine/logic/persistence/stores/auth_helpers.py +117 -0
- ciris_engine/logic/persistence/stores/authentication_store.py +414 -0
- ciris_engine/logic/persistence/utils.py +212 -0
- ciris_engine/logic/processors/__init__.py +30 -0
- ciris_engine/logic/processors/core/__init__.py +1 -0
- ciris_engine/logic/processors/core/base_processor.py +280 -0
- ciris_engine/logic/processors/core/main_processor.py +1777 -0
- ciris_engine/logic/processors/core/step_decorators.py +1583 -0
- ciris_engine/logic/processors/core/thought_processor/__init__.py +20 -0
- ciris_engine/logic/processors/core/thought_processor/action_execution.py +49 -0
- ciris_engine/logic/processors/core/thought_processor/conscience_execution.py +382 -0
- ciris_engine/logic/processors/core/thought_processor/finalize_action.py +66 -0
- ciris_engine/logic/processors/core/thought_processor/gather_context.py +120 -0
- ciris_engine/logic/processors/core/thought_processor/main.py +920 -0
- ciris_engine/logic/processors/core/thought_processor/perform_aspdma.py +86 -0
- ciris_engine/logic/processors/core/thought_processor/perform_dmas.py +106 -0
- ciris_engine/logic/processors/core/thought_processor/recursive_processing.py +237 -0
- ciris_engine/logic/processors/core/thought_processor/round_complete.py +52 -0
- ciris_engine/logic/processors/core/thought_processor/start_round.py +64 -0
- ciris_engine/logic/processors/exceptions.py +59 -0
- ciris_engine/logic/processors/states/__init__.py +1 -0
- ciris_engine/logic/processors/states/dream_processor.py +1381 -0
- ciris_engine/logic/processors/states/play_processor.py +141 -0
- ciris_engine/logic/processors/states/shutdown_processor.py +623 -0
- ciris_engine/logic/processors/states/solitude_processor.py +305 -0
- ciris_engine/logic/processors/states/wakeup_processor.py +802 -0
- ciris_engine/logic/processors/states/work_processor.py +742 -0
- ciris_engine/logic/processors/support/__init__.py +1 -0
- ciris_engine/logic/processors/support/dma_orchestrator.py +336 -0
- ciris_engine/logic/processors/support/processing_queue.py +133 -0
- ciris_engine/logic/processors/support/shutdown_condition_evaluator.py +294 -0
- ciris_engine/logic/processors/support/state_manager.py +358 -0
- ciris_engine/logic/processors/support/task_manager.py +303 -0
- ciris_engine/logic/processors/support/thought_escalation.py +116 -0
- ciris_engine/logic/processors/support/thought_manager.py +328 -0
- ciris_engine/logic/processors/support/thought_manager_enhanced.py +105 -0
- ciris_engine/logic/registries/__init__.py +34 -0
- ciris_engine/logic/registries/base.py +653 -0
- ciris_engine/logic/registries/circuit_breaker.py +275 -0
- ciris_engine/logic/registries/typed_registries.py +184 -0
- ciris_engine/logic/runtime/__init__.py +7 -0
- ciris_engine/logic/runtime/adapter_loader.py +261 -0
- ciris_engine/logic/runtime/adapter_manager.py +1053 -0
- ciris_engine/logic/runtime/ciris_runtime.py +2342 -0
- ciris_engine/logic/runtime/ciris_runtime_helpers.py +923 -0
- ciris_engine/logic/runtime/component_builder.py +361 -0
- ciris_engine/logic/runtime/identity_manager.py +219 -0
- ciris_engine/logic/runtime/module_loader.py +207 -0
- ciris_engine/logic/runtime/prevent_sideeffects.py +30 -0
- ciris_engine/logic/runtime/runtime_interface.py +23 -0
- ciris_engine/logic/runtime/service_initializer.py +1623 -0
- ciris_engine/logic/secrets/__init__.py +30 -0
- ciris_engine/logic/secrets/encryption.py +175 -0
- ciris_engine/logic/secrets/filter.py +295 -0
- ciris_engine/logic/secrets/service.py +652 -0
- ciris_engine/logic/secrets/store.py +669 -0
- ciris_engine/logic/services/__init__.py +1 -0
- ciris_engine/logic/services/adaptation/__init__.py +3 -0
- ciris_engine/logic/services/base_graph_service.py +142 -0
- ciris_engine/logic/services/base_infrastructure_service.py +69 -0
- ciris_engine/logic/services/base_scheduled_service.py +136 -0
- ciris_engine/logic/services/base_service.py +247 -0
- ciris_engine/logic/services/governance/__init__.py +3 -0
- ciris_engine/logic/services/governance/adaptive_filter/__init__.py +14 -0
- ciris_engine/logic/services/governance/adaptive_filter/service.py +818 -0
- ciris_engine/logic/services/governance/consent/__init__.py +53 -0
- ciris_engine/logic/services/governance/consent/air.py +403 -0
- ciris_engine/logic/services/governance/consent/decay.py +324 -0
- ciris_engine/logic/services/governance/consent/dsar_automation.py +589 -0
- ciris_engine/logic/services/governance/consent/exceptions.py +106 -0
- ciris_engine/logic/services/governance/consent/metrics.py +270 -0
- ciris_engine/logic/services/governance/consent/partnership.py +533 -0
- ciris_engine/logic/services/governance/consent/service.py +1256 -0
- ciris_engine/logic/services/governance/dsar/__init__.py +29 -0
- ciris_engine/logic/services/governance/dsar/orchestrator.py +977 -0
- ciris_engine/logic/services/governance/dsar/schemas.py +141 -0
- ciris_engine/logic/services/governance/dsar/signature_service.py +283 -0
- ciris_engine/logic/services/governance/self_observation/__init__.py +20 -0
- ciris_engine/logic/services/governance/self_observation/service.py +1153 -0
- ciris_engine/logic/services/governance/visibility/__init__.py +17 -0
- ciris_engine/logic/services/governance/visibility/service.py +512 -0
- ciris_engine/logic/services/governance/wise_authority/__init__.py +15 -0
- ciris_engine/logic/services/governance/wise_authority/service.py +827 -0
- ciris_engine/logic/services/graph/__init__.py +5 -0
- ciris_engine/logic/services/graph/audit_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/audit_service/service.py +1675 -0
- ciris_engine/logic/services/graph/base.py +208 -0
- ciris_engine/logic/services/graph/config_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/config_service/service.py +372 -0
- ciris_engine/logic/services/graph/incident_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/incident_service/service.py +803 -0
- ciris_engine/logic/services/graph/memory_service.py +1120 -0
- ciris_engine/logic/services/graph/telemetry_service/__init__.py +5 -0
- ciris_engine/logic/services/graph/telemetry_service/exceptions.py +104 -0
- ciris_engine/logic/services/graph/telemetry_service/helpers.py +1337 -0
- ciris_engine/logic/services/graph/telemetry_service/service.py +2429 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/__init__.py +17 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/aggregation_helpers.py +355 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/cleanup_helpers.py +438 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/compressor.py +260 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/__init__.py +27 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/audit.py +326 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/conversation.py +291 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/memory.py +197 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/metrics.py +251 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/task.py +257 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/trace.py +363 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/data_converter.py +545 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/date_calculation_helpers.py +193 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/db_query_helpers.py +296 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/edge_helpers.py +92 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/edge_manager.py +896 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/extensive_helpers.py +322 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/period_manager.py +152 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/profound_helpers.py +277 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/query_manager.py +812 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/service.py +1692 -0
- ciris_engine/logic/services/graph/tsdb_consolidation/sql_builders.py +363 -0
- ciris_engine/logic/services/infrastructure/__init__.py +1 -0
- ciris_engine/logic/services/infrastructure/authentication/__init__.py +5 -0
- ciris_engine/logic/services/infrastructure/authentication/service.py +1634 -0
- ciris_engine/logic/services/infrastructure/database_maintenance/__init__.py +15 -0
- ciris_engine/logic/services/infrastructure/database_maintenance/service.py +764 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/__init__.py +7 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/ciris_billing_provider.py +755 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/service.py +409 -0
- ciris_engine/logic/services/infrastructure/resource_monitor/simple_credit_provider.py +129 -0
- ciris_engine/logic/services/lifecycle/__init__.py +3 -0
- ciris_engine/logic/services/lifecycle/initialization/__init__.py +10 -0
- ciris_engine/logic/services/lifecycle/initialization/service.py +312 -0
- ciris_engine/logic/services/lifecycle/scheduler/__init__.py +5 -0
- ciris_engine/logic/services/lifecycle/scheduler/service.py +607 -0
- ciris_engine/logic/services/lifecycle/shutdown/__init__.py +9 -0
- ciris_engine/logic/services/lifecycle/shutdown/service.py +378 -0
- ciris_engine/logic/services/lifecycle/time/__init__.py +15 -0
- ciris_engine/logic/services/lifecycle/time/service.py +259 -0
- ciris_engine/logic/services/memory_service/__init__.py +8 -0
- ciris_engine/logic/services/mixins/__init__.py +13 -0
- ciris_engine/logic/services/mixins/example_usage.py +200 -0
- ciris_engine/logic/services/mixins/request_metrics.py +179 -0
- ciris_engine/logic/services/runtime/__init__.py +3 -0
- ciris_engine/logic/services/runtime/adapter_configuration/__init__.py +16 -0
- ciris_engine/logic/services/runtime/adapter_configuration/service.py +674 -0
- ciris_engine/logic/services/runtime/adapter_configuration/session.py +67 -0
- ciris_engine/logic/services/runtime/control_service/__init__.py +5 -0
- ciris_engine/logic/services/runtime/control_service/service.py +2269 -0
- ciris_engine/logic/services/runtime/llm_service/__init__.py +14 -0
- ciris_engine/logic/services/runtime/llm_service/pricing_calculator.py +279 -0
- ciris_engine/logic/services/runtime/llm_service/service.py +930 -0
- ciris_engine/logic/services/tools/__init__.py +5 -0
- ciris_engine/logic/services/tools/core_tool_service/__init__.py +8 -0
- ciris_engine/logic/services/tools/core_tool_service/service.py +852 -0
- ciris_engine/logic/setup/__init__.py +1 -0
- ciris_engine/logic/setup/first_run.py +250 -0
- ciris_engine/logic/setup/wizard.py +327 -0
- ciris_engine/logic/telemetry/__init__.py +46 -0
- ciris_engine/logic/telemetry/core.py +239 -0
- ciris_engine/logic/telemetry/hot_cold_config.py +133 -0
- ciris_engine/logic/telemetry/log_collector.py +190 -0
- ciris_engine/logic/telemetry/resource_monitor.py +7 -0
- ciris_engine/logic/telemetry/security.py +79 -0
- ciris_engine/logic/utils/__init__.py +18 -0
- ciris_engine/logic/utils/channel_utils.py +75 -0
- ciris_engine/logic/utils/consent/__init__.py +1 -0
- ciris_engine/logic/utils/consent/partnership_utils.py +172 -0
- ciris_engine/logic/utils/constants.py +92 -0
- ciris_engine/logic/utils/context_utils.py +145 -0
- ciris_engine/logic/utils/directory_setup.py +533 -0
- ciris_engine/logic/utils/graphql_context_provider.py +152 -0
- ciris_engine/logic/utils/identity_resolution.py +843 -0
- ciris_engine/logic/utils/incident_capture_handler.py +303 -0
- ciris_engine/logic/utils/initialization_manager.py +74 -0
- ciris_engine/logic/utils/jsondict_helpers.py +290 -0
- ciris_engine/logic/utils/log_sanitizer.py +97 -0
- ciris_engine/logic/utils/logging_config.py +151 -0
- ciris_engine/logic/utils/observability_decorators.py +544 -0
- ciris_engine/logic/utils/occurrence_utils.py +155 -0
- ciris_engine/logic/utils/path_resolution.py +281 -0
- ciris_engine/logic/utils/platform_detection.py +286 -0
- ciris_engine/logic/utils/privacy.py +266 -0
- ciris_engine/logic/utils/profile_loader.py +124 -0
- ciris_engine/logic/utils/profile_manager.py +16 -0
- ciris_engine/logic/utils/runtime_utils.py +69 -0
- ciris_engine/logic/utils/shutdown_manager.py +107 -0
- ciris_engine/logic/utils/task_formatters.py +60 -0
- ciris_engine/logic/utils/task_thought_factory.py +404 -0
- ciris_engine/logic/utils/thought_utils.py +54 -0
- ciris_engine/logic/utils/user_utils.py +70 -0
- ciris_engine/protocols/__init__.py +0 -0
- ciris_engine/protocols/adapters/__init__.py +35 -0
- ciris_engine/protocols/adapters/base.py +149 -0
- ciris_engine/protocols/adapters/configurable.py +265 -0
- ciris_engine/protocols/adapters/message.py +90 -0
- ciris_engine/protocols/audit/__init__.py +1 -0
- ciris_engine/protocols/buses/__init__.py +1 -0
- ciris_engine/protocols/config/__init__.py +1 -0
- ciris_engine/protocols/conscience/__init__.py +1 -0
- ciris_engine/protocols/consent.py +88 -0
- ciris_engine/protocols/context/__init__.py +1 -0
- ciris_engine/protocols/data/__init__.py +1 -0
- ciris_engine/protocols/dma/__init__.py +1 -0
- ciris_engine/protocols/dma/base.py +107 -0
- ciris_engine/protocols/faculties.py +34 -0
- ciris_engine/protocols/formatters/__init__.py +1 -0
- ciris_engine/protocols/handlers/__init__.py +1 -0
- ciris_engine/protocols/infrastructure/__init__.py +25 -0
- ciris_engine/protocols/infrastructure/base.py +377 -0
- ciris_engine/protocols/persistence/__init__.py +1 -0
- ciris_engine/protocols/pipeline_control.py +609 -0
- ciris_engine/protocols/processors/__init__.py +19 -0
- ciris_engine/protocols/processors/agent.py +299 -0
- ciris_engine/protocols/processors/base.py +130 -0
- ciris_engine/protocols/processors/orchestration.py +62 -0
- ciris_engine/protocols/registries/__init__.py +1 -0
- ciris_engine/protocols/runtime/__init__.py +1 -0
- ciris_engine/protocols/runtime/base.py +163 -0
- ciris_engine/protocols/secrets/__init__.py +1 -0
- ciris_engine/protocols/services/__init__.py +80 -0
- ciris_engine/protocols/services/adaptation/__init__.py +7 -0
- ciris_engine/protocols/services/adaptation/self_observation.py +265 -0
- ciris_engine/protocols/services/governance/__init__.py +20 -0
- ciris_engine/protocols/services/governance/communication.py +58 -0
- ciris_engine/protocols/services/governance/filter.py +56 -0
- ciris_engine/protocols/services/governance/visibility.py +32 -0
- ciris_engine/protocols/services/governance/wa_auth.py +192 -0
- ciris_engine/protocols/services/governance/wise_authority.py +75 -0
- ciris_engine/protocols/services/graph/__init__.py +19 -0
- ciris_engine/protocols/services/graph/audit.py +92 -0
- ciris_engine/protocols/services/graph/config.py +54 -0
- ciris_engine/protocols/services/graph/incident_management.py +103 -0
- ciris_engine/protocols/services/graph/memory.py +110 -0
- ciris_engine/protocols/services/graph/telemetry.py +51 -0
- ciris_engine/protocols/services/graph/tsdb_consolidation.py +87 -0
- ciris_engine/protocols/services/infrastructure/__init__.py +11 -0
- ciris_engine/protocols/services/infrastructure/authentication.py +159 -0
- ciris_engine/protocols/services/infrastructure/credit_gate.py +46 -0
- ciris_engine/protocols/services/infrastructure/database_maintenance.py +25 -0
- ciris_engine/protocols/services/infrastructure/resource_monitor.py +83 -0
- ciris_engine/protocols/services/lifecycle/__init__.py +13 -0
- ciris_engine/protocols/services/lifecycle/initialization.py +41 -0
- ciris_engine/protocols/services/lifecycle/scheduler.py +42 -0
- ciris_engine/protocols/services/lifecycle/shutdown.py +50 -0
- ciris_engine/protocols/services/lifecycle/time.py +31 -0
- ciris_engine/protocols/services/runtime/__init__.py +13 -0
- ciris_engine/protocols/services/runtime/llm.py +50 -0
- ciris_engine/protocols/services/runtime/runtime_control.py +193 -0
- ciris_engine/protocols/services/runtime/secrets.py +100 -0
- ciris_engine/protocols/services/runtime/tool.py +123 -0
- ciris_engine/protocols/telemetry/__init__.py +1 -0
- ciris_engine/protocols/utils/__init__.py +1 -0
- ciris_engine/schemas/__init__.py +112 -0
- ciris_engine/schemas/actions/__init__.py +37 -0
- ciris_engine/schemas/actions/parameters.py +137 -0
- ciris_engine/schemas/adapters/__init__.py +13 -0
- ciris_engine/schemas/adapters/cirisnode.py +135 -0
- ciris_engine/schemas/adapters/cli.py +97 -0
- ciris_engine/schemas/adapters/cli_tools.py +98 -0
- ciris_engine/schemas/adapters/discord.py +125 -0
- ciris_engine/schemas/adapters/graphql_core.py +144 -0
- ciris_engine/schemas/adapters/registration.py +47 -0
- ciris_engine/schemas/adapters/runtime_context.py +48 -0
- ciris_engine/schemas/adapters/tool_execution.py +45 -0
- ciris_engine/schemas/adapters/tools.py +96 -0
- ciris_engine/schemas/api/__init__.py +1 -0
- ciris_engine/schemas/api/agent.py +50 -0
- ciris_engine/schemas/api/audit.py +38 -0
- ciris_engine/schemas/api/auth.py +351 -0
- ciris_engine/schemas/api/config_security.py +242 -0
- ciris_engine/schemas/api/emergency.py +111 -0
- ciris_engine/schemas/api/responses.py +72 -0
- ciris_engine/schemas/api/runtime.py +26 -0
- ciris_engine/schemas/api/telemetry.py +109 -0
- ciris_engine/schemas/api/wa.py +90 -0
- ciris_engine/schemas/audit/__init__.py +13 -0
- ciris_engine/schemas/audit/core.py +139 -0
- ciris_engine/schemas/audit/hash_chain.py +58 -0
- ciris_engine/schemas/audit/verification.py +131 -0
- ciris_engine/schemas/buses/__init__.py +1 -0
- ciris_engine/schemas/config/__init__.py +41 -0
- ciris_engine/schemas/config/agent.py +279 -0
- ciris_engine/schemas/config/cognitive_state_behaviors.py +194 -0
- ciris_engine/schemas/config/default_dsar_sops.py +178 -0
- ciris_engine/schemas/config/essential.py +195 -0
- ciris_engine/schemas/config/tickets.py +86 -0
- ciris_engine/schemas/conscience/__init__.py +25 -0
- ciris_engine/schemas/conscience/context.py +34 -0
- ciris_engine/schemas/conscience/core.py +145 -0
- ciris_engine/schemas/conscience/results.py +24 -0
- ciris_engine/schemas/consent/__init__.py +5 -0
- ciris_engine/schemas/consent/core.py +404 -0
- ciris_engine/schemas/context/__init__.py +1 -0
- ciris_engine/schemas/covenant.py +382 -0
- ciris_engine/schemas/data/__init__.py +1 -0
- ciris_engine/schemas/dma/__init__.py +16 -0
- ciris_engine/schemas/dma/core.py +199 -0
- ciris_engine/schemas/dma/faculty.py +192 -0
- ciris_engine/schemas/dma/prompts.py +172 -0
- ciris_engine/schemas/dma/results.py +103 -0
- ciris_engine/schemas/formatters/__init__.py +1 -0
- ciris_engine/schemas/handlers/__init__.py +10 -0
- ciris_engine/schemas/handlers/context.py +119 -0
- ciris_engine/schemas/handlers/contexts.py +100 -0
- ciris_engine/schemas/handlers/core.py +167 -0
- ciris_engine/schemas/handlers/memory_schemas.py +67 -0
- ciris_engine/schemas/handlers/schemas.py +95 -0
- ciris_engine/schemas/identity.py +149 -0
- ciris_engine/schemas/infrastructure/__init__.py +1 -0
- ciris_engine/schemas/infrastructure/base.py +256 -0
- ciris_engine/schemas/infrastructure/behavioral_patterns.py +129 -0
- ciris_engine/schemas/infrastructure/feedback_loop.py +57 -0
- ciris_engine/schemas/infrastructure/identity_variance.py +141 -0
- ciris_engine/schemas/infrastructure/oauth.py +175 -0
- ciris_engine/schemas/infrastructure/wa_cli_wizard.py +54 -0
- ciris_engine/schemas/persistence/__init__.py +34 -0
- ciris_engine/schemas/persistence/core.py +140 -0
- ciris_engine/schemas/persistence/correlations.py +73 -0
- ciris_engine/schemas/persistence/postgres/__init__.py +1 -0
- ciris_engine/schemas/persistence/postgres/tables.py +280 -0
- ciris_engine/schemas/persistence/sqlite/__init__.py +1 -0
- ciris_engine/schemas/persistence/sqlite/tables.py +281 -0
- ciris_engine/schemas/platform.py +149 -0
- ciris_engine/schemas/processors/__init__.py +26 -0
- ciris_engine/schemas/processors/base.py +130 -0
- ciris_engine/schemas/processors/cognitive.py +77 -0
- ciris_engine/schemas/processors/context.py +35 -0
- ciris_engine/schemas/processors/core.py +152 -0
- ciris_engine/schemas/processors/dma.py +105 -0
- ciris_engine/schemas/processors/error.py +122 -0
- ciris_engine/schemas/processors/main.py +109 -0
- ciris_engine/schemas/processors/phase_results.py +21 -0
- ciris_engine/schemas/processors/results.py +99 -0
- ciris_engine/schemas/processors/solitude.py +79 -0
- ciris_engine/schemas/processors/state.py +202 -0
- ciris_engine/schemas/processors/state_example.py +177 -0
- ciris_engine/schemas/processors/states.py +21 -0
- ciris_engine/schemas/processors/status.py +34 -0
- ciris_engine/schemas/registries/__init__.py +1 -0
- ciris_engine/schemas/registries/base.py +66 -0
- ciris_engine/schemas/resources/__init__.py +15 -0
- ciris_engine/schemas/resources/crisis.py +315 -0
- ciris_engine/schemas/runtime/__init__.py +42 -0
- ciris_engine/schemas/runtime/adapter_management.py +186 -0
- ciris_engine/schemas/runtime/api.py +58 -0
- ciris_engine/schemas/runtime/audit.py +50 -0
- ciris_engine/schemas/runtime/bootstrap.py +33 -0
- ciris_engine/schemas/runtime/contexts.py +61 -0
- ciris_engine/schemas/runtime/core.py +161 -0
- ciris_engine/schemas/runtime/enums.py +167 -0
- ciris_engine/schemas/runtime/extended.py +232 -0
- ciris_engine/schemas/runtime/manifest.py +311 -0
- ciris_engine/schemas/runtime/memory.py +60 -0
- ciris_engine/schemas/runtime/messages.py +108 -0
- ciris_engine/schemas/runtime/models.py +156 -0
- ciris_engine/schemas/runtime/processing_context.py +43 -0
- ciris_engine/schemas/runtime/protocols_core.py +96 -0
- ciris_engine/schemas/runtime/resources.py +33 -0
- ciris_engine/schemas/runtime/system_context.py +417 -0
- ciris_engine/schemas/secrets/__init__.py +1 -0
- ciris_engine/schemas/secrets/core.py +267 -0
- ciris_engine/schemas/secrets/service.py +95 -0
- ciris_engine/schemas/services/__init__.py +33 -0
- ciris_engine/schemas/services/audit_summary_node.py +172 -0
- ciris_engine/schemas/services/authority/__init__.py +39 -0
- ciris_engine/schemas/services/authority/jwt.py +158 -0
- ciris_engine/schemas/services/authority/wa_updates.py +138 -0
- ciris_engine/schemas/services/authority/wise_authority.py +163 -0
- ciris_engine/schemas/services/authority_core.py +370 -0
- ciris_engine/schemas/services/capabilities.py +72 -0
- ciris_engine/schemas/services/community_core.py +95 -0
- ciris_engine/schemas/services/context.py +111 -0
- ciris_engine/schemas/services/conversation_summary_node.py +189 -0
- ciris_engine/schemas/services/core/__init__.py +153 -0
- ciris_engine/schemas/services/core/runtime.py +262 -0
- ciris_engine/schemas/services/core/runtime_config.py +117 -0
- ciris_engine/schemas/services/core/secrets.py +65 -0
- ciris_engine/schemas/services/correlation_node.py +179 -0
- ciris_engine/schemas/services/credit_gate.py +92 -0
- ciris_engine/schemas/services/discord_nodes.py +299 -0
- ciris_engine/schemas/services/feedback_core.py +131 -0
- ciris_engine/schemas/services/filters_core.py +270 -0
- ciris_engine/schemas/services/governance.py +26 -0
- ciris_engine/schemas/services/graph/__init__.py +26 -0
- ciris_engine/schemas/services/graph/attributes.py +254 -0
- ciris_engine/schemas/services/graph/audit.py +98 -0
- ciris_engine/schemas/services/graph/consolidation.py +338 -0
- ciris_engine/schemas/services/graph/edge_types.py +43 -0
- ciris_engine/schemas/services/graph/edges.py +88 -0
- ciris_engine/schemas/services/graph/incident.py +312 -0
- ciris_engine/schemas/services/graph/memory.py +84 -0
- ciris_engine/schemas/services/graph/node_data.py +174 -0
- ciris_engine/schemas/services/graph/query_results.py +82 -0
- ciris_engine/schemas/services/graph/telemetry.py +250 -0
- ciris_engine/schemas/services/graph/tsdb_consolidation.py +27 -0
- ciris_engine/schemas/services/graph/tsdb_models.py +107 -0
- ciris_engine/schemas/services/graph_core.py +196 -0
- ciris_engine/schemas/services/graph_typed_nodes.py +194 -0
- ciris_engine/schemas/services/infrastructure/__init__.py +1 -0
- ciris_engine/schemas/services/infrastructure/resource_monitor.py +20 -0
- ciris_engine/schemas/services/lifecycle/__init__.py +9 -0
- ciris_engine/schemas/services/lifecycle/initialization.py +33 -0
- ciris_engine/schemas/services/lifecycle/time.py +50 -0
- ciris_engine/schemas/services/llm.py +187 -0
- ciris_engine/schemas/services/metadata.py +43 -0
- ciris_engine/schemas/services/nodes.py +704 -0
- ciris_engine/schemas/services/operations.py +126 -0
- ciris_engine/schemas/services/requests.py +128 -0
- ciris_engine/schemas/services/resources_core.py +182 -0
- ciris_engine/schemas/services/runtime_control.py +1010 -0
- ciris_engine/schemas/services/shutdown.py +88 -0
- ciris_engine/schemas/services/special/__init__.py +0 -0
- ciris_engine/schemas/services/special/self_observation.py +396 -0
- ciris_engine/schemas/services/trace_summary_node.py +199 -0
- ciris_engine/schemas/services/visibility.py +98 -0
- ciris_engine/schemas/streaming/__init__.py +10 -0
- ciris_engine/schemas/streaming/reasoning_stream.py +95 -0
- ciris_engine/schemas/telemetry/__init__.py +0 -0
- ciris_engine/schemas/telemetry/collector.py +67 -0
- ciris_engine/schemas/telemetry/core.py +252 -0
- ciris_engine/schemas/telemetry/unified.py +59 -0
- ciris_engine/schemas/tools.py +72 -0
- ciris_engine/schemas/types.py +47 -0
- ciris_engine/schemas/utils/__init__.py +1 -0
- ciris_engine/schemas/utils/config_validator.py +54 -0
- ciris_engine/utils/__init__.py +1 -0
- ciris_engine/utils/serialization.py +35 -0
- ciris_sdk/__init__.py +124 -0
- ciris_sdk/auth_store.py +261 -0
- ciris_sdk/client.py +261 -0
- ciris_sdk/exceptions.py +73 -0
- ciris_sdk/model_types.py +258 -0
- ciris_sdk/models.py +354 -0
- ciris_sdk/pagination.py +214 -0
- ciris_sdk/rate_limiter.py +188 -0
- ciris_sdk/setup.py +17 -0
- ciris_sdk/telemetry_models.py +257 -0
- ciris_sdk/telemetry_responses.py +199 -0
- ciris_sdk/transport.py +177 -0
- ciris_sdk/websocket.py +400 -0
- main.py +766 -0
|
@@ -0,0 +1,3049 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System management endpoints for CIRIS API v3.0 (Simplified).
|
|
3
|
+
|
|
4
|
+
Consolidates health, time, resources, runtime control, services, and shutdown
|
|
5
|
+
into a unified system operations interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import html
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response
|
|
18
|
+
from pydantic import BaseModel, Field, ValidationError, field_serializer
|
|
19
|
+
from starlette.responses import JSONResponse
|
|
20
|
+
|
|
21
|
+
from ciris_engine.constants import CIRIS_VERSION
|
|
22
|
+
from ciris_engine.logic.utils.path_resolution import get_package_root
|
|
23
|
+
from ciris_engine.protocols.services.lifecycle.time import TimeServiceProtocol
|
|
24
|
+
from ciris_engine.schemas.adapters.tools import ToolParameterSchema
|
|
25
|
+
from ciris_engine.schemas.api.responses import SuccessResponse
|
|
26
|
+
from ciris_engine.schemas.api.telemetry import ServiceMetrics, TimeSyncStatus
|
|
27
|
+
from ciris_engine.schemas.runtime.adapter_management import (
|
|
28
|
+
AdapterConfig,
|
|
29
|
+
AdapterListResponse,
|
|
30
|
+
AdapterMetrics,
|
|
31
|
+
AdapterOperationResult,
|
|
32
|
+
ModuleConfigParameter,
|
|
33
|
+
ModuleTypeInfo,
|
|
34
|
+
ModuleTypesResponse,
|
|
35
|
+
)
|
|
36
|
+
from ciris_engine.schemas.runtime.adapter_management import RuntimeAdapterStatus as AdapterStatusSchema
|
|
37
|
+
from ciris_engine.schemas.runtime.enums import ServiceType
|
|
38
|
+
from ciris_engine.schemas.runtime.manifest import ConfigurationStep
|
|
39
|
+
from ciris_engine.schemas.services.core.runtime import ProcessorStatus
|
|
40
|
+
from ciris_engine.schemas.services.resources_core import ResourceBudget, ResourceSnapshot
|
|
41
|
+
from ciris_engine.schemas.types import JSONDict
|
|
42
|
+
from ciris_engine.utils.serialization import serialize_timestamp
|
|
43
|
+
|
|
44
|
+
from ..constants import (
|
|
45
|
+
DESC_CURRENT_COGNITIVE_STATE,
|
|
46
|
+
DESC_HUMAN_READABLE_STATUS,
|
|
47
|
+
ERROR_RESOURCE_MONITOR_NOT_AVAILABLE,
|
|
48
|
+
ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE,
|
|
49
|
+
ERROR_SHUTDOWN_SERVICE_NOT_AVAILABLE,
|
|
50
|
+
ERROR_TIME_SERVICE_NOT_AVAILABLE,
|
|
51
|
+
)
|
|
52
|
+
from ..dependencies.auth import AuthContext, require_admin, require_observer
|
|
53
|
+
|
|
54
|
+
router = APIRouter(prefix="/system", tags=["system"])
|
|
55
|
+
|
|
56
|
+
# Capability constants (avoid duplication)
|
|
57
|
+
CAP_COMM_SEND_MESSAGE = "communication:send_message"
|
|
58
|
+
CAP_COMM_FETCH_MESSAGES = "communication:fetch_messages"
|
|
59
|
+
MANIFEST_FILENAME = "manifest.json"
|
|
60
|
+
ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE = "Adapter configuration service not available"
|
|
61
|
+
|
|
62
|
+
# Common communication capabilities for adapters
|
|
63
|
+
COMM_CAPABILITIES = [CAP_COMM_SEND_MESSAGE, CAP_COMM_FETCH_MESSAGES]
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Request/Response Models
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SystemHealthResponse(BaseModel):
|
|
71
|
+
"""Overall system health status."""
|
|
72
|
+
|
|
73
|
+
status: str = Field(..., description="Overall health status (healthy/degraded/critical)")
|
|
74
|
+
version: str = Field(..., description="System version")
|
|
75
|
+
uptime_seconds: float = Field(..., description="System uptime in seconds")
|
|
76
|
+
services: Dict[str, Dict[str, int]] = Field(..., description="Service health summary")
|
|
77
|
+
initialization_complete: bool = Field(..., description="Whether system initialization is complete")
|
|
78
|
+
cognitive_state: Optional[str] = Field(None, description="Current cognitive state if available")
|
|
79
|
+
timestamp: datetime = Field(..., description="Current server time")
|
|
80
|
+
|
|
81
|
+
@field_serializer("timestamp")
|
|
82
|
+
def serialize_ts(self, timestamp: datetime, _info: Any) -> Optional[str]:
|
|
83
|
+
return serialize_timestamp(timestamp, _info)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SystemTimeResponse(BaseModel):
|
|
87
|
+
"""System and agent time information."""
|
|
88
|
+
|
|
89
|
+
system_time: datetime = Field(..., description="Host system time (OS time)")
|
|
90
|
+
agent_time: datetime = Field(..., description="Agent's TimeService time")
|
|
91
|
+
uptime_seconds: float = Field(..., description="Service uptime in seconds")
|
|
92
|
+
time_sync: TimeSyncStatus = Field(..., description="Time synchronization status")
|
|
93
|
+
|
|
94
|
+
@field_serializer("system_time", "agent_time")
|
|
95
|
+
def serialize_times(self, dt: datetime, _info: Any) -> Optional[str]:
|
|
96
|
+
return serialize_timestamp(dt, _info)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ResourceUsageResponse(BaseModel):
|
|
100
|
+
"""System resource usage and limits."""
|
|
101
|
+
|
|
102
|
+
current_usage: ResourceSnapshot = Field(..., description="Current resource usage")
|
|
103
|
+
limits: ResourceBudget = Field(..., description="Configured resource limits")
|
|
104
|
+
health_status: str = Field(..., description="Resource health (healthy/warning/critical)")
|
|
105
|
+
warnings: List[str] = Field(default_factory=list, description="Resource warnings")
|
|
106
|
+
critical: List[str] = Field(default_factory=list, description="Critical resource issues")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RuntimeAction(BaseModel):
|
|
110
|
+
"""Runtime control action request."""
|
|
111
|
+
|
|
112
|
+
reason: Optional[str] = Field(None, description="Reason for the action")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class StateTransitionRequest(BaseModel):
|
|
116
|
+
"""Request to transition cognitive state."""
|
|
117
|
+
|
|
118
|
+
target_state: str = Field(..., description="Target cognitive state (WORK, DREAM, PLAY, SOLITUDE)")
|
|
119
|
+
reason: Optional[str] = Field(None, description="Reason for the transition")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class StateTransitionResponse(BaseModel):
|
|
123
|
+
"""Response to cognitive state transition request."""
|
|
124
|
+
|
|
125
|
+
success: bool = Field(..., description="Whether transition was initiated")
|
|
126
|
+
message: str = Field(..., description="Human-readable status message")
|
|
127
|
+
previous_state: Optional[str] = Field(None, description="State before transition")
|
|
128
|
+
current_state: str = Field(..., description="Current cognitive state after transition attempt")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class RuntimeControlResponse(BaseModel):
|
|
132
|
+
"""Response to runtime control actions."""
|
|
133
|
+
|
|
134
|
+
success: bool = Field(..., description="Whether action succeeded")
|
|
135
|
+
message: str = Field(..., description=DESC_HUMAN_READABLE_STATUS)
|
|
136
|
+
processor_state: str = Field(..., description="Current processor state")
|
|
137
|
+
cognitive_state: Optional[str] = Field(None, description=DESC_CURRENT_COGNITIVE_STATE)
|
|
138
|
+
queue_depth: int = Field(0, description="Number of items in processing queue")
|
|
139
|
+
|
|
140
|
+
# Enhanced pause response fields for UI display
|
|
141
|
+
current_step: Optional[str] = Field(None, description="Current pipeline step when paused")
|
|
142
|
+
current_step_schema: Optional[JSONDict] = Field(None, description="Full schema object for current step")
|
|
143
|
+
pipeline_state: Optional[JSONDict] = Field(None, description="Complete pipeline state when paused")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ServiceStatus(BaseModel):
|
|
147
|
+
"""Individual service status."""
|
|
148
|
+
|
|
149
|
+
name: str = Field(..., description="Service name")
|
|
150
|
+
type: str = Field(..., description="Service type")
|
|
151
|
+
healthy: bool = Field(..., description="Whether service is healthy")
|
|
152
|
+
available: bool = Field(..., description="Whether service is available")
|
|
153
|
+
uptime_seconds: Optional[float] = Field(None, description="Service uptime if tracked")
|
|
154
|
+
metrics: ServiceMetrics = Field(
|
|
155
|
+
default_factory=lambda: ServiceMetrics(
|
|
156
|
+
uptime_seconds=None,
|
|
157
|
+
requests_handled=None,
|
|
158
|
+
error_count=None,
|
|
159
|
+
avg_response_time_ms=None,
|
|
160
|
+
memory_mb=None,
|
|
161
|
+
custom_metrics=None,
|
|
162
|
+
),
|
|
163
|
+
description="Service-specific metrics",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ServicesStatusResponse(BaseModel):
|
|
168
|
+
"""Status of all system services."""
|
|
169
|
+
|
|
170
|
+
services: List[ServiceStatus] = Field(..., description="List of service statuses")
|
|
171
|
+
total_services: int = Field(..., description="Total number of services")
|
|
172
|
+
healthy_services: int = Field(..., description="Number of healthy services")
|
|
173
|
+
timestamp: datetime = Field(..., description="When status was collected")
|
|
174
|
+
|
|
175
|
+
@field_serializer("timestamp")
|
|
176
|
+
def serialize_ts(self, timestamp: datetime, _info: Any) -> Optional[str]:
|
|
177
|
+
return serialize_timestamp(timestamp, _info)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ShutdownRequest(BaseModel):
|
|
181
|
+
"""Graceful shutdown request."""
|
|
182
|
+
|
|
183
|
+
reason: str = Field(..., description="Reason for shutdown")
|
|
184
|
+
force: bool = Field(False, description="Force immediate shutdown")
|
|
185
|
+
confirm: bool = Field(..., description="Confirmation flag (must be true)")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ShutdownResponse(BaseModel):
|
|
189
|
+
"""Response to shutdown request."""
|
|
190
|
+
|
|
191
|
+
status: str = Field(..., description="Shutdown status")
|
|
192
|
+
message: str = Field(..., description=DESC_HUMAN_READABLE_STATUS)
|
|
193
|
+
shutdown_initiated: bool = Field(..., description="Whether shutdown was initiated")
|
|
194
|
+
timestamp: datetime = Field(..., description="When shutdown was initiated")
|
|
195
|
+
|
|
196
|
+
@field_serializer("timestamp")
|
|
197
|
+
def serialize_ts(self, timestamp: datetime, _info: Any) -> Optional[str]:
|
|
198
|
+
return serialize_timestamp(timestamp, _info)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class AdapterActionRequest(BaseModel):
|
|
202
|
+
"""Request for adapter operations."""
|
|
203
|
+
|
|
204
|
+
config: Optional[AdapterConfig] = Field(None, description="Adapter configuration")
|
|
205
|
+
auto_start: bool = Field(True, description="Whether to auto-start the adapter")
|
|
206
|
+
force: bool = Field(False, description="Force the operation")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ToolInfoResponse(BaseModel):
|
|
210
|
+
"""Tool information response with provider details."""
|
|
211
|
+
|
|
212
|
+
name: str = Field(..., description="Tool name")
|
|
213
|
+
description: str = Field(..., description="Tool description")
|
|
214
|
+
provider: str = Field(..., description="Provider service name")
|
|
215
|
+
parameters: Optional[ToolParameterSchema] = Field(None, description="Tool parameter schema")
|
|
216
|
+
category: str = Field("general", description="Tool category")
|
|
217
|
+
cost: float = Field(0.0, description="Cost to execute the tool")
|
|
218
|
+
when_to_use: Optional[str] = Field(None, description="Guidance on when to use the tool")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Adapter Configuration Response Models
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ConfigStepInfo(BaseModel):
|
|
225
|
+
"""Information about a configuration step."""
|
|
226
|
+
|
|
227
|
+
step_id: str = Field(..., description="Unique step identifier")
|
|
228
|
+
step_type: str = Field(..., description="Type of step (discovery, oauth, select, confirm)")
|
|
229
|
+
title: str = Field(..., description="Step title")
|
|
230
|
+
description: str = Field(..., description="Step description")
|
|
231
|
+
optional: bool = Field(False, description="Whether this step is optional")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class ConfigurableAdapterInfo(BaseModel):
|
|
235
|
+
"""Information about an adapter that supports interactive configuration."""
|
|
236
|
+
|
|
237
|
+
adapter_type: str = Field(..., description="Type identifier for the adapter")
|
|
238
|
+
name: str = Field(..., description="Human-readable name")
|
|
239
|
+
description: str = Field(..., description="Description of the adapter")
|
|
240
|
+
workflow_type: str = Field(..., description="Type of configuration workflow")
|
|
241
|
+
step_count: int = Field(..., description="Number of steps in the configuration workflow")
|
|
242
|
+
requires_oauth: bool = Field(False, description="Whether this adapter requires OAuth authentication")
|
|
243
|
+
steps: List[ConfigStepInfo] = Field(default_factory=list, description="Configuration steps")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ConfigurableAdaptersResponse(BaseModel):
|
|
247
|
+
"""Response containing list of configurable adapters."""
|
|
248
|
+
|
|
249
|
+
adapters: List[ConfigurableAdapterInfo] = Field(..., description="List of configurable adapters")
|
|
250
|
+
total_count: int = Field(..., description="Total number of configurable adapters")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ConfigurationSessionResponse(BaseModel):
|
|
254
|
+
"""Response for starting a configuration session."""
|
|
255
|
+
|
|
256
|
+
session_id: str = Field(..., description="Unique session identifier")
|
|
257
|
+
adapter_type: str = Field(..., description="Adapter being configured")
|
|
258
|
+
status: str = Field(..., description="Current session status")
|
|
259
|
+
current_step_index: int = Field(..., description="Index of current step")
|
|
260
|
+
current_step: Optional[ConfigurationStep] = Field(None, description="Current step information")
|
|
261
|
+
total_steps: int = Field(..., description="Total number of steps in workflow")
|
|
262
|
+
created_at: datetime = Field(..., description="When session was created")
|
|
263
|
+
|
|
264
|
+
@field_serializer("created_at")
|
|
265
|
+
def serialize_ts(self, created_at: datetime, _info: Any) -> Optional[str]:
|
|
266
|
+
return serialize_timestamp(created_at, _info)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ConfigurationStatusResponse(BaseModel):
|
|
270
|
+
"""Response for configuration session status."""
|
|
271
|
+
|
|
272
|
+
session_id: str = Field(..., description="Session identifier")
|
|
273
|
+
adapter_type: str = Field(..., description="Adapter being configured")
|
|
274
|
+
status: str = Field(..., description="Current session status")
|
|
275
|
+
current_step_index: int = Field(..., description="Index of current step")
|
|
276
|
+
current_step: Optional[ConfigurationStep] = Field(None, description="Current step information")
|
|
277
|
+
total_steps: int = Field(..., description="Total number of steps in workflow")
|
|
278
|
+
collected_config: Dict[str, Any] = Field(..., description="Configuration collected so far")
|
|
279
|
+
created_at: datetime = Field(..., description="When session was created")
|
|
280
|
+
updated_at: datetime = Field(..., description="When session was last updated")
|
|
281
|
+
|
|
282
|
+
@field_serializer("created_at", "updated_at")
|
|
283
|
+
def serialize_times(self, dt: datetime, _info: Any) -> Optional[str]:
|
|
284
|
+
return serialize_timestamp(dt, _info)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class StepExecutionRequest(BaseModel):
|
|
288
|
+
"""Request to execute a configuration step."""
|
|
289
|
+
|
|
290
|
+
step_data: Dict[str, Any] = Field(default_factory=dict, description="Data for step execution")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class StepExecutionResponse(BaseModel):
|
|
294
|
+
"""Response from executing a configuration step."""
|
|
295
|
+
|
|
296
|
+
step_id: str = Field(..., description="ID of the executed step")
|
|
297
|
+
success: bool = Field(..., description="Whether step execution succeeded")
|
|
298
|
+
data: Dict[str, Any] = Field(default_factory=dict, description="Data returned by the step")
|
|
299
|
+
next_step_index: Optional[int] = Field(None, description="Index of next step to execute")
|
|
300
|
+
error: Optional[str] = Field(None, description="Error message if execution failed")
|
|
301
|
+
awaiting_callback: bool = Field(False, description="Whether step is waiting for external callback")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ConfigurationCompleteRequest(BaseModel):
|
|
305
|
+
"""Request body for completing a configuration session."""
|
|
306
|
+
|
|
307
|
+
persist: bool = Field(default=False, description="If True, persist configuration for automatic loading on startup")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ConfigurationCompleteResponse(BaseModel):
|
|
311
|
+
"""Response from completing a configuration session."""
|
|
312
|
+
|
|
313
|
+
success: bool = Field(..., description="Whether configuration was applied successfully")
|
|
314
|
+
adapter_type: str = Field(..., description="Adapter that was configured")
|
|
315
|
+
message: str = Field(..., description="Human-readable result message")
|
|
316
|
+
applied_config: Dict[str, Any] = Field(default_factory=dict, description="Configuration that was applied")
|
|
317
|
+
persisted: bool = Field(default=False, description="Whether configuration was persisted for startup")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Endpoints
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@router.get("/health", response_model=SuccessResponse[SystemHealthResponse])
|
|
324
|
+
async def get_system_health(request: Request) -> SuccessResponse[SystemHealthResponse]:
|
|
325
|
+
"""
|
|
326
|
+
Overall system health.
|
|
327
|
+
|
|
328
|
+
Returns comprehensive system health including service status,
|
|
329
|
+
initialization state, and current cognitive state.
|
|
330
|
+
"""
|
|
331
|
+
# Get basic system info
|
|
332
|
+
uptime_seconds = _get_system_uptime(request)
|
|
333
|
+
current_time = _get_current_time(request)
|
|
334
|
+
cognitive_state = _get_cognitive_state_safe(request)
|
|
335
|
+
init_complete = _check_initialization_status(request)
|
|
336
|
+
|
|
337
|
+
# Collect service health data
|
|
338
|
+
services = await _collect_service_health(request)
|
|
339
|
+
processor_healthy = await _check_processor_health(request)
|
|
340
|
+
|
|
341
|
+
# Determine overall system status
|
|
342
|
+
status = _determine_overall_status(init_complete, processor_healthy, services)
|
|
343
|
+
|
|
344
|
+
response = SystemHealthResponse(
|
|
345
|
+
status=status,
|
|
346
|
+
version=CIRIS_VERSION,
|
|
347
|
+
uptime_seconds=uptime_seconds,
|
|
348
|
+
services=services,
|
|
349
|
+
initialization_complete=init_complete,
|
|
350
|
+
cognitive_state=cognitive_state,
|
|
351
|
+
timestamp=current_time,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return SuccessResponse(data=response)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@router.get("/time", response_model=SuccessResponse[SystemTimeResponse])
|
|
358
|
+
async def get_system_time(
|
|
359
|
+
request: Request, auth: AuthContext = Depends(require_observer)
|
|
360
|
+
) -> SuccessResponse[SystemTimeResponse]:
|
|
361
|
+
"""
|
|
362
|
+
System time information.
|
|
363
|
+
|
|
364
|
+
Returns both system time (host OS) and agent time (TimeService),
|
|
365
|
+
along with synchronization status.
|
|
366
|
+
"""
|
|
367
|
+
# Get time service
|
|
368
|
+
time_service: Optional[TimeServiceProtocol] = getattr(request.app.state, "time_service", None)
|
|
369
|
+
if not time_service:
|
|
370
|
+
raise HTTPException(status_code=503, detail=ERROR_TIME_SERVICE_NOT_AVAILABLE)
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
# Get system time (actual OS time)
|
|
374
|
+
system_time = datetime.now(timezone.utc)
|
|
375
|
+
|
|
376
|
+
# Get agent time (from TimeService)
|
|
377
|
+
agent_time = time_service.now()
|
|
378
|
+
|
|
379
|
+
# Calculate uptime
|
|
380
|
+
start_time = getattr(time_service, "_start_time", None)
|
|
381
|
+
if not start_time:
|
|
382
|
+
start_time = agent_time
|
|
383
|
+
uptime_seconds = 0.0
|
|
384
|
+
else:
|
|
385
|
+
uptime_seconds = (agent_time - start_time).total_seconds()
|
|
386
|
+
|
|
387
|
+
# Calculate time sync status
|
|
388
|
+
is_mocked = getattr(time_service, "_mock_time", None) is not None
|
|
389
|
+
time_diff_ms = (agent_time - system_time).total_seconds() * 1000
|
|
390
|
+
|
|
391
|
+
time_sync = TimeSyncStatus(
|
|
392
|
+
synchronized=not is_mocked and abs(time_diff_ms) < 1000, # Within 1 second
|
|
393
|
+
drift_ms=time_diff_ms,
|
|
394
|
+
last_sync=getattr(time_service, "_last_sync", agent_time),
|
|
395
|
+
sync_source="mock" if is_mocked else "system",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
response = SystemTimeResponse(
|
|
399
|
+
system_time=system_time, agent_time=agent_time, uptime_seconds=uptime_seconds, time_sync=time_sync
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return SuccessResponse(data=response)
|
|
403
|
+
except Exception as e:
|
|
404
|
+
raise HTTPException(status_code=500, detail=f"Failed to get time information: {str(e)}")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@router.get("/resources", response_model=SuccessResponse[ResourceUsageResponse])
|
|
408
|
+
async def get_resource_usage(
|
|
409
|
+
request: Request, auth: AuthContext = Depends(require_observer)
|
|
410
|
+
) -> SuccessResponse[ResourceUsageResponse]:
|
|
411
|
+
"""
|
|
412
|
+
Resource usage and limits.
|
|
413
|
+
|
|
414
|
+
Returns current resource consumption, configured limits,
|
|
415
|
+
and health status.
|
|
416
|
+
"""
|
|
417
|
+
resource_monitor = getattr(request.app.state, "resource_monitor", None)
|
|
418
|
+
if not resource_monitor:
|
|
419
|
+
raise HTTPException(status_code=503, detail=ERROR_RESOURCE_MONITOR_NOT_AVAILABLE)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
# Get current snapshot and budget
|
|
423
|
+
snapshot = resource_monitor.snapshot
|
|
424
|
+
budget = resource_monitor.budget
|
|
425
|
+
|
|
426
|
+
# Determine health status
|
|
427
|
+
if snapshot.critical:
|
|
428
|
+
health_status = "critical"
|
|
429
|
+
elif snapshot.warnings:
|
|
430
|
+
health_status = "warning"
|
|
431
|
+
else:
|
|
432
|
+
health_status = "healthy"
|
|
433
|
+
|
|
434
|
+
response = ResourceUsageResponse(
|
|
435
|
+
current_usage=snapshot,
|
|
436
|
+
limits=budget,
|
|
437
|
+
health_status=health_status,
|
|
438
|
+
warnings=snapshot.warnings,
|
|
439
|
+
critical=snapshot.critical,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return SuccessResponse(data=response)
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
logger.error(f"Error getting resource usage: {e}")
|
|
446
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _get_runtime_control_service(request: Request) -> Any:
|
|
450
|
+
"""Get runtime control service from request, trying main service first."""
|
|
451
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
452
|
+
if not runtime_control:
|
|
453
|
+
runtime_control = getattr(request.app.state, "runtime_control_service", None)
|
|
454
|
+
if not runtime_control:
|
|
455
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
456
|
+
return runtime_control
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _validate_runtime_action(action: str) -> None:
|
|
460
|
+
"""Validate the runtime control action."""
|
|
461
|
+
valid_actions = ["pause", "resume", "state"]
|
|
462
|
+
if action not in valid_actions:
|
|
463
|
+
raise HTTPException(status_code=400, detail=f"Invalid action. Must be one of: {', '.join(valid_actions)}")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
async def _execute_pause_action(runtime_control: Any, body: RuntimeAction) -> bool:
|
|
467
|
+
"""Execute pause action and return success status."""
|
|
468
|
+
# Check if the service expects a reason parameter (API runtime control) or not (main runtime control)
|
|
469
|
+
import inspect
|
|
470
|
+
|
|
471
|
+
sig = inspect.signature(runtime_control.pause_processing)
|
|
472
|
+
if len(sig.parameters) > 0: # API runtime control service
|
|
473
|
+
success: bool = await runtime_control.pause_processing(body.reason or "API request")
|
|
474
|
+
else: # Main runtime control service
|
|
475
|
+
control_response = await runtime_control.pause_processing()
|
|
476
|
+
success = control_response.success
|
|
477
|
+
return success
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _extract_pipeline_state_info(
|
|
481
|
+
request: Request,
|
|
482
|
+
) -> tuple[Optional[str], Optional[JSONDict], Optional[JSONDict]]:
|
|
483
|
+
"""
|
|
484
|
+
Extract pipeline state information for UI display.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Tuple of (current_step, current_step_schema, pipeline_state)
|
|
488
|
+
"""
|
|
489
|
+
current_step: Optional[str] = None
|
|
490
|
+
current_step_schema: Optional[JSONDict] = None
|
|
491
|
+
pipeline_state: Optional[JSONDict] = None
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
# Try to get current pipeline state from the runtime
|
|
495
|
+
runtime = getattr(request.app.state, "runtime", None)
|
|
496
|
+
if runtime and hasattr(runtime, "agent_processor") and runtime.agent_processor:
|
|
497
|
+
if (
|
|
498
|
+
hasattr(runtime.agent_processor, "_pipeline_controller")
|
|
499
|
+
and runtime.agent_processor._pipeline_controller
|
|
500
|
+
):
|
|
501
|
+
pipeline_controller = runtime.agent_processor._pipeline_controller
|
|
502
|
+
|
|
503
|
+
# Get current pipeline state
|
|
504
|
+
try:
|
|
505
|
+
pipeline_state_obj = pipeline_controller.get_current_state()
|
|
506
|
+
if pipeline_state_obj and hasattr(pipeline_state_obj, "current_step"):
|
|
507
|
+
current_step = pipeline_state_obj.current_step
|
|
508
|
+
if pipeline_state_obj and hasattr(pipeline_state_obj, "pipeline_state"):
|
|
509
|
+
pipeline_state = pipeline_state_obj.pipeline_state
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.debug(f"Could not get current step from pipeline: {e}")
|
|
512
|
+
|
|
513
|
+
# Get the full step schema/metadata
|
|
514
|
+
if current_step:
|
|
515
|
+
try:
|
|
516
|
+
# Get step schema - this would include all step metadata
|
|
517
|
+
current_step_schema = {
|
|
518
|
+
"step_point": current_step,
|
|
519
|
+
"description": f"System paused at step: {current_step}",
|
|
520
|
+
"timestamp": datetime.now().isoformat(),
|
|
521
|
+
"can_single_step": True,
|
|
522
|
+
"next_actions": ["single_step", "resume"],
|
|
523
|
+
}
|
|
524
|
+
except Exception as e:
|
|
525
|
+
logger.debug(f"Could not get step schema: {e}")
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.debug(f"Could not get pipeline information: {e}")
|
|
528
|
+
|
|
529
|
+
return current_step, current_step_schema, pipeline_state
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _create_pause_response(
|
|
533
|
+
success: bool,
|
|
534
|
+
current_step: Optional[str],
|
|
535
|
+
current_step_schema: Optional[JSONDict],
|
|
536
|
+
pipeline_state: Optional[JSONDict],
|
|
537
|
+
) -> RuntimeControlResponse:
|
|
538
|
+
"""Create pause action response."""
|
|
539
|
+
# Create clear message based on success state
|
|
540
|
+
if success:
|
|
541
|
+
step_suffix = f" at step: {current_step}" if current_step else ""
|
|
542
|
+
message = f"Processing paused{step_suffix}"
|
|
543
|
+
else:
|
|
544
|
+
message = "Already paused"
|
|
545
|
+
|
|
546
|
+
result = RuntimeControlResponse(
|
|
547
|
+
success=success,
|
|
548
|
+
message=message,
|
|
549
|
+
processor_state="paused" if success else "unknown",
|
|
550
|
+
cognitive_state="UNKNOWN",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Add current step information to response for UI
|
|
554
|
+
if current_step:
|
|
555
|
+
result.current_step = current_step
|
|
556
|
+
result.current_step_schema = current_step_schema
|
|
557
|
+
result.pipeline_state = pipeline_state
|
|
558
|
+
|
|
559
|
+
return result
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
async def _execute_resume_action(runtime_control: Any) -> RuntimeControlResponse:
|
|
563
|
+
"""Execute resume action."""
|
|
564
|
+
# Check if the service returns a control response or just boolean
|
|
565
|
+
resume_result = await runtime_control.resume_processing()
|
|
566
|
+
if hasattr(resume_result, "success"): # Main runtime control service
|
|
567
|
+
success = resume_result.success
|
|
568
|
+
else: # API runtime control service
|
|
569
|
+
success = resume_result
|
|
570
|
+
|
|
571
|
+
return RuntimeControlResponse(
|
|
572
|
+
success=success,
|
|
573
|
+
message="Processing resumed" if success else "Not paused",
|
|
574
|
+
processor_state="active" if success else "unknown",
|
|
575
|
+
cognitive_state="UNKNOWN",
|
|
576
|
+
queue_depth=0,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
async def _execute_state_action(runtime_control: Any) -> RuntimeControlResponse:
|
|
581
|
+
"""Execute state query action."""
|
|
582
|
+
# Get current state without changing it
|
|
583
|
+
status = await runtime_control.get_runtime_status()
|
|
584
|
+
# Get queue depth from the same source as queue endpoint
|
|
585
|
+
queue_status = await runtime_control.get_processor_queue_status()
|
|
586
|
+
actual_queue_depth = queue_status.queue_size if queue_status else 0
|
|
587
|
+
|
|
588
|
+
return RuntimeControlResponse(
|
|
589
|
+
success=True,
|
|
590
|
+
message="Current runtime state retrieved",
|
|
591
|
+
processor_state="paused" if status.processor_status == ProcessorStatus.PAUSED else "active",
|
|
592
|
+
cognitive_state=status.cognitive_state or "UNKNOWN",
|
|
593
|
+
queue_depth=actual_queue_depth,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _get_system_uptime(request: Request) -> float:
|
|
598
|
+
"""Get system uptime in seconds."""
|
|
599
|
+
time_service: Optional[TimeServiceProtocol] = getattr(request.app.state, "time_service", None)
|
|
600
|
+
start_time = getattr(time_service, "_start_time", None) if time_service else None
|
|
601
|
+
current_time = time_service.now() if time_service else datetime.now(timezone.utc)
|
|
602
|
+
return (current_time - start_time).total_seconds() if start_time else 0.0
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _get_current_time(request: Request) -> datetime:
|
|
606
|
+
"""Get current system time."""
|
|
607
|
+
time_service: Optional[TimeServiceProtocol] = getattr(request.app.state, "time_service", None)
|
|
608
|
+
return time_service.now() if time_service else datetime.now(timezone.utc)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _get_cognitive_state_safe(request: Request) -> Optional[str]:
|
|
612
|
+
"""Safely get cognitive state from agent processor."""
|
|
613
|
+
runtime = getattr(request.app.state, "runtime", None)
|
|
614
|
+
if not (runtime and hasattr(runtime, "agent_processor") and runtime.agent_processor is not None):
|
|
615
|
+
return None
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
state: str = runtime.agent_processor.get_current_state()
|
|
619
|
+
return state
|
|
620
|
+
except Exception as e:
|
|
621
|
+
logger.warning(
|
|
622
|
+
f"Failed to retrieve cognitive state: {type(e).__name__}: {str(e)} - Agent processor may not be initialized"
|
|
623
|
+
)
|
|
624
|
+
return None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _check_initialization_status(request: Request) -> bool:
|
|
628
|
+
"""Check if system initialization is complete."""
|
|
629
|
+
init_service = getattr(request.app.state, "initialization_service", None)
|
|
630
|
+
if init_service and hasattr(init_service, "is_initialized"):
|
|
631
|
+
result: bool = init_service.is_initialized()
|
|
632
|
+
return result
|
|
633
|
+
return True
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
async def _check_provider_health(provider: Any) -> bool:
|
|
637
|
+
"""Check if a single provider is healthy."""
|
|
638
|
+
try:
|
|
639
|
+
if hasattr(provider, "is_healthy"):
|
|
640
|
+
if asyncio.iscoroutinefunction(provider.is_healthy):
|
|
641
|
+
result: bool = await provider.is_healthy()
|
|
642
|
+
return result
|
|
643
|
+
else:
|
|
644
|
+
result_sync: bool = provider.is_healthy()
|
|
645
|
+
return result_sync
|
|
646
|
+
else:
|
|
647
|
+
return True # Assume healthy if no method
|
|
648
|
+
except Exception:
|
|
649
|
+
return False
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
async def _collect_service_health(request: Request) -> Dict[str, Dict[str, int]]:
|
|
653
|
+
"""Collect service health data from service registry."""
|
|
654
|
+
services: Dict[str, Dict[str, int]] = {}
|
|
655
|
+
if not (hasattr(request.app.state, "service_registry") and request.app.state.service_registry is not None):
|
|
656
|
+
return services
|
|
657
|
+
|
|
658
|
+
service_registry = request.app.state.service_registry
|
|
659
|
+
try:
|
|
660
|
+
for service_type in list(ServiceType):
|
|
661
|
+
providers = service_registry.get_services_by_type(service_type)
|
|
662
|
+
if providers:
|
|
663
|
+
healthy_count = 0
|
|
664
|
+
for provider in providers:
|
|
665
|
+
if await _check_provider_health(provider):
|
|
666
|
+
healthy_count += 1
|
|
667
|
+
else:
|
|
668
|
+
logger.debug(f"Service health check returned unhealthy for {service_type.value}")
|
|
669
|
+
services[service_type.value] = {"available": len(providers), "healthy": healthy_count}
|
|
670
|
+
except Exception as e:
|
|
671
|
+
logger.error(f"Error checking service health: {e}")
|
|
672
|
+
|
|
673
|
+
return services
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _check_processor_via_runtime(runtime: Any) -> Optional[bool]:
|
|
677
|
+
"""Check processor health via runtime's agent_processor directly.
|
|
678
|
+
|
|
679
|
+
Returns True if healthy, False if unhealthy, None if cannot determine.
|
|
680
|
+
"""
|
|
681
|
+
if not runtime:
|
|
682
|
+
return None
|
|
683
|
+
agent_processor = getattr(runtime, "agent_processor", None)
|
|
684
|
+
if not agent_processor:
|
|
685
|
+
return None
|
|
686
|
+
# Agent processor exists - check if it's running
|
|
687
|
+
is_running = getattr(agent_processor, "_running", False)
|
|
688
|
+
if is_running:
|
|
689
|
+
return True
|
|
690
|
+
# Also check via _agent_task if available
|
|
691
|
+
agent_task = getattr(runtime, "_agent_task", None)
|
|
692
|
+
if agent_task and not agent_task.done():
|
|
693
|
+
return True
|
|
694
|
+
return None
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _get_runtime_control_from_app(request: Request) -> Any:
|
|
698
|
+
"""Get RuntimeControlService from app state, trying multiple locations."""
|
|
699
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
700
|
+
if not runtime_control:
|
|
701
|
+
runtime_control = getattr(request.app.state, "runtime_control_service", None)
|
|
702
|
+
return runtime_control
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
async def _check_health_via_runtime_control(runtime_control: Any) -> Optional[bool]:
|
|
706
|
+
"""Check processor health via RuntimeControlService.
|
|
707
|
+
|
|
708
|
+
Returns True if healthy, False if unhealthy, None if cannot determine.
|
|
709
|
+
"""
|
|
710
|
+
if not runtime_control:
|
|
711
|
+
return None
|
|
712
|
+
try:
|
|
713
|
+
# Try get_processor_queue_status if available
|
|
714
|
+
if hasattr(runtime_control, "get_processor_queue_status"):
|
|
715
|
+
queue_status = await runtime_control.get_processor_queue_status()
|
|
716
|
+
processor_healthy = queue_status.processor_name != "unknown"
|
|
717
|
+
runtime_status = await runtime_control.get_runtime_status()
|
|
718
|
+
return bool(processor_healthy and runtime_status.is_running)
|
|
719
|
+
# Fallback: Check runtime status dict (APIRuntimeControlService)
|
|
720
|
+
elif hasattr(runtime_control, "get_runtime_status"):
|
|
721
|
+
status = runtime_control.get_runtime_status()
|
|
722
|
+
if isinstance(status, dict):
|
|
723
|
+
# APIRuntimeControlService returns dict, not paused = healthy
|
|
724
|
+
return not status.get("paused", False)
|
|
725
|
+
except Exception as e:
|
|
726
|
+
logger.warning(f"Failed to check processor health via runtime_control: {e}")
|
|
727
|
+
return None
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
async def _check_processor_health(request: Request) -> bool:
|
|
731
|
+
"""Check if processor thread is healthy."""
|
|
732
|
+
runtime = getattr(request.app.state, "runtime", None)
|
|
733
|
+
|
|
734
|
+
# First try: Check the runtime's agent_processor directly
|
|
735
|
+
runtime_result = _check_processor_via_runtime(runtime)
|
|
736
|
+
if runtime_result is True:
|
|
737
|
+
return True
|
|
738
|
+
|
|
739
|
+
# Second try: Use RuntimeControlService if available (for full API)
|
|
740
|
+
runtime_control = _get_runtime_control_from_app(request)
|
|
741
|
+
control_result = await _check_health_via_runtime_control(runtime_control)
|
|
742
|
+
if control_result is not None:
|
|
743
|
+
return control_result
|
|
744
|
+
|
|
745
|
+
# If we have a runtime with agent_processor, consider healthy
|
|
746
|
+
if runtime and getattr(runtime, "agent_processor", None) is not None:
|
|
747
|
+
return True
|
|
748
|
+
|
|
749
|
+
return False
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _determine_overall_status(init_complete: bool, processor_healthy: bool, services: Dict[str, Dict[str, int]]) -> str:
|
|
753
|
+
"""Determine overall system status based on components."""
|
|
754
|
+
total_services = sum(s.get("available", 0) for s in services.values())
|
|
755
|
+
healthy_services = sum(s.get("healthy", 0) for s in services.values())
|
|
756
|
+
|
|
757
|
+
if not init_complete:
|
|
758
|
+
return "initializing"
|
|
759
|
+
elif not processor_healthy:
|
|
760
|
+
return "critical" # Processor thread dead = critical
|
|
761
|
+
elif healthy_services == total_services:
|
|
762
|
+
return "healthy"
|
|
763
|
+
elif healthy_services >= total_services * 0.8:
|
|
764
|
+
return "degraded"
|
|
765
|
+
else:
|
|
766
|
+
return "critical"
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _get_cognitive_state(request: Request) -> Optional[str]:
|
|
770
|
+
"""Get cognitive state from agent processor if available."""
|
|
771
|
+
cognitive_state: Optional[str] = None
|
|
772
|
+
runtime = getattr(request.app.state, "runtime", None)
|
|
773
|
+
if runtime and hasattr(runtime, "agent_processor") and runtime.agent_processor is not None:
|
|
774
|
+
try:
|
|
775
|
+
cognitive_state = runtime.agent_processor.get_current_state()
|
|
776
|
+
except Exception as e:
|
|
777
|
+
logger.warning(
|
|
778
|
+
f"Failed to retrieve cognitive state: {type(e).__name__}: {str(e)} - Agent processor may not be initialized"
|
|
779
|
+
)
|
|
780
|
+
return cognitive_state
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _create_final_response(
|
|
784
|
+
base_result: RuntimeControlResponse, cognitive_state: Optional[str]
|
|
785
|
+
) -> RuntimeControlResponse:
|
|
786
|
+
"""Create final response with cognitive state and any enhanced fields."""
|
|
787
|
+
response = RuntimeControlResponse(
|
|
788
|
+
success=base_result.success,
|
|
789
|
+
message=base_result.message,
|
|
790
|
+
processor_state=base_result.processor_state,
|
|
791
|
+
cognitive_state=cognitive_state or base_result.cognitive_state or "UNKNOWN",
|
|
792
|
+
queue_depth=base_result.queue_depth,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# Copy enhanced fields if they exist
|
|
796
|
+
if hasattr(base_result, "current_step"):
|
|
797
|
+
response.current_step = base_result.current_step
|
|
798
|
+
if hasattr(base_result, "current_step_schema"):
|
|
799
|
+
response.current_step_schema = base_result.current_step_schema
|
|
800
|
+
if hasattr(base_result, "pipeline_state"):
|
|
801
|
+
response.pipeline_state = base_result.pipeline_state
|
|
802
|
+
|
|
803
|
+
return response
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@router.post("/runtime/{action}", response_model=SuccessResponse[RuntimeControlResponse])
|
|
807
|
+
async def control_runtime(
|
|
808
|
+
action: str, request: Request, body: RuntimeAction = Body(...), auth: AuthContext = Depends(require_admin)
|
|
809
|
+
) -> SuccessResponse[RuntimeControlResponse]:
|
|
810
|
+
"""
|
|
811
|
+
Runtime control actions.
|
|
812
|
+
|
|
813
|
+
Control agent runtime behavior. Valid actions:
|
|
814
|
+
- pause: Pause message processing
|
|
815
|
+
- resume: Resume message processing
|
|
816
|
+
- state: Get current runtime state
|
|
817
|
+
|
|
818
|
+
Requires ADMIN role.
|
|
819
|
+
"""
|
|
820
|
+
try:
|
|
821
|
+
runtime_control = _get_runtime_control_service(request)
|
|
822
|
+
_validate_runtime_action(action)
|
|
823
|
+
|
|
824
|
+
# Execute action
|
|
825
|
+
if action == "pause":
|
|
826
|
+
success = await _execute_pause_action(runtime_control, body)
|
|
827
|
+
current_step, current_step_schema, pipeline_state = _extract_pipeline_state_info(request)
|
|
828
|
+
result = _create_pause_response(success, current_step, current_step_schema, pipeline_state)
|
|
829
|
+
elif action == "resume":
|
|
830
|
+
result = await _execute_resume_action(runtime_control)
|
|
831
|
+
elif action == "state":
|
|
832
|
+
result = await _execute_state_action(runtime_control)
|
|
833
|
+
return SuccessResponse(data=result)
|
|
834
|
+
|
|
835
|
+
# Get cognitive state and create final response
|
|
836
|
+
cognitive_state = _get_cognitive_state(request)
|
|
837
|
+
response = _create_final_response(result, cognitive_state)
|
|
838
|
+
|
|
839
|
+
return SuccessResponse(data=response)
|
|
840
|
+
|
|
841
|
+
except HTTPException:
|
|
842
|
+
raise
|
|
843
|
+
except Exception as e:
|
|
844
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
# Valid cognitive states for transition
|
|
848
|
+
VALID_COGNITIVE_STATES = {"WORK", "DREAM", "PLAY", "SOLITUDE"}
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
@router.post("/state/transition", response_model=SuccessResponse[StateTransitionResponse])
|
|
852
|
+
async def transition_cognitive_state(
|
|
853
|
+
request: Request,
|
|
854
|
+
body: StateTransitionRequest = Body(...),
|
|
855
|
+
auth: AuthContext = Depends(require_admin),
|
|
856
|
+
) -> SuccessResponse[StateTransitionResponse]:
|
|
857
|
+
"""
|
|
858
|
+
Request a cognitive state transition.
|
|
859
|
+
|
|
860
|
+
Transitions the agent to a different cognitive state (WORK, DREAM, PLAY, SOLITUDE).
|
|
861
|
+
Valid transitions depend on the current state:
|
|
862
|
+
- From WORK: Can transition to DREAM, PLAY, or SOLITUDE
|
|
863
|
+
- From PLAY: Can transition to WORK or SOLITUDE
|
|
864
|
+
- From SOLITUDE: Can transition to WORK
|
|
865
|
+
- From DREAM: Typically transitions back to WORK when complete
|
|
866
|
+
|
|
867
|
+
Requires ADMIN role.
|
|
868
|
+
"""
|
|
869
|
+
try:
|
|
870
|
+
target_state = body.target_state.upper()
|
|
871
|
+
logger.info(f"[STATE_TRANSITION] Request received: target_state={target_state}, reason={body.reason}")
|
|
872
|
+
|
|
873
|
+
# Validate target state
|
|
874
|
+
if target_state not in VALID_COGNITIVE_STATES:
|
|
875
|
+
logger.error(f"[STATE_TRANSITION] FAIL: Invalid target state '{target_state}'")
|
|
876
|
+
raise HTTPException(
|
|
877
|
+
status_code=400,
|
|
878
|
+
detail=f"Invalid target state '{target_state}'. Must be one of: {', '.join(sorted(VALID_COGNITIVE_STATES))}",
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
# Get current state
|
|
882
|
+
previous_state = _get_cognitive_state(request)
|
|
883
|
+
logger.info(f"[STATE_TRANSITION] Current state: {previous_state}")
|
|
884
|
+
|
|
885
|
+
# Get runtime control service - FAIL FAST with detailed logging
|
|
886
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
887
|
+
if not runtime_control:
|
|
888
|
+
runtime_control = getattr(request.app.state, "runtime_control_service", None)
|
|
889
|
+
|
|
890
|
+
if not runtime_control:
|
|
891
|
+
logger.error("[STATE_TRANSITION] FAIL: No runtime control service available in app.state")
|
|
892
|
+
logger.error(f"[STATE_TRANSITION] Available app.state attrs: {dir(request.app.state)}")
|
|
893
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
894
|
+
|
|
895
|
+
# Log service type for debugging
|
|
896
|
+
service_type = type(runtime_control).__name__
|
|
897
|
+
service_module = type(runtime_control).__module__
|
|
898
|
+
logger.info(f"[STATE_TRANSITION] Runtime control service: {service_type} from {service_module}")
|
|
899
|
+
|
|
900
|
+
# Check if request_state_transition is available - FAIL LOUD
|
|
901
|
+
has_method = hasattr(runtime_control, "request_state_transition")
|
|
902
|
+
logger.info(f"[STATE_TRANSITION] Has request_state_transition method: {has_method}")
|
|
903
|
+
|
|
904
|
+
if not has_method:
|
|
905
|
+
available_methods = [m for m in dir(runtime_control) if not m.startswith("_")]
|
|
906
|
+
logger.error(f"[STATE_TRANSITION] FAIL: Service {service_type} missing request_state_transition")
|
|
907
|
+
logger.error(f"[STATE_TRANSITION] Available methods: {available_methods}")
|
|
908
|
+
raise HTTPException(
|
|
909
|
+
status_code=503,
|
|
910
|
+
detail=f"State transition not supported by {service_type}. Missing request_state_transition method.",
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
# Request the transition
|
|
914
|
+
reason = body.reason or f"Requested via API from {previous_state or 'UNKNOWN'}"
|
|
915
|
+
logger.info(f"[STATE_TRANSITION] Calling request_state_transition({target_state}, {reason})")
|
|
916
|
+
success = await runtime_control.request_state_transition(target_state, reason)
|
|
917
|
+
logger.info(f"[STATE_TRANSITION] Transition result: success={success}")
|
|
918
|
+
|
|
919
|
+
# Get current state after transition attempt
|
|
920
|
+
current_state = _get_cognitive_state(request) or target_state
|
|
921
|
+
logger.info(f"[STATE_TRANSITION] Post-transition state: {current_state}")
|
|
922
|
+
|
|
923
|
+
if success:
|
|
924
|
+
message = f"Transition to {target_state} initiated successfully"
|
|
925
|
+
else:
|
|
926
|
+
message = f"Transition to {target_state} could not be initiated"
|
|
927
|
+
|
|
928
|
+
return SuccessResponse(
|
|
929
|
+
data=StateTransitionResponse(
|
|
930
|
+
success=success,
|
|
931
|
+
message=message,
|
|
932
|
+
previous_state=previous_state,
|
|
933
|
+
current_state=current_state,
|
|
934
|
+
)
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
except HTTPException:
|
|
938
|
+
raise
|
|
939
|
+
except Exception as e:
|
|
940
|
+
logger.error(f"[STATE_TRANSITION] FAIL: Unexpected error: {type(e).__name__}: {e}")
|
|
941
|
+
import traceback
|
|
942
|
+
|
|
943
|
+
logger.error(f"[STATE_TRANSITION] Traceback:\n{traceback.format_exc()}")
|
|
944
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _parse_direct_service_key(service_key: str) -> tuple[str, str]:
|
|
948
|
+
"""Parse direct service key and return service_type and display_name."""
|
|
949
|
+
parts = service_key.split(".")
|
|
950
|
+
if len(parts) >= 3:
|
|
951
|
+
service_type = parts[1] # 'graph', 'infrastructure', etc.
|
|
952
|
+
service_name = parts[2] # 'memory_service', 'time_service', etc.
|
|
953
|
+
|
|
954
|
+
# Convert snake_case to PascalCase for display
|
|
955
|
+
display_name = "".join(word.capitalize() for word in service_name.split("_"))
|
|
956
|
+
return service_type, display_name
|
|
957
|
+
return "unknown", service_key
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _parse_registry_service_key(service_key: str) -> tuple[str, str]:
|
|
961
|
+
"""Parse registry service key and return service_type and display_name."""
|
|
962
|
+
parts = service_key.split(".")
|
|
963
|
+
logger.debug(f"Parsing registry key: {service_key}, parts: {parts}")
|
|
964
|
+
|
|
965
|
+
# Handle both 3-part and 4-part keys
|
|
966
|
+
if len(parts) >= 4 and parts[1] == "ServiceType":
|
|
967
|
+
# Format: registry.ServiceType.ENUM.ServiceName_id
|
|
968
|
+
service_type_enum = f"{parts[1]}.{parts[2]}" # 'ServiceType.TOOL'
|
|
969
|
+
service_name = parts[3] # 'APIToolService_127803015745648'
|
|
970
|
+
logger.debug(f"4-part key: {service_key}, service_name: {service_name}")
|
|
971
|
+
else:
|
|
972
|
+
# Fallback: registry.ENUM.ServiceName
|
|
973
|
+
service_type_enum = parts[1] # 'ServiceType.COMMUNICATION', etc.
|
|
974
|
+
service_name = parts[2] if len(parts) > 2 else parts[1] # Service name or enum value
|
|
975
|
+
logger.debug(f"3-part key: {service_key}, service_name: {service_name}")
|
|
976
|
+
|
|
977
|
+
# Clean up service name (remove instance ID)
|
|
978
|
+
if "_" in service_name:
|
|
979
|
+
service_name = service_name.split("_")[0]
|
|
980
|
+
|
|
981
|
+
# Extract adapter type from service name
|
|
982
|
+
adapter_prefix = ""
|
|
983
|
+
if "Discord" in service_name:
|
|
984
|
+
adapter_prefix = "DISCORD"
|
|
985
|
+
elif "API" in service_name:
|
|
986
|
+
adapter_prefix = "API"
|
|
987
|
+
elif "CLI" in service_name:
|
|
988
|
+
adapter_prefix = "CLI"
|
|
989
|
+
|
|
990
|
+
# Map ServiceType enum to category and set display name
|
|
991
|
+
service_type, display_name = _map_service_type_enum(service_type_enum, service_name, adapter_prefix)
|
|
992
|
+
|
|
993
|
+
return service_type, display_name
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _map_service_type_enum(service_type_enum: str, service_name: str, adapter_prefix: str) -> tuple[str, str]:
|
|
997
|
+
"""Map ServiceType enum to category and create display name."""
|
|
998
|
+
service_type = _get_service_category(service_type_enum)
|
|
999
|
+
display_name = _create_display_name(service_type_enum, service_name, adapter_prefix)
|
|
1000
|
+
|
|
1001
|
+
return service_type, display_name
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _get_service_category(service_type_enum: str) -> str:
|
|
1005
|
+
"""Get the service category based on the service type enum."""
|
|
1006
|
+
# Tool Services (need to check first due to SECRETS_TOOL containing SECRETS)
|
|
1007
|
+
if "TOOL" in service_type_enum:
|
|
1008
|
+
return "tool"
|
|
1009
|
+
|
|
1010
|
+
# Adapter Services (Communication is adapter-specific)
|
|
1011
|
+
elif "COMMUNICATION" in service_type_enum:
|
|
1012
|
+
return "adapter"
|
|
1013
|
+
|
|
1014
|
+
# Runtime Services (need to check RUNTIME_CONTROL before SECRETS in infrastructure)
|
|
1015
|
+
elif any(service in service_type_enum for service in ["LLM", "RUNTIME_CONTROL", "TASK_SCHEDULER"]):
|
|
1016
|
+
return "runtime"
|
|
1017
|
+
|
|
1018
|
+
# Graph Services (6)
|
|
1019
|
+
elif any(
|
|
1020
|
+
service in service_type_enum
|
|
1021
|
+
for service in ["MEMORY", "CONFIG", "TELEMETRY", "AUDIT", "INCIDENT_MANAGEMENT", "TSDB_CONSOLIDATION"]
|
|
1022
|
+
):
|
|
1023
|
+
return "graph"
|
|
1024
|
+
|
|
1025
|
+
# Infrastructure Services (7)
|
|
1026
|
+
elif any(
|
|
1027
|
+
service in service_type_enum
|
|
1028
|
+
for service in [
|
|
1029
|
+
"TIME",
|
|
1030
|
+
"SECRETS",
|
|
1031
|
+
"AUTHENTICATION",
|
|
1032
|
+
"RESOURCE_MONITOR",
|
|
1033
|
+
"DATABASE_MAINTENANCE",
|
|
1034
|
+
"INITIALIZATION",
|
|
1035
|
+
"SHUTDOWN",
|
|
1036
|
+
]
|
|
1037
|
+
):
|
|
1038
|
+
return "infrastructure"
|
|
1039
|
+
|
|
1040
|
+
# Governance Services (4)
|
|
1041
|
+
elif any(
|
|
1042
|
+
service in service_type_enum
|
|
1043
|
+
for service in ["WISE_AUTHORITY", "ADAPTIVE_FILTER", "VISIBILITY", "SELF_OBSERVATION"]
|
|
1044
|
+
):
|
|
1045
|
+
return "governance"
|
|
1046
|
+
|
|
1047
|
+
else:
|
|
1048
|
+
return "unknown"
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _create_display_name(service_type_enum: str, service_name: str, adapter_prefix: str) -> str:
|
|
1052
|
+
"""Create appropriate display name based on service type and adapter prefix."""
|
|
1053
|
+
if not adapter_prefix:
|
|
1054
|
+
return service_name
|
|
1055
|
+
|
|
1056
|
+
if "COMMUNICATION" in service_type_enum:
|
|
1057
|
+
return f"{adapter_prefix}-COMM"
|
|
1058
|
+
elif "RUNTIME_CONTROL" in service_type_enum:
|
|
1059
|
+
return f"{adapter_prefix}-RUNTIME"
|
|
1060
|
+
elif "TOOL" in service_type_enum:
|
|
1061
|
+
return f"{adapter_prefix}-TOOL"
|
|
1062
|
+
elif "WISE_AUTHORITY" in service_type_enum:
|
|
1063
|
+
return f"{adapter_prefix}-WISE"
|
|
1064
|
+
else:
|
|
1065
|
+
return service_name
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _parse_service_key(service_key: str) -> tuple[str, str]:
|
|
1069
|
+
"""Parse any service key and return service_type and display_name."""
|
|
1070
|
+
parts = service_key.split(".")
|
|
1071
|
+
|
|
1072
|
+
# Handle direct services (format: direct.service_type.service_name)
|
|
1073
|
+
if service_key.startswith("direct.") and len(parts) >= 3:
|
|
1074
|
+
return _parse_direct_service_key(service_key)
|
|
1075
|
+
|
|
1076
|
+
# Handle registry services (format: registry.ServiceType.ENUM.ServiceName_id)
|
|
1077
|
+
elif service_key.startswith("registry.") and len(parts) >= 3:
|
|
1078
|
+
return _parse_registry_service_key(service_key)
|
|
1079
|
+
|
|
1080
|
+
else:
|
|
1081
|
+
return "unknown", service_key
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _create_service_status(service_key: str, details: JSONDict) -> ServiceStatus:
|
|
1085
|
+
"""Create ServiceStatus from service key and details."""
|
|
1086
|
+
service_type, display_name = _parse_service_key(service_key)
|
|
1087
|
+
|
|
1088
|
+
return ServiceStatus(
|
|
1089
|
+
name=display_name,
|
|
1090
|
+
type=service_type,
|
|
1091
|
+
healthy=details.get("healthy", False),
|
|
1092
|
+
available=details.get("healthy", False), # Use healthy as available
|
|
1093
|
+
uptime_seconds=None, # Not available in simplified view
|
|
1094
|
+
metrics=ServiceMetrics(),
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _update_service_summary(service_summary: Dict[str, Dict[str, int]], service_type: str, is_healthy: bool) -> None:
|
|
1099
|
+
"""Update service summary with service type and health status."""
|
|
1100
|
+
if service_type not in service_summary:
|
|
1101
|
+
service_summary[service_type] = {"total": 0, "healthy": 0}
|
|
1102
|
+
service_summary[service_type]["total"] += 1
|
|
1103
|
+
if is_healthy:
|
|
1104
|
+
service_summary[service_type]["healthy"] += 1
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@router.get("/services", response_model=SuccessResponse[ServicesStatusResponse])
|
|
1108
|
+
async def get_services_status(
|
|
1109
|
+
request: Request, auth: AuthContext = Depends(require_observer)
|
|
1110
|
+
) -> SuccessResponse[ServicesStatusResponse]:
|
|
1111
|
+
"""
|
|
1112
|
+
Service status.
|
|
1113
|
+
|
|
1114
|
+
Returns status of all system services including health,
|
|
1115
|
+
availability, and basic metrics.
|
|
1116
|
+
"""
|
|
1117
|
+
# Use the runtime control service to get all services
|
|
1118
|
+
try:
|
|
1119
|
+
runtime_control = _get_runtime_control_service(request)
|
|
1120
|
+
except HTTPException:
|
|
1121
|
+
# Handle case where no runtime control service is available
|
|
1122
|
+
return SuccessResponse(
|
|
1123
|
+
data=ServicesStatusResponse(
|
|
1124
|
+
services=[], total_services=0, healthy_services=0, timestamp=datetime.now(timezone.utc)
|
|
1125
|
+
)
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
# Get service health status from runtime control
|
|
1129
|
+
try:
|
|
1130
|
+
health_status = await runtime_control.get_service_health_status()
|
|
1131
|
+
|
|
1132
|
+
# Convert service details to ServiceStatus list using helper functions
|
|
1133
|
+
services = []
|
|
1134
|
+
service_summary: Dict[str, Dict[str, int]] = {}
|
|
1135
|
+
|
|
1136
|
+
# Include ALL services (both direct and registry)
|
|
1137
|
+
for service_key, details in health_status.service_details.items():
|
|
1138
|
+
status = _create_service_status(service_key, details)
|
|
1139
|
+
services.append(status)
|
|
1140
|
+
_update_service_summary(service_summary, status.type, status.healthy)
|
|
1141
|
+
|
|
1142
|
+
return SuccessResponse(
|
|
1143
|
+
data=ServicesStatusResponse(
|
|
1144
|
+
services=services,
|
|
1145
|
+
total_services=len(services),
|
|
1146
|
+
healthy_services=sum(1 for s in services if s.healthy),
|
|
1147
|
+
timestamp=datetime.now(timezone.utc),
|
|
1148
|
+
)
|
|
1149
|
+
)
|
|
1150
|
+
except Exception as e:
|
|
1151
|
+
logger.error(f"Error getting service status: {e}")
|
|
1152
|
+
return SuccessResponse(
|
|
1153
|
+
data=ServicesStatusResponse(
|
|
1154
|
+
services=[], total_services=0, healthy_services=0, timestamp=datetime.now(timezone.utc)
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _validate_shutdown_request(body: ShutdownRequest) -> None:
|
|
1160
|
+
"""Validate shutdown request confirmation."""
|
|
1161
|
+
if not body.confirm:
|
|
1162
|
+
raise HTTPException(status_code=400, detail="Confirmation required (confirm=true)")
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _get_shutdown_service(request: Request) -> Any:
|
|
1166
|
+
"""Get shutdown service from runtime, raising HTTPException if not available."""
|
|
1167
|
+
runtime = getattr(request.app.state, "runtime", None)
|
|
1168
|
+
if not runtime:
|
|
1169
|
+
raise HTTPException(status_code=503, detail="Runtime not available")
|
|
1170
|
+
|
|
1171
|
+
shutdown_service = getattr(runtime, "shutdown_service", None)
|
|
1172
|
+
if not shutdown_service:
|
|
1173
|
+
raise HTTPException(status_code=503, detail=ERROR_SHUTDOWN_SERVICE_NOT_AVAILABLE)
|
|
1174
|
+
|
|
1175
|
+
return shutdown_service, runtime
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def _check_shutdown_already_requested(shutdown_service: Any) -> None:
|
|
1179
|
+
"""Check if shutdown is already in progress."""
|
|
1180
|
+
if shutdown_service.is_shutdown_requested():
|
|
1181
|
+
existing_reason = shutdown_service.get_shutdown_reason()
|
|
1182
|
+
raise HTTPException(status_code=409, detail=f"Shutdown already requested: {existing_reason}")
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _build_shutdown_reason(body: ShutdownRequest, auth: AuthContext) -> str:
|
|
1186
|
+
"""Build and sanitize shutdown reason."""
|
|
1187
|
+
reason = f"{body.reason} (API shutdown by {auth.user_id})"
|
|
1188
|
+
if body.force:
|
|
1189
|
+
reason += " [FORCED]"
|
|
1190
|
+
|
|
1191
|
+
# Sanitize reason for logging to prevent log injection
|
|
1192
|
+
# Replace newlines and control characters with spaces
|
|
1193
|
+
safe_reason = "".join(c if c.isprintable() and c not in "\n\r\t" else " " for c in reason)
|
|
1194
|
+
|
|
1195
|
+
return safe_reason
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _create_audit_metadata(body: ShutdownRequest, auth: AuthContext, request: Request) -> Dict[str, Any]:
|
|
1199
|
+
"""Create metadata dict for shutdown audit event."""
|
|
1200
|
+
is_service_account = auth.role.value == "SERVICE_ACCOUNT"
|
|
1201
|
+
return {
|
|
1202
|
+
"force": body.force,
|
|
1203
|
+
"is_service_account": is_service_account,
|
|
1204
|
+
"auth_role": auth.role.value,
|
|
1205
|
+
"ip_address": request.client.host if request.client else "unknown",
|
|
1206
|
+
"user_agent": request.headers.get("user-agent", "unknown"),
|
|
1207
|
+
"request_path": str(request.url.path),
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
async def _audit_shutdown_request(
|
|
1212
|
+
request: Request, body: ShutdownRequest, auth: AuthContext, safe_reason: str
|
|
1213
|
+
) -> None: # NOSONAR - async required for create_task
|
|
1214
|
+
"""Audit the shutdown request for security tracking."""
|
|
1215
|
+
audit_service = getattr(request.app.state, "audit_service", None)
|
|
1216
|
+
if not audit_service:
|
|
1217
|
+
return
|
|
1218
|
+
|
|
1219
|
+
from ciris_engine.schemas.services.graph.audit import AuditEventData
|
|
1220
|
+
|
|
1221
|
+
audit_event = AuditEventData(
|
|
1222
|
+
entity_id="system",
|
|
1223
|
+
actor=auth.user_id,
|
|
1224
|
+
outcome="initiated",
|
|
1225
|
+
severity="high" if body.force else "warning",
|
|
1226
|
+
action="system_shutdown",
|
|
1227
|
+
resource="system",
|
|
1228
|
+
reason=safe_reason,
|
|
1229
|
+
metadata=_create_audit_metadata(body, auth, request),
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
import asyncio
|
|
1233
|
+
|
|
1234
|
+
# Store task reference to prevent garbage collection
|
|
1235
|
+
# Using _ prefix to indicate we're intentionally not awaiting
|
|
1236
|
+
_audit_task = asyncio.create_task(audit_service.log_event("system_shutdown_request", audit_event))
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
async def _execute_shutdown(shutdown_service: Any, runtime: Any, body: ShutdownRequest, reason: str) -> None:
|
|
1240
|
+
"""Execute the shutdown with appropriate method based on force flag."""
|
|
1241
|
+
if body.force:
|
|
1242
|
+
# Forced shutdown: bypass thought processing, immediate termination
|
|
1243
|
+
await shutdown_service.emergency_shutdown(reason, timeout_seconds=5)
|
|
1244
|
+
else:
|
|
1245
|
+
# Normal shutdown: allow thoughtful consideration via runtime
|
|
1246
|
+
# The runtime's request_shutdown will call the shutdown service AND set global flags
|
|
1247
|
+
runtime.request_shutdown(reason)
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
@router.post("/shutdown", response_model=SuccessResponse[ShutdownResponse])
|
|
1251
|
+
async def shutdown_system(
|
|
1252
|
+
body: ShutdownRequest, request: Request, auth: AuthContext = Depends(require_admin)
|
|
1253
|
+
) -> SuccessResponse[ShutdownResponse]:
|
|
1254
|
+
"""
|
|
1255
|
+
Graceful shutdown.
|
|
1256
|
+
|
|
1257
|
+
Initiates graceful system shutdown. Requires confirmation
|
|
1258
|
+
flag to prevent accidental shutdowns.
|
|
1259
|
+
|
|
1260
|
+
Requires ADMIN role.
|
|
1261
|
+
"""
|
|
1262
|
+
try:
|
|
1263
|
+
# Validate and get required services
|
|
1264
|
+
_validate_shutdown_request(body)
|
|
1265
|
+
shutdown_service, runtime = _get_shutdown_service(request)
|
|
1266
|
+
|
|
1267
|
+
# Check if already shutting down
|
|
1268
|
+
_check_shutdown_already_requested(shutdown_service)
|
|
1269
|
+
|
|
1270
|
+
# Build and sanitize shutdown reason
|
|
1271
|
+
safe_reason = _build_shutdown_reason(body, auth)
|
|
1272
|
+
|
|
1273
|
+
# Log shutdown request
|
|
1274
|
+
logger.warning(f"SHUTDOWN requested: {safe_reason}")
|
|
1275
|
+
|
|
1276
|
+
# Audit shutdown request
|
|
1277
|
+
await _audit_shutdown_request(request, body, auth, safe_reason)
|
|
1278
|
+
|
|
1279
|
+
# Execute shutdown
|
|
1280
|
+
await _execute_shutdown(shutdown_service, runtime, body, safe_reason)
|
|
1281
|
+
|
|
1282
|
+
# Create response
|
|
1283
|
+
response = ShutdownResponse(
|
|
1284
|
+
status="initiated",
|
|
1285
|
+
message=f"System shutdown initiated: {safe_reason}",
|
|
1286
|
+
shutdown_initiated=True,
|
|
1287
|
+
timestamp=datetime.now(timezone.utc),
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
return SuccessResponse(data=response)
|
|
1291
|
+
|
|
1292
|
+
except HTTPException:
|
|
1293
|
+
raise
|
|
1294
|
+
except Exception as e:
|
|
1295
|
+
logger.error(f"Error initiating shutdown: {e}")
|
|
1296
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def _is_localhost_request(request: Request) -> bool:
|
|
1300
|
+
"""Check if request originates from localhost (safe for unauthenticated shutdown)."""
|
|
1301
|
+
client_host = request.client.host if request.client else None
|
|
1302
|
+
# Accept localhost variants: 127.0.0.1, ::1, localhost
|
|
1303
|
+
return client_host in ("127.0.0.1", "::1", "localhost", None)
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
# Constants for local shutdown
|
|
1307
|
+
_RESUME_TIMEOUT_SECONDS = 30.0
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _get_server_state(runtime: Any) -> Dict[str, Any]:
|
|
1311
|
+
"""Get server state info for logging and responses.
|
|
1312
|
+
|
|
1313
|
+
Args:
|
|
1314
|
+
runtime: The runtime instance (may be None)
|
|
1315
|
+
|
|
1316
|
+
Returns:
|
|
1317
|
+
Dict with server_state, uptime_seconds, resume_in_progress, resume_elapsed_seconds
|
|
1318
|
+
"""
|
|
1319
|
+
if not runtime:
|
|
1320
|
+
return {
|
|
1321
|
+
"server_state": "STARTING",
|
|
1322
|
+
"uptime_seconds": 0,
|
|
1323
|
+
"resume_in_progress": False,
|
|
1324
|
+
"resume_elapsed_seconds": None,
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
uptime = time.time() - getattr(runtime, "_startup_time", time.time())
|
|
1328
|
+
resume_in_progress = getattr(runtime, "_resume_in_progress", False)
|
|
1329
|
+
resume_started = getattr(runtime, "_resume_started_at", None)
|
|
1330
|
+
resume_elapsed = (time.time() - resume_started) if resume_started else None
|
|
1331
|
+
shutdown_in_progress = getattr(runtime, "_shutdown_in_progress", False)
|
|
1332
|
+
|
|
1333
|
+
state = _determine_server_state(runtime, shutdown_in_progress, resume_in_progress)
|
|
1334
|
+
|
|
1335
|
+
return {
|
|
1336
|
+
"server_state": state,
|
|
1337
|
+
"uptime_seconds": round(uptime, 2),
|
|
1338
|
+
"resume_in_progress": resume_in_progress,
|
|
1339
|
+
"resume_elapsed_seconds": round(resume_elapsed, 2) if resume_elapsed else None,
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _determine_server_state(runtime: Any, shutdown_in_progress: bool, resume_in_progress: bool) -> str:
|
|
1344
|
+
"""Determine the current server state string.
|
|
1345
|
+
|
|
1346
|
+
Args:
|
|
1347
|
+
runtime: The runtime instance
|
|
1348
|
+
shutdown_in_progress: Whether shutdown is in progress
|
|
1349
|
+
resume_in_progress: Whether resume is in progress
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
State string: SHUTTING_DOWN, RESUMING, READY, or INITIALIZING
|
|
1353
|
+
"""
|
|
1354
|
+
if shutdown_in_progress:
|
|
1355
|
+
return "SHUTTING_DOWN"
|
|
1356
|
+
if resume_in_progress:
|
|
1357
|
+
return "RESUMING"
|
|
1358
|
+
if runtime and getattr(runtime, "_initialized", False):
|
|
1359
|
+
return "READY"
|
|
1360
|
+
return "INITIALIZING"
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
def _check_resume_blocking(runtime: Any, state_info: Dict[str, Any]) -> Optional[Response]:
|
|
1364
|
+
"""Check if resume is in progress and should block shutdown.
|
|
1365
|
+
|
|
1366
|
+
Args:
|
|
1367
|
+
runtime: The runtime instance
|
|
1368
|
+
state_info: Current server state info dict
|
|
1369
|
+
|
|
1370
|
+
Returns:
|
|
1371
|
+
JSONResponse if shutdown should be blocked, None if OK to proceed
|
|
1372
|
+
"""
|
|
1373
|
+
resume_in_progress = getattr(runtime, "_resume_in_progress", False)
|
|
1374
|
+
if not resume_in_progress:
|
|
1375
|
+
return None
|
|
1376
|
+
|
|
1377
|
+
resume_started_at = getattr(runtime, "_resume_started_at", None)
|
|
1378
|
+
resume_elapsed = (time.time() - resume_started_at) if resume_started_at else 0
|
|
1379
|
+
|
|
1380
|
+
if resume_elapsed >= _RESUME_TIMEOUT_SECONDS:
|
|
1381
|
+
# Resume stuck - allow shutdown
|
|
1382
|
+
logger.warning(
|
|
1383
|
+
f"[LOCAL_SHUTDOWN] Resume exceeded timeout ({resume_elapsed:.1f}s > "
|
|
1384
|
+
f"{_RESUME_TIMEOUT_SECONDS}s) - treating as stuck, allowing shutdown"
|
|
1385
|
+
)
|
|
1386
|
+
return None
|
|
1387
|
+
|
|
1388
|
+
# Resume actively happening - ask caller to retry
|
|
1389
|
+
remaining = _RESUME_TIMEOUT_SECONDS - resume_elapsed
|
|
1390
|
+
retry_after_ms = min(2000, int(remaining * 1000))
|
|
1391
|
+
|
|
1392
|
+
logger.warning(
|
|
1393
|
+
f"[LOCAL_SHUTDOWN] Rejected (409) - resume in progress for {resume_elapsed:.1f}s, "
|
|
1394
|
+
f"retry in {retry_after_ms}ms (timeout at {_RESUME_TIMEOUT_SECONDS}s)"
|
|
1395
|
+
)
|
|
1396
|
+
return JSONResponse(
|
|
1397
|
+
status_code=409,
|
|
1398
|
+
content={
|
|
1399
|
+
"status": "busy",
|
|
1400
|
+
"reason": f"Resume from first-run in progress ({resume_elapsed:.1f}s elapsed)",
|
|
1401
|
+
"retry_after_ms": retry_after_ms,
|
|
1402
|
+
"resume_timeout_seconds": _RESUME_TIMEOUT_SECONDS,
|
|
1403
|
+
**state_info,
|
|
1404
|
+
},
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _check_shutdown_already_in_progress(runtime: Any, state_info: Dict[str, Any]) -> Optional[Response]:
|
|
1409
|
+
"""Check if shutdown is already in progress.
|
|
1410
|
+
|
|
1411
|
+
Args:
|
|
1412
|
+
runtime: The runtime instance
|
|
1413
|
+
state_info: Current server state info dict
|
|
1414
|
+
|
|
1415
|
+
Returns:
|
|
1416
|
+
JSONResponse if shutdown already in progress, None otherwise
|
|
1417
|
+
"""
|
|
1418
|
+
shutdown_service = getattr(runtime, "shutdown_service", None)
|
|
1419
|
+
shutdown_in_progress = getattr(runtime, "_shutdown_in_progress", False)
|
|
1420
|
+
|
|
1421
|
+
is_shutting_down = shutdown_in_progress or (shutdown_service and shutdown_service.is_shutdown_requested())
|
|
1422
|
+
|
|
1423
|
+
if not is_shutting_down:
|
|
1424
|
+
return None
|
|
1425
|
+
|
|
1426
|
+
existing_reason = shutdown_service.get_shutdown_reason() if shutdown_service else "unknown"
|
|
1427
|
+
logger.info(f"[LOCAL_SHUTDOWN] Shutdown already in progress: {existing_reason}")
|
|
1428
|
+
return JSONResponse(
|
|
1429
|
+
status_code=202,
|
|
1430
|
+
content={
|
|
1431
|
+
"status": "accepted",
|
|
1432
|
+
"reason": f"Shutdown already in progress: {existing_reason}",
|
|
1433
|
+
**state_info,
|
|
1434
|
+
},
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def _initiate_force_shutdown(runtime: Any, reason: str) -> None:
|
|
1439
|
+
"""Initiate forced shutdown with background exit thread.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
runtime: The runtime instance
|
|
1443
|
+
reason: Shutdown reason string
|
|
1444
|
+
"""
|
|
1445
|
+
import os
|
|
1446
|
+
import threading
|
|
1447
|
+
|
|
1448
|
+
runtime._shutdown_in_progress = True
|
|
1449
|
+
|
|
1450
|
+
def _force_exit() -> None:
|
|
1451
|
+
"""Force process exit after brief delay to allow response to be sent."""
|
|
1452
|
+
time.sleep(0.5)
|
|
1453
|
+
logger.warning("[LOCAL_SHUTDOWN] Force exiting process NOW")
|
|
1454
|
+
os._exit(0)
|
|
1455
|
+
|
|
1456
|
+
exit_thread = threading.Thread(target=_force_exit, daemon=True)
|
|
1457
|
+
exit_thread.start()
|
|
1458
|
+
|
|
1459
|
+
# Also request normal shutdown in case force exit fails
|
|
1460
|
+
runtime.request_shutdown(reason)
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
@router.post("/local-shutdown", response_model=SuccessResponse[ShutdownResponse])
|
|
1464
|
+
async def local_shutdown(request: Request) -> Response:
|
|
1465
|
+
"""
|
|
1466
|
+
Localhost-only shutdown endpoint (no authentication required).
|
|
1467
|
+
|
|
1468
|
+
This endpoint is designed for Android/mobile apps where:
|
|
1469
|
+
- App data may be cleared (losing auth tokens)
|
|
1470
|
+
- Previous Python process may still be running
|
|
1471
|
+
- Need to gracefully shut down before starting new instance
|
|
1472
|
+
|
|
1473
|
+
Security: Only accepts requests from localhost (127.0.0.1, ::1).
|
|
1474
|
+
This is safe because only processes on the same device can call it.
|
|
1475
|
+
|
|
1476
|
+
Response codes for SmartStartup negotiation:
|
|
1477
|
+
- 200: Shutdown initiated successfully
|
|
1478
|
+
- 202: Shutdown already in progress
|
|
1479
|
+
- 403: Not localhost (security rejection)
|
|
1480
|
+
- 409: Resume in progress, retry later (with retry_after_ms)
|
|
1481
|
+
- 503: Server not ready
|
|
1482
|
+
"""
|
|
1483
|
+
# Verify request is from localhost
|
|
1484
|
+
client_host = request.client.host if request.client else "unknown"
|
|
1485
|
+
if not _is_localhost_request(request):
|
|
1486
|
+
logger.warning(f"[LOCAL_SHUTDOWN] Rejected from non-local client: {client_host}")
|
|
1487
|
+
raise HTTPException(status_code=403, detail="This endpoint only accepts requests from localhost")
|
|
1488
|
+
|
|
1489
|
+
logger.info(f"[LOCAL_SHUTDOWN] Request received from {client_host}")
|
|
1490
|
+
|
|
1491
|
+
# Get runtime
|
|
1492
|
+
runtime = getattr(request.app.state, "runtime", None)
|
|
1493
|
+
if not runtime:
|
|
1494
|
+
logger.warning("[LOCAL_SHUTDOWN] Runtime not available (503)")
|
|
1495
|
+
return JSONResponse(
|
|
1496
|
+
status_code=503,
|
|
1497
|
+
content={
|
|
1498
|
+
"status": "error",
|
|
1499
|
+
"reason": "Runtime not available",
|
|
1500
|
+
"retry_after_ms": 1000,
|
|
1501
|
+
"server_state": "STARTING",
|
|
1502
|
+
},
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
state_info = _get_server_state(runtime)
|
|
1506
|
+
logger.info(f"[LOCAL_SHUTDOWN] Server state: {state_info}")
|
|
1507
|
+
|
|
1508
|
+
# Check if resume is blocking shutdown
|
|
1509
|
+
resume_response = _check_resume_blocking(runtime, state_info)
|
|
1510
|
+
if resume_response:
|
|
1511
|
+
return resume_response
|
|
1512
|
+
|
|
1513
|
+
# Check if already shutting down
|
|
1514
|
+
shutdown_response = _check_shutdown_already_in_progress(runtime, state_info)
|
|
1515
|
+
if shutdown_response:
|
|
1516
|
+
return shutdown_response
|
|
1517
|
+
|
|
1518
|
+
# Verify shutdown service is available
|
|
1519
|
+
shutdown_service = getattr(runtime, "shutdown_service", None)
|
|
1520
|
+
if not shutdown_service:
|
|
1521
|
+
logger.warning("[LOCAL_SHUTDOWN] Shutdown service not available (503)")
|
|
1522
|
+
return JSONResponse(
|
|
1523
|
+
status_code=503,
|
|
1524
|
+
content={
|
|
1525
|
+
"status": "error",
|
|
1526
|
+
"reason": "Shutdown service not available",
|
|
1527
|
+
"retry_after_ms": 1000,
|
|
1528
|
+
**state_info,
|
|
1529
|
+
},
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
# Initiate shutdown
|
|
1533
|
+
reason = "Local shutdown requested (Android SmartStartup)"
|
|
1534
|
+
logger.warning(f"[LOCAL_SHUTDOWN] Initiating IMMEDIATE shutdown: {reason}")
|
|
1535
|
+
_initiate_force_shutdown(runtime, reason)
|
|
1536
|
+
|
|
1537
|
+
logger.info("[LOCAL_SHUTDOWN] Shutdown initiated successfully (200)")
|
|
1538
|
+
return JSONResponse(
|
|
1539
|
+
status_code=200,
|
|
1540
|
+
content={
|
|
1541
|
+
"status": "accepted",
|
|
1542
|
+
"reason": reason,
|
|
1543
|
+
**state_info,
|
|
1544
|
+
},
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
# Adapter Management Endpoints
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
@router.get("/adapters", response_model=SuccessResponse[AdapterListResponse])
|
|
1552
|
+
async def list_adapters(
|
|
1553
|
+
request: Request, auth: AuthContext = Depends(require_observer)
|
|
1554
|
+
) -> SuccessResponse[AdapterListResponse]:
|
|
1555
|
+
"""
|
|
1556
|
+
List all loaded adapters.
|
|
1557
|
+
|
|
1558
|
+
Returns information about all currently loaded adapter instances
|
|
1559
|
+
including their type, status, and basic metrics.
|
|
1560
|
+
"""
|
|
1561
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
1562
|
+
if not runtime_control:
|
|
1563
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
1564
|
+
|
|
1565
|
+
try:
|
|
1566
|
+
# Get adapter list from runtime control service
|
|
1567
|
+
adapters = await runtime_control.list_adapters()
|
|
1568
|
+
|
|
1569
|
+
# Convert to response format
|
|
1570
|
+
adapter_statuses = []
|
|
1571
|
+
for adapter in adapters:
|
|
1572
|
+
# Convert AdapterInfo to AdapterStatusSchema
|
|
1573
|
+
# Check status using the enum value (which is lowercase)
|
|
1574
|
+
from ciris_engine.schemas.services.core.runtime import AdapterStatus
|
|
1575
|
+
|
|
1576
|
+
is_running = adapter.status == AdapterStatus.RUNNING or str(adapter.status).lower() == "running"
|
|
1577
|
+
|
|
1578
|
+
config = AdapterConfig(adapter_type=adapter.adapter_type, enabled=is_running, settings={})
|
|
1579
|
+
|
|
1580
|
+
metrics = None
|
|
1581
|
+
if adapter.messages_processed > 0 or adapter.error_count > 0:
|
|
1582
|
+
metrics = AdapterMetrics(
|
|
1583
|
+
messages_processed=adapter.messages_processed,
|
|
1584
|
+
errors_count=adapter.error_count,
|
|
1585
|
+
uptime_seconds=(
|
|
1586
|
+
(datetime.now(timezone.utc) - adapter.started_at).total_seconds() if adapter.started_at else 0
|
|
1587
|
+
),
|
|
1588
|
+
last_error=adapter.last_error,
|
|
1589
|
+
last_error_time=None,
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
adapter_statuses.append(
|
|
1593
|
+
AdapterStatusSchema(
|
|
1594
|
+
adapter_id=adapter.adapter_id,
|
|
1595
|
+
adapter_type=adapter.adapter_type,
|
|
1596
|
+
is_running=is_running,
|
|
1597
|
+
loaded_at=adapter.started_at or datetime.now(timezone.utc),
|
|
1598
|
+
services_registered=[], # Not available from AdapterInfo
|
|
1599
|
+
config_params=config,
|
|
1600
|
+
metrics=metrics, # Pass the AdapterMetrics object directly
|
|
1601
|
+
last_activity=None,
|
|
1602
|
+
tools=adapter.tools, # Include tools information
|
|
1603
|
+
)
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
running_count = sum(1 for a in adapter_statuses if a.is_running)
|
|
1607
|
+
|
|
1608
|
+
response = AdapterListResponse(
|
|
1609
|
+
adapters=adapter_statuses, total_count=len(adapter_statuses), running_count=running_count
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
return SuccessResponse(data=response)
|
|
1613
|
+
|
|
1614
|
+
except ValidationError as e:
|
|
1615
|
+
logger.error(f"Validation error listing adapters: {e}")
|
|
1616
|
+
logger.error(f"Validation errors detail: {e.errors()}")
|
|
1617
|
+
# Return empty list on validation error to avoid breaking GUI
|
|
1618
|
+
return SuccessResponse(data=AdapterListResponse(adapters=[], total_count=0, running_count=0))
|
|
1619
|
+
except Exception as e:
|
|
1620
|
+
logger.error(f"Error listing adapters: {e}")
|
|
1621
|
+
logger.error(f"Error type: {type(e).__name__}")
|
|
1622
|
+
import traceback
|
|
1623
|
+
|
|
1624
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
1625
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
# Module types helper functions
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def _get_core_adapter_info(adapter_type: str) -> ModuleTypeInfo:
|
|
1632
|
+
"""Generate ModuleTypeInfo for a core adapter."""
|
|
1633
|
+
core_adapters: Dict[str, Dict[str, Any]] = {
|
|
1634
|
+
"api": {
|
|
1635
|
+
"name": "API Adapter",
|
|
1636
|
+
"description": "REST API adapter providing HTTP endpoints for CIRIS interaction",
|
|
1637
|
+
"service_types": ["COMMUNICATION", "TOOL", "RUNTIME_CONTROL"],
|
|
1638
|
+
"capabilities": [*COMM_CAPABILITIES, "tool:api", "runtime_control"],
|
|
1639
|
+
"configuration": [
|
|
1640
|
+
ModuleConfigParameter(
|
|
1641
|
+
name="host",
|
|
1642
|
+
param_type="string",
|
|
1643
|
+
default="127.0.0.1",
|
|
1644
|
+
description="Host address to bind to",
|
|
1645
|
+
env_var="CIRIS_API_HOST",
|
|
1646
|
+
required=False,
|
|
1647
|
+
),
|
|
1648
|
+
ModuleConfigParameter(
|
|
1649
|
+
name="port",
|
|
1650
|
+
param_type="integer",
|
|
1651
|
+
default=8000,
|
|
1652
|
+
description="Port to listen on",
|
|
1653
|
+
env_var="CIRIS_API_PORT",
|
|
1654
|
+
required=False,
|
|
1655
|
+
),
|
|
1656
|
+
ModuleConfigParameter(
|
|
1657
|
+
name="debug",
|
|
1658
|
+
param_type="boolean",
|
|
1659
|
+
default=False,
|
|
1660
|
+
description="Enable debug mode",
|
|
1661
|
+
env_var="CIRIS_API_DEBUG",
|
|
1662
|
+
required=False,
|
|
1663
|
+
),
|
|
1664
|
+
],
|
|
1665
|
+
},
|
|
1666
|
+
"cli": {
|
|
1667
|
+
"name": "CLI Adapter",
|
|
1668
|
+
"description": "Command-line interface adapter for interactive terminal sessions",
|
|
1669
|
+
"service_types": ["COMMUNICATION"],
|
|
1670
|
+
"capabilities": COMM_CAPABILITIES,
|
|
1671
|
+
"configuration": [
|
|
1672
|
+
ModuleConfigParameter(
|
|
1673
|
+
name="prompt",
|
|
1674
|
+
param_type="string",
|
|
1675
|
+
default="CIRIS> ",
|
|
1676
|
+
description="CLI prompt string",
|
|
1677
|
+
required=False,
|
|
1678
|
+
),
|
|
1679
|
+
],
|
|
1680
|
+
},
|
|
1681
|
+
"discord": {
|
|
1682
|
+
"name": "Discord Adapter",
|
|
1683
|
+
"description": "Discord bot adapter for community interaction",
|
|
1684
|
+
"service_types": ["COMMUNICATION", "TOOL"],
|
|
1685
|
+
"capabilities": [*COMM_CAPABILITIES, "tool:discord"],
|
|
1686
|
+
"configuration": [
|
|
1687
|
+
ModuleConfigParameter(
|
|
1688
|
+
name="discord_token",
|
|
1689
|
+
param_type="string",
|
|
1690
|
+
description="Discord bot token",
|
|
1691
|
+
env_var="CIRIS_DISCORD_TOKEN",
|
|
1692
|
+
required=True,
|
|
1693
|
+
sensitivity="HIGH",
|
|
1694
|
+
),
|
|
1695
|
+
ModuleConfigParameter(
|
|
1696
|
+
name="guild_id",
|
|
1697
|
+
param_type="string",
|
|
1698
|
+
description="Discord guild ID to operate in",
|
|
1699
|
+
env_var="CIRIS_DISCORD_GUILD_ID",
|
|
1700
|
+
required=False,
|
|
1701
|
+
),
|
|
1702
|
+
ModuleConfigParameter(
|
|
1703
|
+
name="channel_id",
|
|
1704
|
+
param_type="string",
|
|
1705
|
+
description="Default channel ID for messages",
|
|
1706
|
+
env_var="CIRIS_DISCORD_CHANNEL_ID",
|
|
1707
|
+
required=False,
|
|
1708
|
+
),
|
|
1709
|
+
],
|
|
1710
|
+
},
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
adapter_info = core_adapters.get(adapter_type, {})
|
|
1714
|
+
return ModuleTypeInfo(
|
|
1715
|
+
module_id=adapter_type,
|
|
1716
|
+
name=adapter_info.get("name", adapter_type.title()),
|
|
1717
|
+
version="1.0.0",
|
|
1718
|
+
description=adapter_info.get("description", f"Core {adapter_type} adapter"),
|
|
1719
|
+
author="CIRIS Team",
|
|
1720
|
+
module_source="core",
|
|
1721
|
+
service_types=adapter_info.get("service_types", []),
|
|
1722
|
+
capabilities=adapter_info.get("capabilities", []),
|
|
1723
|
+
configuration_schema=adapter_info.get("configuration", []),
|
|
1724
|
+
requires_external_deps=adapter_type == "discord",
|
|
1725
|
+
external_dependencies={"discord.py": ">=2.0.0"} if adapter_type == "discord" else {},
|
|
1726
|
+
is_mock=False,
|
|
1727
|
+
safe_domain=None,
|
|
1728
|
+
prohibited=[],
|
|
1729
|
+
metadata=None,
|
|
1730
|
+
)
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
def _check_platform_requirements_satisfied(platform_requirements: List[str]) -> bool:
|
|
1734
|
+
"""Check if current platform satisfies the given requirements.
|
|
1735
|
+
|
|
1736
|
+
Args:
|
|
1737
|
+
platform_requirements: List of requirement strings
|
|
1738
|
+
|
|
1739
|
+
Returns:
|
|
1740
|
+
True if platform satisfies all requirements, False otherwise
|
|
1741
|
+
"""
|
|
1742
|
+
if not platform_requirements:
|
|
1743
|
+
return True
|
|
1744
|
+
|
|
1745
|
+
from ciris_engine.logic.utils.platform_detection import detect_platform_capabilities
|
|
1746
|
+
from ciris_engine.schemas.platform import PlatformRequirement
|
|
1747
|
+
|
|
1748
|
+
try:
|
|
1749
|
+
caps = detect_platform_capabilities()
|
|
1750
|
+
req_enums = []
|
|
1751
|
+
for req_str in platform_requirements:
|
|
1752
|
+
try:
|
|
1753
|
+
req_enums.append(PlatformRequirement(req_str))
|
|
1754
|
+
except ValueError:
|
|
1755
|
+
pass # Unknown requirement, skip
|
|
1756
|
+
return caps.satisfies(req_enums)
|
|
1757
|
+
except Exception:
|
|
1758
|
+
return False
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def _should_filter_adapter(manifest_data: Dict[str, Any], filter_by_platform: bool = True) -> bool:
|
|
1762
|
+
"""Check if an adapter should be filtered from public listings.
|
|
1763
|
+
|
|
1764
|
+
Filters out:
|
|
1765
|
+
- Mock adapters (module.MOCK: true)
|
|
1766
|
+
- Library modules (metadata.type: "library")
|
|
1767
|
+
- Modules with no services (empty services array)
|
|
1768
|
+
- Common/utility modules (name ends with _common)
|
|
1769
|
+
- Adapters that don't meet platform requirements (if filter_by_platform=True)
|
|
1770
|
+
|
|
1771
|
+
Args:
|
|
1772
|
+
manifest_data: The manifest JSON data
|
|
1773
|
+
filter_by_platform: If True, also filter adapters that don't meet platform requirements
|
|
1774
|
+
|
|
1775
|
+
Returns:
|
|
1776
|
+
True if the adapter should be filtered (hidden), False otherwise
|
|
1777
|
+
"""
|
|
1778
|
+
module_info = manifest_data.get("module", {})
|
|
1779
|
+
metadata = manifest_data.get("metadata", {})
|
|
1780
|
+
services = manifest_data.get("services", [])
|
|
1781
|
+
|
|
1782
|
+
# Filter mock adapters
|
|
1783
|
+
if module_info.get("MOCK", False):
|
|
1784
|
+
return True
|
|
1785
|
+
|
|
1786
|
+
# Filter library modules
|
|
1787
|
+
if isinstance(metadata, dict) and metadata.get("type") == "library":
|
|
1788
|
+
return True
|
|
1789
|
+
|
|
1790
|
+
# Filter modules with no services (utility/common modules)
|
|
1791
|
+
if not services:
|
|
1792
|
+
return True
|
|
1793
|
+
|
|
1794
|
+
# Filter common modules by name pattern
|
|
1795
|
+
module_name = module_info.get("name", "")
|
|
1796
|
+
if module_name.endswith("_common") or module_name.endswith("common"):
|
|
1797
|
+
return True
|
|
1798
|
+
|
|
1799
|
+
# Filter adapters that don't meet platform requirements
|
|
1800
|
+
if filter_by_platform:
|
|
1801
|
+
platform_requirements = manifest_data.get("platform_requirements", [])
|
|
1802
|
+
if not _check_platform_requirements_satisfied(platform_requirements):
|
|
1803
|
+
return True
|
|
1804
|
+
|
|
1805
|
+
return False
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def _extract_service_types(manifest_data: Dict[str, Any]) -> List[str]:
|
|
1809
|
+
"""Extract unique service types from manifest services list."""
|
|
1810
|
+
service_types = []
|
|
1811
|
+
for svc in manifest_data.get("services", []):
|
|
1812
|
+
svc_type = svc.get("type", "")
|
|
1813
|
+
if svc_type and svc_type not in service_types:
|
|
1814
|
+
service_types.append(svc_type)
|
|
1815
|
+
return service_types
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
def _parse_config_parameters(manifest_data: Dict[str, Any]) -> List[ModuleConfigParameter]:
|
|
1819
|
+
"""Parse configuration parameters from manifest."""
|
|
1820
|
+
config_params: List[ModuleConfigParameter] = []
|
|
1821
|
+
for param_name, param_data in manifest_data.get("configuration", {}).items():
|
|
1822
|
+
if isinstance(param_data, dict):
|
|
1823
|
+
config_params.append(
|
|
1824
|
+
ModuleConfigParameter(
|
|
1825
|
+
name=param_name,
|
|
1826
|
+
param_type=param_data.get("type", "string"),
|
|
1827
|
+
default=param_data.get("default"),
|
|
1828
|
+
description=param_data.get("description", ""),
|
|
1829
|
+
env_var=param_data.get("env"),
|
|
1830
|
+
required=param_data.get("required", True),
|
|
1831
|
+
sensitivity=param_data.get("sensitivity"),
|
|
1832
|
+
)
|
|
1833
|
+
)
|
|
1834
|
+
return config_params
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
def _parse_manifest_to_module_info(manifest_data: Dict[str, Any], module_id: str) -> ModuleTypeInfo:
|
|
1838
|
+
"""Parse a module manifest into a ModuleTypeInfo."""
|
|
1839
|
+
module_info = manifest_data.get("module", {})
|
|
1840
|
+
|
|
1841
|
+
# Extract service types and config params using helpers
|
|
1842
|
+
service_types = _extract_service_types(manifest_data)
|
|
1843
|
+
config_params = _parse_config_parameters(manifest_data)
|
|
1844
|
+
|
|
1845
|
+
# Extract external dependencies
|
|
1846
|
+
deps = manifest_data.get("dependencies", {})
|
|
1847
|
+
external_deps = deps.get("external", {}) if isinstance(deps, dict) else {}
|
|
1848
|
+
external_deps = external_deps or {}
|
|
1849
|
+
|
|
1850
|
+
# Extract metadata
|
|
1851
|
+
metadata = manifest_data.get("metadata", {})
|
|
1852
|
+
safe_domain = metadata.get("safe_domain") if isinstance(metadata, dict) else None
|
|
1853
|
+
prohibited = metadata.get("prohibited", []) if isinstance(metadata, dict) else []
|
|
1854
|
+
|
|
1855
|
+
# Extract platform requirements
|
|
1856
|
+
platform_requirements = manifest_data.get("platform_requirements", [])
|
|
1857
|
+
platform_requirements_rationale = manifest_data.get("platform_requirements_rationale")
|
|
1858
|
+
|
|
1859
|
+
# Check platform availability using shared helper
|
|
1860
|
+
platform_available = _check_platform_requirements_satisfied(platform_requirements)
|
|
1861
|
+
|
|
1862
|
+
return ModuleTypeInfo(
|
|
1863
|
+
module_id=module_id,
|
|
1864
|
+
name=module_info.get("name", module_id),
|
|
1865
|
+
version=module_info.get("version", "1.0.0"),
|
|
1866
|
+
description=module_info.get("description", ""),
|
|
1867
|
+
author=module_info.get("author", "Unknown"),
|
|
1868
|
+
module_source="modular",
|
|
1869
|
+
service_types=service_types,
|
|
1870
|
+
capabilities=manifest_data.get("capabilities", []),
|
|
1871
|
+
configuration_schema=config_params,
|
|
1872
|
+
requires_external_deps=bool(external_deps),
|
|
1873
|
+
external_dependencies=external_deps,
|
|
1874
|
+
is_mock=module_info.get("MOCK", False) or module_info.get("is_mock", False),
|
|
1875
|
+
safe_domain=safe_domain if isinstance(safe_domain, str) else None,
|
|
1876
|
+
prohibited=prohibited if isinstance(prohibited, list) else [],
|
|
1877
|
+
metadata=metadata if isinstance(metadata, dict) else None,
|
|
1878
|
+
platform_requirements=platform_requirements,
|
|
1879
|
+
platform_requirements_rationale=platform_requirements_rationale,
|
|
1880
|
+
platform_available=platform_available,
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
# Entry point group for adapter discovery (defined in setup.py)
|
|
1885
|
+
ADAPTER_ENTRY_POINT_GROUP = "ciris.adapters"
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
async def _read_manifest_async(manifest_path: Path) -> Optional[Dict[str, Any]]:
|
|
1889
|
+
"""Read and parse a manifest file asynchronously."""
|
|
1890
|
+
import aiofiles
|
|
1891
|
+
|
|
1892
|
+
try:
|
|
1893
|
+
async with aiofiles.open(manifest_path, mode="r") as f:
|
|
1894
|
+
content = await f.read()
|
|
1895
|
+
result: Dict[str, Any] = json.loads(content)
|
|
1896
|
+
return result
|
|
1897
|
+
except Exception:
|
|
1898
|
+
return None
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
def _try_load_service_manifest(service_name: str, apply_filter: bool = True) -> Optional[ModuleTypeInfo]:
|
|
1902
|
+
"""Try to load a modular service manifest by name.
|
|
1903
|
+
|
|
1904
|
+
Args:
|
|
1905
|
+
service_name: Name of the service to load
|
|
1906
|
+
apply_filter: If True, filter out mock/common/library modules
|
|
1907
|
+
|
|
1908
|
+
Returns:
|
|
1909
|
+
ModuleTypeInfo if found and not filtered, None otherwise
|
|
1910
|
+
"""
|
|
1911
|
+
import importlib
|
|
1912
|
+
|
|
1913
|
+
try:
|
|
1914
|
+
submodule = importlib.import_module(f"ciris_adapters.{service_name}")
|
|
1915
|
+
if not hasattr(submodule, "__path__"):
|
|
1916
|
+
return None
|
|
1917
|
+
manifest_file = Path(submodule.__path__[0]) / MANIFEST_FILENAME
|
|
1918
|
+
if not manifest_file.exists():
|
|
1919
|
+
return None
|
|
1920
|
+
with open(manifest_file) as f:
|
|
1921
|
+
manifest_data = json.load(f)
|
|
1922
|
+
|
|
1923
|
+
# Filter out mock/common/library modules from public listings
|
|
1924
|
+
if apply_filter and _should_filter_adapter(manifest_data):
|
|
1925
|
+
logger.debug("Filtering adapter %s from listings (mock/common/library)", service_name)
|
|
1926
|
+
return None
|
|
1927
|
+
|
|
1928
|
+
return _parse_manifest_to_module_info(manifest_data, service_name)
|
|
1929
|
+
except Exception as e:
|
|
1930
|
+
logger.debug("Service %s not available: %s", service_name, e)
|
|
1931
|
+
return None
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
async def _discover_services_from_directory(services_base: Path) -> List[ModuleTypeInfo]:
|
|
1935
|
+
"""Discover modular services by iterating the services directory.
|
|
1936
|
+
|
|
1937
|
+
Filters out mock, common, and library modules from the listing.
|
|
1938
|
+
"""
|
|
1939
|
+
adapters: List[ModuleTypeInfo] = []
|
|
1940
|
+
|
|
1941
|
+
for item in services_base.iterdir():
|
|
1942
|
+
if not item.is_dir() or item.name.startswith("_"):
|
|
1943
|
+
continue
|
|
1944
|
+
|
|
1945
|
+
# Try importlib-based loading first (Android compatibility)
|
|
1946
|
+
# Filter is applied inside _try_load_service_manifest
|
|
1947
|
+
module_info = _try_load_service_manifest(item.name)
|
|
1948
|
+
if module_info:
|
|
1949
|
+
adapters.append(module_info)
|
|
1950
|
+
logger.debug("Discovered modular service: %s", item.name)
|
|
1951
|
+
continue
|
|
1952
|
+
|
|
1953
|
+
# Fallback to direct file access
|
|
1954
|
+
manifest_path = item / MANIFEST_FILENAME
|
|
1955
|
+
manifest_data = await _read_manifest_async(manifest_path)
|
|
1956
|
+
if manifest_data:
|
|
1957
|
+
# Apply filter for direct file access path
|
|
1958
|
+
if _should_filter_adapter(manifest_data):
|
|
1959
|
+
logger.debug("Filtering adapter %s from listings (mock/common/library)", item.name)
|
|
1960
|
+
continue
|
|
1961
|
+
|
|
1962
|
+
module_info = _parse_manifest_to_module_info(manifest_data, item.name)
|
|
1963
|
+
adapters.append(module_info)
|
|
1964
|
+
logger.debug("Discovered modular service (direct): %s", item.name)
|
|
1965
|
+
|
|
1966
|
+
return adapters
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
async def _discover_services_via_entry_points() -> List[ModuleTypeInfo]:
|
|
1970
|
+
"""Discover modular services via importlib.metadata entry points.
|
|
1971
|
+
|
|
1972
|
+
This is the preferred discovery method as it works across all platforms
|
|
1973
|
+
including Android where filesystem iteration may fail. Entry points are
|
|
1974
|
+
defined in setup.py under the 'ciris.adapters' group.
|
|
1975
|
+
|
|
1976
|
+
Note: This function is async for API consistency even though the underlying
|
|
1977
|
+
operations are synchronous. This allows uniform await usage in callers.
|
|
1978
|
+
"""
|
|
1979
|
+
from importlib.metadata import entry_points
|
|
1980
|
+
from typing import Iterable
|
|
1981
|
+
|
|
1982
|
+
adapters: List[ModuleTypeInfo] = []
|
|
1983
|
+
|
|
1984
|
+
try:
|
|
1985
|
+
# Get entry points - API varies by Python version
|
|
1986
|
+
eps = entry_points()
|
|
1987
|
+
|
|
1988
|
+
# Try the modern API first (Python 3.10+)
|
|
1989
|
+
adapter_eps: Iterable[Any]
|
|
1990
|
+
if hasattr(eps, "select"):
|
|
1991
|
+
# Python 3.10+ with SelectableGroups
|
|
1992
|
+
adapter_eps = eps.select(group=ADAPTER_ENTRY_POINT_GROUP)
|
|
1993
|
+
elif isinstance(eps, dict):
|
|
1994
|
+
# Python 3.9 style dict-like access
|
|
1995
|
+
adapter_eps = eps.get(ADAPTER_ENTRY_POINT_GROUP, [])
|
|
1996
|
+
else:
|
|
1997
|
+
# Fallback - try to iterate or access as needed
|
|
1998
|
+
adapter_eps = getattr(eps, ADAPTER_ENTRY_POINT_GROUP, [])
|
|
1999
|
+
|
|
2000
|
+
for ep in adapter_eps:
|
|
2001
|
+
module_info = _try_load_service_manifest(ep.name)
|
|
2002
|
+
if module_info:
|
|
2003
|
+
adapters.append(module_info)
|
|
2004
|
+
logger.debug("Discovered adapter via entry point: %s", ep.name)
|
|
2005
|
+
|
|
2006
|
+
except Exception as e:
|
|
2007
|
+
logger.warning("Entry point discovery failed: %s", e)
|
|
2008
|
+
|
|
2009
|
+
return adapters
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
async def _discover_adapters() -> List[ModuleTypeInfo]:
|
|
2013
|
+
"""Discover all available modular services.
|
|
2014
|
+
|
|
2015
|
+
Uses a fallback chain:
|
|
2016
|
+
1. Try filesystem iteration (fastest, works in dev)
|
|
2017
|
+
2. Fall back to entry points (works on Android and installed packages)
|
|
2018
|
+
"""
|
|
2019
|
+
try:
|
|
2020
|
+
import ciris_adapters
|
|
2021
|
+
|
|
2022
|
+
if not hasattr(ciris_adapters, "__path__"):
|
|
2023
|
+
return await _discover_services_via_entry_points()
|
|
2024
|
+
|
|
2025
|
+
services_base = Path(ciris_adapters.__path__[0])
|
|
2026
|
+
logger.debug("Modular services base path: %s", services_base)
|
|
2027
|
+
|
|
2028
|
+
try:
|
|
2029
|
+
return await _discover_services_from_directory(services_base)
|
|
2030
|
+
except OSError as e:
|
|
2031
|
+
logger.debug("iterdir failed (%s), falling back to entry points", e)
|
|
2032
|
+
return await _discover_services_via_entry_points()
|
|
2033
|
+
|
|
2034
|
+
except ImportError as e:
|
|
2035
|
+
logger.debug("ciris_adapters not available: %s", e)
|
|
2036
|
+
return await _discover_services_via_entry_points()
|
|
2037
|
+
|
|
2038
|
+
|
|
2039
|
+
@router.get("/adapters/types", response_model=SuccessResponse[ModuleTypesResponse])
|
|
2040
|
+
async def list_module_types(
|
|
2041
|
+
request: Request, auth: AuthContext = Depends(require_observer)
|
|
2042
|
+
) -> SuccessResponse[ModuleTypesResponse]:
|
|
2043
|
+
"""
|
|
2044
|
+
List all available module/adapter types.
|
|
2045
|
+
|
|
2046
|
+
Returns both core adapters (api, cli, discord) and modular services
|
|
2047
|
+
(mcp_client, mcp_server, reddit, etc.) with their typed configuration schemas.
|
|
2048
|
+
|
|
2049
|
+
This endpoint is useful for:
|
|
2050
|
+
- Dynamic adapter loading UI
|
|
2051
|
+
- Configuration validation
|
|
2052
|
+
- Capability discovery
|
|
2053
|
+
|
|
2054
|
+
Requires OBSERVER role.
|
|
2055
|
+
"""
|
|
2056
|
+
try:
|
|
2057
|
+
# Get core adapters
|
|
2058
|
+
core_adapter_types = ["api", "cli", "discord"]
|
|
2059
|
+
core_modules = [_get_core_adapter_info(t) for t in core_adapter_types]
|
|
2060
|
+
|
|
2061
|
+
# Discover modular services
|
|
2062
|
+
adapters = await _discover_adapters()
|
|
2063
|
+
|
|
2064
|
+
response = ModuleTypesResponse(
|
|
2065
|
+
core_modules=core_modules,
|
|
2066
|
+
adapters=adapters,
|
|
2067
|
+
total_core=len(core_modules),
|
|
2068
|
+
total_adapters=len(adapters),
|
|
2069
|
+
)
|
|
2070
|
+
|
|
2071
|
+
return SuccessResponse(data=response)
|
|
2072
|
+
|
|
2073
|
+
except Exception as e:
|
|
2074
|
+
logger.error("Error listing module types: %s", e)
|
|
2075
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2076
|
+
|
|
2077
|
+
|
|
2078
|
+
# NOTE: Static routes like /adapters/persisted and /adapters/configurable must come BEFORE
|
|
2079
|
+
# the parametrized route /adapters/{adapter_id} to avoid being captured by the path parameter.
|
|
2080
|
+
|
|
2081
|
+
|
|
2082
|
+
class PersistedConfigsResponse(BaseModel):
|
|
2083
|
+
"""Response for persisted adapter configurations."""
|
|
2084
|
+
|
|
2085
|
+
persisted_configs: Dict[str, Dict[str, Any]] = Field(
|
|
2086
|
+
default_factory=dict,
|
|
2087
|
+
description="Map of adapter_type to configuration data",
|
|
2088
|
+
)
|
|
2089
|
+
count: int = Field(..., description="Number of persisted configurations")
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
@router.get(
|
|
2093
|
+
"/adapters/persisted",
|
|
2094
|
+
response_model=SuccessResponse[PersistedConfigsResponse],
|
|
2095
|
+
)
|
|
2096
|
+
async def list_persisted_configurations(
|
|
2097
|
+
request: Request,
|
|
2098
|
+
auth: AuthContext = Depends(require_admin),
|
|
2099
|
+
) -> SuccessResponse[PersistedConfigsResponse]:
|
|
2100
|
+
"""
|
|
2101
|
+
List all persisted adapter configurations.
|
|
2102
|
+
|
|
2103
|
+
Returns configurations that are set to load on startup.
|
|
2104
|
+
|
|
2105
|
+
Requires ADMIN role.
|
|
2106
|
+
"""
|
|
2107
|
+
try:
|
|
2108
|
+
adapter_config_service = getattr(request.app.state, "adapter_configuration_service", None)
|
|
2109
|
+
config_service = getattr(request.app.state, "config_service", None)
|
|
2110
|
+
|
|
2111
|
+
if not adapter_config_service:
|
|
2112
|
+
raise HTTPException(status_code=503, detail=ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE)
|
|
2113
|
+
|
|
2114
|
+
persisted_configs: Dict[str, Dict[str, Any]] = {}
|
|
2115
|
+
if config_service:
|
|
2116
|
+
persisted_configs = await adapter_config_service.load_persisted_configs(config_service)
|
|
2117
|
+
|
|
2118
|
+
response = PersistedConfigsResponse(
|
|
2119
|
+
persisted_configs=persisted_configs,
|
|
2120
|
+
count=len(persisted_configs),
|
|
2121
|
+
)
|
|
2122
|
+
return SuccessResponse(data=response)
|
|
2123
|
+
|
|
2124
|
+
except HTTPException:
|
|
2125
|
+
raise
|
|
2126
|
+
except Exception as e:
|
|
2127
|
+
logger.error(f"Error listing persisted configurations: {e}")
|
|
2128
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
class RemovePersistedResponse(BaseModel):
|
|
2132
|
+
"""Response for removing a persisted configuration."""
|
|
2133
|
+
|
|
2134
|
+
success: bool = Field(..., description="Whether the removal succeeded")
|
|
2135
|
+
adapter_type: str = Field(..., description="Adapter type that was removed")
|
|
2136
|
+
message: str = Field(..., description="Status message")
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
@router.delete(
|
|
2140
|
+
"/adapters/{adapter_type}/persisted",
|
|
2141
|
+
response_model=SuccessResponse[RemovePersistedResponse],
|
|
2142
|
+
)
|
|
2143
|
+
async def remove_persisted_configuration(
|
|
2144
|
+
adapter_type: str,
|
|
2145
|
+
request: Request,
|
|
2146
|
+
auth: AuthContext = Depends(require_admin),
|
|
2147
|
+
) -> SuccessResponse[RemovePersistedResponse]:
|
|
2148
|
+
"""
|
|
2149
|
+
Remove a persisted adapter configuration.
|
|
2150
|
+
|
|
2151
|
+
This prevents the adapter from being automatically loaded on startup.
|
|
2152
|
+
|
|
2153
|
+
Requires ADMIN role.
|
|
2154
|
+
"""
|
|
2155
|
+
try:
|
|
2156
|
+
adapter_config_service = getattr(request.app.state, "adapter_configuration_service", None)
|
|
2157
|
+
config_service = getattr(request.app.state, "config_service", None)
|
|
2158
|
+
|
|
2159
|
+
if not adapter_config_service:
|
|
2160
|
+
raise HTTPException(status_code=503, detail=ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE)
|
|
2161
|
+
|
|
2162
|
+
if not config_service:
|
|
2163
|
+
raise HTTPException(status_code=503, detail="Config service not available")
|
|
2164
|
+
|
|
2165
|
+
success = await adapter_config_service.remove_persisted_config(
|
|
2166
|
+
adapter_type=adapter_type,
|
|
2167
|
+
config_service=config_service,
|
|
2168
|
+
)
|
|
2169
|
+
|
|
2170
|
+
if success:
|
|
2171
|
+
message = f"Removed persisted configuration for {adapter_type}"
|
|
2172
|
+
else:
|
|
2173
|
+
message = f"No persisted configuration found for {adapter_type}"
|
|
2174
|
+
|
|
2175
|
+
response = RemovePersistedResponse(
|
|
2176
|
+
success=success,
|
|
2177
|
+
adapter_type=adapter_type,
|
|
2178
|
+
message=message,
|
|
2179
|
+
)
|
|
2180
|
+
return SuccessResponse(data=response)
|
|
2181
|
+
|
|
2182
|
+
except HTTPException:
|
|
2183
|
+
raise
|
|
2184
|
+
except Exception as e:
|
|
2185
|
+
logger.error(f"Error removing persisted configuration: {e}")
|
|
2186
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
def _get_adapter_config_service(request: Request) -> Any:
|
|
2190
|
+
"""Get AdapterConfigurationService from app state."""
|
|
2191
|
+
service = getattr(request.app.state, "adapter_configuration_service", None)
|
|
2192
|
+
if not service:
|
|
2193
|
+
raise HTTPException(status_code=503, detail=ERROR_ADAPTER_CONFIG_SERVICE_NOT_AVAILABLE)
|
|
2194
|
+
return service
|
|
2195
|
+
|
|
2196
|
+
|
|
2197
|
+
@router.get("/adapters/configurable", response_model=SuccessResponse[ConfigurableAdaptersResponse])
|
|
2198
|
+
async def list_configurable_adapters(
|
|
2199
|
+
request: Request, auth: AuthContext = Depends(require_admin)
|
|
2200
|
+
) -> SuccessResponse[ConfigurableAdaptersResponse]:
|
|
2201
|
+
"""
|
|
2202
|
+
List adapters that support interactive configuration.
|
|
2203
|
+
|
|
2204
|
+
Returns information about all adapters that have defined interactive
|
|
2205
|
+
configuration workflows, including their workflow types and step counts.
|
|
2206
|
+
|
|
2207
|
+
Requires ADMIN role.
|
|
2208
|
+
"""
|
|
2209
|
+
try:
|
|
2210
|
+
config_service = _get_adapter_config_service(request)
|
|
2211
|
+
adapter_types = config_service.get_configurable_adapters()
|
|
2212
|
+
|
|
2213
|
+
# Build detailed info for each adapter
|
|
2214
|
+
adapters = []
|
|
2215
|
+
for adapter_type in adapter_types:
|
|
2216
|
+
manifest = config_service._adapter_manifests.get(adapter_type)
|
|
2217
|
+
if not manifest:
|
|
2218
|
+
continue
|
|
2219
|
+
|
|
2220
|
+
# Check if any step is OAuth
|
|
2221
|
+
requires_oauth = any(step.step_type == "oauth" for step in manifest.steps)
|
|
2222
|
+
|
|
2223
|
+
adapters.append(
|
|
2224
|
+
ConfigurableAdapterInfo(
|
|
2225
|
+
adapter_type=adapter_type,
|
|
2226
|
+
name=adapter_type.replace("_", " ").title(),
|
|
2227
|
+
description=f"Interactive configuration for {adapter_type}",
|
|
2228
|
+
workflow_type=manifest.workflow_type,
|
|
2229
|
+
step_count=len(manifest.steps),
|
|
2230
|
+
requires_oauth=requires_oauth,
|
|
2231
|
+
steps=[
|
|
2232
|
+
ConfigStepInfo(
|
|
2233
|
+
step_id=step.step_id,
|
|
2234
|
+
step_type=step.step_type,
|
|
2235
|
+
title=step.title,
|
|
2236
|
+
description=step.description,
|
|
2237
|
+
optional=getattr(step, "optional", False),
|
|
2238
|
+
)
|
|
2239
|
+
for step in manifest.steps
|
|
2240
|
+
],
|
|
2241
|
+
)
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
response = ConfigurableAdaptersResponse(adapters=adapters, total_count=len(adapters))
|
|
2245
|
+
return SuccessResponse(data=response)
|
|
2246
|
+
|
|
2247
|
+
except HTTPException:
|
|
2248
|
+
raise
|
|
2249
|
+
except Exception as e:
|
|
2250
|
+
logger.error(f"Error listing configurable adapters: {e}")
|
|
2251
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
@router.get("/adapters/{adapter_id}", response_model=SuccessResponse[AdapterStatusSchema])
|
|
2255
|
+
async def get_adapter_status(
|
|
2256
|
+
adapter_id: str, request: Request, auth: AuthContext = Depends(require_observer)
|
|
2257
|
+
) -> SuccessResponse[AdapterStatusSchema]:
|
|
2258
|
+
"""
|
|
2259
|
+
Get detailed status of a specific adapter.
|
|
2260
|
+
|
|
2261
|
+
Returns comprehensive information about an adapter instance
|
|
2262
|
+
including configuration, metrics, and service registrations.
|
|
2263
|
+
"""
|
|
2264
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
2265
|
+
if not runtime_control:
|
|
2266
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
2267
|
+
|
|
2268
|
+
try:
|
|
2269
|
+
# Get adapter info from runtime control service
|
|
2270
|
+
adapter_info = await runtime_control.get_adapter_info(adapter_id)
|
|
2271
|
+
|
|
2272
|
+
if not adapter_info:
|
|
2273
|
+
raise HTTPException(status_code=404, detail=f"Adapter '{adapter_id}' not found")
|
|
2274
|
+
|
|
2275
|
+
# Debug logging
|
|
2276
|
+
logger.info(f"Adapter info type: {type(adapter_info)}, value: {adapter_info}")
|
|
2277
|
+
|
|
2278
|
+
# Convert to response format
|
|
2279
|
+
metrics_dict = None
|
|
2280
|
+
if adapter_info.messages_processed > 0 or adapter_info.error_count > 0:
|
|
2281
|
+
metrics = AdapterMetrics(
|
|
2282
|
+
messages_processed=adapter_info.messages_processed,
|
|
2283
|
+
errors_count=adapter_info.error_count,
|
|
2284
|
+
uptime_seconds=(
|
|
2285
|
+
(datetime.now(timezone.utc) - adapter_info.started_at).total_seconds()
|
|
2286
|
+
if adapter_info.started_at
|
|
2287
|
+
else 0
|
|
2288
|
+
),
|
|
2289
|
+
last_error=adapter_info.last_error,
|
|
2290
|
+
last_error_time=None,
|
|
2291
|
+
)
|
|
2292
|
+
metrics_dict = metrics.__dict__
|
|
2293
|
+
|
|
2294
|
+
status = AdapterStatusSchema(
|
|
2295
|
+
adapter_id=adapter_info.adapter_id,
|
|
2296
|
+
adapter_type=adapter_info.adapter_type,
|
|
2297
|
+
is_running=adapter_info.status == "RUNNING",
|
|
2298
|
+
loaded_at=adapter_info.started_at,
|
|
2299
|
+
services_registered=[], # Not exposed via AdapterInfo
|
|
2300
|
+
config_params=AdapterConfig(adapter_type=adapter_info.adapter_type, enabled=True, settings={}),
|
|
2301
|
+
metrics=metrics_dict,
|
|
2302
|
+
last_activity=None,
|
|
2303
|
+
tools=adapter_info.tools, # Include tools information
|
|
2304
|
+
)
|
|
2305
|
+
|
|
2306
|
+
return SuccessResponse(data=status)
|
|
2307
|
+
|
|
2308
|
+
except HTTPException:
|
|
2309
|
+
raise
|
|
2310
|
+
except Exception as e:
|
|
2311
|
+
logger.error(f"Error getting adapter status: {e}")
|
|
2312
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
@router.post("/adapters/{adapter_type}", response_model=SuccessResponse[AdapterOperationResult])
|
|
2316
|
+
async def load_adapter(
|
|
2317
|
+
adapter_type: str,
|
|
2318
|
+
body: AdapterActionRequest,
|
|
2319
|
+
request: Request,
|
|
2320
|
+
adapter_id: Optional[str] = None,
|
|
2321
|
+
auth: AuthContext = Depends(require_admin),
|
|
2322
|
+
) -> SuccessResponse[AdapterOperationResult]:
|
|
2323
|
+
"""
|
|
2324
|
+
Load a new adapter instance.
|
|
2325
|
+
|
|
2326
|
+
Dynamically loads and starts a new adapter of the specified type.
|
|
2327
|
+
Requires ADMIN role.
|
|
2328
|
+
|
|
2329
|
+
Adapter types: cli, api, discord, mcp, mcp_server
|
|
2330
|
+
|
|
2331
|
+
Args:
|
|
2332
|
+
adapter_type: Type of adapter to load
|
|
2333
|
+
adapter_id: Optional unique ID for the adapter (auto-generated if not provided)
|
|
2334
|
+
"""
|
|
2335
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
2336
|
+
if not runtime_control:
|
|
2337
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
2338
|
+
|
|
2339
|
+
try:
|
|
2340
|
+
# Generate adapter ID if not provided
|
|
2341
|
+
import uuid
|
|
2342
|
+
|
|
2343
|
+
if not adapter_id:
|
|
2344
|
+
adapter_id = f"{adapter_type}_{uuid.uuid4().hex[:8]}"
|
|
2345
|
+
|
|
2346
|
+
logger.info(f"[LOAD_ADAPTER] Loading adapter: type={adapter_type}, id={adapter_id}")
|
|
2347
|
+
logger.debug(f"[LOAD_ADAPTER] Config: {body.config}, auto_start={body.auto_start}")
|
|
2348
|
+
|
|
2349
|
+
result = await runtime_control.load_adapter(
|
|
2350
|
+
adapter_type=adapter_type, adapter_id=adapter_id, config=body.config, auto_start=body.auto_start
|
|
2351
|
+
)
|
|
2352
|
+
|
|
2353
|
+
logger.info(
|
|
2354
|
+
f"[LOAD_ADAPTER] Result: success={result.success}, adapter_id={result.adapter_id}, error={result.error}"
|
|
2355
|
+
)
|
|
2356
|
+
|
|
2357
|
+
# Convert response
|
|
2358
|
+
response = AdapterOperationResult(
|
|
2359
|
+
success=result.success,
|
|
2360
|
+
adapter_id=result.adapter_id,
|
|
2361
|
+
adapter_type=adapter_type,
|
|
2362
|
+
message=result.error if not result.success else f"Adapter {result.adapter_id} loaded successfully",
|
|
2363
|
+
error=result.error,
|
|
2364
|
+
details={"timestamp": result.timestamp.isoformat()},
|
|
2365
|
+
)
|
|
2366
|
+
|
|
2367
|
+
return SuccessResponse(data=response)
|
|
2368
|
+
|
|
2369
|
+
except Exception as e:
|
|
2370
|
+
logger.error(f"[LOAD_ADAPTER] Error loading adapter type={adapter_type}, id={adapter_id}: {e}", exc_info=True)
|
|
2371
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2372
|
+
|
|
2373
|
+
|
|
2374
|
+
@router.delete("/adapters/{adapter_id}", response_model=SuccessResponse[AdapterOperationResult])
|
|
2375
|
+
async def unload_adapter(
|
|
2376
|
+
adapter_id: str, request: Request, auth: AuthContext = Depends(require_admin)
|
|
2377
|
+
) -> SuccessResponse[AdapterOperationResult]:
|
|
2378
|
+
"""
|
|
2379
|
+
Unload an adapter instance.
|
|
2380
|
+
|
|
2381
|
+
Stops and removes an adapter from the runtime.
|
|
2382
|
+
Will fail if it's the last communication-capable adapter.
|
|
2383
|
+
Requires ADMIN role.
|
|
2384
|
+
"""
|
|
2385
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
2386
|
+
if not runtime_control:
|
|
2387
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
2388
|
+
|
|
2389
|
+
try:
|
|
2390
|
+
# Unload adapter through runtime control service
|
|
2391
|
+
result = await runtime_control.unload_adapter(
|
|
2392
|
+
adapter_id=adapter_id, force=False # Never force, respect safety checks
|
|
2393
|
+
)
|
|
2394
|
+
|
|
2395
|
+
# Log failures explicitly
|
|
2396
|
+
if not result.success:
|
|
2397
|
+
logger.error(f"Adapter unload failed: {result.error}")
|
|
2398
|
+
|
|
2399
|
+
# Convert response
|
|
2400
|
+
response = AdapterOperationResult(
|
|
2401
|
+
success=result.success,
|
|
2402
|
+
adapter_id=result.adapter_id,
|
|
2403
|
+
adapter_type=result.adapter_type,
|
|
2404
|
+
message=result.error if not result.success else f"Adapter {result.adapter_id} unloaded successfully",
|
|
2405
|
+
error=result.error,
|
|
2406
|
+
details={"timestamp": result.timestamp.isoformat()},
|
|
2407
|
+
)
|
|
2408
|
+
|
|
2409
|
+
return SuccessResponse(data=response)
|
|
2410
|
+
|
|
2411
|
+
except Exception as e:
|
|
2412
|
+
logger.error(f"Error unloading adapter: {e}")
|
|
2413
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
@router.put("/adapters/{adapter_id}/reload", response_model=SuccessResponse[AdapterOperationResult])
|
|
2417
|
+
async def reload_adapter(
|
|
2418
|
+
adapter_id: str, body: AdapterActionRequest, request: Request, auth: AuthContext = Depends(require_admin)
|
|
2419
|
+
) -> SuccessResponse[AdapterOperationResult]:
|
|
2420
|
+
"""
|
|
2421
|
+
Reload an adapter with new configuration.
|
|
2422
|
+
|
|
2423
|
+
Stops the adapter and restarts it with new configuration.
|
|
2424
|
+
Useful for applying configuration changes without full restart.
|
|
2425
|
+
Requires ADMIN role.
|
|
2426
|
+
"""
|
|
2427
|
+
runtime_control = getattr(request.app.state, "main_runtime_control_service", None)
|
|
2428
|
+
if not runtime_control:
|
|
2429
|
+
raise HTTPException(status_code=503, detail=ERROR_RUNTIME_CONTROL_SERVICE_NOT_AVAILABLE)
|
|
2430
|
+
|
|
2431
|
+
try:
|
|
2432
|
+
# Get current adapter info to preserve type
|
|
2433
|
+
adapter_info = await runtime_control.get_adapter_info(adapter_id)
|
|
2434
|
+
if not adapter_info:
|
|
2435
|
+
raise HTTPException(status_code=404, detail=f"Adapter '{adapter_id}' not found")
|
|
2436
|
+
|
|
2437
|
+
# First unload the adapter
|
|
2438
|
+
unload_result = await runtime_control.unload_adapter(adapter_id, force=False)
|
|
2439
|
+
if not unload_result.success:
|
|
2440
|
+
raise HTTPException(status_code=400, detail=f"Failed to unload adapter: {unload_result.error}")
|
|
2441
|
+
|
|
2442
|
+
# Then reload with new config
|
|
2443
|
+
load_result = await runtime_control.load_adapter(
|
|
2444
|
+
adapter_type=adapter_info.adapter_type,
|
|
2445
|
+
adapter_id=adapter_id,
|
|
2446
|
+
config=body.config,
|
|
2447
|
+
auto_start=body.auto_start,
|
|
2448
|
+
)
|
|
2449
|
+
|
|
2450
|
+
# Convert response
|
|
2451
|
+
response = AdapterOperationResult(
|
|
2452
|
+
success=load_result.success,
|
|
2453
|
+
adapter_id=load_result.adapter_id,
|
|
2454
|
+
adapter_type=adapter_info.adapter_type,
|
|
2455
|
+
message=(
|
|
2456
|
+
f"Adapter {adapter_id} reloaded successfully"
|
|
2457
|
+
if load_result.success
|
|
2458
|
+
else f"Reload failed: {load_result.error}"
|
|
2459
|
+
),
|
|
2460
|
+
error=load_result.error,
|
|
2461
|
+
details={"timestamp": load_result.timestamp.isoformat()},
|
|
2462
|
+
)
|
|
2463
|
+
|
|
2464
|
+
return SuccessResponse(data=response)
|
|
2465
|
+
|
|
2466
|
+
except HTTPException:
|
|
2467
|
+
raise
|
|
2468
|
+
except Exception as e:
|
|
2469
|
+
logger.error(f"Error reloading adapter: {e}")
|
|
2470
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2471
|
+
|
|
2472
|
+
|
|
2473
|
+
# Adapter Configuration Workflow Endpoints
|
|
2474
|
+
|
|
2475
|
+
|
|
2476
|
+
@router.post("/adapters/{adapter_type}/configure/start", response_model=SuccessResponse[ConfigurationSessionResponse])
|
|
2477
|
+
async def start_adapter_configuration(
|
|
2478
|
+
adapter_type: str,
|
|
2479
|
+
request: Request,
|
|
2480
|
+
auth: AuthContext = Depends(require_admin),
|
|
2481
|
+
) -> SuccessResponse[ConfigurationSessionResponse]:
|
|
2482
|
+
"""
|
|
2483
|
+
Start interactive configuration session for an adapter.
|
|
2484
|
+
|
|
2485
|
+
Creates a new configuration session and returns the session ID along with
|
|
2486
|
+
information about the first step in the workflow.
|
|
2487
|
+
|
|
2488
|
+
Requires ADMIN role.
|
|
2489
|
+
"""
|
|
2490
|
+
try:
|
|
2491
|
+
config_service = _get_adapter_config_service(request)
|
|
2492
|
+
|
|
2493
|
+
# Start the session
|
|
2494
|
+
session = await config_service.start_session(adapter_type=adapter_type, user_id=auth.user_id)
|
|
2495
|
+
|
|
2496
|
+
# Get manifest to access steps
|
|
2497
|
+
manifest = config_service._adapter_manifests.get(adapter_type)
|
|
2498
|
+
if not manifest:
|
|
2499
|
+
raise HTTPException(status_code=404, detail=f"Adapter '{adapter_type}' not found")
|
|
2500
|
+
|
|
2501
|
+
# Get current step
|
|
2502
|
+
current_step = manifest.steps[0] if manifest.steps else None
|
|
2503
|
+
|
|
2504
|
+
response = ConfigurationSessionResponse(
|
|
2505
|
+
session_id=session.session_id,
|
|
2506
|
+
adapter_type=session.adapter_type,
|
|
2507
|
+
status=session.status.value,
|
|
2508
|
+
current_step_index=session.current_step_index,
|
|
2509
|
+
current_step=current_step,
|
|
2510
|
+
total_steps=len(manifest.steps),
|
|
2511
|
+
created_at=session.created_at,
|
|
2512
|
+
)
|
|
2513
|
+
|
|
2514
|
+
return SuccessResponse(data=response)
|
|
2515
|
+
|
|
2516
|
+
except ValueError as e:
|
|
2517
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
2518
|
+
except HTTPException:
|
|
2519
|
+
raise
|
|
2520
|
+
except Exception as e:
|
|
2521
|
+
logger.error(f"Error starting configuration session: {e}")
|
|
2522
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
@router.get("/adapters/configure/{session_id}", response_model=SuccessResponse[ConfigurationStatusResponse])
|
|
2526
|
+
async def get_configuration_status(
|
|
2527
|
+
session_id: str,
|
|
2528
|
+
request: Request,
|
|
2529
|
+
auth: AuthContext = Depends(require_observer),
|
|
2530
|
+
) -> SuccessResponse[ConfigurationStatusResponse]:
|
|
2531
|
+
"""
|
|
2532
|
+
Get current status of a configuration session.
|
|
2533
|
+
|
|
2534
|
+
Returns complete session state including current step, collected configuration,
|
|
2535
|
+
and session status.
|
|
2536
|
+
|
|
2537
|
+
Requires OBSERVER role.
|
|
2538
|
+
"""
|
|
2539
|
+
try:
|
|
2540
|
+
config_service = _get_adapter_config_service(request)
|
|
2541
|
+
session = config_service.get_session(session_id)
|
|
2542
|
+
|
|
2543
|
+
if not session:
|
|
2544
|
+
raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
|
|
2545
|
+
|
|
2546
|
+
# Get manifest to access steps
|
|
2547
|
+
manifest = config_service._adapter_manifests.get(session.adapter_type)
|
|
2548
|
+
if not manifest:
|
|
2549
|
+
raise HTTPException(status_code=500, detail=f"Manifest for '{session.adapter_type}' not found")
|
|
2550
|
+
|
|
2551
|
+
# Get current step
|
|
2552
|
+
current_step = None
|
|
2553
|
+
if session.current_step_index < len(manifest.steps):
|
|
2554
|
+
current_step = manifest.steps[session.current_step_index]
|
|
2555
|
+
|
|
2556
|
+
response = ConfigurationStatusResponse(
|
|
2557
|
+
session_id=session.session_id,
|
|
2558
|
+
adapter_type=session.adapter_type,
|
|
2559
|
+
status=session.status.value,
|
|
2560
|
+
current_step_index=session.current_step_index,
|
|
2561
|
+
current_step=current_step,
|
|
2562
|
+
total_steps=len(manifest.steps),
|
|
2563
|
+
collected_config=session.collected_config,
|
|
2564
|
+
created_at=session.created_at,
|
|
2565
|
+
updated_at=session.updated_at,
|
|
2566
|
+
)
|
|
2567
|
+
|
|
2568
|
+
return SuccessResponse(data=response)
|
|
2569
|
+
|
|
2570
|
+
except HTTPException:
|
|
2571
|
+
raise
|
|
2572
|
+
except Exception as e:
|
|
2573
|
+
logger.error(f"Error getting configuration status: {e}")
|
|
2574
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2575
|
+
|
|
2576
|
+
|
|
2577
|
+
@router.post("/adapters/configure/{session_id}/step", response_model=SuccessResponse[StepExecutionResponse])
|
|
2578
|
+
async def execute_configuration_step(
|
|
2579
|
+
session_id: str,
|
|
2580
|
+
request: Request,
|
|
2581
|
+
body: StepExecutionRequest = Body(...),
|
|
2582
|
+
auth: AuthContext = Depends(require_admin),
|
|
2583
|
+
) -> SuccessResponse[StepExecutionResponse]:
|
|
2584
|
+
"""
|
|
2585
|
+
Execute the current configuration step.
|
|
2586
|
+
|
|
2587
|
+
The body contains step-specific data such as user selections, input values,
|
|
2588
|
+
or OAuth callback data. The step type determines what data is expected.
|
|
2589
|
+
|
|
2590
|
+
Requires ADMIN role.
|
|
2591
|
+
"""
|
|
2592
|
+
try:
|
|
2593
|
+
config_service = _get_adapter_config_service(request)
|
|
2594
|
+
|
|
2595
|
+
# Execute the step
|
|
2596
|
+
result = await config_service.execute_step(session_id, body.step_data)
|
|
2597
|
+
|
|
2598
|
+
response = StepExecutionResponse(
|
|
2599
|
+
step_id=result.step_id,
|
|
2600
|
+
success=result.success,
|
|
2601
|
+
data=result.data,
|
|
2602
|
+
next_step_index=result.next_step_index,
|
|
2603
|
+
error=result.error,
|
|
2604
|
+
awaiting_callback=result.awaiting_callback,
|
|
2605
|
+
)
|
|
2606
|
+
|
|
2607
|
+
return SuccessResponse(data=response)
|
|
2608
|
+
|
|
2609
|
+
except HTTPException:
|
|
2610
|
+
raise
|
|
2611
|
+
except Exception as e:
|
|
2612
|
+
logger.error(f"Error executing configuration step: {e}")
|
|
2613
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
@router.get("/adapters/configure/{session_id}/status")
|
|
2617
|
+
async def get_session_status(
|
|
2618
|
+
session_id: str,
|
|
2619
|
+
request: Request,
|
|
2620
|
+
) -> SuccessResponse[ConfigurationSessionResponse]:
|
|
2621
|
+
"""
|
|
2622
|
+
Get the current status of a configuration session.
|
|
2623
|
+
|
|
2624
|
+
Useful for polling after OAuth callback to check if authentication completed.
|
|
2625
|
+
"""
|
|
2626
|
+
try:
|
|
2627
|
+
config_service = _get_adapter_config_service(request)
|
|
2628
|
+
session = config_service.get_session(session_id)
|
|
2629
|
+
|
|
2630
|
+
if not session:
|
|
2631
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
2632
|
+
|
|
2633
|
+
# Get adapter steps from the adapter manifest (InteractiveConfiguration)
|
|
2634
|
+
current_step = None
|
|
2635
|
+
total_steps = 0
|
|
2636
|
+
manifest = config_service._adapter_manifests.get(session.adapter_type)
|
|
2637
|
+
if manifest and manifest.steps:
|
|
2638
|
+
steps = manifest.steps
|
|
2639
|
+
total_steps = len(steps)
|
|
2640
|
+
if session.current_step_index < len(steps):
|
|
2641
|
+
# Use the ConfigurationStep directly from the manifest
|
|
2642
|
+
current_step = steps[session.current_step_index]
|
|
2643
|
+
|
|
2644
|
+
response = ConfigurationSessionResponse(
|
|
2645
|
+
session_id=session.session_id,
|
|
2646
|
+
adapter_type=session.adapter_type,
|
|
2647
|
+
status=session.status.value,
|
|
2648
|
+
current_step_index=session.current_step_index,
|
|
2649
|
+
current_step=current_step,
|
|
2650
|
+
total_steps=total_steps,
|
|
2651
|
+
created_at=session.created_at,
|
|
2652
|
+
)
|
|
2653
|
+
|
|
2654
|
+
return SuccessResponse(data=response)
|
|
2655
|
+
|
|
2656
|
+
except HTTPException:
|
|
2657
|
+
raise
|
|
2658
|
+
except Exception as e:
|
|
2659
|
+
logger.error(f"Error getting session status: {e}")
|
|
2660
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2661
|
+
|
|
2662
|
+
|
|
2663
|
+
@router.get("/adapters/configure/{session_id}/oauth/callback")
|
|
2664
|
+
async def oauth_callback(
|
|
2665
|
+
session_id: str,
|
|
2666
|
+
code: str,
|
|
2667
|
+
state: str,
|
|
2668
|
+
request: Request,
|
|
2669
|
+
) -> Response:
|
|
2670
|
+
"""
|
|
2671
|
+
Handle OAuth callback from external service.
|
|
2672
|
+
|
|
2673
|
+
This endpoint is called by OAuth providers after user authorization.
|
|
2674
|
+
It processes the authorization code and advances the configuration workflow.
|
|
2675
|
+
Returns HTML that redirects back to the app or shows success message.
|
|
2676
|
+
|
|
2677
|
+
No authentication required (OAuth state validation provides security).
|
|
2678
|
+
"""
|
|
2679
|
+
logger.info("=" * 60)
|
|
2680
|
+
logger.info("[OAUTH CALLBACK] *** CALLBACK RECEIVED ***")
|
|
2681
|
+
logger.info(f"[OAUTH CALLBACK] Full URL: {request.url}")
|
|
2682
|
+
logger.info(f"[OAUTH CALLBACK] Path: {request.url.path}")
|
|
2683
|
+
logger.info(f"[OAUTH CALLBACK] session_id: {session_id}")
|
|
2684
|
+
logger.info(f"[OAUTH CALLBACK] state: {state}")
|
|
2685
|
+
logger.info(f"[OAUTH CALLBACK] code length: {len(code)}")
|
|
2686
|
+
logger.info(
|
|
2687
|
+
f"[OAUTH CALLBACK] code preview: {code[:20]}..." if len(code) > 20 else f"[OAUTH CALLBACK] code: {code}"
|
|
2688
|
+
)
|
|
2689
|
+
logger.info(f"[OAUTH CALLBACK] Headers: {dict(request.headers)}")
|
|
2690
|
+
logger.info("=" * 60)
|
|
2691
|
+
try:
|
|
2692
|
+
config_service = _get_adapter_config_service(request)
|
|
2693
|
+
|
|
2694
|
+
# Verify session exists and state matches
|
|
2695
|
+
session = config_service.get_session(session_id)
|
|
2696
|
+
if not session:
|
|
2697
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
2698
|
+
|
|
2699
|
+
if state != session_id:
|
|
2700
|
+
raise HTTPException(status_code=400, detail="Invalid OAuth state")
|
|
2701
|
+
|
|
2702
|
+
# Execute the OAuth callback step
|
|
2703
|
+
result = await config_service.execute_step(session_id, {"code": code, "state": state})
|
|
2704
|
+
|
|
2705
|
+
if not result.success:
|
|
2706
|
+
error_html = f"""<!DOCTYPE html>
|
|
2707
|
+
<html>
|
|
2708
|
+
<head><title>OAuth Failed</title></head>
|
|
2709
|
+
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
2710
|
+
<h1 style="color: #d32f2f;">Authentication Failed</h1>
|
|
2711
|
+
<p>{html.escape(result.error or "OAuth callback failed")}</p>
|
|
2712
|
+
<p>Please close this window and try again in the app.</p>
|
|
2713
|
+
</body>
|
|
2714
|
+
</html>"""
|
|
2715
|
+
return Response(content=error_html, media_type="text/html")
|
|
2716
|
+
|
|
2717
|
+
# Return HTML that tells user to go back to app
|
|
2718
|
+
# Try to use deep link to return to app automatically
|
|
2719
|
+
success_html = f"""<!DOCTYPE html>
|
|
2720
|
+
<html>
|
|
2721
|
+
<head>
|
|
2722
|
+
<title>OAuth Success</title>
|
|
2723
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2724
|
+
</head>
|
|
2725
|
+
<body style="font-family: sans-serif; text-align: center; padding: 50px; background: #f5f5f5;">
|
|
2726
|
+
<div style="background: white; padding: 40px; border-radius: 10px; max-width: 400px; margin: 0 auto; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
|
2727
|
+
<h1 style="color: #4caf50; margin-bottom: 20px;">✓ Connected!</h1>
|
|
2728
|
+
<p style="color: #666; font-size: 18px;">Authentication successful.</p>
|
|
2729
|
+
<p style="color: #888; margin-top: 20px;">You can close this window and return to the CIRIS app.</p>
|
|
2730
|
+
<p style="color: #aaa; font-size: 12px; margin-top: 30px;">Session: {html.escape(session_id[:8])}...</p>
|
|
2731
|
+
</div>
|
|
2732
|
+
</body>
|
|
2733
|
+
</html>"""
|
|
2734
|
+
return Response(content=success_html, media_type="text/html")
|
|
2735
|
+
|
|
2736
|
+
except HTTPException:
|
|
2737
|
+
raise
|
|
2738
|
+
except Exception as e:
|
|
2739
|
+
logger.error(f"Error handling OAuth callback: {e}")
|
|
2740
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2741
|
+
|
|
2742
|
+
|
|
2743
|
+
@router.get("/adapters/oauth/callback")
|
|
2744
|
+
async def oauth_deeplink_callback(
|
|
2745
|
+
code: str,
|
|
2746
|
+
state: str,
|
|
2747
|
+
request: Request,
|
|
2748
|
+
provider: Optional[str] = None,
|
|
2749
|
+
source: Optional[str] = None,
|
|
2750
|
+
) -> SuccessResponse[Dict[str, Any]]:
|
|
2751
|
+
"""
|
|
2752
|
+
Handle OAuth callback forwarded from Android deep link (ciris://oauth/callback).
|
|
2753
|
+
|
|
2754
|
+
This endpoint receives OAuth callbacks that were forwarded from OAuthCallbackActivity
|
|
2755
|
+
on Android. The Android app uses a deep link (ciris://oauth/callback) to receive
|
|
2756
|
+
the OAuth redirect from the system browser, then forwards to this endpoint.
|
|
2757
|
+
|
|
2758
|
+
This is a generic endpoint that works for any OAuth2 provider (Home Assistant,
|
|
2759
|
+
Discord, Google, Microsoft, Reddit, etc.) - the state parameter contains the
|
|
2760
|
+
session_id which identifies the configuration session.
|
|
2761
|
+
|
|
2762
|
+
Args:
|
|
2763
|
+
code: Authorization code from OAuth provider
|
|
2764
|
+
state: State parameter (contains session_id for session lookup)
|
|
2765
|
+
provider: Optional provider hint (home_assistant, discord, etc.)
|
|
2766
|
+
source: Source of callback (deeplink indicates forwarded from Android)
|
|
2767
|
+
|
|
2768
|
+
Returns:
|
|
2769
|
+
Success response with callback processing result
|
|
2770
|
+
"""
|
|
2771
|
+
logger.info("=" * 60)
|
|
2772
|
+
logger.info("[OAUTH DEEPLINK CALLBACK] *** FORWARDED CALLBACK RECEIVED ***")
|
|
2773
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] Full URL: {request.url}")
|
|
2774
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] state (session_id): {state}")
|
|
2775
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] provider: {provider}")
|
|
2776
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] source: {source}")
|
|
2777
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] code length: {len(code)}")
|
|
2778
|
+
logger.info("=" * 60)
|
|
2779
|
+
|
|
2780
|
+
try:
|
|
2781
|
+
config_service = _get_adapter_config_service(request)
|
|
2782
|
+
|
|
2783
|
+
# The state parameter IS the session_id
|
|
2784
|
+
session_id = state
|
|
2785
|
+
|
|
2786
|
+
# Handle provider-prefixed state (e.g., "ha:actual_session_id")
|
|
2787
|
+
if ":" in state:
|
|
2788
|
+
parts = state.split(":", 1)
|
|
2789
|
+
if len(parts) == 2 and len(parts[0]) < 20:
|
|
2790
|
+
# Looks like "provider:session_id"
|
|
2791
|
+
provider = provider or parts[0]
|
|
2792
|
+
session_id = parts[1]
|
|
2793
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] Extracted provider={provider}, session_id={session_id}")
|
|
2794
|
+
|
|
2795
|
+
# Verify session exists
|
|
2796
|
+
session = config_service.get_session(session_id)
|
|
2797
|
+
if not session:
|
|
2798
|
+
logger.error(f"[OAUTH DEEPLINK CALLBACK] Session not found: {session_id}")
|
|
2799
|
+
raise HTTPException(status_code=404, detail=f"Session not found: {session_id}")
|
|
2800
|
+
|
|
2801
|
+
# Execute the OAuth callback step
|
|
2802
|
+
result = await config_service.execute_step(session_id, {"code": code, "state": state})
|
|
2803
|
+
|
|
2804
|
+
if not result.success:
|
|
2805
|
+
logger.error(f"[OAUTH DEEPLINK CALLBACK] OAuth step failed: {result.error}")
|
|
2806
|
+
raise HTTPException(status_code=400, detail=result.error or "OAuth callback failed")
|
|
2807
|
+
|
|
2808
|
+
logger.info(f"[OAUTH DEEPLINK CALLBACK] Successfully processed OAuth callback for session {session_id}")
|
|
2809
|
+
|
|
2810
|
+
return SuccessResponse(
|
|
2811
|
+
data={
|
|
2812
|
+
"session_id": session_id,
|
|
2813
|
+
"success": True,
|
|
2814
|
+
"message": "OAuth callback processed successfully",
|
|
2815
|
+
"next_step": result.next_step_index,
|
|
2816
|
+
}
|
|
2817
|
+
)
|
|
2818
|
+
|
|
2819
|
+
except HTTPException:
|
|
2820
|
+
raise
|
|
2821
|
+
except Exception as e:
|
|
2822
|
+
logger.error(f"[OAUTH DEEPLINK CALLBACK] Error: {e}")
|
|
2823
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2824
|
+
|
|
2825
|
+
|
|
2826
|
+
async def _get_runtime_control_service_for_adapter_load(request: Request) -> Any:
|
|
2827
|
+
"""Get RuntimeControlService for adapter loading (returns None if unavailable)."""
|
|
2828
|
+
from ciris_engine.schemas.runtime.enums import ServiceType
|
|
2829
|
+
|
|
2830
|
+
runtime_control_service = getattr(request.app.state, "main_runtime_control_service", None)
|
|
2831
|
+
if runtime_control_service:
|
|
2832
|
+
return runtime_control_service
|
|
2833
|
+
|
|
2834
|
+
runtime_control_service = getattr(request.app.state, "runtime_control_service", None)
|
|
2835
|
+
if runtime_control_service:
|
|
2836
|
+
return runtime_control_service
|
|
2837
|
+
|
|
2838
|
+
service_registry = getattr(request.app.state, "service_registry", None)
|
|
2839
|
+
if service_registry:
|
|
2840
|
+
return await service_registry.get_service(handler="api", service_type=ServiceType.RUNTIME_CONTROL)
|
|
2841
|
+
|
|
2842
|
+
return None
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
async def _load_adapter_after_config(request: Request, session: Any) -> str:
|
|
2846
|
+
"""Load adapter after configuration and return status message."""
|
|
2847
|
+
import uuid
|
|
2848
|
+
|
|
2849
|
+
runtime_control_service = await _get_runtime_control_service_for_adapter_load(request)
|
|
2850
|
+
if not runtime_control_service:
|
|
2851
|
+
logger.warning("[COMPLETE_CONFIG] RuntimeControlService not available, adapter not loaded")
|
|
2852
|
+
return " - runtime control service unavailable"
|
|
2853
|
+
|
|
2854
|
+
logger.info("[COMPLETE_CONFIG] Loading adapter via RuntimeControlService.load_adapter")
|
|
2855
|
+
adapter_config = dict(session.collected_config)
|
|
2856
|
+
adapter_id = f"{session.adapter_type}_{uuid.uuid4().hex[:8]}"
|
|
2857
|
+
|
|
2858
|
+
load_result = await runtime_control_service.load_adapter(
|
|
2859
|
+
adapter_type=session.adapter_type,
|
|
2860
|
+
adapter_id=adapter_id,
|
|
2861
|
+
config=adapter_config,
|
|
2862
|
+
)
|
|
2863
|
+
|
|
2864
|
+
if load_result.success:
|
|
2865
|
+
logger.info(f"[COMPLETE_CONFIG] Adapter loaded successfully: {adapter_id}")
|
|
2866
|
+
return f" - adapter '{adapter_id}' loaded and started"
|
|
2867
|
+
else:
|
|
2868
|
+
logger.error(f"[COMPLETE_CONFIG] Adapter load failed: {load_result.error}")
|
|
2869
|
+
return f" - adapter load failed: {load_result.error}"
|
|
2870
|
+
|
|
2871
|
+
|
|
2872
|
+
async def _persist_config_if_requested(
|
|
2873
|
+
body: ConfigurationCompleteRequest, session: Any, adapter_config_service: Any, request: Request
|
|
2874
|
+
) -> tuple[bool, str]:
|
|
2875
|
+
"""Persist configuration if requested. Returns (persisted, message_suffix)."""
|
|
2876
|
+
if not body.persist:
|
|
2877
|
+
return False, ""
|
|
2878
|
+
|
|
2879
|
+
graph_config_service = getattr(request.app.state, "config_service", None)
|
|
2880
|
+
persisted = await adapter_config_service.persist_adapter_config(
|
|
2881
|
+
adapter_type=session.adapter_type,
|
|
2882
|
+
config=session.collected_config,
|
|
2883
|
+
config_service=graph_config_service,
|
|
2884
|
+
)
|
|
2885
|
+
return persisted, " and persisted for startup" if persisted else " (persistence failed)"
|
|
2886
|
+
|
|
2887
|
+
|
|
2888
|
+
@router.post("/adapters/configure/{session_id}/complete", response_model=SuccessResponse[ConfigurationCompleteResponse])
|
|
2889
|
+
async def complete_configuration(
|
|
2890
|
+
session_id: str,
|
|
2891
|
+
request: Request,
|
|
2892
|
+
body: ConfigurationCompleteRequest = Body(default=ConfigurationCompleteRequest()),
|
|
2893
|
+
auth: AuthContext = Depends(require_admin),
|
|
2894
|
+
) -> SuccessResponse[ConfigurationCompleteResponse]:
|
|
2895
|
+
"""
|
|
2896
|
+
Finalize and apply the configuration.
|
|
2897
|
+
|
|
2898
|
+
Validates the collected configuration and applies it to the adapter.
|
|
2899
|
+
Once completed, the adapter should be ready to use with the new configuration.
|
|
2900
|
+
|
|
2901
|
+
If `persist` is True, the configuration will be saved for automatic loading
|
|
2902
|
+
on startup, allowing the adapter to be automatically configured when the
|
|
2903
|
+
system restarts.
|
|
2904
|
+
|
|
2905
|
+
Requires ADMIN role.
|
|
2906
|
+
"""
|
|
2907
|
+
try:
|
|
2908
|
+
adapter_config_service = _get_adapter_config_service(request)
|
|
2909
|
+
|
|
2910
|
+
session = adapter_config_service.get_session(session_id)
|
|
2911
|
+
if not session:
|
|
2912
|
+
raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
|
|
2913
|
+
|
|
2914
|
+
success = await adapter_config_service.complete_session(session_id)
|
|
2915
|
+
persisted = False
|
|
2916
|
+
message = ""
|
|
2917
|
+
|
|
2918
|
+
if success:
|
|
2919
|
+
message = f"Configuration applied successfully for {session.adapter_type}"
|
|
2920
|
+
logger.info(f"[COMPLETE_CONFIG] Config applied, attempting to start adapter for {session.adapter_type}")
|
|
2921
|
+
|
|
2922
|
+
try:
|
|
2923
|
+
message += await _load_adapter_after_config(request, session)
|
|
2924
|
+
except Exception as e:
|
|
2925
|
+
logger.error(f"Error loading adapter after config: {e}", exc_info=True)
|
|
2926
|
+
message += f" - adapter load error: {e}"
|
|
2927
|
+
|
|
2928
|
+
persisted, persist_msg = await _persist_config_if_requested(body, session, adapter_config_service, request)
|
|
2929
|
+
message += persist_msg
|
|
2930
|
+
else:
|
|
2931
|
+
message = f"Configuration validation or application failed for {session.adapter_type}"
|
|
2932
|
+
|
|
2933
|
+
response = ConfigurationCompleteResponse(
|
|
2934
|
+
success=success,
|
|
2935
|
+
adapter_type=session.adapter_type,
|
|
2936
|
+
message=message,
|
|
2937
|
+
applied_config=session.collected_config if success else {},
|
|
2938
|
+
persisted=persisted,
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
return SuccessResponse(data=response)
|
|
2942
|
+
|
|
2943
|
+
except HTTPException:
|
|
2944
|
+
raise
|
|
2945
|
+
except Exception as e:
|
|
2946
|
+
logger.error(f"Error completing configuration: {e}")
|
|
2947
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2948
|
+
|
|
2949
|
+
|
|
2950
|
+
# Tool endpoints
|
|
2951
|
+
@router.get("/tools")
|
|
2952
|
+
async def get_available_tools(request: Request, auth: AuthContext = Depends(require_observer)) -> JSONDict:
|
|
2953
|
+
"""
|
|
2954
|
+
Get list of all available tools from all tool providers.
|
|
2955
|
+
|
|
2956
|
+
Returns tools from:
|
|
2957
|
+
- Core tool services (secrets, self_help)
|
|
2958
|
+
- Adapter tool services (API, Discord, etc.)
|
|
2959
|
+
|
|
2960
|
+
Requires OBSERVER role.
|
|
2961
|
+
"""
|
|
2962
|
+
|
|
2963
|
+
try:
|
|
2964
|
+
all_tools = []
|
|
2965
|
+
tool_providers = set() # Use set to avoid counting duplicates
|
|
2966
|
+
|
|
2967
|
+
# Get all tool providers from the service registry
|
|
2968
|
+
service_registry = getattr(request.app.state, "service_registry", None)
|
|
2969
|
+
if service_registry:
|
|
2970
|
+
# Get provider info for TOOL services
|
|
2971
|
+
provider_info = service_registry.get_provider_info(service_type=ServiceType.TOOL.value)
|
|
2972
|
+
provider_info.get("services", {}).get(ServiceType.TOOL.value, [])
|
|
2973
|
+
|
|
2974
|
+
# Get the actual provider instances from the registry
|
|
2975
|
+
if hasattr(service_registry, "_services") and ServiceType.TOOL in service_registry._services:
|
|
2976
|
+
for provider_data in service_registry._services[ServiceType.TOOL]:
|
|
2977
|
+
try:
|
|
2978
|
+
provider = provider_data.instance
|
|
2979
|
+
provider_name = provider.__class__.__name__
|
|
2980
|
+
tool_providers.add(provider_name) # Use add to avoid duplicates
|
|
2981
|
+
|
|
2982
|
+
if hasattr(provider, "get_all_tool_info"):
|
|
2983
|
+
# Modern interface with ToolInfo objects
|
|
2984
|
+
tool_infos = await provider.get_all_tool_info()
|
|
2985
|
+
for info in tool_infos:
|
|
2986
|
+
all_tools.append(
|
|
2987
|
+
ToolInfoResponse(
|
|
2988
|
+
name=info.name,
|
|
2989
|
+
description=info.description,
|
|
2990
|
+
provider=provider_name,
|
|
2991
|
+
parameters=info.parameters if hasattr(info, "parameters") else None,
|
|
2992
|
+
category=getattr(info, "category", "general"),
|
|
2993
|
+
cost=getattr(info, "cost", 0.0),
|
|
2994
|
+
when_to_use=getattr(info, "when_to_use", None),
|
|
2995
|
+
)
|
|
2996
|
+
)
|
|
2997
|
+
elif hasattr(provider, "list_tools"):
|
|
2998
|
+
# Legacy interface
|
|
2999
|
+
tool_names = await provider.list_tools()
|
|
3000
|
+
for name in tool_names:
|
|
3001
|
+
all_tools.append(
|
|
3002
|
+
ToolInfoResponse(
|
|
3003
|
+
name=name,
|
|
3004
|
+
description=f"{name} tool",
|
|
3005
|
+
provider=provider_name,
|
|
3006
|
+
parameters=None,
|
|
3007
|
+
category="general",
|
|
3008
|
+
cost=0.0,
|
|
3009
|
+
when_to_use=None,
|
|
3010
|
+
)
|
|
3011
|
+
)
|
|
3012
|
+
except Exception as e:
|
|
3013
|
+
logger.warning(f"Failed to get tools from provider {provider_name}: {e}", exc_info=True)
|
|
3014
|
+
|
|
3015
|
+
# Deduplicate tools by name (in case multiple providers offer the same tool)
|
|
3016
|
+
seen_tools = {}
|
|
3017
|
+
unique_tools = []
|
|
3018
|
+
for tool in all_tools:
|
|
3019
|
+
if tool.name not in seen_tools:
|
|
3020
|
+
seen_tools[tool.name] = tool
|
|
3021
|
+
unique_tools.append(tool)
|
|
3022
|
+
else:
|
|
3023
|
+
# If we see the same tool from multiple providers, add provider info
|
|
3024
|
+
existing = seen_tools[tool.name]
|
|
3025
|
+
if existing.provider != tool.provider:
|
|
3026
|
+
existing.provider = f"{existing.provider}, {tool.provider}"
|
|
3027
|
+
|
|
3028
|
+
# Log provider information for debugging
|
|
3029
|
+
logger.info(f"Tool providers found: {len(tool_providers)} unique providers: {tool_providers}")
|
|
3030
|
+
logger.info(f"Total tools collected: {len(all_tools)}, Unique tools: {len(unique_tools)}")
|
|
3031
|
+
logger.info(f"Tool provider summary: {list(tool_providers)}")
|
|
3032
|
+
|
|
3033
|
+
# Create response with additional metadata for tool providers
|
|
3034
|
+
# Since ResponseMetadata is immutable, we need to create a dict response
|
|
3035
|
+
return {
|
|
3036
|
+
"data": [tool.model_dump() for tool in unique_tools],
|
|
3037
|
+
"metadata": {
|
|
3038
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
3039
|
+
"request_id": None,
|
|
3040
|
+
"duration_ms": None,
|
|
3041
|
+
"providers": list(tool_providers),
|
|
3042
|
+
"provider_count": len(tool_providers),
|
|
3043
|
+
"total_tools": len(unique_tools),
|
|
3044
|
+
},
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
except Exception as e:
|
|
3048
|
+
logger.error(f"Error getting available tools: {e}")
|
|
3049
|
+
raise HTTPException(status_code=500, detail=str(e))
|