Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Read Agent Web 服务 | |
| Flask Web 应用,提供 API 接口供前端调用 | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import logging | |
| import queue | |
| import threading | |
| import uuid | |
| import time | |
| from pathlib import Path | |
| from urllib.parse import urlparse | |
| from flask import Flask, request, jsonify, send_from_directory, Response | |
| from dotenv import load_dotenv | |
| from src.agent import ReadAgent, Memory | |
| from src.repo_manager import RepoManager | |
| from src.api_key_manager import init_manager, get_global_manager | |
| from src.session_storage import SessionStorage | |
| from prompts import get_system_prompt, get_react_format_prompt | |
| # 加载环境变量 | |
| load_dotenv() | |
| # 配置日志 | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # 创建 Flask 应用 | |
| app = Flask(__name__, static_folder='static') | |
| # API Key 管理器(全局共享) | |
| key_manager = None | |
| # 仓库管理器(全局共享) | |
| repo_manager = None | |
| # 会话持久化存储 | |
| session_storage = SessionStorage("./data/sessions.db") | |
| # 会话存储:{session_id: agent_instance} | |
| sessions = {} | |
| # 线程安全锁 | |
| sessions_lock = threading.Lock() | |
| repo_manager_lock = threading.Lock() | |
| key_manager_lock = threading.Lock() | |
| # 仓库同步标志(避免重复同步) | |
| _repo_synced = False | |
| _repo_sync_lock = threading.Lock() | |
| # 弹窗配置文件路径 | |
| POPUP_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config', 'popup.json') | |
| # 弹窗配置缓存 | |
| _popup_config_cache = None | |
| _popup_config_cache_lock = threading.Lock() | |
| def get_env(key: str, default: str = "") -> str: | |
| """获取环境变量""" | |
| return os.getenv(key, default) | |
| def get_env_bool(key: str, default: bool = False) -> bool: | |
| """获取布尔型环境变量""" | |
| value = get_env(key, "").lower() | |
| if value in ("true", "1", "yes", "on"): | |
| return True | |
| elif value in ("false", "0", "no", "off"): | |
| return False | |
| return default | |
| def load_popup_config(): | |
| """加载弹窗配置(带缓存)""" | |
| global _popup_config_cache | |
| with _popup_config_cache_lock: | |
| if _popup_config_cache is not None: | |
| return _popup_config_cache | |
| try: | |
| if os.path.exists(POPUP_CONFIG_PATH): | |
| with open(POPUP_CONFIG_PATH, 'r', encoding='utf-8') as f: | |
| config = json.load(f) | |
| _popup_config_cache = config | |
| logger.info(f"加载弹窗配置: {POPUP_CONFIG_PATH}") | |
| return config | |
| else: | |
| logger.warning(f"弹窗配置文件不存在: {POPUP_CONFIG_PATH}") | |
| return None | |
| except Exception as e: | |
| logger.error(f"加载弹窗配置失败: {e}") | |
| return None | |
| def reload_popup_config(): | |
| """重新加载弹窗配置""" | |
| global _popup_config_cache | |
| with _popup_config_cache_lock: | |
| _popup_config_cache = None | |
| return load_popup_config() | |
| def format_action_args(action_args: dict) -> str: | |
| """格式化 action_args 为字符串""" | |
| if not action_args: | |
| return "" | |
| return ", ".join(f'{k}="{v}"' for k, v in action_args.items()) | |
| def get_repo_manager(): | |
| """获取或创建仓库管理器(线程安全)""" | |
| global repo_manager | |
| with repo_manager_lock: | |
| if repo_manager is None: | |
| code_dir = get_env("CODE_DIR", "./code") | |
| repo_manager = RepoManager(code_dir) | |
| return repo_manager | |
| def get_key_manager(): | |
| """获取或创建 API Key 管理器(线程安全)""" | |
| global key_manager | |
| with key_manager_lock: | |
| if key_manager is None: | |
| api_keys = get_env("OPENAI_API_KEY", "") | |
| if api_keys: | |
| key_manager = init_manager(api_keys) | |
| logger.info(f"API Key 管理器初始化完成,共 {key_manager.key_count} 个 Key") | |
| else: | |
| logger.warning("未配置 OPENAI_API_KEY") | |
| return key_manager | |
| def get_agent(session_id: str = None): | |
| """获取或创建会话的 Agent,返回 (agent, session_id)(线程安全)""" | |
| with sessions_lock: | |
| # 如果没有 session_id 或会话不存在,创建新的 | |
| if not session_id or session_id not in sessions: | |
| # 生成 session_id(如果未提供) | |
| session_id = session_id or str(uuid.uuid4()) | |
| # 在锁外获取环境变量和初始化耗时操作 | |
| # 注意:这里先释放锁,然后再重新获取锁进行 sessions 写入 | |
| # 但为了避免竞态条件,我们在创建完成前保持 session_id 的唯一性 | |
| else: | |
| return sessions[session_id], session_id | |
| # 释放锁后执行耗时操作(避免阻塞其他线程) | |
| # 首先尝试从存储恢复会话 | |
| if session_id: | |
| saved_session = session_storage.load_session(session_id) | |
| if saved_session: | |
| # 恢复会话 | |
| agent = ReadAgent( | |
| code_dir=saved_session["code_dir"], | |
| base_url=saved_session["base_url"], | |
| model=saved_session["model"], | |
| max_steps=saved_session["max_steps"], | |
| stream_output=saved_session["stream_output"], | |
| tree_depth=saved_session["tree_depth"], | |
| api_key_manager=get_key_manager() | |
| ) | |
| # 恢复状态 | |
| agent.conversation_history = saved_session["conversation_history"] | |
| for m in saved_session["memories"]: | |
| agent.memories.append(Memory( | |
| file_path=m["file_path"], | |
| overview=m["overview"], | |
| key_definitions=m["key_definitions"], | |
| core_logic=m["core_logic"], | |
| dependencies=m["dependencies"], | |
| needed_info=m["needed_info"] | |
| )) | |
| # 重新获取锁并写入 sessions(防止重复创建) | |
| with sessions_lock: | |
| if session_id not in sessions: | |
| sessions[session_id] = agent | |
| logger.info(f"恢复会话: {session_id}") | |
| else: | |
| agent = sessions[session_id] | |
| return agent, session_id | |
| # 没有找到已保存的会话,创建新的 | |
| # 获取 API Key 管理器 | |
| key_manager = get_key_manager() | |
| if not key_manager or not key_manager.has_keys: | |
| logger.warning("OPENAI_API_KEY 未设置") | |
| base_url = get_env("OPENAI_BASE_URL", "https://api.openai.com/v1") | |
| model = get_env("OPENAI_MODEL", "gpt-4") | |
| code_dir = get_env("CODE_DIR", "./code") | |
| max_steps = int(get_env("MAX_STEPS", "10")) | |
| stream_output = get_env_bool("STREAM_OUTPUT", True) | |
| tree_depth = int(get_env("TREE_DEPTH", "3")) | |
| # 初始化仓库管理器 | |
| repo_manager = get_repo_manager() | |
| # 仓库同步已在启动时完成,无需再次同步 | |
| # 创建代码目录 | |
| Path(code_dir).mkdir(parents=True, exist_ok=True) | |
| agent = ReadAgent( | |
| code_dir=code_dir, | |
| base_url=base_url, | |
| model=model, | |
| max_steps=max_steps, | |
| stream_output=stream_output, | |
| tree_depth=tree_depth, | |
| api_key_manager=key_manager | |
| ) | |
| # 保存新会话到持久化存储 | |
| session_storage.save_session( | |
| session_id=session_id, | |
| model=model, | |
| base_url=base_url, | |
| code_dir=code_dir, | |
| max_steps=max_steps, | |
| stream_output=stream_output, | |
| tree_depth=tree_depth, | |
| conversation_history=agent.conversation_history, | |
| memories=[m.to_dict() for m in agent.memories] | |
| ) | |
| # 重新获取锁并写入 sessions(防止重复创建) | |
| with sessions_lock: | |
| # 再次检查,可能在创建过程中另一个线程已经创建了 | |
| if session_id not in sessions: | |
| sessions[session_id] = agent | |
| logger.info(f"创建新会话: {session_id}, model={model}") | |
| else: | |
| # 如果已存在,使用现有的 agent | |
| agent = sessions[session_id] | |
| logger.info(f"使用现有会话: {session_id}") | |
| return agent, session_id | |
| def save_agent_state(session_id: str, agent: ReadAgent): | |
| """保存 Agent 状态到持久化存储""" | |
| try: | |
| session_storage.save_session( | |
| session_id=session_id, | |
| model=agent.model, | |
| base_url=agent.base_url, | |
| code_dir=str(agent.searcher.root_dir), | |
| max_steps=agent.max_steps, | |
| stream_output=agent.stream_output, | |
| tree_depth=agent.tree_depth, | |
| conversation_history=agent.conversation_history, | |
| memories=[m.to_dict() for m in agent.memories] | |
| ) | |
| except Exception as e: | |
| logger.warning(f"保存会话状态失败: {e}") | |
| def index(): | |
| """主页""" | |
| return send_from_directory('static', 'index.html') | |
| def static_files(filename): | |
| """静态文件""" | |
| return send_from_directory('static', filename) | |
| def prompt(): | |
| """返回提示词""" | |
| from src.agent import ReadAgent | |
| from prompts import get_system_prompt | |
| # 获取 Agent 的工具信息 | |
| agent = ReadAgent() | |
| tools_info = agent.tool_executor.get_available_tools() | |
| max_steps = agent.max_steps | |
| # 使用 prompts.py 中的函数生成提示词 | |
| return get_system_prompt(tools_info, max_steps) | |
| def use_document(): | |
| """返回参考文档""" | |
| # 可以扩展为返回实际文档内容 | |
| return "" | |
| def ask(): | |
| """Read Agent 问答 API""" | |
| data = request.json | |
| if not data: | |
| return jsonify({"error": "请求体不能为空"}), 400 | |
| question = data.get('question', '') | |
| stream = data.get('stream', True) # 默认启用流式输出 | |
| session_id = data.get('session_id') # 获取会话 ID | |
| if not question: | |
| return jsonify({"error": "问题不能为空"}), 400 | |
| # 获取或创建会话的 Agent | |
| agent, session_id = get_agent(session_id) | |
| if stream: | |
| # 流式响应 - 真正的 token 级别流式输出 | |
| def generate(): | |
| try: | |
| import sys | |
| # 发送会话 ID | |
| yield "data: " + json.dumps({"type": "session_id", "session_id": session_id}) + "\n\n" | |
| sys.stdout.flush() | |
| # 发送开始信号 | |
| yield "data: " + json.dumps({"type": "start"}) + "\n\n" | |
| sys.stdout.flush() | |
| # 发送问题 | |
| yield "data: " + json.dumps({"type": "question", "content": question}) + "\n\n" | |
| sys.stdout.flush() | |
| # 初始化当前会话的 Agent | |
| agent.steps = [] | |
| agent.conversation_history.append({"role": "user", "content": question}) | |
| full_answer = "" | |
| current_step = 0 | |
| for step in range(1, agent.max_steps + 1): | |
| current_step = step | |
| # 创建流式回调来实时发送 LLM 响应 | |
| msg_queue = queue.Queue() | |
| def stream_callback(chunk): | |
| msg_queue.put(("chunk", chunk)) | |
| def done_callback(): | |
| msg_queue.put(("done", None)) | |
| # 在后台线程中调用 LLM(支持重试) | |
| def call_llm(): | |
| import time | |
| # 使用 agent 的重试配置 | |
| max_retries = agent.max_retries | |
| retry_delays = agent.retry_delays | |
| # 可重试的 HTTP 状态码 | |
| retryable_status_codes = [401, 429, 500, 502, 503, 504] | |
| for attempt in range(max_retries): | |
| try: | |
| # 使用 http.client 实现真正的流式读取 | |
| import http.client | |
| # 从 ApiKeyManager 获取 key(随机选择) | |
| api_key = None | |
| if agent.api_key_manager: | |
| api_key = agent.api_key_manager.get_key() | |
| if not api_key: | |
| raise Exception("未配置 API Key") | |
| # 解析 base_url | |
| parsed_url = urlparse(agent.base_url) | |
| host = parsed_url.hostname or "api.openai.com" | |
| port = parsed_url.port or 443 | |
| # 拼接 path 和 endpoint | |
| api_path = parsed_url.path.strip() if parsed_url.path else "/v1" | |
| endpoint = "/chat/completions" | |
| path = f"{api_path}{endpoint}" | |
| headers = { | |
| "Content-Type": "application/json", | |
| "Authorization": f"Bearer {api_key}", | |
| "Host": host | |
| } | |
| messages = [ | |
| {"role": "system", "content": agent._build_system_prompt()} | |
| ] | |
| for msg in agent.conversation_history: | |
| messages.append(msg) | |
| messages.append({ | |
| "role": "user", | |
| "content": f"用户问题:{question}\n\n{get_react_format_prompt()}" | |
| }) | |
| body = json.dumps({ | |
| "model": agent.model, | |
| "messages": messages, | |
| "temperature": 0.3, | |
| "stream": True | |
| }) | |
| conn = http.client.HTTPSConnection(host, port, timeout=60) | |
| conn.request("POST", path, body, headers) | |
| response = conn.getresponse() | |
| if response.status != 200: | |
| # 检查是否可重试 | |
| if response.status in retryable_status_codes and attempt < max_retries - 1: | |
| conn.close() | |
| if agent.api_key_manager and api_key: | |
| agent.api_key_manager.record_error(api_key, f"HTTP {response.status} (重试 {attempt + 1}/{max_retries})") | |
| # 获取延迟(如果超出数组长度,使用最后一个值) | |
| delay = retry_delays[min(attempt, len(retry_delays) - 1)] | |
| msg_queue.put(("retry", delay, response.status, attempt + 1, max_retries)) | |
| time.sleep(delay) | |
| continue | |
| else: | |
| # 最后一次重试或不可重试的错误 | |
| conn.close() | |
| if agent.api_key_manager and api_key: | |
| agent.api_key_manager.record_error(api_key, f"HTTP {response.status}") | |
| raise Exception(f"API 错误: {response.status}") | |
| # 真正流式读取响应 | |
| llm_response = "" | |
| while True: | |
| line = response.readline() | |
| if not line: | |
| break | |
| line = line.decode("utf-8").strip() | |
| if not line.startswith("data: "): | |
| continue | |
| if line == "data: [DONE]": | |
| break | |
| data_str = line[6:] | |
| try: | |
| chunk = json.loads(data_str) | |
| if chunk.get("choices") and len(chunk["choices"]) > 0: | |
| delta = chunk["choices"][0].get("delta", {}) | |
| content = delta.get("content", "") | |
| if content: | |
| stream_callback(content) | |
| except json.JSONDecodeError: | |
| continue | |
| conn.close() | |
| # 记录成功 | |
| if agent.api_key_manager and api_key: | |
| agent.api_key_manager.record_success(api_key) | |
| done_callback() | |
| return | |
| except Exception as e: | |
| # 网络错误,可重试 | |
| if attempt < max_retries - 1 and ("timeout" in str(e).lower() or "connection" in str(e).lower()): | |
| if agent.api_key_manager and api_key: | |
| agent.api_key_manager.record_error(api_key, f"网络错误: {str(e)} (重试 {attempt + 1}/{max_retries})") | |
| # 获取延迟(如果超出数组长度,使用最后一个值) | |
| delay = retry_delays[min(attempt, len(retry_delays) - 1)] | |
| msg_queue.put(("retry", delay, "网络错误", attempt + 1, max_retries)) | |
| time.sleep(delay) | |
| continue | |
| else: | |
| # 最后一次重试或不可重试的错误 | |
| if agent.api_key_manager and api_key: | |
| agent.api_key_manager.record_error(api_key, str(e)) | |
| msg_queue.put(("error", str(e))) | |
| return | |
| llm_thread = threading.Thread(target=call_llm) | |
| llm_thread.start() | |
| # 收集 LLM 响应(支持超时后重试当前步骤) | |
| llm_timeout_retries = int(os.getenv("LLM_TIMEOUT_RETRIES", "2")) | |
| llm_timeout_delay = int(os.getenv("LLM_TIMEOUT_DELAY", "2")) | |
| for timeout_attempt in range(llm_timeout_retries + 1): | |
| llm_response = "" | |
| received_done = False | |
| try: | |
| while True: | |
| msg = msg_queue.get(timeout=60) | |
| msg_type = msg[0] | |
| if msg_type == "error": | |
| raise Exception(msg[1]) | |
| elif msg_type == "done": | |
| received_done = True | |
| break | |
| elif msg_type == "retry": | |
| # 重试消息: (retry, delay, status_or_error, attempt, max_retries) | |
| delay, status_or_error, attempt, max_retries = msg[1], msg[2], msg[3], msg[4] | |
| yield "data: " + json.dumps({ | |
| "type": "retry", | |
| "delay": delay, | |
| "status": status_or_error, | |
| "attempt": attempt, | |
| "max_retries": max_retries | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| else: # chunk - (msg_type, content) | |
| content = msg[1] | |
| llm_response += content | |
| # 发送思考内容流式输出 | |
| yield "data: " + json.dumps({ | |
| "type": "chunk", | |
| "step": step, | |
| "content": content, | |
| "stream_type": "thought" | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| break # 成功获取响应,退出重试循环 | |
| except queue.Empty: | |
| if timeout_attempt < llm_timeout_retries: | |
| logger.warning(f"步骤 {step} 等待 LLM 响应超时,{llm_timeout_delay} 秒后重试 ({timeout_attempt + 1}/{llm_timeout_retries + 1})") | |
| yield "data: " + json.dumps({ | |
| "type": "step_timeout", | |
| "step": step, | |
| "retry_delay": llm_timeout_delay, | |
| "attempt": timeout_attempt + 1, | |
| "max_retries": llm_timeout_retries + 1, | |
| "message": f"等待 LLM 响应超时,{llm_timeout_delay} 秒后重试..." | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| time.sleep(llm_timeout_delay) | |
| # 等待 LLM 线程结束 | |
| if llm_thread.is_alive(): | |
| llm_thread.join(timeout=1) | |
| # 重新启动 LLM 调用 | |
| llm_thread = threading.Thread(target=call_llm) | |
| llm_thread.start() | |
| else: | |
| logger.error(f"步骤 {step} 等待 LLM 响应超时,已达最大重试次数") | |
| break | |
| llm_thread.join() | |
| # 调试日志 | |
| logger.info(f"步骤 {step} LLM 响应: done={received_done}, length={len(llm_response)}, response_preview={llm_response[:200]}...") | |
| # 如果没有收到有效响应,触发重试 | |
| if not llm_response: | |
| if timeout_attempt < llm_timeout_retries: | |
| logger.warning(f"步骤 {step} 收到空响应,{llm_timeout_delay} 秒后重试 ({timeout_attempt + 1}/{llm_timeout_retries + 1})") | |
| yield "data: " + json.dumps({ | |
| "type": "step_empty_response", | |
| "step": step, | |
| "retry_delay": llm_timeout_delay, | |
| "attempt": timeout_attempt + 1, | |
| "max_retries": llm_timeout_retries + 1, | |
| "message": f"LLM 返回空响应,{llm_timeout_delay} 秒后重试..." | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| time.sleep(llm_timeout_delay) | |
| # 重新启动 LLM 调用(继续外层重试循环) | |
| llm_thread = threading.Thread(target=call_llm) | |
| llm_thread.start() | |
| continue | |
| else: | |
| logger.error(f"步骤 {step} 收到空响应,已达最大重试次数") | |
| # 发送错误信号 | |
| yield "data: " + json.dumps({"type": "error", "error": "LLM 未返回有效响应"}) + "\n\n" | |
| return | |
| # 处理 LLM 响应(支持批量 Actions) | |
| thought, actions_list = agent._extract_thought_action(llm_response) | |
| final_answer, memory_data = agent._extract_final_answer(llm_response) | |
| # 获取第一个 action 用于日志 | |
| first_action = None | |
| if actions_list and len(actions_list) > 0: | |
| # actions_list 的每个元素是 (action_name, args_dict) | |
| first_action_tuple = actions_list[0] | |
| if first_action_tuple and len(first_action_tuple) > 0: | |
| first_action = first_action_tuple[0] # action_name | |
| logger.info(f"步骤 {step} 解析: thought={thought[:30] if thought else 'None'}..., action={first_action}, has_final={bool(final_answer)}, actions_count={len(actions_list)}") | |
| # 发送完成当前步骤的思考 | |
| yield "data: " + json.dumps({ | |
| "type": "step_thought_done", | |
| "step": step, | |
| "thought": thought, | |
| "has_action": len(actions_list) > 0 | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| # 如果有 Memory,保存 | |
| if memory_data: | |
| path = memory_data.get("file", "") | |
| if path: | |
| existing = [m for m in agent.memories if m.file_path == path] | |
| if existing: | |
| agent.memories.remove(existing[0]) | |
| # 使用 src.agent 中定义的 Memory 类 | |
| from src.agent import Memory | |
| memory = Memory( | |
| file_path=path, | |
| overview=memory_data.get("overview", ""), | |
| key_definitions=memory_data.get("key_definitions", []), | |
| core_logic=memory_data.get("core_logic", ""), | |
| dependencies=memory_data.get("dependencies", []), | |
| needed_info=memory_data.get("needed_info", "") | |
| ) | |
| agent.memories.append(memory) | |
| if final_answer: | |
| # 发送最终答案流式输出 | |
| for char in final_answer: | |
| yield "data: " + json.dumps({ | |
| "type": "chunk", | |
| "step": step, | |
| "content": char, | |
| "stream_type": "answer" | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| # 发送完成信号 | |
| yield "data: " + json.dumps({ | |
| "type": "step", | |
| "step": step, | |
| "final_answer": final_answer | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| yield "data: " + json.dumps({"type": "done"}) + "\n\n" | |
| sys.stdout.flush() | |
| return | |
| # 执行工具调用(支持批量) | |
| if actions_list: | |
| # 如果是批量执行 | |
| if len(actions_list) > 1: | |
| batch_results = [] | |
| # 发送批量执行提示 | |
| yield "data: " + json.dumps({ | |
| "type": "batch_start", | |
| "step": step, | |
| "count": len(actions_list) | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| # 批量执行所有 Actions | |
| for i, (action, action_args) in enumerate(actions_list, 1): | |
| # 使用 ** 解包字典作为关键字参数 | |
| tool_result = agent.tool_executor.execute_tool(action, **action_args) | |
| batch_results.append({ | |
| "index": i, | |
| "action": f"{action}({format_action_args(action_args)})", | |
| "result": tool_result | |
| }) | |
| # 发送单个 Action 结果 | |
| yield "data: " + json.dumps({ | |
| "type": "batch_item", | |
| "step": step, | |
| "index": i, | |
| "action": f"{action}({format_action_args(action_args)})", | |
| "observation": tool_result | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| # 发送批量完成信号 | |
| yield "data: " + json.dumps({ | |
| "type": "batch_done", | |
| "step": step, | |
| "results": batch_results | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| # 将批量观察结果添加到对话 | |
| agent.conversation_history.append({ | |
| "role": "user", | |
| "content": f"Observation: {json.dumps(batch_results, ensure_ascii=False)}" | |
| }) | |
| else: | |
| # 单个 Action | |
| action, action_args = actions_list[0] | |
| tool_result = agent.tool_executor.execute_tool(action, **action_args) | |
| # 发送工具调用 | |
| yield "data: " + json.dumps({ | |
| "type": "step", | |
| "step": step, | |
| "thought": thought, | |
| "action": f"{action}({action_args})", | |
| "observation": tool_result | |
| }, ensure_ascii=False) + "\n\n" | |
| sys.stdout.flush() | |
| # 将观察结果添加到对话 | |
| agent.conversation_history.append({ | |
| "role": "user", | |
| "content": f"Observation: {json.dumps(tool_result, ensure_ascii=False)}" | |
| }) | |
| # 超时 | |
| yield "data: " + json.dumps({"type": "done"}) + "\n\n" | |
| sys.stdout.flush() | |
| except Exception as e: | |
| logger.error(f"流式响应错误: {e}") | |
| yield "data: " + json.dumps({"type": "error", "error": str(e)}) + "\n\n" | |
| sys.stdout.flush() | |
| finally: | |
| # 保存会话状态 | |
| save_agent_state(session_id, agent) | |
| response = Response(generate(), mimetype='text/event-stream') | |
| response.headers['Cache-Control'] = 'no-cache' | |
| response.headers['X-Accel-Buffering'] = 'no' | |
| return response | |
| else: | |
| # 非流式响应 | |
| try: | |
| agent.stream_output = False | |
| agent.ask(question) | |
| # 保存会话状态 | |
| save_agent_state(session_id, agent) | |
| # 构建响应 | |
| steps = [] | |
| final_answer = "" | |
| for step_info in agent.steps: | |
| step = { | |
| "step": step_info.get("step"), | |
| "thought": step_info.get("thought", ""), | |
| "action": step_info.get("action_str", ""), | |
| "observation": step_info.get("observation"), | |
| "final_answer": step_info.get("final_answer", "") | |
| } | |
| steps.append(step) | |
| if step_info.get("final_answer"): | |
| final_answer = step_info["final_answer"] | |
| return jsonify({ | |
| "success": True, | |
| "question": question, | |
| "answer": final_answer or "未找到答案", | |
| "steps": steps | |
| }) | |
| except Exception as e: | |
| logger.error(f"响应错误: {e}") | |
| return jsonify({"success": False, "error": str(e)}), 500 | |
| def health(): | |
| """健康检查""" | |
| return jsonify({"status": "ok", "message": "Read Agent 服务运行中"}) | |
| def status(): | |
| """获取服务状态""" | |
| session_id = request.args.get('session_id') | |
| agent, _ = get_agent(session_id) | |
| stats = agent.get_stats() | |
| return jsonify({ | |
| "status": "running", | |
| "stats": stats | |
| }) | |
| # ============ 会话管理 API ============ | |
| def clear_session(): | |
| """清除指定会话(线程安全)""" | |
| data = request.json or {} | |
| session_id = data.get('session_id') | |
| with sessions_lock: | |
| if session_id and session_id in sessions: | |
| del sessions[session_id] | |
| # 同时删除持久化存储 | |
| session_storage.delete_session(session_id) | |
| return jsonify({ | |
| "success": True, | |
| "message": f"会话已清除: {session_id}" | |
| }) | |
| elif not session_id: | |
| # 清除所有会话 | |
| count = len(sessions) | |
| sessions.clear() | |
| # 同时清除持久化存储 | |
| session_storage.clear_all() | |
| return jsonify({ | |
| "success": True, | |
| "message": f"已清除 {count} 个会话" | |
| }) | |
| else: | |
| return jsonify({ | |
| "success": False, | |
| "error": "会话不存在" | |
| }), 404 | |
| def get_or_create_session(): | |
| """获取或创建会话(前端页面加载时自动调用)""" | |
| session_id = request.args.get('session_id') | |
| agent, session_id = get_agent(session_id) | |
| return jsonify({ | |
| "success": True, | |
| "session_id": session_id, | |
| "model": agent.model, | |
| "code_dir": str(agent.searcher.root_dir), | |
| "max_steps": agent.max_steps, | |
| "tree_depth": agent.tree_depth | |
| }) | |
| def new_session(): | |
| """创建新会话""" | |
| _, session_id = get_agent(None) | |
| return jsonify({ | |
| "success": True, | |
| "session_id": session_id | |
| }) | |
| def list_sessions(): | |
| """列出所有会话(从持久化存储)""" | |
| sessions_list = session_storage.list_sessions() | |
| return jsonify({ | |
| "success": True, | |
| "sessions": sessions_list, | |
| "count": len(sessions_list) | |
| }) | |
| # ============ 仓库管理 API ============ | |
| def list_repos(): | |
| """获取已同步的仓库列表(线程安全)""" | |
| repo_manager = get_repo_manager() | |
| repos = repo_manager.get_repo_list() | |
| return jsonify({ | |
| "repos": repos, | |
| "count": len(repos) | |
| }) | |
| def sync_repos(): | |
| """手动触发仓库同步(线程安全)""" | |
| data = request.json or {} | |
| force = data.get('force', False) | |
| repo_manager = get_repo_manager() | |
| results = repo_manager.sync_all(parallel=True, force=force) | |
| return jsonify({ | |
| "success": results["success"], | |
| "skipped": results.get("skipped", []), | |
| "failed": results["failed"], | |
| "message": f"同步完成: 成功 {len(results['success'])}, 跳过 {len(results.get('skipped', []))}, 失败 {len(results['failed'])}" | |
| }) | |
| def get_repo_config(): | |
| """获取仓库配置(线程安全)""" | |
| repo_manager = get_repo_manager() | |
| repos = repo_manager.load_from_env() | |
| return jsonify({ | |
| "repos": [ | |
| { | |
| "name": r.name, | |
| "url": r.url, | |
| "branch": r.branch, | |
| "auto_update": r.auto_update | |
| } | |
| for r in repos | |
| ], | |
| "count": len(repos) | |
| }) | |
| def clear_repos(): | |
| """清空所有仓库(线程安全)""" | |
| repo_manager = get_repo_manager() | |
| count = repo_manager.clear_all() | |
| return jsonify({ | |
| "message": f"已清空 {count} 个仓库", | |
| "count": count | |
| }) | |
| # ============ API Key 管理 API ============ | |
| def get_api_keys_stats(): | |
| """获取 API Key 统计信息(线程安全)""" | |
| key_manager = get_key_manager() | |
| if not key_manager: | |
| return jsonify({ | |
| "error": "未配置 API Key" | |
| }), 404 | |
| return jsonify(key_manager.get_stats()) | |
| def reset_api_keys_stats(): | |
| """重置 API Key 统计信息(线程安全)""" | |
| key_manager = get_key_manager() | |
| if not key_manager: | |
| return jsonify({ | |
| "error": "未配置 API Key" | |
| }), 404 | |
| key_manager.reset_stats() | |
| return jsonify({ | |
| "success": True, | |
| "message": "统计信息已重置" | |
| }) | |
| # ============ 弹窗管理 API ============ | |
| def get_popup_config(): | |
| """获取弹窗配置""" | |
| config = load_popup_config() | |
| if not config: | |
| return jsonify({ | |
| "enabled": False, | |
| "message": "弹窗配置未找到" | |
| }) | |
| return jsonify({ | |
| "enabled": config.get("enabled", False), | |
| "id": config.get("id", ""), | |
| "display": config.get("display", {}), | |
| "content": config.get("content", {}), | |
| "buttons": config.get("buttons", []), | |
| "storage": config.get("storage", {}), | |
| "showRules": config.get("showRules", {}) | |
| }) | |
| def reload_popup(): | |
| """重新加载弹窗配置(管理接口)""" | |
| try: | |
| config = reload_popup_config() | |
| return jsonify({ | |
| "success": True, | |
| "message": "弹窗配置已重新加载", | |
| "config": config | |
| }) | |
| except Exception as e: | |
| logger.error(f"重新加载弹窗配置失败: {e}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def initialize_app(): | |
| """启动时预初始化全局组件""" | |
| global key_manager, repo_manager, _repo_synced | |
| logger.info("=" * 60) | |
| logger.info("Read Agent 初始化中...") | |
| # 初始化 API Key 管理器 | |
| logger.info("初始化 API Key 管理器...") | |
| key_manager = get_key_manager() | |
| if key_manager and key_manager.has_keys: | |
| logger.info(f" ✓ 已加载 {key_manager.key_count} 个 API Key") | |
| else: | |
| logger.warning(" ⚠ 未配置 API Key") | |
| # 初始化仓库管理器 | |
| logger.info("初始化仓库管理器...") | |
| repo_manager = get_repo_manager() | |
| logger.info(f" ✓ 代码目录: {repo_manager.base_dir}") | |
| # 预同步仓库(如果配置启用) | |
| sync_on_startup = get_env_bool("REPO_SYNC_ON_STARTUP", True) | |
| if sync_on_startup: | |
| logger.info("预同步仓库...") | |
| results = repo_manager.sync_all(parallel=True) | |
| success_count = len(results['success']) | |
| skipped_count = len(results.get('skipped', [])) | |
| failed_count = len(results['failed']) | |
| logger.info(f" ✓ 同步完成: 成功 {success_count}, 跳过 {skipped_count}, 失败 {failed_count}") | |
| with _repo_sync_lock: | |
| _repo_synced = True | |
| else: | |
| logger.info(" ✓ 跳过仓库同步 (REPO_SYNC_ON_STARTUP=false)") | |
| logger.info("=" * 60) | |
| logger.info("初始化完成,等待请求...") | |
| if __name__ == '__main__': | |
| port = int(get_env("WEB_PORT", "7860")) | |
| debug = get_env_bool("DEBUG", False) | |
| # 启动时预初始化 | |
| initialize_app() | |
| logger.info(f"启动 Read Agent Web 服务,端口: {port}") | |
| logger.info(f"MAX_STEPS={get_env('MAX_STEPS', '10')}") | |
| app.run(host='0.0.0.0', port=port, debug=debug) | |