feat: add multi-function agent with tools for weather, time, calculator, file listing, and jokes

- Created multiFunctionAgent to handle multiple user requests.
- Implemented tools for weather information, current time, basic arithmetic operations, file listing, and telling jokes.
- Added necessary schemas and execution logic for each tool.
- Set up main Mastra instance to include the new agent.
- Created a script to run the agent and test its functionality.
- Configured TypeScript settings for the project.
This commit is contained in:
dzq 2025-12-11 15:18:09 +08:00
commit f3e5a0db54
17 changed files with 6302 additions and 0 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
OPENAI_API_KEY=<your-api-key>

65
.gitignore vendored Normal file
View File

@ -0,0 +1,65 @@
node_modules
# Build outputs
dist/
.mastra/
.output/
*.tsbuildinfo
.cache/
__snapshots__/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE files
.vscode/
.idea/
.claude/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Coverage
coverage/
.nyc_output/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Optional npm cache directory
.npm
# Optional pnpm store directory
.pnpm-store
# Optional REPL history
.node_repl_history
# Temporary files
tmp/
temp/
# Mastra specific
.mastra/output/

120
CLAUDE.md Normal file
View File

@ -0,0 +1,120 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个基于 [Mastra 框架](https://mastra.ai) 构建的多功能 AI Agent 项目。项目采用模块化架构,包含天气查询、时间查询、计算器、文件列表和笑话生成等基础功能。
## 常用命令
### 开发环境
```bash
pnpm install # 安装依赖
pnpm dev # 启动开发服务器http://localhost:4111
pnpm build # 构建项目用于部署
```
### 脚本工具
```bash
pnpm generate # 运行生成脚本
```
## 项目架构
### 核心组件
#### 1. Mastra 配置 (`src/mastra/index.ts`)
- 初始化 Mastra 框架实例
- 注册所有 Agent
- 全局配置入口
#### 2. Agent 系统 (`src/mastra/agents/`)
- `multi-function-agent.ts`: 主代理,集成所有工具功能
- Agent 配置包含:名称、系统指令、模型配置、可用工具
#### 3. Tool 系统 (`src/mastra/tools/`)
- 每个工具使用 `createTool` 工厂函数
- 遵循统一结构id、描述、输入/输出 Schema、执行逻辑
- 工具通过 `tools/index.ts` 统一导出
### 工具清单
- `weatherTool`: 天气查询(使用 Open-Meteo API
- `timeTool`: 时间查询
- `calculatorTool`: 基本算术运算
- `filelistTool`: 目录文件列表
- `jokeTool`: 随机笑话生成
### 配置系统
- **环境变量**: `.env` 文件存储 API 密钥和配置
- **TypeScript**: `tsconfig.json` 配置为 ES2022 模块系统
- **依赖管理**: 使用 pnpm 和 `package.json`
## 开发流程
### 1. 环境设置
```bash
cp .env.example .env
# 编辑 .env 文件,配置必要的 API 密钥
```
### 2. 运行开发服务器
启动后可通过以下方式访问:
- **Studio UI**: http://localhost:4111交互式测试
- **REST API**: http://localhost:4111/api/agents/multiFunctionAgent/generate
- **Swagger UI**: http://localhost:4111/swagger-uiAPI 文档)
### 3. 扩展项目
#### 添加新工具
1. 在 `src/mastra/tools/` 下创建新工具文件
2. 使用 Zod 定义输入/输出 Schema
3. 实现 `execute` 函数逻辑
4. 在 `tools/index.ts` 中导出
5. 在 Agent 配置中添加新工具
#### 添加记忆功能
```bash
pnpm add @mastra/memory @mastra/libsql
```
然后在 Agent 配置中添加 Memory 配置。
#### 添加语音功能
```bash
pnpm add @mastra/voice-openai
```
## 关键设计模式
### 1. 模块化工具设计
- 每个工具独立实现,通过统一接口接入
- 工具间无依赖,便于测试和替换
### 2. 配置驱动
- Agent 能力通过配置组合
- 扩展无需修改核心代码
### 3. 类型安全
- 使用 Zod 进行运行时验证
- TypeScript 提供编译时类型检查
## 注意事项
1. **模型配置**: Agent 当前使用 DeepSeek 模型,在 `multi-function-agent.ts` 中配置
2. **API 调用**: 天气工具依赖外部 API需要网络连接
3. **构建要求**: 项目使用 ES2022 模块系统,需确保兼容性
4. **环境变量**: 开发和生产环境都依赖 `.env` 配置
## 调试和测试
### 通过 Studio UI
访问 http://localhost:4111 进行交互式测试,可以:
- 查看工具调用过程
- 调试 Agent 推理链
- 测试不同输入场景
### 通过 REST API
使用 curl 或 HTTP 客户端测试特定功能。
### 查看构建输出
构建后在 `.mastra/output/` 目录中可查看编译结果。

236
README.md Normal file
View File

@ -0,0 +1,236 @@
# 多功能 Mastra Agent
这是一个使用 Mastra 框架构建的多功能 AI Agent具备以下基础功能
- **天气查询**:获取任意城市的当前天气信息
- **时间查询**:返回当前时间和时区
- **计算器**:执行基本算术运算(加、减、乘、除)
- **文件列表**:列出指定目录的文件
- **笑话生成**:随机讲述笑话
## 项目结构
```
src/mastra/
├── agents/
│ ├── multi-function-agent.ts # 多功能 agent
│ └── index.ts # agent 导出
├── tools/
│ ├── weather-tool.ts # 天气工具
│ ├── time-tool.ts # 时间工具
│ ├── calculator-tool.ts # 计算器工具
│ ├── filelist-tool.ts # 文件列表工具
│ ├── joke-tool.ts # 笑话工具
│ └── index.ts # 工具导出
├── workflows/ # (可选)工作流目录
└── index.ts # Mastra 入口点
```
## 快速开始
### 1. 安装依赖
```bash
pnpm install
```
### 2. 配置 API 密钥
复制 `.env.example``.env`,并填入你的 OpenAI API 密钥:
```bash
cp .env.example .env
```
编辑 `.env` 文件:
```
OPENAI_API_KEY=sk-your-openai-api-key-here
```
### 3. 启动开发服务器
```bash
pnpm dev
```
服务器将在 http://localhost:4111 启动。
### 4. 使用 Agent
#### 通过 Studio UI 交互
打开浏览器访问 http://localhost:4111在 Studio 界面中与你的 agent 对话。
#### 通过 HTTP API 调用
```bash
# 获取当前时间
curl -X POST http://localhost:4111/api/agents/multiFunctionAgent/generate \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "What time is it?"
}
]
}'
# 计算 15 + 27
curl -X POST http://localhost:4111/api/agents/multiFunctionAgent/generate \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "What is 15 + 27?"
}
]
}'
# 查询东京天气
curl -X POST http://localhost:4111/api/agents/multiFunctionAgent/generate \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "What'\''s the weather in Tokyo?"
}
]
}'
```
#### 通过 TypeScript 代码调用
```typescript
import { mastra } from "./src/mastra/index.js";
async function callAgent() {
const agent = mastra.getAgent("multiFunctionAgent");
// 查询时间
const timeResponse = await agent.generate("What time is it?");
console.log(timeResponse.text);
// 计算
const calcResponse = await agent.generate("What is 15 + 27?");
console.log(calcResponse.text);
// 查询天气
const weatherResponse = await agent.generate("What's the weather in Tokyo?");
console.log(weatherResponse.text);
// 列出文件
const fileResponse = await agent.generate("List files in current directory");
console.log(fileResponse.text);
// 讲笑话
const jokeResponse = await agent.generate("Tell me a joke");
console.log(jokeResponse.text);
}
callAgent();
```
## 工具详情
### 1. 天气工具 (`weatherTool`)
- **功能**:通过 Open-Meteo API 获取实时天气数据
- **输入**:城市名称
- **输出**:温度、体感温度、湿度、风速、天气状况
### 2. 时间工具 (`timeTool`)
- **功能**:返回当前 ISO 时间和时区
- **输入**:无
- **输出**:当前时间、时区
### 3. 计算器工具 (`calculatorTool`)
- **功能**:执行基本算术运算
- **输入**操作类型add/subtract/multiply/divide、两个数字
- **输出**:计算结果、操作描述
### 4. 文件列表工具 (`filelistTool`)
- **功能**:列出指定目录的文件
- **输入**:(可选)目录路径
- **输出**:文件数组、目录路径
### 5. 笑话工具 (`jokeTool`)
- **功能**:随机返回一个预设笑话
- **输入**:无
- **输出**:笑话文本
## 扩展功能
### 添加新工具
1. 在 `src/mastra/tools/` 目录下创建新的工具文件
2. 实现工具逻辑,使用 `createTool` 函数
3. 在 `tools/index.ts` 中导出新工具
4. 在 agent 配置中添加该工具
### 添加记忆功能
安装 memory 包并配置:
```bash
pnpm add @mastra/memory @mastra/libsql
```
然后在 agent 配置中添加:
```typescript
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/libsql";
export const agent = new Agent({
// ... 其他配置
memory: new Memory({
storage: new LibSQLStore({
url: "file:./mastra.db",
}),
}),
});
```
### 添加语音功能
安装语音包并配置:
```bash
pnpm add @mastra/voice-openai
```
```typescript
import { OpenAIVoice } from "@mastra/voice-openai";
export const agent = new Agent({
// ... 其他配置
voice: new OpenAIVoice(),
});
```
## 故障排除
### 常见问题
1. **API 密钥错误**
- 检查 `.env` 文件中的 `OPENAI_API_KEY` 是否正确
- 确保密钥有足够的额度
2. **服务器启动失败**
- 检查端口 4111 是否被占用
- 尝试 `pnpm build` 检查构建错误
3. **工具调用失败**
- 检查网络连接(天气工具需要网络)
- 确认工具输入格式正确
### 获取帮助
- [Mastra 官方文档](https://mastra.ai/docs)
- [Mastra Discord 社区](https://discord.gg/BTYqqHKUrf)
## 许可证
MIT

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "myagent",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "mastra dev",
"build": "mastra build",
"generate": "pnpx tsx ./src/scripts/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@ai-sdk/deepseek": "^1.0.31",
"@ai-sdk/openai": "^2.0.80",
"@mastra/core": "^0.24.8",
"mastra": "^0.18.8",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/node": "^25.0.0",
"typescript": "^5.9.3"
}
}

5570
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
export { multiFunctionAgent } from "./multi-function-agent";

View File

@ -0,0 +1,40 @@
import { Agent } from "@mastra/core/agent";
import {
weatherTool,
timeTool,
calculatorTool,
filelistTool,
jokeTool,
} from "../tools";
import { createDeepSeek } from '@ai-sdk/deepseek';
const deepseek = createDeepSeek({
apiKey: 'sk-8603b08e1125422ca6238c8b4a1a40d8',
});
export const multiFunctionAgent = new Agent({
name: "Multi-Function Agent",
instructions: `
You are a helpful assistant with multiple capabilities. You can:
1. **Weather Information**: Provide current weather for any location using the weather tool.
2. **Time Check**: Tell the current time and timezone.
3. **Calculator**: Perform basic arithmetic operations (addition, subtraction, multiplication, division).
4. **File Listing**: List files in a directory (default is current directory).
5. **Jokes**: Tell random jokes to lighten the mood.
Always be polite and helpful. If the user asks for something outside your capabilities, politely explain what you can do.
Use the appropriate tool based on the user's request. If unsure, ask for clarification.
`,
model: deepseek('deepseek-chat'),
tools: {
weatherTool,
timeTool,
calculatorTool,
filelistTool,
jokeTool,
},
});

6
src/mastra/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { Mastra } from "@mastra/core/mastra";
import { multiFunctionAgent } from "./agents";
export const mastra = new Mastra({
agents: { multiFunctionAgent },
});

View File

@ -0,0 +1,41 @@
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const calculatorTool = createTool({
id: "calculator",
description: "Perform basic arithmetic operations",
inputSchema: z.object({
operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The arithmetic operation to perform"),
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
}),
outputSchema: z.object({
result: z.number(),
operation: z.string(),
}),
execute: async ({ context }) => {
const { operation, a, b } = context;
let result: number;
switch (operation) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
if (b === 0) throw new Error("Division by zero");
result = a / b;
break;
default:
throw new Error(`Unknown operation: ${operation}`);
}
return {
result,
operation: `${a} ${operation} ${b}`,
};
},
});

View File

@ -0,0 +1,24 @@
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
export const filelistTool = createTool({
id: "list-files",
description: "List files in the current directory",
inputSchema: z.object({
directory: z.string().optional().describe("Directory path (default: current directory)"),
}),
outputSchema: z.object({
files: z.array(z.string()),
directory: z.string(),
}),
execute: async ({ context }) => {
const targetDir = context.directory ? path.resolve(context.directory) : process.cwd();
const files = await fs.readdir(targetDir);
return {
files,
directory: targetDir,
};
},
});

View File

@ -0,0 +1,5 @@
export { weatherTool } from "./weather-tool";
export { timeTool } from "./time-tool";
export { calculatorTool } from "./calculator-tool";
export { filelistTool } from "./filelist-tool";
export { jokeTool } from "./joke-tool";

View File

@ -0,0 +1,30 @@
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
const jokes = [
"Why don't scientists trust atoms? Because they make up everything!",
"Why did the scarecrow win an award? Because he was outstanding in his field!",
"What do you call a fake noodle? An impasta!",
"Why don't eggs tell jokes? They'd crack each other up!",
"How does a penguin build its house? Igloos it together!",
"Why did the math book look so sad? Because it had too many problems.",
"What do you call a bear with no teeth? A gummy bear!",
"Why did the bicycle fall over? Because it was two-tired!",
"What do you call a fish wearing a bowtie? Sofishticated!",
"Why can't you give Elsa a balloon? Because she will let it go!",
];
export const jokeTool = createTool({
id: "tell-joke",
description: "Tell a random joke",
inputSchema: z.object({}),
outputSchema: z.object({
joke: z.string(),
}),
execute: async () => {
const randomIndex = Math.floor(Math.random() * jokes.length);
return {
joke: jokes[randomIndex],
};
},
});

View File

@ -0,0 +1,19 @@
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const timeTool = createTool({
id: "get-time",
description: "Get current time",
inputSchema: z.object({}),
outputSchema: z.object({
currentTime: z.string(),
timezone: z.string(),
}),
execute: async () => {
const now = new Date();
return {
currentTime: now.toISOString(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
},
});

View File

@ -0,0 +1,95 @@
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
interface WeatherResponse {
current: {
time: string;
temperature_2m: number;
apparent_temperature: number;
relative_humidity_2m: number;
wind_speed_10m: number;
wind_gusts_10m: number;
weather_code: number;
};
}
export const weatherTool = createTool({
id: "get-weather",
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City name"),
}),
outputSchema: z.object({
temperature: z.number(),
feelsLike: z.number(),
humidity: z.number(),
windSpeed: z.number(),
windGust: z.number(),
conditions: z.string(),
location: z.string(),
}),
execute: async ({ context }) => {
return await getWeather(context.location);
},
});
const getWeather = async (location: string) => {
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
const geocodingResponse = await fetch(geocodingUrl);
const geocodingData = await geocodingResponse.json();
if (!geocodingData.results?.[0]) {
throw new Error(`Location '${location}' not found`);
}
const { latitude, longitude, name } = geocodingData.results[0];
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
const response = await fetch(weatherUrl);
const data: WeatherResponse = await response.json();
return {
temperature: data.current.temperature_2m,
feelsLike: data.current.apparent_temperature,
humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m,
windGust: data.current.wind_gusts_10m,
conditions: getWeatherCondition(data.current.weather_code),
location: name,
};
};
function getWeatherCondition(code: number): string {
const conditions: Record<number, string> = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail",
};
return conditions[code] || "Unknown";
}

8
src/scripts/index.ts Normal file
View File

@ -0,0 +1,8 @@
import { multiFunctionAgent } from "../mastra/agents";
export const runAgent = async (input: string) => {
const response = await multiFunctionAgent.generate(input);
return response.text;
}
console.log(await runAgent('讲个笑话'));

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}