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,1471 @@
|
|
|
1
|
+
"""Reddit adapter modular service implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from pydantic import BaseModel, ValidationError
|
|
13
|
+
|
|
14
|
+
from ciris_engine.logic.buses import BusManager
|
|
15
|
+
from ciris_engine.logic.secrets.service import SecretsService
|
|
16
|
+
from ciris_engine.logic.services.base_service import BaseService
|
|
17
|
+
from ciris_engine.protocols.services.lifecycle.time import TimeServiceProtocol
|
|
18
|
+
from ciris_engine.schemas.adapters.tools import ToolExecutionResult, ToolExecutionStatus, ToolInfo, ToolParameterSchema
|
|
19
|
+
from ciris_engine.schemas.runtime.enums import ServiceType
|
|
20
|
+
from ciris_engine.schemas.runtime.messages import FetchedMessage
|
|
21
|
+
from ciris_engine.schemas.services.core import ServiceCapabilities
|
|
22
|
+
from ciris_engine.schemas.types import JSONDict, JSONValue
|
|
23
|
+
|
|
24
|
+
from .protocol import RedditCommunicationProtocol, RedditOAuthProtocol, RedditToolProtocol
|
|
25
|
+
from .schemas import (
|
|
26
|
+
RedditChannelReference,
|
|
27
|
+
RedditChannelType,
|
|
28
|
+
RedditCommentResult,
|
|
29
|
+
RedditCommentSummary,
|
|
30
|
+
RedditCredentials,
|
|
31
|
+
RedditDeleteContentRequest,
|
|
32
|
+
RedditDeletionResult,
|
|
33
|
+
RedditDeletionStatus,
|
|
34
|
+
RedditDisclosureRequest,
|
|
35
|
+
RedditGetSubmissionRequest,
|
|
36
|
+
RedditPostResult,
|
|
37
|
+
RedditRemovalResult,
|
|
38
|
+
RedditRemoveContentRequest,
|
|
39
|
+
RedditSubmissionSummary,
|
|
40
|
+
RedditSubmitCommentRequest,
|
|
41
|
+
RedditSubmitPostRequest,
|
|
42
|
+
RedditTimelineEntry,
|
|
43
|
+
RedditTimelineResponse,
|
|
44
|
+
RedditToken,
|
|
45
|
+
RedditUserContext,
|
|
46
|
+
RedditUserContextRequest,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_channel_reference(
|
|
53
|
+
subreddit: str,
|
|
54
|
+
submission_id: Optional[str] = None,
|
|
55
|
+
comment_id: Optional[str] = None,
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Return a canonical reddit:r/<sub>:post/<id>:comment/<id> reference."""
|
|
58
|
+
|
|
59
|
+
if comment_id:
|
|
60
|
+
reference = RedditChannelReference(
|
|
61
|
+
target=RedditChannelType.COMMENT,
|
|
62
|
+
subreddit=subreddit,
|
|
63
|
+
submission_id=submission_id,
|
|
64
|
+
comment_id=comment_id,
|
|
65
|
+
)
|
|
66
|
+
elif submission_id:
|
|
67
|
+
reference = RedditChannelReference(
|
|
68
|
+
target=RedditChannelType.SUBMISSION,
|
|
69
|
+
subreddit=subreddit,
|
|
70
|
+
submission_id=submission_id,
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
reference = RedditChannelReference(target=RedditChannelType.SUBREDDIT, subreddit=subreddit)
|
|
74
|
+
return reference.to_string()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RedditAPIClient:
|
|
78
|
+
"""Thin wrapper around the Reddit REST API with OAuth management."""
|
|
79
|
+
|
|
80
|
+
_TOKEN_URL = "https://www.reddit.com/api/v1/access_token"
|
|
81
|
+
_API_BASE_URL = "https://oauth.reddit.com"
|
|
82
|
+
_USER_AGENT_FALLBACK = "CIRIS-RedditAdapter/1.0 (+https://ciris.ai)"
|
|
83
|
+
|
|
84
|
+
def __init__(self, credentials: RedditCredentials, time_service: Optional[TimeServiceProtocol] = None) -> None:
|
|
85
|
+
self._credentials = credentials
|
|
86
|
+
self._time_service = time_service
|
|
87
|
+
self._http_client: Optional[httpx.AsyncClient] = None
|
|
88
|
+
self._token: Optional[RedditToken] = None
|
|
89
|
+
self._token_lock = asyncio.Lock()
|
|
90
|
+
self._request_count = 0
|
|
91
|
+
self._error_count = 0
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Lifecycle
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
async def start(self) -> None:
|
|
97
|
+
headers = {
|
|
98
|
+
"User-Agent": self._credentials.user_agent or self._USER_AGENT_FALLBACK,
|
|
99
|
+
"Accept": "application/json",
|
|
100
|
+
}
|
|
101
|
+
timeout = httpx.Timeout(connect=10.0, read=20.0, write=20.0, pool=10.0)
|
|
102
|
+
self._http_client = httpx.AsyncClient(base_url=self._API_BASE_URL, headers=headers, timeout=timeout)
|
|
103
|
+
await self.refresh_token(force=True)
|
|
104
|
+
|
|
105
|
+
async def stop(self) -> None:
|
|
106
|
+
if self._http_client:
|
|
107
|
+
await self._http_client.aclose()
|
|
108
|
+
self._http_client = None
|
|
109
|
+
self._token = None
|
|
110
|
+
|
|
111
|
+
async def update_credentials(self, credentials: RedditCredentials) -> None:
|
|
112
|
+
self._credentials = credentials
|
|
113
|
+
self._token = None
|
|
114
|
+
if self._http_client:
|
|
115
|
+
await self.refresh_token(force=True)
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Helpers
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
def _now(self) -> datetime:
|
|
121
|
+
if self._time_service:
|
|
122
|
+
return self._time_service.now()
|
|
123
|
+
return datetime.now(timezone.utc)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def token_active(self) -> bool:
|
|
127
|
+
return bool(self._token and not self._token.is_expired(self._now()))
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def metrics(self) -> Dict[str, float]:
|
|
131
|
+
return {"requests": float(self._request_count), "errors": float(self._error_count)}
|
|
132
|
+
|
|
133
|
+
def _add_ciris_attribution(self, text: str, *, max_length: int = 10000) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Add CIRIS attribution footer to post/comment text.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
text: Original post/comment text
|
|
139
|
+
max_length: Reddit character limit (10000 for posts/comments)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Text with attribution, truncated if necessary to fit within limit
|
|
143
|
+
|
|
144
|
+
Note: Reddit rejects posts/comments longer than 10,000 characters.
|
|
145
|
+
This method ensures attribution is always included by truncating
|
|
146
|
+
the original text if needed.
|
|
147
|
+
"""
|
|
148
|
+
attribution = (
|
|
149
|
+
"\n\n"
|
|
150
|
+
"Posted by a CIRIS agent, learn more at https://ciris.ai "
|
|
151
|
+
"or chat with scout at https://scout.ciris.ai"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# If text + attribution fits within limit, return as-is
|
|
155
|
+
if len(text) + len(attribution) <= max_length:
|
|
156
|
+
return text + attribution
|
|
157
|
+
|
|
158
|
+
# Otherwise, truncate text to make room for attribution
|
|
159
|
+
# Leave space for attribution + ellipsis + newline
|
|
160
|
+
truncation_marker = "...\n"
|
|
161
|
+
available_space = max_length - len(attribution) - len(truncation_marker)
|
|
162
|
+
|
|
163
|
+
if available_space < 100: # Sanity check: need at least 100 chars for meaningful content
|
|
164
|
+
# If attribution is too large for the limit, skip it (shouldn't happen with 10k limit)
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"Attribution footer ({len(attribution)} chars) too large for limit ({max_length}), "
|
|
167
|
+
"submitting without attribution"
|
|
168
|
+
)
|
|
169
|
+
return text[:max_length]
|
|
170
|
+
|
|
171
|
+
truncated_text = text[:available_space]
|
|
172
|
+
logger.info(
|
|
173
|
+
f"Truncated text from {len(text)} to {len(truncated_text)} chars to fit attribution "
|
|
174
|
+
f"within {max_length} char limit"
|
|
175
|
+
)
|
|
176
|
+
return truncated_text + truncation_marker + attribution
|
|
177
|
+
|
|
178
|
+
async def refresh_token(self, force: bool = False) -> bool:
|
|
179
|
+
async with self._token_lock:
|
|
180
|
+
if not force and self._token and not self._token.is_expired(self._now()):
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
auth = (self._credentials.client_id, self._credentials.client_secret)
|
|
184
|
+
data = {
|
|
185
|
+
"grant_type": "password",
|
|
186
|
+
"username": self._credentials.username,
|
|
187
|
+
"password": self._credentials.password,
|
|
188
|
+
}
|
|
189
|
+
headers = {"User-Agent": self._credentials.user_agent or self._USER_AGENT_FALLBACK}
|
|
190
|
+
|
|
191
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, read=20.0)) as client:
|
|
192
|
+
response = await client.post(self._TOKEN_URL, data=data, auth=auth, headers=headers)
|
|
193
|
+
|
|
194
|
+
if response.status_code >= 300:
|
|
195
|
+
self._error_count += 1
|
|
196
|
+
raise RuntimeError(f"Token request failed ({response.status_code}): {response.text}")
|
|
197
|
+
|
|
198
|
+
payload = self._expect_dict(response.json(), context="token")
|
|
199
|
+
access_token = self._get_str(payload, "access_token")
|
|
200
|
+
|
|
201
|
+
# Validate access token - empty token indicates auth failure (suspended account, invalid credentials)
|
|
202
|
+
if not access_token or access_token.strip() == "":
|
|
203
|
+
error_msg = payload.get("error", "Unknown error")
|
|
204
|
+
error_desc = payload.get("error_description", "No access_token in response")
|
|
205
|
+
logger.error(
|
|
206
|
+
f"Reddit OAuth failed - likely suspended account or invalid credentials. "
|
|
207
|
+
f"Error: {error_msg}, Description: {error_desc}, Response: {payload}"
|
|
208
|
+
)
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
f"Reddit authentication failed: {error_msg} - {error_desc}. "
|
|
211
|
+
"This may indicate a suspended Reddit account or invalid credentials."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
expires_in = int(float(self._get_str(payload, "expires_in", default="3600")))
|
|
215
|
+
expires_at = self._now() + timedelta(seconds=expires_in)
|
|
216
|
+
self._token = RedditToken(access_token=access_token, expires_at=expires_at)
|
|
217
|
+
logger.info("Refreshed Reddit OAuth token; expires at %s", expires_at.isoformat())
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
async def _request(
|
|
221
|
+
self,
|
|
222
|
+
method: str,
|
|
223
|
+
path: str,
|
|
224
|
+
*,
|
|
225
|
+
params: Optional[Dict[str, Any]] = None,
|
|
226
|
+
data: Optional[Dict[str, Any]] = None,
|
|
227
|
+
) -> httpx.Response:
|
|
228
|
+
if not self._http_client:
|
|
229
|
+
raise RuntimeError("HTTP client not initialized")
|
|
230
|
+
|
|
231
|
+
await self.refresh_token()
|
|
232
|
+
assert self._token is not None
|
|
233
|
+
headers = {"Authorization": f"bearer {self._token.access_token}"}
|
|
234
|
+
|
|
235
|
+
response = await self._http_client.request(method, path, params=params, data=data, headers=headers)
|
|
236
|
+
self._request_count += 1
|
|
237
|
+
|
|
238
|
+
if response.status_code == 401:
|
|
239
|
+
await self.refresh_token(force=True)
|
|
240
|
+
assert self._token is not None
|
|
241
|
+
headers["Authorization"] = f"bearer {self._token.access_token}"
|
|
242
|
+
response = await self._http_client.request(method, path, params=params, data=data, headers=headers)
|
|
243
|
+
|
|
244
|
+
if response.status_code == 429:
|
|
245
|
+
retry_after = float(response.headers.get("Retry-After", "1"))
|
|
246
|
+
await asyncio.sleep(max(retry_after, 0))
|
|
247
|
+
response = await self._http_client.request(method, path, params=params, data=data, headers=headers)
|
|
248
|
+
|
|
249
|
+
if response.status_code >= 400:
|
|
250
|
+
self._error_count += 1
|
|
251
|
+
return response
|
|
252
|
+
|
|
253
|
+
async def _request_json(
|
|
254
|
+
self,
|
|
255
|
+
method: str,
|
|
256
|
+
path: str,
|
|
257
|
+
*,
|
|
258
|
+
params: Optional[Dict[str, Any]] = None,
|
|
259
|
+
data: Optional[Dict[str, Any]] = None,
|
|
260
|
+
) -> JSONDict:
|
|
261
|
+
response = await self._request(method, path, params=params, data=data)
|
|
262
|
+
payload = response.json()
|
|
263
|
+
return self._expect_dict(payload, context=path)
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------------
|
|
266
|
+
# Expectation helpers
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
def _expect_dict(self, value: JSONValue, *, context: str) -> JSONDict:
|
|
269
|
+
if not isinstance(value, dict):
|
|
270
|
+
raise RuntimeError(f"{context}: expected object")
|
|
271
|
+
return value
|
|
272
|
+
|
|
273
|
+
def _get_str(self, data: JSONDict, key: str, *, default: str = "") -> str:
|
|
274
|
+
value = data.get(key, default)
|
|
275
|
+
if value is None:
|
|
276
|
+
return default
|
|
277
|
+
if isinstance(value, str):
|
|
278
|
+
return value
|
|
279
|
+
if isinstance(value, (int, float)):
|
|
280
|
+
return str(value)
|
|
281
|
+
return default
|
|
282
|
+
|
|
283
|
+
def _build_permalink(self, permalink: str) -> str:
|
|
284
|
+
if permalink.startswith("http"):
|
|
285
|
+
return permalink
|
|
286
|
+
return f"https://www.reddit.com{permalink}" if permalink else ""
|
|
287
|
+
|
|
288
|
+
def _strip_prefix(self, value: str, prefix: str) -> str:
|
|
289
|
+
if value.startswith(prefix):
|
|
290
|
+
return value[len(prefix) :]
|
|
291
|
+
return value
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
# High level API methods
|
|
295
|
+
# ------------------------------------------------------------------
|
|
296
|
+
async def fetch_user_context(self, request: RedditUserContextRequest) -> RedditUserContext:
|
|
297
|
+
about_payload = await self._request_json("GET", f"/user/{request.username}/about")
|
|
298
|
+
about_data = self._expect_dict(about_payload.get("data"), context="user_about.data")
|
|
299
|
+
|
|
300
|
+
account_created = datetime.fromtimestamp(
|
|
301
|
+
float(self._get_str(about_data, "created_utc", default="0")), tz=timezone.utc
|
|
302
|
+
)
|
|
303
|
+
context = RedditUserContext(
|
|
304
|
+
username=self._get_str(about_data, "name"),
|
|
305
|
+
user_id=self._get_str(about_data, "id"),
|
|
306
|
+
link_karma=int(float(self._get_str(about_data, "link_karma", default="0"))),
|
|
307
|
+
comment_karma=int(float(self._get_str(about_data, "comment_karma", default="0"))),
|
|
308
|
+
is_mod=bool(about_data.get("is_mod", False)),
|
|
309
|
+
account_created_at=account_created,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if request.include_history:
|
|
313
|
+
submissions = await self._fetch_listing(
|
|
314
|
+
f"/user/{request.username}/submitted", limit=request.history_limit, entry_type="submission"
|
|
315
|
+
)
|
|
316
|
+
comments = await self._fetch_listing(
|
|
317
|
+
f"/user/{request.username}/comments", limit=request.history_limit, entry_type="comment"
|
|
318
|
+
)
|
|
319
|
+
context.recent_posts = submissions
|
|
320
|
+
context.recent_comments = comments
|
|
321
|
+
|
|
322
|
+
return context
|
|
323
|
+
|
|
324
|
+
async def submit_post(self, request: RedditSubmitPostRequest) -> RedditSubmissionSummary:
|
|
325
|
+
subreddit = request.subreddit or self._credentials.subreddit
|
|
326
|
+
# Add CIRIS attribution to post body
|
|
327
|
+
body_with_attribution = self._add_ciris_attribution(request.body)
|
|
328
|
+
payload = {
|
|
329
|
+
"sr": subreddit,
|
|
330
|
+
"kind": "self",
|
|
331
|
+
"title": request.title,
|
|
332
|
+
"text": body_with_attribution,
|
|
333
|
+
"resubmit": "true",
|
|
334
|
+
"sendreplies": "true" if request.send_replies else "false",
|
|
335
|
+
}
|
|
336
|
+
if request.flair_id:
|
|
337
|
+
payload["flair_id"] = request.flair_id
|
|
338
|
+
if request.flair_text:
|
|
339
|
+
payload["flair_text"] = request.flair_text
|
|
340
|
+
if request.nsfw:
|
|
341
|
+
payload["nsfw"] = "true"
|
|
342
|
+
if request.spoiler:
|
|
343
|
+
payload["spoiler"] = "true"
|
|
344
|
+
|
|
345
|
+
response = await self._request("POST", "/api/submit", data=payload)
|
|
346
|
+
result = await self._parse_submission_response(
|
|
347
|
+
response, subreddit=subreddit, title=request.title, body=request.body
|
|
348
|
+
)
|
|
349
|
+
if not result:
|
|
350
|
+
raise RuntimeError("Submission failed")
|
|
351
|
+
return result.submission
|
|
352
|
+
|
|
353
|
+
async def submit_comment(self, request: RedditSubmitCommentRequest) -> RedditCommentSummary:
|
|
354
|
+
# Add CIRIS attribution to comment text
|
|
355
|
+
text_with_attribution = self._add_ciris_attribution(request.text)
|
|
356
|
+
payload = {"thing_id": request.parent_fullname, "text": text_with_attribution}
|
|
357
|
+
response = await self._request("POST", "/api/comment", data=payload)
|
|
358
|
+
comment = await self._parse_comment_response(response)
|
|
359
|
+
if not comment:
|
|
360
|
+
raise RuntimeError(
|
|
361
|
+
f"Comment response missing data - status: {response.status_code}, " f"text: {response.text[:200]}"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if request.lock_thread and comment.submission_id:
|
|
365
|
+
await self._request("POST", "/api/lock", data={"id": f"t3_{comment.submission_id}"})
|
|
366
|
+
return comment
|
|
367
|
+
|
|
368
|
+
async def remove_content(self, request: RedditRemoveContentRequest) -> RedditRemovalResult:
|
|
369
|
+
payload = {"id": request.thing_fullname, "spam": "true" if request.spam else "false"}
|
|
370
|
+
response = await self._request("POST", "/api/remove", data=payload)
|
|
371
|
+
if response.status_code >= 300:
|
|
372
|
+
raise RuntimeError(f"Removal failed ({response.status_code}): {response.text}")
|
|
373
|
+
return RedditRemovalResult(thing_fullname=request.thing_fullname, removed=True, spam=request.spam)
|
|
374
|
+
|
|
375
|
+
async def delete_content(self, thing_fullname: str) -> bool:
|
|
376
|
+
"""
|
|
377
|
+
Permanently delete content from Reddit (Reddit ToS compliance).
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
thing_fullname: Reddit thing fullname (t3_xxxxx or t1_xxxxx)
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
True if deletion successful
|
|
384
|
+
|
|
385
|
+
Note: This uses DELETE /api/del which permanently removes content.
|
|
386
|
+
This is different from remove_content which hides content.
|
|
387
|
+
"""
|
|
388
|
+
payload = {"id": thing_fullname}
|
|
389
|
+
response = await self._request("POST", "/api/del", data=payload)
|
|
390
|
+
if response.status_code >= 300:
|
|
391
|
+
raise RuntimeError(f"Deletion failed ({response.status_code}): {response.text}")
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
async def get_submission_summary(
|
|
395
|
+
self,
|
|
396
|
+
submission_id: str,
|
|
397
|
+
*,
|
|
398
|
+
include_comments: bool,
|
|
399
|
+
comment_limit: int,
|
|
400
|
+
) -> RedditSubmissionSummary:
|
|
401
|
+
fullname = f"t3_{submission_id}"
|
|
402
|
+
metadata = await self._fetch_item_metadata(fullname)
|
|
403
|
+
if not metadata:
|
|
404
|
+
raise RuntimeError("Submission not found")
|
|
405
|
+
return await self._build_submission_summary(
|
|
406
|
+
metadata, include_comments=include_comments, comment_limit=comment_limit
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
async def fetch_subreddit_new(self, subreddit: str, *, limit: int) -> List[RedditTimelineEntry]:
|
|
410
|
+
return await self._fetch_listing(f"/r/{subreddit}/new", limit=limit, entry_type="submission")
|
|
411
|
+
|
|
412
|
+
async def fetch_subreddit_comments(self, subreddit: str, *, limit: int) -> List[RedditTimelineEntry]:
|
|
413
|
+
return await self._fetch_listing(f"/r/{subreddit}/comments", limit=limit, entry_type="comment")
|
|
414
|
+
|
|
415
|
+
async def fetch_submission_comments(self, submission_id: str, *, limit: int) -> List[RedditCommentSummary]:
|
|
416
|
+
params = {"limit": str(limit)}
|
|
417
|
+
response = await self._request("GET", f"/comments/{submission_id}", params=params)
|
|
418
|
+
if response.status_code >= 300:
|
|
419
|
+
raise RuntimeError(f"Failed to fetch comments: {response.status_code}")
|
|
420
|
+
|
|
421
|
+
payload = response.json()
|
|
422
|
+
if not isinstance(payload, list) or len(payload) < 2:
|
|
423
|
+
return []
|
|
424
|
+
|
|
425
|
+
comments_listing = payload[1]
|
|
426
|
+
listing_data = self._expect_dict(comments_listing.get("data"), context="comments.data")
|
|
427
|
+
children = listing_data.get("children", [])
|
|
428
|
+
summaries: List[RedditCommentSummary] = []
|
|
429
|
+
if isinstance(children, list):
|
|
430
|
+
for child in children:
|
|
431
|
+
child_dict = self._expect_dict(child, context="comment.child")
|
|
432
|
+
child_data = self._expect_dict(child_dict.get("data"), context="comment.child.data")
|
|
433
|
+
summary = self._build_comment_summary(child_dict.get("kind"), child_data, submission_id=submission_id)
|
|
434
|
+
if summary:
|
|
435
|
+
summaries.append(summary)
|
|
436
|
+
if len(summaries) >= limit:
|
|
437
|
+
break
|
|
438
|
+
return summaries
|
|
439
|
+
|
|
440
|
+
async def fetch_user_activity(self, username: str, *, limit: int) -> RedditTimelineResponse:
|
|
441
|
+
posts = await self._fetch_listing(f"/user/{username}/submitted", limit=limit, entry_type="submission")
|
|
442
|
+
comments = await self._fetch_listing(f"/user/{username}/comments", limit=limit, entry_type="comment")
|
|
443
|
+
return RedditTimelineResponse(entries=posts + comments)
|
|
444
|
+
|
|
445
|
+
# ------------------------------------------------------------------
|
|
446
|
+
# Parsing helpers
|
|
447
|
+
# ------------------------------------------------------------------
|
|
448
|
+
async def _parse_submission_response(
|
|
449
|
+
self, response: httpx.Response, *, subreddit: str, title: str, body: str
|
|
450
|
+
) -> Optional[RedditPostResult]:
|
|
451
|
+
if response.status_code >= 300:
|
|
452
|
+
raise RuntimeError(f"Submission failed ({response.status_code}): {response.text}")
|
|
453
|
+
|
|
454
|
+
payload = self._expect_dict(response.json(), context="submit")
|
|
455
|
+
json_data = payload.get("json")
|
|
456
|
+
if not isinstance(json_data, dict):
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
errors = json_data.get("errors", [])
|
|
460
|
+
if isinstance(errors, list) and errors:
|
|
461
|
+
raise RuntimeError(f"Reddit returned errors: {errors}")
|
|
462
|
+
|
|
463
|
+
data_dict = self._expect_dict(json_data.get("data"), context="submit.data")
|
|
464
|
+
submission_id = self._strip_prefix(self._get_str(data_dict, "id"), prefix="t3_")
|
|
465
|
+
fullname = self._get_str(data_dict, "name") or f"t3_{submission_id}"
|
|
466
|
+
url = self._get_str(data_dict, "url")
|
|
467
|
+
return RedditPostResult(
|
|
468
|
+
submission=RedditSubmissionSummary(
|
|
469
|
+
submission_id=submission_id,
|
|
470
|
+
fullname=fullname,
|
|
471
|
+
title=title,
|
|
472
|
+
self_text=body,
|
|
473
|
+
url=url,
|
|
474
|
+
subreddit=subreddit,
|
|
475
|
+
author=self._credentials.username,
|
|
476
|
+
score=1,
|
|
477
|
+
num_comments=0,
|
|
478
|
+
created_at=self._now(),
|
|
479
|
+
permalink=url,
|
|
480
|
+
channel_reference=_build_channel_reference(subreddit, submission_id),
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
async def _parse_comment_response(self, response: httpx.Response) -> Optional[RedditCommentSummary]:
|
|
485
|
+
if response.status_code >= 300:
|
|
486
|
+
raise RuntimeError(f"Comment failed ({response.status_code}): {response.text}")
|
|
487
|
+
|
|
488
|
+
payload = self._expect_dict(response.json(), context="comment")
|
|
489
|
+
json_data = payload.get("json")
|
|
490
|
+
if not isinstance(json_data, dict):
|
|
491
|
+
logger.error(f"Comment response missing 'json' dict: {payload}")
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
errors = json_data.get("errors", [])
|
|
495
|
+
if isinstance(errors, list) and errors:
|
|
496
|
+
raise RuntimeError(f"Reddit returned errors: {errors}")
|
|
497
|
+
|
|
498
|
+
data_dict = self._expect_dict(json_data.get("data"), context="comment.data")
|
|
499
|
+
things = data_dict.get("things", [])
|
|
500
|
+
if not isinstance(things, list) or not things:
|
|
501
|
+
logger.error(f"Comment response missing 'things' list: json_data={json_data}")
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
first = self._expect_dict(things[0], context="comment.thing")
|
|
505
|
+
comment_data = self._expect_dict(first.get("data"), context="comment.thing.data")
|
|
506
|
+
subreddit = self._get_str(comment_data, "subreddit")
|
|
507
|
+
submission_id = self._strip_prefix(self._get_str(comment_data, "link_id"), prefix="t3_")
|
|
508
|
+
comment_id = self._get_str(comment_data, "id")
|
|
509
|
+
permalink = self._build_permalink(self._get_str(comment_data, "permalink"))
|
|
510
|
+
return RedditCommentSummary(
|
|
511
|
+
comment_id=comment_id,
|
|
512
|
+
fullname=self._get_str(comment_data, "name"),
|
|
513
|
+
submission_id=submission_id,
|
|
514
|
+
body=self._get_str(comment_data, "body"),
|
|
515
|
+
author=self._get_str(comment_data, "author"),
|
|
516
|
+
subreddit=subreddit,
|
|
517
|
+
permalink=permalink,
|
|
518
|
+
created_at=datetime.fromtimestamp(
|
|
519
|
+
float(self._get_str(comment_data, "created_utc", default="0")), tz=timezone.utc
|
|
520
|
+
),
|
|
521
|
+
score=int(float(self._get_str(comment_data, "score", default="0"))),
|
|
522
|
+
channel_reference=_build_channel_reference(subreddit, submission_id, comment_id),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
async def _fetch_item_metadata(self, fullname: str) -> Optional[JSONDict]:
|
|
526
|
+
response = await self._request("GET", "/api/info", params={"id": fullname})
|
|
527
|
+
if response.status_code >= 300:
|
|
528
|
+
raise RuntimeError(f"Failed to fetch metadata ({response.status_code}): {response.text}")
|
|
529
|
+
|
|
530
|
+
payload = self._expect_dict(response.json(), context="info")
|
|
531
|
+
data = self._expect_dict(payload.get("data"), context="info.data")
|
|
532
|
+
children = data.get("children", [])
|
|
533
|
+
if not isinstance(children, list) or not children:
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
first = self._expect_dict(children[0], context="info.child")
|
|
537
|
+
return self._expect_dict(first.get("data"), context="info.child.data")
|
|
538
|
+
|
|
539
|
+
async def _build_submission_summary(
|
|
540
|
+
self,
|
|
541
|
+
data: JSONDict,
|
|
542
|
+
*,
|
|
543
|
+
include_comments: bool,
|
|
544
|
+
comment_limit: int,
|
|
545
|
+
) -> RedditSubmissionSummary:
|
|
546
|
+
subreddit = self._get_str(data, "subreddit")
|
|
547
|
+
submission_id = self._get_str(data, "id")
|
|
548
|
+
permalink = self._build_permalink(self._get_str(data, "permalink"))
|
|
549
|
+
summary = RedditSubmissionSummary(
|
|
550
|
+
submission_id=submission_id,
|
|
551
|
+
fullname=self._get_str(data, "name"),
|
|
552
|
+
title=self._get_str(data, "title"),
|
|
553
|
+
self_text=self._get_str(data, "selftext"),
|
|
554
|
+
url=self._get_str(data, "url"),
|
|
555
|
+
subreddit=subreddit,
|
|
556
|
+
author=self._get_str(data, "author"),
|
|
557
|
+
score=int(float(self._get_str(data, "score", default="0"))),
|
|
558
|
+
num_comments=int(float(self._get_str(data, "num_comments", default="0"))),
|
|
559
|
+
created_at=datetime.fromtimestamp(float(self._get_str(data, "created_utc", default="0")), tz=timezone.utc),
|
|
560
|
+
permalink=permalink,
|
|
561
|
+
channel_reference=_build_channel_reference(subreddit, submission_id),
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if include_comments:
|
|
565
|
+
summary.top_comments = await self.fetch_submission_comments(submission_id, limit=comment_limit)
|
|
566
|
+
return summary
|
|
567
|
+
|
|
568
|
+
async def _fetch_listing(self, path: str, *, limit: int, entry_type: str) -> List[RedditTimelineEntry]:
|
|
569
|
+
payload = await self._request_json("GET", path, params={"limit": str(limit)})
|
|
570
|
+
data = self._expect_dict(payload.get("data"), context="listing.data")
|
|
571
|
+
children_value = data.get("children", [])
|
|
572
|
+
entries: List[RedditTimelineEntry] = []
|
|
573
|
+
|
|
574
|
+
if isinstance(children_value, list):
|
|
575
|
+
for child in children_value:
|
|
576
|
+
child_dict = self._expect_dict(child, context="listing.child")
|
|
577
|
+
child_data = self._expect_dict(child_dict.get("data"), context="listing.child.data")
|
|
578
|
+
entry = self._build_timeline_entry(child_dict.get("kind"), child_data)
|
|
579
|
+
if entry and entry.entry_type == entry_type:
|
|
580
|
+
entries.append(entry)
|
|
581
|
+
if len(entries) >= limit:
|
|
582
|
+
break
|
|
583
|
+
|
|
584
|
+
return entries
|
|
585
|
+
|
|
586
|
+
def _build_timeline_entry(self, kind_value: JSONValue, child_data: JSONDict) -> Optional[RedditTimelineEntry]:
|
|
587
|
+
if not isinstance(kind_value, str):
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
created = datetime.fromtimestamp(float(self._get_str(child_data, "created_utc", default="0")), tz=timezone.utc)
|
|
591
|
+
permalink = self._build_permalink(self._get_str(child_data, "permalink"))
|
|
592
|
+
subreddit = self._get_str(child_data, "subreddit")
|
|
593
|
+
fullname = self._get_str(child_data, "name")
|
|
594
|
+
item_id = self._strip_prefix(fullname, prefix="t3_" if kind_value == "t3" else "t1_")
|
|
595
|
+
score = int(float(self._get_str(child_data, "score", default="0")))
|
|
596
|
+
|
|
597
|
+
if kind_value == "t3":
|
|
598
|
+
return RedditTimelineEntry(
|
|
599
|
+
entry_type="submission",
|
|
600
|
+
item_id=item_id,
|
|
601
|
+
fullname=fullname,
|
|
602
|
+
subreddit=subreddit,
|
|
603
|
+
permalink=permalink,
|
|
604
|
+
score=score,
|
|
605
|
+
created_at=created,
|
|
606
|
+
channel_reference=_build_channel_reference(subreddit, item_id),
|
|
607
|
+
author=self._get_str(child_data, "author"),
|
|
608
|
+
title=self._get_str(child_data, "title"),
|
|
609
|
+
body=self._get_str(child_data, "selftext"),
|
|
610
|
+
url=self._get_str(child_data, "url"),
|
|
611
|
+
)
|
|
612
|
+
if kind_value == "t1":
|
|
613
|
+
submission_id = self._strip_prefix(self._get_str(child_data, "link_id"), prefix="t3_")
|
|
614
|
+
parent_id = self._get_str(child_data, "parent_id", default="") or None
|
|
615
|
+
return RedditTimelineEntry(
|
|
616
|
+
entry_type="comment",
|
|
617
|
+
item_id=item_id,
|
|
618
|
+
fullname=fullname,
|
|
619
|
+
subreddit=subreddit,
|
|
620
|
+
permalink=permalink,
|
|
621
|
+
score=score,
|
|
622
|
+
created_at=created,
|
|
623
|
+
channel_reference=_build_channel_reference(subreddit, submission_id, item_id),
|
|
624
|
+
author=self._get_str(child_data, "author"),
|
|
625
|
+
body=self._get_str(child_data, "body"),
|
|
626
|
+
parent_id=parent_id,
|
|
627
|
+
)
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
def _build_comment_summary(
|
|
631
|
+
self,
|
|
632
|
+
kind_value: JSONValue,
|
|
633
|
+
comment_data: JSONDict,
|
|
634
|
+
*,
|
|
635
|
+
submission_id: str,
|
|
636
|
+
) -> Optional[RedditCommentSummary]:
|
|
637
|
+
if kind_value != "t1":
|
|
638
|
+
return None
|
|
639
|
+
comment_id = self._get_str(comment_data, "id")
|
|
640
|
+
subreddit = self._get_str(comment_data, "subreddit")
|
|
641
|
+
permalink = self._build_permalink(self._get_str(comment_data, "permalink"))
|
|
642
|
+
parent_id = self._get_str(comment_data, "parent_id", default="") or None
|
|
643
|
+
return RedditCommentSummary(
|
|
644
|
+
comment_id=comment_id,
|
|
645
|
+
fullname=self._get_str(comment_data, "name"),
|
|
646
|
+
submission_id=submission_id,
|
|
647
|
+
body=self._get_str(comment_data, "body"),
|
|
648
|
+
author=self._get_str(comment_data, "author"),
|
|
649
|
+
subreddit=subreddit,
|
|
650
|
+
permalink=permalink,
|
|
651
|
+
created_at=datetime.fromtimestamp(
|
|
652
|
+
float(self._get_str(comment_data, "created_utc", default="0")), tz=timezone.utc
|
|
653
|
+
),
|
|
654
|
+
score=int(float(self._get_str(comment_data, "score", default="0"))),
|
|
655
|
+
channel_reference=_build_channel_reference(subreddit, submission_id, comment_id),
|
|
656
|
+
parent_id=parent_id,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
# ----------------------------------------------------------------------
|
|
661
|
+
# Base service shared by tool and communication implementations
|
|
662
|
+
# ----------------------------------------------------------------------
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class RedditServiceBase(BaseService, RedditOAuthProtocol):
|
|
666
|
+
"""Base class providing shared Reddit API functionality."""
|
|
667
|
+
|
|
668
|
+
def __init__(
|
|
669
|
+
self,
|
|
670
|
+
credentials: Optional[RedditCredentials] = None,
|
|
671
|
+
*,
|
|
672
|
+
time_service: Optional[TimeServiceProtocol] = None,
|
|
673
|
+
service_name: str,
|
|
674
|
+
) -> None:
|
|
675
|
+
super().__init__(time_service=time_service, service_name=service_name, version="1.0.0")
|
|
676
|
+
resolved_credentials = credentials or RedditCredentials.from_env()
|
|
677
|
+
if not resolved_credentials:
|
|
678
|
+
raise RuntimeError("Reddit credentials are not configured")
|
|
679
|
+
self._credentials = resolved_credentials
|
|
680
|
+
self._client = RedditAPIClient(self._credentials, time_service=time_service)
|
|
681
|
+
self._subreddit = RedditChannelReference._normalize_subreddit(self._credentials.subreddit)
|
|
682
|
+
|
|
683
|
+
# BaseService overrides -------------------------------------------------
|
|
684
|
+
def _check_dependencies(self) -> bool:
|
|
685
|
+
return self._credentials is not None and self._credentials.is_complete()
|
|
686
|
+
|
|
687
|
+
async def _on_start(self) -> None:
|
|
688
|
+
await self._client.start()
|
|
689
|
+
|
|
690
|
+
async def _on_stop(self) -> None:
|
|
691
|
+
await self._client.stop()
|
|
692
|
+
|
|
693
|
+
def _collect_custom_metrics(self) -> Dict[str, float]:
|
|
694
|
+
metrics = self._client.metrics
|
|
695
|
+
metrics["token_active"] = 1.0 if self._client.token_active else 0.0
|
|
696
|
+
return metrics
|
|
697
|
+
|
|
698
|
+
def _register_dependencies(self) -> None:
|
|
699
|
+
super()._register_dependencies()
|
|
700
|
+
self._dependencies.add("httpx")
|
|
701
|
+
|
|
702
|
+
# RedditOAuthProtocol ---------------------------------------------------
|
|
703
|
+
async def update_credentials(self, credentials: RedditCredentials) -> None:
|
|
704
|
+
self._credentials = credentials
|
|
705
|
+
self._subreddit = RedditChannelReference._normalize_subreddit(credentials.subreddit)
|
|
706
|
+
await self._client.update_credentials(credentials)
|
|
707
|
+
|
|
708
|
+
async def refresh_token(self, force: bool = False) -> bool:
|
|
709
|
+
try:
|
|
710
|
+
return await self._client.refresh_token(force=force)
|
|
711
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
712
|
+
self._track_error(exc)
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
# ----------------------------------------------------------------------
|
|
717
|
+
# Tool service implementation
|
|
718
|
+
# ----------------------------------------------------------------------
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class RedditToolService(RedditServiceBase):
|
|
722
|
+
"""Tool service providing Reddit moderation and outreach utilities."""
|
|
723
|
+
|
|
724
|
+
def __init__(
|
|
725
|
+
self,
|
|
726
|
+
credentials: Optional[RedditCredentials] = None,
|
|
727
|
+
*,
|
|
728
|
+
time_service: Optional[TimeServiceProtocol] = None,
|
|
729
|
+
) -> None:
|
|
730
|
+
super().__init__(credentials, time_service=time_service, service_name="RedditToolService")
|
|
731
|
+
self._results: Dict[str, ToolExecutionResult] = {}
|
|
732
|
+
self._tool_handlers = {
|
|
733
|
+
"reddit_get_user_context": self._tool_get_user_context,
|
|
734
|
+
"reddit_submit_post": self._tool_submit_post,
|
|
735
|
+
"reddit_submit_comment": self._tool_submit_comment,
|
|
736
|
+
"reddit_remove_content": self._tool_remove_content,
|
|
737
|
+
"reddit_get_submission": self._tool_get_submission,
|
|
738
|
+
"reddit_observe": self._tool_observe,
|
|
739
|
+
"reddit_delete_content": self._tool_delete_content,
|
|
740
|
+
"reddit_disclose_identity": self._tool_disclose_identity,
|
|
741
|
+
}
|
|
742
|
+
self._request_models: Dict[str, type[BaseModel]] = {
|
|
743
|
+
"reddit_get_user_context": RedditUserContextRequest,
|
|
744
|
+
"reddit_submit_post": RedditSubmitPostRequest,
|
|
745
|
+
"reddit_submit_comment": RedditSubmitCommentRequest,
|
|
746
|
+
"reddit_remove_content": RedditRemoveContentRequest,
|
|
747
|
+
"reddit_get_submission": RedditGetSubmissionRequest,
|
|
748
|
+
"reddit_delete_content": RedditDeleteContentRequest,
|
|
749
|
+
"reddit_disclose_identity": RedditDisclosureRequest,
|
|
750
|
+
}
|
|
751
|
+
# Deletion status tracking (DSAR pattern)
|
|
752
|
+
self._deletion_statuses: Dict[str, RedditDeletionStatus] = {}
|
|
753
|
+
self._tool_schemas = self._build_tool_schemas()
|
|
754
|
+
self._tool_info = self._build_tool_info()
|
|
755
|
+
self._executions = 0
|
|
756
|
+
self._failures = 0
|
|
757
|
+
|
|
758
|
+
# BaseService -----------------------------------------------------------
|
|
759
|
+
def get_service_type(self) -> ServiceType:
|
|
760
|
+
return ServiceType.TOOL
|
|
761
|
+
|
|
762
|
+
def _get_actions(self) -> List[str]:
|
|
763
|
+
return list(self._tool_handlers.keys())
|
|
764
|
+
|
|
765
|
+
# ToolServiceProtocol ---------------------------------------------------
|
|
766
|
+
async def execute_tool(self, tool_name: str, parameters: JSONDict) -> ToolExecutionResult:
|
|
767
|
+
self._track_request()
|
|
768
|
+
self._executions += 1
|
|
769
|
+
|
|
770
|
+
correlation_id_raw = parameters.get("correlation_id")
|
|
771
|
+
correlation_id = str(correlation_id_raw) if correlation_id_raw else str(uuid.uuid4())
|
|
772
|
+
|
|
773
|
+
handler = self._tool_handlers.get(tool_name)
|
|
774
|
+
if not handler:
|
|
775
|
+
self._failures += 1
|
|
776
|
+
result = ToolExecutionResult(
|
|
777
|
+
tool_name=tool_name,
|
|
778
|
+
status=ToolExecutionStatus.NOT_FOUND,
|
|
779
|
+
success=False,
|
|
780
|
+
data=None,
|
|
781
|
+
error=f"Unknown Reddit tool: {tool_name}",
|
|
782
|
+
correlation_id=correlation_id,
|
|
783
|
+
)
|
|
784
|
+
self._results[correlation_id] = result
|
|
785
|
+
return result
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
result = await handler(parameters, correlation_id)
|
|
789
|
+
if not result.success:
|
|
790
|
+
self._failures += 1
|
|
791
|
+
self._results[correlation_id] = result
|
|
792
|
+
return result
|
|
793
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
794
|
+
self._failures += 1
|
|
795
|
+
self._track_error(exc)
|
|
796
|
+
result = ToolExecutionResult(
|
|
797
|
+
tool_name=tool_name,
|
|
798
|
+
status=ToolExecutionStatus.FAILED,
|
|
799
|
+
success=False,
|
|
800
|
+
data=None,
|
|
801
|
+
error=str(exc),
|
|
802
|
+
correlation_id=correlation_id,
|
|
803
|
+
)
|
|
804
|
+
self._results[correlation_id] = result
|
|
805
|
+
return result
|
|
806
|
+
|
|
807
|
+
async def list_tools(self) -> List[str]:
|
|
808
|
+
return list(self._tool_handlers.keys())
|
|
809
|
+
|
|
810
|
+
async def get_tool_schema(self, tool_name: str) -> Optional[ToolParameterSchema]:
|
|
811
|
+
return self._tool_schemas.get(tool_name)
|
|
812
|
+
|
|
813
|
+
async def get_available_tools(self) -> List[str]:
|
|
814
|
+
return await self.list_tools()
|
|
815
|
+
|
|
816
|
+
async def get_tool_info(self, tool_name: str) -> Optional[ToolInfo]:
|
|
817
|
+
return self._tool_info.get(tool_name)
|
|
818
|
+
|
|
819
|
+
async def get_all_tool_info(self) -> List[ToolInfo]:
|
|
820
|
+
return list(self._tool_info.values())
|
|
821
|
+
|
|
822
|
+
async def validate_parameters(self, tool_name: str, parameters: JSONDict) -> bool:
|
|
823
|
+
model_cls = self._request_models.get(tool_name)
|
|
824
|
+
if not model_cls:
|
|
825
|
+
return False
|
|
826
|
+
try:
|
|
827
|
+
model_cls.model_validate(parameters)
|
|
828
|
+
return True
|
|
829
|
+
except ValidationError:
|
|
830
|
+
return False
|
|
831
|
+
|
|
832
|
+
async def get_tool_result(self, correlation_id: str, timeout: float = 30.0) -> Optional[ToolExecutionResult]:
|
|
833
|
+
return self._results.get(correlation_id)
|
|
834
|
+
|
|
835
|
+
def _collect_custom_metrics(self) -> Dict[str, float]:
|
|
836
|
+
metrics = super()._collect_custom_metrics()
|
|
837
|
+
metrics.update({"tool_executions": float(self._executions), "tool_failures": float(self._failures)})
|
|
838
|
+
return metrics
|
|
839
|
+
|
|
840
|
+
# Tool handlers ---------------------------------------------------------
|
|
841
|
+
async def _tool_get_user_context(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
842
|
+
try:
|
|
843
|
+
request = RedditUserContextRequest.model_validate(parameters)
|
|
844
|
+
except ValidationError as error:
|
|
845
|
+
return self._validation_error_result("reddit_get_user_context", correlation_id, error)
|
|
846
|
+
|
|
847
|
+
try:
|
|
848
|
+
context = await self._client.fetch_user_context(request)
|
|
849
|
+
except Exception as exc:
|
|
850
|
+
return self._api_error_result("reddit_get_user_context", correlation_id, str(exc))
|
|
851
|
+
|
|
852
|
+
return ToolExecutionResult(
|
|
853
|
+
tool_name="reddit_get_user_context",
|
|
854
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
855
|
+
success=True,
|
|
856
|
+
data=context.model_dump(mode="json"),
|
|
857
|
+
error=None,
|
|
858
|
+
correlation_id=correlation_id,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
async def _tool_submit_post(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
862
|
+
try:
|
|
863
|
+
request = RedditSubmitPostRequest.model_validate(parameters)
|
|
864
|
+
except ValidationError as error:
|
|
865
|
+
return self._validation_error_result("reddit_submit_post", correlation_id, error)
|
|
866
|
+
|
|
867
|
+
try:
|
|
868
|
+
summary = await self._client.submit_post(request)
|
|
869
|
+
except Exception as exc:
|
|
870
|
+
return self._api_error_result("reddit_submit_post", correlation_id, str(exc))
|
|
871
|
+
|
|
872
|
+
return ToolExecutionResult(
|
|
873
|
+
tool_name="reddit_submit_post",
|
|
874
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
875
|
+
success=True,
|
|
876
|
+
data=summary.model_dump(mode="json"),
|
|
877
|
+
error=None,
|
|
878
|
+
correlation_id=correlation_id,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
async def _tool_submit_comment(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
882
|
+
try:
|
|
883
|
+
request = RedditSubmitCommentRequest.model_validate(parameters)
|
|
884
|
+
except ValidationError as error:
|
|
885
|
+
return self._validation_error_result("reddit_submit_comment", correlation_id, error)
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
comment = await self._client.submit_comment(request)
|
|
889
|
+
except Exception as exc:
|
|
890
|
+
return self._api_error_result("reddit_submit_comment", correlation_id, str(exc))
|
|
891
|
+
|
|
892
|
+
return ToolExecutionResult(
|
|
893
|
+
tool_name="reddit_submit_comment",
|
|
894
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
895
|
+
success=True,
|
|
896
|
+
data=comment.model_dump(mode="json"),
|
|
897
|
+
error=None,
|
|
898
|
+
correlation_id=correlation_id,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
async def _tool_remove_content(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
902
|
+
try:
|
|
903
|
+
request = RedditRemoveContentRequest.model_validate(parameters)
|
|
904
|
+
except ValidationError as error:
|
|
905
|
+
return self._validation_error_result("reddit_remove_content", correlation_id, error)
|
|
906
|
+
|
|
907
|
+
try:
|
|
908
|
+
removal_result = await self._client.remove_content(request)
|
|
909
|
+
except Exception as exc:
|
|
910
|
+
return self._api_error_result("reddit_remove_content", correlation_id, str(exc))
|
|
911
|
+
|
|
912
|
+
return ToolExecutionResult(
|
|
913
|
+
tool_name="reddit_remove_content",
|
|
914
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
915
|
+
success=True,
|
|
916
|
+
data=removal_result.model_dump(mode="json"),
|
|
917
|
+
error=None,
|
|
918
|
+
correlation_id=correlation_id,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
async def _tool_get_submission(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
922
|
+
try:
|
|
923
|
+
request = RedditGetSubmissionRequest.model_validate(parameters)
|
|
924
|
+
except ValidationError as error:
|
|
925
|
+
return self._validation_error_result("reddit_get_submission", correlation_id, error)
|
|
926
|
+
|
|
927
|
+
submission_id = request.submission_id
|
|
928
|
+
if not submission_id and request.permalink:
|
|
929
|
+
submission_id = self._extract_submission_id_from_permalink(request.permalink)
|
|
930
|
+
if not submission_id:
|
|
931
|
+
return self._api_error_result("reddit_get_submission", correlation_id, "Unable to determine submission ID")
|
|
932
|
+
|
|
933
|
+
try:
|
|
934
|
+
summary = await self._client.get_submission_summary(
|
|
935
|
+
submission_id,
|
|
936
|
+
include_comments=request.include_comments,
|
|
937
|
+
comment_limit=request.comment_limit,
|
|
938
|
+
)
|
|
939
|
+
except Exception as exc:
|
|
940
|
+
return self._api_error_result("reddit_get_submission", correlation_id, str(exc))
|
|
941
|
+
|
|
942
|
+
return ToolExecutionResult(
|
|
943
|
+
tool_name="reddit_get_submission",
|
|
944
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
945
|
+
success=True,
|
|
946
|
+
data=summary.model_dump(mode="json"),
|
|
947
|
+
error=None,
|
|
948
|
+
correlation_id=correlation_id,
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
async def _tool_observe(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
952
|
+
target = parameters.get("channel_reference")
|
|
953
|
+
if not isinstance(target, str):
|
|
954
|
+
return self._api_error_result("reddit_observe", correlation_id, "channel_reference is required")
|
|
955
|
+
|
|
956
|
+
limit_value = parameters.get("limit", 25)
|
|
957
|
+
try:
|
|
958
|
+
# Handle various types that might come from parameters
|
|
959
|
+
if isinstance(limit_value, int):
|
|
960
|
+
limit = limit_value
|
|
961
|
+
elif isinstance(limit_value, (float, str)):
|
|
962
|
+
limit = int(float(limit_value))
|
|
963
|
+
else:
|
|
964
|
+
limit = 25
|
|
965
|
+
except (TypeError, ValueError):
|
|
966
|
+
limit = 25
|
|
967
|
+
|
|
968
|
+
try:
|
|
969
|
+
reference = RedditChannelReference.parse(target)
|
|
970
|
+
except ValueError as exc:
|
|
971
|
+
return self._api_error_result("reddit_observe", correlation_id, str(exc))
|
|
972
|
+
|
|
973
|
+
try:
|
|
974
|
+
payload = await self._active_observe(reference, limit=limit)
|
|
975
|
+
except Exception as exc:
|
|
976
|
+
return self._api_error_result("reddit_observe", correlation_id, str(exc))
|
|
977
|
+
|
|
978
|
+
return ToolExecutionResult(
|
|
979
|
+
tool_name="reddit_observe",
|
|
980
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
981
|
+
success=True,
|
|
982
|
+
data=payload.model_dump(mode="json"),
|
|
983
|
+
error=None,
|
|
984
|
+
correlation_id=correlation_id,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
async def _tool_delete_content(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
988
|
+
"""
|
|
989
|
+
Permanently delete Reddit content (Reddit ToS compliance).
|
|
990
|
+
|
|
991
|
+
Reddit ToS Requirement: Zero retention of deleted content.
|
|
992
|
+
"""
|
|
993
|
+
try:
|
|
994
|
+
request = RedditDeleteContentRequest.model_validate(parameters)
|
|
995
|
+
except ValidationError as error:
|
|
996
|
+
return self._validation_error_result("reddit_delete_content", correlation_id, error)
|
|
997
|
+
|
|
998
|
+
now = datetime.now(timezone.utc)
|
|
999
|
+
content_id = request.thing_fullname
|
|
1000
|
+
content_type = "submission" if content_id.startswith("t3_") else "comment"
|
|
1001
|
+
|
|
1002
|
+
try:
|
|
1003
|
+
# Phase 1: Delete from Reddit
|
|
1004
|
+
deletion_confirmed = await self._client.delete_content(content_id)
|
|
1005
|
+
|
|
1006
|
+
# Phase 2: Purge from local cache (Reddit ToS compliance)
|
|
1007
|
+
cache_purged = False
|
|
1008
|
+
if request.purge_cache and hasattr(self, "_client"):
|
|
1009
|
+
# NOTE: Cache purge logic would go here if cache exists
|
|
1010
|
+
# For now, we just mark as purged since there's no cache in base client
|
|
1011
|
+
cache_purged = True
|
|
1012
|
+
|
|
1013
|
+
# Phase 3: Create audit trail entry
|
|
1014
|
+
audit_entry_id = str(uuid.uuid4())
|
|
1015
|
+
|
|
1016
|
+
# Track deletion status (DSAR pattern)
|
|
1017
|
+
deletion_status = RedditDeletionStatus(
|
|
1018
|
+
content_id=content_id,
|
|
1019
|
+
initiated_at=now,
|
|
1020
|
+
completed_at=now if (deletion_confirmed and cache_purged) else None,
|
|
1021
|
+
deletion_confirmed=deletion_confirmed,
|
|
1022
|
+
cache_purged=cache_purged,
|
|
1023
|
+
audit_trail_updated=True,
|
|
1024
|
+
)
|
|
1025
|
+
self._deletion_statuses[content_id] = deletion_status
|
|
1026
|
+
|
|
1027
|
+
deletion_result = RedditDeletionResult(
|
|
1028
|
+
content_id=content_id,
|
|
1029
|
+
content_type=content_type,
|
|
1030
|
+
deleted_from_reddit=deletion_confirmed,
|
|
1031
|
+
purged_from_cache=cache_purged,
|
|
1032
|
+
audit_entry_id=audit_entry_id,
|
|
1033
|
+
deleted_at=now,
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
logger.info(f"Deleted Reddit {content_type} {content_id} (ToS compliance)")
|
|
1037
|
+
|
|
1038
|
+
except Exception as exc:
|
|
1039
|
+
return self._api_error_result("reddit_delete_content", correlation_id, str(exc))
|
|
1040
|
+
|
|
1041
|
+
return ToolExecutionResult(
|
|
1042
|
+
tool_name="reddit_delete_content",
|
|
1043
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
1044
|
+
success=True,
|
|
1045
|
+
data=deletion_result.model_dump(mode="json"),
|
|
1046
|
+
error=None,
|
|
1047
|
+
correlation_id=correlation_id,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
async def _tool_disclose_identity(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
|
|
1051
|
+
"""
|
|
1052
|
+
Post AI transparency disclosure (Reddit community guidelines compliance).
|
|
1053
|
+
"""
|
|
1054
|
+
try:
|
|
1055
|
+
request = RedditDisclosureRequest.model_validate(parameters)
|
|
1056
|
+
except ValidationError as error:
|
|
1057
|
+
return self._validation_error_result("reddit_disclose_identity", correlation_id, error)
|
|
1058
|
+
|
|
1059
|
+
# Default disclosure message
|
|
1060
|
+
default_message = (
|
|
1061
|
+
"Hello! I'm CIRIS, an AI assistant helping moderate this community.\n\n"
|
|
1062
|
+
"I can help with content moderation, but all major decisions are reviewed "
|
|
1063
|
+
"by human moderators. If you have concerns, please contact the mod team."
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
# Disclosure footer (always appended)
|
|
1067
|
+
disclosure_footer = (
|
|
1068
|
+
"\n\n---\n"
|
|
1069
|
+
"*I am CIRIS, an AI moderation assistant. "
|
|
1070
|
+
"[Learn more](https://ciris.ai) | [Report issues](https://ciris.ai/report)*"
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
comment_text = (request.custom_message or default_message) + disclosure_footer
|
|
1074
|
+
|
|
1075
|
+
try:
|
|
1076
|
+
# Parse channel reference
|
|
1077
|
+
reference = RedditChannelReference.parse(request.channel_reference)
|
|
1078
|
+
|
|
1079
|
+
# Determine submission ID for comment
|
|
1080
|
+
submission_id = reference.submission_id
|
|
1081
|
+
if not submission_id:
|
|
1082
|
+
return self._api_error_result(
|
|
1083
|
+
"reddit_disclose_identity", correlation_id, "Disclosure requires submission ID in channel reference"
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
# Post disclosure as comment
|
|
1087
|
+
comment_request = RedditSubmitCommentRequest(
|
|
1088
|
+
parent_fullname=f"t3_{submission_id}",
|
|
1089
|
+
text=comment_text,
|
|
1090
|
+
lock_thread=False,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
comment_result = await self._client.submit_comment(comment_request)
|
|
1094
|
+
|
|
1095
|
+
logger.info(f"Posted AI disclosure to {request.channel_reference}")
|
|
1096
|
+
|
|
1097
|
+
except Exception as exc:
|
|
1098
|
+
return self._api_error_result("reddit_disclose_identity", correlation_id, str(exc))
|
|
1099
|
+
|
|
1100
|
+
return ToolExecutionResult(
|
|
1101
|
+
tool_name="reddit_disclose_identity",
|
|
1102
|
+
status=ToolExecutionStatus.COMPLETED,
|
|
1103
|
+
success=True,
|
|
1104
|
+
data=comment_result.model_dump(mode="json"),
|
|
1105
|
+
error=None,
|
|
1106
|
+
correlation_id=correlation_id,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
def get_deletion_status(self, content_id: str) -> Optional[RedditDeletionStatus]:
|
|
1110
|
+
"""
|
|
1111
|
+
Get deletion status for content (DSAR pattern).
|
|
1112
|
+
|
|
1113
|
+
Args:
|
|
1114
|
+
content_id: Reddit content ID (t3_xxxxx or t1_xxxxx)
|
|
1115
|
+
|
|
1116
|
+
Returns:
|
|
1117
|
+
Deletion status if tracked, None otherwise
|
|
1118
|
+
"""
|
|
1119
|
+
return self._deletion_statuses.get(content_id)
|
|
1120
|
+
|
|
1121
|
+
# Observation helpers ---------------------------------------------------
|
|
1122
|
+
async def _active_observe(self, reference: RedditChannelReference, *, limit: int) -> RedditTimelineResponse:
|
|
1123
|
+
if reference.target is RedditChannelType.USER and reference.username:
|
|
1124
|
+
return await self._client.fetch_user_activity(reference.username, limit=limit)
|
|
1125
|
+
|
|
1126
|
+
if reference.target is RedditChannelType.SUBREDDIT and reference.subreddit:
|
|
1127
|
+
entries = await self._client.fetch_subreddit_new(reference.subreddit, limit=limit)
|
|
1128
|
+
return RedditTimelineResponse(entries=entries)
|
|
1129
|
+
|
|
1130
|
+
if reference.target in {RedditChannelType.SUBMISSION, RedditChannelType.COMMENT}:
|
|
1131
|
+
submission_id = reference.submission_id
|
|
1132
|
+
if not submission_id:
|
|
1133
|
+
raise RuntimeError("Submission ID required for observation")
|
|
1134
|
+
comments = await self._client.fetch_submission_comments(submission_id, limit=limit)
|
|
1135
|
+
entries = [
|
|
1136
|
+
RedditTimelineEntry(
|
|
1137
|
+
entry_type="comment",
|
|
1138
|
+
item_id=comment.comment_id,
|
|
1139
|
+
fullname=comment.fullname,
|
|
1140
|
+
subreddit=comment.subreddit,
|
|
1141
|
+
permalink=comment.permalink,
|
|
1142
|
+
score=comment.score,
|
|
1143
|
+
created_at=comment.created_at,
|
|
1144
|
+
channel_reference=comment.channel_reference,
|
|
1145
|
+
author=comment.author,
|
|
1146
|
+
body=comment.body,
|
|
1147
|
+
)
|
|
1148
|
+
for comment in comments
|
|
1149
|
+
]
|
|
1150
|
+
return RedditTimelineResponse(entries=entries)
|
|
1151
|
+
|
|
1152
|
+
raise RuntimeError(f"Unsupported observation target: {reference.target.value}")
|
|
1153
|
+
|
|
1154
|
+
# Shared helpers --------------------------------------------------------
|
|
1155
|
+
def get_capabilities(self) -> ServiceCapabilities:
|
|
1156
|
+
return ServiceCapabilities(
|
|
1157
|
+
service_name=self.service_name,
|
|
1158
|
+
actions=self._get_actions(),
|
|
1159
|
+
version="1.0.0",
|
|
1160
|
+
dependencies=list(self._dependencies),
|
|
1161
|
+
metadata={"provider": "reddit", "channel_format": "reddit:r/<sub>:post/<id>:comment/<id>"},
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
def _validation_error_result(
|
|
1165
|
+
self, tool_name: str, correlation_id: str, error: ValidationError
|
|
1166
|
+
) -> ToolExecutionResult:
|
|
1167
|
+
return ToolExecutionResult(
|
|
1168
|
+
tool_name=tool_name,
|
|
1169
|
+
status=ToolExecutionStatus.FAILED,
|
|
1170
|
+
success=False,
|
|
1171
|
+
data=None,
|
|
1172
|
+
error=str(error),
|
|
1173
|
+
correlation_id=correlation_id,
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
def _api_error_result(self, tool_name: str, correlation_id: str, message: str) -> ToolExecutionResult:
|
|
1177
|
+
return ToolExecutionResult(
|
|
1178
|
+
tool_name=tool_name,
|
|
1179
|
+
status=ToolExecutionStatus.FAILED,
|
|
1180
|
+
success=False,
|
|
1181
|
+
data=None,
|
|
1182
|
+
error=message,
|
|
1183
|
+
correlation_id=correlation_id,
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
def _schema_to_param_schema(self, json_schema: JSONDict) -> ToolParameterSchema:
|
|
1187
|
+
"""Convert a Pydantic JSON schema to ToolParameterSchema format."""
|
|
1188
|
+
return ToolParameterSchema(
|
|
1189
|
+
type=json_schema.get("type", "object"),
|
|
1190
|
+
properties=json_schema.get("properties", {}),
|
|
1191
|
+
required=json_schema.get("required", []),
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
def _build_tool_schemas(self) -> Dict[str, ToolParameterSchema]:
|
|
1195
|
+
return {
|
|
1196
|
+
"reddit_get_user_context": self._schema_to_param_schema(RedditUserContextRequest.model_json_schema()),
|
|
1197
|
+
"reddit_submit_post": self._schema_to_param_schema(RedditSubmitPostRequest.model_json_schema()),
|
|
1198
|
+
"reddit_submit_comment": self._schema_to_param_schema(RedditSubmitCommentRequest.model_json_schema()),
|
|
1199
|
+
"reddit_remove_content": self._schema_to_param_schema(RedditRemoveContentRequest.model_json_schema()),
|
|
1200
|
+
"reddit_get_submission": self._schema_to_param_schema(RedditGetSubmissionRequest.model_json_schema()),
|
|
1201
|
+
"reddit_delete_content": self._schema_to_param_schema(RedditDeleteContentRequest.model_json_schema()),
|
|
1202
|
+
"reddit_disclose_identity": self._schema_to_param_schema(RedditDisclosureRequest.model_json_schema()),
|
|
1203
|
+
"reddit_observe": ToolParameterSchema(
|
|
1204
|
+
type="object",
|
|
1205
|
+
properties={
|
|
1206
|
+
"channel_reference": {"type": "string"},
|
|
1207
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
|
1208
|
+
},
|
|
1209
|
+
required=["channel_reference"],
|
|
1210
|
+
),
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
def _build_tool_info(self) -> Dict[str, ToolInfo]:
|
|
1214
|
+
tool_descriptions = {
|
|
1215
|
+
"reddit_get_user_context": "Fetch metadata and recent activity for a Reddit user",
|
|
1216
|
+
"reddit_submit_post": "Submit a markdown self-post to the configured subreddit",
|
|
1217
|
+
"reddit_submit_comment": "Reply to a submission or comment",
|
|
1218
|
+
"reddit_remove_content": "Remove a submission or comment",
|
|
1219
|
+
"reddit_get_submission": "Fetch metadata for a submission",
|
|
1220
|
+
"reddit_delete_content": "Permanently delete content from Reddit (ToS compliance)",
|
|
1221
|
+
"reddit_disclose_identity": "Post AI transparency disclosure (community guidelines compliance)",
|
|
1222
|
+
"reddit_observe": "Fetch passive observation data for a subreddit, submission, comment, or user",
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
name: ToolInfo(name=name, description=tool_descriptions.get(name, ""), parameters=schema)
|
|
1226
|
+
for name, schema in self._tool_schemas.items()
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
def _extract_submission_id_from_permalink(self, permalink: str) -> Optional[str]:
|
|
1230
|
+
from urllib.parse import urlparse
|
|
1231
|
+
|
|
1232
|
+
parsed = urlparse(permalink)
|
|
1233
|
+
path_parts = [part for part in parsed.path.split("/") if part]
|
|
1234
|
+
|
|
1235
|
+
if not path_parts:
|
|
1236
|
+
return None
|
|
1237
|
+
|
|
1238
|
+
lowered_parts = [part.lower() for part in path_parts]
|
|
1239
|
+
|
|
1240
|
+
# Canonical reddit URLs follow /r/<sub>/comments/<id>/slug
|
|
1241
|
+
if "comments" in lowered_parts:
|
|
1242
|
+
idx = lowered_parts.index("comments")
|
|
1243
|
+
if idx + 1 < len(path_parts):
|
|
1244
|
+
return path_parts[idx + 1]
|
|
1245
|
+
|
|
1246
|
+
# Shortlinks use redd.it/<id> or /comments/<id>
|
|
1247
|
+
if lowered_parts[0] == "comments" and len(path_parts) > 1:
|
|
1248
|
+
return path_parts[1]
|
|
1249
|
+
|
|
1250
|
+
# redd.it shortlinks surface the id as the only path component
|
|
1251
|
+
if len(path_parts) == 1:
|
|
1252
|
+
return path_parts[0]
|
|
1253
|
+
return None
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
# ----------------------------------------------------------------------
|
|
1257
|
+
# Communication service implementation
|
|
1258
|
+
# ----------------------------------------------------------------------
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
class RedditCommunicationService(RedditServiceBase):
|
|
1262
|
+
"""Communication service that lets CIRIS speak and fetch on Reddit."""
|
|
1263
|
+
|
|
1264
|
+
def __init__(
|
|
1265
|
+
self,
|
|
1266
|
+
credentials: Optional[RedditCredentials] = None,
|
|
1267
|
+
*,
|
|
1268
|
+
time_service: Optional[TimeServiceProtocol] = None,
|
|
1269
|
+
bus_manager: Optional[object] = None,
|
|
1270
|
+
memory_service: Optional[object] = None,
|
|
1271
|
+
agent_id: Optional[str] = None,
|
|
1272
|
+
filter_service: Optional[object] = None,
|
|
1273
|
+
secrets_service: Optional[object] = None,
|
|
1274
|
+
agent_occurrence_id: str = "default",
|
|
1275
|
+
) -> None:
|
|
1276
|
+
super().__init__(credentials, time_service=time_service, service_name="RedditCommunicationService")
|
|
1277
|
+
self._home_channel: Optional[str] = None
|
|
1278
|
+
self._wakeup_submission_id: Optional[str] = None
|
|
1279
|
+
# Store runtime dependencies for observer creation
|
|
1280
|
+
self._bus_manager = bus_manager
|
|
1281
|
+
self._memory_service = memory_service
|
|
1282
|
+
self._agent_id = agent_id
|
|
1283
|
+
self._filter_service = filter_service
|
|
1284
|
+
self._secrets_service = secrets_service
|
|
1285
|
+
self._agent_occurrence_id = agent_occurrence_id
|
|
1286
|
+
self._observer: Optional[object] = None # RedditObserver instance
|
|
1287
|
+
|
|
1288
|
+
async def _on_start(self) -> None:
|
|
1289
|
+
await super()._on_start()
|
|
1290
|
+
await self._resolve_home_channel()
|
|
1291
|
+
|
|
1292
|
+
# Create and start Reddit observer if runtime dependencies are available
|
|
1293
|
+
# Note: agent_id is optional, observer will use "ciris" as fallback
|
|
1294
|
+
if self._bus_manager and self._memory_service:
|
|
1295
|
+
from .observer import RedditObserver
|
|
1296
|
+
|
|
1297
|
+
logger.info("Creating RedditObserver with runtime dependencies")
|
|
1298
|
+
self._observer = RedditObserver(
|
|
1299
|
+
credentials=self._credentials,
|
|
1300
|
+
subreddit=self._credentials.subreddit if self._credentials else None,
|
|
1301
|
+
bus_manager=self._bus_manager if isinstance(self._bus_manager, BusManager) else None,
|
|
1302
|
+
memory_service=self._memory_service,
|
|
1303
|
+
agent_id=self._agent_id,
|
|
1304
|
+
filter_service=self._filter_service,
|
|
1305
|
+
secrets_service=self._secrets_service if isinstance(self._secrets_service, SecretsService) else None,
|
|
1306
|
+
time_service=self._time_service,
|
|
1307
|
+
agent_occurrence_id=self._agent_occurrence_id,
|
|
1308
|
+
)
|
|
1309
|
+
await self._observer.start()
|
|
1310
|
+
logger.info(
|
|
1311
|
+
f"RedditObserver started and monitoring r/{self._credentials.subreddit if self._credentials else 'unknown'}"
|
|
1312
|
+
)
|
|
1313
|
+
else:
|
|
1314
|
+
logger.warning("RedditCommunicationService: Runtime dependencies not available, observer not started")
|
|
1315
|
+
|
|
1316
|
+
async def _on_stop(self) -> None:
|
|
1317
|
+
# Stop observer if it was started
|
|
1318
|
+
if self._observer and hasattr(self._observer, "stop"):
|
|
1319
|
+
await self._observer.stop()
|
|
1320
|
+
logger.info("RedditObserver stopped")
|
|
1321
|
+
await super()._on_stop()
|
|
1322
|
+
|
|
1323
|
+
def get_service_type(self) -> ServiceType:
|
|
1324
|
+
return ServiceType.COMMUNICATION
|
|
1325
|
+
|
|
1326
|
+
def _get_actions(self) -> List[str]:
|
|
1327
|
+
return ["send_message", "fetch_messages"]
|
|
1328
|
+
|
|
1329
|
+
async def send_message(self, channel_id: str, content: str) -> bool:
|
|
1330
|
+
reference = RedditChannelReference.parse(channel_id)
|
|
1331
|
+
if reference.target == RedditChannelType.SUBREDDIT:
|
|
1332
|
+
raise RuntimeError(
|
|
1333
|
+
"Cannot send plain messages to a subreddit without a submission context. "
|
|
1334
|
+
"Provide a submission or comment reference (e.g., reddit:r/ciris:post/<id>)."
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
if reference.target == RedditChannelType.SUBMISSION:
|
|
1338
|
+
parent_fullname = f"t3_{reference.submission_id}"
|
|
1339
|
+
elif reference.target == RedditChannelType.COMMENT:
|
|
1340
|
+
parent_fullname = f"t1_{reference.comment_id}"
|
|
1341
|
+
else:
|
|
1342
|
+
raise RuntimeError(f"Unsupported channel target for send_message: {reference.target.value}")
|
|
1343
|
+
|
|
1344
|
+
request = RedditSubmitCommentRequest(parent_fullname=parent_fullname, text=content)
|
|
1345
|
+
await self._client.submit_comment(request)
|
|
1346
|
+
return True
|
|
1347
|
+
|
|
1348
|
+
async def fetch_messages(
|
|
1349
|
+
self,
|
|
1350
|
+
channel_id: str,
|
|
1351
|
+
*,
|
|
1352
|
+
limit: int = 50,
|
|
1353
|
+
before: Optional[datetime] = None,
|
|
1354
|
+
) -> List[FetchedMessage]:
|
|
1355
|
+
del before # Reddit does not support before filters in this implementation
|
|
1356
|
+
reference = RedditChannelReference.parse(channel_id)
|
|
1357
|
+
messages: List[FetchedMessage] = []
|
|
1358
|
+
|
|
1359
|
+
if reference.target == RedditChannelType.SUBREDDIT and reference.subreddit:
|
|
1360
|
+
entries = await self._client.fetch_subreddit_new(reference.subreddit, limit=limit)
|
|
1361
|
+
for entry in entries:
|
|
1362
|
+
messages.append(
|
|
1363
|
+
FetchedMessage(
|
|
1364
|
+
id=entry.item_id,
|
|
1365
|
+
content=entry.title or entry.body,
|
|
1366
|
+
author_name=entry.author,
|
|
1367
|
+
author_id=entry.author,
|
|
1368
|
+
timestamp=entry.created_at.isoformat(),
|
|
1369
|
+
channel_reference=entry.channel_reference,
|
|
1370
|
+
permalink=entry.permalink,
|
|
1371
|
+
)
|
|
1372
|
+
)
|
|
1373
|
+
return messages
|
|
1374
|
+
|
|
1375
|
+
if reference.target == RedditChannelType.SUBMISSION and reference.submission_id:
|
|
1376
|
+
comments = await self._client.fetch_submission_comments(reference.submission_id, limit=limit)
|
|
1377
|
+
for comment in comments:
|
|
1378
|
+
messages.append(
|
|
1379
|
+
FetchedMessage(
|
|
1380
|
+
id=comment.comment_id,
|
|
1381
|
+
content=comment.body,
|
|
1382
|
+
author_name=comment.author,
|
|
1383
|
+
author_id=comment.author,
|
|
1384
|
+
timestamp=comment.created_at.isoformat(),
|
|
1385
|
+
channel_reference=comment.channel_reference,
|
|
1386
|
+
permalink=comment.permalink,
|
|
1387
|
+
)
|
|
1388
|
+
)
|
|
1389
|
+
return messages
|
|
1390
|
+
|
|
1391
|
+
if reference.target == RedditChannelType.COMMENT and reference.comment_id:
|
|
1392
|
+
if not reference.submission_id:
|
|
1393
|
+
raise RuntimeError("Comment references must include a submission id")
|
|
1394
|
+
# Fetch the comment itself
|
|
1395
|
+
summary = await self._client.fetch_submission_comments(reference.submission_id, limit=limit)
|
|
1396
|
+
for comment in summary:
|
|
1397
|
+
if comment.comment_id == reference.comment_id:
|
|
1398
|
+
messages.append(
|
|
1399
|
+
FetchedMessage(
|
|
1400
|
+
id=comment.comment_id,
|
|
1401
|
+
content=comment.body,
|
|
1402
|
+
author_name=comment.author,
|
|
1403
|
+
author_id=comment.author,
|
|
1404
|
+
timestamp=comment.created_at.isoformat(),
|
|
1405
|
+
channel_reference=comment.channel_reference,
|
|
1406
|
+
permalink=comment.permalink,
|
|
1407
|
+
)
|
|
1408
|
+
)
|
|
1409
|
+
break
|
|
1410
|
+
return messages
|
|
1411
|
+
|
|
1412
|
+
if reference.target == RedditChannelType.USER and reference.username:
|
|
1413
|
+
timeline = await self._client.fetch_user_activity(reference.username, limit=limit)
|
|
1414
|
+
for entry in timeline.entries:
|
|
1415
|
+
messages.append(
|
|
1416
|
+
FetchedMessage(
|
|
1417
|
+
id=entry.item_id,
|
|
1418
|
+
content=entry.title or entry.body,
|
|
1419
|
+
author_name=entry.author,
|
|
1420
|
+
author_id=entry.author,
|
|
1421
|
+
timestamp=entry.created_at.isoformat(),
|
|
1422
|
+
channel_reference=entry.channel_reference,
|
|
1423
|
+
permalink=entry.permalink,
|
|
1424
|
+
)
|
|
1425
|
+
)
|
|
1426
|
+
return messages
|
|
1427
|
+
|
|
1428
|
+
raise RuntimeError(f"Unsupported channel reference for fetch_messages: {channel_id}")
|
|
1429
|
+
|
|
1430
|
+
def get_home_channel_id(self) -> Optional[str]:
|
|
1431
|
+
if self._home_channel:
|
|
1432
|
+
return self._home_channel
|
|
1433
|
+
return _build_channel_reference(self._subreddit)
|
|
1434
|
+
|
|
1435
|
+
async def _resolve_home_channel(self) -> None:
|
|
1436
|
+
"""Resolve the WAKEUP submission used for default Reddit messaging."""
|
|
1437
|
+
|
|
1438
|
+
subreddit = self._subreddit
|
|
1439
|
+
try:
|
|
1440
|
+
entries = await self._client.fetch_subreddit_new(subreddit, limit=25)
|
|
1441
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
1442
|
+
logger.warning(
|
|
1443
|
+
"RedditCommunicationService: failed to resolve WAKEUP submission for r/%s: %s",
|
|
1444
|
+
subreddit,
|
|
1445
|
+
exc,
|
|
1446
|
+
)
|
|
1447
|
+
self._home_channel = _build_channel_reference(subreddit)
|
|
1448
|
+
return
|
|
1449
|
+
|
|
1450
|
+
for entry in entries:
|
|
1451
|
+
if entry.entry_type != "submission":
|
|
1452
|
+
continue
|
|
1453
|
+
title = entry.title or ""
|
|
1454
|
+
body = entry.body or ""
|
|
1455
|
+
if "wakeup" in title.lower() or "wakeup" in body.lower():
|
|
1456
|
+
self._wakeup_submission_id = entry.item_id
|
|
1457
|
+
self._home_channel = _build_channel_reference(subreddit, submission_id=entry.item_id)
|
|
1458
|
+
logger.info(
|
|
1459
|
+
"RedditCommunicationService: resolved WAKEUP submission %s as default home channel",
|
|
1460
|
+
entry.item_id,
|
|
1461
|
+
)
|
|
1462
|
+
return
|
|
1463
|
+
|
|
1464
|
+
self._home_channel = _build_channel_reference(subreddit)
|
|
1465
|
+
logger.info(
|
|
1466
|
+
"RedditCommunicationService: no WAKEUP submission detected in r/%s; defaulting to subreddit channel",
|
|
1467
|
+
subreddit,
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
__all__ = ["RedditToolService", "RedditCommunicationService"]
|