pulse-engine 0.2.20__tar.gz → 0.2.22__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.20 → pulse_engine-0.2.22}/PKG-INFO +1 -1
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/pyproject.toml +2 -2
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/__init__.py +5 -0
- pulse_engine-0.2.22/src/pulse_engine/adapters/document_event_campaign_mapper.py +489 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/models.py +14 -0
- pulse_engine-0.2.22/src/pulse_engine/adapters/prompts.py +32 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/config.py +1 -1
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/README.md +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/audio_transcription.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/batcher.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/digital_news.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/exceptions.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/mart_export.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/opensearch_sql.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/report_generator.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/speech_content.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/speech_metadata.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/base.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/config.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/factory.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/providers/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/providers/deepgram.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/providers/openai_whisper.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/providers/selfhosted_job.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/providers/youtube_captions.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/service.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/source.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/twitter.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/youtube_downloader.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/api/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/api/v1/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/api/v1/auth.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/api/v1/health.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/api/v1/router.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/main.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/client.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/error_handlers.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/exceptions.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/job_token.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/logging.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/scope.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/core/security.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/models.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/pipeline_sync.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/repository.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/router.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/runner.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/schemas.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/service.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/tasks.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/cron/translator.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/database.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/dependencies.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/deployment/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/deployment/infra_provisioner.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/evolution/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/evolution/adapter.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/evolution/client.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/evolution/models.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/evolution/router.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/evolution/service.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/extractor/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/extractor/base.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/main.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/__main__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/server.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/tools_kb.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/tools_modules.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/mcp/tools_processor.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/middleware/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/middleware/rate_limit.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/middleware/request_id.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/middleware/security_headers.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/middleware/tenant.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/notifications/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/config_parser.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/expression.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/models.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/repositories.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/router_modules.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/router_pipelines.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/schemas.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/service.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/translators/base.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/pipeline/translators/prefect_translator.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/base.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/core/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/core/analysis.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/core/chunking.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/core/prompts.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/defaults/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/llm/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/llm/provider.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/ocr/gemini.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/pipeline.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/router.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/processor/schemas.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/registry.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/agent.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/fixtures.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/models.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/prompts.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/service.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/tools.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/reporting/wiring.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/runners/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/runners/lambda_runner.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/runners/pipeline_runner.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/runners/prefect_pipeline_flow.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/runners/prefect_runner.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/s3.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/backend.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/lambda_microvm.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/microvm_agent/Dockerfile +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/microvm_agent/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/microvm_agent/server.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/sandbox/session.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/secrets.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/services/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/services/bootstrap.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/services/opensearch.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/catalog.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/models.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/repo.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/seed.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/seed_data/skill_files.json +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/seed_data/skills.json +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/sync.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/skills/tools.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/connectors/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/connectors/athena.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/connectors/base.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/knowledge_base.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/router.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/storage/schemas.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/testing/__init__.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/testing/fixtures.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/testing/mocks.py +0 -0
- {pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pulse-engine"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.22"
|
|
4
4
|
description = "Pulse Engine — Hybrid framework for building Pulse products"
|
|
5
5
|
authors = ["Pulse Team"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -122,7 +122,7 @@ module = ["alembic.*", "alembic.versions.*", "alembic.env"]
|
|
|
122
122
|
ignore_errors = true
|
|
123
123
|
|
|
124
124
|
[[tool.mypy.overrides]]
|
|
125
|
-
module = ["pulse_engine.database"]
|
|
125
|
+
module = ["pulse_engine.database", "pulse_engine.sandbox.lambda_microvm"]
|
|
126
126
|
disallow_subclassing_any = false
|
|
127
127
|
|
|
128
128
|
[[tool.mypy.overrides]]
|
|
@@ -4,11 +4,13 @@ from .audio_transcription import AudioTranscriptionAdapter
|
|
|
4
4
|
from .batcher import BatcherAdapter
|
|
5
5
|
from .digital_news import DigitalNewsAdapter
|
|
6
6
|
from .digital_news_metadata import DigitalNewsMetadataAdapter
|
|
7
|
+
from .document_event_campaign_mapper import DocumentEventCampaignMapper
|
|
7
8
|
from .exceptions import SitemapDiscoveryError
|
|
8
9
|
from .mart_export import MartExportAdapter
|
|
9
10
|
from .models import (
|
|
10
11
|
ArticleContent,
|
|
11
12
|
ArticleResult,
|
|
13
|
+
DocumentMapping,
|
|
12
14
|
NewsArticleMetadata,
|
|
13
15
|
SpeechContent,
|
|
14
16
|
SpeechMetadata,
|
|
@@ -37,6 +39,9 @@ __all__ = [
|
|
|
37
39
|
"OpenSearchSqlAdapter",
|
|
38
40
|
"MartExportAdapter",
|
|
39
41
|
"ReportGeneratorAdapter",
|
|
42
|
+
# Document → event/campaign mapping
|
|
43
|
+
"DocumentEventCampaignMapper",
|
|
44
|
+
"DocumentMapping",
|
|
40
45
|
# Digital news
|
|
41
46
|
"DigitalNewsMetadataAdapter",
|
|
42
47
|
"DigitalNewsAdapter",
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""Maps documents to event code(s) + campaign id(s) via date-windowed LLM matching.
|
|
2
|
+
|
|
3
|
+
For each document the adapter pre-filters the events/campaigns catalog by date
|
|
4
|
+
window, batches date-adjacent documents together (one shared candidate list per
|
|
5
|
+
batch), and asks the LLM which candidates each document is actually about. Events
|
|
6
|
+
and campaigns are read from the media_coverage Postgres tables using a caller-
|
|
7
|
+
supplied ``database_url``; the LLM is resolved via the engine's ``get_llm``
|
|
8
|
+
provider (OpenAI by default, per ``Settings``).
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
adapter = DocumentEventCampaignMapper(
|
|
13
|
+
database_url="postgresql+asyncpg://user:pass@host:5432/media_coverage",
|
|
14
|
+
)
|
|
15
|
+
mappings = await adapter.map(documents)
|
|
16
|
+
# [DocumentMapping(doc_id="123", event_codes=["SM_2026_131"],
|
|
17
|
+
# campaign_ids=["3"]), ...]
|
|
18
|
+
|
|
19
|
+
Provider/model/api_key/temperature override the engine defaults from ``Settings``::
|
|
20
|
+
|
|
21
|
+
adapter = DocumentEventCampaignMapper(
|
|
22
|
+
database_url=url, provider="openai", model="gpt-5-mini",
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import json
|
|
30
|
+
from datetime import date, datetime, timedelta
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import structlog
|
|
34
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
35
|
+
from pydantic import BaseModel, Field
|
|
36
|
+
from sqlalchemy import text
|
|
37
|
+
|
|
38
|
+
from pulse_engine.processor.llm.provider import get_llm
|
|
39
|
+
|
|
40
|
+
from .models import DocumentMapping
|
|
41
|
+
from .prompts import (
|
|
42
|
+
DOCUMENT_BLOCK,
|
|
43
|
+
MAPPING_PROMPT,
|
|
44
|
+
MAX_CONTENT_CHARS,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = structlog.get_logger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _DocPrediction(BaseModel):
|
|
51
|
+
"""One document's predicted event/campaign matches."""
|
|
52
|
+
|
|
53
|
+
doc_id: str = Field(description="The document id, echoed back")
|
|
54
|
+
event_codes: list[str] = Field(
|
|
55
|
+
default_factory=list, description="Matched event codes, or empty list"
|
|
56
|
+
)
|
|
57
|
+
campaign_ids: list[str] = Field(
|
|
58
|
+
default_factory=list, description="Matched campaign ids, or empty list"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _BatchPrediction(BaseModel):
|
|
63
|
+
"""Structured LLM output: one prediction per document in the batch."""
|
|
64
|
+
|
|
65
|
+
predictions: list[_DocPrediction] = Field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DocumentEventCampaignMapper:
|
|
69
|
+
"""Map documents to event codes and campaign ids using date-windowed LLM matching.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
database_url: Async Postgres URL for the media_coverage DB, e.g.
|
|
73
|
+
``postgresql+asyncpg://user:pass@host:5432/media_coverage``. Events and
|
|
74
|
+
campaigns are read from its ``events`` / ``campaigns`` tables.
|
|
75
|
+
provider: LLM provider override (defaults to ``Settings.pulse_llm_provider``).
|
|
76
|
+
model: LLM model override (defaults to ``"gpt-5-mini"``).
|
|
77
|
+
api_key: LLM API key override (defaults to ``Settings.pulse_llm_api_key``).
|
|
78
|
+
temperature: LLM temperature override (defaults to ``Settings``).
|
|
79
|
+
seed: OpenAI request seed for reproducible output (default 42). The gpt-5
|
|
80
|
+
family only supports temperature 1, so a fixed seed is what makes
|
|
81
|
+
identical inputs return identical results. Pass ``None`` to disable.
|
|
82
|
+
event_window_days: Days before a document's date to include events
|
|
83
|
+
(default 3). An event is a candidate when its date falls in
|
|
84
|
+
``[doc_date - window, doc_date]``.
|
|
85
|
+
campaign_window_days: Days after a campaign start to keep it eligible
|
|
86
|
+
(default 30).
|
|
87
|
+
batch_size: Max documents per LLM call (default 10).
|
|
88
|
+
batch_gap_days: Start a new batch when consecutive documents are more than this
|
|
89
|
+
many days apart, so a batch never mixes far-apart documents (default 3).
|
|
90
|
+
concurrency: Max batches processed concurrently (default 4).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
database_url: str,
|
|
96
|
+
*,
|
|
97
|
+
provider: str | None = None,
|
|
98
|
+
model: str | None = "gpt-5-mini",
|
|
99
|
+
api_key: str | None = None,
|
|
100
|
+
temperature: float | None = None,
|
|
101
|
+
seed: int | None = 42,
|
|
102
|
+
event_window_days: int = 3,
|
|
103
|
+
campaign_window_days: int = 30,
|
|
104
|
+
batch_size: int = 10,
|
|
105
|
+
batch_gap_days: int = 3,
|
|
106
|
+
concurrency: int = 4,
|
|
107
|
+
) -> None:
|
|
108
|
+
if not database_url:
|
|
109
|
+
raise ValueError("database_url is required")
|
|
110
|
+
self._database_url = database_url
|
|
111
|
+
self._provider = provider
|
|
112
|
+
self._model = model
|
|
113
|
+
self._api_key = api_key
|
|
114
|
+
self._temperature = temperature
|
|
115
|
+
self._seed = seed
|
|
116
|
+
self._event_window_days = event_window_days
|
|
117
|
+
self._campaign_window_days = campaign_window_days
|
|
118
|
+
self._batch_size = batch_size
|
|
119
|
+
self._batch_gap_days = batch_gap_days
|
|
120
|
+
self._concurrency = concurrency
|
|
121
|
+
|
|
122
|
+
async def map(self, documents: list[dict[str, Any]]) -> list[DocumentMapping]:
|
|
123
|
+
"""Map each document to its matching event codes and campaign ids.
|
|
124
|
+
|
|
125
|
+
``documents`` are dicts, each either flat (id/content/title/timestamp) or an
|
|
126
|
+
OpenSearch ``_source``-style dict (content, metadata.title, timestamp,
|
|
127
|
+
identifier). Returns one :class:`DocumentMapping` per input document, in input
|
|
128
|
+
order (empty lists when nothing matched).
|
|
129
|
+
"""
|
|
130
|
+
if not documents:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
docs = [_normalize_document(d, i) for i, d in enumerate(documents)]
|
|
134
|
+
|
|
135
|
+
events, campaigns = await self._load_catalog()
|
|
136
|
+
valid_event_codes = {e["event_code"] for e in events}
|
|
137
|
+
valid_campaign_ids = {c["campaign_id"] for c in campaigns}
|
|
138
|
+
|
|
139
|
+
batches = _build_batches(docs, self._batch_size, self._batch_gap_days)
|
|
140
|
+
logger.info(
|
|
141
|
+
"mapping_started",
|
|
142
|
+
documents=len(docs),
|
|
143
|
+
batches=len(batches),
|
|
144
|
+
event_window_days=self._event_window_days,
|
|
145
|
+
campaign_window_days=self._campaign_window_days,
|
|
146
|
+
batch_size=self._batch_size,
|
|
147
|
+
batch_gap_days=self._batch_gap_days,
|
|
148
|
+
concurrency=self._concurrency,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
llm = get_llm(
|
|
152
|
+
provider=self._provider,
|
|
153
|
+
model=self._model,
|
|
154
|
+
api_key=self._api_key,
|
|
155
|
+
temperature=self._temperature,
|
|
156
|
+
)
|
|
157
|
+
# The gpt-5 family only supports temperature 1 (non-deterministic), so pin a
|
|
158
|
+
# seed to make identical inputs return identical output across runs. The seed
|
|
159
|
+
# is bound per-call (not on the shared get_llm singleton).
|
|
160
|
+
if self._seed is not None:
|
|
161
|
+
llm = llm.bind(seed=self._seed)
|
|
162
|
+
structured_llm = llm.with_structured_output(_BatchPrediction)
|
|
163
|
+
|
|
164
|
+
semaphore = asyncio.Semaphore(self._concurrency)
|
|
165
|
+
|
|
166
|
+
async def _guarded(batch: list[dict[str, Any]]) -> dict[str, DocumentMapping]:
|
|
167
|
+
async with semaphore:
|
|
168
|
+
return await self._process_batch(
|
|
169
|
+
batch,
|
|
170
|
+
structured_llm,
|
|
171
|
+
events,
|
|
172
|
+
campaigns,
|
|
173
|
+
valid_event_codes,
|
|
174
|
+
valid_campaign_ids,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
per_batch = await asyncio.gather(*(_guarded(b) for b in batches))
|
|
178
|
+
|
|
179
|
+
merged: dict[str, DocumentMapping] = {}
|
|
180
|
+
for batch_result in per_batch:
|
|
181
|
+
merged.update(batch_result)
|
|
182
|
+
|
|
183
|
+
# One entry per input document, in input order.
|
|
184
|
+
return [
|
|
185
|
+
merged.get(d["doc_id"], DocumentMapping(doc_id=d["doc_id"])) for d in docs
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
# -- catalog loading ----------------------------------------------------
|
|
189
|
+
|
|
190
|
+
async def _load_catalog(
|
|
191
|
+
self,
|
|
192
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
193
|
+
"""Read events and campaigns from the media_coverage DB (read-only)."""
|
|
194
|
+
from pulse_engine.database import build_async_engine, build_session_factory
|
|
195
|
+
|
|
196
|
+
engine = build_async_engine(self._database_url)
|
|
197
|
+
factory = build_session_factory(engine)
|
|
198
|
+
try:
|
|
199
|
+
async with factory() as session:
|
|
200
|
+
event_rows = (
|
|
201
|
+
await session.execute(
|
|
202
|
+
text(
|
|
203
|
+
"SELECT event_id, event_type, description, key_issue, "
|
|
204
|
+
"date_of_event FROM events"
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
).all()
|
|
208
|
+
campaign_rows = (
|
|
209
|
+
await session.execute(
|
|
210
|
+
text(
|
|
211
|
+
"SELECT id, campaign_name, description, "
|
|
212
|
+
"campaign_start_date FROM campaigns"
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
).all()
|
|
216
|
+
except Exception:
|
|
217
|
+
logger.error("catalog_load_failed", exc_info=True)
|
|
218
|
+
raise
|
|
219
|
+
finally:
|
|
220
|
+
await engine.dispose()
|
|
221
|
+
|
|
222
|
+
events = [
|
|
223
|
+
{
|
|
224
|
+
"event_code": r.event_id,
|
|
225
|
+
"event_type": r.event_type or "",
|
|
226
|
+
"description": r.description or "",
|
|
227
|
+
"key_issue": r.key_issue or "",
|
|
228
|
+
"date": _coerce_date(r.date_of_event),
|
|
229
|
+
}
|
|
230
|
+
for r in event_rows
|
|
231
|
+
]
|
|
232
|
+
campaigns = [
|
|
233
|
+
{
|
|
234
|
+
"campaign_id": str(r.id),
|
|
235
|
+
"campaign_name": r.campaign_name or "",
|
|
236
|
+
"description": r.description or "",
|
|
237
|
+
"start_date": _coerce_date(r.campaign_start_date),
|
|
238
|
+
}
|
|
239
|
+
for r in campaign_rows
|
|
240
|
+
]
|
|
241
|
+
logger.info("catalog_loaded", events=len(events), campaigns=len(campaigns))
|
|
242
|
+
return events, campaigns
|
|
243
|
+
|
|
244
|
+
# -- per-batch mapping --------------------------------------------------
|
|
245
|
+
|
|
246
|
+
async def _process_batch(
|
|
247
|
+
self,
|
|
248
|
+
batch: list[dict[str, Any]],
|
|
249
|
+
structured_llm: Any,
|
|
250
|
+
events: list[dict[str, Any]],
|
|
251
|
+
campaigns: list[dict[str, Any]],
|
|
252
|
+
valid_event_codes: set[str],
|
|
253
|
+
valid_campaign_ids: set[str],
|
|
254
|
+
) -> dict[str, DocumentMapping]:
|
|
255
|
+
"""Filter candidates for a batch, call the LLM, return per-doc mappings."""
|
|
256
|
+
for doc in batch:
|
|
257
|
+
doc["cand_events"] = _candidate_events(
|
|
258
|
+
doc["timestamp"], events, self._event_window_days
|
|
259
|
+
)
|
|
260
|
+
doc["cand_campaigns"] = _candidate_campaigns(
|
|
261
|
+
doc["timestamp"], campaigns, self._campaign_window_days
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Documents with no date-eligible candidates skip the LLM entirely.
|
|
265
|
+
llm_docs = [d for d in batch if d["cand_events"] or d["cand_campaigns"]]
|
|
266
|
+
|
|
267
|
+
predictions: dict[str, _DocPrediction] = {}
|
|
268
|
+
if llm_docs:
|
|
269
|
+
shared_events = _dedup_by(
|
|
270
|
+
(e for d in llm_docs for e in d["cand_events"]),
|
|
271
|
+
"event_code",
|
|
272
|
+
"date",
|
|
273
|
+
)
|
|
274
|
+
shared_campaigns = _dedup_by(
|
|
275
|
+
(c for d in llm_docs for c in d["cand_campaigns"]),
|
|
276
|
+
"campaign_id",
|
|
277
|
+
"start_date",
|
|
278
|
+
)
|
|
279
|
+
try:
|
|
280
|
+
parsed = await self._call_llm(
|
|
281
|
+
structured_llm, llm_docs, shared_events, shared_campaigns
|
|
282
|
+
)
|
|
283
|
+
predictions = {p.doc_id.strip(): p for p in parsed.predictions}
|
|
284
|
+
except Exception as exc: # noqa: BLE001 — one batch failing is non-fatal
|
|
285
|
+
logger.warning(
|
|
286
|
+
"batch_failed",
|
|
287
|
+
size=len(llm_docs),
|
|
288
|
+
doc_ids=[d["doc_id"] for d in llm_docs],
|
|
289
|
+
error=str(exc),
|
|
290
|
+
exc_info=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
result: dict[str, DocumentMapping] = {}
|
|
294
|
+
for doc in batch:
|
|
295
|
+
p = predictions.get(doc["doc_id"])
|
|
296
|
+
result[doc["doc_id"]] = DocumentMapping(
|
|
297
|
+
doc_id=doc["doc_id"],
|
|
298
|
+
event_codes=_valid_ids(p.event_codes if p else [], valid_event_codes),
|
|
299
|
+
campaign_ids=_valid_ids(
|
|
300
|
+
p.campaign_ids if p else [], valid_campaign_ids
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
async def _call_llm(
|
|
306
|
+
self,
|
|
307
|
+
structured_llm: Any,
|
|
308
|
+
batch: list[dict[str, Any]],
|
|
309
|
+
shared_events: list[dict[str, Any]],
|
|
310
|
+
shared_campaigns: list[dict[str, Any]],
|
|
311
|
+
) -> _BatchPrediction:
|
|
312
|
+
"""Send one batch against its candidate lists; return structured output."""
|
|
313
|
+
events_json = json.dumps(
|
|
314
|
+
[
|
|
315
|
+
{
|
|
316
|
+
"event_code": e["event_code"],
|
|
317
|
+
"event_type": e["event_type"],
|
|
318
|
+
"description": e["description"],
|
|
319
|
+
"key_issue": e["key_issue"],
|
|
320
|
+
}
|
|
321
|
+
for e in shared_events
|
|
322
|
+
],
|
|
323
|
+
ensure_ascii=False,
|
|
324
|
+
)
|
|
325
|
+
campaigns_json = json.dumps(
|
|
326
|
+
[
|
|
327
|
+
{
|
|
328
|
+
"campaign_id": c["campaign_id"],
|
|
329
|
+
"campaign_name": c["campaign_name"],
|
|
330
|
+
"description": c["description"],
|
|
331
|
+
}
|
|
332
|
+
for c in shared_campaigns
|
|
333
|
+
],
|
|
334
|
+
ensure_ascii=False,
|
|
335
|
+
)
|
|
336
|
+
documents_text = "\n".join(
|
|
337
|
+
f"--- Document {i + 1} ---\n{self._render_document(doc)}"
|
|
338
|
+
for i, doc in enumerate(batch)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
system = MAPPING_PROMPT.format(
|
|
342
|
+
candidate_events=events_json,
|
|
343
|
+
candidate_campaigns=campaigns_json,
|
|
344
|
+
)
|
|
345
|
+
messages = [
|
|
346
|
+
SystemMessage(content=system),
|
|
347
|
+
HumanMessage(content=f"DOCUMENTS:\n{documents_text}"),
|
|
348
|
+
]
|
|
349
|
+
result: _BatchPrediction = await structured_llm.ainvoke(messages)
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
def _render_document(self, doc: dict[str, Any]) -> str:
|
|
353
|
+
title_line = f"title: {doc['title']}\n" if doc["title"] else ""
|
|
354
|
+
timestamp = (
|
|
355
|
+
doc["timestamp"].date().isoformat() if doc["timestamp"] else "unknown"
|
|
356
|
+
)
|
|
357
|
+
return DOCUMENT_BLOCK.format(
|
|
358
|
+
doc_id=doc["doc_id"],
|
|
359
|
+
title_line=title_line,
|
|
360
|
+
content=doc["content"][:MAX_CONTENT_CHARS],
|
|
361
|
+
timestamp=timestamp,
|
|
362
|
+
event_window_days=self._event_window_days,
|
|
363
|
+
campaign_window_days=self._campaign_window_days,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
# Pure helpers
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _parse_timestamp(value: Any) -> datetime | None:
|
|
373
|
+
"""Parse an ISO date/datetime string (or date/datetime) to a naive datetime."""
|
|
374
|
+
if value is None or value == "":
|
|
375
|
+
return None
|
|
376
|
+
if isinstance(value, datetime):
|
|
377
|
+
return value.replace(tzinfo=None)
|
|
378
|
+
if isinstance(value, date):
|
|
379
|
+
return datetime(value.year, value.month, value.day)
|
|
380
|
+
try:
|
|
381
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00")).replace(
|
|
382
|
+
tzinfo=None
|
|
383
|
+
)
|
|
384
|
+
except (ValueError, TypeError):
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _coerce_date(value: Any) -> datetime | None:
|
|
389
|
+
"""Coerce a DB date/datetime column value to a naive datetime for comparison."""
|
|
390
|
+
return _parse_timestamp(value)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _normalize_document(doc: dict[str, Any], index: int) -> dict[str, Any]:
|
|
394
|
+
"""Coerce an input document into the internal shape the mapper works with."""
|
|
395
|
+
doc_id = (
|
|
396
|
+
doc.get("id")
|
|
397
|
+
or doc.get("doc_id")
|
|
398
|
+
or doc.get("identifier")
|
|
399
|
+
or doc.get("sn")
|
|
400
|
+
or doc.get("SN")
|
|
401
|
+
or index
|
|
402
|
+
)
|
|
403
|
+
metadata = doc.get("metadata") or {}
|
|
404
|
+
title = doc.get("title") or metadata.get("title") or ""
|
|
405
|
+
content = doc.get("content") or ""
|
|
406
|
+
return {
|
|
407
|
+
"doc_id": str(doc_id).strip(),
|
|
408
|
+
"content": str(content),
|
|
409
|
+
"title": str(title),
|
|
410
|
+
"timestamp": _parse_timestamp(doc.get("timestamp")),
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _candidate_events(
|
|
415
|
+
doc_ts: datetime | None, events: list[dict[str, Any]], window_days: int
|
|
416
|
+
) -> list[dict[str, Any]]:
|
|
417
|
+
if doc_ts is None:
|
|
418
|
+
return []
|
|
419
|
+
# Events dated in [doc_ts - window_days, doc_ts]: the window_days before the
|
|
420
|
+
# document, up to and including the document's own date.
|
|
421
|
+
lo, hi = doc_ts - timedelta(days=window_days), doc_ts
|
|
422
|
+
return [e for e in events if e["date"] is not None and lo <= e["date"] <= hi]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _candidate_campaigns(
|
|
426
|
+
doc_ts: datetime | None, campaigns: list[dict[str, Any]], window_days: int
|
|
427
|
+
) -> list[dict[str, Any]]:
|
|
428
|
+
if doc_ts is None:
|
|
429
|
+
return []
|
|
430
|
+
return [
|
|
431
|
+
c
|
|
432
|
+
for c in campaigns
|
|
433
|
+
if c["start_date"] is not None
|
|
434
|
+
and c["start_date"] <= doc_ts <= c["start_date"] + timedelta(days=window_days)
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _dedup_by(items: Any, key: str, key_date: str) -> list[dict[str, Any]]:
|
|
439
|
+
"""Dedup dicts by ``key`` (keep first seen), sorted by ``key_date`` (None last)."""
|
|
440
|
+
seen: dict[Any, dict[str, Any]] = {}
|
|
441
|
+
for item in items:
|
|
442
|
+
seen.setdefault(item[key], item)
|
|
443
|
+
return sorted(
|
|
444
|
+
seen.values(),
|
|
445
|
+
key=lambda x: (x.get(key_date) is None, x.get(key_date) or datetime.min),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _build_batches(
|
|
450
|
+
docs: list[dict[str, Any]], batch_size: int, gap_days: int
|
|
451
|
+
) -> list[list[dict[str, Any]]]:
|
|
452
|
+
"""Sort docs by timestamp, then split into batches of date-adjacent documents.
|
|
453
|
+
|
|
454
|
+
A new batch starts whenever the batch is full (``batch_size``) or the gap between
|
|
455
|
+
consecutive documents' timestamps exceeds ``gap_days`` — so a batch never mixes
|
|
456
|
+
documents far apart in time. Undated documents sort last and group by size only.
|
|
457
|
+
"""
|
|
458
|
+
ordered = sorted(
|
|
459
|
+
docs, key=lambda d: (d["timestamp"] is None, d["timestamp"] or datetime.min)
|
|
460
|
+
)
|
|
461
|
+
batches: list[list[dict[str, Any]]] = []
|
|
462
|
+
current: list[dict[str, Any]] = []
|
|
463
|
+
for doc in ordered:
|
|
464
|
+
if current:
|
|
465
|
+
prev_ts, ts = current[-1]["timestamp"], doc["timestamp"]
|
|
466
|
+
gap_too_large = (
|
|
467
|
+
prev_ts is not None
|
|
468
|
+
and ts is not None
|
|
469
|
+
and (ts - prev_ts) > timedelta(days=gap_days)
|
|
470
|
+
) or (prev_ts is None) != (ts is None)
|
|
471
|
+
if len(current) >= batch_size or gap_too_large:
|
|
472
|
+
batches.append(current)
|
|
473
|
+
current = []
|
|
474
|
+
current.append(doc)
|
|
475
|
+
if current:
|
|
476
|
+
batches.append(current)
|
|
477
|
+
return batches
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _valid_ids(values: list[str], valid: set[str]) -> list[str]:
|
|
481
|
+
"""Keep only known, non-empty ids, deduped, preserving order."""
|
|
482
|
+
out: list[str] = []
|
|
483
|
+
for v in values:
|
|
484
|
+
if v in ("", "null", "NONE", None):
|
|
485
|
+
continue
|
|
486
|
+
v = str(v).strip()
|
|
487
|
+
if v in valid and v not in out:
|
|
488
|
+
out.append(v)
|
|
489
|
+
return out
|
|
@@ -154,3 +154,17 @@ class VideoResult:
|
|
|
154
154
|
video_id: str
|
|
155
155
|
audio: VideoAudio | None
|
|
156
156
|
error: str | None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Document → event/campaign mapping models
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class DocumentMapping:
|
|
166
|
+
"""Event codes and campaign ids matched to a single document."""
|
|
167
|
+
|
|
168
|
+
doc_id: str
|
|
169
|
+
event_codes: list[str] = field(default_factory=list)
|
|
170
|
+
campaign_ids: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
MAX_CONTENT_CHARS = 4000
|
|
2
|
+
|
|
3
|
+
# System prompt: the matching rules, with the shared candidate lists injected.
|
|
4
|
+
MAPPING_PROMPT = (
|
|
5
|
+
"For each document, pick its event_code(s) and campaign_id(s) from the shared\n"
|
|
6
|
+
"candidate lists below (already date-filtered). Judge by content only.\n\n"
|
|
7
|
+
"candidate_events:\n{candidate_events}\n\n"
|
|
8
|
+
"candidate_campaigns:\n{candidate_campaigns}\n\n"
|
|
9
|
+
"Event rules:\n"
|
|
10
|
+
"- Match an event only if the document is actually reporting on or depicting\n"
|
|
11
|
+
" that specific event. Match what it describes, not just its date: live event\n"
|
|
12
|
+
" if it's happening now, the aftermath/follow-up event if it's looking back.\n"
|
|
13
|
+
"- A shared theme, location, date, or a mentioned participant is NOT enough.\n"
|
|
14
|
+
" When in doubt, don't match — a wrong match is worse than none.\n\n"
|
|
15
|
+
"Campaign rules — match a campaign only if BOTH:\n"
|
|
16
|
+
"1. The document names the campaign (or something unique to it) or directly\n"
|
|
17
|
+
" addresses its specific subject — not just a shared theme.\n"
|
|
18
|
+
"2. That campaign is the document's main subject, not a passing mention.\n\n"
|
|
19
|
+
"Return an empty list when nothing clearly matches. A document may match more\n"
|
|
20
|
+
"than one event or campaign."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Per-document block rendered into the human message for each batch.
|
|
24
|
+
DOCUMENT_BLOCK = (
|
|
25
|
+
"doc_id: {doc_id}\n"
|
|
26
|
+
"{title_line}"
|
|
27
|
+
"content: {content}\n"
|
|
28
|
+
"candidate_events: events dated within {event_window_days} days before this "
|
|
29
|
+
"document's timestamp {timestamp}, up to and including that date\n"
|
|
30
|
+
"candidate_campaigns: campaigns whose start date is within the "
|
|
31
|
+
"{campaign_window_days} day window before this document's timestamp {timestamp}"
|
|
32
|
+
)
|
|
@@ -54,7 +54,7 @@ class Settings(BaseSettings):
|
|
|
54
54
|
# Fleet-wide cap on concurrently-running flow runs for the ECS work pool.
|
|
55
55
|
# Acts as the queue throttle: scheduled runs beyond this wait in Prefect's
|
|
56
56
|
# queue instead of all launching at once (and OOM-ing the workers).
|
|
57
|
-
prefect_work_pool_concurrency_limit: int =
|
|
57
|
+
prefect_work_pool_concurrency_limit: int = 50
|
|
58
58
|
# ECR image used to run pipeline_flow in ECS work pool
|
|
59
59
|
prefect_engine_image: str = ""
|
|
60
60
|
# Override compute backend for all pipeline steps ("ecs", "local", etc.)
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/audio_transcription.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/digital_news_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/config.py
RENAMED
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/service.py
RENAMED
|
File without changes
|
{pulse_engine-0.2.20 → pulse_engine-0.2.22}/src/pulse_engine/adapters/transcription/source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|