from flask import Blueprint, current_app, request from flask_jwt_extended import jwt_required from app.rag.llm_client import LLMConfigManager, ask_dify, ask_fastgpt from app.rag.local_retriever import LocalKnowledgeRetriever from app.utils.auth import admin_required, current_user from app.utils.response import ok qa_bp = Blueprint("qa", __name__) def _retriever() -> LocalKnowledgeRetriever: return LocalKnowledgeRetriever(current_app.config["LOCAL_KB_PATH"]) def _llm_config() -> dict: manager = LLMConfigManager(current_app.config["LLM_CONFIG_PATH"]) return manager.load() def _default_provider(config: dict) -> str: provider = (config.get("active_provider") or "fastgpt").strip().lower() if isinstance(config, dict) else "fastgpt" return provider if provider in {"fastgpt", "dify"} else "fastgpt" def _context_text(hits: list) -> str: chunks = [] for idx, item in enumerate(hits, start=1): chunks.append(f"[{idx}] 问: {item['question']}\n答: {item['answer']}") return "\n\n".join(chunks) def _local_answer(hits: list) -> str: if not hits: return "本地知识库暂未检索到高相关内容,建议补充问题细节。" top = hits[0] return f"根据本地知识库:{top['answer']}" def _advice_template(advice_type: str) -> str: templates = { "gain_muscle": "增肌建议:每天蛋白质建议 1.6-2.2g/kg,训练后 30 分钟内补充蛋白+碳水,优先鸡胸肉/牛肉/鸡蛋/燕麦。", "lose_fat": "减脂建议:保持 10%-20% 轻热量缺口,优先高蛋白高纤维食物,减少高糖饮料和深夜加餐。", "keto": "生酮建议:控制碳水通常小于 50g/天,优先健康脂肪和适量蛋白,并关注电解质补充。", "nutritionist": "营养师建议:结合体重、体脂、活动量和目标制定周计划,每 7 天复盘体重和围度变化。", "general": "通用建议:保证规律作息、三餐结构稳定、适量运动和充足饮水。", } return templates.get(advice_type, templates["general"]) def _ask_by_provider(provider: str, config: dict, question: str, context: str, user_id: str, username: str): if provider == "dify": return ask_dify(config, question, context=context, user=username) return ask_fastgpt(config, question, context=context, custom_uid=user_id) @qa_bp.get("/kb/search") @jwt_required(optional=True) def search_kb(): query = (request.args.get("query") or "").strip() top_k = min(max(int(request.args.get("top_k", 5) or 5), 1), 10) retriever = _retriever() hits = retriever.search(query, top_k=top_k) return ok({"items": hits}) @qa_bp.post("/ask") @jwt_required() def ask_nutrition_qa(): payload = request.get_json(silent=True) or {} question = (payload.get("question") or "").strip() if not question: return ok({"answer": "请输入问题", "items": []}, "empty_question") mode = (payload.get("mode") or "auto").strip().lower() llm_cfg = _llm_config() provider = (payload.get("provider") or _default_provider(llm_cfg)).strip().lower() if provider not in {"fastgpt", "dify"}: provider = _default_provider(llm_cfg) retriever = _retriever() hits = retriever.search(question, top_k=3) context = _context_text(hits) local_answer = _local_answer(hits) if mode == "local": return ok({"answer": local_answer, "provider": "local_kb", "items": hits}) user = current_user() llm_result = _ask_by_provider( provider, llm_cfg, question, context, str(user.id) if user else "", user.username if user else "anonymous", ) if mode == "llm": if llm_result.get("ok"): return ok({"answer": llm_result.get("answer"), "provider": provider, "items": hits}) return ok( { "answer": f"LLM 调用失败,已回退本地知识库。{local_answer}", "provider": "local_kb_fallback", "items": hits, "llm_error": llm_result, } ) if llm_result.get("ok"): return ok({"answer": llm_result.get("answer"), "provider": provider, "items": hits}) return ok( { "answer": local_answer, "provider": "local_kb", "items": hits, "llm_error": llm_result, } ) @qa_bp.post("/advice") @jwt_required() def advice(): payload = request.get_json(silent=True) or {} advice_type = (payload.get("advice_type") or "general").strip().lower() question = (payload.get("question") or "请根据我的状态给建议").strip() llm_cfg = _llm_config() provider = (payload.get("provider") or _default_provider(llm_cfg)).strip().lower() if provider not in {"fastgpt", "dify"}: provider = _default_provider(llm_cfg) retriever = _retriever() hits = retriever.search(f"{advice_type} {question}", top_k=3) context = _context_text(hits) base_text = _advice_template(advice_type) user = current_user() llm_result = _ask_by_provider( provider, llm_cfg, f"请给出{advice_type}方向的个性化饮食建议。用户补充: {question}", f"{base_text}\n\n{context}", str(user.id) if user else "", user.username if user else "anonymous", ) if llm_result.get("ok"): answer = llm_result.get("answer") source = provider else: answer = f"{base_text}\n\n(提示:LLM 暂不可用,当前为本地模板建议)" source = "local_template" return ok( { "advice_type": advice_type, "answer": answer, "provider": source, "knowledge_hits": hits, "llm_status": llm_result, } ) @qa_bp.post("/kb/reload") @admin_required def reload_kb(): retriever = _retriever() retriever.reload() return ok({"count": len(retriever.documents)}, "知识库已重载")