aifeifei798 commited on
Commit
3d40769
·
verified ·
1 Parent(s): 1495603

Upload 8 files

Browse files
app.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import time
4
+
5
+ # ------------------------------------------------------------------
6
+ # 1. 加载环境变量 (在Hugging Face Spaces中从Secrets加载)
7
+ # ------------------------------------------------------------------
8
+ # 使用os.environ.get()来安全地获取,如果没有设置,可以给一个默认值或报错
9
+ # 这里假设你在Hugging Face Spaces的Secrets中设置了'GEMINI_API_KEY'
10
+ api_key = os.environ.get("GEMINI_API_KEY")
11
+ if not api_key:
12
+ print("警告:未找到 GEMINI_API_KEY。请在Hugging Face Spaces的Secrets中设置它。")
13
+ # 为了本地测试,可以从.env文件加载(需要安装python-dotenv)
14
+ # from dotenv import load_dotenv
15
+ # load_dotenv()
16
+ # api_key = os.environ.get("GEMINI_API_KEY")
17
+
18
+ # ------------------------------------------------------------------
19
+ # 2. 初始化后端 (这是整个系统的启动点)
20
+ # ------------------------------------------------------------------
21
+ print("--- 正在启动 AI 助理系统 ---")
22
+
23
+ # 导入我们的核心模块
24
+ # 使用try...except来处理可能的导入错误,这在调试时很有用
25
+ try:
26
+ from database.setup import initialize_system
27
+ from core.agent import SmartAIAgent
28
+
29
+ print("核心模块导入成功。")
30
+ except ImportError as e:
31
+ print(f"导入模块时出错: {e}")
32
+ print("请确保所有项目文件都已正确放置。")
33
+ # 如果导入失败,系统无法运行,这里可以抛出异常或退出
34
+ raise
35
+
36
+ # 执行一次性的系统初始化(创建数据库、向量索引、加载工具等)
37
+ # 这个函数应该被设计为幂等的,即多次运行不会产生副作用
38
+ try:
39
+ # initialize_system() 函数将负责所有数据库和向量库的设置
40
+ # 它会返回一个已注册工具的列表或其他必要信息
41
+ registered_tools, tool_recommender = initialize_system()
42
+ print("系统数据库和工具推荐器初始化完成。")
43
+
44
+ # 创建 AI 智能体实例,并将推荐器和工具传递给它
45
+ # Agent需要知道所有可以执行的工具函数
46
+ agent = SmartAIAgent(
47
+ tool_recommender=tool_recommender,
48
+ registered_tools=registered_tools,
49
+ api_key=api_key,
50
+ )
51
+ print("AI 智能体核心已成功创建。")
52
+ except Exception as e:
53
+ print(f"系统初始化过程中发生严重错误: {e}")
54
+ agent = None # 标记Agent不可用
55
+ # 在Gradio界面上可以显示错误信息
56
+
57
+ print("--- AI 助理系统已准备就绪 ---")
58
+
59
+ # ------------------------------------------------------------------
60
+ # 3. Gradio 事件处理函数
61
+ # ------------------------------------------------------------------
62
+
63
+
64
+ def handle_user_message(user_input, history):
65
+ """
66
+ 当用户发送消息时,此函数首先被调用。
67
+ 它将用户的消息添加到聊天历史记录中。
68
+ """
69
+ if not user_input.strip():
70
+ # 如果用户输入为空,不做任何事
71
+ return "", history
72
+ # 将用户消息和一条空的机器人消息占位符添加到历史
73
+ history.append((user_input, None))
74
+ # 返回空字符串以清空输入框,并返回更新后的历史记录
75
+ return "", history
76
+
77
+
78
+ def generate_bot_response(history):
79
+ """
80
+ 此函数以流式方式生成机器人的响应。
81
+ 它调用Agent核心来处理最新的用户消息。
82
+ """
83
+ if agent is None:
84
+ # 如果Agent初始化失败,返回错误信息
85
+ history[-1][1] = "抱歉,AI助理系统初始化失败,无法提供服务。"
86
+ yield history
87
+ return
88
+
89
+ # 获取最新的用户问题
90
+ user_question = history[-1][0]
91
+
92
+ # 初始化一个空的机器人消息
93
+ bot_message = ""
94
+ history[-1][1] = bot_message
95
+
96
+ try:
97
+ # 调用Agent的流式处理方法
98
+ # agent.stream_run() 应该是一个生成器,逐步yield出思考过程和最终答案
99
+ for chunk in agent.stream_run(user_question):
100
+ # 将每个新的文本块追加到机器人消息中
101
+ bot_message += chunk
102
+ # 更新历史记录中最后一个元组的机器人部分
103
+ history[-1][1] = bot_message
104
+ # yield更新后的历史记录,Gradio会用它来刷新界面
105
+ yield history
106
+ # (可选) 增加一个微小的延迟,让流式效果更明显
107
+ time.sleep(0.01)
108
+
109
+ except Exception as e:
110
+ # 如果在处理过程中发生错误,将错误信息显示给用户
111
+ error_message = f"\n\n抱歉,处理您的请求时发生了错误:\n`{str(e)}`"
112
+ history[-1][1] += error_message
113
+ yield history
114
+
115
+
116
+ # ------------------------------------------------------------------
117
+ # 4. 创建 Gradio 界面
118
+ # ------------------------------------------------------------------
119
+
120
+ # 自定义CSS来美化界面
121
+ custom_css = """
122
+ /* 简单的CSS自定义,让界面更好看 */
123
+ #chatbot .user {
124
+ background-color: #E0F7FA; /* 浅青色背景 */
125
+ }
126
+ #chatbot .bot {
127
+ background-color: #F1F8E9; /* 浅绿色背景 */
128
+ }
129
+ """
130
+
131
+ with gr.Blocks(
132
+ theme=gr.themes.Soft(primary_hue="teal", secondary_hue="lime"),
133
+ css=custom_css,
134
+ title="智能 AI 助理",
135
+ ) as demo:
136
+ gr.Markdown(
137
+ """
138
+ # 🚀 智能 AI 助理 Demo
139
+ ### (LangChain + LlamaIndex + Gemini)
140
+ 这是一个演示如何结合 LangChain (智能体编排) 和 LlamaIndex (工具检索) 构建高级AI助理的Demo。
141
+ - **提问:** 在下面的文本框中输入你的问题。
142
+ - **观察:** 观察AI的思考过程,包括它如何推荐、选择和调用工具。
143
+ """
144
+ )
145
+
146
+ chatbot = gr.Chatbot(
147
+ [],
148
+ elem_id="chatbot",
149
+ label="聊天窗口",
150
+ bubble_full_width=False,
151
+ height=650,
152
+ avatar_images=(
153
+ None,
154
+ "https://raw.githubusercontent.com/gradio-app/gradio/main/guides/assets/logo.png",
155
+ ), # 机器人用Gradio logo
156
+ )
157
+
158
+ with gr.Row():
159
+ text_input = gr.Textbox(
160
+ scale=4,
161
+ show_label=False,
162
+ placeholder="例如: '苹果公司(AAPL)今天的股价是多少?' 或 '关于AI的最新进展有什么新闻?'",
163
+ container=False,
164
+ )
165
+ submit_button = gr.Button("发送", variant="primary", scale=1, min_width=150)
166
+
167
+ # 示例问题,方便用户快速体验
168
+ gr.Examples(
169
+ examples=[
170
+ "苹果公司(AAPL)的股价是多少?",
171
+ "关于AI驱动的药物发现有什么最新新闻?",
172
+ "你好,你能做什么?",
173
+ "用Python写一个快速排序算法", # 测试不使用工具的场景
174
+ ],
175
+ inputs=text_input,
176
+ )
177
+
178
+ # 定义事件的触发流程
179
+ # 当用户提交输入时(点击按钮或按回车)
180
+ submit_event = text_input.submit(
181
+ fn=handle_user_message,
182
+ inputs=[text_input, chatbot],
183
+ outputs=[text_input, chatbot],
184
+ queue=False, # 立即执行,不排队
185
+ ).then(
186
+ fn=generate_bot_response,
187
+ inputs=[chatbot],
188
+ outputs=[chatbot],
189
+ )
190
+
191
+ submit_button.click(
192
+ fn=handle_user_message,
193
+ inputs=[text_input, chatbot],
194
+ outputs=[text_input, chatbot],
195
+ queue=False,
196
+ ).then(
197
+ fn=generate_bot_response,
198
+ inputs=[chatbot],
199
+ outputs=[chatbot],
200
+ )
201
+
202
+ # ------------------------------------------------------------------
203
+ # 5. 启动应用
204
+ # ------------------------------------------------------------------
205
+ if __name__ == "__main__":
206
+ # 使用 .queue() 来允许多个用户同时使用,这在Hugging Face Spaces上是推荐做法
207
+ demo.queue()
208
+ # .launch() 会启动Web服务器
209
+ # 在Hugging Face Spaces上,它会自动找到并运行这个
210
+ demo.launch(debug=True) # debug=True可以在本地看到更详细的日志
core/agent.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/agent.py
2
+
3
+ from langchain_google_genai import ChatGoogleGenerativeAI
4
+ from langchain.agents import AgentExecutor, create_json_chat_agent
5
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
6
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
7
+ from typing import List, Any
8
+ import json
9
+
10
+ from .tool_recommender import LlamaIndexToolRecommender
11
+ from tools.tool_registry import get_tool_by_name
12
+
13
+ # Agent的思考模板
14
+ AGENT_PROMPT_TEMPLATE = """
15
+ 你是一个强大的AI助理。你的任务是理解用户的问题,并决定是否需要使用工具来回答。
16
+
17
+ 你有以下工具可用:
18
+ {tools}
19
+
20
+ 如果需要使用工具,请严格按照以下JSON格式进行响应,不要包含任何其他文本或解释:
21
+ {{
22
+ "tool": "要调用的工具名称",
23
+ "tool_input": {{ "参数1": "值1", "参数2": "值2" }}
24
+ }}
25
+
26
+ 如果不需要使用任何工具,请直接回答用户的问题。
27
+
28
+ 这是对话历史:
29
+ {chat_history}
30
+
31
+ 用户问题:{input}
32
+
33
+ 现在,请你思考并作出回应(JSON或直接回答):
34
+ """
35
+
36
+
37
+ class SmartAIAgent:
38
+ def __init__(
39
+ self,
40
+ tool_recommender: LlamaIndexToolRecommender,
41
+ registered_tools: List[Any],
42
+ api_key: str,
43
+ ):
44
+ self.tool_recommender = tool_recommender
45
+ self.registered_tools = registered_tools
46
+ self.llm = ChatGoogleGenerativeAI(
47
+ model="gemini-1.5-pro-latest",
48
+ google_api_key=api_key,
49
+ convert_system_message_to_human=True, # 兼容性设置
50
+ )
51
+ self.chat_history = []
52
+ print("LangChain Agent已初始化,使用Gemini 1.5 Pro。")
53
+
54
+ def _format_tools_for_prompt(self, tools: List[dict]) -> str:
55
+ """将工具列表格式化为清晰的字符串,用于Prompt。"""
56
+ if not tools:
57
+ return "没有可用的工具。"
58
+
59
+ tool_strings = []
60
+ for tool in tools:
61
+ # 解析JSON字符串参数
62
+ params = json.loads(tool["parameters"])
63
+ param_str = ", ".join(
64
+ [f"{p_name}: {p_type}" for p_name, p_type in params.items()]
65
+ )
66
+ tool_strings.append(
67
+ f"- 工具名称: {tool['name']}\n - 描述: {tool['description']}\n - 参数: {param_str}"
68
+ )
69
+ return "\n".join(tool_strings)
70
+
71
+ def _format_chat_history(self) -> str:
72
+ """格式化聊天历史。"""
73
+ return "\n".join([f"{msg.type}: {msg.content}" for msg in self.chat_history])
74
+
75
+ def stream_run(self, user_input: str):
76
+ """
77
+ 处理用户输入的流式方法。
78
+ 这是一个生成器,会逐步yield出思考过程和结果。
79
+ """
80
+ # 1. 将用户输入添加到历史记录
81
+ self.chat_history.append(HumanMessage(content=user_input))
82
+ yield "🤔 正在分析您的问题...\n"
83
+
84
+ # 2. 调用工具推荐系统
85
+ yield "🔍 正在从工具库中推荐相关工具...\n"
86
+ recommended_tools_meta = self.tool_recommender.recommend_tools(user_input)
87
+
88
+ if not recommended_tools_meta:
89
+ yield "ℹ️ 未找到相关工具,将直接回答。\n"
90
+ recommended_tools_prompt = "没有推荐的工具。"
91
+ else:
92
+ tool_names = [t["name"] for t in recommended_tools_meta]
93
+ yield f"✅ 推荐工具: `{', '.join(tool_names)}`\n"
94
+ recommended_tools_prompt = self._format_tools_for_prompt(
95
+ recommended_tools_meta
96
+ )
97
+
98
+ # 3. 构建Agent Prompt,让LLM决策
99
+ yield "🧠 正在让AI大脑(Gemini Pro)决定如何行动...\n"
100
+ prompt = AGENT_PROMPT_TEMPLATE.format(
101
+ tools=recommended_tools_prompt,
102
+ chat_history=self._format_chat_history(),
103
+ input=user_input,
104
+ )
105
+
106
+ # 4. 调用LLM获取决策
107
+ llm_response = self.llm.invoke(prompt)
108
+ llm_decision_content = llm_response.content
109
+
110
+ # 5. 解析和执行决策
111
+ try:
112
+ # 尝试将LLM的响应解析为JSON
113
+ decision = json.loads(llm_decision_content)
114
+ tool_name = decision.get("tool")
115
+ tool_input = decision.get("tool_input")
116
+
117
+ yield f"💡 AI决策:调用工具 `{tool_name}`,参数为 `{tool_input}`\n"
118
+
119
+ # 查找并执行工具
120
+ tool_to_execute = get_tool_by_name(tool_name)
121
+ if tool_to_execute:
122
+ yield f"⚙️ 正在执行工具 `{tool_name}`...\n"
123
+ tool_output = tool_to_execute.invoke(tool_input)
124
+ yield f"📊 工具返回结果:\n---\n{str(tool_output)[:500]}...\n---\n"
125
+
126
+ # 将工具调用和结果添加到历史
127
+ self.chat_history.append(AIMessage(content=llm_decision_content))
128
+ self.chat_history.append(
129
+ ToolMessage(content=str(tool_output), tool_call_id="N/A")
130
+ ) # 简单记录
131
+
132
+ # 6. 基于工具结果生成最终答案
133
+ yield "✍️ 正在根据工具结果生成最终回答...\n\n"
134
+ final_answer_prompt = f"""
135
+ 基于以下对话历史和最新的工具结果,请为用户生成一个最终的、完整的、自然的回答。
136
+
137
+ 对话历史:
138
+ {self._format_chat_history()}
139
+
140
+ 请直接回答,不要提及你的思考过程。
141
+ """
142
+ final_answer_stream = self.llm.stream(final_answer_prompt)
143
+ full_final_answer = ""
144
+ for chunk in final_answer_stream:
145
+ yield chunk.content
146
+ full_final_answer += chunk.content
147
+
148
+ # 将最终答案添加到历史
149
+ self.chat_history.append(AIMessage(content=full_final_answer))
150
+
151
+ else:
152
+ yield f"❌ 错误:AI决策调用的工具 `{tool_name}` 不存在。\n"
153
+
154
+ except (json.JSONDecodeError, KeyError):
155
+ # 如果LLM的响应不是JSON,则认为是直接回答
156
+ yield "✅ AI决策:直接回答。\n\n"
157
+ # 直接将LLM的响应作为最终答案
158
+ yield llm_decision_content
159
+ self.chat_history.append(AIMessage(content=llm_decision_content))
core/tool_recommender.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/tool_recommender.py
2
+
3
+ import sqlite3
4
+ from pymilvus import MilvusClient
5
+ from llama_index.embeddings.google import GooglePairedEmbeddings
6
+ from typing import List, Dict
7
+
8
+
9
+ class LlamaIndexToolRecommender:
10
+ """
11
+ 使用LlamaIndex的嵌入模型和Milvus Lite进行工具推荐。
12
+ """
13
+
14
+ def __init__(self, milvus_client: MilvusClient, sqlite_db_path: str):
15
+ self.milvus_client = milvus_client
16
+ self.sqlite_db_path = sqlite_db_path
17
+ self.collection_name = "tool_embeddings"
18
+
19
+ try:
20
+ self.embed_model = GooglePairedEmbeddings(
21
+ model_name="models/text-embedding-004",
22
+ task_type="retrieval_query", # 用于查询的嵌入
23
+ )
24
+ except Exception as e:
25
+ print(f"错误:无法初始化Google嵌入模型。请检查API Key。 - {e}")
26
+ raise
27
+
28
+ print("LlamaIndex工具推荐器已初始化。")
29
+
30
+ def recommend_tools(self, user_query: str, top_k: int = 3) -> List[Dict]:
31
+ """
32
+ 根据用户查询,推荐最相关的top_k个工具。
33
+ 返回一个包含工具元数据字典的列表。
34
+ """
35
+ print(f"\n[推荐系统] 收到查询: '{user_query}'")
36
+
37
+ # 1. 生成查询嵌入
38
+ query_embedding = self.embed_model.get_text_embedding(user_query)
39
+
40
+ # 2. 在Milvus中搜索相似的工具
41
+ search_results = self.milvus_client.search(
42
+ collection_name=self.collection_name,
43
+ data=[query_embedding],
44
+ limit=top_k,
45
+ output_fields=["id"],
46
+ )
47
+
48
+ if not search_results or not search_results[0]:
49
+ print("[推荐系统] 在Milvus中未找到相似工具。")
50
+ return []
51
+
52
+ # 3. 提取推荐的工具ID
53
+ recommended_ids = [hit.id for hit in search_results[0]]
54
+ print(f"[推荐系统] Milvus推荐的工具ID: {recommended_ids}")
55
+
56
+ # 4. 从SQLite中根据ID获取完整的工具元数据
57
+ with sqlite3.connect(self.sqlite_db_path) as conn:
58
+ cursor = conn.cursor()
59
+ # 使用IN子句一次性查询所有ID
60
+ placeholders = ",".join("?" for _ in recommended_ids)
61
+ cursor.execute(
62
+ f"SELECT name, description, parameters FROM tools WHERE id IN ({placeholders})",
63
+ recommended_ids,
64
+ )
65
+ tools_metadata = cursor.fetchall()
66
+
67
+ # 5. 将结果格式化为字典列表
68
+ # 注意:数据库返回的顺序可能与推荐顺序不同,需要重新排序
69
+ id_map = {tool_id: i for i, tool_id in enumerate(recommended_ids)}
70
+
71
+ formatted_tools = []
72
+ for name, description, parameters in tools_metadata:
73
+ # 找到这个工具在推荐列表中的原始ID
74
+ # 这是一个简化的查找,实际中可以做的更高效
75
+ for tool_id in recommended_ids:
76
+ cursor.execute("SELECT name FROM tools WHERE id = ?", (tool_id,))
77
+ if cursor.fetchone()[0] == name:
78
+ formatted_tools.append(
79
+ {
80
+ "name": name,
81
+ "description": description,
82
+ "parameters": parameters,
83
+ "original_rank": id_map.get(tool_id),
84
+ }
85
+ )
86
+
87
+ formatted_tools.sort(key=lambda x: x["original_rank"])
88
+ print(f"[推荐系统] 最终推荐的工具: {[t['name'] for t in formatted_tools]}")
89
+ return [
90
+ {k: v for k, v in t.items() if k != "original_rank"}
91
+ for t in formatted_tools
92
+ ]
database/setup.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # database/setup.py
2
+
3
+ import os
4
+ import sqlite3
5
+ import json
6
+ from pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataType
7
+ from llama_index.embeddings.google import GooglePairedEmbeddings
8
+
9
+ # 导入你的工具注册表
10
+ from tools.tool_registry import get_all_tools
11
+
12
+ # --- 配置持久化路径 ---
13
+ DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
14
+ SQLITE_DB_PATH = os.path.join(DATA_DIR, "tools.metadata.db")
15
+ MILVUS_DATA_PATH = os.path.join(
16
+ DATA_DIR, "milvus_data.db"
17
+ ) # Milvus Lite 将数据存在一个文件中
18
+
19
+ # --- Milvus Lite 配置 ---
20
+ MILVUS_COLLECTION_NAME = "tool_embeddings"
21
+ EMBEDDING_DIM = 768 # Google's text-embedding-004 model dimension
22
+
23
+ # --- 全局变量,避免重复初始化 ---
24
+ _db_initialized = False
25
+ _milvus_initialized = False
26
+
27
+
28
+ def initialize_system():
29
+ """
30
+ 系统的主初始化函数。
31
+ 它会创建目录、设置数据库和向量库,并加载工具。
32
+ 这个函数是幂等的,即多次调用不会产生副作用。
33
+ """
34
+ global _db_initialized, _milvus_initialized
35
+
36
+ print("--- 开始系统初始化 ---")
37
+
38
+ # 1. 创建数据目录
39
+ os.makedirs(DATA_DIR, exist_ok=True)
40
+
41
+ # 2. 初始化SQLite数据库
42
+ if not _db_initialized:
43
+ _init_sqlite_db()
44
+ _db_initialized = True
45
+
46
+ # 3. 初始化Milvus Lite向量数据库
47
+ if not _milvus_initialized:
48
+ milvus_client = _init_milvus_lite()
49
+ _milvus_initialized = True
50
+ else:
51
+ milvus_client = MilvusClient(uri=MILVUS_DATA_PATH)
52
+
53
+ # 4. 获取所有工具定义
54
+ all_tools_definitions = get_all_tools()
55
+
56
+ # 5. 将工具元数据同步到SQLite
57
+ _sync_tools_to_sqlite(all_tools_definitions)
58
+
59
+ # 6. 将工具描述的嵌入同步到Milvus Lite
60
+ _sync_tool_embeddings_to_milvus(milvus_client)
61
+
62
+ # 7. 从LlamaIndex创建工具推荐器 (在这里创建并返回)
63
+ from core.tool_recommender import LlamaIndexToolRecommender
64
+
65
+ tool_recommender = LlamaIndexToolRecommender(
66
+ milvus_client=milvus_client, sqlite_db_path=SQLITE_DB_PATH
67
+ )
68
+
69
+ print("--- 系统初始化完成 ---")
70
+ return all_tools_definitions, tool_recommender
71
+
72
+
73
+ def _init_sqlite_db():
74
+ """初始化SQLite数据库并创建表。"""
75
+ print(f"SQLite DB 路径: {SQLITE_DB_PATH}")
76
+ with sqlite3.connect(SQLITE_DB_PATH) as conn:
77
+ cursor = conn.cursor()
78
+ cursor.execute(
79
+ """
80
+ CREATE TABLE IF NOT EXISTS tools (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ name TEXT UNIQUE NOT NULL,
83
+ description TEXT NOT NULL,
84
+ parameters TEXT NOT NULL -- 存储JSON字符串
85
+ )
86
+ """
87
+ )
88
+ conn.commit()
89
+ print("SQLite DB 表已确认存在。")
90
+
91
+
92
+ def _init_milvus_lite():
93
+ """初始化Milvus Lite并创建集合和索引。"""
94
+ print(f"Milvus Lite 数据路径: {MILVUS_DATA_PATH}")
95
+ client = MilvusClient(uri=MILVUS_DATA_PATH)
96
+
97
+ if not client.has_collection(collection_name=MILVUS_COLLECTION_NAME):
98
+ print(f"Milvus集合 '{MILVUS_COLLECTION_NAME}' 不存在,正在创建...")
99
+ fields = [
100
+ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
101
+ FieldSchema(
102
+ name="embedding", dtype=DataType.FLOAT_VECTOR, dim=EMBEDDING_DIM
103
+ ),
104
+ ]
105
+ schema = CollectionSchema(fields, description="Tool embedding collection")
106
+ client.create_collection(collection_name=MILVUS_COLLECTION_NAME, schema=schema)
107
+
108
+ index_params = client.prepare_index_params()
109
+ index_params.add_index(
110
+ field_name="embedding",
111
+ index_type="AUTOINDEX", # 让Milvus自动选择最佳索引
112
+ metric_type="L2",
113
+ )
114
+ client.create_index(
115
+ collection_name=MILVUS_COLLECTION_NAME, index_params=index_params
116
+ )
117
+ print("Milvus集合和索引创建完成。")
118
+ else:
119
+ print(f"Milvus集合 '{MILVUS_COLLECTION_NAME}' 已存在。")
120
+ # 确保集合已加载到内存中以供搜索
121
+ client.load_collection(collection_name=MILVUS_COLLECTION_NAME)
122
+
123
+ return client
124
+
125
+
126
+ def _sync_tools_to_sqlite(tools_definitions):
127
+ """将工具定义同步到SQLite数据库。"""
128
+ print("正在同步工具元数据到SQLite...")
129
+ with sqlite3.connect(SQLITE_DB_PATH) as conn:
130
+ cursor = conn.cursor()
131
+ for tool in tools_definitions:
132
+ cursor.execute("SELECT id FROM tools WHERE name = ?", (tool.name,))
133
+ if cursor.fetchone() is None:
134
+ # 工具不存在,插入新工具
135
+ cursor.execute(
136
+ "INSERT INTO tools (name, description, parameters) VALUES (?, ?, ?)",
137
+ (tool.name, tool.description, json.dumps(tool.args)),
138
+ )
139
+ print(f" - 新���工具到SQLite: {tool.name}")
140
+ conn.commit()
141
+ print("SQLite同步完成。")
142
+
143
+
144
+ def _sync_tool_embeddings_to_milvus(milvus_client):
145
+ """计算并同步工具描述的嵌入到Milvus Lite。"""
146
+ print("正在同步工具嵌入到Milvus...")
147
+
148
+ # 1. 从SQLite获取所有工具
149
+ with sqlite3.connect(SQLITE_DB_PATH) as conn:
150
+ cursor = conn.cursor()
151
+ cursor.execute("SELECT id, description FROM tools")
152
+ all_tools_in_db = cursor.fetchall()
153
+
154
+ # 2. 获取Milvus中已存在的工具ID
155
+ try:
156
+ existing_milvus_ids_raw = milvus_client.query(
157
+ collection_name=MILVUS_COLLECTION_NAME,
158
+ filter="id > 0",
159
+ output_fields=["id"],
160
+ )
161
+ existing_milvus_ids = {item["id"] for item in existing_milvus_ids_raw}
162
+ except Exception:
163
+ existing_milvus_ids = set()
164
+
165
+ # 3. 找出需要计算嵌入的新工具
166
+ new_tools_to_embed = [
167
+ (tool_id, description)
168
+ for tool_id, description in all_tools_in_db
169
+ if tool_id not in existing_milvus_ids
170
+ ]
171
+
172
+ if not new_tools_to_embed:
173
+ print("所有工具嵌入已是最新,无需同步。")
174
+ return
175
+
176
+ print(f"发现 {len(new_tools_to_embed)} 个新工具需要生成嵌入...")
177
+
178
+ # 4. 初始化嵌入模型
179
+ try:
180
+ # 确保你的API Key已在环境中设置
181
+ embed_model = GooglePairedEmbeddings(
182
+ model_name="models/text-embedding-004",
183
+ task_type="retrieval_document", # 用于存储的文档嵌入
184
+ )
185
+ except Exception as e:
186
+ print(f"错误:无法初始化Google嵌入模型。请检查API Key。 - {e}")
187
+ return
188
+
189
+ # 5. 生成嵌入并准备插入
190
+ tool_ids_to_insert = [tool[0] for tool in new_tools_to_embed]
191
+ descriptions_to_embed = [tool[1] for tool in new_tools_to_embed]
192
+
193
+ embeddings = embed_model.get_text_embedding_batch(
194
+ descriptions_to_embed, show_progress=True
195
+ )
196
+
197
+ data_to_insert = [
198
+ {"id": tool_id, "embedding": embedding}
199
+ for tool_id, embedding in zip(tool_ids_to_insert, embeddings)
200
+ ]
201
+
202
+ # 6. 插入到Milvus
203
+ milvus_client.insert(collection_name=MILVUS_COLLECTION_NAME, data=data_to_insert)
204
+ milvus_client.flush([MILVUS_COLLECTION_NAME]) # 确保数据写入
205
+ print(f"成功将 {len(data_to_insert)} 个新嵌入插入到Milvus。")
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core
2
+ gradio
3
+ python-dotenv
4
+
5
+ # LangChain & LlamaIndex
6
+ langchain
7
+ langchain-core
8
+ langchain-google-genai
9
+ llama-index
10
+ llama-index-embeddings-google
11
+ llama-index-llms-google
12
+
13
+ # Vector DB
14
+ pymilvus-lite
15
+
16
+ # Tools
17
+ requests
18
+ beautifulsoup4
tools/news_tool.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tools/news_tool.py
2
+
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+
6
+
7
+ def search_latest_news(query: str) -> str:
8
+ """
9
+ 使用requests和BeautifulSoup抓取DuckDuckGo搜索结果来模拟新闻搜索。
10
+ """
11
+ print(f"--- 正在执行工具: search_latest_news, 参数: {query} ---")
12
+ headers = {
13
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
14
+ }
15
+ url = f"https://html.duckduckgo.com/html/?q={query}"
16
+
17
+ try:
18
+ response = requests.get(url, headers=headers, timeout=5)
19
+ response.raise_for_status()
20
+
21
+ soup = BeautifulSoup(response.text, "html.parser")
22
+ results = soup.find_all("div", class_="result")
23
+
24
+ if not results:
25
+ return "没有找到相关的新闻报道。"
26
+
27
+ # 提取前三个结果的摘要
28
+ snippets = []
29
+ for result in results[:3]:
30
+ title_tag = result.find("a", class_="result__a")
31
+ snippet_tag = result.find("a", class_="result__snippet")
32
+ if title_tag and snippet_tag:
33
+ title = title_tag.text.strip()
34
+ snippet = snippet_tag.text.strip()
35
+ snippets.append(f"标题: {title}\n摘要: {snippet}\n")
36
+
37
+ return "\n---\n".join(snippets)
38
+
39
+ except requests.RequestException as e:
40
+ return f"搜索新闻时发生网络错误: {e}"
41
+ except Exception as e:
42
+ return f"解析新闻搜索结果时发生错误: {e}"
tools/stock_tool.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tools/stock_tool.py
2
+
3
+ import requests
4
+ import random
5
+
6
+
7
+ def get_stock_price(symbol: str) -> str:
8
+ """
9
+ 模拟获取股票价格的函数。
10
+ 在真实世界中,这里会调用一个真正的金融API。
11
+ """
12
+ print(f"--- 正在执行工具: get_stock_price, 参数: {symbol} ---")
13
+ symbol = symbol.upper()
14
+ # 模拟API调用
15
+ try:
16
+ # 这是一个模拟,实际应该调用如Alpha Vantage, Yahoo Finance等API
17
+ if symbol in ["AAPL", "GOOGL", "MSFT"]:
18
+ price = round(random.uniform(100, 500), 2)
19
+ return f"股票 {symbol} 的模拟实时价格是 ${price}。"
20
+ else:
21
+ return f"找不到股票代码为 {symbol} 的信息。"
22
+ except Exception as e:
23
+ return f"调用股票API时发生错误: {e}"
tools/tool_registry.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tools/tool_registry.py
2
+
3
+ from langchain_core.tools import tool
4
+ from typing import List, Dict, Any
5
+
6
+ # 导入你的实际工具函数
7
+ from .stock_tool import get_stock_price
8
+ from .news_tool import search_latest_news
9
+
10
+ # 使用LangChain的@tool装饰器来定义工具,它会自动处理描述和参数结构
11
+ # 这比手动构建字典更健壮
12
+
13
+
14
+ # 使用 @tool 装饰器定义你的工具
15
+ @tool
16
+ def get_stock_price_tool(symbol: str) -> str:
17
+ """
18
+ 获取指定股票代码(例如AAPL, GOOGL)的实时股票价格。
19
+ 当用户询问特定公司的股价时使用此工具。
20
+ """
21
+ return get_stock_price(symbol)
22
+
23
+
24
+ @tool
25
+ def search_latest_news_tool(query: str) -> str:
26
+ """
27
+ 根据关键词搜索最新的新闻报道。
28
+ 当用户询问关于某个主题的最新动态、事件或新闻时使用此工具。
29
+ """
30
+ return search_latest_news(query)
31
+
32
+
33
+ # 集中管理所有工具
34
+ _all_tools = [
35
+ get_stock_price_tool,
36
+ search_latest_news_tool,
37
+ ]
38
+
39
+
40
+ def get_all_tools() -> List[Any]:
41
+ """返回一个包含所有已定义工具的列表。"""
42
+ return _all_tools
43
+
44
+
45
+ def get_tool_by_name(name: str) -> Any:
46
+ """根据名称查找并返回工具对象。"""
47
+ for tool_obj in _all_tools:
48
+ if tool_obj.name == name:
49
+ return tool_obj
50
+ return None