Skip to content

Commit

Permalink
feat (ai/vue): add useAssistant (#2245)
Browse files Browse the repository at this point in the history
Co-authored-by: Nelson Nelson-Atuonwu <[email protected]>
  • Loading branch information
lgrammel and Kunoacc authored Jul 11, 2024
1 parent 8ef3386 commit dd0d854
Show file tree
Hide file tree
Showing 19 changed files with 883 additions and 26 deletions.
6 changes: 6 additions & 0 deletions .changeset/smart-fishes-pay.md
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
2 changes: 1 addition & 1 deletion content/docs/05-ai-sdk-ui/01-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Here is a comparison of the supported functions across these frameworks:
| [useChat](/docs/reference/ai-sdk-ui/use-chat) tool calling | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> |
| [useCompletion](/docs/reference/ai-sdk-ui/use-completion) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
| [useObject](/docs/reference/ai-sdk-ui/use-object) | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
| [useAssistant](/docs/reference/ai-sdk-ui/use-assistant) | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
| [useAssistant](/docs/reference/ai-sdk-ui/use-assistant) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |

<Note>
[Contributions](https://github.com/vercel/ai/blob/main/CONTRIBUTING.md) are
Expand Down
2 changes: 1 addition & 1 deletion content/docs/05-ai-sdk-ui/10-openai-assistants.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The `useAssistant` hook allows you to handle the client state when interacting w
This hook is useful when you want to integrate assistant capabilities into your application,
with the UI updated automatically as the assistant is streaming its execution.

The `useAssistant` hook is currently supported with `ai/react` and `ai/svelte`.
The `useAssistant` hook is supported in `ai/react`, `ai/svelte`, and `ai/vue`.

## Example

Expand Down
2 changes: 1 addition & 1 deletion content/docs/07-reference/ai-sdk-ui/20-use-assistant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ with the UI updated automatically as the assistant is streaming its execution.
This works in conjunction with [`AssistantResponse`](./assistant-response) in the backend.

<Note>
`useAssistant` is currently supported with `ai/react` and `ai/svelte`.
`useAssistant` is supported in `ai/react`, `ai/svelte`, and `ai/vue`.
</Note>

## Import
Expand Down
2 changes: 1 addition & 1 deletion content/docs/07-reference/ai-sdk-ui/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Here is a comparison of the supported functions across these frameworks:
| [useChat](/docs/reference/ai-sdk-ui/use-chat) tool calling | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Check size={18} /> |
| [useCompletion](/docs/reference/ai-sdk-ui/use-completion) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
| [useObject](/docs/reference/ai-sdk-ui/use-object) | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
| [useAssistant](/docs/reference/ai-sdk-ui/use-assistant) | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
| [useAssistant](/docs/reference/ai-sdk-ui/use-assistant) | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |

<Note>
[Contributions](https://github.com/vercel/ai/blob/main/CONTRIBUTING.md) are
Expand Down
1 change: 1 addition & 0 deletions examples/nuxt-openai/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NUXT_OPENAI_API_KEY=xxxxxxx
NUXT_ASSISTANT_ID=xxxxxxx
1 change: 1 addition & 0 deletions examples/nuxt-openai/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineNuxtConfig({

runtimeConfig: {
openaiApiKey: '',
assistantId: '',
},

compatibilityDate: '2024-07-05',
Expand Down
2 changes: 1 addition & 1 deletion examples/nuxt-openai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@vue/shared": "^3.4.31",
"ai": "latest",
"nuxt": "^3.12.3",
"openai": "4.52.3",
"openai": "4.47.1",
"tailwindcss": "^3.4.4",
"ufo": "^1.5.3",
"unctx": "^2.3.1",
Expand Down
83 changes: 83 additions & 0 deletions examples/nuxt-openai/pages/assistant/index.vue
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>
130 changes: 130 additions & 0 deletions examples/nuxt-openai/server/api/assistant.ts
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 },
),
);
}
},
);
});
});
7 changes: 7 additions & 0 deletions packages/core/vue/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
useChat as useChatVue,
useCompletion as useCompletionVue,
useAssistant as useAssistantVue,
} from '@ai-sdk/vue';

/**
Expand All @@ -13,6 +14,11 @@ export const useChat = useChatVue;
*/
export const useCompletion = useCompletionVue;

/**
* @deprecated Use `useAssistant` from `@ai-sdk/vue` instead.
*/
export const useAssistant = useAssistantVue;

/**
* @deprecated Use `@ai-sdk/vue` instead.
*/
Expand All @@ -21,4 +27,5 @@ export type {
Message,
UseChatOptions,
UseChatHelpers,
UseAssistantHelpers,
} from '@ai-sdk/vue';
5 changes: 3 additions & 2 deletions packages/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

[Vue.js](https://vuejs.org/) UI components for the [Vercel AI SDK](https://sdk.vercel.ai/docs):

- [`useChat`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat) hook
- [`useCompletion`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-completion) hook
- [`useChat`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat) composable
- [`useCompletion`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-completion) composable
- [`useAssistant`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-assistant) composable
1 change: 1 addition & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
}
},
"dependencies": {
"@ai-sdk/provider-utils": "0.0.14",
"@ai-sdk/ui-utils": "0.0.12",
"swrv": "1.0.4"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/vue/src/TestChatAssistantStreamComponent.vue
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 packages/vue/src/TestChatAssistantThreadChangeComponent.vue
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>
1 change: 1 addition & 0 deletions packages/vue/src/index.ts
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';
Loading

0 comments on commit dd0d854

Please sign in to comment.