PMFlow/backend/app/api/signal_sources.py

259 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""食谱系统信号源 API。"""
from __future__ import annotations
import json
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.logging import get_logger
from ..database import get_db
from ..dependencies.tenant import get_tenant_id
from ..models import ProjectSignalLink, Signal
from .signal_sources_models import RecipeBatchRequest, RecipeRecognitionRequest, RecipeRecognitionResponse
from .signal_sources_utils import resolve_target_project_id
router = APIRouter(prefix="/signal-sources", tags=["Signal Sources"])
logger = get_logger(__name__)
@router.post("/recipe/webhook", response_model=RecipeRecognitionResponse, summary="食谱系统 Webhook")
async def recipe_webhook(
request: RecipeRecognitionRequest,
project_id: Optional[int] = Query(None, description="项目ID,不传则自动查找食谱系统项目"),
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
):
"""接收食谱系统的识别请求。"""
try:
target_project_id = await resolve_target_project_id(
db=db,
tenant_id=tenant_id,
project_id=project_id,
)
content_parts = [f"食物:{request.food_name}"]
if request.food_description:
content_parts.append(f"描述:{request.food_description}")
if request.calories or request.protein or request.fat or request.carbs or request.fiber:
nutrition = []
if request.calories:
nutrition.append(f"热量 {request.calories} kcal")
if request.protein:
nutrition.append(f"蛋白 {request.protein}g")
if request.fat:
nutrition.append(f"脂肪 {request.fat}g")
if request.carbs:
nutrition.append(f"碳水 {request.carbs}g")
if request.fiber:
nutrition.append(f"纤维 {request.fiber}g")
content_parts.append("营养:" + ", ".join(nutrition))
if request.category:
content_parts.append(f"分类:{request.category}")
if request.cuisine:
content_parts.append(f"菜系:{request.cuisine}")
content_parts.append(f"意图:{request.intent}")
content = "\n".join(content_parts)
intent_text = {
"analyze": "热量分析",
"save": "保存食谱",
"recommend": "推荐菜谱",
}.get(request.intent, request.intent)
title = f"🍽️ 食谱{intent_text}:{request.food_name}"
summary = f"食物:{request.food_name}"
if request.calories:
summary += f"\n热量:{request.calories} kcal"
if request.category:
summary += f"\n分类:{request.category}"
summary += f"\n意图:{intent_text}"
signal_metadata = {
"source": "recipe_system",
"intent": request.intent,
"category": request.category,
"cuisine": request.cuisine,
"nutrition": {
"calories": request.calories,
"protein": request.protein,
"fat": request.fat,
"carbs": request.carbs,
"fiber": request.fiber,
},
"image_url": request.image_url,
"user_name": request.user_name,
}
signal = Signal(
tenant_id=tenant_id,
source="recipe_system",
title=title,
content=content,
summary=summary,
signal_type="recipe_recognition",
priority="medium",
impact_score=0.5,
status="待处理",
processed=False,
strategic_analysis=json.dumps(signal_metadata, ensure_ascii=False),
)
db.add(signal)
await db.flush()
link = ProjectSignalLink(
tenant_id=tenant_id,
project_id=target_project_id,
signal_id=signal.id,
)
db.add(link)
await db.commit()
await db.refresh(signal)
return RecipeRecognitionResponse(
success=True,
signal_id=signal.id,
project_id=target_project_id,
message="信号创建成功并已关联到食谱系统项目",
details={
"signal_id": signal.id,
"project_id": target_project_id,
"title": title,
"status": signal.status,
},
)
except HTTPException:
await db.rollback()
raise
except Exception as exc:
await db.rollback()
logger.exception(
"recipe_webhook_create_signal_failed",
tenant_id=tenant_id,
project_id=project_id,
food_name=request.food_name,
error=str(exc),
)
raise HTTPException(status_code=500, detail=f"创建信号失败: {str(exc)}")
@router.get("/recipe/status/{signal_id}", summary="查询食谱信号状态")
async def get_recipe_signal_status(
signal_id: int,
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
):
"""查询食谱识别信号的处理状态"""
result = await db.execute(
select(Signal).where(
Signal.id == signal_id,
Signal.tenant_id == tenant_id,
Signal.source == "recipe_system",
)
)
signal = result.scalar_one_or_none()
if not signal:
raise HTTPException(status_code=404, detail="信号不存在")
return {
"signal_id": signal.id,
"title": signal.title,
"status": signal.status,
"processed": signal.processed,
"summary": signal.summary,
}
@router.post("/recipe/batch", response_model=RecipeRecognitionResponse, summary="批量导入食谱")
async def recipe_batch_import(
request: RecipeBatchRequest,
project_id: Optional[int] = Query(None, description="项目ID"),
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
):
"""批量导入食谱记录,每个都会创建信号"""
try:
target_project_id = await resolve_target_project_id(
db=db,
tenant_id=tenant_id,
project_id=project_id,
)
signal_ids = []
for recipe in request.recipes:
content = f"食物:{recipe.food_name}"
if recipe.calories:
content += f"\n热量:{recipe.calories} kcal"
title = f"🍽️ 食谱记录:{recipe.food_name}"
metadata = {
"source": "recipe_system",
"intent": recipe.intent,
"nutrition": {
"calories": recipe.calories,
"protein": recipe.protein,
"fat": recipe.fat,
"carbs": recipe.carbs,
},
}
signal = Signal(
tenant_id=tenant_id,
source="recipe_system",
title=title,
content=content,
summary=f"食物:{recipe.food_name}",
signal_type="recipe_recognition",
priority="low",
impact_score=0.3,
status="待处理",
processed=True,
strategic_analysis=json.dumps(metadata),
)
db.add(signal)
await db.flush()
link = ProjectSignalLink(
tenant_id=tenant_id,
project_id=target_project_id,
signal_id=signal.id,
)
db.add(link)
signal_ids.append(signal.id)
await db.commit()
return RecipeRecognitionResponse(
success=True,
signal_id=len(signal_ids),
project_id=target_project_id,
message=f"成功导入 {len(signal_ids)} 条食谱记录",
details={"signal_count": len(signal_ids)},
)
except HTTPException:
await db.rollback()
raise
except Exception as exc:
await db.rollback()
logger.exception(
"recipe_batch_import_failed",
tenant_id=tenant_id,
project_id=project_id,
recipe_count=len(request.recipes),
error=str(exc),
)
raise HTTPException(status_code=500, detail=f"批量导入失败: {str(exc)}")