Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
220 lines
8.0 KiB
Python
220 lines
8.0 KiB
Python
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
|
|
|
|
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
|
|
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
|
|
(对应书中所说的"FastAPI 的热加载")。
|
|
|
|
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
|
|
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
|
|
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
|
|
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
|
|
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
|
|
|
|
启动方式(二选一,行为一致):
|
|
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
|
|
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
|
|
"""
|
|
|
|
import os
|
|
import argparse
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
|
|
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
except Exception:
|
|
pass
|
|
|
|
app = FastAPI(title="Conversational UI Backend")
|
|
|
|
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
class ChatRequest(BaseModel):
|
|
message: str
|
|
|
|
|
|
def _chat_model() -> str:
|
|
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
|
|
|
|
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
|
|
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
|
|
"""
|
|
return (os.getenv("CHAT_MODEL") or "").strip()
|
|
|
|
|
|
def _llm_reply(message: str, model: str) -> str:
|
|
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
|
|
|
|
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
|
|
"""
|
|
try:
|
|
from openai import OpenAI
|
|
except Exception:
|
|
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
|
|
|
|
api_key = os.getenv("OPENAI_API_KEY")
|
|
base_url = os.getenv("OPENAI_BASE_URL")
|
|
orkey = os.getenv("OPENROUTER_API_KEY")
|
|
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
|
|
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
|
|
if prefer_or or (not api_key and orkey):
|
|
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
|
|
if "/" not in model:
|
|
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
|
|
if not api_key:
|
|
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
|
|
|
|
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
|
|
if base_url:
|
|
client_kwargs["base_url"] = base_url
|
|
|
|
try:
|
|
client = OpenAI(**client_kwargs)
|
|
resp = client.chat.completions.create(
|
|
model=model,
|
|
messages=[
|
|
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
|
|
{"role": "user", "content": message},
|
|
],
|
|
temperature=(1 if any(k in (model or "").lower()
|
|
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
|
|
else 0.7),
|
|
)
|
|
return resp.choices[0].message.content or "(模型返回了空回复)"
|
|
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
|
|
return f"(调用 LLM 失败:{e};已回退占位回复)"
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
model = _chat_model()
|
|
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
|
|
|
|
|
|
@app.post("/api/chat")
|
|
def chat(req: ChatRequest):
|
|
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
|
|
|
|
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
|
|
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
|
|
"""
|
|
model = _chat_model()
|
|
if model:
|
|
return {"reply": _llm_reply(req.message, model)}
|
|
return {"reply": f"我收到了你的消息:{req.message}"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
|
|
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
|
|
# ---------------------------------------------------------------------------
|
|
def parse_args(argv=None):
|
|
parser = argparse.ArgumentParser(
|
|
prog="main.py",
|
|
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
|
|
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--host",
|
|
default="127.0.0.1",
|
|
help="监听地址;对外可用 0.0.0.0。",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=8000,
|
|
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
|
|
)
|
|
reload_group = parser.add_mutually_exclusive_group()
|
|
reload_group.add_argument(
|
|
"--reload",
|
|
dest="reload",
|
|
action="store_true",
|
|
default=True,
|
|
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
|
|
)
|
|
reload_group.add_argument(
|
|
"--no-reload",
|
|
dest="reload",
|
|
action="store_false",
|
|
help="关闭热加载(更接近生产运行)。",
|
|
)
|
|
parser.add_argument(
|
|
"--model",
|
|
default=os.getenv("CHAT_MODEL") or None,
|
|
metavar="NAME",
|
|
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
|
|
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
|
|
)
|
|
parser.add_argument(
|
|
"--log-level",
|
|
default="info",
|
|
choices=["critical", "error", "warning", "info", "debug", "trace"],
|
|
help="uvicorn 日志/输出级别。",
|
|
)
|
|
parser.add_argument(
|
|
"--print-config",
|
|
action="store_true",
|
|
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv=None):
|
|
import json
|
|
|
|
args = parse_args(argv)
|
|
|
|
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
|
|
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
|
|
if args.model:
|
|
os.environ["CHAT_MODEL"] = args.model
|
|
else:
|
|
os.environ.pop("CHAT_MODEL", None)
|
|
|
|
config = {
|
|
"host": args.host,
|
|
"port": args.port,
|
|
"reload": args.reload,
|
|
"mode": "llm" if args.model else "echo",
|
|
"model": args.model,
|
|
"log_level": args.log_level,
|
|
}
|
|
|
|
if args.print_config:
|
|
print(json.dumps(config, ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
import uvicorn
|
|
|
|
print(
|
|
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
|
|
f" 模式={config['mode']}"
|
|
f" 热加载={'开' if args.reload else '关'}"
|
|
)
|
|
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
|
|
uvicorn.run(
|
|
"main:app",
|
|
host=args.host,
|
|
port=args.port,
|
|
reload=args.reload,
|
|
log_level=args.log_level,
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|