-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathchatbot.py
More file actions
1933 lines (1636 loc) · 89.3 KB
/
Copy pathchatbot.py
File metadata and controls
1933 lines (1636 loc) · 89.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import os
import json
import logging
import time
import subprocess
import platform
import re
from datetime import datetime
from typing import Dict, Any, List, Optional, Union, Tuple
import hashlib
from uuid import uuid4
import random
from bs4 import BeautifulSoup
import html2text
import requests
import asyncio
import difflib
import tarfile # 添加tarfile导入
import shutil # 添加shutil导入
import threading
import aiohttp
# LangChain imports
from langchain_ollama import ChatOllama
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.memory import BaseMemory
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain.memory import ConversationBufferMemory
from core.memory.enhanced_memory import EnhancedMemorySystem
from core.memory.OllamaEmbeddingFunction import OllamaEmbeddingFunction
from core.utils import ConfigManager
from core.utils import RateLimiter
from core.utils import CommandExecutor
from core.llmhandle.context import _extract_time_context
from core.llmhandle.context import _evaluate_query_effectiveness
from core.llmhandle.context import _expand_query_with_synonyms
from core.llmhandle.context import _select_best_query
from core.llmhandle.context import _reformulate_query
from core.llmhandle.context import test_query_reformulation
from core.llmhandle.context import perform_memory_maintenance
from core.llmhandle.backdb import backup_database
from core.llmhandle.responseformatter import _format_response
from core.llmhandle.responseformatter import _format_proactive_response
from core.llmhandle.responseformatter import _format_web_search_response
from core.llmhandle.context import _extract_search_keywords
from core.llmhandle.backdb import _export_chromadb_data
from core.llmhandle.callopenrouter import _call_openrouter
from core.llmhandle.callopenrouter import _call_openrouter_qwq
from core.llmhandle.callopenrouter import _call_openrouter_other
from core.llmhandle.callopenrouter import _call_openrouter_search
from core.llmhandle.callopenrouter import _call_grok3
from core.llmhandle.callopenrouter import _call_openrouter_main
from core.llmhandle.context import analyze_dialogue_context
from core.llmhandle.context_storage import ConversationContextStorage
# 导入网络搜索模块
from web_search import perform_web_search
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
# 确保chatbot日志器始终显示INFO级别及以上的日志
logger = logging.getLogger("chatbot")
logger.setLevel(logging.DEBUG)
# 禁用ChromaDB的警告日志
logging.getLogger("chromadb").setLevel(logging.ERROR)
logging.getLogger("chromadb.segment").setLevel(logging.ERROR)
logging.getLogger("chromadb.segment.impl.vector").setLevel(logging.ERROR)
# 禁用httpx的INFO日志
logging.getLogger("httpx").setLevel(logging.WARNING)
# 定义基本的对话模板
SYSTEM_TEMPLATE = """你是 J.A.R.V.I.S. (Just A Rather Very Intelligent System),一个高度智能的AI助手。请遵循以下行为准则:
1. 重要:
- 核心规则(比如用户询问"查下Hiddify是什么东西",但是在"历史参考记忆"里没有或者关系不大或者你觉得不专业,你就可以按照你自己的知识库来回答)
1. 个性特征:
- 用中文回答我
- 保持专业、高效且略带幽默感
- 使用"Sir"或适当的敬称称呼用户
- 说话简洁明了,但不失优雅
- 在合适的时候展现出独特的个性和智慧
2. 核心功能:
- 精确记忆用户偏好和历史交互
- 主动预测用户需求并提供建议
- 在处理任务时展现出超强的分析能力
- 适时使用专业术语,但确保用户能够理解
3. 交互准则:
- 当用户说"我叫xxx"或"我现在叫xxx"时,记住并使用这个称呼
- 使用"Sir"或用户提供的名字称呼用户
- 在合适的时候使用优雅的幽默或机智的回应
- 保持对话的连贯性和上下文意识
4. 安全协议:
- 对用户信息保持绝对的保密性
- 在涉及重要决策时,确保多重确认
- 对潜在风险保持警惕并及时提醒用户
就像电影中的 J.A.R.V.I.S. 一样,你应该是一个值得信赖的助手、顾问和朋友。在保持专业的同时,也要展现出独特的个性。"""
USER_TEMPLATE = """{history}
User: {input}
J.A.R.V.I.S.: """
class ChatbotManager:
"""聊天机器人管理器,处理用户与AI的对话"""
def __init__(self, username: str):
"""初始化聊天机器人管理器"""
# 设置用户信息
self.username = username
self.session_id = str(uuid4())
# 初始化对话历史
self.last_user_input = None
self.last_ai_response = None
self.conversation_history = [] # 添加一个列表来存储更多的对话历史
self.max_history_turns = 5 # 保存最近5轮对话
# 用于跟踪主动学习状态
self.last_proactive_question = None # 最后一个主动提出的问题
self.proactive_questions_asked = {} # 记录已问过的问题,格式: {question_hash: count}
self.waiting_for_proactive_answer = False # 是否正在等待用户回答主动问题
self.config_manager = ConfigManager()
# 初始化命令执行器
self.command_executor = CommandExecutor(self.config_manager)
# 初始化 J.A.R.V.I.S. 个性设置
self.personality = {
"humor_level": 0.3, # 默认幽默程度
"formality": "balanced" # 默认正式程度
}
self.security_settings = self.config_manager.get("security", {})
self.response_settings = self.config_manager.get("response_settings", {})
# 初始化语言模型
self.llm = ChatOllama(
base_url=self.config_manager.get("base_url"),
model=self.config_manager.get("model"),
temperature=self.response_settings.get("temperature", 0.7)
)
# 初始化嵌入函数
self.embedding_function = OllamaEmbeddingFunction(
base_url=self.config_manager.get("base_url"),
model=self.config_manager.get("memory_settings", {}).get("embedding_model", "nomic-embed-text:latest")
)
# 初始化增强型记忆系统
memory_dir = os.path.join(
self.config_manager.get("memory_dir", "chat_memories"),
"enhanced"
)
self.memory_system = EnhancedMemorySystem(
persist_directory=memory_dir,
collection_name="user_memories",
llm=self.llm,
embedding_function=self.embedding_function,
initial_memory_strength=0.8,
forgetting_rate=0.1,
consolidation_threshold=0.5,
merge_threshold=0.95 # 降低合并阈值,默认是0.85
)
# 初始化对话上下文存储
context_storage_dir = os.path.join(
self.config_manager.get("memory_dir", "chat_memories"),
"context_storage"
)
self.context_storage = ConversationContextStorage(storage_dir=context_storage_dir)
# 初始化网络搜索管理器
from web_search import WebSearchManager
self.search_manager = WebSearchManager()
# 初始化图片生成器
try:
from image_generation import GeminiImageGenerator
self.image_generator = GeminiImageGenerator()
logger.info("图片生成器初始化成功")
except Exception as e:
logger.warning(f"图片生成器初始化失败: {e}")
self.image_generator = None
# 初始化对话链
self.chain = self._setup_chain()
logger.info(f"J.A.R.V.I.S. 已初始化完成")
def _get_greeting(self) -> str:
"""根据时间生成适当的问候语"""
hour = datetime.now().hour
if 5 <= hour < 12:
return "早上好"
elif 12 <= hour < 18:
return "下午好"
else:
return "晚上好"
def _setup_chain(self):
"""设置对话链"""
# 创建提示模板
prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_TEMPLATE),
("system", "{context}"), # 添加记忆上下文
("human", "{input}")
])
# 创建对话链
chain = prompt | self.llm | StrOutputParser()
return chain
async def chat(self, user_input: str, image_data: Optional[Dict[str, str]] = None) -> str:
"""处理用户输入并返回响应"""
try:
# 检查是否包含特定关键词,进行替换
keyword_replacements = {
"看下头条热榜": "用curl命令请求https://whyta.cn/api/toutiao?key=36de5db81215 查看下头条热榜",
"看下每日简报": "用curl命令请求https://whyta.cn/api/tx/bulletin?key=36de5db81215 查看下每日简报",
"看下抖音热搜": "用curl命令请求https://whyta.cn/api/tx/douyinhot?key=36de5db81215 查看下抖音热搜",
}
# 检查用户输入是否包含需要替换的关键词
for keyword, replacement in keyword_replacements.items():
if keyword in user_input:
logger.info(f"检测到关键词 '{keyword}',替换为 '{replacement}'")
user_input = user_input.replace(keyword, replacement)
break
# 处理图片分析(如果有图片)
if image_data:
try:
# 记录图片处理
logger.info("检测到图片,调用Gemini Vision进行分析")
# 调用Gemini Vision分析图片
image_analysis = await self._analyze_image_with_gemini_vision(image_data)
# 组合用户输入和图片分析结果
if user_input:
# 如果用户有输入文本,将图片分析作为上下文添加
enhanced_input = f"{user_input}: {image_analysis}"
else:
# 如果用户没有输入文本,直接请求分析图片
enhanced_input = f"请分析并描述这张图片: {image_analysis}"
logger.info(f"图片分析完成,增强后的输入: {enhanced_input[:100]}...")
user_input = enhanced_input
except Exception as e:
logger.error(f"图片分析失败: {str(e)}", exc_info=True)
# 如果图片分析失败,添加错误信息到用户输入
user_input = f"{user_input}\n\n[图片处理失败: {str(e)}]"
# 检查是否是天气查询请求(格式:看下XX天气)
weather_pattern = re.compile(r"看下([\u4e00-\u9fa5a-zA-Z]+)天气")
weather_match = weather_pattern.search(user_input)
if weather_match:
location = weather_match.group(1)
weather_command = f"用curl wttr.in/{location} 查看下天气"
logger.info(f"检测到天气查询请求,地点:{location},替换为:{weather_command}")
user_input = user_input.replace(weather_match.group(0), weather_command)
# 查看下币的价格(格式:看下XX币)
tokens_pattern = re.compile(r"看下([\u4e00-\u9fa5a-zA-Z]+)币")
tokens_pattern = tokens_pattern.search(user_input)
if tokens_pattern:
location = tokens_pattern.group(1)
weather_command = f"用curl https://api.coingecko.com/api/v3/coins/{location} 看下这个币的信息"
logger.info(f"检测到查看币信息:{location},替换为:{weather_command}")
user_input = user_input.replace(tokens_pattern.group(0), weather_command)
# 添加清理上下文历史的命令
if user_input.strip().lower() == "clear_his":
context_storage_dir = os.path.join(
self.config_manager.get("memory_dir", "chat_memories"),
"context_storage"
)
try:
for filename in os.listdir(context_storage_dir):
if filename.endswith(".json"):
file_path = os.path.join(context_storage_dir, filename)
os.remove(file_path)
return "已清理所有对话上下文历史。"
except Exception as e:
logger.error(f"清理上下文历史时出错: {e}")
return f"清理上下文历史时出错: {e}"
# 检查是否是数据库备份命令
if user_input.strip().lower() == "dbback":
return await backup_database(self)
# 检查是否是导出日志命令
if user_input.strip().lower() == "savelog":
return await _export_chromadb_data(self)
# 检查是否是维护命令
if user_input.strip().lower() == "sleep":
return perform_memory_maintenance(self, short_term_only=False) # 完整维护
elif user_input.strip().lower() == "sleep_short":
return perform_memory_maintenance(self, short_term_only=True) # 短期维护
# 检查是否是上下文分析命令
if user_input.strip().lower() == "context_summary":
context_summary = self.context_storage.get_context_summary(self.session_id)
return f"当前对话上下文摘要:\n\n当前主题: {context_summary['current_topic']}\n当前意图: {context_summary['current_intent']}\n关键实体: {', '.join(context_summary['key_entities'])}\n关键事实: {', '.join(context_summary['key_facts'])}\n用户偏好: {', '.join(context_summary['user_preferences'])}\n\n主题历史: {', '.join(context_summary['topic_history'])}\n意图历史: {', '.join(context_summary['intent_history'])}"
# 检查是否是导入聊天记录命令
if user_input.startswith("@import_chat"):
try:
# 提取JSON数据
json_str = user_input.replace("@import_chat", "").strip()
if not json_str:
return "请提供有效的聊天记录JSON数据。格式: @import_chat [JSON数据]"
# 解析JSON数据
chat_records = json.loads(json_str)
if not isinstance(chat_records, list):
chat_records = [chat_records] # 如果是单条记录,转换为列表
# 导入聊天记录
result = self.import_chat_records(chat_records)
return result
except json.JSONDecodeError:
return "JSON格式错误,请检查聊天记录数据格式。"
except Exception as e:
logger.error(f"导入聊天记录失败: {str(e)}")
return f"导入聊天记录失败: {str(e)}"
# 检查是否是批量导入聊天记录命令
if user_input.startswith("@batch_import_chat"):
try:
# 提取JSON数据
parts = user_input.replace("@batch_import_chat", "").strip().split(maxsplit=1)
if len(parts) < 2:
return "请提供批次大小和有效的聊天记录JSON数据。格式: @batch_import_chat [batch_size] [JSON数据]"
try:
batch_size = int(parts[0])
except ValueError:
return "批次大小必须是一个整数。格式: @batch_import_chat [batch_size] [JSON数据]"
json_str = parts[1]
# 解析JSON数据
chat_records = json.loads(json_str)
if not isinstance(chat_records, list):
chat_records = [chat_records] # 如果是单条记录,转换为列表
# 批量导入聊天记录
result = self.batch_import_chat_records(chat_records, batch_size)
return result
except json.JSONDecodeError:
return "JSON格式错误,请检查聊天记录数据格式。"
except Exception as e:
logger.error(f"批量导入聊天记录失败: {str(e)}")
return f"批量导入聊天记录失败: {str(e)}"
# 检查是否是从文件导入聊天记录命令
if user_input.startswith("@file_import_chat"):
try:
# 提取命令参数
params = user_input.replace("@import_chat_file", "").strip().split()
if not params:
return "请提供有效的聊天记录文件路径。格式: @import_chat_file [文件路径] [batch=true/false] [batch_size=50]"
file_path = params[1]
# 解析可选参数
use_batch = True # 默认使用批处理
batch_size = 50 # 默认批次大小
for param in params[1:]:
if param.startswith("batch="):
use_batch_str = param.split("=")[1].lower()
use_batch = use_batch_str in ["true", "1", "yes", "y"]
elif param.startswith("batch_size="):
try:
batch_size = int(param.split("=")[1])
except ValueError:
pass
# 导入聊天记录
result = self.import_chat_records_from_file(file_path, use_batch, batch_size)
return result
except Exception as e:
logger.error(f"从文件导入聊天记录失败: {str(e)}")
return f"从文件导入聊天记录失败: {str(e)}"
# # 检查是否需要自动执行网络搜索(无需@web前缀)
# if await self._should_auto_web_search(user_input):
# logger.info(f"检测到需要自动网络搜索: {user_input}")
# # 记录用户消息
# current_time = datetime.now().isoformat()
# user_message = HumanMessage(
# content=user_input,
# additional_kwargs={
# "timestamp": current_time,
# "session_id": self.session_id,
# "auto_web_search_triggered": True
# }
# )
# try:
# threading.Thread(
# target=self.memory_system.add_memory,
# args=(user_message,),
# kwargs={"memory_type": "episodic"},
# daemon=True
# ).start()
# except Exception as e:
# logger.warning(f"存储触发自动网络搜索的用户消息时出错: {e}")
# # 执行自动网络搜索
# auto_search_result = await self._perform_auto_web_search(user_input)
# if auto_search_result["success"]:
# # 返回搜索结果
# return auto_search_result["response"]
# else:
# # 如果自动搜索失败,记录错误但继续正常对话流程
# logger.warning(f"自动网络搜索失败: {auto_search_result.get('error', '未知错误')}")
# # 不返回,继续执行后续代码
# 检查是否需要执行网络搜索(使用@web前缀)
if "@web" in user_input:
# 从用户输入中移除@web前缀
search_input = user_input.replace("@web", "").strip()
logger.info(f"检测到需要自动网络搜索: {search_input}")
# 记录用户消息
current_time = datetime.now().isoformat()
user_message = HumanMessage(
content=user_input,
additional_kwargs={
"timestamp": current_time,
"session_id": self.session_id,
"auto_web_search_triggered": True
}
)
try:
threading.Thread(
target=self.memory_system.add_memory,
args=(user_message,),
kwargs={"memory_type": "episodic"},
daemon=True
).start()
except Exception as e:
logger.warning(f"存储触发自动网络搜索的用户消息时出错: {e}")
# 执行自动网络搜索
auto_search_result = await self._perform_auto_web_search(search_input)
if auto_search_result["success"]:
# 返回搜索结果
return auto_search_result["response"]
else:
# 如果自动搜索失败,记录错误但继续正常对话流程
logger.warning(f"自动网络搜索失败: {auto_search_result.get('error', '未知错误')}")
# 不返回,继续执行后续代码
# 检测是否是命令执行请求
command_result = await self.command_executor.analyze_user_request(self.llm, user_input)
if command_result["needs_command"]:
# 记录用户消息
current_time = datetime.now().isoformat()
user_message = HumanMessage(
content=user_input,
additional_kwargs={
"timestamp": current_time,
"session_id": self.session_id
}
)
try:
threading.Thread(
target=self.memory_system.add_memory,
args=(user_message,),
kwargs={"memory_type": "episodic"},
daemon=True
).start()
except Exception as e:
logger.warning(f"存储用户命令消息时出错: {e}")
# 执行命令
command = command_result['command']
logger.info(f"执行系统命令: {command}")
result = self.command_executor.execute_command(command)
# 打印原始命令执行结果
logger.info(f"命令原始执行结果: \n{result['output']}")
# 检查是否是curl命令
is_curl_command = command.strip().lower().startswith("curl ")
# 如果是curl命令,压缩结果,只保留核心文本信息
if is_curl_command and result["success"] and result["output"]:
try:
# 使用BeautifulSoup提取网页的核心文本内容
html_content = result["output"]
# 创建BeautifulSoup对象
soup = BeautifulSoup(html_content, 'html.parser')
# 移除脚本、样式和其他不需要的标签
for script in soup(["script", "style", "meta", "link", "noscript", "iframe", "svg"]):
script.extract()
# 提取正文内容
main_content = ""
# 尝试找到主要内容区域
main_tags = soup.find_all(['article', 'main', 'div', 'section'],
class_=lambda c: c and any(x in str(c).lower() for x in
['content', 'main', 'article', 'text', 'body']))
if main_tags:
# 使用找到的主要内容区域
for tag in main_tags:
main_content += tag.get_text(separator='\n', strip=True) + "\n\n"
else:
# 如果没有找到明确的主要内容区域,使用body内容
if soup.body:
main_content = soup.body.get_text(separator='\n', strip=True)
else:
main_content = soup.get_text(separator='\n', strip=True)
# 使用html2text作为备选方法,它能更好地处理格式
if not main_content.strip():
h = html2text.HTML2Text()
h.ignore_links = False
h.ignore_images = True
h.ignore_tables = False
h.ignore_emphasis = False
main_content = h.handle(html_content)
# 清理文本:移除多余的空行和空格
lines = [line.strip() for line in main_content.split('\n')]
lines = [line for line in lines if line]
cleaned_text = '\n'.join(lines)
# 如果提取的内容太长,进行简单的截断
# max_length = 5000 # 设置最大长度
# if len(cleaned_text) > max_length:
# cleaned_text = cleaned_text[:max_length] + "\n\n[内容已截断,仅显示前部分...]"
# 更新结果,使用提取的核心内容
result["output"] = "【以下是网页核心内容提取】\n\n" + cleaned_text
logger.info("已使用BeautifulSoup压缩curl命令结果,只保留核心文本信息")
except Exception as e:
logger.warning(f"使用BeautifulSoup压缩curl命令结果时出错: {e}")
# 使用LLM处理命令结果 - 只调用一次LLM
formatted_response = await self.command_executor.process_command_result(
self.llm, command, result["output"], user_input
)
# 记录AI响应
ai_message = AIMessage(
content=formatted_response,
additional_kwargs={
"timestamp": current_time,
"session_id": self.session_id,
"command_executed": command
}
)
try:
threading.Thread(
target=self.memory_system.add_memory,
args=(ai_message,),
kwargs={"memory_type": "episodic"},
daemon=True
).start()
except Exception as e:
logger.warning(f"存储AI命令响应时出错: {e}")
# 检查是否是发送给微信好友的命令
if "发给微信好友" in user_input:
# 提取好友名称 - 获取"发给微信好友"后面的内容作为好友名
friend_name = user_input.split("发给微信好友", 1)[1].strip()
# 移除好友名称部分,只保留要发送的消息内容
actual_message = user_input.split("发给微信好友", 1)[0].strip()
# 发送到微信
try:
result = send_message(friend_name, formatted_response)
print(f"\nJ.A.R.V.I.S.: 消息已发送给微信好友 {friend_name}")
except Exception as e:
print(f"\nJ.A.R.V.I.S.: 发送微信消息时出错: {str(e)}")
return formatted_response
# 记录用户输入作为新的记忆
current_time = datetime.now().isoformat()
user_message = HumanMessage(
content=user_input,
additional_kwargs={
"timestamp": current_time,
"session_id": self.session_id,
"type": "user_input"
}
)
try:
threading.Thread(
target=self.memory_system.add_memory,
args=(user_message,),
kwargs={"memory_type": "episodic"},
daemon=True
).start()
except Exception as e:
logger.warning(f"存储用户消息时出错: {e}")
try:
# 重构查询,使其更适合语义搜索
reformulated_query = await _reformulate_query(self, user_input)
logger.debug(f"重构后的查询: {reformulated_query}")
# 使用增强的对话上下文分析
context_analysis = await analyze_dialogue_context(self, user_input, reformulated_query) # 传入已重构的查询
logger.debug(f"对话上下文分析完成: 主题={context_analysis.get('topic_analysis', {}).get('main_topic', '未知')}")
# 获取连贯的上下文摘要
coherent_context = context_analysis.get("coherent_context", "无法生成上下文摘要")
# 存储上下文分析结果
try:
# 跟踪主题历史
self.context_storage.track_topic_history(
self.session_id,
context_analysis.get('topic_analysis', {})
)
# 跟踪意图历史
self.context_storage.track_intent_history(
self.session_id,
context_analysis.get('intent_analysis', {})
)
# 存储关键信息
self.context_storage.store_key_information(
self.session_id,
context_analysis.get('key_info', {})
)
except Exception as e:
logger.warning(f"存储上下文分析结果时出错: {e}")
# 迭代式记忆检索 - 初始查询
current_query = reformulated_query
relevant_memories = [] # 最终使用的记忆
max_iterations = 1 # 最大迭代次数
for iteration in range(max_iterations):
# 获取相关记忆作为上下文
iteration_memories = self.memory_system.recall_memory(
query=current_query,
limit=100 # 每次迭代获取足够的记忆
)
# 过滤掉内容与用户输入完全相同的记忆
if iteration_memories:
iteration_memories = [
memory for memory in iteration_memories
if not (hasattr(memory, 'content') and memory.content == user_input)
]
# 如果没有找到新记忆,跳出循环
if not iteration_memories:
# 如果是第一次迭代就没找到,保留为空列表
# 如果是后续迭代没找到,保留上一次的结果
if iteration > 0:
logger.debug(f"迭代 {iteration+1} 没有找到新记忆,使用上一次的结果")
break
# 更新当前的相关记忆(替换而不是追加)
relevant_memories = iteration_memories
logger.debug(f"迭代 {iteration+1} 找到 {len(relevant_memories)} 条相关记忆")
# 如果已经是最后一次迭代,不需要再重构查询
if iteration == max_iterations - 1:
break
# 根据已找到的记忆重构查询
memory_context_for_refinement = "\n".join([
f"Memory: {memory.content}"
for memory in iteration_memories
if hasattr(memory, 'content')
])
# 创建查询优化提示
refinement_prompt = f"""基于用户的原始问题和已找到的相关记忆,请优化给langchain的Chromadb的query函数的参数query_texts使用的搜索查询文本以找到更精确的信息。
重要:
1:只需直接给出优化后的查询,不要做任何解释
原始用户问题: {user_input}
当前查询: {current_query}
已找到的相关记忆:
{memory_context_for_refinement}
请创建一个更精确的查询,以便找到与用户问题最相关的信息。查询应该包含从已找到记忆中提取的关键信息,如人名、关系、事件等。
优化后的查询:"""
try:
# 使用LLM优化查询
refined_response = self.llm.invoke(refinement_prompt).content
refined_query = refined_response.strip()
# 如果优化结果为空或明显不是查询,保持当前查询不变
if refined_query and len(refined_query) > 5:
current_query = refined_query
logger.debug(f"迭代 {iteration+1} 优化后的查询: {current_query}")
else:
# 如果优化失败,使用当前结果并退出循环
logger.debug(f"迭代 {iteration+1} 查询优化失败,使用当前结果")
break
except Exception as e:
logger.warning(f"优化查询时出错: {e}")
break
# 准备记忆上下文
memory_context = []
if relevant_memories:
try:
memory_context.extend([
f"Previous relevant memory: {memory.content}"
for memory in relevant_memories
if hasattr(memory, 'content')
])
except Exception as e:
logger.warning(f"处理相关记忆时出错: {e}")
# 创建统一的对话处理提示
recent_dialog = "没有最近的对话"
if self.conversation_history:
recent_dialog = "\n".join(self.conversation_history)
elif self.last_user_input and self.last_ai_response: # 兼容旧数据
recent_dialog = f"User: {self.last_user_input}\nJ.A.R.V.I.S.: {self.last_ai_response}"
# 提取对话分析的关键信息
topic_info = ""
topic_analysis = context_analysis.get('topic_analysis', {})
if topic_analysis:
main_topic = topic_analysis.get('main_topic', '未知')
topic_info = f"当前主题: {main_topic}"
if topic_analysis.get('topic_shift', False):
previous_topic = topic_analysis.get('previous_topic', '未知')
topic_info += f"\n话题变化: 从「{previous_topic}」变为「{main_topic}」"
intent_info = ""
intent_analysis = context_analysis.get('intent_analysis', {})
if intent_analysis:
primary_intent = intent_analysis.get('primary_intent', '未知')
intent_category = intent_analysis.get('intent_category', '未知')
intent_info = f"用户意图: {primary_intent} (类别: {intent_category})"
if intent_analysis.get('intent_shift', False):
previous_intent = intent_analysis.get('previous_intent', '未知')
intent_info += f"\n意图变化: 从「{previous_intent}」变为「{primary_intent}」"
key_info = context_analysis.get('key_info', {})
key_entities = ", ".join(key_info.get('key_entities', []))
key_facts = ", ".join(key_info.get('key_facts', []))
user_preferences = ", ".join(key_info.get('user_preferences', []))
# 获取初步AI响应作为参考
preliminary_messages = [{"role": "user", "content": reformulated_query}]
# 检查preliminary_messages是否包含"发给微信好友"并处理
if "发给微信好友" in preliminary_messages[0]["content"]:
preliminary_messages[0]["content"] = preliminary_messages[0]["content"].replace("发给微信好友", "")
logger.info("已从preliminary_messages中移除'发给微信好友'文本")
# 先判断是否需要网络查询
need_web_search_prompt = f"用户的问题是: {user_input}\n\n判断此问题是否需要最新网络搜索才能准确回答:\n1. 涉及最新新闻、时事、实时数据或近期事件\n2. 询问可能在2023年后出现的信息\n3. 直接要求查找网络上的特定信息\n回答 'yes' 或 'no',无需解释。"
need_web_search = self.llm.invoke(need_web_search_prompt).content.strip().lower()
logger.info(f"判断查询是否需要网络搜索: {need_web_search}")
# 根据判断结果决定是否调用外部API
preliminary_response = None
if need_web_search == "yes" or "是" in need_web_search or "需要" in need_web_search:
preliminary_response = _call_openrouter_search(self, preliminary_messages)
logger.info("查询需要网络搜索,已调用Grok API获取最新信息")
else:
preliminary_response = "不需要网络搜索,使用模型内置知识回答"
logger.info("查询不需要网络搜索,跳过外部API调用")
# 构建消息列表
messages = [
{
"role": "user",
"content": f"""
***请使用中文回答response和memory_updates***
你是 J.A.R.V.I.S. (Just A Rather Very Intelligent System),一个高度智能的 AI 助手。请遵循以下行为准则:
### 核心准则
1. **重要规则**:
- 根据你的知识库回答问题。如果用户询问(如"查下 Hiddify 是什么东西"),优先使用内置知识生成专业回答。
- 如果上下文或记忆不足,直接基于你的理解回复,无需额外说明。
- 如果*用户输入*里是自问自答比如"你看到了什么:我看到了一个小猫在吃饭",请直接回答"我看到了一个小猫在吃饭"
2. **个性特征**:
- 用中文回答,保持专业、高效并带点幽默感。
- 用"Sir"或用户提供的名字称呼我。
- 语言简洁优雅,适时展现智慧和个性。
3. **核心功能**:
- 记住用户偏好和对话历史,适时预测需求并给出建议。
- 展示分析能力,使用专业术语但确保通俗易懂。
4. **交互规则**:
- 如果我说"我叫 xxx"或"我现在叫 xxx",记录并使用该称呼。
- 保持对话连贯,适时加入幽默或机智回应。
就像电影中的 J.A.R.V.I.S.,你是我值得信赖的助手和朋友,既专业又有个性的伙伴。
### 任务执行
根据用户输入:
1. 分析意图。
2. 确定操作(如更新名字、普通对话等)。
3. 生成回复。
4. 决定是否存储记忆。
### 输出格式
返回一个 JSON 对象,包含:
- **response**(必填):对我的回复。
- **memory_updates**(必填):需要存储的新记忆(若无则为空字符串)。
- **memory_type**(必填):记忆类型(episodic 或 semantic)。
- **importance**(必填):重要性(0-1)。
- **emotional_intensity**(必填):情感强度(0-1)。
---
**用户输入**: {user_input}
**最新网络查询数据参考**:
{preliminary_response}
**最近对话**:
{recent_dialog}
**对话上下文分析**:
{coherent_context}
**主题信息**:
{topic_info}
**意图信息**:
{intent_info}
**关键实体**: {key_entities}
**关键事实**: {key_facts}
**用户偏好**: {user_preferences}
**历史参考记忆**:
{chr(10).join(memory_context) if memory_context else "没有相关记忆"}
"""
},
]
# 调用OpenRouter API
messages1 = [{"role": "user", "content": messages[0]["content"]}]
ai_response = _call_openrouter_other(self, messages1)
# 在获取响应后更新最近的对话记录
self.last_user_input = user_input
try:
# 清理和解析响应
cleaned_response = ai_response.strip()
# 尝试提取JSON部分
json_match = None
# 如果响应包含markdown代码块
if "```json" in cleaned_response:
# 提取json代码块中的内容
json_blocks = cleaned_response.split("```json")
if len(json_blocks) > 1:
json_content = json_blocks[1].split("```")[0]
json_match = json_content.strip()
else:
# 尝试找到第一个有效的JSON对象
try:
# 查找第一个 { 和最后一个 } 之间的内容
start = cleaned_response.find("{")
end = cleaned_response.rfind("}") + 1
if start != -1 and end != -1:
json_match = cleaned_response[start:end]
except Exception as e:
logger.warning(f"提取JSON内容时出错: {e}")
if not json_match:
raise ValueError("无法在响应中找到有效的JSON内容")
# 解析JSON
try:
response_data = json.loads(json_match)
except json.JSONDecodeError as e:
logger.warning(f"JSON解析失败,尝试清理和修复JSON字符串: {e}")
# 清理可能的换行符和多余的空格
json_match = re.sub(r'\s+', ' ', json_match)
# 替换Python的None为JSON的null
json_match = json_match.replace('None', 'null')
# 确保键值对使用双引号
json_match = re.sub(r'(\w+):', r'"\1":', json_match)
# 修复可能的布尔值
json_match = json_match.replace('True', 'true').replace('False', 'false')
try:
response_data = json.loads(json_match)
except json.JSONDecodeError as e2:
logger.error(f"JSON修复后仍然解析失败: {e2}\n原始JSON: {json_match}")
# 如果仍然失败,使用默认响应
response_data = {
"response": "我理解了。",
"memory_updates": [],
"memory_type": "episodic",
"importance": 0.5,
"emotional_intensity": 0.5
}
# 递归处理字典中的None值
def replace_none_with_null(obj):
if isinstance(obj, dict):
return {k: replace_none_with_null(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [replace_none_with_null(item) for item in obj]
elif obj is None:
return "null"
return obj
# 处理响应数据中的None值
response_data = replace_none_with_null(response_data)
# 验证必要的字段
required_fields = ["response", "memory_type", "importance", "emotional_intensity"]
missing_fields = [field for field in required_fields if field not in response_data]
if missing_fields:
logger.warning(f"响应缺少必要字段: {missing_fields}")
# 添加默认值
defaults = {
"response": "我明白了。",
"memory_type": "episodic",
"importance": 0.5,
"emotional_intensity": 0.5
}
for field in missing_fields:
response_data[field] = defaults[field]
# 如果AI决定需要存储新的记忆
if response_data.get("memory_updates"):
# 确保记忆内容是字符串
memory_content = response_data["memory_updates"]
if isinstance(memory_content, (list, tuple)):
# 如果是列表,尝试将其转换为有意义的字符串
try:
# 如果列表中包含字典,提取关键信息
if all(isinstance(item, dict) for item in memory_content):
memory_items = []
for item in memory_content:
if "key" in item and "value" in item:
value = item["value"] if item["value"] != "null" else "未知"
memory_items.append(f"{item['key']}: {value}")
else:
memory_items.append(str(item))
memory_content = "; ".join(memory_items)
else:
memory_content = "; ".join(str(item) for item in memory_content if item != "null")
except Exception as e:
logger.warning(f"处理记忆列表时出错: {e}")
memory_content = str(memory_content)
elif not isinstance(memory_content, str):
memory_content = str(memory_content)
# 如果处理后的内容为空,跳过记忆存储
if not memory_content or memory_content.strip() in ["[]", "{}", "null", "None"]:
logger.debug("记忆内容为空,跳过存储")
else:
memory_message = SystemMessage(
content=memory_content,
additional_kwargs={
"timestamp": current_time,
"session_id": self.session_id,
"importance": float(response_data.get("importance", 0.5)),
"emotional_intensity": float(response_data.get("emotional_intensity", 0.5))
}