sse_app.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  1. #!/usr/bin/env python3
  2. """
  3. SSE + FastAPI 实时对话窗口
  4. 用户输入消息,后端随机生成回复
  5. """
  6. import asyncio
  7. import json
  8. import time
  9. import random
  10. import os
  11. from datetime import datetime
  12. from typing import List, Dict, Any
  13. from concurrent.futures import ThreadPoolExecutor
  14. from fastapi import FastAPI, Request
  15. from fastapi.responses import StreamingResponse, HTMLResponse
  16. import uvicorn
  17. import dotenv
  18. # Agent相关导入
  19. try:
  20. from agno.agent import Agent
  21. from agno.memory.v2.db.sqlite import SqliteMemoryDb
  22. from agno.memory.v2.memory import Memory
  23. from agno.models.openai import OpenAILike
  24. from agno.storage.sqlite import SqliteStorage
  25. AGENT_AVAILABLE = True
  26. print("✅ Agent依赖已加载")
  27. except ImportError as e:
  28. print(f"⚠️ Agent依赖未安装: {e}")
  29. AGENT_AVAILABLE = False
  30. # 加载环境变量
  31. dotenv.load_dotenv()
  32. # 移除消息队列,现在使用直接SSE流
  33. # 全局Agent实例和Memory
  34. global_agent = None
  35. global_memory = None
  36. global_user_id = "user_web_chat"
  37. # 线程池执行器
  38. thread_executor = ThreadPoolExecutor(max_workers=2)
  39. # Agent工具函数
  40. def get_current_time():
  41. """获取当前时间"""
  42. return f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
  43. def get_user_info():
  44. """获取用户信息"""
  45. return "用户信息: 当前用户正在使用web实时对话窗口"
  46. async def call_agent_async(agent, message, user_id):
  47. """异步调用Agent,避免阻塞事件循环"""
  48. loop = asyncio.get_event_loop()
  49. try:
  50. print(f"🔄 开始异步调用Agent... (消息: {message[:50]})")
  51. # 在线程池中执行同步的Agent调用,设置超时
  52. response = await asyncio.wait_for(
  53. loop.run_in_executor(
  54. thread_executor,
  55. lambda: agent.run(message, user_id=user_id)
  56. ),
  57. timeout=30.0 # 30秒超时
  58. )
  59. print(f"✅ Agent异步调用完成")
  60. return response
  61. except asyncio.TimeoutError:
  62. print(f"⏰ Agent调用超时 (30秒)")
  63. return None
  64. except Exception as e:
  65. print(f"❌ Agent异步调用失败: {e}")
  66. return None
  67. def create_agent():
  68. """创建具有Memory功能的Agent实例"""
  69. global global_agent, global_memory
  70. if not AGENT_AVAILABLE:
  71. print("❌ Agent依赖不可用,将使用随机回复")
  72. return None
  73. # 检查环境变量
  74. api_key = os.getenv("BAILIAN_API_KEY")
  75. base_url = os.getenv("BAILIAN_API_BASE_URL")
  76. if not api_key or not base_url:
  77. print("⚠️ 环境变量未设置,Agent功能将不可用,使用随机回复")
  78. return None
  79. try:
  80. print("🚀 创建具有Memory功能的Agent实例...")
  81. # 创建模型
  82. model = OpenAILike(
  83. id="qwen3-32b",
  84. api_key=api_key,
  85. base_url=base_url,
  86. request_params={"extra_body": {"enable_thinking": False}},
  87. )
  88. # 数据库文件
  89. db_file = "tmp/agent_memory.db"
  90. os.makedirs("tmp", exist_ok=True)
  91. # 初始化Memory v2
  92. memory = Memory(
  93. model=model, # 使用相同的模型进行记忆管理
  94. db=SqliteMemoryDb(table_name="user_memories", db_file=db_file),
  95. )
  96. # 初始化存储
  97. storage = SqliteStorage(table_name="agent_sessions", db_file=db_file)
  98. # 定义记忆工具函数
  99. def remember_info(info: str):
  100. """主动记住信息的工具函数"""
  101. return f"我已经记住了这个信息: {info}"
  102. # 创建Agent with Memory功能
  103. agent = Agent(
  104. model=model,
  105. # Store memories in a database
  106. memory=memory,
  107. # Give the Agent the ability to update memories
  108. enable_agentic_memory=True,
  109. # Run the MemoryManager after each response
  110. enable_user_memories=True,
  111. # Store the chat history in the database
  112. storage=storage,
  113. # Add the chat history to the messages
  114. add_history_to_messages=True,
  115. # Number of history runs to include
  116. num_history_runs=3,
  117. # Tools
  118. tools=[get_current_time, get_user_info, remember_info],
  119. markdown=False, # 简单文本回复
  120. show_tool_calls=False, # 关闭工具调用显示,避免影响web显示
  121. instructions="""
  122. 你是一个具有记忆功能的友好AI助手,正在通过web实时对话窗口与用户交流。
  123. 🧠 **记忆功能**:
  124. - 你可以记住用户的姓名、偏好和兴趣
  125. - 保持跨会话的对话连贯性
  126. - 基于历史对话提供个性化建议
  127. - 记住之前讨论过的话题
  128. 💬 **对话原则**:
  129. - 使用简洁、自然的中文回答
  130. - 语气友好、热情
  131. - 回答要有帮助性
  132. - 可以调用工具获取信息
  133. - 主动记住重要信息
  134. - 基于记忆提供个性化回应
  135. 🎯 **个性化服务**:
  136. - 如果用户告诉你他们的姓名,主动记住
  137. - 记住用户的偏好和兴趣
  138. - 在后续对话中引用之前的内容
  139. - 提供基于历史的个性化建议
  140. 请与用户进行愉快的对话!我会记住我们的每次交流。
  141. """,
  142. )
  143. global_agent = agent
  144. global_memory = memory
  145. print("✅ Memory Agent创建成功!")
  146. print(f"📱 模型: qwen3-32b")
  147. print(f"🧠 记忆: SQLite数据库 ({db_file})")
  148. print(f"💾 存储: 会话历史记录")
  149. print(f"👤 用户ID: {global_user_id}")
  150. # 简单测试Agent是否正常工作
  151. try:
  152. test_response = agent.run("你好", user_id=global_user_id)
  153. print(f"🧪 Agent测试成功: {str(test_response)[:50]}...")
  154. except Exception as e:
  155. print(f"⚠️ Agent测试失败: {e}")
  156. return agent
  157. except Exception as e:
  158. print(f"❌ Agent创建失败: {e}")
  159. return None
  160. # 随机回复内容(Agent不可用时的备用)
  161. RANDOM_REPLIES = [
  162. "这是一个有趣的观点!",
  163. "我完全同意你的看法。",
  164. "让我想想这个问题...",
  165. "你说得很有道理。",
  166. "这让我想到了另一个话题。",
  167. "非常好的问题!",
  168. "我觉得你可以试试这样做。",
  169. "这确实是个挑战。",
  170. "你的想法很有创意!",
  171. "我需要更多信息来帮助你。",
  172. "这个话题很深入呢。",
  173. "你考虑得很周全。"
  174. ]
  175. # 创建FastAPI应用
  176. app = FastAPI(title="SSE实时对话", description="简单的实时聊天系统", version="1.0.0")
  177. # 应用启动时初始化Agent
  178. @app.on_event("startup")
  179. async def startup_event():
  180. print("🚀 启动SSE实时对话系统...")
  181. print("📍 访问地址: http://localhost:8000")
  182. # 初始化Agent
  183. try:
  184. create_agent()
  185. if global_agent:
  186. print("✅ Memory Agent已就绪,将提供具有记忆功能的智能回复")
  187. print("🧠 记忆功能: 可记住用户信息和对话历史")
  188. print("💬 特殊命令: 在对话中输入 '记忆' 查看记忆状态")
  189. else:
  190. print("⚠️ Agent不可用,将使用随机回复")
  191. except Exception as e:
  192. print(f"❌ Agent创建过程中出错: {e}")
  193. print("⚠️ 系统将使用随机回复模式")
  194. @app.get("/")
  195. async def home():
  196. """主页 - 对话界面"""
  197. html_content = """
  198. <!DOCTYPE html>
  199. <html lang="zh-CN">
  200. <head>
  201. <meta charset="UTF-8">
  202. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  203. <title>实时对话窗口</title>
  204. <style>
  205. * {
  206. margin: 0;
  207. padding: 0;
  208. box-sizing: border-box;
  209. }
  210. body {
  211. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  212. background: #f0f2f5;
  213. height: 100vh;
  214. display: flex;
  215. justify-content: center;
  216. align-items: center;
  217. }
  218. .chat-container {
  219. width: 600px;
  220. height: 500px;
  221. background: white;
  222. border-radius: 10px;
  223. box-shadow: 0 4px 20px rgba(0,0,0,0.1);
  224. display: flex;
  225. flex-direction: column;
  226. overflow: hidden;
  227. }
  228. .chat-header {
  229. background: #4a90e2;
  230. color: white;
  231. padding: 15px 20px;
  232. text-align: center;
  233. font-size: 18px;
  234. font-weight: 600;
  235. }
  236. .status-bar {
  237. padding: 8px 15px;
  238. background: #e8f4f8;
  239. font-size: 12px;
  240. color: #5a5a5a;
  241. border-bottom: 1px solid #e0e0e0;
  242. }
  243. .messages-container {
  244. flex: 1;
  245. padding: 15px;
  246. overflow-y: auto;
  247. background: #fafafa;
  248. }
  249. .message {
  250. margin-bottom: 12px;
  251. display: flex;
  252. animation: slideIn 0.3s ease-out;
  253. }
  254. @keyframes slideIn {
  255. from {
  256. opacity: 0;
  257. transform: translateY(10px);
  258. }
  259. to {
  260. opacity: 1;
  261. transform: translateY(0);
  262. }
  263. }
  264. .message.user {
  265. justify-content: flex-end;
  266. }
  267. .message-bubble {
  268. max-width: 75%;
  269. padding: 10px 15px;
  270. border-radius: 18px;
  271. word-wrap: break-word;
  272. position: relative;
  273. }
  274. .message.user .message-bubble {
  275. background: #4a90e2;
  276. color: white;
  277. }
  278. .message.bot .message-bubble {
  279. background: white;
  280. color: #333;
  281. border: 1px solid #e0e0e0;
  282. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  283. }
  284. .message.system .message-bubble {
  285. background: #fff3cd;
  286. color: #856404;
  287. border: 1px solid #ffeaa7;
  288. font-style: italic;
  289. text-align: center;
  290. max-width: 100%;
  291. }
  292. .message-time {
  293. font-size: 10px;
  294. color: #999;
  295. margin-top: 3px;
  296. text-align: right;
  297. }
  298. .message.user .message-time {
  299. text-align: right;
  300. }
  301. .message.bot .message-time {
  302. text-align: left;
  303. }
  304. .input-container {
  305. padding: 15px;
  306. background: white;
  307. border-top: 1px solid #e0e0e0;
  308. display: flex;
  309. gap: 10px;
  310. }
  311. .message-input {
  312. flex: 1;
  313. padding: 10px 15px;
  314. border: 1px solid #ddd;
  315. border-radius: 20px;
  316. outline: none;
  317. font-size: 14px;
  318. transition: border-color 0.3s;
  319. }
  320. .message-input:focus {
  321. border-color: #4a90e2;
  322. box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
  323. }
  324. .send-button {
  325. background: #4a90e2;
  326. color: white;
  327. border: none;
  328. border-radius: 20px;
  329. padding: 10px 20px;
  330. cursor: pointer;
  331. font-size: 14px;
  332. font-weight: 600;
  333. transition: all 0.3s;
  334. }
  335. .send-button:hover {
  336. background: #357abd;
  337. transform: translateY(-1px);
  338. }
  339. .send-button:disabled {
  340. background: #ccc;
  341. cursor: not-allowed;
  342. transform: none;
  343. }
  344. .typing-indicator {
  345. display: none;
  346. padding: 10px 15px;
  347. color: #666;
  348. font-style: italic;
  349. font-size: 12px;
  350. }
  351. .typing-dots {
  352. display: inline-block;
  353. }
  354. .typing-dots::after {
  355. content: '';
  356. animation: typing 1.5s infinite;
  357. }
  358. @keyframes typing {
  359. 0%, 60%, 100% { content: ''; }
  360. 30% { content: '.'; }
  361. 40% { content: '..'; }
  362. 50% { content: '...'; }
  363. }
  364. /* 打字机光标效果 */
  365. .typing-cursor {
  366. display: inline-block;
  367. background-color: #333;
  368. width: 2px;
  369. height: 1em;
  370. margin-left: 1px;
  371. animation: blink 1s infinite;
  372. }
  373. .message.bot .typing-cursor {
  374. background-color: #333;
  375. }
  376. @keyframes blink {
  377. 0%, 50% { opacity: 1; }
  378. 51%, 100% { opacity: 0; }
  379. }
  380. </style>
  381. </head>
  382. <body>
  383. <div class="chat-container">
  384. <div class="chat-header">
  385. 💬 实时对话窗口
  386. </div>
  387. <div class="status-bar" id="statusBar">
  388. 正在连接...
  389. </div>
  390. <div class="messages-container" id="messagesContainer">
  391. <!-- 消息显示区域 -->
  392. </div>
  393. <div class="typing-indicator" id="typingIndicator">
  394. 机器人正在输入<span class="typing-dots"></span>
  395. </div>
  396. <div class="input-container">
  397. <input
  398. type="text"
  399. id="messageInput"
  400. class="message-input"
  401. placeholder="输入您的消息... (现在支持打字机效果!输入 '记忆' 查看记忆状态)"
  402. maxlength="500"
  403. >
  404. <button id="sendButton" class="send-button">发送</button>
  405. </div>
  406. </div>
  407. <script>
  408. // 全局变量
  409. let currentBotMessageElement = null; // 当前正在构建的机器人消息元素
  410. const messagesContainer = document.getElementById('messagesContainer');
  411. const messageInput = document.getElementById('messageInput');
  412. const sendButton = document.getElementById('sendButton');
  413. const statusBar = document.getElementById('statusBar');
  414. const typingIndicator = document.getElementById('typingIndicator');
  415. // 页面加载完成后初始化
  416. window.addEventListener('load', function() {
  417. console.log('页面加载完成');
  418. // 启用输入功能
  419. messageInput.disabled = false;
  420. sendButton.disabled = false;
  421. messageInput.focus();
  422. // 显示欢迎消息
  423. addMessage('🎉 欢迎使用AI实时对话窗口!我是您的智能助手,具有记忆功能和打字机效果。我会一个词一个词地回复您,就像真人打字一样!请开始聊天吧~', 'system');
  424. // 更新状态
  425. statusBar.textContent = '✅ 已就绪 - 请开始对话';
  426. statusBar.style.background = '#d4edda';
  427. statusBar.style.color = '#155724';
  428. });
  429. // 创建单次SSE连接用于获取回复
  430. function createSSEConnection(message) {
  431. return new Promise((resolve, reject) => {
  432. const encodedMessage = encodeURIComponent(message);
  433. const eventSource = new EventSource(`/api/chat?message=${encodedMessage}`);
  434. eventSource.onopen = function() {
  435. console.log('SSE连接已建立');
  436. statusBar.textContent = '🔄 正在处理消息...';
  437. statusBar.style.background = '#fff3cd';
  438. statusBar.style.color = '#856404';
  439. };
  440. eventSource.onmessage = function(event) {
  441. try {
  442. const data = JSON.parse(event.data);
  443. handleSSEMessage(data);
  444. // 当收到complete消息时关闭连接
  445. if (data.type === 'complete') {
  446. eventSource.close();
  447. resolve();
  448. }
  449. } catch (e) {
  450. console.error('消息解析错误:', e);
  451. eventSource.close();
  452. reject(e);
  453. }
  454. };
  455. eventSource.onerror = function() {
  456. console.log('SSE连接失败');
  457. eventSource.close();
  458. statusBar.textContent = '❌ 连接失败';
  459. statusBar.style.background = '#f8d7da';
  460. statusBar.style.color = '#721c24';
  461. reject(new Error('SSE连接失败'));
  462. };
  463. });
  464. }
  465. // 处理SSE消息
  466. function handleSSEMessage(data) {
  467. console.log('收到SSE消息:', data);
  468. switch(data.type) {
  469. case 'status':
  470. // 更新状态栏显示处理进度
  471. statusBar.textContent = data.message;
  472. statusBar.style.background = '#fff3cd';
  473. statusBar.style.color = '#856404';
  474. break;
  475. case 'bot_message_start':
  476. // 开始新的机器人回复(打字机效果)
  477. hideTypingIndicator();
  478. currentBotMessageElement = createEmptyBotMessage();
  479. break;
  480. case 'bot_message_token':
  481. // 添加单个单词到当前机器人回复
  482. if (currentBotMessageElement) {
  483. appendTokenToBotMessage(currentBotMessageElement, data.token);
  484. }
  485. break;
  486. case 'bot_message_end':
  487. // 完成机器人回复
  488. if (currentBotMessageElement) {
  489. removeTypingCursor(currentBotMessageElement);
  490. }
  491. currentBotMessageElement = null;
  492. console.log('✅ 打字机效果回复完成:', data.complete_message);
  493. break;
  494. case 'bot_message':
  495. // 兼容旧版本的完整消息模式
  496. hideTypingIndicator();
  497. addMessage(data.message, 'bot');
  498. break;
  499. case 'complete':
  500. // 回复完成,更新状态
  501. statusBar.textContent = '✅ 已就绪 - 请开始对话';
  502. statusBar.style.background = '#d4edda';
  503. statusBar.style.color = '#155724';
  504. break;
  505. case 'error':
  506. hideTypingIndicator();
  507. addMessage('❌ 错误: ' + data.message, 'system');
  508. statusBar.textContent = '❌ 处理失败';
  509. statusBar.style.background = '#f8d7da';
  510. statusBar.style.color = '#721c24';
  511. break;
  512. }
  513. }
  514. // 添加消息到界面
  515. function addMessage(content, type) {
  516. console.log(`添加消息: [${type}] ${content}`);
  517. const messageDiv = document.createElement('div');
  518. messageDiv.className = `message ${type}`;
  519. const bubbleDiv = document.createElement('div');
  520. bubbleDiv.className = 'message-bubble';
  521. bubbleDiv.textContent = content;
  522. const timeDiv = document.createElement('div');
  523. timeDiv.className = 'message-time';
  524. timeDiv.textContent = new Date().toLocaleTimeString();
  525. messageDiv.appendChild(bubbleDiv);
  526. messageDiv.appendChild(timeDiv);
  527. messagesContainer.appendChild(messageDiv);
  528. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  529. }
  530. // 创建空的机器人消息元素(用于打字机效果)
  531. function createEmptyBotMessage() {
  532. console.log('创建空的机器人消息元素');
  533. const messageDiv = document.createElement('div');
  534. messageDiv.className = 'message bot';
  535. const bubbleDiv = document.createElement('div');
  536. bubbleDiv.className = 'message-bubble';
  537. // 添加打字光标
  538. const cursor = document.createElement('span');
  539. cursor.className = 'typing-cursor';
  540. bubbleDiv.appendChild(cursor);
  541. const timeDiv = document.createElement('div');
  542. timeDiv.className = 'message-time';
  543. timeDiv.textContent = new Date().toLocaleTimeString();
  544. messageDiv.appendChild(bubbleDiv);
  545. messageDiv.appendChild(timeDiv);
  546. messagesContainer.appendChild(messageDiv);
  547. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  548. return messageDiv;
  549. }
  550. // 向机器人消息添加单词(打字机效果)
  551. function appendTokenToBotMessage(messageElement, token) {
  552. if (!messageElement) return;
  553. const bubbleDiv = messageElement.querySelector('.message-bubble');
  554. const cursor = bubbleDiv.querySelector('.typing-cursor');
  555. if (bubbleDiv && cursor) {
  556. // 在光标前插入单词
  557. const textNode = document.createTextNode(token);
  558. bubbleDiv.insertBefore(textNode, cursor);
  559. // 滚动到底部,保持跟踪打字进度
  560. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  561. }
  562. }
  563. // 移除打字光标
  564. function removeTypingCursor(messageElement) {
  565. if (!messageElement) return;
  566. const cursor = messageElement.querySelector('.typing-cursor');
  567. if (cursor) {
  568. cursor.remove();
  569. }
  570. }
  571. // 显示输入指示器
  572. function showTypingIndicator() {
  573. typingIndicator.style.display = 'block';
  574. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  575. }
  576. // 隐藏输入指示器
  577. function hideTypingIndicator() {
  578. typingIndicator.style.display = 'none';
  579. }
  580. // 显示记忆状态
  581. async function showMemoryStatus() {
  582. try {
  583. showTypingIndicator();
  584. const response = await fetch('/api/memory');
  585. const result = await response.json();
  586. hideTypingIndicator();
  587. if (result.available) {
  588. let memoryInfo = `🧠 **记忆状态信息**\n\n`;
  589. memoryInfo += `👤 用户ID: ${result.user_id}\n`;
  590. memoryInfo += `📊 记忆数量: ${result.memory_count}\n`;
  591. memoryInfo += `💾 数据库: ${result.database}\n\n`;
  592. if (result.recent_memories && result.recent_memories.length > 0) {
  593. memoryInfo += `📝 **最近的记忆:**\n`;
  594. result.recent_memories.forEach((mem, index) => {
  595. memoryInfo += `${index + 1}. ${mem.content}...\n`;
  596. });
  597. } else {
  598. memoryInfo += `📭 暂无记忆内容\n`;
  599. }
  600. memoryInfo += `\n💭 **记忆功能说明:**\n`;
  601. memoryInfo += `- 我可以记住您的姓名、偏好和兴趣\n`;
  602. memoryInfo += `- 保持跨会话的对话连贯性\n`;
  603. memoryInfo += `- 基于历史对话提供个性化建议\n`;
  604. memoryInfo += `- 记住之前讨论过的话题`;
  605. addMessage(memoryInfo, 'bot');
  606. } else {
  607. addMessage('❌ 记忆功能不可用: ' + result.message, 'system');
  608. }
  609. } catch (error) {
  610. hideTypingIndicator();
  611. addMessage('❌ 获取记忆状态失败: ' + error.message, 'system');
  612. }
  613. }
  614. // 发送消息
  615. async function sendMessage() {
  616. const message = messageInput.value.trim();
  617. console.log('准备发送消息:', message);
  618. if (!message) {
  619. console.log('消息为空');
  620. return;
  621. }
  622. // 检查特殊命令
  623. if (message === '记忆' || message.toLowerCase() === 'memory') {
  624. console.log('处理记忆查询命令');
  625. addMessage(message, 'user');
  626. messageInput.value = '';
  627. await showMemoryStatus();
  628. return;
  629. }
  630. // 立即显示用户消息
  631. addMessage(message, 'user');
  632. messageInput.value = '';
  633. // 禁用输入
  634. messageInput.disabled = true;
  635. sendButton.disabled = true;
  636. // 显示输入指示器
  637. showTypingIndicator();
  638. try {
  639. // 使用SSE获取回复
  640. console.log('发送SSE请求...');
  641. await createSSEConnection(message);
  642. console.log('SSE对话完成');
  643. } catch (error) {
  644. console.error('SSE对话错误:', error);
  645. hideTypingIndicator();
  646. addMessage('❌ 对话失败: ' + error.message, 'system');
  647. statusBar.textContent = '❌ 对话失败';
  648. statusBar.style.background = '#f8d7da';
  649. statusBar.style.color = '#721c24';
  650. } finally {
  651. // 重新启用输入
  652. messageInput.disabled = false;
  653. sendButton.disabled = false;
  654. messageInput.focus();
  655. }
  656. }
  657. // 事件监听器
  658. sendButton.addEventListener('click', sendMessage);
  659. messageInput.addEventListener('keypress', function(e) {
  660. if (e.key === 'Enter' && !e.shiftKey) {
  661. e.preventDefault();
  662. sendMessage();
  663. }
  664. });
  665. // 页面关闭时的清理工作
  666. window.addEventListener('beforeunload', function() {
  667. console.log('页面即将关闭');
  668. });
  669. </script>
  670. </body>
  671. </html>
  672. """
  673. return HTMLResponse(content=html_content)
  674. # 旧的SSE接口已被移除,现在使用 /api/chat 直接SSE接口
  675. async def generate_chat_response(user_message: str):
  676. """生成聊天回复的SSE流"""
  677. global global_agent, global_user_id
  678. try:
  679. print(f"📨 收到用户消息: {user_message}")
  680. print(f"🤖 Agent可用性: {global_agent is not None}")
  681. # 发送开始处理消息
  682. yield f"data: {json.dumps({'type': 'status', 'message': '正在处理您的消息...'}, ensure_ascii=False)}\n\n"
  683. # 模拟思考时间
  684. await asyncio.sleep(random.uniform(0.5, 1.5))
  685. bot_reply = None
  686. # 尝试使用Agent生成回复
  687. if global_agent:
  688. try:
  689. print("🤖 调用Memory Agent生成回复...")
  690. yield f"data: {json.dumps({'type': 'status', 'message': '🤖 正在生成智能回复...'}, ensure_ascii=False)}\n\n"
  691. # 异步调用Agent处理消息,传入user_id以关联记忆
  692. response = await call_agent_async(global_agent, user_message, global_user_id)
  693. if response:
  694. bot_reply = response.content if hasattr(response, 'content') else str(response)
  695. print(f"✅ Agent回复: {bot_reply}")
  696. print(f"🧠 记忆已更新 (用户ID: {global_user_id})")
  697. else:
  698. print("❌ Agent返回空响应")
  699. bot_reply = None
  700. except Exception as e:
  701. print(f"❌ Agent调用失败: {e}")
  702. bot_reply = None
  703. # 如果Agent不可用或失败,使用随机回复
  704. if not bot_reply:
  705. bot_reply = random.choice(RANDOM_REPLIES)
  706. print(f"🎲 使用随机回复: {bot_reply}")
  707. # 确保回复不为空
  708. if not bot_reply:
  709. bot_reply = "抱歉,我暂时无法回复。请稍后再试。"
  710. print(f"⚠️ 使用默认回复: {bot_reply}")
  711. # 发送打字机效果的回复
  712. print(f"⌨️ 开始打字机效果发送回复...")
  713. # 发送开始打字消息
  714. yield f"data: {json.dumps({'type': 'bot_message_start', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n"
  715. # 按单词逐个发送,实现打字机效果
  716. import re
  717. # 使用正则表达式分割文本为单词,包含标点符号
  718. words = re.findall(r'\S+|\s+', bot_reply)
  719. for i, word in enumerate(words):
  720. # 发送单个单词
  721. word_message = {
  722. "type": "bot_message_token",
  723. "token": word,
  724. "position": i
  725. }
  726. yield f"data: {json.dumps(word_message, ensure_ascii=False)}\n\n"
  727. # 控制打字速度:根据单词类型调整停顿时间
  728. if word.strip() == '':
  729. # 空白字符(空格、换行等)
  730. await asyncio.sleep(random.uniform(0.05, 0.1))
  731. elif any(punct in word for punct in '。!?'):
  732. # 包含句末标点的单词
  733. await asyncio.sleep(random.uniform(0.5, 0.8))
  734. elif any(punct in word for punct in ',;:'):
  735. # 包含句中标点的单词
  736. await asyncio.sleep(random.uniform(0.3, 0.5))
  737. elif len(word.strip()) > 5:
  738. # 长单词
  739. await asyncio.sleep(random.uniform(0.2, 0.4))
  740. else:
  741. # 普通单词
  742. await asyncio.sleep(random.uniform(0.1, 0.3))
  743. # 发送结束消息
  744. end_message = {
  745. "type": "bot_message_end",
  746. "complete_message": bot_reply,
  747. "timestamp": datetime.now().isoformat()
  748. }
  749. yield f"data: {json.dumps(end_message, ensure_ascii=False)}\n\n"
  750. print(f"✅ 打字机效果发送完成")
  751. # 发送完成状态
  752. yield f"data: {json.dumps({'type': 'complete', 'message': '回复完成'}, ensure_ascii=False)}\n\n"
  753. except Exception as e:
  754. print(f"处理消息错误: {e}")
  755. error_message = {
  756. "type": "error",
  757. "message": f"处理错误: {str(e)}"
  758. }
  759. yield f"data: {json.dumps(error_message, ensure_ascii=False)}\n\n"
  760. @app.get("/api/chat")
  761. async def chat_api_sse(message: str = ""):
  762. """SSE聊天接口"""
  763. if not message.strip():
  764. return {"error": "消息不能为空"}
  765. return StreamingResponse(
  766. generate_chat_response(message.strip()),
  767. media_type="text/event-stream",
  768. headers={
  769. "Cache-Control": "no-cache",
  770. "Connection": "keep-alive",
  771. "Access-Control-Allow-Origin": "*",
  772. }
  773. )
  774. @app.get("/api/status")
  775. async def get_status():
  776. """获取系统状态"""
  777. return {
  778. "status": "running",
  779. "agent_available": global_agent is not None,
  780. "timestamp": datetime.now().isoformat()
  781. }
  782. @app.post("/api/clear")
  783. async def clear_messages():
  784. """清空消息历史(现在使用直接SSE,无需清空队列)"""
  785. # 由于现在使用直接SSE,不再需要清空消息队列
  786. # 这个接口保留用于兼容性,但实际不执行任何操作
  787. return {"success": True, "message": "消息历史已清空(使用SSE直接模式)"}
  788. @app.get("/api/memory")
  789. async def get_memory_status():
  790. """获取Agent记忆状态"""
  791. global global_memory, global_user_id, global_agent
  792. if not global_agent or not global_memory:
  793. return {
  794. "available": False,
  795. "message": "Memory功能不可用",
  796. "user_id": global_user_id
  797. }
  798. try:
  799. # 尝试获取记忆信息
  800. memories = global_memory.get_user_memories(user_id=global_user_id)
  801. memory_count = len(memories) if memories else 0
  802. # 获取最近的3条记忆摘要
  803. recent_memories = []
  804. if memories:
  805. for mem in memories[-3:]:
  806. try:
  807. # UserMemory对象的属性访问
  808. content = getattr(mem, 'content', '')[:100] if hasattr(mem, 'content') else str(mem)[:100]
  809. timestamp = getattr(mem, 'created_at', '') if hasattr(mem, 'created_at') else ''
  810. recent_memories.append({
  811. "content": content,
  812. "timestamp": str(timestamp),
  813. })
  814. except Exception as e:
  815. # 备用方案:直接转换为字符串
  816. recent_memories.append({
  817. "content": str(mem)[:100],
  818. "timestamp": "",
  819. })
  820. return {
  821. "available": True,
  822. "user_id": global_user_id,
  823. "memory_count": memory_count,
  824. "recent_memories": recent_memories,
  825. "database": "tmp/agent_memory.db",
  826. "timestamp": datetime.now().isoformat()
  827. }
  828. except Exception as e:
  829. return {
  830. "available": True,
  831. "user_id": global_user_id,
  832. "memory_count": "unknown",
  833. "error": str(e),
  834. "message": "记忆系统已启用但获取详情失败",
  835. "timestamp": datetime.now().isoformat()
  836. }
  837. if __name__ == "__main__":
  838. print("🚀 启动SSE实时对话系统...")
  839. print("🌐 访问地址: http://localhost:8081")
  840. print("🤖 支持Agent智能回复 + SSE实时推送")
  841. print("📋 如需Agent功能,请配置环境变量:")
  842. print(" BAILIAN_API_KEY=your_api_key")
  843. print(" BAILIAN_API_BASE_URL=your_base_url")
  844. print("=" * 50)
  845. uvicorn.run(
  846. "sse_app:app",
  847. host="0.0.0.0",
  848. port=8081,
  849. reload=True,
  850. log_level="info"
  851. )