initial commit for prototype

This commit is contained in:
shell committed 2026-11-01 13:02:23 +08:00
commit 2c1bac9bcb
14 files changed
+883

No files matched your search

View File
Whitespace-only changes.
+153
View File
@@ -0,0 +1,153 @@
import json
import os
from typing import Any
from mini_agent.tools.tavily_search import TavilySearch
from openai import OpenAI
from mini_agent.tools.tool_search import ToolSearch
from mini_agent.tools.tool_manager import ToolManager
from dotenv import load_dotenv
load_dotenv()
class ReActAgent:
SYSTEM_PROMPT = """
你是一个能够调用外部工具的智能助手。
工作规则:
1. 首先理解用户的问题,判断是否需要外部工具。
2. 如果需要工具,调用 search_tools 搜索合适的工具。
3. 根据搜索结果选择合适的工具。
4. 工具执行后,根据返回结果判断下一步行动。
5. 如果需要其他工具,可以再次调用 search_tools。
6. 当已经获得足够的信息,能够完整回答用户问题时,直接给出最终答案。
注意事项:
- 不允许编造工具执行结果。
- 不允许调用尚未通过 search_tools 发现的业务工具。
- 工具搜索没有结果时,可以尝试调整关键词重新搜索。
- 如果工具无法完成任务,明确告知用户。
- 不要为了调用工具而调用工具。
- 只有在任务已经完成或明确无法继续时才结束回答。
"""
def __init__(
self,
tool_manager: ToolManager,
model: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
timeout: int | None = None,
max_steps: int = 10,
max_tool_calls: int = 10,
) -> None:
self.model = model or os.getenv("LLM_MODEL_ID")
self.base_url = base_url or os.getenv("LLM_BASE_URL")
self.api_key = api_key or os.getenv("LLM_API_KEY")
self.timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
if not all([self.model, self.base_url, self.api_key]):
raise ValueError(
"模型ID、API密钥和服务地址必须被提供或在.env文件中定义。"
)
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url,
timeout=self.timeout,
)
self.tool_manager = tool_manager
self.max_steps = max_steps
self.max_tool_calls = max_tool_calls
self.search_tool = ToolSearch(self.tool_manager)
self.tool_manager.register(self.search_tool)
web_search_tool = TavilySearch()
web_search_tool.config(**{"api_key":os.getenv("TAVILY_KEY","")})
self.tool_manager.register(web_search_tool)
def run(self, question: str) -> str:
# 已发现的业务工具
# 初始只开放工具发现能力
available_tools = [self.search_tool]
tool_calls_count = 0
messages = [
{"role":"user","content":question}
]
for step in range(self.max_steps):
print(f"\n========== Step {step + 1} ==========")
tools = [self.tool_manager.tool_to_openai(tool) for tool in available_tools]
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
# 模型没有调用工具,说明它选择直接回答
if not message.tool_calls:
if message.content:
print(f"最终回答: {message.content}")
return message.content
raise RuntimeError("模型没有返回有效内容")
# 保存模型的工具调用消息
messages.append(
message.model_dump(exclude_none=True)
)
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
try:
arguments = json.loads(
tool_call.function.arguments or "{}"
)
except json.JSONDecodeError:
observation = json.dumps(
{"error": "工具参数不是合法的 JSON"},
ensure_ascii=False,
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": observation,
})
continue
print(f"调用工具: {tool_name}")
print(f"参数: {arguments}")
observation = self.tool_manager.call_tool(tool_name,arguments)
# 更新当前可用工具
if tool_name == self.search_tool.name:
new_tools = []
for item in observation:
if not any(tool.name == item.name for tool in available_tools):
available_tools.append(item)
new_tools.append(item)
observation = [self.tool_manager.tool_to_openai(tool) for tool in new_tools]
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(observation),
})
raise RuntimeError(
f"Agent 已达到最大执行步数 ({self.max_steps}),任务未完成"
)
View File
Whitespace-only changes.
+60
View File
@@ -0,0 +1,60 @@
from mini_agent.tools.tool import Configuration, Tool,Parameter
from pydantic import BaseModel, Field
from tavily import TavilyClient
class ResultItem(BaseModel):
url:str
title:str
content:str
score:float
raw_content:str | None = None
id:str
class SearchResult(BaseModel):
query:str
answer:str | None
images:list[str] = []
results: list[ResultItem]
class TavilySearch(Tool[SearchResult]):
@property
def name(self)->str:
return "Tavily网络搜索工具 "
@property
def description(self) -> str:
return """
使用 Tavily 搜索引擎进行互联网搜索,获取最新网页信息。
适用于查询时效性信息、新闻、技术文档、产品资料以及需要外部信息验证的问题。
"""
@property
def catalog(self) -> str:
return "web.search"
@property
def configurations(self) -> list[Configuration]:
return [
Configuration(name="api_key",type="str",required=True)
]
@property
def parameters(self) -> list[Parameter]:
return [
Parameter(name="query",type="str",description="query keyword",required=True)
]
def config(self,**kwargs) -> None:
api_key = kwargs.get("api_key",None)
if not api_key:
raise ValueError("API Key is required")
self.client = TavilyClient(api_key=api_key)
def run(self, **kwargs) -> SearchResult:
query = kwargs.get("query",None)
if not query:
raise ValueError("查询参数不能为空")
response = self.client.search(query=query,search_depth="advanced",include_answer="advanced")
return SearchResult.model_validate(response)
+49
View File
@@ -0,0 +1,49 @@
from pydantic import BaseModel
from abc import ABC,abstractmethod
class Parameter(BaseModel):
name:str
type:str
description:str
required:bool =True
class Configuration(BaseModel):
name:str
type:str
required:bool = True
class Tool[T](ABC):
@property
@abstractmethod
def name(self)->str:
pass
@property
@abstractmethod
def description(self)->str:
pass
@property
@abstractmethod
def catalog(self)->str:
pass
@property
@abstractmethod
def parameters(self)->list[Parameter]:
pass
@property
@abstractmethod
def configurations(self)->list[Configuration]:
pass
@abstractmethod
def config(self,**kwargs)->None:
pass
@abstractmethod
def run(self,**kwargs)->T:
pass
+80
View File
@@ -0,0 +1,80 @@
from typing import Any
from mini_agent.tools.tool import Tool
class ToolManager:
_tools:dict[str,Tool] = {}
def register[T](self,tool:Tool[T])->None:
if tool.name in self._tools:
raise ValueError(f"{tool.name} is already registered")
self._tools[tool.name] = tool
def get_tools(self,keyword:str,catalog:str | None)->list[Tool]:
return list(self._tools.values())
def get_tools_for_openai(self,keyword:str,catalog:str|None)->list[dict[str,Any]]:
tools = self.get_tools(keyword,catalog)
result = []
for tool in tools:
result.append(self.tool_to_openai(tool))
return result
def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
tool = self._tools.get(name, None)
if tool is None:
raise ValueError(f"Tool not found: {name}")
return tool.run(**arguments)
def tool_to_openai[T](self,tool:Tool[T])->dict[str,Any]:
type_mapping = {
"str": "string",
"string": "string",
"int": "integer",
"integer": "integer",
"float": "number",
"number": "number",
"bool": "boolean",
"boolean": "boolean",
"list": "array",
"dict": "object",
}
properties = {}
required = []
for parameter in tool.parameters:
json_type = type_mapping.get(parameter.type)
if json_type is None:
raise ValueError(
f"不支持的参数类型: {parameter.type}"
)
properties[parameter.name] = {
"type": json_type,
}
if parameter.description:
properties[parameter.name]["description"] = (
parameter.description
)
if parameter.required:
required.append(parameter.name)
return {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description.strip(),
"parameters": {
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": False,
},
},
}
+43
View File
@@ -0,0 +1,43 @@
from mini_agent.tools.tool import Tool,Parameter,Configuration
from mini_agent.tools.tool_manager import ToolManager
class ToolSearch(Tool[list[Tool]]):
def __init__(self,tool_manager:ToolManager) -> None:
self.tool_manager = tool_manager
@property
def name(self)->str:
return """search_tools"""
@property
def description(self) -> str:
return """
"根据用户需求搜索可用工具。 当需要外部数据、查询业务信息或执行特定操作时,首先使用该工具查找合适的业务工具。可以根据需要多次搜索不同的工具。
"""
@property
def catalog(self) -> str:
return "system"
def config(self, **kwargs) -> None:
pass
@property
def configurations(self) -> list[Configuration]:
return []
@property
def parameters(self) -> list[Parameter]:
return [
Parameter(name="keyword",type="str",description="关键词",required=True),
Parameter(name="catalog",type="str",description="工具分类",required=False)
]
def run(self, **kwargs) -> list[Tool]:
keyword = kwargs.get("keyword", None)
catalog = kwargs.get("catalog", None)
if not keyword:
raise ValueError("keyword is required for query")
return self.tool_manager.get_tools(keyword,catalog)