fix(retrieval): list_gaps returns ORM entities; admin switches back to the domain API

T12 集成实测暴露:select(KbGap) 的结果调 .mappings(),行键是实体类,
GapSummary 按列名取值必然 AttributeError。改为 .scalars() 返回实体;
admin 的缺口看板从临时自实现切回 retrieval.list_gaps(语义单一定义)。
This commit is contained in:
xsl
2026-08-14 21:22:51 +08:00
parent fa3da930cc
commit 4764235343
2 changed files with 19 additions and 30 deletions
+12 -27
View File
@@ -3,11 +3,9 @@
数据来自检索无结果时的自动埋点(T-P2-06),看板只读:它回答「下一批内容
该生产什么」,是内容排期的输入,不是待办队列。
偏差说明(详见提交信息):协调提交提供的 ``packages.retrieval.list_gaps``
对 ``select(KbGap)`` 的结果调 ``.mappings()``,行键是实体名而非列名,
``GapSummary(row)`` 取 ``row.gap_id`` 必然 ``AttributeError``(该路径无既有
测试覆盖,真库一跑即暴露)。``packages/**`` 本任务禁改,故此处以等价查询
在 admin 包内自实现;上游修复后应切回 retrieval 的 ``list_gaps``。
数据访问走 retrieval 域的 ``list_gaps``(T12 开发时它有个 mappings 误用
的缺陷,当时在 admin 内自实现了等价查询;上游已在合并阶段修复,现切回,
缺口语义只有一处定义)。
"""
from __future__ import annotations
@@ -15,44 +13,31 @@ from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from apps.admin.deps import SessionDep, admin_auth
from apps.admin.schemas import GapOut
from packages.shared.models import KbGap
from packages.retrieval import service as retrieval_service
from packages.shared.response import ok
router = APIRouter(prefix="/kb", tags=["admin:gaps"], dependencies=[Depends(admin_auth)])
async def list_gap_rows(session: AsyncSession, *, limit: int, offset: int) -> list[KbGap]:
"""kb_gap 看板查询:hit_count 降序,同频次按最近出现降序(与 retrieval 语义一致)。"""
result = await session.execute(
select(KbGap)
.order_by(KbGap.hit_count.desc(), KbGap.last_seen_at.desc())
.limit(limit)
.offset(offset)
)
return list(result.scalars().all())
@router.get("/gaps", summary="知识缺口列表(hit_count 降序:最常被问到的排最前)")
async def list_gaps(
session: SessionDep,
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
) -> dict[str, Any]:
rows = await list_gap_rows(session, limit=limit, offset=offset)
summaries = await retrieval_service.list_gaps(session, limit=limit, offset=offset)
items = [
GapOut(
gap_id=str(row.gap_id),
query_text=row.query_text,
filter_ctx=dict(row.filter_ctx),
hit_count=row.hit_count,
first_seen_at=row.first_seen_at,
last_seen_at=row.last_seen_at,
gap_id=str(g.gap_id),
query_text=g.query_text,
filter_ctx=dict(g.filter_ctx),
hit_count=g.hit_count,
first_seen_at=g.first_seen_at,
last_seen_at=g.last_seen_at,
).model_dump()
for row in rows
for g in summaries
]
return ok({"items": items, "total": len(items), "limit": limit, "offset": offset})
+7 -3
View File
@@ -268,8 +268,12 @@ async def record_gap(
INVALIDATION_VERSION = 1
async def list_gaps(session: AsyncSession, *, limit: int = 50, offset: int = 0) -> list[RowMapping]:
"""缺口看板查询:hit_count 降序,同频次按最近出现降序。"""
async def list_gaps(session: AsyncSession, *, limit: int = 50, offset: int = 0) -> list[KbGap]:
"""缺口看板查询:hit_count 降序,同频次按最近出现降序。
返回 ORM 实体而非 mappings:单实体 select 的 mapping 行键是实体类,
按列名取值必然 AttributeError(T12 集成实测暴露,此处修正)。
"""
from sqlalchemy import select
from packages.shared.models import KbGap
@@ -281,4 +285,4 @@ async def list_gaps(session: AsyncSession, *, limit: int = 50, offset: int = 0)
.offset(offset)
)
result = await session.execute(stmt)
return list(result.mappings().all())
return list(result.scalars().all())