Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- app.py +210 -0
- core/agent.py +159 -0
- core/tool_recommender.py +92 -0
- database/setup.py +205 -0
- requirements.txt +18 -0
- tools/news_tool.py +42 -0
- tools/stock_tool.py +23 -0
- tools/tool_registry.py +50 -0
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
|