api_server.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # vim:fenc=utf-8
  4. import time
  5. import logging
  6. import werkzeug.exceptions
  7. from flask import Flask, request, jsonify
  8. from argparse import ArgumentParser
  9. from sqlalchemy.orm import sessionmaker
  10. from pqai_agent import configs
  11. from pqai_agent import chat_service, prompt_templates
  12. from pqai_agent.logging import logger, setup_root_logger
  13. from pqai_agent.toolkit import global_tool_map
  14. from pqai_agent.data_models.agent_configuration import AgentConfiguration
  15. from pqai_agent.data_models.service_module import ServiceModule
  16. from pqai_agent.history_dialogue_service import HistoryDialogueService
  17. from pqai_agent.user_manager import MySQLUserManager, MySQLUserRelationManager
  18. from pqai_agent.utils.db_utils import create_ai_agent_db_engine
  19. from pqai_agent.utils.prompt_utils import format_agent_profile, format_user_profile
  20. from pqai_agent_server.const import AgentApiConst
  21. from pqai_agent_server.models import MySQLSessionManager
  22. import pqai_agent_server.utils
  23. from pqai_agent_server.utils import wrap_response
  24. from pqai_agent_server.utils import (
  25. run_extractor_prompt,
  26. run_chat_prompt,
  27. run_response_type_prompt,
  28. )
  29. app = Flask('agent_api_server')
  30. const = AgentApiConst()
  31. @app.route('/api/listStaffs', methods=['GET'])
  32. def list_staffs():
  33. staff_data = app.user_relation_manager.list_staffs()
  34. return wrap_response(200, data=staff_data)
  35. @app.route('/api/getStaffProfile', methods=['GET'])
  36. def get_staff_profile():
  37. staff_id = request.args['staff_id']
  38. profile = app.user_manager.get_staff_profile(staff_id)
  39. if not profile:
  40. return wrap_response(404, msg='staff not found')
  41. else:
  42. return wrap_response(200, data=profile)
  43. @app.route('/api/getUserProfile', methods=['GET'])
  44. def get_user_profile():
  45. user_id = request.args['user_id']
  46. profile = app.user_manager.get_user_profile(user_id)
  47. if not profile:
  48. resp = {
  49. 'code': 404,
  50. 'msg': 'user not found'
  51. }
  52. else:
  53. resp = {
  54. 'code': 200,
  55. 'msg': 'success',
  56. 'data': profile
  57. }
  58. return jsonify(resp)
  59. @app.route('/api/listUsers', methods=['GET'])
  60. def list_users():
  61. user_name = request.args.get('user_name', None)
  62. user_union_id = request.args.get('user_union_id', None)
  63. if not user_name and not user_union_id:
  64. resp = {
  65. 'code': 400,
  66. 'msg': 'user_name or user_union_id is required'
  67. }
  68. return jsonify(resp)
  69. data = app.user_manager.list_users(user_name=user_name, user_union_id=user_union_id)
  70. return jsonify({'code': 200, 'data': data})
  71. @app.route('/api/getDialogueHistory', methods=['GET'])
  72. def get_dialogue_history():
  73. staff_id = request.args['staff_id']
  74. user_id = request.args['user_id']
  75. recent_minutes = int(request.args.get('recent_minutes', 1440))
  76. dialogue_history = app.history_dialogue_service.get_dialogue_history(staff_id, user_id, recent_minutes)
  77. return jsonify({'code': 200, 'data': dialogue_history})
  78. @app.route('/api/listModels', methods=['GET'])
  79. def list_models():
  80. models = {
  81. "deepseek-chat": chat_service.VOLCENGINE_MODEL_DEEPSEEK_V3,
  82. "gpt-4o": chat_service.OPENAI_MODEL_GPT_4o,
  83. "gpt-4o-mini": chat_service.OPENAI_MODEL_GPT_4o_mini,
  84. "doubao-pro-32k": chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
  85. "doubao-pro-1.5": chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
  86. "doubao-1.5-vision-pro": chat_service.VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
  87. }
  88. ret_data = [
  89. {
  90. 'model_type': 'openai_compatible',
  91. 'model_name': model_name,
  92. 'display_name': model_display_name
  93. }
  94. for model_display_name, model_name in models.items()
  95. ]
  96. return wrap_response(200, data=ret_data)
  97. @app.route('/api/listScenes', methods=['GET'])
  98. def list_scenes():
  99. scenes = [
  100. {'scene': 'greeting', 'display_name': '问候'},
  101. {'scene': 'chitchat', 'display_name': '闲聊'},
  102. {'scene': 'profile_extractor', 'display_name': '画像提取'},
  103. {'scene': 'response_type_detector', 'display_name': '回复模态判断'},
  104. {'scene': 'custom_debugging', 'display_name': '自定义调试场景'}
  105. ]
  106. return wrap_response(200, data=scenes)
  107. @app.route('/api/getBasePrompt', methods=['GET'])
  108. def get_base_prompt():
  109. scene = request.args['scene']
  110. prompt_map = {
  111. 'greeting': prompt_templates.GENERAL_GREETING_PROMPT,
  112. 'chitchat': prompt_templates.CHITCHAT_PROMPT_COZE,
  113. 'profile_extractor': prompt_templates.USER_PROFILE_EXTRACT_PROMPT_V2,
  114. 'response_type_detector': prompt_templates.RESPONSE_TYPE_DETECT_PROMPT,
  115. 'custom_debugging': '',
  116. }
  117. model_map = {
  118. 'greeting': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
  119. 'chitchat': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
  120. 'profile_extractor': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
  121. 'response_type_detector': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
  122. 'custom_debugging': chat_service.VOLCENGINE_BOT_DEEPSEEK_V3_SEARCH
  123. }
  124. if scene not in prompt_map:
  125. return wrap_response(404, msg='scene not found')
  126. data = {
  127. 'model_name': model_map[scene],
  128. 'content': prompt_map[scene]
  129. }
  130. return wrap_response(200, data=data)
  131. @app.route('/api/runPrompt', methods=['POST'])
  132. def run_prompt():
  133. try:
  134. req_data = request.json
  135. logger.debug(req_data)
  136. scene = req_data['scene']
  137. if scene == 'profile_extractor':
  138. response = run_extractor_prompt(req_data)
  139. return wrap_response(200, data=response)
  140. elif scene == 'response_type_detector':
  141. response = run_response_type_prompt(req_data)
  142. return wrap_response(200, data=response.choices[0].message.content)
  143. else:
  144. response = run_chat_prompt(req_data)
  145. return wrap_response(200, data=response.choices[0].message.content)
  146. except Exception as e:
  147. logger.error(e)
  148. return wrap_response(500, msg='Error: {}'.format(e))
  149. @app.route('/api/formatForPrompt', methods=['POST'])
  150. def format_data_for_prompt():
  151. try:
  152. req_data = request.json
  153. content = req_data['content']
  154. format_type = req_data['format_type']
  155. if format_type == 'staff_profile':
  156. if not isinstance(content, dict):
  157. return wrap_response(400, msg='staff_profile should be a dict')
  158. response = format_agent_profile(content)
  159. elif format_type == 'user_profile':
  160. if not isinstance(content, dict):
  161. return wrap_response(400, msg='user_profile should be a dict')
  162. response = format_user_profile(content)
  163. elif format_type == 'dialogue':
  164. if not isinstance(content, list):
  165. return wrap_response(400, msg='dialogue should be a list')
  166. from pqai_agent_server.utils.prompt_util import format_dialogue_history
  167. response = format_dialogue_history(content)
  168. else:
  169. return wrap_response(400, msg='Invalid format_type')
  170. return wrap_response(200, data=response)
  171. except Exception as e:
  172. logger.error(e)
  173. return wrap_response(500, msg='Error: {}'.format(e))
  174. @app.route("/api/healthCheck", methods=["GET"])
  175. def health_check():
  176. return wrap_response(200, msg="OK")
  177. @app.route("/api/getStaffSessionSummary", methods=["GET"])
  178. def get_staff_session_summary():
  179. staff_id = request.args.get("staff_id")
  180. status = request.args.get("status", const.DEFAULT_STAFF_STATUS)
  181. page_id = request.args.get("page_id", const.DEFAULT_PAGE_ID)
  182. page_size = request.args.get("page_size", const.DEFAULT_PAGE_SIZE)
  183. # check params
  184. try:
  185. page_id = int(page_id)
  186. page_size = int(page_size)
  187. status = int(status)
  188. except Exception as e:
  189. return wrap_response(404, msg="Invalid parameter: {}".format(e))
  190. staff_session_summary = app.session_manager.get_staff_sessions_summary(
  191. staff_id, page_id, page_size, status
  192. )
  193. if not staff_session_summary:
  194. return wrap_response(404, msg="staff not found")
  195. else:
  196. return wrap_response(200, data=staff_session_summary)
  197. @app.route("/api/getStaffSessionList", methods=["GET"])
  198. def get_staff_session_list():
  199. staff_id = request.args.get("staff_id")
  200. if not staff_id:
  201. return wrap_response(404, msg="staff_id is required")
  202. page_size = request.args.get("page_size", const.DEFAULT_PAGE_SIZE)
  203. page_id = request.args.get("page_id", const.DEFAULT_PAGE_ID)
  204. # check params
  205. try:
  206. page_id = int(page_id)
  207. page_size = int(page_size)
  208. except Exception as e:
  209. return wrap_response(404, msg="Invalid parameter: {}".format(e))
  210. staff_session_list = app.session_manager.get_staff_session_list(staff_id, page_id, page_size)
  211. if not staff_session_list:
  212. return wrap_response(404, msg="staff not found")
  213. return wrap_response(200, data=staff_session_list)
  214. @app.route("/api/getStaffList", methods=["GET"])
  215. def get_staff_list():
  216. page_size = request.args.get("page_size", const.DEFAULT_PAGE_SIZE)
  217. page_id = request.args.get("page_id", const.DEFAULT_PAGE_ID)
  218. # check params
  219. try:
  220. page_id = int(page_id)
  221. page_size = int(page_size)
  222. except Exception as e:
  223. return wrap_response(404, msg="Invalid parameter: {}".format(e))
  224. staff_list = app.user_manager.get_staff_list(page_id, page_size)
  225. if not staff_list:
  226. return wrap_response(404, msg="staff not found")
  227. return wrap_response(200, data=staff_list)
  228. @app.route("/api/getConversationList", methods=["GET"])
  229. def get_conversation_list():
  230. """
  231. 获取staff && user 私聊对话列表
  232. :return:
  233. """
  234. staff_id = request.args.get("staff_id")
  235. user_id = request.args.get("user_id")
  236. if not staff_id or not user_id:
  237. return wrap_response(404, msg="staff_id and user_id are required")
  238. page = request.args.get("page")
  239. response = app.session_manager.get_conversation_list(staff_id, user_id, page, const.DEFAULT_CONVERSATION_SIZE)
  240. return wrap_response(200, data=response)
  241. @app.route("/api/sendMessage", methods=["POST"])
  242. def send_message():
  243. return wrap_response(200, msg="暂不实现功能")
  244. @app.route("/api/quitHumanIntervention", methods=["POST"])
  245. def quit_human_intervention():
  246. """
  247. 退出人工介入状态
  248. :return:
  249. """
  250. req_data = request.json
  251. staff_id = req_data["staff_id"]
  252. user_id = req_data["user_id"]
  253. if not user_id or not staff_id:
  254. return wrap_response(404, msg="user_id and staff_id are required")
  255. if pqai_agent_server.utils.common.quit_human_intervention(user_id, staff_id):
  256. return wrap_response(200, msg="success")
  257. else:
  258. return wrap_response(500, msg="error")
  259. @app.route("/api/enterHumanIntervention", methods=["POST"])
  260. def enter_human_intervention():
  261. """
  262. 进入人工介入状态
  263. :return:
  264. """
  265. req_data = request.json
  266. staff_id = req_data["staff_id"]
  267. user_id = req_data["user_id"]
  268. if not user_id or not staff_id:
  269. return wrap_response(404, msg="user_id and staff_id are required")
  270. if pqai_agent_server.utils.common.enter_human_intervention(user_id, staff_id):
  271. return wrap_response(200, msg="success")
  272. else:
  273. return wrap_response(500, msg="error")
  274. ## Agent管理接口
  275. @app.route("/api/getNativeAgentList", methods=["GET"])
  276. def get_native_agent_list():
  277. """
  278. 获取所有的Agent列表
  279. :return:
  280. """
  281. page = request.args.get('page', 1)
  282. page_size = request.args.get('page_size', 50)
  283. create_user = request.args.get('create_user', None)
  284. update_user = request.args.get('update_user', None)
  285. offset = (int(page) - 1) * int(page_size)
  286. with app.session_maker() as session:
  287. query = session.query(AgentConfiguration) \
  288. .filter(AgentConfiguration.is_delete == 0)
  289. if create_user:
  290. query = query.filter(AgentConfiguration.create_user == create_user)
  291. if update_user:
  292. query = query.filter(AgentConfiguration.update_user == update_user)
  293. query = query.offset(offset).limit(int(page_size))
  294. data = query.all()
  295. ret_data = [
  296. {
  297. 'id': agent.id,
  298. 'name': agent.name,
  299. 'display_name': agent.display_name,
  300. 'type': agent.type,
  301. 'execution_model': agent.execution_model,
  302. 'create_user': agent.create_user,
  303. 'update_user': agent.update_user,
  304. 'create_time': agent.create_time.strftime('%Y-%m-%d %H:%M:%S'),
  305. 'update_time': agent.update_time.strftime('%Y-%m-%d %H:%M:%S')
  306. }
  307. for agent in data
  308. ]
  309. return wrap_response(200, data=ret_data)
  310. @app.route("/api/getNativeAgentConfiguration", methods=["GET"])
  311. def get_native_agent_configuration():
  312. """
  313. 获取指定Agent的配置
  314. :return:
  315. """
  316. agent_id = request.args.get('agent_id')
  317. if not agent_id:
  318. return wrap_response(404, msg='agent_id is required')
  319. with app.session_maker() as session:
  320. agent = session.query(AgentConfiguration).filter(AgentConfiguration.id == agent_id).first()
  321. if not agent:
  322. return wrap_response(404, msg='Agent not found')
  323. data = {
  324. 'id': agent.id,
  325. 'name': agent.name,
  326. 'display_name': agent.display_name,
  327. 'type': agent.type,
  328. 'execution_model': agent.execution_model,
  329. 'system_prompt': agent.system_prompt,
  330. 'task_prompt': agent.task_prompt,
  331. 'tools': agent.tools,
  332. 'sub_agents': agent.sub_agents,
  333. 'extra_params': agent.extra_params,
  334. 'create_time': agent.create_time.strftime('%Y-%m-%d %H:%M:%S'),
  335. 'update_time': agent.update_time.strftime('%Y-%m-%d %H:%M:%S')
  336. }
  337. return wrap_response(200, data=data)
  338. @app.route("/api/saveNativeAgentConfiguration", methods=["POST"])
  339. def save_native_agent_configuration():
  340. """
  341. 保存Agent配置
  342. :return:
  343. """
  344. req_data = request.json
  345. agent_id = req_data.get('agent_id', None)
  346. name = req_data.get('name')
  347. display_name = req_data.get('display_name', None)
  348. type_ = req_data.get('type', 0)
  349. execution_model = req_data.get('execution_model', None)
  350. system_prompt = req_data.get('system_prompt', None)
  351. task_prompt = req_data.get('task_prompt', None)
  352. tools = req_data.get('tools', [])
  353. sub_agents = req_data.get('sub_agents', [])
  354. extra_params = req_data.get('extra_params', {})
  355. if not name:
  356. return wrap_response(400, msg='name is required')
  357. with app.session_maker() as session:
  358. if agent_id:
  359. agent_id = int(agent_id)
  360. agent = session.query(AgentConfiguration).filter(AgentConfiguration.id == agent_id).first()
  361. if not agent:
  362. return wrap_response(404, msg='Agent not found')
  363. agent.name = name
  364. agent.display_name = display_name
  365. agent.type = type_
  366. agent.execution_model = execution_model
  367. agent.system_prompt = system_prompt
  368. agent.task_prompt = task_prompt
  369. agent.tools = tools
  370. agent.sub_agents = sub_agents
  371. agent.extra_params = extra_params
  372. else:
  373. agent = AgentConfiguration(
  374. name=name,
  375. display_name=display_name,
  376. type=type_,
  377. execution_model=execution_model,
  378. system_prompt=system_prompt,
  379. task_prompt=task_prompt,
  380. tools=tools,
  381. sub_agents=sub_agents,
  382. extra_params=extra_params
  383. )
  384. session.add(agent)
  385. session.commit()
  386. return wrap_response(200, msg='Agent configuration saved successfully', data={'id': agent.id})
  387. @app.route("/api/getModuleList", methods=["GET"])
  388. def get_module_list():
  389. """
  390. 获取所有的模块列表
  391. :return:
  392. """
  393. with app.session_maker() as session:
  394. query = session.query(ServiceModule) \
  395. .filter(ServiceModule.is_delete == 0)
  396. data = query.all()
  397. ret_data = [
  398. {
  399. 'id': module.id,
  400. 'name': module.name,
  401. 'display_name': module.display_name,
  402. 'default_agent_type': module.default_agent_type,
  403. 'default_agent_id': module.default_agent_id,
  404. 'create_time': module.create_time.strftime('%Y-%m-%d %H:%M:%S'),
  405. 'update_time': module.update_time.strftime('%Y-%m-%d %H:%M:%S')
  406. }
  407. for module in data
  408. ]
  409. return wrap_response(200, data=ret_data)
  410. @app.route("/api/getModuleConfiguration", methods=["GET"])
  411. def get_module_configuration():
  412. """
  413. 获取指定模块的配置
  414. :return:
  415. """
  416. module_id = request.args.get('module_id')
  417. if not module_id:
  418. return wrap_response(404, msg='module_id is required')
  419. with app.session_maker() as session:
  420. module = session.query(ServiceModule).filter(ServiceModule.id == module_id).first()
  421. if not module:
  422. return wrap_response(404, msg='Module not found')
  423. data = {
  424. 'id': module.id,
  425. 'name': module.name,
  426. 'display_name': module.display_name,
  427. 'default_agent_type': module.default_agent_type,
  428. 'default_agent_id': module.default_agent_id,
  429. 'create_time': module.create_time.strftime('%Y-%m-%d %H:%M:%S'),
  430. 'updated_time': module.updated_time.strftime('%Y-%m-%d %H:%M:%S')
  431. }
  432. return wrap_response(200, data=data)
  433. @app.route("/api/saveModuleConfiguration", methods=["POST"])
  434. def save_module_configuration():
  435. """
  436. 保存模块配置
  437. :return:
  438. """
  439. req_data = request.json
  440. module_id = req_data.get('module_id', None)
  441. name = req_data.get('name')
  442. display_name = req_data.get('display_name', None)
  443. default_agent_type = req_data.get('default_agent_type', 0)
  444. default_agent_id = req_data.get('default_agent_id', None)
  445. if not name:
  446. return wrap_response(400, msg='name is required')
  447. with app.session_maker() as session:
  448. if module_id:
  449. module_id = int(module_id)
  450. module = session.query(ServiceModule).filter(ServiceModule.id == module_id).first()
  451. if not module:
  452. return wrap_response(404, msg='Module not found')
  453. module.name = name
  454. module.display_name = display_name
  455. module.default_agent_type = default_agent_type
  456. module.default_agent_id = default_agent_id
  457. else:
  458. module = ServiceModule(
  459. name=name,
  460. display_name=display_name,
  461. default_agent_type=default_agent_type,
  462. default_agent_id=default_agent_id
  463. )
  464. session.add(module)
  465. session.commit()
  466. return wrap_response(200, msg='Module configuration saved successfully', data={'id': module.id})
  467. @app.route("/api/getToolList", methods=["GET"])
  468. def get_tool_list():
  469. """
  470. 获取所有的工具列表
  471. :return:
  472. """
  473. tools = []
  474. for tool_name, tool in global_tool_map.items():
  475. tools.append({
  476. 'name': tool_name,
  477. 'description': tool.get_function_description(),
  478. 'parameters': tool.parameters if hasattr(tool, 'parameters') else {}
  479. })
  480. return wrap_response(200, data=tools)
  481. @app.route("/api/getModuleAgentTypes", methods=["GET"])
  482. def get_agent_types():
  483. """
  484. 获取所有的Agent类型
  485. :return:
  486. """
  487. agent_types = [
  488. {'type': 0, 'display_name': '原生'},
  489. {'type': 1, 'display_name': 'Coze'}
  490. ]
  491. return wrap_response(200, data=agent_types)
  492. @app.errorhandler(werkzeug.exceptions.BadRequest)
  493. def handle_bad_request(e):
  494. logger.error(e)
  495. return wrap_response(400, msg='Bad Request: {}'.format(e.description))
  496. if __name__ == '__main__':
  497. parser = ArgumentParser()
  498. parser.add_argument('--prod', action='store_true')
  499. parser.add_argument('--host', default='127.0.0.1')
  500. parser.add_argument('--port', type=int, default=8083)
  501. parser.add_argument('--log-level', default='INFO')
  502. args = parser.parse_args()
  503. config = configs.get()
  504. logging_level = logging.getLevelName(args.log_level)
  505. setup_root_logger(level=logging_level, logfile_name='agent_api_server.log')
  506. # set db config
  507. agent_db_config = config['database']['ai_agent']
  508. growth_db_config = config['database']['growth']
  509. user_db_config = config['storage']['user']
  510. staff_db_config = config['storage']['staff']
  511. agent_state_db_config = config['storage']['agent_state']
  512. chat_history_db_config = config['storage']['chat_history']
  513. # init user manager
  514. user_manager = MySQLUserManager(agent_db_config, user_db_config['table'], staff_db_config['table'])
  515. app.user_manager = user_manager
  516. # init session manager
  517. session_manager = MySQLSessionManager(
  518. db_config=agent_db_config,
  519. staff_table=staff_db_config['table'],
  520. user_table=user_db_config['table'],
  521. agent_state_table=agent_state_db_config['table'],
  522. chat_history_table=chat_history_db_config['table']
  523. )
  524. app.session_manager = session_manager
  525. agent_db_engine = create_ai_agent_db_engine()
  526. app.session_maker = sessionmaker(bind=agent_db_engine)
  527. wecom_db_config = config['storage']['user_relation']
  528. user_relation_manager = MySQLUserRelationManager(
  529. agent_db_config, growth_db_config,
  530. config['storage']['staff']['table'],
  531. user_db_config['table'],
  532. wecom_db_config['table']['staff'],
  533. wecom_db_config['table']['relation'],
  534. wecom_db_config['table']['user']
  535. )
  536. app.user_relation_manager = user_relation_manager
  537. app.history_dialogue_service = HistoryDialogueService(
  538. config['storage']['history_dialogue']['api_base_url']
  539. )
  540. app.run(debug=not args.prod, host=args.host, port=args.port)