app.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. from quart import Quart, render_template, request, jsonify, Response
  2. from Client import PythonCTranspiler, AIMessage
  3. import asyncio
  4. import aiofiles
  5. from contextlib import asynccontextmanager
  6. from typing import Optional, AsyncIterator
  7. import re
  8. import json
  9. from pathlib import Path
  10. # 全局agent实例
  11. agent_instance: Optional[PythonCTranspiler] = None
  12. # 配置扫描路径
  13. LOG_DIR = None # 日志文件目录
  14. FILE_DIR = None # 文件扫描目录
  15. # 存储已读取的日志信息和历史内容
  16. file_history = []
  17. processed_files = set()
  18. app = Quart(__name__)
  19. # 在应用启动前执行
  20. @app.before_serving
  21. async def startup():
  22. print("=== Application Starting ===")
  23. global agent_instance
  24. global LOG_DIR
  25. global FILE_DIR
  26. agent_instance = await PythonCTranspiler().initialize()
  27. # 获取日志存储路径
  28. if LOG_DIR == None:
  29. LOG_DIR = agent_instance.session_log_dir
  30. print(f"Get log_dir:{LOG_DIR}")
  31. # 获取文件存储路径
  32. if FILE_DIR == None:
  33. FILE_DIR = agent_instance.session_work_dir
  34. print(f"Get file_dir:{FILE_DIR}")
  35. print("Agent initialized!")
  36. # 在应用关闭后执行
  37. @app.after_serving
  38. async def shutdown():
  39. print("=== Application Shutting Down ===")
  40. if agent_instance:
  41. await agent_instance.close()
  42. print("Agent closed!")
  43. # 构建网页界面
  44. @app.route('/')
  45. async def index():
  46. print("In index")
  47. return await render_template('index.html')
  48. @app.route('/chat', methods=['POST'])
  49. async def chat():
  50. print("In chat")
  51. try:
  52. # 检查agent是否已经初始化
  53. if not agent_instance or not getattr(agent_instance, 'initialized', False):
  54. return jsonify({'error': 'Agent未初始化,请稍后重试'}), 503
  55. data = await request.get_json()
  56. user_input = data.get('message', '').strip()
  57. if not user_input:
  58. return jsonify({'error': '输入不能为空'}), 400
  59. if not agent_instance:
  60. return jsonify({'error': 'Agent未初始化'}), 500
  61. # 使用全局agent实例处理输入
  62. state = await agent_instance.process_input(user_input)
  63. # 提取最终的AI响应
  64. if state["messages"] and isinstance(state["messages"][-1], AIMessage):
  65. agent_response = state['messages'][-1].content
  66. return jsonify({
  67. 'user_input': user_input,
  68. 'agent_response': agent_response,
  69. 'success': True
  70. })
  71. except Exception as e:
  72. return jsonify({'error': f'处理请求时出错: {str(e)}'}), 500
  73. # 实时获取日志内容
  74. @app.route('/scan_logs', methods=['GET'])
  75. async def scan_logs():
  76. print("scan_logs start")
  77. try:
  78. path = LOG_DIR
  79. print(f"log_dir:{path}")
  80. # 检查路径是否存在
  81. if not path.exists() or path is None:
  82. return jsonify({
  83. 'success': False,
  84. 'error': '日志目录未创建'
  85. }), 400
  86. """SSE流式传输新文件内容"""
  87. async def generate():
  88. n_value = 1
  89. max_n = 0 # 跟踪最大n值
  90. encoding = 'utf-8'
  91. while True:
  92. # 使用正则表达式匹配日志文件名格式:*****_LLM[n_value].log
  93. log_pattern = re.compile(fr"_LLM{n_value}\.log$")
  94. new_file = None
  95. # 获取匹配的日志文件
  96. for file in path.iterdir():
  97. if file.is_file() and log_pattern.search(file.name):
  98. new_file = file
  99. break
  100. # 检查是否存在完成文件
  101. completion_file_path = path / "complate_message.log"
  102. if completion_file_path.exists() and new_file is None:
  103. try:
  104. completion_file_content = completion_file_path.read_text(encoding=encoding)
  105. data = {
  106. 'filename': "complate_message.log",
  107. 'content': completion_file_content,
  108. 'n': max_n + 1,
  109. 'is_completion': True
  110. }
  111. data_json = json.dumps(data, ensure_ascii=False)
  112. print(f"发送完成文件: {data_json}")
  113. yield f"data: {data_json}\n\n"
  114. print("检测到完成文件: complate_message.log")
  115. break # 完成文件后退出循环
  116. except Exception as e:
  117. data = {
  118. 'filename': "complate_message.log",
  119. 'content': f'读取完成文件时出错: {str(e)}',
  120. 'n': max_n + 1,
  121. 'is_completion': True
  122. }
  123. data_json = json.dumps(data, ensure_ascii=False)
  124. yield f"data: {data_json}\n\n"
  125. break
  126. elif new_file:
  127. try:
  128. # 打开并读取日志文件
  129. file_content = new_file.read_text(encoding=encoding)
  130. # 将JSON字符串转换为Python对象
  131. file_info = json.loads(file_content)
  132. # 过滤日志内容,只保留需要的字段
  133. filtered_info = {
  134. 'model': file_info.get('model'),
  135. 'parameters': file_info.get('parameters'),
  136. 'current_message': file_info.get('current_message'),
  137. 'response': file_info.get('response')
  138. }
  139. # 移除空的字段
  140. filtered_info = {k: v for k, v in filtered_info.items() if v is not None}
  141. # 更新max_n
  142. if n_value > max_n:
  143. max_n = n_value
  144. # 发送SSE事件 - 修正:发送完整的data结构
  145. data = {
  146. 'filename': new_file.name,
  147. 'content': filtered_info,
  148. 'n': n_value,
  149. 'is_completion': False
  150. }
  151. data_json = json.dumps(data, ensure_ascii=False)
  152. yield f"data: {data_json}\n\n"
  153. # n_value加一
  154. n_value += 1
  155. except Exception as e:
  156. print(f"处理文件出错: {str(e)}")
  157. data = {
  158. 'error': f'处理文件错误: {str(e)}',
  159. 'filename': new_file.name
  160. }
  161. data_json = json.dumps(data, ensure_ascii=False) # 修正:定义data_json
  162. yield f"data: {data_json}\n\n"
  163. n_value += 1
  164. else:
  165. # 如果没有找到新文件,发送心跳保持连接
  166. yield "data: {\"heartbeat\": true}\n\n"
  167. # 每2秒检查一次
  168. await asyncio.sleep(2)
  169. return Response(
  170. generate(),
  171. mimetype='text/event-stream',
  172. headers={
  173. 'Cache-Control': 'no-cache',
  174. 'Connection': 'keep-alive',
  175. 'Access-Control-Allow-Origin': '*',
  176. }
  177. )
  178. except Exception as e:
  179. print(f"scan_logs顶层错误: {str(e)}")
  180. return jsonify({
  181. 'success': False,
  182. 'error': f'服务器错误: {str(e)}'
  183. }), 500
  184. # 获取file_create内文件内容
  185. @app.route('/refresh_files', methods=['POST'])
  186. async def refresh_files():
  187. try:
  188. files_data = []
  189. # 确保文件目录存在
  190. if not FILE_DIR.exists():
  191. return jsonify({'success': False, 'error': f'文件目录不存在: {FILE_DIR}'})
  192. # 递归扫描目录
  193. await scan_directory(FILE_DIR, files_data)
  194. return jsonify({'success': True, 'files': files_data})
  195. except Exception as e:
  196. return jsonify({'success': False, 'error': str(e)})
  197. async def scan_directory(directory: Path, files_data: list, depth=0):
  198. """递归扫描目录"""
  199. try:
  200. # 首先添加当前目录的信息
  201. if depth > 0: # 不显示根目录
  202. files_data.append({
  203. 'type': 'directory',
  204. 'name': directory.name,
  205. 'path': str(directory.relative_to(FILE_DIR)),
  206. 'depth': depth,
  207. 'content': f"目录: {directory.name}"
  208. })
  209. # 遍历目录中的所有项目
  210. for item in directory.iterdir():
  211. if item.is_dir():
  212. # 递归扫描子目录
  213. await scan_directory(item, files_data, depth + 1)
  214. elif item.is_file():
  215. # 处理文件
  216. await process_file(item, files_data, depth + 1)
  217. except PermissionError:
  218. files_data.append({
  219. 'type': 'error',
  220. 'name': directory.name,
  221. 'path': str(directory.relative_to(FILE_DIR)),
  222. 'depth': depth,
  223. 'content': '[权限不足,无法访问此目录]'
  224. })
  225. async def process_file(file_path: Path, files_data: list, depth: int):
  226. """处理单个文件"""
  227. try:
  228. # 检查文件是否为二进制文件
  229. if await is_binary_file(file_path):
  230. content = '[二进制文件,无法显示内容]'
  231. else:
  232. # 尝试读取文件内容
  233. try:
  234. async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
  235. content = await f.read()
  236. except UnicodeDecodeError:
  237. # 如果 UTF-8 解码失败,尝试其他编码
  238. try:
  239. async with aiofiles.open(file_path, 'r', encoding='latin-1') as f:
  240. content = await f.read()
  241. except Exception as e:
  242. content = f'[读取文件时出错: {str(e)}]'
  243. except Exception as e:
  244. content = f'[读取文件时出错: {str(e)}]'
  245. files_data.append({
  246. 'type': 'file',
  247. 'name': file_path.name,
  248. 'path': str(file_path.relative_to(FILE_DIR)),
  249. 'depth': depth,
  250. 'content': content
  251. })
  252. except Exception as e:
  253. files_data.append({
  254. 'type': 'error',
  255. 'name': file_path.name,
  256. 'path': str(file_path.relative_to(FILE_DIR)),
  257. 'depth': depth,
  258. 'content': f'[处理文件时出错: {str(e)}]'
  259. })
  260. async def is_binary_file(file_path: Path) -> bool:
  261. """检查文件是否为二进制文件"""
  262. try:
  263. # 检查文件扩展名(常见的文本文件扩展名)
  264. text_extensions = {'.txt', '.py', '.js', '.html', '.css', '.json', '.xml',
  265. '.csv', '.md', '.rst', '.yml', '.yaml', '.ini', '.cfg',
  266. '.conf', '.log', '.sh', '.bat', '.ps1', '.java', '.c',
  267. '.cpp', '.h', '.hpp', '.cs', '.php', '.rb', '.go', '.rs'}
  268. if file_path.suffix.lower() in text_extensions:
  269. return False
  270. # 进一步检查文件内容
  271. async with aiofiles.open(file_path, 'rb') as f:
  272. chunk = await f.read(1024)
  273. if not chunk:
  274. return False
  275. # 检查是否包含空字节(二进制文件的标志)
  276. if b'\0' in chunk:
  277. return True
  278. # 检查文本字符比例
  279. text_characters = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
  280. non_text = chunk.translate(None, text_characters)
  281. return float(len(non_text)) / len(chunk) > 0.3
  282. except Exception:
  283. return True
  284. def is_text_file(file_path):
  285. """简单判断是否为文本文件"""
  286. text_extensions = {'.txt', '.log', '.py', '.js', '.html', '.css', '.json', '.xml', '.csv', '.md'}
  287. return file_path.suffix.lower() in text_extensions
  288. if __name__ == '__main__':
  289. print("=== SCRIPT START ===")
  290. app.run(debug=True, host="0.0.0.0", port=5001)
  291. print("=== SCRIPT END ===")