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.
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/PKG-INFO +4 -1
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/pyproject.toml +5 -2
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/__init__.py +6 -0
- pulse_engine-0.2.17/src/pulse_engine/adapters/mart_export.py +122 -0
- pulse_engine-0.2.17/src/pulse_engine/adapters/opensearch_sql.py +78 -0
- pulse_engine-0.2.17/src/pulse_engine/adapters/report_generator.py +130 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/config.py +20 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/llm/provider.py +7 -1
- pulse_engine-0.2.17/src/pulse_engine/reporting/__init__.py +6 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/agent.py +41 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/fixtures.py +60 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/models.py +62 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/prompts.py +18 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/service.py +151 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/tools.py +101 -0
- pulse_engine-0.2.17/src/pulse_engine/reporting/wiring.py +226 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/__init__.py +9 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/backend.py +240 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/lambda_microvm.py +152 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/microvm_agent/Dockerfile +65 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/microvm_agent/__init__.py +1 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/microvm_agent/server.py +99 -0
- pulse_engine-0.2.17/src/pulse_engine/sandbox/session.py +31 -0
- pulse_engine-0.2.17/src/pulse_engine/skills/__init__.py +4 -0
- pulse_engine-0.2.17/src/pulse_engine/skills/catalog.py +68 -0
- pulse_engine-0.2.17/src/pulse_engine/skills/models.py +85 -0
- pulse_engine-0.2.17/src/pulse_engine/skills/repo.py +59 -0
- pulse_engine-0.2.17/src/pulse_engine/skills/sync.py +85 -0
- pulse_engine-0.2.17/src/pulse_engine/skills/tools.py +194 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/README.md +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/audio_transcription.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/batcher.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/digital_news.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/exceptions.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/models.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/speech_content.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/speech_metadata.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/base.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/config.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/factory.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/deepgram.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/openai_whisper.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/selfhosted_job.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/providers/youtube_captions.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/service.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/transcription/source.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/twitter.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/youtube_downloader.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/auth.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/health.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/api/v1/router.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/main.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
- {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
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
- {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
- {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
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/client.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/error_handlers.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/exceptions.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/job_token.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/logging.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/scope.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/core/security.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/models.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/pipeline_sync.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/repository.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/router.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/runner.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/schemas.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/service.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/tasks.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/cron/translator.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/database.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/dependencies.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/deployment/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/deployment/infra_provisioner.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/adapter.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/client.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/models.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/router.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/evolution/service.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/extractor/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/extractor/base.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/main.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/__main__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/server.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_kb.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_modules.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/mcp/tools_processor.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/rate_limit.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/request_id.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/security_headers.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/middleware/tenant.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/notifications/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/config_parser.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/expression.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/models.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/repositories.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/router_modules.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/router_pipelines.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/schemas.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/service.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/base.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/pipeline/translators/prefect_translator.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/base.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/analysis.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/chunking.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/prompts.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/llm/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/ocr/gemini.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/pipeline.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/router.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/processor/schemas.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/registry.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/lambda_runner.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/pipeline_runner.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/prefect_pipeline_flow.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/runners/prefect_runner.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/s3.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/secrets.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/services/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/services/bootstrap.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/services/opensearch.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/athena.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/base.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/knowledge_base.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/router.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/storage/schemas.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/testing/__init__.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/testing/fixtures.py +0 -0
- {pulse_engine-0.2.16 → pulse_engine-0.2.17}/src/pulse_engine/testing/mocks.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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,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
|
+
)
|