Add CP source adapter registry with multi-worker queues and LLM extract.

Replace legacy root backend/frontend with Telegram, Crawl4AI, and VIINA adapters routed by Redis job families.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-14 11:34:28 +03:00
co-authored by Cursor
parent 1492576fd9
commit 8fbabd3c11
54 changed files with 1625 additions and 2521 deletions
+5 -2
View File
@@ -2,13 +2,16 @@ FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
COPY centers/analytics/api/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
COPY contracts/ ./contracts/
COPY centers/analytics/api/app/ ./app/
RUN mkdir -p /data
ENV PYTHONPATH=/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+9
View File
@@ -1,8 +1,17 @@
from contextlib import asynccontextmanager
import sys
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
# Monorepo / Docker: contracts live next to app or at repo root
_HERE = Path(__file__).resolve()
for _candidate in (_HERE.parent, *_HERE.parents):
if (_candidate / "contracts").is_dir() and str(_candidate) not in sys.path:
sys.path.insert(0, str(_candidate))
break
from .database import Base, engine, get_db
from .routers import admin, internal, map, objects, v1
from .seed import seed_objects, seed_test_consumer
+14 -2
View File
@@ -37,11 +37,23 @@ from ..services.jobs import enqueue_job
router = APIRouter(prefix="/admin", tags=["admin"])
def _validated_source_config(source_type: str, source_config: dict) -> dict:
try:
from contracts.queues import family_for_source
from contracts.sources import parse_source_config
family_for_source(source_type)
return parse_source_config(source_type, source_config or {}).model_dump()
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/jobs", response_model=ParseJobRead, status_code=201)
def create_parse_job(payload: ParseJobCreate, db: Session = Depends(get_db)):
source_config = _validated_source_config(payload.source_type, payload.source_config)
job = ParseJob(
source_type=payload.source_type,
source_config=payload.source_config,
source_config=source_config,
schedule=payload.schedule,
interval_seconds=payload.interval_seconds,
is_active=payload.is_active,
@@ -96,7 +108,7 @@ def update_parse_job(
raise HTTPException(status_code=404, detail="Job not found")
if payload.source_config is not None:
job.source_config = payload.source_config
job.source_config = _validated_source_config(job.source_type, payload.source_config)
if payload.interval_seconds is not None:
job.interval_seconds = payload.interval_seconds
if payload.is_active is not None:
+23 -3
View File
@@ -1,10 +1,22 @@
import json
import os
import sys
from pathlib import Path
import redis
# Allow importing shared contracts from monorepo root in local runs
_HERE = Path(__file__).resolve()
for _candidate in (_HERE.parent, *_HERE.parents):
if (_candidate / "contracts").is_dir():
if str(_candidate) not in sys.path:
sys.path.insert(0, str(_candidate))
break
from contracts.queues import queue_key_for_source # noqa: E402
from contracts.sources import parse_source_config # noqa: E402
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
JOB_QUEUE_KEY = "cp:jobs"
def get_redis() -> redis.Redis:
@@ -12,9 +24,17 @@ def get_redis() -> redis.Redis:
def enqueue_job(job_id: int, source_type: str, source_config: dict) -> None:
# Validate known configs early; unknown types still raise from queue_key_for_source
try:
parse_source_config(source_type, source_config or {})
except Exception:
# Keep enqueue resilient for legacy rows; worker validates again
pass
payload = {
"job_id": job_id,
"source_type": source_type,
"source_config": source_config,
"source_config": source_config or {},
}
get_redis().rpush(JOB_QUEUE_KEY, json.dumps(payload))
key = queue_key_for_source(source_type)
get_redis().rpush(key, json.dumps(payload))