server.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from __future__ import annotations
  2. import json
  3. import mimetypes
  4. import os
  5. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  6. from pathlib import Path
  7. from typing import Any
  8. from urllib.parse import unquote, urlparse
  9. from .agent_graph import SmqjhAgentGraph
  10. from .config import load_runtime_config
  11. RUNTIME_ROOT = Path(__file__).resolve().parents[1]
  12. REPO_ROOT = RUNTIME_ROOT.parent
  13. PUBLIC_ROOT = RUNTIME_ROOT / "public"
  14. LOGO_PATH = REPO_ROOT / "smqjh-admin-agent" / "build" / "logo.png"
  15. MAX_BODY_BYTES = 1024 * 1024
  16. CONFIG = load_runtime_config()
  17. AGENT = SmqjhAgentGraph(CONFIG)
  18. class AgentRequestHandler(BaseHTTPRequestHandler):
  19. server_version = "SMQJHAgentPython/0.1"
  20. def do_OPTIONS(self) -> None:
  21. self._send_text(204, "")
  22. def do_GET(self) -> None:
  23. try:
  24. path = urlparse(self.path).path
  25. if path == "/api/health":
  26. self._handle_health()
  27. return
  28. if path == "/api/tools":
  29. self._send_json(200, {"ok": True, "tools": AGENT.tools()})
  30. return
  31. if path == "/assets/logo.png":
  32. self._serve_file(LOGO_PATH, "image/png")
  33. return
  34. self._serve_static(path)
  35. except Exception as error:
  36. self._send_json(500, {"ok": False, "message": str(error) or error.__class__.__name__})
  37. def do_POST(self) -> None:
  38. try:
  39. path = urlparse(self.path).path
  40. if path != "/api/chat":
  41. self._send_json(404, {"ok": False, "message": "Not found"})
  42. return
  43. payload = self._read_json_body()
  44. message = str(payload.get("message") or "").strip() if isinstance(payload, dict) else ""
  45. if not message:
  46. self._send_json(400, {"ok": False, "message": "message is required"})
  47. return
  48. history = payload.get("history") if isinstance(payload, dict) else []
  49. if not isinstance(history, list):
  50. history = []
  51. result = AGENT.run(message, history)
  52. self._send_json(200, {"ok": True, "result": result})
  53. except Exception as error:
  54. self._send_json(500, {"ok": False, "message": str(error) or error.__class__.__name__})
  55. def log_message(self, format: str, *args: Any) -> None:
  56. if os.getenv("WEB_AGENT_ACCESS_LOG", "").lower() in {"1", "true", "yes"}:
  57. super().log_message(format, *args)
  58. def _handle_health(self) -> None:
  59. try:
  60. mcp = AGENT.health()
  61. self._send_json(
  62. 200,
  63. {
  64. "ok": True,
  65. "runtime": {
  66. "name": "smqjh-agent-runtime",
  67. "mode": "web",
  68. "framework": "Python + LangGraph",
  69. "environmentName": CONFIG.environment_name,
  70. "baseUrl": CONFIG.base_url,
  71. },
  72. "mcp": mcp,
  73. },
  74. )
  75. except Exception as error:
  76. self._send_json(
  77. 200,
  78. {
  79. "ok": False,
  80. "runtime": {
  81. "name": "smqjh-agent-runtime",
  82. "mode": "web",
  83. "framework": "Python + LangGraph",
  84. },
  85. "mcp": {
  86. "ok": False,
  87. "message": str(error) or error.__class__.__name__,
  88. },
  89. },
  90. )
  91. def _serve_static(self, request_path: str) -> None:
  92. clean_path = "/index.html" if request_path == "/" else request_path
  93. relative = unquote(clean_path).lstrip("/")
  94. file_path = (PUBLIC_ROOT / relative).resolve()
  95. try:
  96. file_path.relative_to(PUBLIC_ROOT.resolve())
  97. except ValueError:
  98. self._send_text(403, "Forbidden")
  99. return
  100. self._serve_file(file_path)
  101. def _serve_file(self, file_path: Path, content_type: str | None = None) -> None:
  102. if not file_path.exists() or not file_path.is_file():
  103. self._send_text(404, "Not found")
  104. return
  105. mime = content_type or mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
  106. data = file_path.read_bytes()
  107. self.send_response(200)
  108. self._cors_headers()
  109. self.send_header("Content-Type", _with_charset(mime))
  110. self.send_header("Cache-Control", "no-cache" if file_path.suffix == ".html" else "public, max-age=300")
  111. self.send_header("Content-Length", str(len(data)))
  112. self.end_headers()
  113. self.wfile.write(data)
  114. def _read_json_body(self) -> dict[str, Any]:
  115. length = int(self.headers.get("Content-Length") or "0")
  116. if length > MAX_BODY_BYTES:
  117. raise ValueError("Request body is too large")
  118. raw = self.rfile.read(length).decode("utf-8") if length else "{}"
  119. return json.loads(raw) if raw.strip() else {}
  120. def _send_json(self, status: int, payload: Any) -> None:
  121. body = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
  122. self.send_response(status)
  123. self._cors_headers()
  124. self.send_header("Content-Type", "application/json; charset=utf-8")
  125. self.send_header("Content-Length", str(len(body)))
  126. self.end_headers()
  127. self.wfile.write(body)
  128. def _send_text(self, status: int, text: str) -> None:
  129. body = text.encode("utf-8")
  130. self.send_response(status)
  131. self._cors_headers()
  132. self.send_header("Content-Type", "text/plain; charset=utf-8")
  133. self.send_header("Content-Length", str(len(body)))
  134. self.end_headers()
  135. self.wfile.write(body)
  136. def _cors_headers(self) -> None:
  137. self.send_header("Access-Control-Allow-Origin", "*")
  138. self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
  139. self.send_header("Access-Control-Allow-Headers", "Content-Type,Authorization,X-MCP-Token")
  140. def _with_charset(mime: str) -> str:
  141. if mime.startswith("text/") or mime in {"application/javascript", "application/json"}:
  142. return f"{mime}; charset=utf-8"
  143. return mime
  144. def main() -> None:
  145. server = ThreadingHTTPServer((CONFIG.host, CONFIG.port), AgentRequestHandler)
  146. print(f"SMQJH Python Web Agent started: http://127.0.0.1:{CONFIG.port}", flush=True)
  147. print(f"Runtime framework: Python + LangGraph", flush=True)
  148. print(f"MCP endpoint: {CONFIG.mcp.url}", flush=True)
  149. try:
  150. server.serve_forever()
  151. except KeyboardInterrupt:
  152. pass
  153. finally:
  154. server.server_close()
  155. if __name__ == "__main__":
  156. main()