Skip to content

Generative UI ​

Модель отвечает не только текстом, но и компонентами. Когда она вызывает инструмент, <MessageParts> рендерит компонент, который вы сопоставили этому инструменту. Компонент получает аргументы по мере их стриминга, а затем результат. Он также может попросить пользователя принять решение, прежде чем диалог продолжится.

Демо использует заскриптованную модель, поэтому запускается мгновенно и без сети. Код ниже такой же, как с настоящим провайдером.

1. Объявите инструменты ​

ts
import { defineTool } from 'vue-llm'

export const getWeather = defineTool<{ city: string; days: number }, Weather>({
  name: 'get_weather',
  description: 'Current weather and forecast for a city',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string' }, days: { type: 'number' } },
    required: ['city'],
  },
  execute: ({ city, days }, { signal }) =>
    fetch(`/api/weather?city=${city}&days=${days}`, { signal }).then(r => r.json()),
})

// Без execute: вызов ждёт, пока пользователь ответит в интерфейсе
export const confirmBooking = defineTool<Booking, Booking & { confirmed: boolean }>({
  name: 'confirm_booking',
  description: 'Ask the user to confirm a restaurant booking',
  parameters: { /* … */ },
})

2. Напишите компонент для каждого инструмента ​

Компоненты инструментов получают ToolComponentProps<Args, Result>:

ПропОписание
argsАргументы, неполные, пока state равен 'streaming'
statestreaming → pending → running → result / error
resultЗначение, которое вернул инструмент
errorТекст ошибки, если инструмент её выбросил
addResult(result)Завершает вызов из интерфейса
toolCallId, toolNameИдентификаторы
vue
<!-- WeatherCard.vue -->
<script setup lang="ts">
import type { ToolComponentProps } from 'vue-llm'

const props = defineProps<ToolComponentProps<{ city: string; days: number }, Weather>>()
</script>

<template>
  <div class="card">
    <h3>{{ props.args?.city ?? '…' }}</h3>
    <Forecast v-if="props.result" :weather="props.result" />
    <Skeleton v-else />
  </div>
</template>
vue
<!-- BookingCard.vue: human in the loop -->
<script setup lang="ts">
import type { ToolComponentProps } from 'vue-llm'

const props = defineProps<ToolComponentProps<Booking, Booking & { confirmed: boolean }>>()
const decide = (confirmed: boolean) => props.addResult({ ...props.args as Booking, confirmed })
</script>

<template>
  <div class="card">
    {{ props.args?.restaurant }} at {{ props.args?.time }}
    <template v-if="props.state === 'pending'">
      <button @click="decide(true)">Confirm</button>
      <button @click="decide(false)">Cancel</button>
    </template>
  </div>
</template>

3. Отрендерите сообщения ​

vue
<script setup lang="ts">
import { useChat } from 'vue-llm'
import { MessageParts } from 'vue-llm/components'
import 'vue-llm/style.css'

const { messages, input, handleSubmit, addToolResult } = useChat({
  provider: ai,
  model: 'gpt-4o',
  tools: [getWeather, confirmBooking],
})

const components = { get_weather: WeatherCard, confirm_booking: BookingCard }
</script>

<template>
  <MessageParts
    v-for="message in messages"
    :key="message.id"
    :message="message"
    :components="components"
    @tool-result="addToolResult"
  />
</template>

Вот и вся настройка. get_weather выполняется сам, и модель продолжает, как только приходит результат. confirm_booking ставит чат на паузу в состоянии pending. Когда пользователь нажимает Confirm, addToolResult записывает ответ, и чат продолжается. См. Human in the loop.

Без MessageParts ​

MessageParts использовать необязательно. Вызовы инструментов — обычные части сообщения, так что их можно рендерить самостоятельно:

vue
<template v-for="part in message.parts">
  <WeatherCard
    v-if="part.type === 'tool-call' && part.toolName === 'get_weather'"
    v-bind="part"
    :add-result="(result) => addToolResult({ toolCallId: part.toolCallId, result })"
  />
</template>

Это работает и на Vue 2.7, где vue-llm/components недоступен.

Стриминг аргументов ​

Аргументы приходят токен за токеном. vue-llm разбирает неполный JSON на каждом чанке функцией parsePartialJSON (она тоже экспортируется), поэтому args.city появляется ещё до того, как модель допишет вызов. Поля добавляются по мере готовности. Строки растут на месте, а незаконченные ключи отбрасываются, пока не будут дописаны.

ts
import { parsePartialJSON } from 'vue-llm'

parsePartialJSON('{"city":"Os')           // { city: 'Os' }
parsePartialJSON('{"items":[{"n":"a"},{') // { items: [{ n: 'a' }, {}] }

Инструменты на сервере ​

Инструментам, которым нужны секреты, место на сервере (инструменты createLLMHandler). Их вызовы всё равно стримятся на клиент как части tool-call с state: 'result', поэтому их рендерят те же компоненты.

Распространяется по лицензии MIT.