Skip to content

Commit 783beb0

Browse files
committed
docs(examples): add quick start demos
Change-Id: Ib7f2484992ffeb488cd0aa2d49a97f9913e6b31e
1 parent 29c132a commit 783beb0

2 files changed

Lines changed: 363 additions & 0 deletions

File tree

examples/quick-start-with-tools.ts

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/**
2+
* Quick Start with Tool Calling Example
3+
* 带有工具调用的快速开始示例
4+
*
5+
* 此示例展示如何在 AgentRun Server 中实现工具调用功能。
6+
* This example demonstrates how to implement tool calling in AgentRun Server.
7+
*
8+
* 运行方式 / Run with:
9+
* npm run build && node dist-examples/quick-start-with-tools.js
10+
* # 或使用 tsx
11+
* npx tsx examples/quick-start-with-tools.ts
12+
*
13+
* 测试方式 / Test with:
14+
*
15+
* 1. OpenAI Chat Completions API (非流式 / Non-streaming):
16+
* curl http://127.0.0.1:9000/openai/v1/chat/completions -X POST \
17+
* -H "Content-Type: application/json" \
18+
* -d '{"messages": [{"role": "user", "content": "What is the weather in Beijing?"}], "stream": false}'
19+
*
20+
* 2. OpenAI Chat Completions API (流式 / Streaming):
21+
* curl http://127.0.0.1:9000/openai/v1/chat/completions -X POST \
22+
* -H "Content-Type: application/json" \
23+
* -d '{"messages": [{"role": "user", "content": "What is the weather in Shanghai?"}], "stream": true}'
24+
*
25+
* 3. AG-UI Protocol:
26+
* curl http://127.0.0.1:9000/ag-ui/agent -X POST \
27+
* -H "Content-Type: application/json" \
28+
* -d '{"messages": [{"role": "user", "content": "Calculate 15 * 23"}]}'
29+
*/
30+
31+
import { AgentRunServer, AgentRequest, EventType, AgentEvent } from '../src/index';
32+
import { logger } from '../src/utils/log';
33+
34+
// =============================================================================
35+
// Tool Definitions
36+
// 工具定义
37+
// =============================================================================
38+
39+
interface ToolDefinition {
40+
name: string;
41+
description: string;
42+
parameters: {
43+
type: 'object';
44+
properties: Record<string, { type: string; description: string }>;
45+
required?: string[];
46+
};
47+
execute: (args: Record<string, unknown>) => Promise<string>;
48+
}
49+
50+
// Define available tools
51+
const tools: ToolDefinition[] = [
52+
{
53+
name: 'get_weather',
54+
description: 'Get the current weather for a location',
55+
parameters: {
56+
type: 'object',
57+
properties: {
58+
location: {
59+
type: 'string',
60+
description: 'The city name, e.g., Beijing, Shanghai, New York',
61+
},
62+
},
63+
required: ['location'],
64+
},
65+
execute: async (args) => {
66+
const location = args.location as string;
67+
// Simulate weather API call
68+
await new Promise((resolve) => setTimeout(resolve, 500));
69+
const weathers = ['Sunny', 'Cloudy', 'Rainy', 'Windy'];
70+
const temps = [15, 20, 25, 30, 35];
71+
const weather = weathers[Math.floor(Math.random() * weathers.length)];
72+
const temp = temps[Math.floor(Math.random() * temps.length)];
73+
return `Weather in ${location}: ${weather}, ${temp}°C`;
74+
},
75+
},
76+
{
77+
name: 'calculate',
78+
description: 'Perform a mathematical calculation',
79+
parameters: {
80+
type: 'object',
81+
properties: {
82+
expression: {
83+
type: 'string',
84+
description: 'The mathematical expression to evaluate, e.g., "2 + 2", "15 * 23"',
85+
},
86+
},
87+
required: ['expression'],
88+
},
89+
execute: async (args) => {
90+
const expression = args.expression as string;
91+
try {
92+
// Simple and safe evaluation for basic math
93+
// In production, use a proper math parser library
94+
const sanitized = expression.replace(/[^0-9+\-*/().%\s]/g, '');
95+
// eslint-disable-next-line no-eval
96+
const result = eval(sanitized);
97+
return `Result: ${expression} = ${result}`;
98+
} catch {
99+
return `Error: Could not evaluate "${expression}"`;
100+
}
101+
},
102+
},
103+
{
104+
name: 'get_time',
105+
description: 'Get the current date and time',
106+
parameters: {
107+
type: 'object',
108+
properties: {
109+
timezone: {
110+
type: 'string',
111+
description: 'The timezone, e.g., "UTC", "Asia/Shanghai", "America/New_York"',
112+
},
113+
},
114+
},
115+
execute: async (args) => {
116+
const timezone = (args.timezone as string) || 'UTC';
117+
try {
118+
const now = new Date().toLocaleString('en-US', { timeZone: timezone });
119+
return `Current time in ${timezone}: ${now}`;
120+
} catch {
121+
const now = new Date().toISOString();
122+
return `Current time (UTC): ${now}`;
123+
}
124+
},
125+
},
126+
];
127+
128+
// Tool lookup map
129+
const toolMap = new Map(tools.map((t) => [t.name, t]));
130+
131+
// =============================================================================
132+
// Simple Intent Detection (Mock LLM behavior)
133+
// 简单意图检测(模拟 LLM 行为)
134+
// =============================================================================
135+
136+
interface DetectedIntent {
137+
toolName: string;
138+
args: Record<string, unknown>;
139+
}
140+
141+
function detectIntent(message: string): DetectedIntent | null {
142+
const lowerMessage = message.toLowerCase();
143+
144+
// Weather intent
145+
if (lowerMessage.includes('weather')) {
146+
const locations = ['beijing', 'shanghai', 'new york', 'tokyo', 'london', 'paris'];
147+
for (const loc of locations) {
148+
if (lowerMessage.includes(loc)) {
149+
return { toolName: 'get_weather', args: { location: loc.charAt(0).toUpperCase() + loc.slice(1) } };
150+
}
151+
}
152+
// Default location if none specified
153+
return { toolName: 'get_weather', args: { location: 'Beijing' } };
154+
}
155+
156+
// Calculate intent
157+
if (lowerMessage.includes('calculate') || lowerMessage.includes('compute') || /\d+\s*[+\-*/]\s*\d+/.test(message)) {
158+
const match = message.match(/(\d+[\s+\-*/\d.()]+\d+)/);
159+
if (match) {
160+
return { toolName: 'calculate', args: { expression: match[1].trim() } };
161+
}
162+
}
163+
164+
// Time intent
165+
if (lowerMessage.includes('time') || lowerMessage.includes('date')) {
166+
if (lowerMessage.includes('shanghai') || lowerMessage.includes('china')) {
167+
return { toolName: 'get_time', args: { timezone: 'Asia/Shanghai' } };
168+
}
169+
if (lowerMessage.includes('new york') || lowerMessage.includes('us')) {
170+
return { toolName: 'get_time', args: { timezone: 'America/New_York' } };
171+
}
172+
return { toolName: 'get_time', args: { timezone: 'UTC' } };
173+
}
174+
175+
return null;
176+
}
177+
178+
// =============================================================================
179+
// Helper: Token-by-token streaming
180+
// 辅助函数:逐 token 流式输出
181+
// =============================================================================
182+
183+
/**
184+
* Simulate token-by-token streaming output
185+
* 模拟逐 token 流式输出
186+
*
187+
* @param text - The text to stream token by token
188+
* @param delayMs - Delay between tokens in milliseconds (default: 50ms)
189+
*/
190+
async function* streamTokens(text: string, delayMs = 50): AsyncGenerator<string> {
191+
// Split by words while preserving spaces and punctuation
192+
const tokens = text.match(/\S+|\s+/g) || [text];
193+
for (const token of tokens) {
194+
yield token;
195+
await new Promise((resolve) => setTimeout(resolve, delayMs));
196+
}
197+
}
198+
199+
// =============================================================================
200+
// Agent Implementation with Tool Calling
201+
// 带有工具调用的 Agent 实现
202+
// =============================================================================
203+
204+
async function* invokeAgent(request: AgentRequest): AsyncGenerator<string | AgentEvent> {
205+
const lastMessage = request.messages[request.messages.length - 1];
206+
const userContent = typeof lastMessage?.content === 'string' ? lastMessage.content : '';
207+
208+
logger.info(`Received message: ${userContent}`);
209+
210+
// Detect user intent and determine if we need to call a tool
211+
const intent = detectIntent(userContent);
212+
213+
if (intent) {
214+
const tool = toolMap.get(intent.toolName);
215+
if (tool) {
216+
const toolCallId = `call_${Date.now()}`;
217+
218+
logger.info(`Detected intent: ${intent.toolName}`);
219+
logger.info(`Tool arguments: ${JSON.stringify(intent.args)}`);
220+
221+
// Step 1: Emit thinking text token by token (真正的流式输出)
222+
yield* streamTokens('Let me check that for you... ');
223+
224+
// Step 2: Emit TOOL_CALL event
225+
// SDK will automatically convert this to the appropriate protocol format
226+
yield {
227+
event: EventType.TOOL_CALL,
228+
data: {
229+
id: toolCallId,
230+
name: tool.name,
231+
args: JSON.stringify(intent.args),
232+
},
233+
} as AgentEvent;
234+
235+
// Step 3: Execute the tool
236+
logger.info(`Executing tool: ${tool.name}`);
237+
const result = await tool.execute(intent.args);
238+
logger.info(`Tool result: ${result}`);
239+
240+
// Step 4: Emit TOOL_RESULT event
241+
yield {
242+
event: EventType.TOOL_RESULT,
243+
data: {
244+
id: toolCallId,
245+
result: result,
246+
},
247+
} as AgentEvent;
248+
249+
// Step 5: Generate response based on tool result (真正的流式输出)
250+
yield '\n\n';
251+
yield* streamTokens(`Based on my search: ${result}`);
252+
return;
253+
}
254+
}
255+
256+
// No tool needed - just respond directly (真正的流式输出)
257+
yield* streamTokens(`I received your message: "${userContent}". `);
258+
yield* streamTokens('I can help you with:\n');
259+
yield* streamTokens('• Weather information (try: "What is the weather in Beijing?")\n');
260+
yield* streamTokens('• Calculations (try: "Calculate 15 * 23")\n');
261+
yield* streamTokens('• Current time (try: "What time is it in Shanghai?")\n');
262+
}
263+
264+
// =============================================================================
265+
// Server Setup
266+
// 服务器设置
267+
// =============================================================================
268+
269+
const server = new AgentRunServer({
270+
invokeAgent,
271+
config: {
272+
corsOrigins: ['*'],
273+
},
274+
});
275+
276+
// Print startup information
277+
logger.info('Starting AgentRun Server with Tool Calling...');
278+
logger.info('');
279+
logger.info('Available Tools:');
280+
for (const tool of tools) {
281+
logger.info(` • ${tool.name}: ${tool.description}`);
282+
}
283+
logger.info('');
284+
logger.info('Test Examples:');
285+
logger.info('');
286+
logger.info('1. OpenAI Chat Completions API (Non-streaming):');
287+
logger.info(' curl http://127.0.0.1:9000/openai/v1/chat/completions -X POST \\');
288+
logger.info(' -H "Content-Type: application/json" \\');
289+
logger.info(' -d \'{"messages": [{"role": "user", "content": "What is the weather in Beijing?"}], "stream": false}\'');
290+
logger.info('');
291+
logger.info('2. OpenAI Chat Completions API (Streaming):');
292+
logger.info(' curl http://127.0.0.1:9000/openai/v1/chat/completions -X POST \\');
293+
logger.info(' -H "Content-Type: application/json" \\');
294+
logger.info(' -d \'{"messages": [{"role": "user", "content": "Calculate 15 * 23"}], "stream": true}\'');
295+
logger.info('');
296+
logger.info('3. AG-UI Protocol:');
297+
logger.info(' curl http://127.0.0.1:9000/ag-ui/agent -X POST \\');
298+
logger.info(' -H "Content-Type: application/json" \\');
299+
logger.info(' -d \'{"messages": [{"role": "user", "content": "What time is it in Shanghai?"}]}\'');
300+
logger.info('');
301+
302+
server.start({ port: 9000 });

examples/quick-start.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Quick Start Example
3+
*
4+
* 此示例展示如何使用 AgentRun SDK 快速启动一个 Agent 服务器。
5+
*
6+
* 运行方式:
7+
* npx ts-node examples/quick-start.ts
8+
*
9+
* 测试方式:
10+
* curl http://127.0.0.1:9000/openai/v1/chat/completions -X POST \
11+
* -H "Content-Type: application/json" \
12+
* -d '{"messages": [{"role": "user", "content": "Hello!"}], "stream": false}'
13+
*/
14+
15+
import { AgentRunServer, AgentRequest } from '../src/index';
16+
import { logger } from '../src/utils/log';
17+
18+
// Simple echo agent
19+
function invokeAgent(request: AgentRequest) {
20+
const lastMessage = request.messages[request.messages.length - 1];
21+
const userContent = lastMessage?.content || '';
22+
23+
logger.info(`Received message: ${userContent}`);
24+
25+
if (request.stream) {
26+
// Streaming response - yield strings directly
27+
// The SDK will automatically convert strings to TEXT events
28+
return (async function* () {
29+
const response = `You said: "${userContent}". This is a streaming response from AgentRun!`;
30+
31+
// Yield response word by word
32+
const words = response.split(' ');
33+
for (const word of words) {
34+
yield word + ' ';
35+
await new Promise((resolve) => setTimeout(resolve, 100));
36+
}
37+
})();
38+
} else {
39+
// Non-streaming response
40+
return `You said: "${userContent}". This is a response from AgentRun!`;
41+
}
42+
}
43+
44+
// Create and start server
45+
const server = new AgentRunServer({
46+
invokeAgent,
47+
config: {
48+
corsOrigins: ['*'],
49+
},
50+
});
51+
52+
logger.info('Starting AgentRun Server...');
53+
logger.info('');
54+
logger.info('Test with:');
55+
logger.info(' curl http://127.0.0.1:9000/openai/v1/chat/completions -X POST \\');
56+
logger.info(' -H "Content-Type: application/json" \\');
57+
logger.info(' -d \'{"messages": [{"role": "user", "content": "Hello!"}], "stream": false}\'');
58+
logger.info('');
59+
60+
server.start({ port: 9000 });
61+

0 commit comments

Comments
 (0)