chainlit_app.py 12 KB

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