feat: 完成 issue #77 [Ti-1] 前端对话组件集成
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""前端对话组件后端 API —— issue #77(Template-Ti 一期)。
|
||||
|
||||
父 Issue #11「④ LLM 报警解释 / 交接班 / NL 查询」子任务:
|
||||
为驾驶舱/移动端提供对话组件所需的后端 API:
|
||||
|
||||
- `GET /`:返回对话组件页面(chat_widget.html,静态);
|
||||
- `GET /api/health`:服务健康;
|
||||
- `POST /api/chat`:对话接口——按 `scenario` 分发到 Ti 场景
|
||||
(alarm_explain / shift_handover / nl_query),返回统一 JSON:
|
||||
`{answer, route, answer_id, scenario}`。
|
||||
|
||||
纯标准库实现(http.server,无框架依赖),便于联调/内嵌;
|
||||
runner 可注入(测试/替换实现均解耦)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
#: 组件页面路径(相对本模块)
|
||||
WIDGET_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"chat_widget.html")
|
||||
|
||||
#: 场景 → runner 方法映射
|
||||
SCENARIO_METHODS = {
|
||||
"alarm_explain": "explain_alarm",
|
||||
"shift_handover": "generate_handover",
|
||||
"nl_query": "query_cockpit",
|
||||
"default": "query_cockpit",
|
||||
}
|
||||
|
||||
|
||||
def dispatch(runner, request: dict) -> dict:
|
||||
"""按请求分发到场景 runner,返回统一响应。
|
||||
|
||||
Args:
|
||||
runner: 提供 explain_alarm / generate_handover / query_cockpit 的对象;
|
||||
request: `{question, scenario, confidence?}`。
|
||||
Returns:
|
||||
统一 JSON 字典;场景未知 → 降级 nl_query;异常 → error 响应。
|
||||
"""
|
||||
question = str(request.get("question", "")).strip()
|
||||
scenario = str(request.get("scenario", "default"))
|
||||
confidence = float(request.get("confidence", 1.0))
|
||||
if not question:
|
||||
return {"error": "question 不能为空"}
|
||||
|
||||
method_name = SCENARIO_METHODS.get(
|
||||
scenario, SCENARIO_METHODS["default"])
|
||||
resolved = scenario if scenario in SCENARIO_METHODS else "default"
|
||||
method = getattr(runner, method_name, None)
|
||||
if method is None:
|
||||
return {"error": f"场景 {scenario!r} 未实现"}
|
||||
|
||||
try:
|
||||
result = method(question, confidence=confidence)
|
||||
except Exception as exc: # noqa: BLE001 - 统一异常 → error JSON
|
||||
return {"error": f"处理失败: {exc}"}
|
||||
|
||||
return {
|
||||
"answer": result.answer,
|
||||
"route": getattr(result.route, "target", ""),
|
||||
"answer_id": getattr(result, "answer_id", ""),
|
||||
"scenario": resolved,
|
||||
"needs_human": bool(getattr(result, "needs_human", False)),
|
||||
}
|
||||
|
||||
|
||||
class ChatHandler(BaseHTTPRequestHandler):
|
||||
"""对话 API HTTP 处理(GET 组件页 / POST /api/chat)。"""
|
||||
|
||||
#: 场景 runner(由 make_server 注入)
|
||||
runner = None
|
||||
|
||||
def log_message(self, *args): # 静默访问日志
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
if path == "/api/health":
|
||||
self._json(200, {"status": "ok"})
|
||||
return
|
||||
if path in ("/", "/index.html", "/chat_widget.html"):
|
||||
self._html(WIDGET_PATH)
|
||||
return
|
||||
self._json(404, {"error": "not found"})
|
||||
|
||||
def do_POST(self):
|
||||
if urlparse(self.path).path != "/api/chat":
|
||||
self._json(404, {"error": "not found"})
|
||||
return
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
request = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
self._json(400, {"error": "请求体不是合法 JSON"})
|
||||
return
|
||||
self._json(200, dispatch(self.runner, request))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _json(self, code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _html(self, path: str) -> None:
|
||||
if not os.path.isfile(path):
|
||||
self._json(404, {"error": "widget 页面缺失"})
|
||||
return
|
||||
with open(path, "rb") as fh:
|
||||
body = fh.read()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
def make_server(host: str, port: int, runner=None) -> ThreadingHTTPServer:
|
||||
"""构建对话 API 服务器(runner 可注入,便于测试/替换)。"""
|
||||
ChatHandler.runner = runner
|
||||
return ThreadingHTTPServer((host, port), ChatHandler)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="对话 API 服务(issue #77)")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=8080)
|
||||
args = parser.parse_args()
|
||||
print(f"对话 API 服务:http://{args.host}:{args.port}/(组件页)")
|
||||
make_server(args.host, args.port).serve_forever()
|
||||
Reference in New Issue
Block a user