Skip to content

Commit 81d5553

Browse files
committed
feat: 支持 MCP
1 parent cbb827f commit 81d5553

File tree

11 files changed

+326
-123
lines changed

11 files changed

+326
-123
lines changed

docs/tutorials/agent/agent.md

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ controller = Controller(max_turns=10)
3838
## 启动智能体
3939

4040
```python
41-
_, resp = controller.run(agent=translator, message="请帮我翻译蛋白质。")
41+
_, resp = controller.run_sync(agent=translator, message="请帮我翻译蛋白质。")
4242
```
4343

4444
其中 `message` 参数代表用户的具体指令或者是用户与智能体对话的开始。
4545

46-
`controller.run` 方法返回两个值, 其中第一个值代表最后响应的 `Agent` 对象 (暂时还用不到), 第二个值代表智能体最后的相应内容。在这个例子中, `resp` 的值应该是智能体翻译的结果 (不排除其中包含一些提示语)。
46+
`controller.run_sync` 方法返回两个值, 其中第一个值代表最后响应的 `Agent` 对象 (暂时还用不到), 第二个值代表智能体最后的相应内容。在这个例子中, `resp` 的值应该是智能体翻译的结果 (不排除其中包含一些提示语)。
4747

4848
## 使用外部工具
4949

@@ -253,22 +253,111 @@ from course_graph.agent import Result
253253
result = Result(context_variables={'current_time': '2024/09/02'})
254254
```
255255

256+
## MCP 支持
257+
258+
> [!NOTE]
259+
> 目前 MCP 协议是通过 function call 功能实现的。
260+
261+
### 获取一个 MCP Server
262+
263+
你可以通过 [Model Context Protocol servers](https://github.com/modelcontextprotocol/servers) 等项目获取到大量的 MCP Server 资源。
264+
265+
这里我们使用 Python SDK 实现一个简单的 MCP Server:
266+
267+
```python
268+
from mcp.server.fastmcp import FastMCP
269+
import json
270+
271+
mcp = FastMCP('weather')
272+
273+
@mcp.tool()
274+
def get_weather(city: str) -> str:
275+
""" 获取指定城市当天的天气
276+
277+
Args:
278+
city: 城市名称
279+
280+
Returns:
281+
dict: 天气信息
282+
"""
283+
resp = {
284+
'city': city,
285+
'temperature_high': 20,
286+
'temperature_low': 18,
287+
'temperature_unit': 'C',
288+
'weather': 'sunny'
289+
}
290+
return json.dumps(resp, ensure_ascii=False)
291+
292+
if __name__ == '__main__':
293+
mcp.run(transport='stdio')
294+
```
295+
296+
项目创建以及环境配置可以参考 [官方文档](https://modelcontextprotocol.io/quickstart/server)。 该 Server 可以通过以下命令启动:
297+
298+
```bash
299+
uv --directory project_directory run weather.py
300+
```
301+
302+
### 为智能体添加 MCP Server 并启动
303+
304+
```python
305+
from course_graph.agent import Agent, Controller, MCPServer
306+
from course_graph.llm import Qwen
307+
import asyncio
308+
309+
qwen = Qwen()
310+
311+
async def main():
312+
async with MCPServer(
313+
type='stdio',
314+
command='uv',
315+
args=['--directory', 'project_directory', 'run', 'weather'],
316+
) as mcp_server:
317+
318+
agent = Agent(
319+
llm=qwen,
320+
mcp_server=[mcp_server]
321+
)
322+
await agent.initialize()
323+
controller = Controller()
324+
_, resp = await controller.run(agent, "帮我查询南京今天的天气")
325+
print(resp)
326+
327+
if __name__ == '__main__':
328+
asyncio.run(main())
329+
```
330+
331+
这里有三需要注意:
332+
333+
- MCP Server 必须使用 `async with` 语句块来启动。
334+
335+
- 如果给 `Agent` 传递了 `mcp_server` 参数, 必须调用 `await agent.initialize()` 方法等待初始化完成。
336+
337+
- `Controller` 必须使用异步的 `run` 方法来启动。不能使用同步的 `run_sync` 方法, 因为本质上 `run_sync` 只是 `asyncio.run(run(...))` 的包装。
338+
339+
### MCP Server 与外部工具的比较
340+
341+
这里 MCP 协议虽然是通过 function call 功能实现的, 但是缺失了自动注入上下文变量和当前 Agent 的功能。并且本地外部工具的优先级更高,如果遇到同名的工具函数,会优先调用本地函数。
342+
256343
## Trace
257344

258345
Trace 功能, 可以记录智能体的对话、工具调用、上下文变量变化等历史。 `Controller``trace_callback` 参数可以传递一个回调函数, 当 trace 事件发生时, 会调用该回调函数。
259346

260347
```python
348+
from pprint import pprint
349+
261350
controller = Controller(trace_callback=pprint)
262351
```
263352

264-
该回调函数需要接受一个 `TraceEvent` 类型的参数, 具体来说包含以下几种类型:
353+
该回调函数需要接受一个 `TraceEvent` 类型的参数, 其中事件类型包括:
265354

266-
- `TraceEventUserMessage`: 用户消息
267-
- `TraceEventAgentMessage`: 智能体消息
268-
- `TraceEventAgentSwitch`: 智能体切换
269-
- `TraceEventToolCall`: 工具调用
270-
- `TraceEventToolResult`: 工具调用结果
271-
- `TraceEventContextUpdate`: 上下文变量更新
355+
- `USER_MESSAGE`: 用户消息
356+
- `AGENT_MESSAGE`: 智能体消息
357+
- `AGENT_SWITCH`: 智能体切换
358+
- `TOOL_CALL`: 工具调用
359+
- `TOOL_RESULT`: 工具调用结果
360+
- `CONTEXT_UPDATE`: 上下文变量更新
272361

273362
## 多智能体编排
274363

examples/agent/agents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def transfer_to_alarm_clock_agent():
8484

8585
if __name__ == '__main__':
8686
controller = Controller()
87-
_, resp = controller.run(
87+
_, resp = controller.run_sync(
8888
agent=core_agent,
8989
message='帮我查询一下我这里的天气, 并查询一下我的日程信息。如果日程中有考试的话, 请帮我定一个闹钟, 时间是考试开始前的一个小时。')
9090
pprint(resp)

examples/agent/mcp_server.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# -*- coding: utf-8 -*-
2+
# Create Date: 2025/03/29
3+
# Author: wangtao <wangtao.cpu@gmail.com>
4+
# File Name: examples/agent/mcp_server.py
5+
# Description: 一个简单的 MCP Server 示例
6+
7+
from mcp.server.fastmcp import FastMCP
8+
import json
9+
10+
mcp = FastMCP('weather')
11+
12+
13+
@mcp.tool()
14+
def get_weather(city: str) -> str:
15+
""" 获取指定城市当天的天气
16+
17+
Args:
18+
city: 城市名称
19+
20+
Returns:
21+
dict: 天气信息
22+
"""
23+
resp = {
24+
'city': city,
25+
'temperature_high': 20,
26+
'temperature_low': 18,
27+
'temperature_unit': 'C',
28+
'weather': 'sunny'
29+
}
30+
return json.dumps(resp, ensure_ascii=False)
31+
32+
33+
if __name__ == '__main__':
34+
mcp.run(transport='stdio')

examples/agent/use_mcp.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: utf-8 -*-
2+
# Create Date: 2025/03/29
3+
# Author: wangtao <wangtao.cpu@gmail.com>
4+
# File Name: examples/agent/use_mcp.py
5+
# Description: 使用 MCP 工具
6+
7+
from course_graph.agent import Agent, Controller, MCPServer, TraceEvent
8+
from course_graph.llm import Qwen
9+
import asyncio
10+
qwen = Qwen()
11+
12+
13+
async def main():
14+
async with MCPServer(
15+
type='stdio',
16+
command='uv',
17+
args=['--directory', 'examples/agent', 'run', 'mcp_server.py'],
18+
) as mcp_server:
19+
20+
agent = Agent(
21+
llm=qwen,
22+
mcp_server=[mcp_server]
23+
)
24+
await agent.initialize()
25+
controller = Controller()
26+
_, resp = await controller.run(agent, "帮我查询今天南京的天气")
27+
print(resp)
28+
29+
if __name__ == '__main__':
30+
asyncio.run(main())

examples/agent/workflow.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ def get_english_news_editor_instruction(context_variables: ContextVariables):
4646

4747

4848
def chinese_news_write(controller: Controller) -> str:
49-
_, trans = controller.run(agent=translator)
49+
_, trans = controller.run_sync(agent=translator)
5050
controller.context_variables['trans'] = trans
51-
_, res = controller.run(agent=chinese_news_editor)
51+
_, res = controller.run_sync(agent=chinese_news_editor)
5252
return res
5353

5454

5555
def english_news_write(controller: Controller) -> str:
56-
_, res = controller.run(agent=english_news_editor)
56+
_, res = controller.run_sync(agent=english_news_editor)
5757
return res
5858

5959

src/course_graph/agent/agent.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from openai.types.chat import *
1111
import inspect
1212
import docstring_parser
13-
from typing import Callable
13+
from typing import Callable, Awaitable
1414
from typing import Literal
1515
from openai import NOT_GIVEN, NotGiven
1616
from .mcp import MCPServer
@@ -23,7 +23,7 @@ def __init__(
2323
self,
2424
llm: LLMBase,
2525
name: str = 'Assistant',
26-
functions: list[Callable] = None,
26+
functions: list[Callable | Awaitable] = None,
2727
tool_choice: str | NotGiven | Literal['required', 'auto', 'none'] = NOT_GIVEN,
2828
parallel_tool_calls: bool | NotGiven = NOT_GIVEN,
2929
instruction: str | Callable[[ContextVariables], str] | Callable[[], str] = 'You are a helpful assistant.',
@@ -35,7 +35,7 @@ def __init__(
3535
Args:
3636
llm (LLMBase): 大模型
3737
name (str, optional): 名称. Defaults to 'Assistant'.
38-
functions: (list[Callable], optional): 工具函数. Defaults to None.
38+
functions: (list[Callable | Awaitable], optional): 工具函数. Defaults to None.
3939
parallel_tool_calls: (bool, optional): 允许工具并行调用. Defaults to False.
4040
tool_choice: (Literal['required', 'auto', 'none'] | NotGiven, optional). 强制使用工具函数, 选择模式或提供函数名称. Defaults to NOT_GIVEN.
4141
instruction (str | Callable[[ContextVariables], str] | Callable[[], str], optional): 指令. Defaults to 'You are a helpful assistant.'.
@@ -48,7 +48,7 @@ def __init__(
4848

4949
self.tools: list[ChatCompletionToolParam] = [] # for LLM
5050

51-
self.tool_functions: dict[str, Callable] = {} # for local function call
51+
self.tool_functions: dict[str, Callable | Awaitable] = {} # for local function call
5252
self.mcp_functions: dict[str, MCPServer] = {} # for remote function call
5353

5454
self.parallel_tool_calls = parallel_tool_calls
@@ -150,10 +150,10 @@ def add_tool_call_message(self, tool_content: str, tool_call_id: str) -> None:
150150
}
151151
self.messages.append(message)
152152

153-
def tool(self) -> Callable:
153+
def tool(self) -> Callable | Awaitable:
154154
""" 标记一个外部工具函数
155155
"""
156-
def wrapper(function: Callable) -> Callable:
156+
def wrapper(function: Callable | Awaitable) -> Callable | Awaitable:
157157
self.add_tool_functions(function)
158158
return function
159159
return wrapper
@@ -180,7 +180,7 @@ def add_tools(self, *tools: 'Tool') -> 'Agent':
180180
self.use_agent_variables[function_name] = r
181181
return self
182182

183-
def add_tool_functions(self, *functions: Callable) -> 'Agent':
183+
def add_tool_functions(self, *functions: Callable | Awaitable) -> 'Agent':
184184
""" 添加外部工具函数,并从自动解析函数描述、参数类型、以及参数描述等信息 \n
185185
若使用了不支持的文档风格或使用复杂参数,请调用 add_tools 方法手动编写函数描述
186186

0 commit comments

Comments
 (0)