klg-asutk-app/backend/app/api/routes/legal.py
Yuriy 0a19a03b6e fix: seed data, API 500 errors, security hardening
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 21:35:22 +03:00

347 lines
11 KiB
Python

"""
API маршруты для системы юридических документов:
- юрисдикции, документы, перекрёстные ссылки, правовые комментарии, судебная практика
- запуск мультиагентного ИИ-анализа и подготовки документов по нормам
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, require_roles
from app.db.session import get_db
from app.models import Jurisdiction, LegalDocument, CrossReference, LegalComment, JudicialPractice
from app.schemas.legal import (
JurisdictionCreate,
JurisdictionUpdate,
JurisdictionOut,
LegalDocumentCreate,
LegalDocumentUpdate,
LegalDocumentOut,
CrossReferenceCreate,
CrossReferenceOut,
LegalCommentCreate,
LegalCommentUpdate,
LegalCommentOut,
JudicialPracticeCreate,
JudicialPracticeUpdate,
JudicialPracticeOut,
AnalysisRequest,
AnalysisResponse,
)
from app.services.legal_agents import LegalAnalysisOrchestrator
router = APIRouter(prefix="/legal", tags=["legal"])
# --- Jurisdictions ---
@router.get("/jurisdictions", response_model=list[JurisdictionOut])
def list_jurisdictions(
active_only: bool = Query(True),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(Jurisdiction)
if active_only:
q = q.filter(Jurisdiction.is_active.is_(True))
return q.order_by(Jurisdiction.code).all()
@router.post("/jurisdictions", response_model=JurisdictionOut, status_code=status.HTTP_201_CREATED)
def create_jurisdiction(
payload: JurisdictionCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin")),
):
j = Jurisdiction(**payload.model_dump())
db.add(j)
db.commit()
db.refresh(j)
return j
@router.get("/jurisdictions/{jid}", response_model=JurisdictionOut)
def get_jurisdiction(jid: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
j = db.get(Jurisdiction, jid)
if not j:
raise HTTPException(status_code=404, detail="Jurisdiction not found")
return j
@router.patch("/jurisdictions/{jid}", response_model=JurisdictionOut)
def update_jurisdiction(
jid: str,
payload: JurisdictionUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin")),
):
j = db.get(Jurisdiction, jid)
if not j:
raise HTTPException(status_code=404, detail="Jurisdiction not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(j, k, v)
db.commit()
db.refresh(j)
return j
# --- Legal Documents ---
@router.get("/documents", response_model=list[LegalDocumentOut])
def list_legal_documents(
jurisdiction_id: str | None = Query(None),
document_type: str | None = Query(None),
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(LegalDocument)
if jurisdiction_id:
q = q.filter(LegalDocument.jurisdiction_id == jurisdiction_id)
if document_type:
q = q.filter(LegalDocument.document_type == document_type)
return q.order_by(LegalDocument.created_at.desc()).limit(limit).all()
@router.post("/documents", response_model=LegalDocumentOut, status_code=status.HTTP_201_CREATED)
def create_legal_document(
payload: LegalDocumentCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
d = LegalDocument(**payload.model_dump())
db.add(d)
db.commit()
db.refresh(d)
return d
@router.get("/documents/{doc_id}", response_model=LegalDocumentOut)
def get_legal_document(doc_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
d = db.get(LegalDocument, doc_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
return d
@router.patch("/documents/{doc_id}", response_model=LegalDocumentOut)
def update_legal_document(
doc_id: str,
payload: LegalDocumentUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
d = db.get(LegalDocument, doc_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(d, k, v)
db.commit()
db.refresh(d)
return d
@router.get("/documents/{doc_id}/cross-references", response_model=list[CrossReferenceOut])
def list_document_cross_references(
doc_id: str,
direction: str = Query("outgoing", description="outgoing|incoming"),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(CrossReference)
if direction == "incoming":
q = q.filter(CrossReference.target_document_id == doc_id)
else:
q = q.filter(CrossReference.source_document_id == doc_id)
return q.all()
# --- Cross References (ручное добавление) ---
@router.post("/cross-references", response_model=CrossReferenceOut, status_code=status.HTTP_201_CREATED)
def create_cross_reference(
payload: CrossReferenceCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
ref = CrossReference(**payload.model_dump())
db.add(ref)
db.commit()
db.refresh(ref)
return ref
# --- Legal Comments ---
@router.get("/comments", response_model=list[LegalCommentOut])
def list_legal_comments(
jurisdiction_id: str | None = Query(None),
document_id: str | None = Query(None),
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(LegalComment)
if jurisdiction_id:
q = q.filter(LegalComment.jurisdiction_id == jurisdiction_id)
if document_id:
q = q.filter(LegalComment.document_id == document_id)
return q.order_by(LegalComment.created_at.desc()).limit(limit).all()
@router.post("/comments", response_model=LegalCommentOut, status_code=status.HTTP_201_CREATED)
def create_legal_comment(
payload: LegalCommentCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
c = LegalComment(**payload.model_dump())
db.add(c)
db.commit()
db.refresh(c)
return c
@router.patch("/comments/{cid}", response_model=LegalCommentOut)
def update_legal_comment(
cid: str,
payload: LegalCommentUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
c = db.get(LegalComment, cid)
if not c:
raise HTTPException(status_code=404, detail="Comment not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(c, k, v)
db.commit()
db.refresh(c)
return c
# --- Judicial Practice ---
@router.get("/judicial-practices", response_model=list[JudicialPracticeOut])
def list_judicial_practices(
jurisdiction_id: str | None = Query(None),
document_id: str | None = Query(None),
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(JudicialPractice)
if jurisdiction_id:
q = q.filter(JudicialPractice.jurisdiction_id == jurisdiction_id)
if document_id:
q = q.filter(JudicialPractice.document_id == document_id)
# nulls_last портировано под SQLite < 3.30: (col IS NULL) ASC — не-NULL первыми
return q.order_by(
JudicialPractice.decision_date.is_(None),
JudicialPractice.decision_date.desc(),
JudicialPractice.created_at.desc(),
).limit(limit).all()
@router.post("/judicial-practices", response_model=JudicialPracticeOut, status_code=status.HTTP_201_CREATED)
def create_judicial_practice(
payload: JudicialPracticeCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
p = JudicialPractice(**payload.model_dump())
db.add(p)
db.commit()
db.refresh(p)
return p
@router.patch("/judicial-practices/{pid}", response_model=JudicialPracticeOut)
def update_judicial_practice(
pid: str,
payload: JudicialPracticeUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
p = db.get(JudicialPractice, pid)
if not p:
raise HTTPException(status_code=404, detail="Judicial practice not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(p, k, v)
db.commit()
db.refresh(p)
return p
# --- ИИ-анализ (мультиагентный) ---
@router.post("/analyze", response_model=AnalysisResponse)
def analyze_document(
payload: AnalysisRequest,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
"""
Запуск мультиагентного анализа: классификация, соответствие нормам, перекрёстные ссылки,
подбор правовых комментариев и судебной практики, рекомендации по оформлению.
"""
orch = LegalAnalysisOrchestrator(db=db)
out = orch.run(
document_id=payload.document_id,
jurisdiction_id=payload.jurisdiction_id,
title=payload.title,
content=payload.content,
skip_agents=payload.skip_agents,
save_cross_references=payload.save_cross_references,
)
# Опционально: обновить документ в БД, если document_id передан
if payload.document_id and out.get("document_type"):
d = db.get(LegalDocument, payload.document_id)
if d:
d.document_type = out["document_type"]
d.analysis_json = out.get("analysis_json")
d.compliance_notes = out.get("compliance_notes")
db.commit()
return AnalysisResponse(
document_type=out["document_type"],
analysis_json=out.get("analysis_json"),
compliance_notes=out.get("compliance_notes"),
results=out.get("results", {}),
)
@router.post("/documents/{doc_id}/analyze", response_model=AnalysisResponse)
def analyze_existing_document(
doc_id: str,
skip_agents: list[str] | None = Query(None),
save_cross_references: bool = Query(True),
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
"""Запуск ИИ-анализа для уже существующего документа по id."""
d = db.get(LegalDocument, doc_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
orch = LegalAnalysisOrchestrator(db=db)
out = orch.run(
document_id=doc_id,
jurisdiction_id=d.jurisdiction_id,
title=d.title,
content=d.content,
existing_document_type=d.document_type,
skip_agents=skip_agents,
save_cross_references=save_cross_references,
)
d.document_type = out["document_type"]
d.analysis_json = out.get("analysis_json")
d.compliance_notes = out.get("compliance_notes")
db.commit()
db.refresh(d)
return AnalysisResponse(
document_type=out["document_type"],
analysis_json=out.get("analysis_json"),
compliance_notes=out.get("compliance_notes"),
results=out.get("results", {}),
)