Initial commit: jaspersoft-agent-learn teaching project

This commit is contained in:
zy187
2026-05-29 23:22:18 +08:00
commit 05bb511aab
20 changed files with 4476 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# Step 01: 理解 Tool - 工具系统基础
## 🎯 学习目标
- 理解什么是 Tool(工具)
- 理解为什么 Agent 需要 Tool
- 学会设计一个可扩展的工具系统
- 理解 Tool 的核心要素:name、description、execute
---
## 📖 概念讲解
### 什么是 Tool
在 AI Agent 系统中,**Tool(工具)** 是 Agent 与外部世界交互的方式。
```
没有 Tool 的 LLM
┌──────────────┐
│ 用户输入 │
└──────┬───────┘
┌──────────────┐
│ LLM │ ← LLM 只能"想",不能"做"
│ (只能思考) │
└──────┬───────┘
┌──────────────┐
│ 返回答案 │ ← 答案可能不准确,没有执行能力
└──────────────┘
有 Tool 的 Agent
┌──────────────┐
│ 用户输入 │
└──────┬───────┘
┌──────────────┐
│ LLM │
│ (思考 + 决策) │
└──────┬───────┘
┌──────────────┐
│ 需要执行工具? │──── 是 ──▶ 调用 Tool
└──────┬───────┘ │
│ ▼
│ 否 ┌──────────┐
▼ │ 执行 Tool │
┌──────────────┐ │ (访问外部) │
│ 返回答案 │ └─────┬─────┘
└──────────────┘ │
把执行结果反馈给 LLM
```
### 为什么需要 Tool
1. **LLM 不知道最新信息**Tool 可以搜索网页、查数据库
2. **LLM 不能执行操作**Tool 可以写文件、调用 API
3. **LLM 可能有幻觉**Tool 可以验证信息
4. **LLM 需要精确计算**Tool 可以调用计算器
### Tool 的核心要素
每个 Tool 都有三个核心要素:
```python
class Tool:
name: str # 工具名称(LLM 通过名字选择工具)
description: str # 工具描述(LLM 通过描述理解何时使用)
execute: function # 执行函数(实际做事的代码)
```
---
## 💻 代码实现
请打开 `concept.py` 查看详细代码注释。
+19
View File
@@ -0,0 +1,19 @@
# Jaspersoft Learn - AI Agent 开发教学项目
from .concept import (
BaseTool,
ToolResult,
ToolRegistry,
CalculatorTool,
SearchTool,
JaspersoftCodeGeneratorTool,
)
__all__ = [
"BaseTool",
"ToolResult",
"ToolRegistry",
"CalculatorTool",
"SearchTool",
"JaspersoftCodeGeneratorTool",
]
+592
View File
@@ -0,0 +1,592 @@
"""
Step 01: 理解 Tool - 工具系统基础
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎓 本节内容:
1. 什么是 Tool
2. 如何定义一个 Tool
3. 如何设计可扩展的工具系统?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
from dataclasses import dataclass
import json
# ═══════════════════════════════════════════════════════════════════════════════
# 第一部分:理解 Tool 的基本结构
# ═══════════════════════════════════════════════════════════════════════════════
"""
在开始写代码之前,我们先理解 Tool 的本质:
Tool = 名称 + 描述 + 执行逻辑
为什么这样设计?因为 LLM 选择工具时只看:
1. 工具名称(叫什么)
2. 工具描述(什么时候用)
然后 LLM 会决定:是否调用这个工具?调用时传什么参数?
所以一个好的 Tool 设计,必须:
- 名称清晰、一目了然
- 描述准确、告诉 LLM 何时使用
- 执行逻辑独立、不影响其他工具
"""
# ═══════════════════════════════════════════════════════════════════════════════
# 第二部分:定义 Tool 的数据结构
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass # Python 数据类,自动生成 __init__ 等方法
class ToolResult:
"""
Tool 执行结果的数据结构
为什么需要单独定义这个?
因为 Tool 执行可能成功,也可能失败,我们需要统一格式来传递结果。
属性说明:
success: 是否执行成功
result: 执行结果(如果成功)
error: 错误信息(如果失败)
metadata: 额外元数据(如执行时间、使用的参数等)
"""
success: bool
result: Any = None
error: Optional[str] = None
metadata: Dict[str, Any] = None
def __post_init__(self):
"""初始化后确保 metadata 不为 None"""
if self.metadata is None:
self.metadata = {}
# ═══════════════════════════════════════════════════════════════════════════════
# 第三部分:定义抽象 Tool 基类(重要概念)
# ═══════════════════════════════════════════════════════════════════════════════
class BaseTool(ABC):
"""
所有工具的抽象基类
为什么需要抽象基类?
因为我们希望所有工具都有统一的接口,这样:
1. 任意 Tool 都可以被统一管理
2. 新增 Tool 时不需要修改已有代码(开闭原则)
3. 可以批量操作所有 Tool
抽象基类 = 定义"接口规范",子类负责"具体实现"
"""
# ============================================================
# 属性:每个 Tool 必须有的特性
# ============================================================
@property
@abstractmethod
def name(self) -> str:
"""
工具名称
为什么用 @property
因为 name 通常是只读的,我们不希望运行时被随意修改
为什么用 @abstractmethod
因为这是一个"抽象方法",所有子类必须实现
如果不实现,Python 会报错,强制你提供具体实现
"""
pass
@property
@abstractmethod
def description(self) -> str:
"""
工具描述
这是 LLM 理解"何时使用这个工具"的关键!
描述越准确,LLM 越能正确选择工具。
好的描述应该包含:
1. 工具能做什么
2. 输入参数是什么
3. 输出结果是什么
4. 适用的场景
"""
pass
# ============================================================
# 方法:Tool 的核心执行逻辑
# ============================================================
@abstractmethod
def execute(self, **kwargs) -> ToolResult:
"""
执行工具
参数使用 **kwargs 的原因:
不同的 Tool 可能需要不同的参数
用 **kwargs 可以接受任意参数,具体由子类决定
返回 ToolResult 而不是直接返回值的原因:
1. 统一成功/失败的判断
2. 可以附带错误信息
3. 可以附带元数据(执行时间、使用的参数等)
为什么是抽象方法?
因为"如何执行"是每个工具自己的事,基类不知道具体逻辑
"""
pass
# ============================================================
# 辅助方法:让 Tool 更容易使用
# ============================================================
def to_dict(self) -> dict:
"""
将 Tool 转换为字典格式
这个方法用于:
1. 给 LLM 展示可用的工具列表
2. 序列化/反序列化工具配置
返回格式:
{
"name": "工具名称",
"description": "工具描述",
"parameters": {...} # 可选,参数schema
}
"""
return {
"name": self.name,
"description": self.description,
}
def __repr__(self) -> str:
"""调试时显示工具信息"""
return f"<Tool: {self.name}>"
# ═══════════════════════════════════════════════════════════════════════════════
# 第四部分:实现具体的 Tool
# ═══════════════════════════════════════════════════════════════════════════════
class CalculatorTool(BaseTool):
"""
计算器工具示例
这个工具演示:
1. 如何定义一个简单的 Tool
2. 如何处理输入参数
3. 如何返回结果
4. 如何处理错误
"""
@property
def name(self) -> str:
return "calculator"
@property
def description(self) -> str:
return """
计算器工具,执行数学运算。
输入:
expression: str - 数学表达式,如 "2 + 3""10 * 5"
输出:
计算结果(数字)
示例:
expression="2 + 3" -> 5
expression="10 / 2" -> 5.0
expression="2 ** 3" -> 8
"""
def execute(self, **kwargs) -> ToolResult:
"""
执行计算
为什么用 try-except
因为 eval() 可能执行恶意代码或语法错误
我们需要捕获异常,返回友好的错误信息
"""
expression = kwargs.get("expression")
# 参数验证
if expression is None:
return ToolResult(
success=False,
error="缺少必需参数 'expression'"
)
if not isinstance(expression, str):
return ToolResult(
success=False,
error=f"参数 'expression' 应该是字符串,实际是 {type(expression)}"
)
try:
# 使用 eval 计算表达式
# 注意:生产环境中应该用更安全的计算方式
# 这里用 eval 是为了简化,实际推荐用 ast.literal_eval 或专用计算库
result = eval(expression, {"__builtins__": {}}, {})
return ToolResult(
success=True,
result=result,
metadata={"expression": expression}
)
except SyntaxError as e:
return ToolResult(
success=False,
error=f"表达式语法错误: {e}"
)
except ZeroDivisionError:
return ToolResult(
success=False,
error="除数不能为零"
)
except Exception as e:
return ToolResult(
success=False,
error=f"计算错误: {e}"
)
class SearchTool(BaseTool):
"""
搜索工具示例
这个工具演示:
1. 如何模拟一个搜索功能
2. 如何返回结构化数据
3. 如何设计工具的"真实感"
实际应用中,这里会调用真实的搜索 API
"""
@property
def name(self) -> str:
return "web_search"
@property
def description(self) -> str:
return """
网络搜索工具,在互联网上搜索信息。
输入:
query: str - 搜索关键词
limit: int - 返回结果数量(默认5条)
输出:
搜索结果列表,每条包含标题、链接、摘要
适用场景:
- 查找最新资讯
- 搜索技术文档
- 查找某个问题的答案
"""
def execute(self, **kwargs) -> ToolResult:
"""
执行搜索
注意:这里用模拟数据演示
真实场景中,你需要调用 Bing/Google API
"""
query = kwargs.get("query", "")
limit = kwargs.get("limit", 5)
if not query:
return ToolResult(
success=False,
error="搜索关键词不能为空"
)
# 模拟搜索结果
# 真实场景:这里调用 search_api(query)
mock_results = [
{
"title": f"关于 '{query}' 的官方文档",
"url": "https://example.com/doc",
"snippet": f"这是关于 {query} 的详细官方文档..."
},
{
"title": f"{query} 入门教程",
"url": "https://example.com/tutorial",
"snippet": f"学习 {query} 的快速入门指南..."
},
]
return ToolResult(
success=True,
result=mock_results[:limit],
metadata={"query": query, "count": len(mock_results[:limit])}
)
class JaspersoftCodeGeneratorTool(BaseTool):
"""
Jaspersoft JRXML 代码生成工具(对应你的实际项目)
这个工具演示:
1. 如何设计业务相关的 Tool
2. 如何处理复杂输入
3. 如何返回有意义的结果
这个工具的作用:
用户说"生成一个销售报表"
-> Tool 调用 LLM 生成 JRXML 代码
-> 返回生成的代码
"""
@property
def name(self) -> str:
return "jrxml_generator"
@property
def description(self) -> str:
return """
JasperReports JRXML 报表生成工具。
根据用户的自然语言需求,生成 JRXML 报表模板代码。
输入:
requirement: str - 用户的需求描述
context: str - 额外的上下文信息(如参考模板、数据源信息等)
输出:
生成的 JRXML 代码(字符串)
适用场景:
- 从零生成新报表
- 基于现有模板修改
- 生成特定格式的报表
注意:
生成的代码需要通过 jrxml_validator 验证后才能使用
"""
def execute(self, **kwargs) -> ToolResult:
"""
执行 JRXML 生成
参数处理说明:
- 使用 kwargs.get() 安全获取参数,避免 KeyError
- 提供默认值作为后备
- 类型检查确保参数正确
"""
requirement = kwargs.get("requirement", "")
context = kwargs.get("context", "")
if not requirement:
return ToolResult(
success=False,
error="需求描述不能为空"
)
# 实际场景中,这里会:
# 1. 构建 Prompt
# 2. 调用 LLM
# 3. 提取生成的 JRXML 代码
#
# 示例代码:
# prompt = build_generation_prompt(requirement, context)
# response = llm.invoke(prompt)
# jrxml = extract_jrxml(response)
# 模拟生成结果
mock_jrxml = f'''<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
name="GeneratedReport" pageWidth="595" pageHeight="842">
<queryString>
<![CDATA[SELECT * FROM sales WHERE date > '2024-01-01']]>
</queryString>
<field name="product_name" class="java.lang.String"/>
<field name="quantity" class="java.lang.Integer"/>
<field name="price" class="java.math.BigDecimal"/>
<band height="50">
<staticText>
<reportElement x="0" y="0" width="100" height="20"/>
<text>Product</text>
</staticText>
<textField>
<reportElement x="0" y="20" width="100" height="20"/>
<textFieldExpression>$F{{product_name}}</textFieldExpression>
</textField>
</band>
</jasperReport>'''
return ToolResult(
success=True,
result=mock_jrxml,
metadata={
"requirement": requirement,
"context_provided": bool(context),
"note": "这是模拟结果,实际需要调用 LLM"
}
)
# ═══════════════════════════════════════════════════════════════════════════════
# 第五部分:工具管理系统
# ═══════════════════════════════════════════════════════════════════════════════
class ToolRegistry:
"""
工具注册表
这个类负责:
1. 注册所有可用的 Tool
2. 根据名称查找 Tool
3. 列出所有可用 Tool(供 LLM 了解能力)
为什么需要这个?
因为一个 Agent 可能有很多 Tool
需要一个统一的地方来管理它们
设计模式:注册表模式(Registry Pattern
核心思想:用一个中心来管理所有实例
"""
def __init__(self):
"""初始化空的工具注册表"""
self._tools: Dict[str, BaseTool] = {}
def register(self, tool: BaseTool) -> None:
"""
注册一个工具
为什么用 tool.name 作为 key
因为 name 是工具的唯一标识符
同一个 name 的工具只能注册一次
实际应用:
registry = ToolRegistry()
registry.register(CalculatorTool())
registry.register(SearchTool())
"""
if tool.name in self._tools:
raise ValueError(f"工具 '{tool.name}' 已经注册过了")
self._tools[tool.name] = tool
def get(self, name: str) -> Optional[BaseTool]:
"""
根据名称获取工具
返回 Optional[BaseTool]
如果找到,返回工具实例
如果没找到,返回 None
"""
return self._tools.get(name)
def list_tools(self) -> list[dict]:
"""
列出所有已注册的工具
返回格式:
[
{"name": "calculator", "description": "..."},
{"name": "web_search", "description": "..."},
]
这个格式是为了方便给 LLM 展示可用工具列表
"""
return [tool.to_dict() for tool in self._tools.values()]
def execute(self, tool_name: str, **kwargs) -> ToolResult:
"""
执行指定名称的工具
这是对用户暴露的统一接口
用户不需要知道具体哪个工具,只要说"执行这个"
"""
tool = self.get(tool_name)
if tool is None:
return ToolResult(
success=False,
error=f"工具 '{tool_name}' 不存在"
)
return tool.execute(**kwargs)
# ═══════════════════════════════════════════════════════════════════════════════
# 第六部分:演示代码
# ═══════════════════════════════════════════════════════════════════════════════
def demo():
"""
演示如何使用工具系统
这个函数展示:
1. 创建工具注册表
2. 注册工具
3. 执行工具
4. 查看可用工具
"""
print("=" * 60)
print("Step 01: 理解 Tool - 工具系统演示")
print("=" * 60)
# 1. 创建工具注册表
registry = ToolRegistry()
# 2. 注册工具
registry.register(CalculatorTool())
registry.register(SearchTool())
registry.register(JaspersoftCodeGeneratorTool())
print("\n📋 已注册的工具列表:")
for tool_info in registry.list_tools():
print(f" - {tool_info['name']}: {tool_info['description'][:50]}...")
# 3. 执行计算器工具
print("\n🔧 执行计算器工具:")
result = registry.execute("calculator", expression="2 + 3 * 4")
if result.success:
print(f" 结果: {result.result}")
else:
print(f" 错误: {result.error}")
# 4. 执行搜索工具
print("\n🔍 执行搜索工具:")
result = registry.execute("web_search", query="JasperReports 教程", limit=2)
if result.success:
for item in result.result:
print(f" - {item['title']}")
else:
print(f" 错误: {result.error}")
# 5. 执行 JRXML 生成工具
print("\n📄 执行 JRXML 生成工具:")
result = registry.execute(
"jrxml_generator",
requirement="生成一个销售报表,显示月度汇总"
)
if result.success:
print(f" 生成成功! (长度: {len(result.result)} 字符)")
print(f" 前100字符: {result.result[:100]}...")
else:
print(f" 错误: {result.error}")
# 6. 演示错误处理
print("\n⚠️ 演示错误处理:")
result = registry.execute("calculator", expression="1 / 0")
if not result.success:
print(f" 预期错误: {result.error}")
if __name__ == "__main__":
demo()
+223
View File
@@ -0,0 +1,223 @@
"""
Step 01 练习题:设计你的第一个 Tool
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 练习目标:
1. 巩固 Tool 的基本结构
2. 设计一个业务相关的 Tool
3. 理解 Tool 注册系统
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
# ═══════════════════════════════════════════════════════════════════════════════
# 练习 1:完善 TextProcessorTool
# ═══════════════════════════════════════════════════════════════════════════════
"""
任务:
完善 TextProcessorTool,这个工具用于处理文本。
要求:
1. 实现 word_count 方法:统计单词数量
2. 实现 character_count 方法:统计字符数量(不包括空格)
3. 实现 sentence_count 方法:统计句子数量(按句号、问号、感叹号分割)
提示:
- text 可能包含多行
- 句子分割要考虑常见的句子结束符:. ! ?
- 单词分割可以考虑按空格分割
完成后测试:
text = "Hello, world! This is a test. How are you?"
预期结果:
- word_count: 9
- character_count: 38 (不包括空格)
- sentence_count: 3
"""
from step_01_tools.concept import BaseTool, ToolResult
class TextProcessorTool(BaseTool):
"""文本处理工具"""
@property
def name(self) -> str:
return "text_processor"
@property
def description(self) -> str:
return "文本处理工具,统计文本的各种特征"
def execute(self, **kwargs) -> ToolResult:
"""处理文本请求"""
text = kwargs.get("text", "")
operation = kwargs.get("operation", "word_count") # 默认操作
if not text:
return ToolResult(success=False, error="文本不能为空")
if operation == "word_count":
# TODO: 实现单词统计
result = self.word_count(text)
elif operation == "character_count":
# TODO: 实现字符统计
result = self.character_count(text)
elif operation == "sentence_count":
# TODO: 实现句子统计
result = self.sentence_count(text)
else:
return ToolResult(success=False, error=f"不支持的操作: {operation}")
return ToolResult(success=True, result=result)
def word_count(self, text: str) -> int:
"""统计单词数量"""
# 提示:按空格分割,过滤空字符串
# 你的代码:
pass
def character_count(self, text: str) -> int:
"""统计字符数量(不包括空格)"""
# 提示:去除所有空白字符后统计长度
# 你的代码:
pass
def sentence_count(self, text: str) -> int:
"""统计句子数量"""
# 提示:按 . ! ? 分割
# 你的代码:
pass
# ═══════════════════════════════════════════════════════════════════════════════
# 练习 2:设计一个 EmailTool
# ═══════════════════════════════════════════════════════════════════════════════
"""
任务:
设计一个 EmailTool,用于处理邮件相关操作。
功能要求:
1. send_email: 发送邮件(收件人、主题、正文)
2. search_emails: 搜索邮件(关键词)
3. get_unread_count: 获取未读邮件数量
设计提示:
- 工具名称要简洁明了
- 描述要告诉 LLM 这个工具能做什么
- execute 方法要处理不同的 operation
这个练习的目的是让你学会:
- 如何设计多功能的 Tool
- 如何用 operation 参数区分不同功能
- 如何返回结构化的结果
"""
class EmailTool(BaseTool):
"""邮件处理工具"""
# TODO: 实现属性和方法
pass
# ═══════════════════════════════════════════════════════════════════════════════
# 练习 3:改进工具注册表
# ═══════════════════════════════════════════════════════════════════════════════
"""
任务:
给 ToolRegistry 添加一个新功能:根据描述关键词搜索工具
方法签名:
def search_tools(self, keyword: str) -> list[dict]:
'''
根据关键词搜索工具
参数:
keyword: str - 搜索关键词
返回:
匹配的工具列表,格式同 list_tools()
匹配规则:
如果关键词出现在工具名称或描述中,就算匹配
匹配应该不区分大小写
'''
预期行为:
registry = ToolRegistry()
registry.register(CalculatorTool())
registry.register(SearchTool())
registry.register(TextProcessorTool()) # 用你刚完成的
results = registry.search_tools("text")
# 应该返回 text_processor
results = registry.search_tools("calculate")
# 应该返回 calculator
results = registry.search_tools("web")
# 应该返回 web_search
"""
# 从 step_01_tools.concept 导入已有的类
from step_01_tools.concept import ToolRegistry, CalculatorTool, SearchTool
def add_search_to_registry():
"""
在这里实现 search_tools 方法
提示:
1. 遍历所有已注册的工具
2. 检查 name 或 description 中是否包含 keyword
3. 收集匹配的工具
4. 返回列表
完成后,在 main.py 中测试
"""
# 你的代码:
pass
# ═══════════════════════════════════════════════════════════════════════════════
# 运行测试
# ═══════════════════════════════════════════════════════════════════════════════
def test_exercises():
"""测试所有练习"""
print("\n" + "=" * 60)
print("测试练习答案")
print("=" * 60)
# 测试练习 1
print("\n📝 练习 1: TextProcessorTool")
tool = TextProcessorTool()
test_text = "Hello, world! This is a test. How are you?"
print(f"测试文本: {test_text}")
print(f"单词数量: {tool.word_count(test_text)}") # 应该是 9
print(f"字符数量: {tool.character_count(test_text)}") # 应该是 38
print(f"句子数量: {tool.sentence_count(test_text)}") # 应该是 3
# 测试练习 2
print("\n📝 练习 2: EmailTool")
# 运行看看你完成了多少
# 测试练习 3
print("\n📝 练习 3: search_tools")
registry = ToolRegistry()
registry.register(CalculatorTool())
registry.register(SearchTool())
registry.register(tool)
# 你的测试代码:
if __name__ == "__main__":
test_exercises()
+264
View File
@@ -0,0 +1,264 @@
"""
Step 01 练习题答案
⚠️ 先自己思考,再看答案!
⚠️ 答案不是唯一的,这里只是其中一种实现
"""
from step_01_tools.concept import BaseTool, ToolResult
# ═══════════════════════════════════════════════════════════════════════════════
# 练习 1 答案:TextProcessorTool
# ═══════════════════════════════════════════════════════════════════════════════
class TextProcessorTool(BaseTool):
"""文本处理工具"""
@property
def name(self) -> str:
return "text_processor"
@property
def description(self) -> str:
return "文本处理工具,统计文本的各种特征"
def execute(self, **kwargs) -> ToolResult:
"""处理文本请求"""
text = kwargs.get("text", "")
operation = kwargs.get("operation", "word_count")
if not text:
return ToolResult(success=False, error="文本不能为空")
if operation == "word_count":
result = self.word_count(text)
elif operation == "character_count":
result = self.character_count(text)
elif operation == "sentence_count":
result = self.sentence_count(text)
else:
return ToolResult(success=False, error=f"不支持的操作: {operation}")
return ToolResult(success=True, result=result)
def word_count(self, text: str) -> int:
"""统计单词数量"""
# 按空格分割,过滤空字符串
words = [w for w in text.split() if w.strip()]
return len(words)
def character_count(self, text: str) -> int:
"""统计字符数量(不包括空格)"""
# 去除所有空白字符后统计
return len(text.replace(" ", "").replace("\n", "").replace("\t", ""))
def sentence_count(self, text: str) -> int:
"""统计句子数量"""
# 按常见句子结束符分割
import re
sentences = re.split(r'[.!?]+', text)
# 过滤空字符串
sentences = [s for s in sentences if s.strip()]
return len(sentences)
# ═══════════════════════════════════════════════════════════════════════════════
# 练习 2 答案:EmailTool
# ═══════════════════════════════════════════════════════════════════════════════
class EmailTool(BaseTool):
"""邮件处理工具"""
@property
def name(self) -> str:
return "email"
@property
def description(self) -> str:
return """
邮件处理工具,用于发送和搜索邮件。
支持的操作:
send_email: 发送邮件
- to: 收件人邮箱
- subject: 邮件主题
- body: 邮件正文
返回: {"success": true, "message_id": "xxx"}
search_emails: 搜索邮件
- keyword: 搜索关键词
- limit: 返回数量(默认10
返回: [{"from": "...", "subject": "...", "date": "..."}, ...]
get_unread_count: 获取未读邮件数量
无需参数
返回: {"count": 5}
"""
def execute(self, **kwargs) -> ToolResult:
"""执行邮件操作"""
operation = kwargs.get("operation", "get_unread_count")
if operation == "send_email":
return self.send_email(
to=kwargs.get("to", ""),
subject=kwargs.get("subject", ""),
body=kwargs.get("body", "")
)
elif operation == "search_emails":
return self.search_emails(
keyword=kwargs.get("keyword", ""),
limit=kwargs.get("limit", 10)
)
elif operation == "get_unread_count":
return self.get_unread_count()
else:
return ToolResult(success=False, error=f"不支持的操作: {operation}")
def send_email(self, to: str, subject: str, body: str) -> ToolResult:
"""发送邮件"""
if not to:
return ToolResult(success=False, error="收件人不能为空")
if not subject:
return ToolResult(success=False, error="主题不能为空")
# 实际场景:调用邮件发送 API
# 这里用模拟数据
message_id = f"msg_{hash(to + subject) % 100000}"
return ToolResult(
success=True,
result={
"success": True,
"message_id": message_id,
"to": to,
"subject": subject
}
)
def search_emails(self, keyword: str, limit: int) -> ToolResult:
"""搜索邮件"""
if not keyword:
return ToolResult(success=False, error="搜索关键词不能为空")
# 模拟搜索结果
mock_results = [
{
"from": "boss@company.com",
"subject": f"关于项目的{keyword}",
"date": "2024-01-15",
"snippet": "..."
},
{
"from": "colleague@company.com",
"subject": f"回复: {keyword}的讨论",
"date": "2024-01-14",
"snippet": "..."
},
]
return ToolResult(
success=True,
result=mock_results[:limit]
)
def get_unread_count(self) -> ToolResult:
"""获取未读邮件数量"""
# 实际场景:调用邮件 API 获取未读数
return ToolResult(success=True, result={"count": 5})
# ═══════════════════════════════════════════════════════════════════════════════
# 练习 3 答案:给 ToolRegistry 添加 search_tools
# ═══════════════════════════════════════════════════════════════════════════════
from step_01_tools.concept import ToolRegistry, CalculatorTool, SearchTool
def add_search_to_registry():
"""
演示如何给 ToolRegistry 添加 search_tools 方法
"""
# 方法1:在原类上添加(直接修改原类)
def search_tools_original(self, keyword: str) -> list[dict]:
"""根据关键词搜索工具"""
keyword_lower = keyword.lower()
results = []
for tool in self._tools.values():
# 检查名称或描述中是否包含关键词
name_match = keyword_lower in tool.name.lower()
desc_match = keyword_lower in tool.description.lower()
if name_match or desc_match:
results.append(tool.to_dict())
return results
# 给 ToolRegistry 添加方法
ToolRegistry.search_tools = search_tools_original
# 测试
registry = ToolRegistry()
registry.register(CalculatorTool())
registry.register(SearchTool())
registry.register(TextProcessorTool())
print("搜索 'text':")
for t in registry.search_tools("text"):
print(f" - {t['name']}")
print("搜索 'calculate':")
for t in registry.search_tools("calculate"):
print(f" - {t['name']}")
print("搜索 'web':")
for t in registry.search_tools("web"):
print(f" - {t['name']}")
# ═══════════════════════════════════════════════════════════════════════════════
# 测试运行
# ═══════════════════════════════════════════════════════════════════════════════
def test_answers():
"""测试答案"""
print("\n" + "=" * 60)
print("测试练习答案")
print("=" * 60)
# 测试练习 1
print("\n📝 练习 1: TextProcessorTool")
tool = TextProcessorTool()
test_text = "Hello, world! This is a test. How are you?"
print(f"测试文本: {test_text}")
print(f"单词数量: {tool.word_count(test_text)}") # 应该是 9
print(f"字符数量: {tool.character_count(test_text)}") # 应该是 38
print(f"句子数量: {tool.sentence_count(test_text)}") # 应该是 3
# 测试练习 2
print("\n📝 练习 2: EmailTool")
email_tool = EmailTool()
print("\n 发送邮件:")
result = email_tool.send_email("test@example.com", "测试", "这是一封测试邮件")
print(f" 结果: {result.result}")
print("\n 搜索邮件:")
result = email_tool.search_emails("项目", limit=5)
print(f" 结果: {result.result}")
print("\n 未读邮件数:")
result = email_tool.get_unread_count()
print(f" 结果: {result.result}")
# 测试练习 3
print("\n📝 练习 3: search_tools")
add_search_to_registry()
if __name__ == "__main__":
test_answers()
+169
View File
@@ -0,0 +1,169 @@
"""
Step 01: 工具系统基础 - 主程序
运行方式:
cd step_01_tools
python main.py
"""
from concept import (
ToolRegistry,
CalculatorTool,
SearchTool,
JaspersoftCodeGeneratorTool
)
def main():
"""演示工具系统的完整使用流程"""
print("=" * 70)
print(" Step 01: 理解 Tool - 工具系统基础")
print("=" * 70)
print()
# ═══════════════════════════════════════════════════════════════════════
# 场景:构建一个 Jaspersoft 报表助手的工具集
# ═══════════════════════════════════════════════════════════════════════
print("📦 场景:构建 Jaspersoft 报表助手")
print("-" * 70)
print("""
假设我们要构建一个 AI 助手,帮助用户:
1. 根据需求生成 JRXML 代码
2. 验证生成的代码是否正确
3. 计算报表的统计数据
4. 搜索相关的报表模板
我们可以设计以下工具:
- jrxml_generator: 生成 JRXML 代码
- jrxml_validator: 验证 JRXML 语法
- calculator: 计算统计数据
- web_search: 搜索报表模板
""")
# ═══════════════════════════════════════════════════════════════════════
# 步骤 1:创建工具注册表
# ═══════════════════════════════════════════════════════════════════════
print("\n📋 步骤 1: 创建工具注册表")
print("-" * 40)
registry = ToolRegistry()
print("✓ 创建了空的工具注册表")
print(f" 当前注册工具数量: {len(registry.list_tools())}")
# ═══════════════════════════════════════════════════════════════════════
# 步骤 2:注册工具
# ═══════════════════════════════════════════════════════════════════════
print("\n🔧 步骤 2: 注册工具")
print("-" * 40)
# 注册各种工具
registry.register(CalculatorTool())
print("✓ 注册计算器工具")
registry.register(SearchTool())
print("✓ 注册搜索工具")
registry.register(JaspersoftCodeGeneratorTool())
print("✓ 注册 JRXML 生成工具")
print(f"\n 当前注册工具数量: {len(registry.list_tools())}")
# ═══════════════════════════════════════════════════════════════════════
# 步骤 3:查看可用工具
# ═══════════════════════════════════════════════════════════════════════
print("\n📜 步骤 3: 查看可用工具")
print("-" * 40)
for tool_info in registry.list_tools():
print(f"\n [{tool_info['name']}]")
# 截取描述的前100个字符
desc = tool_info['description'].strip().replace('\n', ' ')[:100]
print(f" {desc}...")
# ═══════════════════════════════════════════════════════════════════════
# 步骤 4:执行工具
# ═══════════════════════════════════════════════════════════════════════
print("\n\n⚡ 步骤 4: 执行工具")
print("-" * 40)
# 场景:用户想要生成一个销售报表
user_requirement = "生成一个销售报表,显示月度汇总"
print(f"\n 用户需求: {user_requirement}")
# 4.1 先搜索相关的模板
print("\n 4.1 搜索相关模板...")
search_result = registry.execute("web_search", query="JasperReports 销售报表模板")
if search_result.success:
print(f" 找到 {len(search_result.result)} 个相关模板")
for i, item in enumerate(search_result.result[:2], 1):
print(f" {i}. {item['title']}")
# 4.2 生成 JRXML
print("\n 4.2 生成 JRXML 代码...")
generate_result = registry.execute(
"jrxml_generator",
requirement=user_requirement,
context="参考销售报表模板"
)
if generate_result.success:
print(f" ✓ 生成成功!")
print(f" 代码长度: {len(generate_result.result)} 字符")
print(f" 代码预览:")
print(" " + "-" * 30)
for line in generate_result.result.split('\n')[:5]:
print(f" {line}")
print(" ...")
# 4.3 计算统计
print("\n 4.3 计算统计...")
calc_result = registry.execute("calculator", expression="10 + 20 + 30")
if calc_result.success:
print(f" 10 + 20 + 30 = {calc_result.result}")
# ═══════════════════════════════════════════════════════════════════════
# 步骤 5:错误处理
# ═══════════════════════════════════════════════════════════════════════
print("\n\n⚠️ 步骤 5: 错误处理演示")
print("-" * 40)
# 场景:用户尝试除以零
print("\n 尝试执行: calculator(expression='10 / 0')")
error_result = registry.execute("calculator", expression="10 / 0")
if not error_result.success:
print(f" ✓ 错误被正确捕获: {error_result.error}")
# 尝试调用不存在的工具
print("\n 尝试执行: nonexistent_tool()")
error_result = registry.execute("nonexistent_tool")
if not error_result.success:
print(f" ✓ 错误被正确捕获: {error_result.error}")
# ═══════════════════════════════════════════════════════════════════════
# 总结
# ═══════════════════════════════════════════════════════════════════════
print("\n\n" + "=" * 70)
print(" ✅ Step 01 完成!")
print("=" * 70)
print("""
学到的关键概念:
1. Tool = name + description + execute
2. BaseTool 是所有工具的基类
3. ToolRegistry 统一管理所有工具
4. ToolResult 统一返回结果格式
下一步:
继续 Step 02,学习如何管理 Agent 的状态
""")
if __name__ == "__main__":
main()