rootdriver 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rootdriver-0.1.0/PKG-INFO +102 -0
- rootdriver-0.1.0/README.md +89 -0
- rootdriver-0.1.0/pyproject.toml +26 -0
- rootdriver-0.1.0/rootdriver/__init__.py +32 -0
- rootdriver-0.1.0/rootdriver/agent.py +48 -0
- rootdriver-0.1.0/rootdriver/constants.py +2 -0
- rootdriver-0.1.0/rootdriver/conversation.py +72 -0
- rootdriver-0.1.0/rootdriver/engine.py +108 -0
- rootdriver-0.1.0/rootdriver/exception.py +1 -0
- rootdriver-0.1.0/rootdriver.egg-info/PKG-INFO +102 -0
- rootdriver-0.1.0/rootdriver.egg-info/SOURCES.txt +16 -0
- rootdriver-0.1.0/rootdriver.egg-info/dependency_links.txt +1 -0
- rootdriver-0.1.0/rootdriver.egg-info/requires.txt +5 -0
- rootdriver-0.1.0/rootdriver.egg-info/top_level.txt +1 -0
- rootdriver-0.1.0/setup.cfg +4 -0
- rootdriver-0.1.0/tests/test_base_tool.py +103 -0
- rootdriver-0.1.0/tests/test_conversation.py +78 -0
- rootdriver-0.1.0/tests/test_tool.py +76 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rootdriver
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rooted in Origin, Driving All Things
|
|
5
|
+
Author-email: zimvir <zimvir@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: openai>=1.0.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# RootDriver
|
|
15
|
+
|
|
16
|
+
根源出发, 驱动万物
|
|
17
|
+
|
|
18
|
+
一个轻量级的 Python AI Agent 开发框架。
|
|
19
|
+
|
|
20
|
+
## 特性
|
|
21
|
+
|
|
22
|
+
- **简洁易用**:装饰器方式定义工具,快速构建 Agent
|
|
23
|
+
- **模块化设计**:LLM 适配器、工具系统、会话管理解耦
|
|
24
|
+
- **工具调用**:支持 function calling,自动执行工具并返回结果
|
|
25
|
+
- **多适配器**:基于适配器模式,方便扩展其他 LLM Provider
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install rootdriver
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
### 定义工具
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from rootdriver import tool
|
|
39
|
+
|
|
40
|
+
@tool
|
|
41
|
+
def get_weather(city: str) -> str:
|
|
42
|
+
"""获取城市天气"""
|
|
43
|
+
return f"{city} 晴天"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 创建 Agent
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from rootdriver import Agent, AgentLLM, OpenAIAdapter
|
|
50
|
+
agent_llm=AgentLLM(
|
|
51
|
+
adapter=OpenAIAdapter(
|
|
52
|
+
api_key="YOUR_API_KEY",
|
|
53
|
+
base_url="BASE_URL"
|
|
54
|
+
),
|
|
55
|
+
model="gpt-4",
|
|
56
|
+
|
|
57
|
+
),
|
|
58
|
+
agent = Agent(
|
|
59
|
+
agent_llm=agent_llm,
|
|
60
|
+
tools=[get_weather],
|
|
61
|
+
system_prompt="你是一个有用的助手",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 单次对话
|
|
65
|
+
response = agent.talk("北京天气怎么样?")
|
|
66
|
+
print(response)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 使用工具
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# 完整对话循环(包含工具调用)
|
|
73
|
+
response = Agent.react("帮我查下上海天气")
|
|
74
|
+
print(response)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 核心组件
|
|
78
|
+
|
|
79
|
+
| 组件 | 说明 |
|
|
80
|
+
|------|------|
|
|
81
|
+
| `Agent` | 智能体入口,整合 LLM、工具、会话 |
|
|
82
|
+
| `Engine` | 核心引擎,处理对话循环和工具调用 |
|
|
83
|
+
| `Conversation` | 会话管理,维护消息历史 |
|
|
84
|
+
| `LLM` | LLM 调用封装 |
|
|
85
|
+
| `Tool` | 工具集合,管理所有可调用工具 |
|
|
86
|
+
|
|
87
|
+
## 项目结构
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
rootdriver/
|
|
91
|
+
├── agent.py # Agent 智能体
|
|
92
|
+
├── engine.py # 引擎核心
|
|
93
|
+
├── conversation.py # 对话管理
|
|
94
|
+
├── llm/ # LLM 适配器
|
|
95
|
+
├── tool/ # 工具系统
|
|
96
|
+
├── types/ # 类型定义
|
|
97
|
+
└── utils/ # 工具函数
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# RootDriver
|
|
2
|
+
|
|
3
|
+
根源出发, 驱动万物
|
|
4
|
+
|
|
5
|
+
一个轻量级的 Python AI Agent 开发框架。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- **简洁易用**:装饰器方式定义工具,快速构建 Agent
|
|
10
|
+
- **模块化设计**:LLM 适配器、工具系统、会话管理解耦
|
|
11
|
+
- **工具调用**:支持 function calling,自动执行工具并返回结果
|
|
12
|
+
- **多适配器**:基于适配器模式,方便扩展其他 LLM Provider
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install rootdriver
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 快速开始
|
|
21
|
+
|
|
22
|
+
### 定义工具
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from rootdriver import tool
|
|
26
|
+
|
|
27
|
+
@tool
|
|
28
|
+
def get_weather(city: str) -> str:
|
|
29
|
+
"""获取城市天气"""
|
|
30
|
+
return f"{city} 晴天"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 创建 Agent
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from rootdriver import Agent, AgentLLM, OpenAIAdapter
|
|
37
|
+
agent_llm=AgentLLM(
|
|
38
|
+
adapter=OpenAIAdapter(
|
|
39
|
+
api_key="YOUR_API_KEY",
|
|
40
|
+
base_url="BASE_URL"
|
|
41
|
+
),
|
|
42
|
+
model="gpt-4",
|
|
43
|
+
|
|
44
|
+
),
|
|
45
|
+
agent = Agent(
|
|
46
|
+
agent_llm=agent_llm,
|
|
47
|
+
tools=[get_weather],
|
|
48
|
+
system_prompt="你是一个有用的助手",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# 单次对话
|
|
52
|
+
response = agent.talk("北京天气怎么样?")
|
|
53
|
+
print(response)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 使用工具
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# 完整对话循环(包含工具调用)
|
|
60
|
+
response = Agent.react("帮我查下上海天气")
|
|
61
|
+
print(response)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 核心组件
|
|
65
|
+
|
|
66
|
+
| 组件 | 说明 |
|
|
67
|
+
|------|------|
|
|
68
|
+
| `Agent` | 智能体入口,整合 LLM、工具、会话 |
|
|
69
|
+
| `Engine` | 核心引擎,处理对话循环和工具调用 |
|
|
70
|
+
| `Conversation` | 会话管理,维护消息历史 |
|
|
71
|
+
| `LLM` | LLM 调用封装 |
|
|
72
|
+
| `Tool` | 工具集合,管理所有可调用工具 |
|
|
73
|
+
|
|
74
|
+
## 项目结构
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
rootdriver/
|
|
78
|
+
├── agent.py # Agent 智能体
|
|
79
|
+
├── engine.py # 引擎核心
|
|
80
|
+
├── conversation.py # 对话管理
|
|
81
|
+
├── llm/ # LLM 适配器
|
|
82
|
+
├── tool/ # 工具系统
|
|
83
|
+
├── types/ # 类型定义
|
|
84
|
+
└── utils/ # 工具函数
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rootdriver"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Rooted in Origin, Driving All Things"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "zimvir", email = "zimvir@qq.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"openai>=1.0.0",
|
|
17
|
+
"pydantic>=2.0.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=7.0.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
packages = ["rootdriver"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
__author__ = "zimvir"
|
|
3
|
+
__email__ = "zimvir@qq.com"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .agent import Agent
|
|
10
|
+
from .engine import Engine
|
|
11
|
+
from .conversation import Conversation
|
|
12
|
+
from .llm import LLM
|
|
13
|
+
from .llm.base_adapter import BaseAdapter
|
|
14
|
+
from .llm.adapter import OpenAIAdapter
|
|
15
|
+
from .tool import tool, Tool
|
|
16
|
+
from .types.agent import AgentLLM
|
|
17
|
+
from .types import Message, ToolDefinition, ToolCall
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Agent",
|
|
21
|
+
"Engine",
|
|
22
|
+
"Conversation",
|
|
23
|
+
"LLM",
|
|
24
|
+
"BaseAdapter",
|
|
25
|
+
"OpenAIAdapter",
|
|
26
|
+
"tool",
|
|
27
|
+
"Tool",
|
|
28
|
+
"AgentLLM",
|
|
29
|
+
"Message",
|
|
30
|
+
"ToolDefinition",
|
|
31
|
+
"ToolCall",
|
|
32
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from .engine import Engine
|
|
5
|
+
from .llm import LLM
|
|
6
|
+
from .conversation import Conversation
|
|
7
|
+
from .tool import Tool, BaseTool
|
|
8
|
+
from .types.agent import AgentLLM
|
|
9
|
+
from .types import ToolDefinition
|
|
10
|
+
from .utils import build_system_message, build_message, build_tool_message, build_user_message, build_assistant_message
|
|
11
|
+
|
|
12
|
+
class Agent:
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
agent_llm: AgentLLM,
|
|
17
|
+
*,
|
|
18
|
+
tools: list[BaseTool] | None = None,
|
|
19
|
+
system_prompt: str | None = None,
|
|
20
|
+
id: str | None = None,
|
|
21
|
+
# save_path: str | Path | None = None,
|
|
22
|
+
# max_retries: int = 3,
|
|
23
|
+
timeout: float | None = None,
|
|
24
|
+
):
|
|
25
|
+
self.conversation = Conversation(system_prompt)
|
|
26
|
+
self.tool = Tool(tools if tools else [])
|
|
27
|
+
self.id = id if id else uuid4().hex
|
|
28
|
+
# self.save_path = save_path
|
|
29
|
+
|
|
30
|
+
self.engine = Engine(
|
|
31
|
+
model=agent_llm.model,
|
|
32
|
+
llm=LLM(agent_llm.adapter),
|
|
33
|
+
conversation=self.conversation,
|
|
34
|
+
tool=self.tool,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def react(self, input_prompt:str) -> "str":
|
|
39
|
+
"""一次 react 循环"""
|
|
40
|
+
response = self.engine.run(build_user_message(input_prompt))
|
|
41
|
+
return response.content
|
|
42
|
+
|
|
43
|
+
def talk(self, input_prompt:str) -> "str":
|
|
44
|
+
"""一次 agent 调用"""
|
|
45
|
+
return self.engine.invoke(build_user_message(input_prompt)).content
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from .types import Message
|
|
2
|
+
from .utils import get_iso_timestamp, build_system_message
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Conversation:
|
|
6
|
+
def __init__(self, system_prompt: str = None):
|
|
7
|
+
self.messages: list[Message] = []
|
|
8
|
+
self.system_prompt = system_prompt
|
|
9
|
+
if self.system_prompt:
|
|
10
|
+
self.append(build_system_message(system_prompt))
|
|
11
|
+
|
|
12
|
+
def append(self, message: Message) -> "Conversation":
|
|
13
|
+
self.messages.append(message)
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def append_many(self, messages: list[Message]) -> "Conversation":
|
|
17
|
+
self.messages.extend(messages)
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
# def append_user(self, content: str) -> "Conversation":
|
|
21
|
+
# self.messages.append(Message(role="user", content=content, created_at=get_iso_timestamp()))
|
|
22
|
+
# return self
|
|
23
|
+
#
|
|
24
|
+
# def append_assistant(self, content: str | None = None, tool_calls: list | None = None) -> "Conversation":
|
|
25
|
+
# msg = Message(role="assistant", content=content, created_at=get_iso_timestamp())
|
|
26
|
+
# if tool_calls:
|
|
27
|
+
# msg.tool_calls = tool_calls
|
|
28
|
+
# self.messages.append(msg)
|
|
29
|
+
# return self
|
|
30
|
+
#
|
|
31
|
+
# def append_tool(self, tool_call_id: str, content: str) -> "Conversation":
|
|
32
|
+
# self.messages.append(Message(
|
|
33
|
+
# role="tool",
|
|
34
|
+
# tool_call_id=tool_call_id,
|
|
35
|
+
# content=content,
|
|
36
|
+
# created_at=get_iso_timestamp()
|
|
37
|
+
# ))
|
|
38
|
+
# return self
|
|
39
|
+
|
|
40
|
+
def append_system(self, content: str) -> "Conversation":
|
|
41
|
+
self.messages.append(Message(role="system", content=content, created_at=get_iso_timestamp()))
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def delete(self, index: int=-1) -> "Conversation":
|
|
45
|
+
self.messages.pop(index)
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def get_messages(self) -> list[Message]:
|
|
49
|
+
'''返回 list[message<obj>]'''
|
|
50
|
+
return self.messages
|
|
51
|
+
|
|
52
|
+
def get_messages_in_list(self) -> list[dict]:
|
|
53
|
+
'''返回 字典加列表组成的message(可转成json用于网络、跨语言传输) 组成的列表'''
|
|
54
|
+
return [m.model_dump(exclude_none=True) for m in self.messages]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict_list(cls, messages: list[dict]) -> "Conversation":
|
|
59
|
+
conv = cls()
|
|
60
|
+
conv.messages = [Message.model_validate(m) for m in messages]
|
|
61
|
+
return conv
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_list(cls, messages: list[Message]) -> "Conversation":
|
|
65
|
+
conv = cls()
|
|
66
|
+
conv.messages = list(messages)
|
|
67
|
+
return conv
|
|
68
|
+
|
|
69
|
+
def clear(self) -> "Conversation":
|
|
70
|
+
self.messages = []
|
|
71
|
+
return self
|
|
72
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from .llm import LLM
|
|
4
|
+
from .conversation import Conversation
|
|
5
|
+
from .tool import Tool
|
|
6
|
+
from .types import LLMRequest, Message
|
|
7
|
+
from .utils import get_iso_timestamp,build_tool_message, build_message
|
|
8
|
+
|
|
9
|
+
class Engine:
|
|
10
|
+
def __init__(self, model:str, llm:LLM, conversation:Conversation, tool:Tool, ):
|
|
11
|
+
self.llm = llm
|
|
12
|
+
self.conversation = conversation
|
|
13
|
+
self.tool = tool
|
|
14
|
+
self.model = model
|
|
15
|
+
|
|
16
|
+
def invoke(self, input_message:Message) -> Message:
|
|
17
|
+
self.conversation.append(input_message)
|
|
18
|
+
message = self.chat()
|
|
19
|
+
return message
|
|
20
|
+
|
|
21
|
+
def chat(self) -> Message:
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
1. 读会话消息
|
|
25
|
+
2. 大模型交流
|
|
26
|
+
3. 追加会话消息
|
|
27
|
+
4. 判断 是否调用工具
|
|
28
|
+
5. 是: tool 返回结果并追加,否: 追加并返回
|
|
29
|
+
"""
|
|
30
|
+
# 1. 读会话消息
|
|
31
|
+
message = self.conversation.get_messages()
|
|
32
|
+
|
|
33
|
+
# 2. llm交互
|
|
34
|
+
response = self.llm.invoke(self.messages_to_llm_request(message))
|
|
35
|
+
|
|
36
|
+
# 3. 追加会话消息
|
|
37
|
+
response_message = self.llm_response_to_message(response)
|
|
38
|
+
self.conversation.append(response_message)
|
|
39
|
+
|
|
40
|
+
return response_message
|
|
41
|
+
|
|
42
|
+
def deal_tool_or_output(self, response_message:Message) -> Message|None:
|
|
43
|
+
"""
|
|
44
|
+
1. 追加输入
|
|
45
|
+
2. 读会话消息
|
|
46
|
+
3. 大模型交流
|
|
47
|
+
4. 追加会话消息
|
|
48
|
+
5. 判断 是否调用工具
|
|
49
|
+
6. 是: tool 返回结果并追加,否: 追加并返回
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# 5. 判断 是否调用工具
|
|
57
|
+
if response_message.tool_calls:
|
|
58
|
+
# 6. 是: tool 返回结果追加
|
|
59
|
+
for tool_call in response_message.tool_calls:
|
|
60
|
+
tool_result = self.tool.invoke(tool_call)
|
|
61
|
+
self.conversation.append(build_tool_message(tool_result.tool_call_id, tool_result.content))
|
|
62
|
+
return None
|
|
63
|
+
else:
|
|
64
|
+
# 6. 否: 追加并返回
|
|
65
|
+
return response_message
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run(self, input_message:Message) ->Message:
|
|
69
|
+
"""
|
|
70
|
+
1. 追加输入
|
|
71
|
+
2. 读会话消息
|
|
72
|
+
3. 大模型交流
|
|
73
|
+
4. 追加会话消息
|
|
74
|
+
5. 判断 是否调用工具
|
|
75
|
+
6. 是: tool 返回结果并追加,否: 追加并返回
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
self.conversation.append(input_message)
|
|
82
|
+
while True:
|
|
83
|
+
|
|
84
|
+
response_message = self.chat()
|
|
85
|
+
result = self.deal_tool_or_output(response_message)
|
|
86
|
+
if result is None:
|
|
87
|
+
continue
|
|
88
|
+
elif isinstance(result, Message):
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def messages_to_llm_request(self,messages:list[Message]) -> LLMRequest:
|
|
99
|
+
return LLMRequest(
|
|
100
|
+
model=self.model,
|
|
101
|
+
messages=messages,
|
|
102
|
+
tools=self.tool.to_definitions(),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def llm_response_to_message(self, response) -> Message:
|
|
106
|
+
"""把 LLM 响应转成 Message 。"""
|
|
107
|
+
return response.message
|
|
108
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rootdriver
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rooted in Origin, Driving All Things
|
|
5
|
+
Author-email: zimvir <zimvir@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: openai>=1.0.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# RootDriver
|
|
15
|
+
|
|
16
|
+
根源出发, 驱动万物
|
|
17
|
+
|
|
18
|
+
一个轻量级的 Python AI Agent 开发框架。
|
|
19
|
+
|
|
20
|
+
## 特性
|
|
21
|
+
|
|
22
|
+
- **简洁易用**:装饰器方式定义工具,快速构建 Agent
|
|
23
|
+
- **模块化设计**:LLM 适配器、工具系统、会话管理解耦
|
|
24
|
+
- **工具调用**:支持 function calling,自动执行工具并返回结果
|
|
25
|
+
- **多适配器**:基于适配器模式,方便扩展其他 LLM Provider
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install rootdriver
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
### 定义工具
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from rootdriver import tool
|
|
39
|
+
|
|
40
|
+
@tool
|
|
41
|
+
def get_weather(city: str) -> str:
|
|
42
|
+
"""获取城市天气"""
|
|
43
|
+
return f"{city} 晴天"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 创建 Agent
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from rootdriver import Agent, AgentLLM, OpenAIAdapter
|
|
50
|
+
agent_llm=AgentLLM(
|
|
51
|
+
adapter=OpenAIAdapter(
|
|
52
|
+
api_key="YOUR_API_KEY",
|
|
53
|
+
base_url="BASE_URL"
|
|
54
|
+
),
|
|
55
|
+
model="gpt-4",
|
|
56
|
+
|
|
57
|
+
),
|
|
58
|
+
agent = Agent(
|
|
59
|
+
agent_llm=agent_llm,
|
|
60
|
+
tools=[get_weather],
|
|
61
|
+
system_prompt="你是一个有用的助手",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 单次对话
|
|
65
|
+
response = agent.talk("北京天气怎么样?")
|
|
66
|
+
print(response)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 使用工具
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# 完整对话循环(包含工具调用)
|
|
73
|
+
response = Agent.react("帮我查下上海天气")
|
|
74
|
+
print(response)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 核心组件
|
|
78
|
+
|
|
79
|
+
| 组件 | 说明 |
|
|
80
|
+
|------|------|
|
|
81
|
+
| `Agent` | 智能体入口,整合 LLM、工具、会话 |
|
|
82
|
+
| `Engine` | 核心引擎,处理对话循环和工具调用 |
|
|
83
|
+
| `Conversation` | 会话管理,维护消息历史 |
|
|
84
|
+
| `LLM` | LLM 调用封装 |
|
|
85
|
+
| `Tool` | 工具集合,管理所有可调用工具 |
|
|
86
|
+
|
|
87
|
+
## 项目结构
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
rootdriver/
|
|
91
|
+
├── agent.py # Agent 智能体
|
|
92
|
+
├── engine.py # 引擎核心
|
|
93
|
+
├── conversation.py # 对话管理
|
|
94
|
+
├── llm/ # LLM 适配器
|
|
95
|
+
├── tool/ # 工具系统
|
|
96
|
+
├── types/ # 类型定义
|
|
97
|
+
└── utils/ # 工具函数
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
rootdriver/__init__.py
|
|
4
|
+
rootdriver/agent.py
|
|
5
|
+
rootdriver/constants.py
|
|
6
|
+
rootdriver/conversation.py
|
|
7
|
+
rootdriver/engine.py
|
|
8
|
+
rootdriver/exception.py
|
|
9
|
+
rootdriver.egg-info/PKG-INFO
|
|
10
|
+
rootdriver.egg-info/SOURCES.txt
|
|
11
|
+
rootdriver.egg-info/dependency_links.txt
|
|
12
|
+
rootdriver.egg-info/requires.txt
|
|
13
|
+
rootdriver.egg-info/top_level.txt
|
|
14
|
+
tests/test_base_tool.py
|
|
15
|
+
tests/test_conversation.py
|
|
16
|
+
tests/test_tool.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rootdriver
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from rootdriver.tool.base_tool import BaseTool
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_weather(city: str) -> str:
|
|
6
|
+
"""获取城市天气"""
|
|
7
|
+
return f"{city} 晴天"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def weather_tool():
|
|
12
|
+
return BaseTool(get_weather)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_base_tool_name(weather_tool):
|
|
16
|
+
assert weather_tool.name == "get_weather"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_base_tool_description(weather_tool):
|
|
20
|
+
assert weather_tool.description == "获取城市天气"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_invoke_success(weather_tool):
|
|
24
|
+
result = weather_tool.invoke({"city": "北京"})
|
|
25
|
+
assert result == "北京 晴天"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_invoke_error_returns_string(weather_tool):
|
|
29
|
+
def bad_func(city: str) -> str:
|
|
30
|
+
raise ValueError("参数错误")
|
|
31
|
+
bad_tool = BaseTool(bad_func)
|
|
32
|
+
result = bad_tool.invoke({"city": "北京"})
|
|
33
|
+
assert isinstance(result, str)
|
|
34
|
+
assert "ValueError" in result or "参数错误" in result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_to_definition(weather_tool):
|
|
38
|
+
definition = weather_tool.to_definition()
|
|
39
|
+
assert definition.name == "get_weather"
|
|
40
|
+
assert definition.description == "获取城市天气"
|
|
41
|
+
assert "properties" in definition.parameters
|
|
42
|
+
assert "city" in definition.parameters["properties"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_custom_name():
|
|
46
|
+
def foo():
|
|
47
|
+
pass
|
|
48
|
+
tool = BaseTool(foo, name="custom_name")
|
|
49
|
+
assert tool.name == "custom_name"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_custom_description():
|
|
53
|
+
def foo():
|
|
54
|
+
"""custom desc"""
|
|
55
|
+
pass
|
|
56
|
+
tool = BaseTool(foo)
|
|
57
|
+
assert tool.description == "custom desc"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_callable(weather_tool):
|
|
61
|
+
result = weather_tool(city="上海")
|
|
62
|
+
assert result == "上海 晴天"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_invoke_retry_success():
|
|
66
|
+
"""retry=1 第一次就成功"""
|
|
67
|
+
call_count = 0
|
|
68
|
+
def counter(city: str) -> str:
|
|
69
|
+
nonlocal call_count
|
|
70
|
+
call_count += 1
|
|
71
|
+
return f"{city} 晴天"
|
|
72
|
+
t = BaseTool(counter)
|
|
73
|
+
result = t.invoke({"city": "北京"}, retry=3)
|
|
74
|
+
assert result == "北京 晴天"
|
|
75
|
+
assert call_count == 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_invoke_retry_then_success():
|
|
79
|
+
"""retry=3 前两次失败,第三次成功"""
|
|
80
|
+
call_count = 0
|
|
81
|
+
def flaky(city: str) -> str:
|
|
82
|
+
nonlocal call_count
|
|
83
|
+
call_count += 1
|
|
84
|
+
if call_count < 3:
|
|
85
|
+
raise ValueError(f"第{call_count}次失败")
|
|
86
|
+
return f"{city} 晴天"
|
|
87
|
+
t = BaseTool(flaky)
|
|
88
|
+
result = t.invoke({"city": "北京"}, retry=3)
|
|
89
|
+
assert result == "北京 晴天"
|
|
90
|
+
assert call_count == 3
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_invoke_retry_all_fail():
|
|
94
|
+
"""retry=3 三次都失败,返回最后一次错误"""
|
|
95
|
+
call_count = 0
|
|
96
|
+
def always_fail(city: str) -> str:
|
|
97
|
+
nonlocal call_count
|
|
98
|
+
call_count += 1
|
|
99
|
+
raise ValueError(f"错误{call_count}")
|
|
100
|
+
t = BaseTool(always_fail)
|
|
101
|
+
result = t.invoke({"city": "北京"}, retry=3)
|
|
102
|
+
assert "错误3" in result
|
|
103
|
+
assert call_count == 3
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from rootdriver.conversation import Conversation
|
|
3
|
+
from rootdriver.types import Message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_conversation_empty():
|
|
7
|
+
conv = Conversation()
|
|
8
|
+
assert conv.get_messages() == []
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_conversation_with_system_prompt():
|
|
12
|
+
conv = Conversation(system_prompt="你是一个助手")
|
|
13
|
+
messages = conv.get_messages()
|
|
14
|
+
assert len(messages) == 1
|
|
15
|
+
assert messages[0].role == "system"
|
|
16
|
+
assert messages[0].content == "你是一个助手"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_append_message():
|
|
20
|
+
conv = Conversation()
|
|
21
|
+
conv.append(Message(role="user", content="你好", created_at="2026-01-01T00:00:00"))
|
|
22
|
+
assert len(conv.get_messages()) == 1
|
|
23
|
+
assert conv.get_messages()[0].content == "你好"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_append_many():
|
|
27
|
+
conv = Conversation()
|
|
28
|
+
messages = [
|
|
29
|
+
Message(role="user", content="你好", created_at="2026-01-01T00:00:00"),
|
|
30
|
+
Message(role="assistant", content="你好!", created_at="2026-01-01T00:00:01"),
|
|
31
|
+
]
|
|
32
|
+
conv.append_many(messages)
|
|
33
|
+
assert len(conv.get_messages()) == 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_append_system():
|
|
37
|
+
conv = Conversation()
|
|
38
|
+
conv.append_system("新的系统提示")
|
|
39
|
+
assert conv.get_messages()[-1].role == "system"
|
|
40
|
+
assert conv.get_messages()[-1].content == "新的系统提示"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_delete_message():
|
|
44
|
+
conv = Conversation()
|
|
45
|
+
conv.append(Message(role="user", content="你好", created_at="2026-01-01T00:00:00"))
|
|
46
|
+
conv.append(Message(role="assistant", content="你好!", created_at="2026-01-01T00:00:01"))
|
|
47
|
+
conv.delete()
|
|
48
|
+
assert len(conv.get_messages()) == 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_clear():
|
|
52
|
+
conv = Conversation(system_prompt="test")
|
|
53
|
+
conv.clear()
|
|
54
|
+
assert len(conv.get_messages()) == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_get_messages_in_list():
|
|
58
|
+
conv = Conversation()
|
|
59
|
+
conv.append(Message(role="user", content="你好", created_at="2026-01-01T00:00:00"))
|
|
60
|
+
result = conv.get_messages_in_list()
|
|
61
|
+
assert isinstance(result, list)
|
|
62
|
+
assert result[0]["content"] == "你好"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_from_dict_list():
|
|
66
|
+
data = [
|
|
67
|
+
{"role": "user", "content": "你好", "created_at": "2026-01-01T00:00:00"},
|
|
68
|
+
]
|
|
69
|
+
conv = Conversation.from_dict_list(data)
|
|
70
|
+
assert len(conv.get_messages()) == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_from_list():
|
|
74
|
+
messages = [
|
|
75
|
+
Message(role="user", content="你好", created_at="2026-01-01T00:00:00"),
|
|
76
|
+
]
|
|
77
|
+
conv = Conversation.from_list(messages)
|
|
78
|
+
assert len(conv.get_messages()) == 1
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from rootdriver.tool.base_tool import BaseTool
|
|
3
|
+
from rootdriver.tool.tools import Tool
|
|
4
|
+
from rootdriver.types.tool import ToolCall
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_weather(city: str) -> str:
|
|
8
|
+
"""获取城市天气"""
|
|
9
|
+
return f"{city} 晴天"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_time(city: str) -> str:
|
|
13
|
+
"""获取城市时间"""
|
|
14
|
+
return f"{city} 10:00"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def weather_tool():
|
|
19
|
+
return BaseTool(get_weather)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def time_tool():
|
|
24
|
+
return BaseTool(get_time)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def tool_registry(weather_tool, time_tool):
|
|
29
|
+
return Tool([weather_tool, time_tool])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_tool_add():
|
|
33
|
+
t = Tool()
|
|
34
|
+
t.add(BaseTool(get_weather))
|
|
35
|
+
assert t.get("get_weather") is not None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tool_get(weather_tool):
|
|
39
|
+
t = Tool([weather_tool])
|
|
40
|
+
assert t.get("get_weather") is weather_tool
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_tool_get_not_found():
|
|
44
|
+
t = Tool()
|
|
45
|
+
assert t.get("not_exist") is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_tool_invoke_success(tool_registry):
|
|
49
|
+
result = tool_registry.invoke(ToolCall(id="1", name="get_weather", arguments={"city": "北京"}))
|
|
50
|
+
assert result.tool_call_id == "1"
|
|
51
|
+
assert result.content == "北京 晴天"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_tool_invoke_not_found(tool_registry):
|
|
55
|
+
result = tool_registry.invoke(ToolCall(id="2", name="not_exist", arguments={}))
|
|
56
|
+
assert result.tool_call_id == "2"
|
|
57
|
+
assert "not found" in result.content
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_tool_invoke_many(tool_registry):
|
|
61
|
+
calls = [
|
|
62
|
+
ToolCall(id="1", name="get_weather", arguments={"city": "北京"}),
|
|
63
|
+
ToolCall(id="2", name="get_time", arguments={"city": "北京"}),
|
|
64
|
+
]
|
|
65
|
+
results = tool_registry.invoke_many(calls)
|
|
66
|
+
assert len(results) == 2
|
|
67
|
+
assert results[0].content == "北京 晴天"
|
|
68
|
+
assert results[1].content == "北京 10:00"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_tool_to_definitions(tool_registry):
|
|
72
|
+
defs = tool_registry.to_definitions()
|
|
73
|
+
assert len(defs) == 2
|
|
74
|
+
names = [d.name for d in defs]
|
|
75
|
+
assert "get_weather" in names
|
|
76
|
+
assert "get_time" in names
|