feat: Add Alateya, Clan, Eonarch agents + fix gateway-router connection
## Agents Added - Alateya: R&D, biotech, innovations - Clan (Spirit): Community spirit agent - Eonarch: Consciousness evolution agent ## Changes - docker-compose.node1.yml: Added tokens for all 3 new agents - gateway-bot/http_api.py: Added configs and webhook endpoints - gateway-bot/clan_prompt.txt: New prompt file - gateway-bot/eonarch_prompt.txt: New prompt file ## Fixes - Fixed ROUTER_URL from :9102 to :8000 (internal container port) - All 9 Telegram agents now working ## Documentation - Created PROJECT-MASTER-INDEX.md - single entry point - Added various status documents and scripts Tokens configured: - Helion, NUTRA, Agromatrix (existing) - Alateya, Clan, Eonarch (new) - Druid, GreenFood, DAARWIZZ (configured)
This commit is contained in:
684
services/artifact-registry/app/main.py
Normal file
684
services/artifact-registry/app/main.py
Normal file
@@ -0,0 +1,684 @@
|
||||
"""
|
||||
Artifact Registry v0
|
||||
- Stores artifacts + versions + jobs in Postgres
|
||||
- Stores payloads in MinIO
|
||||
- Publishes render jobs to NATS
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import asyncpg
|
||||
import httpx
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
from nats.aio.client import Client as NATS
|
||||
from pydantic import BaseModel
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_VERSION = "0.1.0"
|
||||
|
||||
POSTGRES_HOST = os.getenv("POSTGRES_HOST", "dagi-postgres")
|
||||
POSTGRES_PORT = int(os.getenv("POSTGRES_PORT", "5432"))
|
||||
POSTGRES_USER = os.getenv("POSTGRES_USER", "daarion")
|
||||
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "DaarionDB2026!")
|
||||
POSTGRES_DB = os.getenv("POSTGRES_DB", "daarion_main")
|
||||
|
||||
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio:9000")
|
||||
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
|
||||
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin")
|
||||
MINIO_BUCKET = os.getenv("MINIO_BUCKET", "artifacts")
|
||||
MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true"
|
||||
|
||||
NATS_URL = os.getenv("NATS_URL", "nats://nats:4222")
|
||||
|
||||
pool: Optional[asyncpg.Pool] = None
|
||||
nats_client: Optional[NATS] = None
|
||||
minio_client: Optional[Minio] = None
|
||||
|
||||
app = FastAPI(
|
||||
title="Artifact Registry",
|
||||
version=APP_VERSION,
|
||||
description="Registry for presentations/docs artifacts"
|
||||
)
|
||||
|
||||
|
||||
class PresentationRenderRequest(BaseModel):
|
||||
brand_id: str
|
||||
project_id: Optional[str] = None
|
||||
title: str
|
||||
slides: List[str]
|
||||
theme_id: Optional[str] = "default-v1"
|
||||
version_label: Optional[str] = "source"
|
||||
acl_ref: Optional[str] = None
|
||||
|
||||
|
||||
class PresentationRenderResponse(BaseModel):
|
||||
artifact_id: str
|
||||
input_version_id: str
|
||||
job_id: str
|
||||
status_url: str
|
||||
|
||||
|
||||
class ArtifactCreateRequest(BaseModel):
|
||||
type: str
|
||||
title: Optional[str] = None
|
||||
brand_id: Optional[str] = None
|
||||
project_id: Optional[str] = None
|
||||
acl_ref: Optional[str] = None
|
||||
created_by: Optional[str] = None
|
||||
|
||||
|
||||
class ArtifactCreateResponse(BaseModel):
|
||||
artifact_id: str
|
||||
|
||||
|
||||
class ArtifactVersionFromUrlRequest(BaseModel):
|
||||
url: str
|
||||
mime: str
|
||||
label: Optional[str] = "source"
|
||||
meta_json: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class ArtifactVersionResponse(BaseModel):
|
||||
version_id: str
|
||||
storage_key: str
|
||||
sha256: str
|
||||
size_bytes: int
|
||||
|
||||
|
||||
class ArtifactVersionCreateRequest(BaseModel):
|
||||
storage_key: str
|
||||
sha256: str
|
||||
mime: str
|
||||
size_bytes: int
|
||||
label: Optional[str] = "source"
|
||||
meta_json: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class ArtifactJobRequest(BaseModel):
|
||||
job_type: str
|
||||
input_version_id: Optional[str] = None
|
||||
force: Optional[bool] = False
|
||||
|
||||
|
||||
class ArtifactJobResponse(BaseModel):
|
||||
job_id: str
|
||||
status_url: str
|
||||
|
||||
|
||||
class JobCompleteRequest(BaseModel):
|
||||
output_storage_key: str
|
||||
mime: str
|
||||
size_bytes: int
|
||||
sha256: str
|
||||
label: Optional[str] = "pptx"
|
||||
|
||||
|
||||
class JobDoneRequest(BaseModel):
|
||||
note: Optional[str] = None
|
||||
meta_json: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class JobFailRequest(BaseModel):
|
||||
error_text: str
|
||||
|
||||
|
||||
SQL_CREATE = """
|
||||
create table if not exists artifacts (
|
||||
id text primary key,
|
||||
type text not null check (type in ('presentation','doc')),
|
||||
title text,
|
||||
brand_id text,
|
||||
project_id text,
|
||||
acl_ref text,
|
||||
created_by text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists artifact_versions (
|
||||
id text primary key,
|
||||
artifact_id text not null references artifacts(id) on delete cascade,
|
||||
label text,
|
||||
sha256 text not null,
|
||||
mime text not null,
|
||||
size_bytes bigint not null,
|
||||
storage_key text not null,
|
||||
meta_json jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (artifact_id, sha256)
|
||||
);
|
||||
|
||||
create table if not exists artifact_jobs (
|
||||
id text primary key,
|
||||
artifact_id text not null references artifacts(id) on delete cascade,
|
||||
input_version_id text not null references artifact_versions(id),
|
||||
job_type text not null check (job_type in ('render_pptx','render_pdf','index_doc')),
|
||||
status text not null check (status in ('queued','running','done','failed')),
|
||||
output_version_id text references artifact_versions(id),
|
||||
error_text text,
|
||||
attempts int not null default 0,
|
||||
locked_at timestamptz,
|
||||
locked_by text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_jobs_status on artifact_jobs(status, job_type);
|
||||
create index if not exists idx_versions_artifact on artifact_versions(artifact_id, created_at desc);
|
||||
"""
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.utcnow().isoformat() + "Z"
|
||||
|
||||
|
||||
def _storage_key(artifact_id: str, version_id: str, filename: str) -> str:
|
||||
return f"artifacts/{artifact_id}/versions/{version_id}/{filename}"
|
||||
|
||||
|
||||
def _hash_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _normalize_meta_json(meta: Any) -> Dict[str, Any]:
|
||||
if meta is None:
|
||||
return {}
|
||||
if isinstance(meta, dict):
|
||||
return meta
|
||||
if isinstance(meta, str):
|
||||
try:
|
||||
parsed = json.loads(meta)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
except Exception:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _format_to_mime(fmt: str) -> str:
|
||||
fmt = fmt.lower()
|
||||
if fmt == "pptx":
|
||||
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
if fmt == "pdf":
|
||||
return "application/pdf"
|
||||
if fmt == "source":
|
||||
return "application/json"
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
async def _download_bytes(url: str) -> bytes:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
resp = await client.get(url)
|
||||
if resp.status_code >= 400:
|
||||
raise HTTPException(status_code=502, detail=f"Failed to download url: {resp.status_code}")
|
||||
return resp.content
|
||||
|
||||
|
||||
async def _ensure_minio() -> None:
|
||||
global minio_client
|
||||
minio_client = Minio(
|
||||
MINIO_ENDPOINT,
|
||||
access_key=MINIO_ACCESS_KEY,
|
||||
secret_key=MINIO_SECRET_KEY,
|
||||
secure=MINIO_SECURE,
|
||||
)
|
||||
if not minio_client.bucket_exists(MINIO_BUCKET):
|
||||
minio_client.make_bucket(MINIO_BUCKET)
|
||||
|
||||
|
||||
async def _ensure_nats() -> None:
|
||||
global nats_client
|
||||
nats_client = NATS()
|
||||
await nats_client.connect(servers=[NATS_URL])
|
||||
|
||||
|
||||
async def _ensure_db() -> None:
|
||||
global pool
|
||||
pool = await asyncpg.create_pool(
|
||||
host=POSTGRES_HOST,
|
||||
port=POSTGRES_PORT,
|
||||
user=POSTGRES_USER,
|
||||
password=POSTGRES_PASSWORD,
|
||||
database=POSTGRES_DB,
|
||||
min_size=1,
|
||||
max_size=5,
|
||||
)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(SQL_CREATE)
|
||||
await conn.execute(
|
||||
"alter table artifact_jobs add column if not exists meta_json jsonb default '{}'::jsonb"
|
||||
)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup() -> None:
|
||||
await _ensure_db()
|
||||
await _ensure_minio()
|
||||
await _ensure_nats()
|
||||
logger.info("Artifact Registry started")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown() -> None:
|
||||
if nats_client:
|
||||
await nats_client.drain()
|
||||
if pool:
|
||||
await pool.close()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
return {"service": "artifact-registry", "version": APP_VERSION}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> Dict[str, Any]:
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
@app.post("/presentations/render", response_model=PresentationRenderResponse)
|
||||
async def presentations_render(req: PresentationRenderRequest) -> PresentationRenderResponse:
|
||||
if not req.slides:
|
||||
raise HTTPException(status_code=400, detail="Slides list is empty")
|
||||
|
||||
artifact_id = f"art_{uuid.uuid4().hex}"
|
||||
version_id = f"ver_{uuid.uuid4().hex}"
|
||||
job_id = f"job_{uuid.uuid4().hex}"
|
||||
|
||||
slidespec = {
|
||||
"schema": "slidespec.v1",
|
||||
"artifact_id": artifact_id,
|
||||
"title": req.title,
|
||||
"brand_id": req.brand_id,
|
||||
"theme_id": req.theme_id or "default-v1",
|
||||
"slides": [{"type": "title", "title": req.title}],
|
||||
"meta": {"lang": "uk"},
|
||||
}
|
||||
for item in req.slides:
|
||||
if item.strip() and item.strip() != req.title:
|
||||
slidespec["slides"].append({"type": "section", "title": item})
|
||||
|
||||
payload = json.dumps(slidespec, ensure_ascii=False).encode("utf-8")
|
||||
sha256 = _hash_bytes(payload)
|
||||
storage_key = _storage_key(artifact_id, version_id, "slidespec.json")
|
||||
|
||||
if not minio_client:
|
||||
raise HTTPException(status_code=500, detail="MinIO not available")
|
||||
|
||||
try:
|
||||
minio_client.put_object(
|
||||
MINIO_BUCKET,
|
||||
storage_key,
|
||||
data=payload,
|
||||
length=len(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
except S3Error as e:
|
||||
raise HTTPException(status_code=502, detail=f"MinIO error: {e}")
|
||||
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifacts (id, type, title, brand_id, project_id, acl_ref, created_by)
|
||||
values ($1, 'presentation', $2, $3, $4, $5, 'gateway')
|
||||
""",
|
||||
artifact_id,
|
||||
req.title,
|
||||
req.brand_id,
|
||||
req.project_id,
|
||||
req.acl_ref,
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifact_versions
|
||||
(id, artifact_id, label, sha256, mime, size_bytes, storage_key, meta_json)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
""",
|
||||
version_id,
|
||||
artifact_id,
|
||||
req.version_label or "source",
|
||||
sha256,
|
||||
"application/json",
|
||||
len(payload),
|
||||
storage_key,
|
||||
json.dumps({"theme_id": req.theme_id or "default-v1"}),
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifact_jobs (id, artifact_id, input_version_id, job_type, status)
|
||||
values ($1, $2, $3, 'render_pptx', 'queued')
|
||||
""",
|
||||
job_id,
|
||||
artifact_id,
|
||||
version_id,
|
||||
)
|
||||
|
||||
if nats_client:
|
||||
msg = {
|
||||
"job_id": job_id,
|
||||
"artifact_id": artifact_id,
|
||||
"input_version_id": version_id,
|
||||
"storage_key": storage_key,
|
||||
"theme_id": req.theme_id or "default-v1",
|
||||
"brand_id": req.brand_id,
|
||||
"acl_ref": req.acl_ref,
|
||||
"project_id": req.project_id,
|
||||
}
|
||||
await nats_client.publish("artifact.job.render_pptx.requested", json.dumps(msg).encode("utf-8"))
|
||||
|
||||
return PresentationRenderResponse(
|
||||
artifact_id=artifact_id,
|
||||
input_version_id=version_id,
|
||||
job_id=job_id,
|
||||
status_url=f"/jobs/{job_id}",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/artifacts", response_model=ArtifactCreateResponse)
|
||||
async def create_artifact(req: ArtifactCreateRequest) -> ArtifactCreateResponse:
|
||||
if req.type not in {"presentation", "doc"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid artifact type")
|
||||
artifact_id = f"art_{uuid.uuid4().hex}"
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifacts (id, type, title, brand_id, project_id, acl_ref, created_by)
|
||||
values ($1, $2, $3, $4, $5, $6, $7)
|
||||
""",
|
||||
artifact_id,
|
||||
req.type,
|
||||
req.title,
|
||||
req.brand_id,
|
||||
req.project_id,
|
||||
req.acl_ref,
|
||||
req.created_by,
|
||||
)
|
||||
return ArtifactCreateResponse(artifact_id=artifact_id)
|
||||
|
||||
|
||||
@app.post("/artifacts/{artifact_id}/versions/from_url", response_model=ArtifactVersionResponse)
|
||||
async def add_version_from_url(artifact_id: str, payload: ArtifactVersionFromUrlRequest) -> ArtifactVersionResponse:
|
||||
if not minio_client:
|
||||
raise HTTPException(status_code=500, detail="MinIO not available")
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
|
||||
version_id = f"ver_{uuid.uuid4().hex}"
|
||||
content = await _download_bytes(payload.url)
|
||||
sha256 = _hash_bytes(content)
|
||||
storage_key = _storage_key(artifact_id, version_id, "source.bin")
|
||||
|
||||
try:
|
||||
minio_client.put_object(
|
||||
MINIO_BUCKET,
|
||||
storage_key,
|
||||
data=BytesIO(content),
|
||||
length=len(content),
|
||||
content_type=payload.mime,
|
||||
)
|
||||
except S3Error as e:
|
||||
raise HTTPException(status_code=502, detail=f"MinIO error: {e}")
|
||||
|
||||
meta_json = _normalize_meta_json(payload.meta_json)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifact_versions
|
||||
(id, artifact_id, label, sha256, mime, size_bytes, storage_key, meta_json)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
""",
|
||||
version_id,
|
||||
artifact_id,
|
||||
payload.label or "source",
|
||||
sha256,
|
||||
payload.mime,
|
||||
len(content),
|
||||
storage_key,
|
||||
json.dumps(meta_json),
|
||||
)
|
||||
|
||||
return ArtifactVersionResponse(
|
||||
version_id=version_id,
|
||||
storage_key=storage_key,
|
||||
sha256=sha256,
|
||||
size_bytes=len(content),
|
||||
)
|
||||
|
||||
|
||||
@app.post("/artifacts/{artifact_id}/versions", response_model=ArtifactVersionResponse)
|
||||
async def add_version(artifact_id: str, payload: ArtifactVersionCreateRequest) -> ArtifactVersionResponse:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
version_id = f"ver_{uuid.uuid4().hex}"
|
||||
meta_json = _normalize_meta_json(payload.meta_json)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifact_versions
|
||||
(id, artifact_id, label, sha256, mime, size_bytes, storage_key, meta_json)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
""",
|
||||
version_id,
|
||||
artifact_id,
|
||||
payload.label or "source",
|
||||
payload.sha256,
|
||||
payload.mime,
|
||||
payload.size_bytes,
|
||||
payload.storage_key,
|
||||
json.dumps(meta_json),
|
||||
)
|
||||
return ArtifactVersionResponse(
|
||||
version_id=version_id,
|
||||
storage_key=payload.storage_key,
|
||||
sha256=payload.sha256,
|
||||
size_bytes=payload.size_bytes,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/artifacts/{artifact_id}/jobs", response_model=ArtifactJobResponse)
|
||||
async def create_job(artifact_id: str, payload: ArtifactJobRequest) -> ArtifactJobResponse:
|
||||
if payload.job_type not in {"render_pptx", "render_pdf", "index_doc"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid job type")
|
||||
job_id = f"job_{uuid.uuid4().hex}"
|
||||
input_version_id = payload.input_version_id
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
async with pool.acquire() as conn:
|
||||
artifact = await conn.fetchrow("select * from artifacts where id=$1", artifact_id)
|
||||
if not artifact:
|
||||
raise HTTPException(status_code=404, detail="Artifact not found")
|
||||
if not input_version_id:
|
||||
if payload.job_type == "index_doc":
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
select id from artifact_versions
|
||||
where artifact_id=$1 and label='source'
|
||||
order by created_at desc limit 1
|
||||
""",
|
||||
artifact_id,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=400, detail="Source version not found")
|
||||
input_version_id = row["id"]
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="input_version_id is required")
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifact_jobs (id, artifact_id, input_version_id, job_type, status, meta_json)
|
||||
values ($1, $2, $3, $4, 'queued', $5)
|
||||
""",
|
||||
job_id,
|
||||
artifact_id,
|
||||
input_version_id,
|
||||
payload.job_type,
|
||||
json.dumps({"force": bool(payload.force)} if payload.force else {}),
|
||||
)
|
||||
|
||||
if nats_client:
|
||||
subject = f"artifact.job.{payload.job_type}.requested"
|
||||
msg = {
|
||||
"job_id": job_id,
|
||||
"artifact_id": artifact_id,
|
||||
"input_version_id": input_version_id,
|
||||
"acl_ref": artifact.get("acl_ref"),
|
||||
"brand_id": artifact.get("brand_id"),
|
||||
"project_id": artifact.get("project_id"),
|
||||
"force": bool(payload.force),
|
||||
}
|
||||
await nats_client.publish(subject, json.dumps(msg).encode("utf-8"))
|
||||
|
||||
return ArtifactJobResponse(job_id=job_id, status_url=f"/jobs/{job_id}")
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}")
|
||||
async def job_status(job_id: str) -> Dict[str, Any]:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("select * from artifact_jobs where id=$1", job_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
data = dict(row)
|
||||
data["meta_json"] = _normalize_meta_json(data.get("meta_json"))
|
||||
return data
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/complete")
|
||||
async def job_complete(job_id: str, payload: JobCompleteRequest) -> Dict[str, Any]:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
output_version_id = f"ver_{uuid.uuid4().hex}"
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
job = await conn.fetchrow("select * from artifact_jobs where id=$1", job_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
await conn.execute(
|
||||
"""
|
||||
insert into artifact_versions
|
||||
(id, artifact_id, label, sha256, mime, size_bytes, storage_key)
|
||||
values ($1, $2, $3, $4, $5, $6, $7)
|
||||
""",
|
||||
output_version_id,
|
||||
job["artifact_id"],
|
||||
payload.label or "pptx",
|
||||
payload.sha256,
|
||||
payload.mime,
|
||||
payload.size_bytes,
|
||||
payload.output_storage_key,
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
update artifact_jobs
|
||||
set status='done', output_version_id=$1, updated_at=now()
|
||||
where id=$2
|
||||
""",
|
||||
output_version_id,
|
||||
job_id,
|
||||
)
|
||||
return {"status": "done", "output_version_id": output_version_id}
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/done")
|
||||
async def job_done(job_id: str, payload: JobDoneRequest) -> Dict[str, Any]:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
meta_json = _normalize_meta_json(payload.meta_json)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
update artifact_jobs
|
||||
set status='done', updated_at=now(), meta_json=$2
|
||||
where id=$1
|
||||
""",
|
||||
job_id,
|
||||
json.dumps(meta_json),
|
||||
)
|
||||
return {"status": "done"}
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/fail")
|
||||
async def job_fail(job_id: str, payload: JobFailRequest) -> Dict[str, Any]:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
update artifact_jobs
|
||||
set status='failed', error_text=$1, updated_at=now()
|
||||
where id=$2
|
||||
""",
|
||||
payload.error_text[:1000],
|
||||
job_id,
|
||||
)
|
||||
return {"status": "failed"}
|
||||
|
||||
|
||||
@app.get("/artifacts/{artifact_id}")
|
||||
async def get_artifact(artifact_id: str) -> Dict[str, Any]:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("select * from artifacts where id=$1", artifact_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Artifact not found")
|
||||
return dict(row)
|
||||
|
||||
|
||||
@app.get("/artifacts/{artifact_id}/versions")
|
||||
async def get_versions(artifact_id: str) -> Dict[str, Any]:
|
||||
if not pool:
|
||||
raise HTTPException(status_code=500, detail="DB not available")
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"select * from artifact_versions where artifact_id=$1 order by created_at desc",
|
||||
artifact_id,
|
||||
)
|
||||
items = []
|
||||
for r in rows:
|
||||
data = dict(r)
|
||||
data["meta_json"] = _normalize_meta_json(data.get("meta_json"))
|
||||
items.append(data)
|
||||
return {"items": items}
|
||||
|
||||
|
||||
@app.get("/artifacts/{artifact_id}/download")
|
||||
async def download_artifact(artifact_id: str, format: str = Query("pptx")) -> Dict[str, Any]:
|
||||
if not pool or not minio_client:
|
||||
raise HTTPException(status_code=500, detail="Service not available")
|
||||
|
||||
mime = _format_to_mime(format)
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
select * from artifact_versions
|
||||
where artifact_id=$1 and mime=$2
|
||||
order by created_at desc limit 1
|
||||
""",
|
||||
artifact_id,
|
||||
mime,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Version not found")
|
||||
try:
|
||||
url = minio_client.presigned_get_object(MINIO_BUCKET, row["storage_key"], expires=1800)
|
||||
except S3Error as e:
|
||||
raise HTTPException(status_code=502, detail=f"MinIO error: {e}")
|
||||
return {"url": url, "storage_key": row["storage_key"], "mime": row["mime"]}
|
||||
Reference in New Issue
Block a user