pulse-engine 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pulse_engine/__init__.py +0 -0
- pulse_engine/adapters/__init__.py +58 -0
- pulse_engine/adapters/audio_transcription.py +167 -0
- pulse_engine/adapters/batcher.py +36 -0
- pulse_engine/adapters/digital_news.py +128 -0
- pulse_engine/adapters/digital_news_metadata.py +536 -0
- pulse_engine/adapters/exceptions.py +10 -0
- pulse_engine/adapters/models.py +134 -0
- pulse_engine/adapters/opensearch_storage.py +160 -0
- pulse_engine/adapters/speech_content.py +130 -0
- pulse_engine/adapters/speech_metadata.py +374 -0
- pulse_engine/adapters/twitter.py +423 -0
- pulse_engine/adapters/youtube_downloader.py +186 -0
- pulse_engine/adapters/youtube_metadata.py +261 -0
- pulse_engine/api/__init__.py +0 -0
- pulse_engine/api/v1/__init__.py +0 -0
- pulse_engine/api/v1/auth.py +91 -0
- pulse_engine/api/v1/health.py +62 -0
- pulse_engine/api/v1/router.py +16 -0
- pulse_engine/chain_recovery.py +131 -0
- pulse_engine/cli/__init__.py +0 -0
- pulse_engine/cli/main.py +169 -0
- pulse_engine/cli/templates/cookiecutter.json +4 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +13 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +32 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +17 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +25 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +8 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
- pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +15 -0
- pulse_engine/client.py +95 -0
- pulse_engine/config.py +157 -0
- pulse_engine/core/__init__.py +0 -0
- pulse_engine/core/error_handlers.py +64 -0
- pulse_engine/core/exceptions.py +67 -0
- pulse_engine/core/job_token.py +109 -0
- pulse_engine/core/logging.py +45 -0
- pulse_engine/core/scope.py +23 -0
- pulse_engine/core/security.py +130 -0
- pulse_engine/database.py +30 -0
- pulse_engine/dependencies.py +166 -0
- pulse_engine/deployment/__init__.py +0 -0
- pulse_engine/deployment/backend_deployment_repository.py +83 -0
- pulse_engine/deployment/backends/__init__.py +0 -0
- pulse_engine/deployment/backends/base.py +50 -0
- pulse_engine/deployment/backends/exceptions.py +20 -0
- pulse_engine/deployment/backends/native_lambda.py +125 -0
- pulse_engine/deployment/backends/prefect_ecs.py +116 -0
- pulse_engine/deployment/backends/prefect_k8s.py +131 -0
- pulse_engine/deployment/backends/registry.py +50 -0
- pulse_engine/deployment/infra_provisioner.py +285 -0
- pulse_engine/deployment/job_launcher.py +178 -0
- pulse_engine/deployment/models.py +48 -0
- pulse_engine/deployment/repository.py +54 -0
- pulse_engine/deployment/router.py +22 -0
- pulse_engine/deployment/schemas.py +18 -0
- pulse_engine/deployment/service.py +65 -0
- pulse_engine/extractor/__init__.py +0 -0
- pulse_engine/extractor/adapters/__init__.py +0 -0
- pulse_engine/extractor/base.py +48 -0
- pulse_engine/extractor/models.py +50 -0
- pulse_engine/extractor/orchestrator/__init__.py +15 -0
- pulse_engine/extractor/orchestrator/base.py +34 -0
- pulse_engine/extractor/orchestrator/noop.py +37 -0
- pulse_engine/extractor/orchestrator/prefect.py +163 -0
- pulse_engine/extractor/repository.py +163 -0
- pulse_engine/extractor/router.py +102 -0
- pulse_engine/extractor/schemas.py +93 -0
- pulse_engine/extractor/service.py +431 -0
- pulse_engine/extractor/stage_models.py +36 -0
- pulse_engine/extractor/stage_repository.py +109 -0
- pulse_engine/main.py +195 -0
- pulse_engine/mcp/__init__.py +0 -0
- pulse_engine/mcp/__main__.py +5 -0
- pulse_engine/mcp/server.py +108 -0
- pulse_engine/mcp/tools_jobs.py +159 -0
- pulse_engine/mcp/tools_kb.py +88 -0
- pulse_engine/mcp/tools_modules.py +115 -0
- pulse_engine/mcp/tools_pipelines.py +215 -0
- pulse_engine/mcp/tools_processor.py +208 -0
- pulse_engine/middleware/__init__.py +0 -0
- pulse_engine/middleware/rate_limit.py +144 -0
- pulse_engine/middleware/request_id.py +16 -0
- pulse_engine/middleware/security_headers.py +25 -0
- pulse_engine/middleware/tenant.py +90 -0
- pulse_engine/pipeline/__init__.py +0 -0
- pulse_engine/pipeline/config_parser.py +148 -0
- pulse_engine/pipeline/expression.py +268 -0
- pulse_engine/pipeline/models.py +98 -0
- pulse_engine/pipeline/repositories.py +224 -0
- pulse_engine/pipeline/router_modules.py +66 -0
- pulse_engine/pipeline/router_pipelines.py +198 -0
- pulse_engine/pipeline/schemas.py +200 -0
- pulse_engine/pipeline/service.py +250 -0
- pulse_engine/pipeline/translators/__init__.py +44 -0
- pulse_engine/pipeline/translators/airflow_status.py +11 -0
- pulse_engine/pipeline/translators/airflow_translator.py +22 -0
- pulse_engine/pipeline/translators/base.py +42 -0
- pulse_engine/pipeline/translators/prefect_status.py +93 -0
- pulse_engine/pipeline/translators/prefect_translator.py +195 -0
- pulse_engine/processor/__init__.py +0 -0
- pulse_engine/processor/base.py +36 -0
- pulse_engine/processor/core/__init__.py +0 -0
- pulse_engine/processor/core/analysis.py +148 -0
- pulse_engine/processor/core/chunking.py +158 -0
- pulse_engine/processor/core/prompts.py +340 -0
- pulse_engine/processor/core/topic_splitter.py +105 -0
- pulse_engine/processor/defaults/__init__.py +11 -0
- pulse_engine/processor/defaults/core_processor.py +12 -0
- pulse_engine/processor/defaults/postprocessor.py +12 -0
- pulse_engine/processor/defaults/preprocessor.py +12 -0
- pulse_engine/processor/llm/__init__.py +0 -0
- pulse_engine/processor/llm/provider.py +58 -0
- pulse_engine/processor/ocr/gemini.py +52 -0
- pulse_engine/processor/pipeline.py +107 -0
- pulse_engine/processor/postprocessor/__init__.py +0 -0
- pulse_engine/processor/postprocessor/embeddings.py +34 -0
- pulse_engine/processor/postprocessor/tasks.py +180 -0
- pulse_engine/processor/preprocessor/__init__.py +0 -0
- pulse_engine/processor/preprocessor/tasks.py +71 -0
- pulse_engine/processor/router.py +192 -0
- pulse_engine/processor/schemas.py +167 -0
- pulse_engine/registry.py +117 -0
- pulse_engine/runners/__init__.py +0 -0
- pulse_engine/runners/lambda_runner.py +26 -0
- pulse_engine/runners/pipeline_runner.py +43 -0
- pulse_engine/runners/prefect_pipeline_flow.py +904 -0
- pulse_engine/runners/prefect_runner.py +33 -0
- pulse_engine/s3.py +72 -0
- pulse_engine/secrets.py +46 -0
- pulse_engine/services/__init__.py +0 -0
- pulse_engine/services/bootstrap.py +211 -0
- pulse_engine/services/opensearch.py +84 -0
- pulse_engine/storage/__init__.py +0 -0
- pulse_engine/storage/connectors/__init__.py +0 -0
- pulse_engine/storage/connectors/athena.py +226 -0
- pulse_engine/storage/connectors/base.py +32 -0
- pulse_engine/storage/connectors/opensearch.py +344 -0
- pulse_engine/storage/knowledge_base.py +68 -0
- pulse_engine/storage/router.py +78 -0
- pulse_engine/storage/schemas.py +93 -0
- pulse_engine/testing/__init__.py +13 -0
- pulse_engine/testing/fixtures.py +50 -0
- pulse_engine/testing/mocks.py +104 -0
- pulse_engine/worker.py +53 -0
- pulse_engine-0.2.0.dist-info/METADATA +654 -0
- pulse_engine-0.2.0.dist-info/RECORD +150 -0
- pulse_engine-0.2.0.dist-info/WHEEL +4 -0
- pulse_engine-0.2.0.dist-info/entry_points.txt +4 -0
pulse_engine/main.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
from pulse_engine.api.v1.router import v1_router
|
|
12
|
+
from pulse_engine.config import Settings, get_settings
|
|
13
|
+
from pulse_engine.core.error_handlers import register_exception_handlers
|
|
14
|
+
from pulse_engine.core.security import (
|
|
15
|
+
CognitoTokenVerifier,
|
|
16
|
+
MockTokenVerifier,
|
|
17
|
+
TokenVerifier,
|
|
18
|
+
)
|
|
19
|
+
from pulse_engine.middleware.rate_limit import RateLimitMiddleware
|
|
20
|
+
from pulse_engine.middleware.request_id import RequestIDMiddleware
|
|
21
|
+
from pulse_engine.middleware.security_headers import SecurityHeadersMiddleware
|
|
22
|
+
from pulse_engine.middleware.tenant import TenantMiddleware
|
|
23
|
+
from pulse_engine.pipeline.router_modules import router as pipeline_modules_router
|
|
24
|
+
from pulse_engine.pipeline.router_pipelines import router as pipeline_pipelines_router
|
|
25
|
+
from pulse_engine.services.bootstrap import bootstrap_services
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from pulse_engine.registry import ProductManifest
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@asynccontextmanager
|
|
32
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
33
|
+
settings: Settings = app.state.settings
|
|
34
|
+
manifest: ProductManifest | None = getattr(app.state, "manifest", None)
|
|
35
|
+
|
|
36
|
+
async with bootstrap_services(settings, manifest) as container:
|
|
37
|
+
app.state.opensearch = container.opensearch
|
|
38
|
+
app.state.knowledge_base = container.kb_service
|
|
39
|
+
app.state.db_session_factory = container.db_session_factory
|
|
40
|
+
app.state.orchestrator_adapter = container.orchestrator_adapter
|
|
41
|
+
app.state.celery_app = container.celery_app
|
|
42
|
+
app.state.pipeline = container.pipeline
|
|
43
|
+
app.state.s3_client = container.s3_client
|
|
44
|
+
app.state.token_issuer = container.token_issuer
|
|
45
|
+
app.state.pipeline_translators = container.pipeline_translators
|
|
46
|
+
app.state.pipeline_status_providers = container.pipeline_status_providers
|
|
47
|
+
|
|
48
|
+
# Start chain recovery polling if configured
|
|
49
|
+
recovery_task = None
|
|
50
|
+
if (
|
|
51
|
+
container.db_session_factory
|
|
52
|
+
and container.token_issuer
|
|
53
|
+
and settings.pulse_engine_url
|
|
54
|
+
and settings.pulse_job_token_secret
|
|
55
|
+
):
|
|
56
|
+
|
|
57
|
+
async def _recovery_loop() -> None:
|
|
58
|
+
from pulse_engine.chain_recovery import ChainRecoveryTask
|
|
59
|
+
from pulse_engine.deployment.backend_deployment_repository import (
|
|
60
|
+
BackendDeploymentRepository,
|
|
61
|
+
)
|
|
62
|
+
from pulse_engine.deployment.backends.registry import BackendRegistry
|
|
63
|
+
from pulse_engine.deployment.job_launcher import JobLauncher
|
|
64
|
+
from pulse_engine.deployment.repository import (
|
|
65
|
+
RegistrationRepository,
|
|
66
|
+
)
|
|
67
|
+
from pulse_engine.extractor.repository import JobRepository
|
|
68
|
+
from pulse_engine.extractor.stage_repository import (
|
|
69
|
+
StageRepository,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
registry = BackendRegistry(settings)
|
|
73
|
+
factory = container.db_session_factory
|
|
74
|
+
assert factory is not None
|
|
75
|
+
while True:
|
|
76
|
+
try:
|
|
77
|
+
async with factory() as session:
|
|
78
|
+
job_launcher = JobLauncher(
|
|
79
|
+
registration_repo=RegistrationRepository(session),
|
|
80
|
+
backend_deployment_repo=BackendDeploymentRepository(
|
|
81
|
+
session
|
|
82
|
+
),
|
|
83
|
+
registry=registry,
|
|
84
|
+
token_issuer=container.token_issuer,
|
|
85
|
+
settings=settings,
|
|
86
|
+
)
|
|
87
|
+
task = ChainRecoveryTask(
|
|
88
|
+
stage_repo=StageRepository(session),
|
|
89
|
+
job_repo=JobRepository(session),
|
|
90
|
+
job_launcher=job_launcher,
|
|
91
|
+
orchestrator=container.orchestrator_adapter,
|
|
92
|
+
settings=settings,
|
|
93
|
+
)
|
|
94
|
+
await task.check_once()
|
|
95
|
+
await task.check_stalled_chains()
|
|
96
|
+
except Exception:
|
|
97
|
+
import structlog
|
|
98
|
+
|
|
99
|
+
structlog.get_logger().warning(
|
|
100
|
+
"chain_recovery_loop_error",
|
|
101
|
+
exc_info=True,
|
|
102
|
+
)
|
|
103
|
+
await asyncio.sleep(60)
|
|
104
|
+
|
|
105
|
+
recovery_task = asyncio.create_task(_recovery_loop())
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
yield
|
|
109
|
+
finally:
|
|
110
|
+
if recovery_task:
|
|
111
|
+
recovery_task.cancel()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_verifier(settings: Settings) -> TokenVerifier:
|
|
115
|
+
if settings.mock_jwt_secret:
|
|
116
|
+
return MockTokenVerifier(
|
|
117
|
+
secret=settings.mock_jwt_secret,
|
|
118
|
+
audience=settings.cognito_app_client_id,
|
|
119
|
+
issuer=settings.cognito_issuer,
|
|
120
|
+
)
|
|
121
|
+
return CognitoTokenVerifier(
|
|
122
|
+
jwks_url=settings.cognito_jwks_url,
|
|
123
|
+
issuer=settings.cognito_issuer,
|
|
124
|
+
audience=settings.cognito_app_client_id,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_app(
|
|
129
|
+
settings: Settings | None = None,
|
|
130
|
+
token_verifier: TokenVerifier | None = None,
|
|
131
|
+
manifest: ProductManifest | None = None,
|
|
132
|
+
) -> FastAPI:
|
|
133
|
+
if settings is None:
|
|
134
|
+
settings = get_settings()
|
|
135
|
+
|
|
136
|
+
if manifest:
|
|
137
|
+
from pulse_engine.registry import ManifestValidationError, validate_manifest
|
|
138
|
+
|
|
139
|
+
errors = validate_manifest(manifest)
|
|
140
|
+
if errors:
|
|
141
|
+
raise ManifestValidationError(errors)
|
|
142
|
+
|
|
143
|
+
app = FastAPI(
|
|
144
|
+
title="Pulse Platform",
|
|
145
|
+
version=settings.app_version,
|
|
146
|
+
lifespan=lifespan,
|
|
147
|
+
docs_url=None if settings.app_env == "production" else "/docs",
|
|
148
|
+
redoc_url=None if settings.app_env == "production" else "/redoc",
|
|
149
|
+
openapi_url=None if settings.app_env == "production" else "/openapi.json",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
app.state.settings = settings
|
|
153
|
+
app.state.manifest = manifest
|
|
154
|
+
|
|
155
|
+
register_exception_handlers(app)
|
|
156
|
+
|
|
157
|
+
verifier = token_verifier or _build_verifier(settings)
|
|
158
|
+
|
|
159
|
+
# Middleware is applied in reverse order — outermost first
|
|
160
|
+
app.add_middleware(
|
|
161
|
+
TenantMiddleware,
|
|
162
|
+
verifier=verifier,
|
|
163
|
+
job_token_secret=settings.pulse_job_token_secret,
|
|
164
|
+
)
|
|
165
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
166
|
+
app.add_middleware(RequestIDMiddleware)
|
|
167
|
+
app.add_middleware(
|
|
168
|
+
CORSMiddleware,
|
|
169
|
+
allow_origins=(
|
|
170
|
+
["*"]
|
|
171
|
+
if settings.app_env == "development"
|
|
172
|
+
else [settings.pulse_engine_url]
|
|
173
|
+
if settings.pulse_engine_url
|
|
174
|
+
else []
|
|
175
|
+
),
|
|
176
|
+
allow_credentials=True,
|
|
177
|
+
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
178
|
+
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
|
|
179
|
+
expose_headers=["X-Request-ID"],
|
|
180
|
+
)
|
|
181
|
+
app.add_middleware(
|
|
182
|
+
RateLimitMiddleware,
|
|
183
|
+
max_requests=100,
|
|
184
|
+
window_seconds=60,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
app.include_router(v1_router)
|
|
188
|
+
app.include_router(pipeline_modules_router)
|
|
189
|
+
app.include_router(pipeline_pipelines_router)
|
|
190
|
+
|
|
191
|
+
if manifest:
|
|
192
|
+
for router in manifest.routers:
|
|
193
|
+
app.include_router(router, prefix="/api/v1")
|
|
194
|
+
|
|
195
|
+
return app
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""MCP server for Pulse Platform.
|
|
2
|
+
|
|
3
|
+
Exposes KB, Jobs, Processor, Pipeline, and Module tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import importlib
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
|
|
13
|
+
from mcp.server.fastmcp import FastMCP
|
|
14
|
+
from pydantic import ValidationError
|
|
15
|
+
|
|
16
|
+
from pulse_engine.config import get_settings as _get_settings
|
|
17
|
+
from pulse_engine.core.exceptions import (
|
|
18
|
+
AppError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
ServiceUnavailableError,
|
|
21
|
+
TooManyRequestsError,
|
|
22
|
+
)
|
|
23
|
+
from pulse_engine.services.bootstrap import ServiceContainer
|
|
24
|
+
|
|
25
|
+
_s = _get_settings()
|
|
26
|
+
mcp = FastMCP("Pulse Platform", host=_s.mcp_sse_host, port=_s.mcp_sse_port)
|
|
27
|
+
|
|
28
|
+
_container: ServiceContainer | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_container() -> ServiceContainer:
|
|
32
|
+
if _container is None:
|
|
33
|
+
raise RuntimeError("MCP server not initialized — no service container")
|
|
34
|
+
return _container
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_mcp_errors(fn: F) -> F:
|
|
41
|
+
@functools.wraps(fn)
|
|
42
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
43
|
+
try:
|
|
44
|
+
return await fn(*args, **kwargs)
|
|
45
|
+
except NotFoundError as e:
|
|
46
|
+
return {"error": True, "error_type": "NotFoundError", "message": e.message}
|
|
47
|
+
except TooManyRequestsError as e:
|
|
48
|
+
return {
|
|
49
|
+
"error": True,
|
|
50
|
+
"error_type": "TooManyRequestsError",
|
|
51
|
+
"message": e.message,
|
|
52
|
+
}
|
|
53
|
+
except ServiceUnavailableError as e:
|
|
54
|
+
return {
|
|
55
|
+
"error": True,
|
|
56
|
+
"error_type": "ServiceUnavailableError",
|
|
57
|
+
"message": e.message,
|
|
58
|
+
}
|
|
59
|
+
except ValidationError as e:
|
|
60
|
+
return {
|
|
61
|
+
"error": True,
|
|
62
|
+
"error_type": "ValidationError",
|
|
63
|
+
"message": str(e),
|
|
64
|
+
}
|
|
65
|
+
except AppError as e:
|
|
66
|
+
return {
|
|
67
|
+
"error": True,
|
|
68
|
+
"error_type": type(e).__name__,
|
|
69
|
+
"message": e.message,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return wrapper # type: ignore[return-value]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_mcp_server(
|
|
76
|
+
manifest: Any | None = None,
|
|
77
|
+
) -> FastMCP:
|
|
78
|
+
"""Return the module-level FastMCP instance, optionally loading product tools."""
|
|
79
|
+
if manifest:
|
|
80
|
+
for module_path in manifest.mcp_tool_modules:
|
|
81
|
+
importlib.import_module(module_path)
|
|
82
|
+
return mcp
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def run_mcp_server(manifest: Any | None = None) -> None:
|
|
86
|
+
global _container
|
|
87
|
+
|
|
88
|
+
from pulse_engine.config import get_settings
|
|
89
|
+
from pulse_engine.services.bootstrap import bootstrap_services
|
|
90
|
+
|
|
91
|
+
settings = get_settings()
|
|
92
|
+
|
|
93
|
+
async with bootstrap_services(settings, manifest) as container:
|
|
94
|
+
_container = container
|
|
95
|
+
|
|
96
|
+
# Import engine tool modules to trigger @mcp.tool() registrations
|
|
97
|
+
import pulse_engine.mcp.tools_jobs # noqa: F401
|
|
98
|
+
import pulse_engine.mcp.tools_kb # noqa: F401
|
|
99
|
+
import pulse_engine.mcp.tools_modules # noqa: F401
|
|
100
|
+
import pulse_engine.mcp.tools_pipelines # noqa: F401
|
|
101
|
+
import pulse_engine.mcp.tools_processor # noqa: F401
|
|
102
|
+
|
|
103
|
+
# Import product tool modules
|
|
104
|
+
if manifest:
|
|
105
|
+
for module_path in manifest.mcp_tool_modules:
|
|
106
|
+
importlib.import_module(module_path)
|
|
107
|
+
|
|
108
|
+
await mcp.run_sse_async()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""MCP tools for Job operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pulse_engine.core.exceptions import ServiceUnavailableError
|
|
6
|
+
from pulse_engine.extractor.repository import JobRepository
|
|
7
|
+
from pulse_engine.extractor.schemas import (
|
|
8
|
+
CreateJobRequest,
|
|
9
|
+
JobPriority,
|
|
10
|
+
StatusUpdateRequest,
|
|
11
|
+
)
|
|
12
|
+
from pulse_engine.extractor.service import JobService
|
|
13
|
+
from pulse_engine.mcp.server import get_container, handle_mcp_errors, mcp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _build_job_service(
|
|
17
|
+
session: Any,
|
|
18
|
+
container: Any,
|
|
19
|
+
) -> JobService:
|
|
20
|
+
repo = JobRepository(session)
|
|
21
|
+
return JobService(
|
|
22
|
+
repository=repo,
|
|
23
|
+
orchestrator=container.orchestrator_adapter,
|
|
24
|
+
settings=container.settings,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@mcp.tool()
|
|
29
|
+
@handle_mcp_errors
|
|
30
|
+
async def jobs_register(
|
|
31
|
+
tenant_id: str,
|
|
32
|
+
job_type: str,
|
|
33
|
+
product: str,
|
|
34
|
+
priority: str = "normal",
|
|
35
|
+
orchestrator_run_id: str | None = None,
|
|
36
|
+
parameters: dict[str, Any] | None = None,
|
|
37
|
+
callback_url: str | None = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Register a new extraction job for a tenant."""
|
|
40
|
+
container = get_container()
|
|
41
|
+
if container.db_session_factory is None:
|
|
42
|
+
raise ServiceUnavailableError("Database not configured")
|
|
43
|
+
async with container.db_session_factory() as session:
|
|
44
|
+
service = _build_job_service(session, container)
|
|
45
|
+
request = CreateJobRequest(
|
|
46
|
+
job_type=job_type,
|
|
47
|
+
product=product,
|
|
48
|
+
priority=JobPriority(priority),
|
|
49
|
+
orchestrator_run_id=orchestrator_run_id,
|
|
50
|
+
parameters=parameters or {},
|
|
51
|
+
callback_url=callback_url,
|
|
52
|
+
)
|
|
53
|
+
result = await service.register_job(tenant_id, request)
|
|
54
|
+
return result.model_dump(mode="json")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@mcp.tool()
|
|
58
|
+
@handle_mcp_errors
|
|
59
|
+
async def jobs_get(
|
|
60
|
+
tenant_id: str,
|
|
61
|
+
job_id: str,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Get details of a specific job."""
|
|
64
|
+
container = get_container()
|
|
65
|
+
if container.db_session_factory is None:
|
|
66
|
+
raise ServiceUnavailableError("Database not configured")
|
|
67
|
+
async with container.db_session_factory() as session:
|
|
68
|
+
service = _build_job_service(session, container)
|
|
69
|
+
result = await service.get_job(tenant_id, job_id)
|
|
70
|
+
return result.model_dump(mode="json")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@mcp.tool()
|
|
74
|
+
@handle_mcp_errors
|
|
75
|
+
async def jobs_list(
|
|
76
|
+
tenant_id: str,
|
|
77
|
+
status: str | None = None,
|
|
78
|
+
product: str | None = None,
|
|
79
|
+
job_type: str | None = None,
|
|
80
|
+
limit: int = 20,
|
|
81
|
+
offset: int = 0,
|
|
82
|
+
sort_by: str = "created_at",
|
|
83
|
+
order: str = "desc",
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""List jobs for a tenant with optional filters."""
|
|
86
|
+
container = get_container()
|
|
87
|
+
if container.db_session_factory is None:
|
|
88
|
+
raise ServiceUnavailableError("Database not configured")
|
|
89
|
+
async with container.db_session_factory() as session:
|
|
90
|
+
service = _build_job_service(session, container)
|
|
91
|
+
result = await service.list_jobs(
|
|
92
|
+
tenant_id,
|
|
93
|
+
status=status,
|
|
94
|
+
product=product,
|
|
95
|
+
job_type=job_type,
|
|
96
|
+
limit=limit,
|
|
97
|
+
offset=offset,
|
|
98
|
+
sort_by=sort_by,
|
|
99
|
+
order=order,
|
|
100
|
+
)
|
|
101
|
+
return result.model_dump(mode="json")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@mcp.tool()
|
|
105
|
+
@handle_mcp_errors
|
|
106
|
+
async def jobs_push_status(
|
|
107
|
+
tenant_id: str,
|
|
108
|
+
job_id: str,
|
|
109
|
+
status: str,
|
|
110
|
+
result_summary: dict[str, Any] | None = None,
|
|
111
|
+
error: str | None = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""Push a status update for a job."""
|
|
114
|
+
container = get_container()
|
|
115
|
+
if container.db_session_factory is None:
|
|
116
|
+
raise ServiceUnavailableError("Database not configured")
|
|
117
|
+
async with container.db_session_factory() as session:
|
|
118
|
+
service = _build_job_service(session, container)
|
|
119
|
+
from pulse_engine.extractor.schemas import JobStatus
|
|
120
|
+
|
|
121
|
+
request = StatusUpdateRequest(
|
|
122
|
+
status=JobStatus(status),
|
|
123
|
+
result_summary=result_summary,
|
|
124
|
+
error=error,
|
|
125
|
+
)
|
|
126
|
+
result = await service.push_status(tenant_id, job_id, request)
|
|
127
|
+
return result.model_dump(mode="json")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
@handle_mcp_errors
|
|
132
|
+
async def jobs_cancel(
|
|
133
|
+
tenant_id: str,
|
|
134
|
+
job_id: str,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
"""Cancel a running or pending job."""
|
|
137
|
+
container = get_container()
|
|
138
|
+
if container.db_session_factory is None:
|
|
139
|
+
raise ServiceUnavailableError("Database not configured")
|
|
140
|
+
async with container.db_session_factory() as session:
|
|
141
|
+
service = _build_job_service(session, container)
|
|
142
|
+
result = await service.cancel_job(tenant_id, job_id)
|
|
143
|
+
return result.model_dump(mode="json")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
@handle_mcp_errors
|
|
148
|
+
async def jobs_delete(
|
|
149
|
+
tenant_id: str,
|
|
150
|
+
job_id: str,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""Delete a job record."""
|
|
153
|
+
container = get_container()
|
|
154
|
+
if container.db_session_factory is None:
|
|
155
|
+
raise ServiceUnavailableError("Database not configured")
|
|
156
|
+
async with container.db_session_factory() as session:
|
|
157
|
+
service = _build_job_service(session, container)
|
|
158
|
+
deleted = await service.delete_job(tenant_id, job_id)
|
|
159
|
+
return {"deleted": deleted, "job_id": job_id}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""MCP tools for Knowledge Base operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pulse_engine.mcp.server import get_container, handle_mcp_errors, mcp
|
|
6
|
+
from pulse_engine.storage.schemas import Document, SearchQuery, SearchQueryType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
@handle_mcp_errors
|
|
11
|
+
async def kb_store_documents(
|
|
12
|
+
tenant_id: str,
|
|
13
|
+
documents: list[dict[str, Any]],
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
"""Store documents in the Knowledge Base for a tenant."""
|
|
16
|
+
container = get_container()
|
|
17
|
+
docs = [Document(**d) for d in documents]
|
|
18
|
+
result = await container.kb_service.store_documents(tenant_id, docs)
|
|
19
|
+
return result.model_dump(mode="json")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
@handle_mcp_errors
|
|
24
|
+
async def kb_retrieve_document(
|
|
25
|
+
tenant_id: str,
|
|
26
|
+
document_id: str,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Retrieve a single document by ID from the Knowledge Base."""
|
|
29
|
+
container = get_container()
|
|
30
|
+
doc = await container.kb_service.retrieve_document(tenant_id, document_id)
|
|
31
|
+
return doc.model_dump(mode="json")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
@handle_mcp_errors
|
|
36
|
+
async def kb_search(
|
|
37
|
+
tenant_id: str,
|
|
38
|
+
query_text: str,
|
|
39
|
+
query_type: str = "text",
|
|
40
|
+
filters: dict[str, Any] | None = None,
|
|
41
|
+
size: int = 10,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""Search documents in the Knowledge Base."""
|
|
44
|
+
container = get_container()
|
|
45
|
+
query = SearchQuery(
|
|
46
|
+
query_type=SearchQueryType(query_type),
|
|
47
|
+
query_text=query_text,
|
|
48
|
+
filters=filters or {},
|
|
49
|
+
size=size,
|
|
50
|
+
)
|
|
51
|
+
result = await container.kb_service.search(tenant_id, query)
|
|
52
|
+
return result.model_dump(mode="json")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
@handle_mcp_errors
|
|
57
|
+
async def kb_delete_document(
|
|
58
|
+
tenant_id: str,
|
|
59
|
+
document_id: str,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Delete a document from the Knowledge Base."""
|
|
62
|
+
container = get_container()
|
|
63
|
+
await container.kb_service.delete_document(tenant_id, document_id)
|
|
64
|
+
return {"deleted": True, "document_id": document_id}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
@handle_mcp_errors
|
|
69
|
+
async def kb_get_stats(
|
|
70
|
+
tenant_id: str,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""Get Knowledge Base statistics for a tenant."""
|
|
73
|
+
container = get_container()
|
|
74
|
+
stats = await container.kb_service.get_stats(tenant_id)
|
|
75
|
+
return stats.model_dump(mode="json")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp.tool()
|
|
79
|
+
@handle_mcp_errors
|
|
80
|
+
async def kb_run_query(
|
|
81
|
+
tenant_id: str,
|
|
82
|
+
sql: str,
|
|
83
|
+
parameters: dict[str, Any] | None = None,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""Execute an Athena SQL query against the Knowledge Base."""
|
|
86
|
+
container = get_container()
|
|
87
|
+
result = await container.kb_service.execute_query(tenant_id, sql, parameters)
|
|
88
|
+
return result.model_dump(mode="json")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""MCP tools for Module Registry operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pulse_engine.core.exceptions import NotFoundError, ServiceUnavailableError
|
|
6
|
+
from pulse_engine.mcp.server import get_container, handle_mcp_errors, mcp
|
|
7
|
+
from pulse_engine.pipeline.repositories import ModuleRegistryRepository
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@handle_mcp_errors
|
|
12
|
+
async def modules_register(
|
|
13
|
+
tenant_id: str,
|
|
14
|
+
product: str,
|
|
15
|
+
modules: dict[str, dict[str, str]],
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
"""Register pipeline module images for a product.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
tenant_id: Tenant identifier.
|
|
21
|
+
product: Product name (e.g. "sample-product").
|
|
22
|
+
modules: Map of module name to {"image": "<ecr-uri>:<tag>"}.
|
|
23
|
+
Example: {"extractor": {"image": "123.ecr/extractor:latest"}}
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
{"product": "...", "modules": {"extractor": {"status": "registered"}, ...}}
|
|
27
|
+
"""
|
|
28
|
+
container = get_container()
|
|
29
|
+
if container.db_session_factory is None:
|
|
30
|
+
raise ServiceUnavailableError("Database not configured")
|
|
31
|
+
|
|
32
|
+
async with container.db_session_factory() as session:
|
|
33
|
+
repo = ModuleRegistryRepository(session)
|
|
34
|
+
result_modules: dict[str, dict[str, str]] = {}
|
|
35
|
+
|
|
36
|
+
for module_name, module_def in modules.items():
|
|
37
|
+
image = module_def.get("image", "")
|
|
38
|
+
if not image:
|
|
39
|
+
result_modules[module_name] = {
|
|
40
|
+
"status": "error",
|
|
41
|
+
"detail": "missing image",
|
|
42
|
+
}
|
|
43
|
+
continue
|
|
44
|
+
await repo.upsert(tenant_id, product, module_name, image)
|
|
45
|
+
result_modules[module_name] = {"status": "registered"}
|
|
46
|
+
|
|
47
|
+
return {"product": product, "modules": result_modules}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@mcp.tool()
|
|
51
|
+
@handle_mcp_errors
|
|
52
|
+
async def modules_list(
|
|
53
|
+
tenant_id: str,
|
|
54
|
+
product: str,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""List registered modules for a product.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
tenant_id: Tenant identifier.
|
|
60
|
+
product: Product name.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
{"product": "...", "modules": [{"name": "...", "image": "..."}, ...]}
|
|
64
|
+
"""
|
|
65
|
+
container = get_container()
|
|
66
|
+
if container.db_session_factory is None:
|
|
67
|
+
raise ServiceUnavailableError("Database not configured")
|
|
68
|
+
|
|
69
|
+
async with container.db_session_factory() as session:
|
|
70
|
+
repo = ModuleRegistryRepository(session)
|
|
71
|
+
entries = await repo.list_by_product(tenant_id, product)
|
|
72
|
+
return {
|
|
73
|
+
"product": product,
|
|
74
|
+
"modules": [
|
|
75
|
+
{
|
|
76
|
+
"name": e.module_name,
|
|
77
|
+
"image": e.image,
|
|
78
|
+
"updated_at": e.updated_at.isoformat() if e.updated_at else None,
|
|
79
|
+
}
|
|
80
|
+
for e in entries
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@mcp.tool()
|
|
86
|
+
@handle_mcp_errors
|
|
87
|
+
async def modules_delete(
|
|
88
|
+
tenant_id: str,
|
|
89
|
+
product: str,
|
|
90
|
+
module_name: str,
|
|
91
|
+
) -> dict[str, Any]:
|
|
92
|
+
"""Delete a module registration.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tenant_id: Tenant identifier.
|
|
96
|
+
product: Product name.
|
|
97
|
+
module_name: Module to delete.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
{"deleted": true, "product": "...", "module_name": "..."}
|
|
101
|
+
"""
|
|
102
|
+
container = get_container()
|
|
103
|
+
if container.db_session_factory is None:
|
|
104
|
+
raise ServiceUnavailableError("Database not configured")
|
|
105
|
+
|
|
106
|
+
async with container.db_session_factory() as session:
|
|
107
|
+
repo = ModuleRegistryRepository(session)
|
|
108
|
+
deleted = await repo.delete(tenant_id, product, module_name)
|
|
109
|
+
if not deleted:
|
|
110
|
+
raise NotFoundError(
|
|
111
|
+
f"Module '{module_name}' not found for product '{product}'",
|
|
112
|
+
product=product,
|
|
113
|
+
module_name=module_name,
|
|
114
|
+
)
|
|
115
|
+
return {"deleted": True, "product": product, "module_name": module_name}
|