pulse-engine 0.2.16__tar.gz → 0.2.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/PKG-INFO +4 -1
  2. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/pyproject.toml +5 -2
  3. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/__init__.py +6 -0
  4. pulse_engine-0.2.17/src/pulse_engine/adapters/mart_export.py +122 -0
  5. pulse_engine-0.2.17/src/pulse_engine/adapters/opensearch_sql.py +78 -0
  6. pulse_engine-0.2.17/src/pulse_engine/adapters/report_generator.py +130 -0
  7. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/config.py +20 -0
  8. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/llm/provider.py +7 -1
  9. pulse_engine-0.2.17/src/pulse_engine/reporting/__init__.py +6 -0
  10. pulse_engine-0.2.17/src/pulse_engine/reporting/agent.py +41 -0
  11. pulse_engine-0.2.17/src/pulse_engine/reporting/fixtures.py +60 -0
  12. pulse_engine-0.2.17/src/pulse_engine/reporting/models.py +62 -0
  13. pulse_engine-0.2.17/src/pulse_engine/reporting/prompts.py +18 -0
  14. pulse_engine-0.2.17/src/pulse_engine/reporting/service.py +151 -0
  15. pulse_engine-0.2.17/src/pulse_engine/reporting/tools.py +101 -0
  16. pulse_engine-0.2.17/src/pulse_engine/reporting/wiring.py +226 -0
  17. pulse_engine-0.2.17/src/pulse_engine/sandbox/__init__.py +9 -0
  18. pulse_engine-0.2.17/src/pulse_engine/sandbox/backend.py +240 -0
  19. pulse_engine-0.2.17/src/pulse_engine/sandbox/lambda_microvm.py +152 -0
  20. pulse_engine-0.2.17/src/pulse_engine/sandbox/microvm_agent/Dockerfile +65 -0
  21. pulse_engine-0.2.17/src/pulse_engine/sandbox/microvm_agent/__init__.py +1 -0
  22. pulse_engine-0.2.17/src/pulse_engine/sandbox/microvm_agent/server.py +99 -0
  23. pulse_engine-0.2.17/src/pulse_engine/sandbox/session.py +31 -0
  24. pulse_engine-0.2.17/src/pulse_engine/skills/__init__.py +4 -0
  25. pulse_engine-0.2.17/src/pulse_engine/skills/catalog.py +68 -0
  26. pulse_engine-0.2.17/src/pulse_engine/skills/models.py +85 -0
  27. pulse_engine-0.2.17/src/pulse_engine/skills/repo.py +59 -0
  28. pulse_engine-0.2.17/src/pulse_engine/skills/sync.py +85 -0
  29. pulse_engine-0.2.17/src/pulse_engine/skills/tools.py +194 -0
  30. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/README.md +0 -0
  31. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/__init__.py +0 -0
  32. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/audio_transcription.py +0 -0
  33. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/batcher.py +0 -0
  34. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/digital_news.py +0 -0
  35. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
  36. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/exceptions.py +0 -0
  37. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/models.py +0 -0
  38. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
  39. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/speech_content.py +0 -0
  40. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/speech_metadata.py +0 -0
  41. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/__init__.py +0 -0
  42. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/base.py +0 -0
  43. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/config.py +0 -0
  44. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/factory.py +0 -0
  45. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/__init__.py +0 -0
  46. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/deepgram.py +0 -0
  47. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/openai_whisper.py +0 -0
  48. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/selfhosted_job.py +0 -0
  49. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/youtube_captions.py +0 -0
  50. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/service.py +0 -0
  51. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/source.py +0 -0
  52. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/twitter.py +0 -0
  53. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/youtube_downloader.py +0 -0
  54. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
  55. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/__init__.py +0 -0
  56. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/__init__.py +0 -0
  57. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/auth.py +0 -0
  58. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/health.py +0 -0
  59. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/router.py +0 -0
  60. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/__init__.py +0 -0
  61. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/main.py +0 -0
  62. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
  63. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
  64. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
  65. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
  66. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
  67. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +0 -0
  68. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
  69. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
  70. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +0 -0
  71. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/client.py +0 -0
  72. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/__init__.py +0 -0
  73. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/error_handlers.py +0 -0
  74. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/exceptions.py +0 -0
  75. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/job_token.py +0 -0
  76. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/logging.py +0 -0
  77. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/scope.py +0 -0
  78. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/security.py +0 -0
  79. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/__init__.py +0 -0
  80. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/models.py +0 -0
  81. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/pipeline_sync.py +0 -0
  82. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/repository.py +0 -0
  83. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/router.py +0 -0
  84. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/runner.py +0 -0
  85. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/schemas.py +0 -0
  86. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/service.py +0 -0
  87. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/tasks.py +0 -0
  88. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/translator.py +0 -0
  89. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/database.py +0 -0
  90. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/dependencies.py +0 -0
  91. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/deployment/__init__.py +0 -0
  92. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/deployment/infra_provisioner.py +0 -0
  93. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/__init__.py +0 -0
  94. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/adapter.py +0 -0
  95. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/client.py +0 -0
  96. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/models.py +0 -0
  97. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/router.py +0 -0
  98. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/service.py +0 -0
  99. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/extractor/__init__.py +0 -0
  100. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
  101. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/extractor/base.py +0 -0
  102. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/main.py +0 -0
  103. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/__init__.py +0 -0
  104. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/__main__.py +0 -0
  105. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/server.py +0 -0
  106. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_kb.py +0 -0
  107. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_modules.py +0 -0
  108. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
  109. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_processor.py +0 -0
  110. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/__init__.py +0 -0
  111. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/rate_limit.py +0 -0
  112. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/request_id.py +0 -0
  113. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/security_headers.py +0 -0
  114. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/tenant.py +0 -0
  115. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/notifications/__init__.py +0 -0
  116. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
  117. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/__init__.py +0 -0
  118. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/config_parser.py +0 -0
  119. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/expression.py +0 -0
  120. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/models.py +0 -0
  121. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/repositories.py +0 -0
  122. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/router_modules.py +0 -0
  123. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/router_pipelines.py +0 -0
  124. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/schemas.py +0 -0
  125. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/service.py +0 -0
  126. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
  127. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
  128. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
  129. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/base.py +0 -0
  130. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
  131. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/prefect_translator.py +0 -0
  132. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/__init__.py +0 -0
  133. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/base.py +0 -0
  134. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/__init__.py +0 -0
  135. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/analysis.py +0 -0
  136. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/chunking.py +0 -0
  137. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/prompts.py +0 -0
  138. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
  139. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/__init__.py +0 -0
  140. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
  141. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
  142. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
  143. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/llm/__init__.py +0 -0
  144. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/ocr/gemini.py +0 -0
  145. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/pipeline.py +0 -0
  146. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
  147. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
  148. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
  149. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
  150. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
  151. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/router.py +0 -0
  152. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/schemas.py +0 -0
  153. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/registry.py +0 -0
  154. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/__init__.py +0 -0
  155. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/lambda_runner.py +0 -0
  156. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/pipeline_runner.py +0 -0
  157. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/prefect_pipeline_flow.py +0 -0
  158. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/prefect_runner.py +0 -0
  159. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/s3.py +0 -0
  160. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/secrets.py +0 -0
  161. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/services/__init__.py +0 -0
  162. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/services/bootstrap.py +0 -0
  163. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/services/opensearch.py +0 -0
  164. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/__init__.py +0 -0
  165. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/__init__.py +0 -0
  166. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/athena.py +0 -0
  167. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/base.py +0 -0
  168. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
  169. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/knowledge_base.py +0 -0
  170. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/router.py +0 -0
  171. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/schemas.py +0 -0
  172. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/testing/__init__.py +0 -0
  173. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/testing/fixtures.py +0 -0
  174. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/testing/mocks.py +0 -0
  175. {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pulse-engine
3
- Version: 0.2.16
3
+ Version: 0.2.17
4
4
  Summary: Pulse Engine — Hybrid framework for building Pulse products
5
5
  Author: Pulse Team
6
6
  Requires-Python: >=3.11,<3.13
@@ -13,17 +13,20 @@ Requires-Dist: apprise (==1.11.0)
13
13
  Requires-Dist: asyncpg (>=0.30.0,<0.31.0)
14
14
  Requires-Dist: asyncssh (>=2.23.0,<3.0.0)
15
15
  Requires-Dist: beautifulsoup4 (>=4.12.0,<5.0.0)
16
+ Requires-Dist: bedrock-agentcore (>=1.17.0,<2.0.0)
16
17
  Requires-Dist: boto3 (>=1.35.0,<2.0.0)
17
18
  Requires-Dist: celery-redbeat (>=2.2.0)
18
19
  Requires-Dist: celery[redis] (>=5.4.0,<6.0.0)
19
20
  Requires-Dist: cookiecutter (>=2.6.0,<3.0.0)
20
21
  Requires-Dist: curl-cffi (>=0.15.0,<0.16.0)
22
+ Requires-Dist: deepagents (>=0.6,<0.7)
21
23
  Requires-Dist: deepgram-sdk (>=3.7.0,<4.0.0)
22
24
  Requires-Dist: email-validator (>=2.3.0,<3.0.0)
23
25
  Requires-Dist: fastapi (>=0.115.0,<0.116.0)
24
26
  Requires-Dist: httpx (>=0.28.0,<0.29.0)
25
27
  Requires-Dist: importlib-metadata (>=9.0.0,<10.0.0)
26
28
  Requires-Dist: langchain (>=1.2.13,<2.0.0)
29
+ Requires-Dist: langchain-agentcore-codeinterpreter (>=0.0.5,<0.0.6)
27
30
  Requires-Dist: langchain-anthropic (>=1.4.0,<2.0.0)
28
31
  Requires-Dist: langchain-openai (>=1.1.11,<2.0.0)
29
32
  Requires-Dist: langdetect (>=1.0.9,<2.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pulse-engine"
3
- version = "0.2.16"
3
+ version = "0.2.17"
4
4
  description = "Pulse Engine — Hybrid framework for building Pulse products"
5
5
  authors = ["Pulse Team"]
6
6
  readme = "README.md"
@@ -49,6 +49,9 @@ apprise = "1.11.0"
49
49
  asyncssh = "^2.23.0"
50
50
  importlib-metadata = "^9.0.0"
51
51
  prefect = {version = "==3.7.1", optional = true}
52
+ deepagents = "^0.6"
53
+ langchain-agentcore-codeinterpreter = "^0.0.5"
54
+ bedrock-agentcore = "^1.17.0"
52
55
 
53
56
 
54
57
  [tool.poetry.extras]
@@ -107,7 +110,7 @@ module = ["langdetect.*", "tiktoken.*", "bs4.*", "jose", "jose.*", "boto3", "bot
107
110
  ignore_missing_imports = true
108
111
 
109
112
  [[tool.mypy.overrides]]
110
- module = ["prefect", "prefect.*", "entrypoint", "fitz", "twikit", "twikit.*", "yt_dlp", "yt_dlp.*", "google", "google.*", "apprise", "apprise.*"]
113
+ module = ["prefect", "prefect.*", "entrypoint", "fitz", "twikit", "twikit.*", "yt_dlp", "yt_dlp.*", "google", "google.*", "apprise", "apprise.*", "deepagents", "deepagents.*", "langchain_agentcore_codeinterpreter", "langchain_agentcore_codeinterpreter.*", "bedrock_agentcore", "bedrock_agentcore.*"]
111
114
  ignore_missing_imports = true
112
115
 
113
116
  [[tool.mypy.overrides]]
@@ -5,6 +5,7 @@ from .batcher import BatcherAdapter
5
5
  from .digital_news import DigitalNewsAdapter
6
6
  from .digital_news_metadata import DigitalNewsMetadataAdapter
7
7
  from .exceptions import SitemapDiscoveryError
8
+ from .mart_export import MartExportAdapter
8
9
  from .models import (
9
10
  ArticleContent,
10
11
  ArticleResult,
@@ -19,7 +20,9 @@ from .models import (
19
20
  VideoMetadata,
20
21
  VideoResult,
21
22
  )
23
+ from .opensearch_sql import OpenSearchSqlAdapter
22
24
  from .opensearch_storage import OpenSearchStorageAdapter
25
+ from .report_generator import ReportGeneratorAdapter
23
26
  from .speech_content import SpeechContentAdapter
24
27
  from .speech_metadata import SpeechMetadataAdapter
25
28
  from .transcription import TranscriptionService, TranscriptionSource
@@ -31,6 +34,9 @@ __all__ = [
31
34
  # Utilities
32
35
  "BatcherAdapter",
33
36
  "OpenSearchStorageAdapter",
37
+ "OpenSearchSqlAdapter",
38
+ "MartExportAdapter",
39
+ "ReportGeneratorAdapter",
34
40
  # Digital news
35
41
  "DigitalNewsMetadataAdapter",
36
42
  "DigitalNewsAdapter",
@@ -0,0 +1,122 @@
1
+ """MartExportAdapter — query OpenSearch via SQL and stage the rows as an S3 mart.
2
+
3
+ This is the upstream half of reporting: it runs an OpenSearch SQL query and writes
4
+ the rows to ``s3://<bucket>/tenants/<tenant_id>/<prefix>/<mart_name>.json``. The
5
+ ``report_generator`` run then reads those fixtures back.
6
+
7
+ The output is **tenant-scoped** to mirror the engine's fixture-reader layout
8
+ (``reporting.wiring.make_reader`` reads ``tenants/<tenant_id>/fixtures/latest/``), so
9
+ the mart must be written under the same tenant segment or the report finds nothing.
10
+ The tenant is passed in by the caller (derived from the authenticated token) — it is
11
+ never taken from a config arg, so it can't drift from the tenant the report runs as.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import re
19
+ from typing import Any
20
+
21
+ from pulse_engine.adapters.opensearch_sql import OpenSearchSqlAdapter
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # tenant_id becomes an S3 key segment; keep it to a safe pattern (no slashes / ``..``).
26
+ _SAFE_TENANT = re.compile(r"^[A-Za-z0-9._-]+$")
27
+ # run_date also becomes a key segment; require a strict ISO date (no slashes / ``..``).
28
+ _ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
29
+
30
+
31
+ class MartExportAdapter:
32
+ """Run an OpenSearch SQL query and stage the result JSON at a tenant-scoped S3 key.
33
+
34
+ Args:
35
+ os_url: OpenSearch endpoint URL.
36
+ os_username: OpenSearch HTTP basic auth username (optional).
37
+ os_password: OpenSearch HTTP basic auth password (optional).
38
+ use_ssl: Whether the OpenSearch client uses SSL. Default True.
39
+ verify_certs: Whether the OpenSearch client verifies certs. Default True.
40
+ s3_client: boto3 S3 client. Defaults to ``boto3.client("s3")`` (from the
41
+ ambient VM/compute role) on first use; inject a fake in tests.
42
+
43
+ Usage::
44
+
45
+ adapter = MartExportAdapter(os_url="https://...", os_username="admin")
46
+ result = await adapter.export(
47
+ sql="SELECT source AS platform, COUNT(*) AS posts FROM sm_posts GROUP BY 1",
48
+ mart_name="mart_engagement",
49
+ bucket="dev-pulse-engine-artifacts",
50
+ tenant_id="org-123",
51
+ run_date="2026-07-09",
52
+ )
53
+ # key -> tenants/org-123/fixtures/2026-07-09/mart_engagement.json
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ os_url: str,
59
+ os_username: str = "",
60
+ os_password: str = "",
61
+ use_ssl: bool = True,
62
+ verify_certs: bool = True,
63
+ s3_client: Any | None = None,
64
+ ) -> None:
65
+ self._sql = OpenSearchSqlAdapter(
66
+ url=os_url,
67
+ username=os_username,
68
+ password=os_password,
69
+ use_ssl=use_ssl,
70
+ verify_certs=verify_certs,
71
+ )
72
+ self._s3_client = s3_client
73
+
74
+ def _s3(self) -> Any:
75
+ if self._s3_client is None:
76
+ import boto3
77
+
78
+ self._s3_client = boto3.client("s3")
79
+ return self._s3_client
80
+
81
+ @staticmethod
82
+ def _resolve_prefix(run_date: str | None, prefix: str | None) -> str:
83
+ """Prefix precedence: explicit ``prefix`` > ``fixtures/{run_date}`` > latest."""
84
+ if prefix:
85
+ return prefix.strip("/")
86
+ if run_date:
87
+ if not _ISO_DATE.fullmatch(run_date):
88
+ raise ValueError(f"run_date must be ISO YYYY-MM-DD: {run_date!r}")
89
+ return f"fixtures/{run_date}"
90
+ return "fixtures/latest"
91
+
92
+ async def export(
93
+ self,
94
+ *,
95
+ sql: str,
96
+ mart_name: str,
97
+ bucket: str,
98
+ tenant_id: str,
99
+ run_date: str | None = None,
100
+ prefix: str | None = None,
101
+ ) -> dict[str, Any]:
102
+ """Query OpenSearch and stage the mart JSON in S3.
103
+
104
+ Returns:
105
+ {"mart_key": str, "rows": int, "bucket": str}
106
+ """
107
+ if not _SAFE_TENANT.fullmatch(tenant_id):
108
+ raise ValueError(f"unsafe tenant_id for S3 path: {tenant_id!r}")
109
+ resolved = self._resolve_prefix(run_date, prefix)
110
+
111
+ rows = await self._sql.query(sql)
112
+
113
+ # Tenant-scoped key mirrors the engine's fixture reader layout.
114
+ key = f"tenants/{tenant_id}/{resolved}/{mart_name}.json"
115
+ self._s3().put_object(
116
+ Bucket=bucket,
117
+ Key=key,
118
+ Body=json.dumps(rows).encode("utf-8"),
119
+ ContentType="application/json",
120
+ )
121
+ logger.info("mart_export: %d rows -> s3://%s/%s", len(rows), bucket, key)
122
+ return {"mart_key": key, "rows": len(rows), "bucket": bucket}
@@ -0,0 +1,78 @@
1
+ """OpenSearchSqlAdapter — run OpenSearch SQL-plugin queries and map rows to dicts.
2
+
3
+ Wraps the OpenSearch SQL plugin (``POST /_plugins/_sql``) so products don't hand-roll
4
+ the HTTP call. Connection config is caller-supplied (constructor args), mirroring
5
+ ``OpenSearchStorageAdapter`` — both wrap ``opensearchpy.AsyncOpenSearch``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OpenSearchSqlAdapter:
17
+ """Run SQL against the OpenSearch SQL plugin and return rows as dicts.
18
+
19
+ Args:
20
+ url: OpenSearch endpoint URL.
21
+ username: HTTP basic auth username (optional).
22
+ password: HTTP basic auth password (optional).
23
+ use_ssl: Whether to use SSL. Default True.
24
+ verify_certs: Whether to verify SSL certificates. Default True.
25
+ timeout: Per-request timeout in seconds. Default 60.
26
+
27
+ Usage::
28
+
29
+ adapter = OpenSearchSqlAdapter(url="https://...", username="admin")
30
+ rows = await adapter.query(
31
+ "SELECT source AS platform, COUNT(*) AS posts FROM sm_posts GROUP BY source"
32
+ )
33
+ # rows == [{"platform": "tweet", "posts": 42}, ...]
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ url: str,
39
+ username: str = "",
40
+ password: str = "",
41
+ use_ssl: bool = True,
42
+ verify_certs: bool = True,
43
+ timeout: int = 60,
44
+ ) -> None:
45
+ self._url = url.rstrip("/")
46
+ self._username = username
47
+ self._password = password
48
+ self._use_ssl = use_ssl
49
+ self._verify_certs = verify_certs
50
+ self._timeout = timeout
51
+
52
+ def _client(self) -> Any:
53
+ from opensearchpy import AsyncOpenSearch
54
+
55
+ kwargs: dict[str, Any] = {
56
+ "hosts": [self._url],
57
+ "use_ssl": self._use_ssl,
58
+ "verify_certs": self._verify_certs,
59
+ "timeout": self._timeout,
60
+ }
61
+ if self._username and self._password:
62
+ kwargs["http_auth"] = (self._username, self._password)
63
+ return AsyncOpenSearch(**kwargs)
64
+
65
+ async def query(self, sql: str) -> list[dict[str, Any]]:
66
+ """Run ``sql`` via the SQL plugin; map schema+datarows to a list of dicts.
67
+
68
+ Prefers each column's AS-alias so mart columns stay clean (``likes`` rather
69
+ than ``SUM(metadata.like_count)``), falling back to the raw column name.
70
+ """
71
+ async with self._client() as client:
72
+ body = await client.transport.perform_request(
73
+ "POST", "/_plugins/_sql", body={"query": sql}
74
+ )
75
+ cols = [c.get("alias") or c["name"] for c in body.get("schema", [])]
76
+ rows = [dict(zip(cols, row, strict=False)) for row in body.get("datarows", [])]
77
+ logger.info("opensearch_sql: %d rows", len(rows))
78
+ return rows
@@ -0,0 +1,130 @@
1
+ """ReportGeneratorAdapter — run skill-driven report generation in-process.
2
+
3
+ Report generation is a **library function call**, not a REST round-trip: this adapter
4
+ assembles a ``ReportingService`` from engine ``Settings`` and runs it directly in the
5
+ caller's process. Product modules call ``generate(...)`` the same way they call any
6
+ other adapter (``TwitterAdapter.fetch()`` etc.) — there is no ``/reports/generate`` API
7
+ in the path.
8
+
9
+ The heavy collaborators are reached directly from the caller's environment:
10
+ * skills catalog -> Postgres (``DATABASE_URL``)
11
+ * report artifact -> S3 (``REPORTS_S3_BUCKET`` / ``SKILLS_S3_BUCKET``)
12
+ * agent LLM -> ``PULSE_LLM_PROVIDER`` + ``PULSE_LLM_API_KEY``
13
+ * sandbox -> a Lambda microVM provisioned via the engine's AWS control-plane
14
+ config in ``Settings`` (``MICROVM_*``); the caller's IAM role
15
+ must be entitled to the ``lambda-microvms`` service.
16
+
17
+ So the microVM configuration stays engine-owned (``Settings``) while the generation
18
+ itself is a plain in-process call.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from datetime import date
25
+ from typing import Any, cast
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class ReportGeneratorAdapter:
31
+ """Assemble a ReportingService from env and run report generation in-process.
32
+
33
+ Args:
34
+ settings: Engine ``Settings``. Defaults to ``Settings()`` (loaded from env).
35
+ s3_client: boto3 S3 client. Defaults to ``boto3.client("s3", region=...)``.
36
+ db_session_factory: async SQLAlchemy sessionmaker for the skills catalog.
37
+ Defaults to one built from ``settings.database_url``.
38
+
39
+ Usage::
40
+
41
+ adapter = ReportGeneratorAdapter()
42
+ result = await adapter.generate(
43
+ run_date="2026-07-09",
44
+ prompt="Produce the daily report for {run_date}.",
45
+ tenant_id="org-123",
46
+ )
47
+ # result["artifact_url"] ->
48
+ # "s3://.../snapshots/org-123/2026-07-09/report-20260709T101500Z.html"
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ settings: Any | None = None,
54
+ s3_client: Any | None = None,
55
+ db_session_factory: Any | None = None,
56
+ ) -> None:
57
+ if settings is None:
58
+ from pulse_engine.config import Settings
59
+
60
+ settings = Settings()
61
+ self._settings = settings
62
+ self._s3_client = s3_client
63
+ self._db_session_factory = db_session_factory
64
+
65
+ def _build_service(self) -> Any:
66
+ import boto3
67
+
68
+ from pulse_engine.database import build_async_engine, build_session_factory
69
+ from pulse_engine.reporting.wiring import build_reporting_service
70
+
71
+ s3_client = self._s3_client
72
+ if s3_client is None:
73
+ s3_client = boto3.client("s3", region_name=self._settings.aws_region)
74
+
75
+ db_session_factory = self._db_session_factory
76
+ if db_session_factory is None:
77
+ if not self._settings.database_url:
78
+ raise RuntimeError(
79
+ "DATABASE_URL is required for report generation "
80
+ "(the skills catalog is read from Postgres)"
81
+ )
82
+ engine = build_async_engine(self._settings.database_url)
83
+ db_session_factory = build_session_factory(engine)
84
+
85
+ return build_reporting_service(
86
+ settings=self._settings,
87
+ s3_client=s3_client,
88
+ db_session_factory=db_session_factory,
89
+ )
90
+
91
+ async def generate(
92
+ self,
93
+ *,
94
+ run_date: str | date,
95
+ prompt: str,
96
+ tenant_id: str,
97
+ skill_ref: dict[str, Any] | None = None,
98
+ marts: list[str] | None = None,
99
+ artifact_type: str | None = None,
100
+ ) -> dict[str, Any]:
101
+ """Run report generation for ``tenant_id``; return the ReportResult as a dict.
102
+
103
+ ``run_date`` and ``prompt`` are required. ``skill_ref`` ({skill_id,
104
+ version_number?}) pins a skill; omit it to let the engine's agent auto-select
105
+ from the tenant's active catalog. ``marts`` (list of mart-name stems) restricts
106
+ which staged fixtures the agent may read; omit for all. ``artifact_type``
107
+ (html|pdf|docx|xlsx|pptx) pins the output format; omit to let the agent decide.
108
+ The fixture source is derived from ``tenant_id`` inside the service — never
109
+ caller-supplied.
110
+ """
111
+ from pulse_engine.reporting.models import ReportRequest
112
+
113
+ # model_validate coerces run_date (str->date), skill_ref (dict->SkillRef),
114
+ # and validates artifact_type against the Literal — all from a plain dict.
115
+ request = ReportRequest.model_validate(
116
+ {
117
+ "run_date": run_date,
118
+ "prompt": prompt,
119
+ "skill_ref": skill_ref,
120
+ "marts": marts,
121
+ "artifact_type": artifact_type,
122
+ }
123
+ )
124
+
125
+ service = self._build_service()
126
+ result = await service.generate_report(request, tenant_id)
127
+ logger.info(
128
+ "report_generator: tenant=%s artifact=%s", tenant_id, result.artifact_url
129
+ )
130
+ return cast(dict[str, Any], result.model_dump(mode="json"))
@@ -91,6 +91,7 @@ class Settings(BaseSettings):
91
91
  pulse_llm_model: str = "gpt-4o-mini"
92
92
  pulse_llm_api_key: str = ""
93
93
  pulse_llm_temperature: float = 0.0
94
+ pulse_llm_max_tokens: int = 8192 # output cap; rich HTML reports need headroom
94
95
  pulse_default_chunk_size: int = 512
95
96
  pulse_default_chunk_strategy: str = "token_count"
96
97
  pulse_job_callback_timeout: int = 10
@@ -100,6 +101,25 @@ class Settings(BaseSettings):
100
101
  pulse_engine_url: str = "" # public URL containers use to call back
101
102
  pulse_job_token_secret: str = "" # HMAC secret for job-scoped JWTs
102
103
  pulse_s3_bucket: str = "" # S3 bucket for inter-stage data
104
+
105
+ # Reporting / skills
106
+ reports_s3_bucket: str = "" # S3 bucket for report fixtures + rendered artifacts
107
+ skills_s3_bucket: str = "" # S3 bucket for skill files (defaults to reports bucket)
108
+
109
+ # Lambda MicroVM sandbox (ephemeral per-run execution env; see DD-054).
110
+ microvm_region: str = "" # AWS region for the microVM (defaults to aws_region)
111
+ microvm_image_identifier: str = "" # built MicroVM image name/ARN to run from
112
+ microvm_ingress_connector: str = "" # ARN; blank → managed ALL_INGRESS for region
113
+ microvm_egress_connector: str = "" # ARN; blank → managed INTERNET_EGRESS
114
+ microvm_idle_max_seconds: int = 900 # suspend after this idle window
115
+ microvm_suspended_seconds: int = 300 # terminate after this long suspended
116
+ microvm_auth_token_ttl_minutes: int = 30 # endpoint auth-token lifetime
117
+ microvm_ready_timeout_seconds: int = 120 # max wait for RUNNING state
118
+ microvm_log_group: str = "" # CloudWatch log group for VM stdout; blank → off
119
+ microvm_execution_role_arn: str = "" # role the VM assumes; required for logging
120
+
121
+ report_agent_max_steps: int = 80 # ainvoke recursion_limit
122
+ report_agent_timeout_seconds: float = 600.0
103
123
  pulse_chain_grace_period_seconds: int = (
104
124
  300 # seconds before auto-triggering next stage
105
125
  )
@@ -12,6 +12,7 @@ def get_llm(
12
12
  model: str | None = None,
13
13
  api_key: str | None = None,
14
14
  temperature: float | None = None,
15
+ max_tokens: int | None = None,
15
16
  ) -> Any:
16
17
  """Return a LangChain chat model, using Settings as defaults.
17
18
 
@@ -28,6 +29,8 @@ def get_llm(
28
29
  api_key = api_key or settings.pulse_llm_api_key
29
30
  if temperature is None:
30
31
  temperature = settings.pulse_llm_temperature
32
+ if max_tokens is None:
33
+ max_tokens = settings.pulse_llm_max_tokens
31
34
 
32
35
  if provider == "openai":
33
36
  from langchain_openai import ChatOpenAI
@@ -36,14 +39,17 @@ def get_llm(
36
39
  model=model,
37
40
  api_key=api_key, # type: ignore[arg-type]
38
41
  temperature=temperature,
42
+ max_tokens=max_tokens, # type: ignore[call-arg]
39
43
  )
40
44
  elif provider == "anthropic":
41
45
  from langchain_anthropic import ChatAnthropic
42
46
 
47
+ # The Claude 5 family deprecates `temperature` (400 if sent); omit it and
48
+ # let Anthropic use its default. Older models are unaffected by omission.
43
49
  _llm_instance = ChatAnthropic( # type: ignore[call-arg]
44
50
  model=model,
45
51
  api_key=api_key, # type: ignore[arg-type]
46
- temperature=temperature,
52
+ max_tokens=max_tokens,
47
53
  )
48
54
  else:
49
55
  msg = f"Unsupported LLM provider: {provider!r}. Use 'openai' or 'anthropic'."
@@ -0,0 +1,6 @@
1
+ """Skill-driven report generation subsystem.
2
+
3
+ A deepagents agent (bound to a microVM sandbox backend) reads mart fixtures and
4
+ a report skill, then writes a report artifact to S3. See
5
+ specs/milestone_reporting_v1_spec.md.
6
+ """
@@ -0,0 +1,41 @@
1
+ """Build the deepagents report agent bound to a microVM sandbox backend.
2
+
3
+ Skill selection uses the platform Agent Skills pattern (DD-055): the
4
+ ``<available_skills>`` catalog is injected into the system prompt and the agent
5
+ drives progressive disclosure through our ``activate_skill`` / ``read_skill_file``
6
+ / ``execute_skill_script`` tools — so we do **not** use deepagents'
7
+ ``SkillsMiddleware`` or pre-sync skills. ``create_deep_agent(backend=…)`` still
8
+ supplies FilesystemMiddleware (write_file/read_file/execute) for building the
9
+ report artifact and running skill scripts in the sandbox.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from deepagents import create_deep_agent
17
+
18
+ from pulse_engine.reporting.prompts import BASE_SYSTEM_PROMPT
19
+
20
+
21
+ def build_report_agent(
22
+ model: Any,
23
+ tools: list[Any],
24
+ backend: Any,
25
+ catalog_suffix: str,
26
+ *,
27
+ _create: Any = create_deep_agent,
28
+ ) -> Any:
29
+ """Return a compiled deepagents agent for report generation.
30
+
31
+ ``model`` is a LangChain chat model (e.g. from ``get_llm()``) or a
32
+ ``provider:model`` string. ``tools`` are the report + skill tools; ``backend``
33
+ is the sandbox backend; ``catalog_suffix`` is the ``<available_skills>``
34
+ section appended to the base system prompt.
35
+ """
36
+ return _create(
37
+ model=model,
38
+ tools=tools,
39
+ system_prompt=BASE_SYSTEM_PROMPT + catalog_suffix,
40
+ backend=backend,
41
+ )
@@ -0,0 +1,60 @@
1
+ """Read mart_export JSON fixtures from S3.
2
+
3
+ One file per mart (``<name>.json`` holding a JSON array of row objects). The
4
+ mart name is the filename stem. ``list()`` returns a self-describing summary
5
+ (name, row_count, fields) per mart and is safe on empty (0-row) fixtures.
6
+
7
+ An optional ``names`` allowlist restricts which marts are exposed: with it set,
8
+ only those named fixtures are listed/readable (the agent sees a deterministic
9
+ input set); without it, every ``.json`` under the prefix is exposed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from collections.abc import Iterable
16
+ from typing import Any
17
+
18
+
19
+ class MartFixtureReader:
20
+ def __init__(
21
+ self,
22
+ s3_client: Any,
23
+ bucket: str,
24
+ prefix: str,
25
+ names: Iterable[str] | None = None,
26
+ ) -> None:
27
+ self._s3 = s3_client
28
+ self._bucket = bucket
29
+ self._prefix = prefix.rstrip("/") + "/"
30
+ self._names = set(names) if names is not None else None
31
+
32
+ def _keys(self) -> dict[str, str]:
33
+ resp = self._s3.list_objects_v2(Bucket=self._bucket, Prefix=self._prefix)
34
+ out: dict[str, str] = {}
35
+ for obj in resp.get("Contents", []):
36
+ key = obj["Key"]
37
+ if key.endswith(".json"):
38
+ name = key.rsplit("/", 1)[-1][: -len(".json")]
39
+ if self._names is not None and name not in self._names:
40
+ continue # restrict to the requested marts
41
+ out[name] = key
42
+ return out
43
+
44
+ def _rows(self, key: str) -> list[dict[str, Any]]:
45
+ body = self._s3.get_object(Bucket=self._bucket, Key=key)["Body"].read()
46
+ data = json.loads(body)
47
+ return data if isinstance(data, list) else [data]
48
+
49
+ def summaries(self) -> list[dict[str, Any]]:
50
+ """One {name, row_count, fields} summary per mart (empty-safe)."""
51
+ result: list[dict[str, Any]] = []
52
+ for name, key in sorted(self._keys().items()):
53
+ rows = self._rows(key)
54
+ fields = list(rows[0].keys()) if rows else [] # never index [0] on empty
55
+ result.append({"name": name, "row_count": len(rows), "fields": fields})
56
+ return result
57
+
58
+ def read(self, name: str) -> list[dict[str, Any]]:
59
+ key = self._keys().get(name)
60
+ return self._rows(key) if key else []
@@ -0,0 +1,62 @@
1
+ """Pydantic models for the reporting subsystem."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+ # The artifact formats the reporting pipeline can emit (mirrors wiring._CONTENT_TYPES).
11
+ ArtifactType = Literal["html", "pdf", "docx", "xlsx", "pptx"]
12
+
13
+
14
+ class SkillRef(BaseModel):
15
+ """Which skill (and optionally which pinned version) drives the report."""
16
+
17
+ skill_id: str
18
+ version_number: int | None = None # None => active version
19
+
20
+
21
+ class ReportRequest(BaseModel):
22
+ run_date: date
23
+ # None => auto-select: the agent picks from the full active-skill catalog.
24
+ # Set => pin: only that skill is offered (deterministic for scheduled runs).
25
+ skill_ref: SkillRef | None = None
26
+ prompt: str = Field(
27
+ ...,
28
+ min_length=1,
29
+ description="Kickoff user message from config; {run_date} is templated in",
30
+ )
31
+ # None => the agent reads every mart fixture staged under the tenant prefix.
32
+ # Set => only these named marts are exposed to the agent (deterministic inputs).
33
+ marts: list[str] | None = Field(
34
+ default=None,
35
+ description="Mart-name stems to expose (e.g. ['mart_top_authors']); None = all",
36
+ )
37
+ # None => the agent chooses the output format. Set => pin it: the format is
38
+ # injected into the kickoff and the run fails if the agent emits a different type.
39
+ artifact_type: ArtifactType | None = Field(
40
+ default=None,
41
+ description="Output format (html|pdf|docx|xlsx|pptx); None = agent decides",
42
+ )
43
+ # NOTE: the fixture source is derived server-side from the authenticated tenant
44
+ # (see wiring.make_reader). It is deliberately NOT caller-supplied — accepting a
45
+ # path here was a cross-tenant read (IDOR): org A could point at org B's prefix.
46
+
47
+ @field_validator("marts")
48
+ @classmethod
49
+ def _non_empty_marts(cls, v: list[str] | None) -> list[str] | None:
50
+ if v is not None and (not v or not all(isinstance(m, str) and m for m in v)):
51
+ raise ValueError("marts must be a non-empty list of non-empty strings")
52
+ return v
53
+
54
+
55
+ class ReportResult(BaseModel):
56
+ artifact_url: str
57
+ artifact_type: str # "html" (v1 default) | "pdf" | "docx" | "xlsx" | "pptx"
58
+ skill_id: str
59
+ skill_version: int | None
60
+ tokens: int
61
+ tool_calls: int
62
+ run_date: date
@@ -0,0 +1,18 @@
1
+ """Static, generic prompts for the reporting agent.
2
+
3
+ The base system prompt carries *behaviour only* — never report logic (that lives
4
+ in the skill). The kickoff *user* prompt comes from pipeline config
5
+ (``ReportRequest.prompt``).
6
+ """
7
+
8
+ BASE_SYSTEM_PROMPT = (
9
+ "You are a report-generation agent running with a sandboxed filesystem and "
10
+ "shell. Available skills are listed in the <available_skills> catalog; when "
11
+ "the task matches one, call activate_skill with its id to load its full "
12
+ "instructions, then follow them. Read mart data with list_marts / read_mart, "
13
+ "load referenced skill files with read_skill_file, and run any scripts the "
14
+ "skill provides with execute_skill_script. Build one complete report artifact "
15
+ "(write it to a file under /work with write_file), then call write_report "
16
+ "exactly once with its path. Do not call write_report until the artifact is "
17
+ "complete."
18
+ )