Files
MapMil/centers/analytics/api/app/routers/parser_profiles.py
T
gitrusprusandCursor 52185aeaa8 Add profile generate refine via manager hint and auto-preview.
When Preview leaves fields empty, managers can send a hint plus the current profile so DeepSeek revises rules; generate now returns preview and empty_fields for the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 22:25:27 +03:00

179 lines
5.9 KiB
Python

"""Admin CRUD for reusable parser profiles + generate/preview."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from contracts.heuristic_profile import HeuristicProfile
from ..database import get_db
from ..models import ParseJob, ParserProfile
from ..schemas import ParserProfileCreate, ParserProfileRead, ParserProfileUpdate
from ..services import parser_builder as builder
router = APIRouter(prefix="/admin/parser-profiles", tags=["parser-profiles"])
class GenerateRequest(BaseModel):
sample_post: str = Field(min_length=1)
hint: str | None = None
current_profile: dict[str, Any] | None = None
class GenerateResponse(BaseModel):
profile: dict[str, Any]
preview: dict[str, str]
empty_fields: list[str] = Field(default_factory=list)
class PreviewRequest(BaseModel):
sample_post: str = Field(min_length=1)
heuristic_profile: dict[str, Any] | None = None
# Accept alias used by older builder UI
profile: dict[str, Any] | None = None
class PreviewResponse(BaseModel):
fields: dict[str, str]
def _validate_heuristic_profile(raw: dict[str, Any] | None) -> dict[str, Any] | None:
if raw is None:
return None
try:
return HeuristicProfile.model_validate(raw).model_dump()
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid heuristic_profile: {exc}") from exc
def _profile_status(heuristic_profile: dict | None, explicit: str | None = None) -> str:
if explicit:
return explicit
return "ready" if heuristic_profile else "draft"
@router.get("/target-fields")
def target_fields():
return {"fields": builder.get_target_fields()}
@router.post("/generate", response_model=GenerateResponse)
async def generate_profile(payload: GenerateRequest):
if not builder.deepseek_enabled():
raise HTTPException(
status_code=503,
detail="DEEPSEEK_API_KEY is not set on ca-api. Add it to .env for parser generation.",
)
try:
profile = await builder.generate_profile(
payload.sample_post,
hint=payload.hint,
current_profile=payload.current_profile,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(
status_code=502,
detail=f"DeepSeek generate failed: {exc}",
) from exc
dumped = profile.model_dump()
preview = builder.preview_with_profile(payload.sample_post, dumped)
return GenerateResponse(
profile=dumped,
preview=preview,
empty_fields=builder.empty_preview_fields(preview),
)
@router.post("/preview", response_model=PreviewResponse)
def preview_profile(payload: PreviewRequest):
raw = payload.heuristic_profile if payload.heuristic_profile is not None else payload.profile
if raw is None:
raise HTTPException(status_code=400, detail="heuristic_profile is required")
try:
HeuristicProfile.model_validate(raw)
fields = builder.preview_with_profile(payload.sample_post, raw)
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return PreviewResponse(fields=fields)
@router.get("", response_model=list[ParserProfileRead])
def list_profiles(db: Session = Depends(get_db)):
return db.query(ParserProfile).order_by(ParserProfile.id.desc()).all()
@router.post("", response_model=ParserProfileRead, status_code=201)
def create_profile(payload: ParserProfileCreate, db: Session = Depends(get_db)):
heuristic = _validate_heuristic_profile(payload.heuristic_profile)
profile = ParserProfile(
name=payload.name.strip(),
sample_post=payload.sample_post or "",
heuristic_profile=heuristic,
status=_profile_status(heuristic, payload.status),
)
db.add(profile)
db.commit()
db.refresh(profile)
return profile
@router.get("/{profile_id}", response_model=ParserProfileRead)
def get_profile(profile_id: int, db: Session = Depends(get_db)):
profile = db.query(ParserProfile).filter(ParserProfile.id == profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
return profile
@router.patch("/{profile_id}", response_model=ParserProfileRead)
def update_profile(
profile_id: int,
payload: ParserProfileUpdate,
db: Session = Depends(get_db),
):
profile = db.query(ParserProfile).filter(ParserProfile.id == profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
if payload.name is not None:
profile.name = payload.name.strip()
if payload.sample_post is not None:
profile.sample_post = payload.sample_post
if "heuristic_profile" in payload.model_fields_set:
profile.heuristic_profile = _validate_heuristic_profile(payload.heuristic_profile)
if payload.status is not None:
profile.status = payload.status
elif "heuristic_profile" in payload.model_fields_set:
profile.status = _profile_status(profile.heuristic_profile)
db.commit()
db.refresh(profile)
return profile
@router.delete("/{profile_id}", status_code=204)
def delete_profile(profile_id: int, db: Session = Depends(get_db)):
profile = db.query(ParserProfile).filter(ParserProfile.id == profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
linked = (
db.query(ParseJob)
.filter(ParseJob.profile_id == profile_id, ParseJob.status.in_(("queued", "running")))
.count()
)
if linked:
raise HTTPException(
status_code=409,
detail="Cannot delete profile used by queued/running jobs",
)
db.delete(profile)
db.commit()