api_server.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. """
  2. API Server - FastAPI 应用入口
  3. 聚合所有模块的 API 路由:
  4. - GET /api/traces — 查询(trace/api.py)
  5. - POST /api/traces — 执行控制(trace/run_api.py,需配置 Runner)
  6. - WS /api/traces/{id}/watch — 实时推送(trace/websocket.py)
  7. - GET /api/experiences — 经验查询(trace/run_api.py,需配置 Runner)
  8. """
  9. import json
  10. import logging
  11. import os
  12. import fcntl
  13. from ipaddress import ip_address
  14. from pathlib import Path
  15. from dotenv import load_dotenv
  16. load_dotenv()
  17. from fastapi import FastAPI, Request, WebSocket
  18. from fastapi.responses import JSONResponse
  19. from fastapi.middleware.cors import CORSMiddleware
  20. from fastapi.middleware.trustedhost import TrustedHostMiddleware
  21. import uvicorn
  22. from cyber_agent.trace import FileSystemTraceStore
  23. from cyber_agent.trace.api import router as api_router, set_trace_store as set_api_trace_store
  24. from cyber_agent.trace.run_api import (
  25. router as run_router,
  26. experiences_router,
  27. set_application_runtime,
  28. set_runner,
  29. )
  30. from cyber_agent.trace.websocket import router as ws_router, set_trace_store as set_ws_trace_store
  31. from cyber_agent.trace.examples_api import router as examples_router
  32. from cyber_agent.trace.logs_websocket import router as logs_router, setup_websocket_logging
  33. from cyber_agent.trace.upload_api import router as upload_router, set_trace_store as set_upload_trace_store
  34. # ===== 日志配置 =====
  35. logging.basicConfig(
  36. level=logging.INFO,
  37. format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
  38. )
  39. logger = logging.getLogger(__name__)
  40. # ===== 单用户本地开发边界 =====
  41. LOCAL_DEVELOPMENT_ORIGINS = (
  42. "http://127.0.0.1:3000",
  43. "http://localhost:3000",
  44. )
  45. LOCAL_DEVELOPMENT_HOSTS = ("127.0.0.1", "localhost")
  46. _writer_lock_file = None
  47. def _env_flag(name: str, *, default: bool = False) -> bool:
  48. """解析显式开关;未配置和未识别的值均按关闭处理。"""
  49. value = os.getenv(name)
  50. if value is None:
  51. return default
  52. return value.strip().lower() in {"1", "true", "yes", "on"}
  53. def _is_local_websocket_origin(websocket: WebSocket) -> bool:
  54. """浏览器 WebSocket 必须来自本地前端。"""
  55. return websocket.headers.get("origin") in LOCAL_DEVELOPMENT_ORIGINS
  56. def _is_loopback_host(host: str | None) -> bool:
  57. """Only numeric IPv4/IPv6 loopback peers are trusted."""
  58. if not host:
  59. return False
  60. try:
  61. return ip_address(host).is_loopback
  62. except ValueError:
  63. return False
  64. def _is_loopback_websocket_peer(websocket: WebSocket) -> bool:
  65. return _is_loopback_host(websocket.client.host if websocket.client else None)
  66. def _acquire_single_writer_lock():
  67. """Fail startup when another API process owns this Trace directory."""
  68. global _writer_lock_file
  69. if _writer_lock_file is not None:
  70. return
  71. lock_path = Path(".trace").resolve() / ".api-writer.lock"
  72. lock_path.parent.mkdir(parents=True, exist_ok=True)
  73. handle = lock_path.open("a+")
  74. try:
  75. fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
  76. except BlockingIOError as exc:
  77. handle.close()
  78. raise RuntimeError(
  79. f"another Agent API writer already owns {lock_path.parent}"
  80. ) from exc
  81. _writer_lock_file = handle
  82. def _release_single_writer_lock():
  83. global _writer_lock_file
  84. if _writer_lock_file is None:
  85. return
  86. fcntl.flock(_writer_lock_file.fileno(), fcntl.LOCK_UN)
  87. _writer_lock_file.close()
  88. _writer_lock_file = None
  89. # 设置WebSocket日志推送
  90. setup_websocket_logging(level=logging.INFO)
  91. # ===== FastAPI 应用 =====
  92. app = FastAPI(
  93. title="Agent API",
  94. description="Agent 查询 + 执行 API",
  95. version="1.0.0"
  96. )
  97. # CORS 配置(允许前端跨域访问)
  98. app.add_middleware(
  99. CORSMiddleware,
  100. allow_origins=list(LOCAL_DEVELOPMENT_ORIGINS),
  101. allow_credentials=False,
  102. allow_methods=["*"],
  103. allow_headers=["*"],
  104. )
  105. # 拒绝非本地 Host,防止误暴露和 DNS rebinding。
  106. app.add_middleware(
  107. TrustedHostMiddleware,
  108. allowed_hosts=list(LOCAL_DEVELOPMENT_HOSTS),
  109. )
  110. @app.middleware("http")
  111. async def reject_non_local_browser_origins(request: Request, call_next):
  112. """Fail closed unless the TCP peer and optional browser Origin are local."""
  113. if not _is_loopback_host(request.client.host if request.client else None):
  114. return JSONResponse(
  115. status_code=403,
  116. content={"detail": "Loopback client required"},
  117. )
  118. origin = request.headers.get("origin")
  119. if origin is not None and origin not in LOCAL_DEVELOPMENT_ORIGINS:
  120. return JSONResponse(
  121. status_code=403,
  122. content={"detail": "Local development origin required"},
  123. )
  124. return await call_next(request)
  125. # ===== 初始化存储 =====
  126. # 使用文件系统存储(支持跨进程和持久化)
  127. trace_store = FileSystemTraceStore(base_path=".trace")
  128. # 注入到 step_tree 模块
  129. set_api_trace_store(trace_store)
  130. set_ws_trace_store(trace_store)
  131. set_upload_trace_store(trace_store)
  132. # ===== 可选:配置 Runner(启用执行 API)=====
  133. # 如需启用 POST /api/traces(新建/运行/停止/反思),取消以下注释并配置 LLM:
  134. from cyber_agent.core.runner import AgentRunner
  135. from cyber_agent.application.bootstrap import build_runtime_from_environment
  136. from cyber_agent.llm import create_openrouter_llm_call
  137. llm_call = create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5")
  138. runner = AgentRunner(
  139. trace_store=trace_store,
  140. llm_call=llm_call,
  141. )
  142. set_runner(runner)
  143. application_runtime = build_runtime_from_environment(
  144. trace_store=trace_store,
  145. llm_call=llm_call,
  146. )
  147. set_application_runtime(application_runtime)
  148. # ===== 注册路由 =====
  149. # Examples API(GET /api/examples)
  150. app.include_router(examples_router)
  151. # Trace 上传 API(POST /api/traces/upload)
  152. app.include_router(upload_router)
  153. # Trace 执行 API(POST + GET /running,需配置 Runner)
  154. # 注意:run_router 必须在 api_router 之前注册,否则 GET /running 会被 /{trace_id} 捕获
  155. app.include_router(run_router)
  156. # 经验 API(GET /api/experiences,需配置 Runner)
  157. app.include_router(experiences_router)
  158. # Trace 查询 API(GET)
  159. app.include_router(api_router)
  160. # Trace WebSocket(实时推送)
  161. app.include_router(ws_router)
  162. # Logs WebSocket(日志推送)
  163. app.include_router(logs_router)
  164. @app.on_event("startup")
  165. async def on_startup():
  166. """服务器启动时执行状态对齐"""
  167. _acquire_single_writer_lock()
  168. for trace in await trace_store.list_traces(limit=10_000):
  169. await trace_store.reconcile_trace_integrity(trace.trace_id)
  170. from cyber_agent.trace.run_api import reconcile_traces
  171. await reconcile_traces()
  172. @app.on_event("shutdown")
  173. async def on_shutdown():
  174. _release_single_writer_lock()
  175. @app.websocket("/ws_ping")
  176. async def ws_ping(websocket: WebSocket):
  177. if not _is_loopback_websocket_peer(websocket):
  178. await websocket.close(code=1008, reason="Loopback client required")
  179. return
  180. if not _is_local_websocket_origin(websocket):
  181. await websocket.close(code=1008, reason="Local development origin required")
  182. return
  183. await websocket.accept()
  184. await websocket.send_text("pong")
  185. await websocket.close()
  186. # ===== 健康检查 =====
  187. @app.get("/health")
  188. async def health_check():
  189. """健康检查"""
  190. return {
  191. "status": "ok",
  192. "service": "Agent Step Tree API",
  193. "version": "1.0.0",
  194. "deployment_mode": "local_development",
  195. "network_scope": "loopback_only",
  196. "authentication": "none",
  197. }
  198. # ===== 启动服务 =====
  199. if __name__ == "__main__":
  200. logger.info("Starting API server...")
  201. uvicorn.run(
  202. "api_server:app",
  203. host="127.0.0.1",
  204. port=8000,
  205. reload=_env_flag("TRACE_API_RELOAD"),
  206. log_level="info"
  207. )