Initial commit: jaspersoft-agent-learn teaching project
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user