-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (ai/vue): add useAssistant (#2245)
Co-authored-by: Nelson Nelson-Atuonwu <[email protected]>
- Loading branch information
Showing
19 changed files
with
883 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'ai': patch | ||
'@ai-sdk/vue': patch | ||
--- | ||
|
||
feat (ai/vue): add useAssistant |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
NUXT_OPENAI_API_KEY=xxxxxxx | ||
NUXT_ASSISTANT_ID=xxxxxxx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<script lang="ts" setup> | ||
import { useAssistant } from '@ai-sdk/vue'; | ||
import type { Message } from '@ai-sdk/vue'; | ||
const roleToColorMap: Record<Message['role'], string> = { | ||
system: 'red', | ||
user: 'black', | ||
function: 'blue', | ||
tool: 'purple', | ||
assistant: 'green', | ||
data: 'orange', | ||
}; | ||
const { messages, status, input, handleSubmit, error, stop } = useAssistant({ | ||
api: '/api/assistant', | ||
}); | ||
// Create a reference of the input element and focus on it when the component is mounted & the assistant status is 'awaiting_message' | ||
const inputRef = ref<HTMLInputElement | null>(null); | ||
watchEffect(() => { | ||
if (inputRef.value && status.value === 'awaiting_message') { | ||
inputRef.value.focus(); | ||
} | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div class="flex flex-col w-full max-w-md py-24 mx-auto stretch"> | ||
<!-- Render Assistant API errors if any --> | ||
<div | ||
class="relative px-6 py-4 text-white bg-red-500 rounded-md" | ||
v-if="error" | ||
> | ||
<span class="block sm:inline"> Error: {{ error?.toString() }} </span> | ||
</div> | ||
|
||
<!-- Render Assistant Messages --> | ||
<div | ||
class="whitespace-pre-wrap" | ||
v-for="(message, index) in messages" | ||
:key="index" | ||
:style="{ color: roleToColorMap[message.role] }" | ||
> | ||
<strong>{{ `${message.role}: ` }}</strong> | ||
{{ message.role !== 'data' && message.content }} | ||
<template v-if="message.role === 'data'"> | ||
{{ (message.data as any)?.description }} | ||
<br /> | ||
<pre class="bg-gray-200">{{ | ||
JSON.stringify(message.data, null, 2) | ||
}}</pre> | ||
</template> | ||
<br /> | ||
<br /> | ||
</div> | ||
|
||
<!-- Render Assistant Status Indicator (In Progress) --> | ||
<div | ||
class="w-full h-8 max-w-md p-2 mb-8 bg-gray-300 rounded-lg dark:bg-gray-600 animate-pulse" | ||
v-if="status === 'in_progress'" | ||
></div> | ||
|
||
<!-- Render Assistant Message Input Form --> | ||
<form @submit.prevent="(e) => handleSubmit(e as any)"> | ||
<input | ||
ref="inputRef" | ||
:disabled="status === 'in_progress'" | ||
class="fixed w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl bottom-14 ax-w-md" | ||
v-model="input" | ||
placeholder="What is the temperature in the living room?" | ||
/> | ||
</form> | ||
|
||
<button | ||
@click="stop" | ||
:disabled="status === 'awaiting_message'" | ||
class="fixed bottom-0 w-full max-w-md p-2 mb-8 text-white bg-red-500 rounded-lg disabled:opacity-50" | ||
> | ||
Stop | ||
</button> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { AssistantResponse } from 'ai'; | ||
import OpenAI from 'openai'; | ||
|
||
type AssistantRequest = { | ||
threadId: string | null; | ||
message: string; | ||
}; | ||
|
||
// Allow streaming responses up to 30 seconds | ||
export const maxDuration = 30; | ||
|
||
export default defineLazyEventHandler(async () => { | ||
// Validate the OpenAI API key and Assistant ID are set | ||
const apiKey = useRuntimeConfig().openaiApiKey; | ||
if (!apiKey) | ||
throw new Error('Missing OpenAI API key, `NUXT_OPEN_API_KEY` not set'); | ||
|
||
const assistantId = useRuntimeConfig().assistantId; | ||
if (!assistantId) | ||
throw new Error('Missing Assistant ID, `NUXT_ASSISTANT_ID` not set'); | ||
|
||
// Create an OpenAI API client (that's edge friendly!) | ||
const openai = new OpenAI({ apiKey }); | ||
|
||
const homeTemperatures = { | ||
bedroom: 20, | ||
'home office': 21, | ||
'living room': 21, | ||
kitchen: 22, | ||
bathroom: 23, | ||
}; | ||
|
||
return defineEventHandler(async (event: any) => { | ||
const { threadId: userThreadId, message }: AssistantRequest = | ||
await readBody(event); | ||
|
||
// Extract the signal from the H3 request if available | ||
const signal = event?.web?.request?.signal; | ||
|
||
// Create a thread if needed | ||
const threadId = userThreadId ?? (await openai.beta.threads.create({})).id; | ||
|
||
// Add a message to the thread | ||
const createdMessage = await openai.beta.threads.messages.create( | ||
threadId, | ||
{ | ||
role: 'user', | ||
content: message, | ||
}, | ||
{ signal }, | ||
); | ||
|
||
return AssistantResponse( | ||
{ threadId, messageId: createdMessage.id }, | ||
async ({ forwardStream, sendDataMessage }) => { | ||
// Run the assistant on the thread | ||
const runStream = openai.beta.threads.runs.stream( | ||
threadId, | ||
{ assistant_id: assistantId }, | ||
{ signal }, | ||
); | ||
|
||
// forward run status would stream message deltas | ||
let runResult = await forwardStream(runStream); | ||
|
||
// status can be: queued, in_progress, requires_action, cancelling, cancelled, failed, completed, or expired | ||
while ( | ||
runResult?.status === 'requires_action' && | ||
runResult?.required_action?.type === 'submit_tool_outputs' | ||
) { | ||
// Process the required action to submit tool outputs | ||
const tool_outputs = | ||
runResult.required_action.submit_tool_outputs.tool_calls.map( | ||
(toolCall: any) => { | ||
const parameters = JSON.parse(toolCall.function.arguments); | ||
|
||
switch (toolCall.function.name) { | ||
case 'getRoomTemperature': { | ||
const room: keyof typeof homeTemperatures = parameters.room; | ||
const temperature = homeTemperatures[room]; | ||
|
||
return { | ||
tool_call_id: toolCall.id, | ||
output: temperature.toString(), | ||
}; | ||
} | ||
|
||
case 'setRoomTemperature': { | ||
const room: keyof typeof homeTemperatures = parameters.room; | ||
const oldTemperature = homeTemperatures[room]; | ||
|
||
homeTemperatures[room] = parameters.temperature; | ||
|
||
sendDataMessage({ | ||
role: 'data', | ||
data: { | ||
oldTemperature, | ||
newTemperature: parameters.temperature, | ||
description: `Temperature in the ${room} changed from ${oldTemperature} to ${parameters.temperature}`, | ||
}, | ||
}); | ||
|
||
return { | ||
tool_call_id: toolCall.id, | ||
output: 'Temperature set successfully', | ||
}; | ||
} | ||
default: { | ||
throw new Error( | ||
`Unknown tool call function: ${toolCall.function.name}`, | ||
); | ||
} | ||
} | ||
}, | ||
); | ||
|
||
// Submit the tool outputs | ||
runResult = await forwardStream( | ||
openai.beta.threads.runs.submitToolOutputsStream( | ||
threadId, | ||
runResult.id, | ||
{ tool_outputs }, | ||
{ signal }, | ||
), | ||
); | ||
} | ||
}, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<script lang="ts" setup> | ||
import { useAssistant } from './use-assistant'; | ||
const { status, messages, append } = useAssistant({ | ||
api: '/api/assistant' | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div data-testid="status">{{ status }}</div> | ||
<div v-for="(message, index) in messages" :data-testid="`message-${index}`" :key="index"> | ||
{{ message.role === 'user' ? 'User: ' : 'AI: ' }} | ||
{{ message.content }} | ||
</div> | ||
<button data-testid="do-append" @click="append({ role: 'user', content: 'hi' })" /> | ||
</div> | ||
</template> |
22 changes: 22 additions & 0 deletions
22
packages/vue/src/TestChatAssistantThreadChangeComponent.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<script setup lang="ts"> | ||
import { useAssistant } from './use-assistant'; | ||
const { status, messages, error, append, setThreadId, threadId } = useAssistant({ | ||
api: '/api/assistant' | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div data-testid="status">{{ status }}</div> | ||
<div data-testid="thread-id">{{ threadId || 'undefined' }}</div> | ||
<div data-testid="error">{{ error?.toString() }}</div> | ||
<div v-for="(message, index) in messages" :data-testid="`message-${index}`" :key="index"> | ||
{{ message.role === 'user' ? 'User: ' : 'AI: ' }} | ||
{{ message.content }} | ||
</div> | ||
<button data-testid="do-append" @click="append({ role: 'user', content: 'hi' })" /> | ||
<button data-testid="do-new-thread" @click="setThreadId(undefined)" /> | ||
<button data-testid="do-thread-3" @click="setThreadId('t3')" /> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './use-chat'; | ||
export * from './use-completion'; | ||
export * from './use-assistant'; |
Oops, something went wrong.