Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/deploy-chat-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ jobs:
needs: [deploy-chat-production, deploy-chat-lambda]
runs-on: ubuntu-latest
steps:
- run: curl -m 10 --retry 5 ${{ secrets.CHAT_DEPLOY_PRODUCTION_PING_URL }}
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_PRODUCTION_PING_URL }}"

ping-failure:
needs: [deploy-chat-production, deploy-chat-lambda]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- run: curl -m 10 --retry 5 ${{ secrets.CHAT_DEPLOY_PRODUCTION_PING_URL }}/fail
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_PRODUCTION_PING_URL }}/fail"
4 changes: 2 additions & 2 deletions .github/workflows/deploy-chat-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ jobs:
needs: [deploy-chat-staging, deploy-chat-lambda]
runs-on: ubuntu-latest
steps:
- run: curl -m 10 --retry 5 ${{ secrets.CHAT_DEPLOY_STAGING_PING_URL }}
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_STAGING_PING_URL }}"

ping-failure:
needs: [deploy-chat-staging, deploy-chat-lambda]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- run: curl -m 10 --retry 5 ${{ secrets.CHAT_DEPLOY_STAGING_PING_URL }}/fail
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_STAGING_PING_URL }}/fail"
2 changes: 1 addition & 1 deletion .github/workflows/deploy-integrations-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
uses: ./.github/actions/deploy-integrations
with:
environment: 'production'
extra_filter: "-F '!docusign' -F '!zendesk' -F '!chat' -F '!grafana'"
extra_filter: "-F '!docusign' -F '!chat' -F '!grafana'"
force: ${{ github.event.inputs.force == 'true' }}
sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prod-master-version-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
REPO: ${{ github.repository }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WORKFLOW_SEAGULL_WEBHOOK_URL }}
run: |
SKIP_INTEGRATIONS=("chat" "docusign" "zendesk" "zendesk-messaging-hitl")
SKIP_INTEGRATIONS=("chat" "docusign" "zendesk-messaging-hitl")

integrations=$(find integrations -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n1 basename | sort -u)
should_fail=0
Expand Down
6 changes: 3 additions & 3 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

## AI
/packages/cognitive @botpress/dragon
/packages/llmz @slvnperron @botpress/swordfish
/packages/vai @slvnperron @botpress/swordfish
/packages/zai @slvnperron @botpress/swordfish
/packages/llmz @slvnperron @botpress/orca @botpress/swordfish
/packages/vai @slvnperron @botpress/orca @botpress/swordfish
/packages/zai @slvnperron @botpress/orca @botpress/swordfish

# Bots
/bots @botpress/shell
Expand Down
2 changes: 1 addition & 1 deletion integrations/monday/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default new IntegrationDefinition({
name: 'monday',
title: 'Monday',
description: 'Manage items in Monday boards.',
version: '1.1.3',
version: '1.1.4',
readme: 'hub.md',
icon: 'icon.svg',
states: {
Expand Down
2 changes: 1 addition & 1 deletion integrations/monday/linkTemplate.vrl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
webhookId = to_string!(.webhookId)
webhookUrl = to_string!(.webhookUrl)

"{{ webhookUrl }}/oauth/wizard/oauth-redirect?state={{ webhookId }}"
"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}"
5 changes: 3 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"scripts": {
"check:type": "tsc --noEmit",
"build:type": "tsup --tsconfig tsconfig.build.json ./src/index.ts --dts-resolve --dts-only",
"build:type": "rollup -c rollup.dts.config.mjs",
"build:browser": "ts-node -T ./build.ts --browser",
"build:node": "ts-node -T ./build.ts --node",
"build:bundle": "ts-node -T ./build.ts --bundle",
Expand All @@ -34,7 +34,8 @@
"@types/qs": "^6.9.7",
"esbuild": "^0.25.10",
"lodash": "^4.17.21",
"tsup": "^8.0.2"
"rollup": "^4.60.4",
"rollup-plugin-dts": "^6.4.1"
},
"engines": {
"node": ">=18.0.0"
Expand Down
15 changes: 15 additions & 0 deletions packages/client/rollup.dts.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import dts from 'rollup-plugin-dts'

export default {
input: './src/index.ts',
external: [/node_modules/],
output: {
file: './dist/index.d.ts',
},
plugins: [
dts({
tsconfig: './tsconfig.build.json',
respectExternal: true,
}),
],
}
38 changes: 38 additions & 0 deletions packages/llmz/examples/21_chat_tool_components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Yielding Components from Tool Handlers

This demo shows how tool handlers can directly yield UI components back to the chat using **async generator functions** (`async function*`). Unlike example 10 where the LLM yields components after calling a tool, here the tool handler itself controls what to display and when.

The `purchase_ticket` tool handler is an async generator that yields progress updates and the final ticket — the LLM doesn't need to know about components at all.

### Handler pattern

```tsx
async *handler({ from, date, to }) {
// Yield progress components step by step
yield ProgressComponent.render({ message: 'Checking flight availability...', step: 1, total: 3 })
yield ProgressComponent.render({ message: 'Calculating best price...', step: 2, total: 3 })

// Yield the final ticket component
yield PlaneTicketComponent.render({
from, to, date,
price: 299.99,
ticketNumber: 'TICKET-345633',
})

// Return the tool result for the LLM
return { price: 299.99, ticketNumber: 'TICKET-345633', confirmation: '...' }
}
```

### Contrast with example 10

| | Example 10 | Example 21 |
| ------------------------------ | ----------------------------------- | ------------------------------- |
| Who yields components | LLM-generated JSX code | Tool handler itself |
| Tool handler type | `async (input) => output` | `async function* (input)` |
| LLM needs to know components | Yes — included as component aliases | No — tool handles it internally |
| Progress updates mid-execution | Not possible | `yield` between steps |

## 🎥 Demo

![Demo](./demo.svg)
1 change: 1 addition & 0 deletions packages/llmz/examples/21_chat_tool_components/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
169 changes: 169 additions & 0 deletions packages/llmz/examples/21_chat_tool_components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Example 21: Yielding Components from Tool Handlers
*
* This example demonstrates how tool handlers can directly yield UI components
* back to the chat interface using async generator functions. Unlike example 10
* where the LLM yielids components, here the tool handler itself controls what
* to display and when.
*
* Key concepts:
* - Tool handlers as async generators (async function*)
* - Yielding components mid-execution for progress updates
* - Component.render() for constructing RenderedComponent objects
* - Tool-side component rendering vs LLM-side component yielding
*
* Contrast with example 10: the LLM no longer needs to know about components —
* the tool handler takes care of rendering directly.
*/

import { Client } from '@botpress/client'
import { z } from '@bpinternal/zui'

import chalk from 'chalk'
import { Component, execute, Tool } from 'llmz'
import { box } from '../utils/box'
import { CLIChat } from '../utils/cli-chat'

// Initialize Botpress client
const client = new Client({
botId: process.env.BOTPRESS_BOT_ID!,
token: process.env.BOTPRESS_TOKEN!,
})

// Define a custom UI component for displaying plane tickets
const PlaneTicketComponent = new Component({
name: 'PlaneTicket',
description: 'A component to display a plane ticket',
type: 'leaf',
leaf: {
props: z.object({
ticketNumber: z.string().describe('The unique ticket number for the plane ticket'),
from: z.string().describe('The departure city'),
to: z.string().describe('The destination city'),
date: z.string().describe('The date of the flight (in YYYY-MM-DD format)'),
price: z.number().optional().describe('The price of the ticket'),
}),
},
examples: [
{
name: 'PlaneTicket',
description: 'A simple plane ticket example',
code: '<PlaneTicket from="New York" to="Los Angeles" date="2023-10-01" price={299.99} ticketNumber="ABC-0000000" />',
},
],
})

// Define a progress component for status updates
const ProgressComponent = new Component({
name: 'Progress',
description: 'Displays a progress update message',
type: 'leaf',
leaf: {
props: z.object({
message: z.string().describe('The progress message'),
step: z.number().describe('Current step number'),
total: z.number().describe('Total number of steps'),
}),
},
examples: [
{
name: 'Basic Progress',
description: 'Show a progress bar with step and total',
code: '<Progress message="Checking availability..." step={1} total={3} />',
},
{
name: 'Mid-progress',
description: 'Midway progress update',
code: '<Progress message="Calculating price..." step={2} total={3} />',
},
],
})

// Tool for purchasing tickets — uses async generator to yield components
const purchaseTicket = new Tool({
name: 'purchase_ticket',
description: 'Purchase a plane ticket, showing progress updates along the way',
input: z.object({
from: z.string().describe('The departure city'),
to: z.string().describe('The destination city'),
date: z.string().describe('The date of the flight (in YYYY-MM-DD format)'),
}),
output: z.object({
price: z.number().describe('The price of the purchased ticket in USD'),
ticketNumber: z.string().describe('The unique ticket number for the purchased ticket'),
confirmation: z.string().describe('Confirmation message for the ticket purchase'),
}),
// Async generator handler: yields components progress, returns final result
async *handler({ from, date, to }) {
// Step 1: Show progress
yield ProgressComponent.render({ message: 'Checking flight availability...', step: 1, total: 3 })
await new Promise((resolve) => setTimeout(resolve, 500))

// Step 2: Show more progress
yield ProgressComponent.render({ message: 'Calculating best price...', step: 2, total: 3 })
await new Promise((resolve) => setTimeout(resolve, 500))

// Step 3: Yiels the ticket component itself
yield PlaneTicketComponent.render({
from,
to,
date,
price: 299.99,
ticketNumber: 'TICKET-345633',
})

// Final return — the tool result for the LLM
return {
price: 299.99,
ticketNumber: 'TICKET-345633',
confirmation: `Ticket from ${from} to ${to} on ${date} purchased successfully!`,
}
},
})

const chat = new CLIChat()

chat.transcript.push({
role: 'user',
content: 'I want to purchase a plane ticket from New York to Los Angeles on 2025-10-01.',
})

// Register component renderers — same as example 10
chat.registerComponent(PlaneTicketComponent, async (message) => {
const { ticketNumber, from, to, date, price } = message.props

const ticket = box([
chalk.white.bold(' ✈️ FLIGHT TICKET'),
`${chalk.yellow.bold('Ticket Number:')} ${chalk.white(ticketNumber)}`,
'',
`${chalk.green.bold('From:')} ${chalk.white(from)}`,
`${chalk.red.bold('To:')} ${chalk.white(to)}`,
'',
`${chalk.magenta.bold('Date:')} ${chalk.white(date)}`,
`${chalk.cyan.bold('Price:')} ${chalk.white(`$${price?.toFixed(2) || 'N/A'}`)}`,
'',
chalk.gray(' Have a safe flight! 🛫'),
])

console.log(ticket)
})

chat.registerComponent(ProgressComponent, async (message) => {
const { step, total, message: msg } = message.props
const bar = '█'.repeat(step) + '░'.repeat(Math.max(0, total - step))
console.log(chalk.blue(`[${bar}]`) + ' ' + chalk.white(msg))
})

// Execute the travel agent workflow
// Note: the LLM no longer needs to know about PlaneTicketComponent —
// the tool handler handles rendering directly
const result = await execute({
instructions:
'You are a travel agent. Help the user purchase a plane ticket. The ticket will be displayed automatically when the purchase completes.',
tools: [purchaseTicket],
chat,
client,
})

console.log("Here's the code generated by the LLMz:")
console.log(result.iteration?.code)
2 changes: 1 addition & 1 deletion packages/llmz/examples/record-demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CAST_PATH="${FOLDER}/demo.cast"
SVG_PATH="${FOLDER}/demo.svg"

echo "🎬 Recording demo for ${FOLDER}..."
asciinema rec "$CAST_PATH" --command "pnpm start ${FOLDER}" --overwrite
asciinema rec "$CAST_PATH" --command "pnpm start ${FOLDER}" --overwrite -f asciicast-v2

echo "🛠 Appending trailing empty frame..."
node -e "
Expand Down
4 changes: 2 additions & 2 deletions packages/llmz/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "llmz",
"type": "module",
"description": "LLMz - An LLM-native Typescript VM built on top of Zui",
"version": "0.0.78",
"version": "0.0.79",
"types": "./dist/index.d.ts",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
Expand Down Expand Up @@ -74,7 +74,7 @@
"@botpress/client": "1.46.0",
"@botpress/cognitive": "0.5.5",
"@bpinternal/thicktoken": "^2.0.0",
"@bpinternal/zui": "^2.1.1"
"@bpinternal/zui": "^2.2.1"
},
"dependenciesMeta": {
"@bpinternal/zui": {
Expand Down
16 changes: 9 additions & 7 deletions packages/llmz/src/component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from '@bpinternal/zui'
import { isAnyJsxComponent, isJsxComponent } from './jsx.js'
import { createJsxComponent, isAnyJsxComponent, isJsxComponent, JsxComponent } from './jsx.js'

export type ExampleUsage = {
name: string
Expand Down Expand Up @@ -206,12 +206,7 @@ type ExtractComponentProps<T extends ComponentDefinition> =
: never

// Rendered component type
export type RenderedComponent<TProps = Record<string, any>> = {
__jsx: true
type: string
children: any[]
props: TProps
}
export type RenderedComponent<TProps extends {} = {}> = JsxComponent<string, TProps>

// Component Class that infers props from component definition
export class Component<T extends ComponentDefinition = ComponentDefinition> {
Expand All @@ -222,6 +217,13 @@ export class Component<T extends ComponentDefinition = ComponentDefinition> {
assertValidComponent(definition)
this.definition = definition
}

public render<TChildren extends any = any>(
props: Component['propsType'],
children: Array<TChildren> = []
): RenderedComponent {
return createJsxComponent({ type: this.definition.name, props, children })
}
}

// Type guard function that infers props from component
Expand Down
Loading
Loading