app.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import chainlit as cl
  2. from chainlit.input_widget import Select
  3. from vanna_llm_factory import create_vanna_instance
  4. import os
  5. # vn.set_api_key(os.environ['VANNA_API_KEY'])
  6. # vn.set_model('chinook')
  7. # vn.connect_to_sqlite('https://vanna.ai/Chinook.sqlite')
  8. vn = create_vanna_instance()
  9. @cl.set_chat_profiles
  10. async def chat_profile():
  11. return [
  12. cl.ChatProfile(
  13. name="Vanna助手",
  14. markdown_description="基于Vanna的智能数据库查询助手,支持自然语言转SQL查询和数据可视化",
  15. icon="./public/avatars/huoche.png",
  16. # 备用在线图标,如果本地图标不显示可以取消注释下面的行
  17. #icon="https://raw.githubusercontent.com/tabler/tabler-icons/master/icons/database.svg",
  18. ),
  19. ]
  20. @cl.step(language="sql", name="Vanna")
  21. async def gen_query(human_query: str):
  22. """
  23. 安全的SQL生成函数,处理所有可能的异常
  24. """
  25. try:
  26. print(f"[INFO] 开始生成SQL: {human_query}")
  27. sql_query = vn.generate_sql(human_query)
  28. if sql_query is None:
  29. print(f"[WARNING] generate_sql 返回 None")
  30. return None
  31. if sql_query.strip() == "":
  32. print(f"[WARNING] generate_sql 返回空字符串")
  33. return None
  34. # 检查是否返回了错误信息而非SQL
  35. if "insufficient context" in sql_query.lower() or "无法生成" in sql_query or "sorry" in sql_query.lower():
  36. print(f"[WARNING] LLM返回无法生成SQL的消息: {sql_query}")
  37. return None
  38. print(f"[SUCCESS] SQL生成成功: {sql_query}")
  39. return sql_query
  40. except Exception as e:
  41. print(f"[ERROR] gen_query 异常: {str(e)}")
  42. print(f"[ERROR] 异常类型: {type(e).__name__}")
  43. return None
  44. @cl.step(name="Vanna")
  45. async def execute_query(query):
  46. current_step = cl.context.current_step
  47. try:
  48. if query is None or query.strip() == "":
  49. current_step.output = "SQL查询为空,无法执行"
  50. return None
  51. print(f"[INFO] 执行SQL: {query}")
  52. df = vn.run_sql(query)
  53. if df is None or df.empty:
  54. current_step.output = "查询执行成功,但没有返回数据"
  55. return None
  56. current_step.output = df.head().to_markdown(index=False)
  57. print(f"[SUCCESS] SQL执行成功,返回 {len(df)} 行数据")
  58. return df
  59. except Exception as e:
  60. error_msg = f"SQL执行失败: {str(e)}"
  61. print(f"[ERROR] {error_msg}")
  62. current_step.output = error_msg
  63. return None
  64. @cl.step(name="Plot", language="python")
  65. async def plot(human_query, sql, df):
  66. current_step = cl.context.current_step
  67. try:
  68. if df is None or df.empty:
  69. current_step.output = "无数据可用于生成图表"
  70. return None
  71. plotly_code = vn.generate_plotly_code(question=human_query, sql=sql, df=df)
  72. fig = vn.get_plotly_figure(plotly_code=plotly_code, df=df)
  73. current_step.output = plotly_code
  74. return fig
  75. except Exception as e:
  76. error_msg = f"图表生成失败: {str(e)}"
  77. print(f"[ERROR] {error_msg}")
  78. current_step.output = error_msg
  79. return None
  80. @cl.step(name="LLM Chat")
  81. async def llm_chat(human_query: str, context: str = None):
  82. """直接与LLM对话,用于非数据库相关问题或SQL生成失败的情况"""
  83. current_step = cl.context.current_step
  84. try:
  85. print(f"[INFO] 使用LLM直接对话: {human_query}")
  86. # 构建更智能的提示词
  87. if context:
  88. # 有上下文时(SQL生成失败)
  89. system_message = (
  90. "你是一个友好的数据库查询助手。用户刚才的问题无法生成有效的SQL查询,"
  91. "可能是因为相关数据不在数据库中,或者问题需要重新表述。"
  92. "请友好地回复用户,解释可能的原因,并建议如何重新表述问题。"
  93. )
  94. user_message = f"用户问题:{human_query}\n\n{context}"
  95. else:
  96. # 无上下文时(一般性对话)
  97. system_message = (
  98. "你是一个友好的AI助手。你主要专注于数据库查询,"
  99. "但也可以回答一般性问题。如果用户询问数据相关问题,"
  100. "请建议他们重新表述以便进行SQL查询。"
  101. )
  102. user_message = human_query
  103. # 使用我们新增的 chat_with_llm 方法
  104. if hasattr(vn, 'chat_with_llm'):
  105. response = vn.chat_with_llm(user_message)
  106. else:
  107. # 回退方案:使用 submit_prompt
  108. if hasattr(vn, 'submit_prompt'):
  109. messages = [
  110. {"role": "system", "content": system_message},
  111. {"role": "user", "content": user_message}
  112. ]
  113. response = vn.submit_prompt(messages)
  114. else:
  115. # 最终回退方案
  116. response = f"我理解您的问题:'{human_query}'。我主要专注于数据库查询,如果您有数据相关的问题,请尝试重新表述,我可以帮您生成SQL查询并分析数据。"
  117. current_step.output = response
  118. return response
  119. except Exception as e:
  120. error_msg = f"LLM对话失败: {str(e)}"
  121. print(f"[ERROR] {error_msg}")
  122. fallback_response = f"抱歉,我暂时无法回答您的问题:'{human_query}'。请稍后重试,或者尝试重新表述您的问题。"
  123. current_step.output = fallback_response
  124. return fallback_response
  125. def is_database_related_query(query: str) -> bool:
  126. """
  127. 判断查询是否与数据库相关(保留函数用于调试和可能的后续优化,但不在主流程中使用)
  128. """
  129. # 数据库相关关键词
  130. db_keywords = [
  131. # 中文关键词
  132. '查询', '数据', '表', '统计', '分析', '汇总', '计算', '查找', '显示',
  133. '列出', '多少', '总计', '平均', '最大', '最小', '排序', '筛选',
  134. '销售', '订单', '客户', '产品', '用户', '记录', '报表',
  135. # 英文关键词
  136. 'select', 'count', 'sum', 'avg', 'max', 'min', 'table', 'data',
  137. 'query', 'database', 'records', 'show', 'list', 'find', 'search'
  138. ]
  139. # 非数据库关键词
  140. non_db_keywords = [
  141. '天气', '新闻', '今天', '明天', '时间', '日期', '你好', '谢谢',
  142. '什么是', '如何', '为什么', '帮助', '介绍', '说明',
  143. 'weather', 'news', 'today', 'tomorrow', 'time', 'hello', 'thank',
  144. 'what is', 'how to', 'why', 'help', 'introduce'
  145. ]
  146. query_lower = query.lower()
  147. # 检查是否包含非数据库关键词
  148. for keyword in non_db_keywords:
  149. if keyword in query_lower:
  150. return False
  151. # 检查是否包含数据库关键词
  152. for keyword in db_keywords:
  153. if keyword in query_lower:
  154. return True
  155. # 默认认为是数据库相关(保守策略)
  156. return True
  157. @cl.step(type="run", name="Vanna")
  158. async def chain(human_query: str):
  159. """
  160. 主要的处理链 - 方案二:尝试-回退策略
  161. 对所有查询都先尝试生成SQL,如果失败则自动fallback到LLM对话
  162. """
  163. try:
  164. # 第一步:直接尝试生成SQL(不做预判断)
  165. print(f"[INFO] 尝试为查询生成SQL: {human_query}")
  166. sql_query = await gen_query(human_query)
  167. if sql_query is None or sql_query.strip() == "":
  168. # SQL生成失败,自动fallback到LLM对话
  169. print(f"[INFO] SQL生成失败,自动fallback到LLM对话")
  170. # 构建上下文信息
  171. context = (
  172. "我尝试为您的问题生成SQL查询,但没有成功。这可能是因为:\n"
  173. "1. 相关数据不在当前数据库中\n"
  174. "2. 问题需要更具体的表述\n"
  175. "3. 涉及的表或字段不在我的训练数据中"
  176. )
  177. response = await llm_chat(human_query, context)
  178. await cl.Message(content=response, author="Vanna助手").send()
  179. return
  180. # 第二步:SQL生成成功,执行查询
  181. print(f"[INFO] 成功生成SQL,开始执行: {sql_query}")
  182. df = await execute_query(sql_query)
  183. if df is None or df.empty:
  184. # SQL执行失败或无结果,提供详细信息并建议
  185. error_context = (
  186. f"我为您生成了SQL查询,但执行后没有找到相关数据。\n\n"
  187. f"生成的SQL:\n```sql\n{sql_query}\n```\n\n"
  188. f"这可能是因为查询条件太严格,或者数据库中暂时没有符合条件的记录。"
  189. )
  190. response = await llm_chat(
  191. f"用户询问:{human_query},但SQL查询没有返回数据。请给出建议。",
  192. error_context
  193. )
  194. await cl.Message(
  195. content=f"{error_context}\n\n{response}",
  196. author="Vanna助手"
  197. ).send()
  198. return
  199. # 第三步:成功获取数据,生成图表和返回结果
  200. print(f"[INFO] 成功获取数据,生成图表")
  201. fig = await plot(human_query, sql_query, df)
  202. # 创建返回元素
  203. elements = [
  204. cl.Text(name="data_table", content=df.to_markdown(index=False), display="inline")
  205. ]
  206. if fig is not None:
  207. elements.append(cl.Plotly(name="chart", figure=fig, display="inline"))
  208. await cl.Message(
  209. content=f"查询完成!以下是关于 '{human_query}' 的分析结果:",
  210. elements=elements,
  211. author="Vanna助手"
  212. ).send()
  213. except Exception as e:
  214. # 最外层异常处理 - 最终fallback
  215. error_msg = f"处理请求时发生意外错误: {str(e)}"
  216. print(f"[ERROR] {error_msg}")
  217. print(f"[ERROR] 异常类型: {type(e).__name__}")
  218. # 使用LLM生成友好的错误回复
  219. try:
  220. final_response = await llm_chat(
  221. f"系统遇到技术问题,用户询问:{human_query},请提供友好的回复和建议。"
  222. )
  223. await cl.Message(
  224. content=f"抱歉,系统遇到了一些技术问题。\n\n{final_response}",
  225. author="Vanna助手"
  226. ).send()
  227. except:
  228. # 如果连LLM都失败了,使用硬编码回复
  229. await cl.Message(
  230. content=f"抱歉,系统暂时遇到技术问题,请稍后重试。如果问题持续存在,请检查网络连接或联系技术支持。",
  231. author="Vanna助手"
  232. ).send()
  233. @cl.on_message
  234. async def main(message: cl.Message):
  235. await chain(message.content)
  236. @cl.on_chat_start
  237. async def on_chat_start():
  238. # 发送中文欢迎消息
  239. welcome_message = """
  240. 🎉 **欢迎使用智能数据库查询助手!**
  241. 我可以帮助您:
  242. - 🔍 将自然语言问题转换为SQL查询
  243. - 📊 执行数据库查询并展示结果
  244. - 📈 生成数据可视化图表
  245. - 💬 回答一般性问题
  246. 请直接输入您的问题,例如:
  247. - "交易次数最多的前5位客户是谁?"
  248. - "查看过去30天的交易趋势"
  249. - "你好,今天天气怎么样?"
  250. 让我们开始吧!✨
  251. """
  252. await cl.Message(
  253. content=welcome_message,
  254. author="Vanna助手"
  255. ).send()