demo_main_fan.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #!/usr/bin/env python3
  2. """历史固定服务的离线演示:队列、网关、独立结果平台、SQLite;不验证自主 Agent。"""
  3. import argparse
  4. import asyncio
  5. import json
  6. import os
  7. from pathlib import Path
  8. import socket
  9. import sys
  10. import uuid
  11. ROOT = Path(__file__).resolve().parents[1]
  12. sys.path.insert(0, str(ROOT))
  13. def local_orchestrator():
  14. """旧工具协议的离线模型,保留给历史调用方;当前 Agent 已使用 Skill 工具集。"""
  15. from langchain_core.language_models.chat_models import BaseChatModel
  16. from langchain_core.messages import AIMessage, AIMessageChunk, ToolMessage, HumanMessage
  17. from langchain_core.outputs import ChatResult, ChatGeneration, ChatGenerationChunk
  18. class OfflineModel(BaseChatModel):
  19. @property
  20. def _llm_type(self):
  21. return "offline-main-fan-orchestrator"
  22. def bind_tools(self, tools, **kwargs):
  23. return self
  24. def response(self, messages):
  25. last = messages[-1]
  26. if isinstance(last, ToolMessage) and last.name == "analyze_main_fan":
  27. value = json.loads(last.content)
  28. return AIMessage(content="", tool_calls=[{"name": "MainFanResult", "args": value, "id": uuid.uuid4().hex}])
  29. text = next(m.content for m in reversed(messages) if isinstance(m, HumanMessage))
  30. text = text.split("输入:", 1)[1]
  31. value, _ = json.JSONDecoder().raw_decode(text.strip())
  32. return AIMessage(content="", tool_calls=[{"name": "analyze_main_fan", "args": {"data": value}, "id": uuid.uuid4().hex}])
  33. def _generate(self, messages, stop=None, run_manager=None, **kwargs):
  34. return ChatResult(generations=[ChatGeneration(message=self.response(messages))])
  35. async def _astream(self, messages, **kwargs):
  36. message = self.response(messages)
  37. yield ChatGenerationChunk(message=AIMessageChunk(content=message.content, tool_calls=message.tool_calls))
  38. return OfflineModel()
  39. async def run(args, output, sock, result_sock):
  40. import uvicorn
  41. from db import business_store, db_conn
  42. from mock_gateway.main_fan import MockState, create_app, task_input
  43. from mock_result_platform.app import ResultStore, create_app as create_result_app
  44. from runtime import business
  45. from runtime.business_tasks import submit, get_controller
  46. from runtime.environment import RuntimeEnvironment
  47. state = MockState(args.data_dir.resolve())
  48. server = uvicorn.Server(uvicorn.Config(create_app(state), log_level="warning", access_log=False))
  49. server_task = asyncio.create_task(server.serve(sockets=[sock]))
  50. received = ResultStore(output / "received_results.sqlite")
  51. result_server = uvicorn.Server(uvicorn.Config(create_result_app(received), log_level="warning", access_log=False))
  52. result_task = asyncio.create_task(result_server.serve(sockets=[result_sock]))
  53. try:
  54. for _ in range(100):
  55. if server.started and result_server.started:
  56. break
  57. if server_task.done():
  58. await server_task
  59. raise RuntimeError("模拟服务器未启动")
  60. if result_task.done():
  61. await result_task
  62. raise RuntimeError("结果接收平台未启动")
  63. await asyncio.sleep(.05)
  64. results = []
  65. management = []
  66. async with RuntimeEnvironment(prewarm=False, billing_retry=False, business_jobs=True):
  67. # 显式使用旧服务做离线网关联调,不能把它当成自主探索模式的模型验收。
  68. from dataclasses import replace
  69. from extensions.main_fan.service import analyze
  70. controller = get_controller()
  71. async def offline_executor(*, graph_config, **kwargs):
  72. configured = graph_config['configurable']
  73. job = await asyncio.to_thread(business_store.get, configured['business_task_id'])
  74. legacy = replace(controller.catalog.business('main_fan'), test_runner=analyze)
  75. return await controller._run_test_pipeline(legacy, job, configured['business_task_input'])
  76. controller.executor = offline_executor
  77. state.seed_reviewed_cache()
  78. for scenario in (args.scenario,) if args.scenario != "all" else ("original", "report", "synthetic"):
  79. created = await asyncio.to_thread(submit, "main_fan", task_input(scenario),
  80. user_name="demo", request_id="demo-"+scenario, publish=True)
  81. for _ in range(160):
  82. job = await asyncio.to_thread(business_store.get, created["id"])
  83. if job["push_status"] == "pushed" or job["status"] in {"failed", "needs_materials", "cancelled"} or job["push_status"] == "failed":
  84. break
  85. await asyncio.sleep(.25)
  86. if job["push_status"] != "pushed":
  87. raise RuntimeError(f"演示任务未成功推送:{(job['status'], job['push_status'], job['error'])}")
  88. results.append({"scenario": scenario, "job_id": job["id"], "status": job["status"],
  89. "push_status": job["push_status"], "result": json.loads(job["result_json"]),
  90. "receipt": json.loads(job["receipt_json"])})
  91. for action, online in (("stop", False), ("start", True)):
  92. await (business.stop("main_fan") if action == "stop" else business.start("main_fan"))
  93. current = await asyncio.to_thread(business_store.state, "main_fan")
  94. assert bool(current["local_enabled"]) == online
  95. management.append({"action": action, "local_enabled": online})
  96. metrics = await asyncio.to_thread(business_store.metrics, "main_fan")
  97. assert not any(call["operation"] in {"register", "push", "upload"} for call in state.calls)
  98. value = {"mode": "legacy_offline_pipeline_gateway_and_separate_results", "facts_source": "manual_transcription_verified_by_sha256",
  99. "results": results, "gateway_calls": state.calls, "uploads": state.uploads,
  100. "management": management, "business_metrics": metrics,
  101. "received_results": received.rows(),
  102. "database": str(output / "demo.sqlite")}
  103. (output / "result.json").write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
  104. print("演示完成:", output / "result.json")
  105. for result in results:
  106. print(result["scenario"], result["status"], result["push_status"],
  107. "关联", len(result["result"]["analysis"]["associations"]),
  108. "计算", len(result["result"]["analysis"]["calculations"]))
  109. finally:
  110. server.should_exit = True
  111. result_server.should_exit = True
  112. await asyncio.gather(server_task, result_task)
  113. def main():
  114. parser = argparse.ArgumentParser()
  115. parser.add_argument("--data-dir", type=Path, required=True)
  116. parser.add_argument("--scenario", choices=["all", "original", "report", "synthetic"], default="all")
  117. parser.add_argument("--output", type=Path)
  118. args = parser.parse_args()
  119. output = (args.output or ROOT / "data" / "main_fan_demo" / uuid.uuid4().hex[:12]).resolve()
  120. output.mkdir(parents=True, exist_ok=True)
  121. if (output / "demo.sqlite").exists():
  122. raise ValueError("演示输出目录已有数据库,请使用新目录避免覆盖")
  123. sock = socket.socket()
  124. sock.bind(("127.0.0.1", 0))
  125. sock.setblocking(False)
  126. port = sock.getsockname()[1]
  127. result_sock = socket.socket()
  128. result_sock.bind(("127.0.0.1", 0))
  129. result_sock.setblocking(False)
  130. result_port = result_sock.getsockname()[1]
  131. os.environ.update({"DEEP_AGENT_ENV_PRIORITY": "none", "DB_DIALECT": "sqlite",
  132. "DB_SQLITE_PATH": str(output / "demo.sqlite"), "CHECKPOINTER": "sqlite",
  133. "CHECKPOINT_DB_PATH": str(output / "checkpoints.sqlite"), "CTX_COMPACT_ENABLED": "0",
  134. "CONTEXT_DEMO_ENABLED": "0", "MAIN_FAN_ENABLED": "1", "MAIN_FAN_AGENT_CODE": "mock-main-fan",
  135. "MAIN_FAN_CHECK_TYPE_CODE": "mock-fan-screening", "MAIN_FAN_TOPICS": "main_fan.judgment",
  136. "SHANXI_GATEWAY_ENABLED": "1", "SHANXI_GATEWAY_URL": f"http://127.0.0.1:{port}",
  137. "SHANXI_FILE_BASE_URL": f"http://127.0.0.1:{port}", "SHANXI_GATEWAY_TOKEN": "mock-internal-token",
  138. "RESULT_PLATFORM_ENABLED": "1", "RESULT_PLATFORM_URL": f"http://127.0.0.1:{result_port}/api/judgments",
  139. "RESULT_PLATFORM_TOKEN": "local-result-platform-token", "MAIN_FAN_AUTO_PUBLISH": "0",
  140. "MAIN_FAN_DEVICES_PATH": "/mock-platform/main-fan/devices", "MAIN_FAN_POINTS_PATH": "/mock-platform/main-fan/points",
  141. "MAIN_FAN_REPORTS_PATH": "/mock-platform/main-fan/reports", "MAIN_FAN_VISION_MODEL": "",
  142. "MAIN_FAN_ALLOW_MANUAL_CACHE": "1",
  143. "TFAGENTS_DAILY_TOKEN_LIMIT": "0", "BILLING_ALLOW_UNPRICED": "1", "LOG_LEVEL": "WARNING"})
  144. try:
  145. asyncio.run(run(args, output, sock, result_sock))
  146. finally:
  147. sock.close()
  148. result_sock.close()
  149. if __name__ == "__main__":
  150. main()