Files
iAOP/web/chat/chat_api.py
T

141 lines
5.1 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.
# -*- 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()