refactor(d3): ARCH-051 — 14 dict-body-Routes auf Pydantic-Schemas umgestellt (entity_permissions bulk ×2, guests invite, users menu-order, system_settings backup-config+dsar, knowledge ×3, self_improvement ×5); DSAR-Export F821-Bug behoben (datetime/timezone undefined → NameError beim GDPR-Export), Zeitstempel auf datetime.now(UTC); Validierung jetzt im Schema statt in Routen
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
@@ -28,6 +29,46 @@ from app.plugins.builtins.self_improvement.services import (
|
||||
router = APIRouter(prefix="/api/v1/improvement", tags=["improvement"])
|
||||
|
||||
|
||||
class CollectSignalsRequest(BaseModel):
|
||||
"""Collect improvement signals from existing system data."""
|
||||
|
||||
since: datetime | None = None
|
||||
limit: int = Field(100, ge=1, le=500)
|
||||
|
||||
|
||||
class DetectPatternsRequest(BaseModel):
|
||||
"""Detect recurring patterns from collected signals."""
|
||||
|
||||
min_occurrences: int = Field(2, ge=2)
|
||||
|
||||
|
||||
class CreateProposalRequest(BaseModel):
|
||||
"""Create a new improvement proposal."""
|
||||
|
||||
pattern_id: uuid.UUID | None = None
|
||||
title: str = Field(..., min_length=1)
|
||||
description: str = ""
|
||||
target_type: str = Field(..., min_length=1)
|
||||
target_ref_id: uuid.UUID | None = None
|
||||
target_name: str | None = None
|
||||
proposed_config: dict[str, Any] = Field(default_factory=dict)
|
||||
rationale: str = ""
|
||||
expected_benefit: str = ""
|
||||
risk_assessment: str = ""
|
||||
|
||||
|
||||
class RequestApprovalRequest(BaseModel):
|
||||
"""Request human approval for a proposal."""
|
||||
|
||||
approver_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class RollbackProposalRequest(BaseModel):
|
||||
"""Rollback an active proposal."""
|
||||
|
||||
reason: str = ""
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# J-SIGNAL: Signal Collection
|
||||
# NOTE: /signals/collect must be defined before /signals to avoid route conflicts
|
||||
@@ -35,21 +76,13 @@ router = APIRouter(prefix="/api/v1/improvement", tags=["improvement"])
|
||||
|
||||
@router.post("/signals/collect")
|
||||
async def collect(
|
||||
body: dict,
|
||||
body: CollectSignalsRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("automation:read")),
|
||||
):
|
||||
"""Collect improvement signals from existing system data."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
since_str = body.get("since")
|
||||
since = None
|
||||
if since_str:
|
||||
try:
|
||||
since = datetime.fromisoformat(since_str)
|
||||
except ValueError:
|
||||
raise HTTPException(400, detail={"detail": "Invalid since format", "code": "invalid_date"}) from None
|
||||
limit = min(body.get("limit", 100), 500)
|
||||
result = await collect_signals(db=db, tenant_id=tenant_id, since=since, limit=limit)
|
||||
result = await collect_signals(db=db, tenant_id=tenant_id, since=body.since, limit=body.limit)
|
||||
await db.commit()
|
||||
return result
|
||||
|
||||
@@ -73,14 +106,13 @@ async def signals(
|
||||
|
||||
@router.post("/patterns/detect")
|
||||
async def detect(
|
||||
body: dict,
|
||||
body: DetectPatternsRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("automation:read")),
|
||||
):
|
||||
"""Detect recurring patterns from collected signals."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
min_occurrences = body.get("min_occurrences", 2)
|
||||
result = await detect_patterns(db=db, tenant_id=tenant_id, min_occurrences=min_occurrences)
|
||||
result = await detect_patterns(db=db, tenant_id=tenant_id, min_occurrences=body.min_occurrences)
|
||||
await db.commit()
|
||||
return result
|
||||
|
||||
@@ -103,7 +135,7 @@ async def patterns(
|
||||
|
||||
@router.post("/proposals")
|
||||
async def create(
|
||||
body: dict,
|
||||
body: CreateProposalRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("automation:write")),
|
||||
):
|
||||
@@ -111,35 +143,18 @@ async def create(
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"]) if current_user.get("user_id") else None
|
||||
|
||||
pattern_id = None
|
||||
if body.get("pattern_id"):
|
||||
try:
|
||||
pattern_id = uuid.UUID(body["pattern_id"])
|
||||
except ValueError:
|
||||
raise HTTPException(400, detail={"detail": "Invalid pattern_id", "code": "invalid_id"}) from None
|
||||
|
||||
target_ref_id = None
|
||||
if body.get("target_ref_id"):
|
||||
try:
|
||||
target_ref_id = uuid.UUID(body["target_ref_id"])
|
||||
except ValueError:
|
||||
raise HTTPException(400, detail={"detail": "Invalid target_ref_id", "code": "invalid_id"}) from None
|
||||
|
||||
if not body.get("title") or not body.get("target_type"):
|
||||
raise HTTPException(400, detail={"detail": "title and target_type required", "code": "missing_fields"})
|
||||
|
||||
proposal = await create_proposal(
|
||||
db=db, tenant_id=tenant_id,
|
||||
pattern_id=pattern_id,
|
||||
title=body["title"],
|
||||
description=body.get("description", ""),
|
||||
target_type=body["target_type"],
|
||||
target_ref_id=target_ref_id,
|
||||
target_name=body.get("target_name"),
|
||||
proposed_config=body.get("proposed_config", {}),
|
||||
rationale=body.get("rationale", ""),
|
||||
expected_benefit=body.get("expected_benefit", ""),
|
||||
risk_assessment=body.get("risk_assessment", ""),
|
||||
pattern_id=body.pattern_id,
|
||||
title=body.title,
|
||||
description=body.description,
|
||||
target_type=body.target_type,
|
||||
target_ref_id=body.target_ref_id,
|
||||
target_name=body.target_name,
|
||||
proposed_config=body.proposed_config,
|
||||
rationale=body.rationale,
|
||||
expected_benefit=body.expected_benefit,
|
||||
risk_assessment=body.risk_assessment,
|
||||
user_id=user_id,
|
||||
)
|
||||
await db.commit()
|
||||
@@ -207,7 +222,7 @@ async def evaluate(
|
||||
@router.post("/proposals/{proposal_id}/request-approval")
|
||||
async def req_approval(
|
||||
proposal_id: str,
|
||||
body: dict,
|
||||
body: RequestApprovalRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("automation:write")),
|
||||
):
|
||||
@@ -218,13 +233,7 @@ async def req_approval(
|
||||
pid = uuid.UUID(proposal_id)
|
||||
except ValueError:
|
||||
raise HTTPException(400, detail={"detail": "Invalid proposal_id", "code": "invalid_id"}) from None
|
||||
approver_id = None
|
||||
if body.get("approver_id"):
|
||||
try:
|
||||
approver_id = uuid.UUID(body["approver_id"])
|
||||
except ValueError:
|
||||
raise HTTPException(400, detail={"detail": "Invalid approver_id", "code": "invalid_id"}) from None
|
||||
result = await request_approval(db=db, tenant_id=tenant_id, proposal_id=pid, requested_by=user_id, approver_id=approver_id)
|
||||
result = await request_approval(db=db, tenant_id=tenant_id, proposal_id=pid, requested_by=user_id, approver_id=body.approver_id)
|
||||
if "error" in result:
|
||||
raise HTTPException(400, detail={"detail": result["error"], "code": "invalid_state"})
|
||||
await db.commit()
|
||||
@@ -258,7 +267,7 @@ async def activate(
|
||||
@router.post("/proposals/{proposal_id}/rollback")
|
||||
async def rollback(
|
||||
proposal_id: str,
|
||||
body: dict,
|
||||
body: RollbackProposalRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("automation:admin")),
|
||||
):
|
||||
@@ -268,8 +277,7 @@ async def rollback(
|
||||
pid = uuid.UUID(proposal_id)
|
||||
except ValueError:
|
||||
raise HTTPException(400, detail={"detail": "Invalid proposal_id", "code": "invalid_id"}) from None
|
||||
reason = body.get("reason", "")
|
||||
result = await rollback_proposal(db=db, tenant_id=tenant_id, proposal_id=pid, reason=reason)
|
||||
result = await rollback_proposal(db=db, tenant_id=tenant_id, proposal_id=pid, reason=body.reason)
|
||||
if "error" in result:
|
||||
raise HTTPException(400, detail={"detail": result["error"], "code": "invalid_state"})
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user