Skip to content

Commit d6fd33b

Browse files
committed
fixing client and panel issues
1 parent 9dd1793 commit d6fd33b

4 files changed

Lines changed: 114 additions & 67 deletions

File tree

packages/typescript/ai-client/src/chat-client.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import type { ChatClientEventEmitter } from './events'
1616

1717
export class ChatClient {
1818
private processor: StreamProcessor
19-
private connectionRef: { current: ConnectionAdapter }
19+
private connection: ConnectionAdapter
2020
private uniqueId: string
21-
private bodyRef: { current?: Record<string, any> }
21+
private body: Record<string, any> = {}
2222
private isLoading = false
2323
private error: Error | undefined = undefined
2424
private abortController: AbortController | null = null
@@ -39,8 +39,8 @@ export class ChatClient {
3939

4040
constructor(options: ChatClientOptions) {
4141
this.uniqueId = options.id || this.generateUniqueId('chat')
42-
this.bodyRef = { current: options.body }
43-
this.connectionRef = { current: options.connection }
42+
this.body = options.body || {}
43+
this.connection = options.connection
4444
this.events = new DefaultChatClientEventEmitter(this.uniqueId)
4545

4646
// Build client tools map
@@ -235,9 +235,9 @@ export class ChatClient {
235235
await this.callbacksRef.current.onResponse()
236236

237237
// Connect and stream
238-
const stream = this.connectionRef.current.connect(
238+
const stream = this.connection.connect(
239239
modelMessages,
240-
this.bodyRef.current,
240+
this.body,
241241
this.abortController.signal,
242242
)
243243

@@ -425,10 +425,10 @@ export class ChatClient {
425425
onError?: (error: Error) => void
426426
}): void {
427427
if (options.connection !== undefined) {
428-
this.connectionRef.current = options.connection
428+
this.connection = options.connection
429429
}
430430
if (options.body !== undefined) {
431-
this.bodyRef.current = options.body
431+
this.body = options.body
432432
}
433433
if (options.tools !== undefined) {
434434
this.clientToolsRef.current = new Map()

testing/panel/src/lib/guitar-tools.ts

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
1-
import { tool } from '@tanstack/ai'
1+
import { toolDefinition } from '@tanstack/ai'
22
import { z } from 'zod'
33
import guitars from '@/data/example-guitars'
44

5-
export const getGuitarsTool = tool({
5+
// Tool definition for getting guitars
6+
export const getGuitarsToolDef = toolDefinition({
67
name: 'getGuitars',
78
description: 'Get all products from the database',
89
inputSchema: z.object({}),
9-
execute: async () => {
10-
return guitars
11-
},
10+
outputSchema: z.array(
11+
z.object({
12+
id: z.number(),
13+
name: z.string(),
14+
image: z.string(),
15+
description: z.string(),
16+
shortDescription: z.string(),
17+
price: z.number(),
18+
}),
19+
),
1220
})
1321

14-
export const recommendGuitarTool = tool({
22+
// Server implementation
23+
export const getGuitars = getGuitarsToolDef.server(() => guitars)
24+
25+
// Tool definition for guitar recommendation
26+
export const recommendGuitarToolDef = toolDefinition({
1527
name: 'recommendGuitar',
1628
description:
1729
'REQUIRED tool to display a guitar recommendation to the user. This tool MUST be used whenever recommending a guitar - do NOT write recommendations yourself. This displays the guitar in a special appealing format with a buy button.',
@@ -22,49 +34,51 @@ export const recommendGuitarTool = tool({
2234
'The ID of the guitar to recommend (from the getGuitars results)',
2335
),
2436
}),
37+
outputSchema: z.object({
38+
id: z.string(),
39+
}),
2540
})
2641

27-
export const getPersonalGuitarPreferenceTool = tool({
42+
// Tool definition for personal preference
43+
export const getPersonalGuitarPreferenceToolDef = toolDefinition({
2844
name: 'getPersonalGuitarPreference',
2945
description:
3046
"Get the user's guitar preference from their local browser storage",
3147
inputSchema: z.object({}),
32-
// No execute = client-side tool
48+
outputSchema: z.object({
49+
preference: z.string(),
50+
}),
3351
})
3452

35-
export const addToWishListTool = tool({
53+
// Tool definition for wish list (needs approval)
54+
export const addToWishListToolDef = toolDefinition({
3655
name: 'addToWishList',
3756
description: "Add a guitar to the user's wish list (requires approval)",
3857
inputSchema: z.object({
3958
guitarId: z.string(),
4059
}),
60+
outputSchema: z.object({
61+
success: z.boolean(),
62+
guitarId: z.string(),
63+
totalItems: z.number(),
64+
}),
4165
needsApproval: true,
42-
// No execute = client-side but needs approval
4366
})
4467

45-
export const addToCartTool = tool({
68+
// Tool definition for add to cart (server + client)
69+
export const addToCartToolDef = toolDefinition({
4670
name: 'addToCart',
4771
description: 'Add a guitar to the shopping cart (requires approval)',
4872
inputSchema: z.object({
4973
guitarId: z.string(),
5074
quantity: z.number(),
5175
}),
76+
outputSchema: z.object({
77+
success: z.boolean(),
78+
cartId: z.string(),
79+
guitarId: z.string(),
80+
quantity: z.number(),
81+
totalItems: z.number(),
82+
}),
5283
needsApproval: true,
53-
execute: async (args) => {
54-
return {
55-
success: true,
56-
cartId: 'CART_' + Date.now(),
57-
guitarId: args.guitarId,
58-
quantity: args.quantity,
59-
totalItems: args.quantity,
60-
}
61-
},
6284
})
63-
64-
export const allTools = [
65-
getGuitarsTool,
66-
recommendGuitarTool,
67-
getPersonalGuitarPreferenceTool,
68-
addToWishListTool,
69-
addToCartTool,
70-
]

testing/panel/src/routes/api.chat.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import { gemini } from '@tanstack/ai-gemini'
77
import { openai } from '@tanstack/ai-openai'
88
import { ollama } from '@tanstack/ai-ollama'
99
import { createEventRecording } from '@/lib/recording'
10-
import { allTools } from '@/lib/guitar-tools'
10+
import {
11+
addToCartToolDef,
12+
addToWishListToolDef,
13+
getGuitars,
14+
getPersonalGuitarPreferenceToolDef,
15+
recommendGuitarToolDef,
16+
} from '@/lib/guitar-tools'
1117

1218
const SYSTEM_PROMPT = `You are a helpful assistant for a guitar store.
1319
@@ -31,6 +37,13 @@ Step 1: Call getGuitars()
3137
Step 2: Call recommendGuitar(id: "6")
3238
Step 3: Done - do NOT add any text after calling recommendGuitar
3339
`
40+
const addToCartToolServer = addToCartToolDef.server((args) => ({
41+
success: true,
42+
cartId: 'CART_' + Date.now(),
43+
guitarId: args.guitarId,
44+
quantity: args.quantity,
45+
totalItems: args.quantity,
46+
}))
3447

3548
type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama'
3649

@@ -97,9 +110,15 @@ export const Route = createFileRoute('/api/chat')({
97110

98111
// Use the stream abort signal for proper cancellation handling
99112
const stream = chat({
100-
adapter,
101-
model: selectedModel as any, // Dynamic model selection
102-
tools: allTools,
113+
adapter: adapter as any,
114+
model: selectedModel as any,
115+
tools: [
116+
getGuitars, // Server tool
117+
recommendGuitarToolDef, // No server execute - client will handle
118+
addToCartToolServer,
119+
addToWishListToolDef,
120+
getPersonalGuitarPreferenceToolDef,
121+
],
103122
systemPrompts: [SYSTEM_PROMPT],
104123
agentLoopStrategy: maxIterations(20),
105124
messages,

testing/panel/src/routes/index.tsx

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import rehypeSanitize from 'rehype-sanitize'
77
import rehypeHighlight from 'rehype-highlight'
88
import remarkGfm from 'remark-gfm'
99
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
10+
import { clientTools } from '@tanstack/ai-client'
1011
import { ThinkingPart } from '@tanstack/ai-react-ui'
1112

1213
import type { UIMessage } from '@tanstack/ai-react'
1314

1415
import GuitarRecommendation from '@/components/example-GuitarRecommendation'
16+
import {
17+
addToCartToolDef,
18+
addToWishListToolDef,
19+
getPersonalGuitarPreferenceToolDef,
20+
recommendGuitarToolDef,
21+
} from '@/lib/guitar-tools'
1522
import {
1623
MODEL_OPTIONS,
1724
getDefaultModelOption,
@@ -20,6 +27,39 @@ import {
2027
} from '@/lib/model-selection'
2128
import './tanchat.css'
2229

30+
const getPersonalGuitarPreferenceToolClient =
31+
getPersonalGuitarPreferenceToolDef.client(() => ({ preference: 'acoustic' }))
32+
33+
const addToWishListToolClient = addToWishListToolDef.client((args) => {
34+
const wishList = JSON.parse(localStorage.getItem('wishList') || '[]')
35+
wishList.push(args.guitarId)
36+
localStorage.setItem('wishList', JSON.stringify(wishList))
37+
return {
38+
success: true,
39+
guitarId: args.guitarId,
40+
totalItems: wishList.length,
41+
}
42+
})
43+
44+
const addToCartToolClient = addToCartToolDef.client((args) => ({
45+
success: true,
46+
cartId: 'CART_CLIENT_' + Date.now(),
47+
guitarId: args.guitarId,
48+
quantity: args.quantity,
49+
totalItems: args.quantity,
50+
}))
51+
52+
const recommendGuitarToolClient = recommendGuitarToolDef.client(({ id }) => ({
53+
id,
54+
}))
55+
56+
const tools = clientTools(
57+
getPersonalGuitarPreferenceToolClient,
58+
addToWishListToolClient,
59+
addToCartToolClient,
60+
recommendGuitarToolClient,
61+
)
62+
2363
function ChatInputArea({ children }: { children: React.ReactNode }) {
2464
return (
2565
<div className="border-t border-orange-500/10 bg-gray-900/80 backdrop-blur-sm">
@@ -365,37 +405,11 @@ function ChatPage() {
365405
const { messages, sendMessage, isLoading, addToolApprovalResponse, stop } =
366406
useChat({
367407
connection: fetchServerSentEvents('/api/chat'),
408+
tools,
368409
onChunk: (chunk: any) => {
369410
setChunks((prev) => [...prev, chunk])
370411
},
371412
body,
372-
onToolCall: async ({ toolName, input }) => {
373-
// Handle client-side tool execution
374-
switch (toolName) {
375-
case 'getPersonalGuitarPreference':
376-
// Pure client tool - executes immediately
377-
return { preference: 'acoustic' }
378-
379-
case 'recommendGuitar':
380-
// Client tool for UI display
381-
return { id: input.id }
382-
383-
case 'addToWishList':
384-
// Hybrid: client execution AFTER approval
385-
// Only runs after user approves
386-
const wishList = JSON.parse(
387-
localStorage.getItem('wishList') || '[]',
388-
)
389-
wishList.push(input.guitarId)
390-
localStorage.setItem('wishList', JSON.stringify(wishList))
391-
return {
392-
success: true,
393-
guitarId: input.guitarId,
394-
totalItems: wishList.length,
395-
}
396-
}
397-
return Promise.resolve({ result: 'Unknown client tool' })
398-
},
399413
})
400414
const [input, setInput] = useState('')
401415

0 commit comments

Comments
 (0)