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
+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()