File size: 6,135 Bytes
9a71f08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env python3
"""
VetNet DAPT 推理服务 — DAPT 微调 Qwen2.5-3B + FAISS RAG + LCPS 2.0

启动:
    VET_MODEL=./vet-qwen3b-v3-dapt KB_DIR=./knowledge_base_simple python server_gpu.py

API 端点:
    GET  /health              — 健康检查 + 模型状态
    POST /run                 — LCPS 推理 + SOAP (兼容旧接口)
    POST /api/v1/diagnose     — LCPS 诊断 (VetCopilot 标准接口)
    POST /chat                — 兽医自由问答
"""
import os, sys, time, json, logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
log = logging.getLogger('vetnet-gpu')

MODEL = None
RAG = None
MODEL_INFO = {}

class RunRequest(BaseModel):
    text: str
    species: str = "狗"

class ChatRequest(BaseModel):
    question: str
    species: str = "狗"

class DiagnoseRequest(BaseModel):
    """VetCopilot 标准诊断请求"""
    dialogue: str
    species: str = "狗"

@asynccontextmanager
async def lifespan(app: FastAPI):
    global MODEL, RAG, MODEL_INFO

    model_path = os.getenv("VET_MODEL", "./vet-qwen3b-v3-dapt")
    kb_dir = os.getenv("KB_DIR", "./knowledge_base_simple")

    log.info("=" * 60)
    log.info("VetNet DAPT 推理服务 v3.1 启动")
    log.info(f"模型: {model_path}")
    log.info(f"知识库: {kb_dir}")
    log.info(f"引擎: DAPT Qwen2.5-3B + LCPS 2.0 + FAISS RAG")
    log.info("=" * 60)

    MODEL_INFO = {
        "name": "vet-qwen3b-v3-dapt",
        "base_model": "Qwen2.5-3B-Instruct",
        "training": "DAPT (Domain-Adaptive Pretraining)",
        "engine": "LCPS 2.0",
    }

    t0 = time.time()

    # 加载 RAG
    try:
        from model.rag_faiss import FAISSRetriever
        RAG = FAISSRetriever(kb_dir=kb_dir, device="cuda")
        RAG.load()
        log.info(f"RAG 加载完成 ({RAG.get_stats()})")
    except Exception as e:
        log.warning(f"RAG 加载失败 (知识库文件缺失): {e}")
        RAG = None

    # 加载 DAPT 模型
    from model.qwen_local import QwenLocalClient
    MODEL = QwenLocalClient(
        model_path=model_path,
        temperature=float(os.getenv("VET_TEMPERATURE", "0.1")),   # 降低温度减少误诊随机性
        max_tokens=int(os.getenv("VET_MAX_TOKENS", "8192")),
        load_in_4bit=False,
        device_map="auto",
        seed=int(os.getenv("VET_SEED", "42")),   # 固定 seed 保证可复现
    )
    MODEL._ensure_loaded()

    MODEL_INFO["load_time_ms"] = int((time.time() - t0) * 1000)
    log.info(f"模型加载完成 ({MODEL_INFO['load_time_ms']}ms)")

    yield

    if MODEL:
        MODEL.unload()
    log.info("服务关闭")


app = FastAPI(title="VetNet GPU v3.0", lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])


@app.get("/health")
def health():
    return {
        "status": "ok",
        "model_loaded": MODEL is not None and MODEL.ping() if MODEL else False,
        "model_info": MODEL_INFO,
        "rag": RAG.get_stats() if RAG else {"ready": False, "vectors": 0, "chunks": 0},
    }


@app.post("/run")
def run(req: RunRequest):
    """LCPS 推理 + SOAP 生成 (兼容旧接口)"""
    return _do_diagnose(req.text, req.species)


@app.post("/api/v1/diagnose")
def diagnose(req: DiagnoseRequest):
    """VetCopilot 标准 LCPS 诊断接口"""
    return _do_diagnose(req.dialogue, req.species)


def _do_diagnose(text: str, species: str) -> dict:
    """统一诊断逻辑"""
    if not MODEL:
        raise HTTPException(503, "模型未加载")

    t0 = time.time()
    try:
        from model.lcps import lcps_single_shot, generate_soap_from_lcps

        rag = RAG if RAG and RAG._ready else None
        result = lcps_single_shot(text, species, MODEL, rag)
        soap = generate_soap_from_lcps(result["lcps_chain"], text, MODEL)

        elapsed = int((time.time() - t0) * 1000)
        return {
            "success": True,
            "diagnosis": result.get("primary_diagnosis", ""),
            "primary_diagnosis": result.get("primary_diagnosis", ""),
            "differential_list": result.get("differential_list", []),
            "must_not_miss": result.get("must_not_miss", []),
            "soap": soap.get("soap", {}),
            "lcps_chain": result.get("lcps_chain", {}),
            "summary": result.get("prioritisation_rationale", ""),
            "kb_used": rag is not None,
            "engine": "dapt-lcps-v3.1",
            "model": MODEL_INFO.get("name", "vet-qwen3b-v3-dapt"),
            "total_ms": elapsed,
            "elapsed_ms": elapsed,
            "error": "",
        }
    except Exception as e:
        log.exception("推理失败")
        return {
            "success": False,
            "diagnosis": "",
            "primary_diagnosis": "",
            "differential_list": [],
            "must_not_miss": [],
            "soap": {},
            "lcps_chain": {},
            "summary": "",
            "engine": "dapt-lcps-v3.1",
            "model": MODEL_INFO.get("name", ""),
            "elapsed_ms": int((time.time() - t0) * 1000),
            "error": str(e),
        }


@app.post("/chat")
def chat(req: ChatRequest):
    if not MODEL:
        raise HTTPException(503, "模型未加载")
    try:
        kb_context = RAG.retrieve(f"{req.species} {req.question[:300]}") if RAG else ""
        system = "你是一位经验丰富的兽医专家。请用专业但易懂的语言回答以下问题。"
        if kb_context:
            system += f"\n\n参考知识:\n{kb_context}"
        answer = MODEL.chat(system, f"物种: {req.species}\n问题: {req.question}")
        return {"answer": answer, "kb_used": bool(kb_context)}
    except Exception as e:
        log.exception("问答失败")
        raise HTTPException(500, str(e))


if __name__ == "__main__":
    import uvicorn
    port = int(os.getenv("PORT", "8000"))
    uvicorn.run(app, host="0.0.0.0", port=port)