157 lines
4.9 KiB
Python
157 lines
4.9 KiB
Python
"""知识摘要 API — 管理会计OS持久记忆
|
|
|
|
提供:
|
|
- 查询最近摘要列表
|
|
- 查询单个摘要详情
|
|
- 手动触发各层级摘要生成
|
|
- 查询未摘要的事件
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, desc
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
|
|
from app.database import get_db
|
|
from app.auth_middleware import require_role
|
|
from app.models import KnowledgeEvent, KnowledgeSummary
|
|
from app.services.knowledge_service import (
|
|
generate_summary_sync,
|
|
generate_daily_sync,
|
|
generate_weekly_sync,
|
|
generate_monthly_sync,
|
|
get_last_summary,
|
|
extract_events,
|
|
)
|
|
import logging
|
|
|
|
logger = logging.getLogger("cma.knowledge_api")
|
|
|
|
router = APIRouter(prefix="/api/cma/knowledge", tags=["知识摘要"],
|
|
dependencies=[Depends(require_role("ceo", "finance", "business", "it"))],
|
|
)
|
|
|
|
|
|
def summary_to_dict(s: KnowledgeSummary) -> dict:
|
|
return {
|
|
"id": s.id,
|
|
"level": s.level,
|
|
"period_key": s.period_key,
|
|
"title": s.title,
|
|
"content": s.content,
|
|
"kpi_changes": s.kpi_changes,
|
|
"decision_points": s.decision_points,
|
|
"key_metrics": s.key_metrics,
|
|
"prev_summary_id": s.prev_summary_id,
|
|
"model": s.model,
|
|
"is_stale": s.is_stale,
|
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
|
}
|
|
|
|
|
|
# ── 查询 ──
|
|
|
|
|
|
@router.get("/summaries")
|
|
def list_summaries(
|
|
level: Optional[str] = None,
|
|
limit: int = 20,
|
|
offset: int = 0,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""获取摘要列表,按层级筛选,按时间倒序"""
|
|
query = db.query(KnowledgeSummary)
|
|
if level:
|
|
query = query.filter(KnowledgeSummary.level == level)
|
|
query = query.order_by(desc(KnowledgeSummary.id)).offset(offset).limit(limit)
|
|
total = db.query(func.count(KnowledgeSummary.id)).select_from(KnowledgeSummary)
|
|
if level:
|
|
total = total.filter(KnowledgeSummary.level == level)
|
|
total = total.scalar()
|
|
return {
|
|
"total": total,
|
|
"items": [summary_to_dict(s) for s in query.all()],
|
|
}
|
|
|
|
|
|
@router.get("/summaries/latest")
|
|
def latest_summary(
|
|
level: str = Query("daily", description="层级: daily/weekly/monthly/cumulative"),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""获取指定层级的最新摘要"""
|
|
s = get_last_summary(db, level)
|
|
if not s:
|
|
return {"detail": f"没有{level}层级的摘要"}, 404
|
|
return summary_to_dict(s)
|
|
|
|
|
|
@router.get("/summaries/{summary_id}")
|
|
def get_summary(summary_id: int, db: Session = Depends(get_db)):
|
|
"""获取单条摘要详情"""
|
|
s = db.query(KnowledgeSummary).filter(KnowledgeSummary.id == summary_id).first()
|
|
if not s:
|
|
raise HTTPException(status_code=404, detail="摘要不存在")
|
|
return summary_to_dict(s)
|
|
|
|
|
|
# ── 事件查询 ──
|
|
|
|
|
|
@router.get("/events")
|
|
def list_events(
|
|
since: Optional[str] = None,
|
|
until: Optional[str] = None,
|
|
limit: int = 50,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""查询未摘要的原始事件
|
|
|
|
如果不传时间,默认返回最近7天的操作记录和预警。
|
|
"""
|
|
try:
|
|
dt_since = datetime.fromisoformat(since) if since else datetime.utcnow() - timedelta(days=7)
|
|
dt_until = datetime.fromisoformat(until) if until else datetime.utcnow()
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="时间格式错误,请使用 ISO 格式如 2026-06-01T00:00:00")
|
|
|
|
events = extract_events(db, dt_since, dt_until)
|
|
return {"since": dt_since.isoformat(), "until": dt_until.isoformat(), "total": len(events), "events": events[:limit]}
|
|
|
|
|
|
# ── 手动触发 ──
|
|
|
|
|
|
@router.post("/generate/daily")
|
|
def trigger_daily_summary(db: Session = Depends(get_db)):
|
|
"""手动触发每日摘要生成"""
|
|
try:
|
|
result = generate_daily_sync(db)
|
|
return {"message": "每日摘要已生成", "summary": result}
|
|
except Exception as e:
|
|
logger.exception("每日摘要生成失败")
|
|
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
|
|
|
|
|
@router.post("/generate/weekly")
|
|
def trigger_weekly_summary(db: Session = Depends(get_db)):
|
|
"""手动触发周度摘要生成"""
|
|
try:
|
|
result = generate_weekly_sync(db)
|
|
return {"message": "周度摘要已生成", "summary": result}
|
|
except Exception as e:
|
|
logger.exception("周度摘要生成失败")
|
|
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
|
|
|
|
|
@router.post("/generate/monthly")
|
|
def trigger_monthly_summary(db: Session = Depends(get_db)):
|
|
"""手动触发月度摘要生成"""
|
|
try:
|
|
result = generate_monthly_sync(db)
|
|
return {"message": "月度摘要已生成", "summary": result}
|
|
except Exception as e:
|
|
logger.exception("月度摘要生成失败")
|
|
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|