Files
shaguabijia-app-server/app/api/v1/compare.py
T
marco e4588303fb feat(compare): 加 /api/v1/intent/step 透传到 pricebot(淘宝多帧意图识别)
配套 pricebot-backend 的 /api/intent/step(淘宝源 Phase 1 多帧意图识别: 展开 + 滚动
采集 → LLM 提取)。沿用 _passthrough 壳: 不鉴权、原样转发, 客户端循环调用直到 done
(done 帧顶层带 result + calibration)。目前仅淘宝源走这条, 其它源走单次 /intent/recognize。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:40:19 +08:00

96 lines
3.6 KiB
Python

"""外卖比价业务透传端点。
把客户端的 POST /api/v1/intent/recognize 和 /api/v1/price/step 请求原样转发给
pricebot-backend 的 /api/intent/recognize 和 /api/price/step。MVP 阶段**不鉴权**
(同 coupon/step,见 docs/待办与技术债.md P1:device_id 透传区分设备,待补 JWT +
device_id↔user_id 绑定后才能做用户级画像)。
- Phase 1 /intent/recognize:从源平台(淘宝闪购/美团/京东外卖)购物车页识别店名+菜品+
价格,一次性。
- Phase 2 /price/step:多轮循环,在目标平台复现订单读到手价,直到 done。
真正的目标驱动比价逻辑在 pricebot-backend(GoalEngine,另一个 repo),本接口只是透传壳。
电商(ecom)那两个端点 food MVP 暂不需要,以后放开 scene 时再加 /ecom/intent/recognize
和 /ecom/step 两行即可。
pricebot 协议文档:
pricebot-backend/docs/main/02_api_protocol.md
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from fastapi import APIRouter, HTTPException, Request, status
from app.core.config import settings
logger = logging.getLogger("shagua.compare")
router = APIRouter(prefix="/api/v1", tags=["compare"])
async def _passthrough(request: Request, upstream_path: str) -> dict[str, Any]:
"""把请求体原样转发给 pricebot-backend 的 {upstream_path}
跟 coupon_step 同款透传壳:不鉴权、不做 schema 校验,仅读 device_id/trace_id/step
打日志。比价单帧是大上下文 / 逐帧 LLM,超时用 PRICEBOT_COMPARE_TIMEOUT_SEC(60s,
比领券的 30s 长)。
"""
try:
body = await request.json()
except Exception as e:
raise HTTPException(status_code=400, detail=f"invalid json body: {e}") from e
url = f"{settings.PRICEBOT_BASE_URL.rstrip('/')}{upstream_path}"
timeout = settings.PRICEBOT_COMPARE_TIMEOUT_SEC
logger.info(
"compare %s device_id=%s trace_id=%s step=%s",
upstream_path,
body.get("device_id"),
body.get("trace_id"),
body.get("step"),
)
try:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(url, json=body)
except httpx.RequestError as e:
logger.error("[pricebot] request failed: %s", e)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"pricebot upstream unreachable: {e}",
) from e
if resp.status_code >= 500:
logger.error(
"[pricebot] 5xx status=%d body=%s",
resp.status_code,
resp.text[:500],
)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"pricebot upstream returned {resp.status_code}",
)
return resp.json()
@router.post("/intent/recognize", summary="外卖比价 Phase 1 意图识别 (透传到 pricebot)")
async def intent_recognize(request: Request) -> dict[str, Any]:
return await _passthrough(request, "/api/intent/recognize")
@router.post("/intent/step", summary="外卖比价 Phase 1 多帧意图识别 (透传到 pricebot, 仅淘宝源)")
async def intent_step(request: Request) -> dict[str, Any]:
# 多帧版意图识别(展开+滚动采集→提取): 循环调用直到 done(done 帧顶层带
# result+calibration)。目前仅淘宝源走这条, 其它源走上面单次 /intent/recognize。
return await _passthrough(request, "/api/intent/step")
@router.post("/price/step", summary="外卖比价 Phase 2 步进 (透传到 pricebot)")
async def price_step(request: Request) -> dict[str, Any]:
return await _passthrough(request, "/api/price/step")