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