@kood/claude-code 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +12 -3
- package/package.json +2 -2
- package/templates/hono/CLAUDE.md +20 -2
- package/templates/hono/docs/architecture/architecture.md +909 -0
- package/templates/hono/docs/deployment/cloudflare.md +537 -190
- package/templates/hono/docs/deployment/docker.md +517 -0
- package/templates/hono/docs/deployment/index.md +181 -213
- package/templates/hono/docs/deployment/railway.md +416 -0
- package/templates/hono/docs/deployment/vercel.md +572 -0
- package/templates/hono/docs/git/git.md +285 -0
- package/templates/hono/docs/library/ai-sdk/index.md +427 -0
- package/templates/hono/docs/library/ai-sdk/openrouter.md +479 -0
- package/templates/hono/docs/library/ai-sdk/providers.md +468 -0
- package/templates/hono/docs/library/ai-sdk/streaming.md +447 -0
- package/templates/hono/docs/library/ai-sdk/structured-output.md +493 -0
- package/templates/hono/docs/library/ai-sdk/tools.md +513 -0
- package/templates/hono/docs/library/hono/env-setup.md +458 -0
- package/templates/hono/docs/library/hono/index.md +1 -0
- package/templates/hono/docs/library/pino/index.md +437 -0
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +503 -0
- package/templates/hono/docs/library/prisma/config.md +362 -0
- package/templates/hono/docs/library/prisma/index.md +86 -13
- package/templates/hono/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +125 -125
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +191 -191
- package/templates/npx/CLAUDE.md +309 -0
- package/templates/npx/docs/git/git.md +307 -0
- package/templates/npx/docs/library/commander/index.md +164 -0
- package/templates/npx/docs/library/fs-extra/index.md +171 -0
- package/templates/npx/docs/library/prompts/index.md +253 -0
- package/templates/npx/docs/mcp/index.md +60 -0
- package/templates/npx/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +134 -0
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +301 -0
- package/templates/tanstack-start/CLAUDE.md +43 -5
- package/templates/tanstack-start/docs/architecture/architecture.md +134 -4
- package/templates/tanstack-start/docs/deployment/cloudflare.md +234 -51
- package/templates/tanstack-start/docs/deployment/index.md +322 -32
- package/templates/tanstack-start/docs/deployment/nitro.md +201 -20
- package/templates/tanstack-start/docs/deployment/railway.md +305 -153
- package/templates/tanstack-start/docs/deployment/vercel.md +353 -78
- package/templates/tanstack-start/docs/git/{index.md → git.md} +81 -7
- package/templates/tanstack-start/docs/guides/best-practices.md +203 -1
- package/templates/tanstack-start/docs/guides/env-setup.md +450 -0
- package/templates/tanstack-start/docs/library/ai-sdk/hooks.md +472 -0
- package/templates/tanstack-start/docs/library/ai-sdk/index.md +264 -0
- package/templates/tanstack-start/docs/library/ai-sdk/openrouter.md +371 -0
- package/templates/tanstack-start/docs/library/ai-sdk/providers.md +403 -0
- package/templates/tanstack-start/docs/library/ai-sdk/streaming.md +320 -0
- package/templates/tanstack-start/docs/library/ai-sdk/structured-output.md +454 -0
- package/templates/tanstack-start/docs/library/ai-sdk/tools.md +473 -0
- package/templates/tanstack-start/docs/library/pino/index.md +320 -0
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +404 -0
- package/templates/tanstack-start/docs/library/prisma/config.md +377 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +3 -1
- package/templates/tanstack-start/docs/library/prisma/schema.md +123 -25
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +80 -2
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +138 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +186 -187
- package/templates/hono/docs/git/index.md +0 -180
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# AI SDK - Tool Calling (Hono)
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [AI SDK](./index.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 개요
|
|
8
|
+
|
|
9
|
+
AI SDK의 도구(Tool)를 Hono에서 사용하면 AI 모델이 외부 함수를 호출할 수 있습니다. 날씨 조회, 데이터베이스 검색, API 호출 등 다양한 작업을 수행할 수 있습니다.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 기본 도구 정의
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Hono } from 'hono'
|
|
17
|
+
import { streamText, tool, convertToModelMessages } from 'ai'
|
|
18
|
+
import { openai } from '@ai-sdk/openai'
|
|
19
|
+
import { z } from 'zod'
|
|
20
|
+
|
|
21
|
+
const app = new Hono()
|
|
22
|
+
|
|
23
|
+
app.post('/api/chat', async (c) => {
|
|
24
|
+
const { messages } = await c.req.json()
|
|
25
|
+
|
|
26
|
+
const result = streamText({
|
|
27
|
+
model: openai('gpt-4o'),
|
|
28
|
+
messages: convertToModelMessages(messages),
|
|
29
|
+
tools: {
|
|
30
|
+
getWeather: tool({
|
|
31
|
+
description: 'Get the weather for a location',
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
location: z.string().describe('The city name'),
|
|
34
|
+
}),
|
|
35
|
+
execute: async ({ location }) => {
|
|
36
|
+
// 실제 날씨 API 호출
|
|
37
|
+
return {
|
|
38
|
+
location,
|
|
39
|
+
temperature: 22,
|
|
40
|
+
condition: 'Sunny',
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return result.toUIMessageStreamResponse()
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## tool() 함수 구조
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { tool } from 'ai'
|
|
57
|
+
import { z } from 'zod'
|
|
58
|
+
|
|
59
|
+
const myTool = tool({
|
|
60
|
+
// 도구 설명 (모델이 언제 사용할지 결정하는데 사용)
|
|
61
|
+
description: 'Tool description for the AI model',
|
|
62
|
+
|
|
63
|
+
// 입력 스키마 (Zod로 정의)
|
|
64
|
+
inputSchema: z.object({
|
|
65
|
+
param1: z.string().describe('Parameter description'),
|
|
66
|
+
param2: z.number().optional(),
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
// 실행 함수
|
|
70
|
+
execute: async (args, context) => {
|
|
71
|
+
// args: 스키마에 맞는 입력 값
|
|
72
|
+
// context: { toolCallId, messages } 등 컨텍스트 정보
|
|
73
|
+
return { result: 'success' }
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 여러 도구 정의
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Hono } from 'hono'
|
|
84
|
+
import { streamText, tool, convertToModelMessages } from 'ai'
|
|
85
|
+
import { openai } from '@ai-sdk/openai'
|
|
86
|
+
import { z } from 'zod'
|
|
87
|
+
|
|
88
|
+
const app = new Hono()
|
|
89
|
+
|
|
90
|
+
app.post('/api/assistant', async (c) => {
|
|
91
|
+
const { messages } = await c.req.json()
|
|
92
|
+
|
|
93
|
+
const result = streamText({
|
|
94
|
+
model: openai('gpt-4o'),
|
|
95
|
+
messages: convertToModelMessages(messages),
|
|
96
|
+
tools: {
|
|
97
|
+
getWeather: tool({
|
|
98
|
+
description: 'Get the weather for a location',
|
|
99
|
+
inputSchema: z.object({
|
|
100
|
+
location: z.string(),
|
|
101
|
+
}),
|
|
102
|
+
execute: async ({ location }) => ({
|
|
103
|
+
temperature: 22,
|
|
104
|
+
condition: 'Sunny',
|
|
105
|
+
}),
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
getAttractions: tool({
|
|
109
|
+
description: 'Get tourist attractions for a city',
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
city: z.string(),
|
|
112
|
+
}),
|
|
113
|
+
execute: async ({ city }) => ({
|
|
114
|
+
attractions: [
|
|
115
|
+
'Gyeongbokgung Palace',
|
|
116
|
+
'N Seoul Tower',
|
|
117
|
+
'Bukchon Hanok Village',
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
searchDatabase: tool({
|
|
123
|
+
description: 'Search the database for information',
|
|
124
|
+
inputSchema: z.object({
|
|
125
|
+
query: z.string(),
|
|
126
|
+
limit: z.number().optional().default(10),
|
|
127
|
+
}),
|
|
128
|
+
execute: async ({ query, limit }) => {
|
|
129
|
+
// 데이터베이스 검색 로직
|
|
130
|
+
return { results: [], total: 0 }
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return result.toUIMessageStreamResponse()
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 도구와 외부 서비스 연동
|
|
143
|
+
|
|
144
|
+
### 데이터베이스 연동
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { Hono } from 'hono'
|
|
148
|
+
import { streamText, tool, convertToModelMessages } from 'ai'
|
|
149
|
+
import { openai } from '@ai-sdk/openai'
|
|
150
|
+
import { z } from 'zod'
|
|
151
|
+
import { prisma } from '@/database/prisma'
|
|
152
|
+
|
|
153
|
+
const app = new Hono()
|
|
154
|
+
|
|
155
|
+
app.post('/api/chat', async (c) => {
|
|
156
|
+
const { messages } = await c.req.json()
|
|
157
|
+
|
|
158
|
+
const result = streamText({
|
|
159
|
+
model: openai('gpt-4o'),
|
|
160
|
+
messages: convertToModelMessages(messages),
|
|
161
|
+
tools: {
|
|
162
|
+
searchProducts: tool({
|
|
163
|
+
description: 'Search for products in the database',
|
|
164
|
+
inputSchema: z.object({
|
|
165
|
+
query: z.string().describe('Search query'),
|
|
166
|
+
category: z.string().optional().describe('Product category'),
|
|
167
|
+
maxPrice: z.number().optional().describe('Maximum price'),
|
|
168
|
+
}),
|
|
169
|
+
execute: async ({ query, category, maxPrice }) => {
|
|
170
|
+
const products = await prisma.product.findMany({
|
|
171
|
+
where: {
|
|
172
|
+
name: { contains: query },
|
|
173
|
+
...(category && { category }),
|
|
174
|
+
...(maxPrice && { price: { lte: maxPrice } }),
|
|
175
|
+
},
|
|
176
|
+
take: 10,
|
|
177
|
+
})
|
|
178
|
+
return { products }
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
|
|
182
|
+
getOrderStatus: tool({
|
|
183
|
+
description: 'Get the status of an order',
|
|
184
|
+
inputSchema: z.object({
|
|
185
|
+
orderId: z.string().describe('The order ID'),
|
|
186
|
+
}),
|
|
187
|
+
execute: async ({ orderId }) => {
|
|
188
|
+
const order = await prisma.order.findUnique({
|
|
189
|
+
where: { id: orderId },
|
|
190
|
+
include: { items: true },
|
|
191
|
+
})
|
|
192
|
+
return order ?? { error: 'Order not found' }
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return result.toUIMessageStreamResponse()
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 외부 API 연동
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { Hono } from 'hono'
|
|
206
|
+
import { streamText, tool, convertToModelMessages } from 'ai'
|
|
207
|
+
import { openai } from '@ai-sdk/openai'
|
|
208
|
+
import { z } from 'zod'
|
|
209
|
+
|
|
210
|
+
type Bindings = {
|
|
211
|
+
WEATHER_API_KEY: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const app = new Hono<{ Bindings: Bindings }>()
|
|
215
|
+
|
|
216
|
+
app.post('/api/chat', async (c) => {
|
|
217
|
+
const { messages } = await c.req.json()
|
|
218
|
+
|
|
219
|
+
const result = streamText({
|
|
220
|
+
model: openai('gpt-4o'),
|
|
221
|
+
messages: convertToModelMessages(messages),
|
|
222
|
+
tools: {
|
|
223
|
+
getWeather: tool({
|
|
224
|
+
description: 'Get real-time weather data',
|
|
225
|
+
inputSchema: z.object({
|
|
226
|
+
city: z.string(),
|
|
227
|
+
}),
|
|
228
|
+
execute: async ({ city }) => {
|
|
229
|
+
const response = await fetch(
|
|
230
|
+
`https://api.weather.com/v1/current?city=${city}&key=${c.env.WEATHER_API_KEY}`
|
|
231
|
+
)
|
|
232
|
+
return response.json()
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
translateText: tool({
|
|
237
|
+
description: 'Translate text to another language',
|
|
238
|
+
inputSchema: z.object({
|
|
239
|
+
text: z.string(),
|
|
240
|
+
targetLanguage: z.string(),
|
|
241
|
+
}),
|
|
242
|
+
execute: async ({ text, targetLanguage }) => {
|
|
243
|
+
// 번역 API 호출
|
|
244
|
+
const response = await fetch('https://api.translate.com/v1/translate', {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: { 'Content-Type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ text, target: targetLanguage }),
|
|
248
|
+
})
|
|
249
|
+
return response.json()
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
return result.toUIMessageStreamResponse()
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 다중 단계 실행
|
|
262
|
+
|
|
263
|
+
AI가 여러 도구를 순차적으로 호출하도록 허용:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { Hono } from 'hono'
|
|
267
|
+
import { generateText, tool, stepCountIs } from 'ai'
|
|
268
|
+
import { openai } from '@ai-sdk/openai'
|
|
269
|
+
import { z } from 'zod'
|
|
270
|
+
|
|
271
|
+
const app = new Hono()
|
|
272
|
+
|
|
273
|
+
app.post('/api/plan-trip', async (c) => {
|
|
274
|
+
const { destination } = await c.req.json()
|
|
275
|
+
|
|
276
|
+
const result = await generateText({
|
|
277
|
+
model: openai('gpt-4o'),
|
|
278
|
+
prompt: `Plan a trip to ${destination}`,
|
|
279
|
+
tools: {
|
|
280
|
+
getWeather: tool({
|
|
281
|
+
description: 'Get weather forecast',
|
|
282
|
+
inputSchema: z.object({ location: z.string() }),
|
|
283
|
+
execute: async ({ location }) => ({ forecast: 'Sunny, 22°C' }),
|
|
284
|
+
}),
|
|
285
|
+
getFlights: tool({
|
|
286
|
+
description: 'Search for flights',
|
|
287
|
+
inputSchema: z.object({ destination: z.string() }),
|
|
288
|
+
execute: async ({ destination }) => ({ flights: ['Flight A', 'Flight B'] }),
|
|
289
|
+
}),
|
|
290
|
+
getHotels: tool({
|
|
291
|
+
description: 'Search for hotels',
|
|
292
|
+
inputSchema: z.object({ location: z.string() }),
|
|
293
|
+
execute: async ({ location }) => ({ hotels: ['Hotel A', 'Hotel B'] }),
|
|
294
|
+
}),
|
|
295
|
+
},
|
|
296
|
+
stopWhen: stepCountIs(5), // 최대 5단계
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return c.json({
|
|
300
|
+
plan: result.text,
|
|
301
|
+
steps: result.steps.map((step) => ({
|
|
302
|
+
toolCalls: step.toolCalls,
|
|
303
|
+
toolResults: step.toolResults,
|
|
304
|
+
})),
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 도구 컨텍스트 활용
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
app.post('/api/chat', async (c) => {
|
|
315
|
+
const { messages } = await c.req.json()
|
|
316
|
+
|
|
317
|
+
const result = streamText({
|
|
318
|
+
model: openai('gpt-4o'),
|
|
319
|
+
messages: convertToModelMessages(messages),
|
|
320
|
+
tools: {
|
|
321
|
+
processRequest: tool({
|
|
322
|
+
description: 'Process a user request',
|
|
323
|
+
inputSchema: z.object({ request: z.string() }),
|
|
324
|
+
execute: async (args, context) => {
|
|
325
|
+
// 도구 호출 ID
|
|
326
|
+
console.log('Tool call ID:', context.toolCallId)
|
|
327
|
+
|
|
328
|
+
// 전체 메시지 히스토리 접근
|
|
329
|
+
console.log('Messages:', context.messages)
|
|
330
|
+
|
|
331
|
+
return { processed: true }
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
return result.toUIMessageStreamResponse()
|
|
338
|
+
})
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 도구 결과 스트리밍
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { Hono } from 'hono'
|
|
347
|
+
import { streamText, tool } from 'ai'
|
|
348
|
+
import { openai } from '@ai-sdk/openai'
|
|
349
|
+
import { z } from 'zod'
|
|
350
|
+
import { stream } from 'hono/streaming'
|
|
351
|
+
|
|
352
|
+
const app = new Hono()
|
|
353
|
+
|
|
354
|
+
app.post('/api/chat', async (c) => {
|
|
355
|
+
const { messages } = await c.req.json()
|
|
356
|
+
|
|
357
|
+
const result = streamText({
|
|
358
|
+
model: openai('gpt-4o'),
|
|
359
|
+
messages,
|
|
360
|
+
tools: {
|
|
361
|
+
getWeather: tool({
|
|
362
|
+
description: 'Get weather',
|
|
363
|
+
inputSchema: z.object({ location: z.string() }),
|
|
364
|
+
execute: async ({ location }) => ({ temp: 22 }),
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
return stream(c, async (stream) => {
|
|
370
|
+
for await (const event of result.fullStream) {
|
|
371
|
+
switch (event.type) {
|
|
372
|
+
case 'text-delta':
|
|
373
|
+
await stream.write(
|
|
374
|
+
JSON.stringify({ type: 'text', content: event.textDelta }) + '\n'
|
|
375
|
+
)
|
|
376
|
+
break
|
|
377
|
+
case 'tool-call':
|
|
378
|
+
await stream.write(
|
|
379
|
+
JSON.stringify({
|
|
380
|
+
type: 'tool-call',
|
|
381
|
+
name: event.toolName,
|
|
382
|
+
args: event.args,
|
|
383
|
+
}) + '\n'
|
|
384
|
+
)
|
|
385
|
+
break
|
|
386
|
+
case 'tool-result':
|
|
387
|
+
await stream.write(
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
type: 'tool-result',
|
|
390
|
+
name: event.toolName,
|
|
391
|
+
result: event.result,
|
|
392
|
+
}) + '\n'
|
|
393
|
+
)
|
|
394
|
+
break
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 타입 안전한 도구
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import { tool, TypedToolCall, TypedToolResult } from 'ai'
|
|
407
|
+
import { z } from 'zod'
|
|
408
|
+
|
|
409
|
+
const myToolSet = {
|
|
410
|
+
greet: tool({
|
|
411
|
+
description: 'Greet a user',
|
|
412
|
+
inputSchema: z.object({ name: z.string() }),
|
|
413
|
+
execute: async ({ name }) => `Hello, ${name}!`,
|
|
414
|
+
}),
|
|
415
|
+
calculate: tool({
|
|
416
|
+
description: 'Calculate sum',
|
|
417
|
+
inputSchema: z.object({
|
|
418
|
+
a: z.number(),
|
|
419
|
+
b: z.number(),
|
|
420
|
+
}),
|
|
421
|
+
execute: async ({ a, b }) => a + b,
|
|
422
|
+
}),
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 타입 추출
|
|
426
|
+
type MyToolCall = TypedToolCall<typeof myToolSet>
|
|
427
|
+
type MyToolResult = TypedToolResult<typeof myToolSet>
|
|
428
|
+
|
|
429
|
+
// Hono 라우트에서 사용
|
|
430
|
+
app.post('/api/typed-chat', async (c) => {
|
|
431
|
+
const { messages } = await c.req.json()
|
|
432
|
+
|
|
433
|
+
const result = await generateText({
|
|
434
|
+
model: openai('gpt-4o'),
|
|
435
|
+
tools: myToolSet,
|
|
436
|
+
messages,
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// 타입 안전한 접근
|
|
440
|
+
const toolCalls: MyToolCall[] = result.toolCalls
|
|
441
|
+
const toolResults: MyToolResult[] = result.toolResults
|
|
442
|
+
|
|
443
|
+
return c.json({ toolCalls, toolResults })
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## 도구 에러 처리
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
app.post('/api/chat', async (c) => {
|
|
453
|
+
const { messages } = await c.req.json()
|
|
454
|
+
|
|
455
|
+
const result = streamText({
|
|
456
|
+
model: openai('gpt-4o'),
|
|
457
|
+
messages: convertToModelMessages(messages),
|
|
458
|
+
tools: {
|
|
459
|
+
riskyOperation: tool({
|
|
460
|
+
description: 'Perform a risky operation',
|
|
461
|
+
inputSchema: z.object({ data: z.string() }),
|
|
462
|
+
execute: async ({ data }) => {
|
|
463
|
+
try {
|
|
464
|
+
// 위험한 작업 수행
|
|
465
|
+
const result = await performRiskyTask(data)
|
|
466
|
+
return { success: true, result }
|
|
467
|
+
} catch (error) {
|
|
468
|
+
// 에러를 AI가 이해할 수 있는 형태로 반환
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
},
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
return result.toUIMessageStreamResponse()
|
|
480
|
+
})
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## 도구 정의 팁
|
|
486
|
+
|
|
487
|
+
1. **명확한 설명**: 모델이 언제 도구를 사용할지 이해하도록 상세히 작성
|
|
488
|
+
2. **스키마 설명**: 각 파라미터에 `.describe()` 추가
|
|
489
|
+
3. **에러 처리**: execute 함수에서 적절한 에러 처리
|
|
490
|
+
4. **반환값**: 모델이 이해할 수 있는 구조화된 데이터 반환
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
const goodTool = tool({
|
|
494
|
+
description: `
|
|
495
|
+
Search for products in the database.
|
|
496
|
+
Use this when the user asks about available products,
|
|
497
|
+
product prices, or product availability.
|
|
498
|
+
`,
|
|
499
|
+
inputSchema: z.object({
|
|
500
|
+
query: z.string().describe('Search query for product name or category'),
|
|
501
|
+
maxResults: z.number().default(10).describe('Maximum number of results'),
|
|
502
|
+
category: z.string().optional().describe('Filter by category'),
|
|
503
|
+
}),
|
|
504
|
+
execute: async ({ query, maxResults, category }) => {
|
|
505
|
+
try {
|
|
506
|
+
const results = await db.products.search({ query, maxResults, category })
|
|
507
|
+
return { success: true, products: results }
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return { success: false, error: 'Failed to search products' }
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
```
|