Skip to content

Commit

Permalink
feat (ai/core): add experimental output setting to streamText (#4117)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgrammel authored Jan 3, 2025
1 parent 95a3dcc commit ae0485b
Show file tree
Hide file tree
Showing 15 changed files with 649 additions and 81 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-flowers-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'ai': patch
---

feat (ai/core): add experimental output setting to streamText
44 changes: 38 additions & 6 deletions content/docs/03-ai-sdk-core/10-generating-structured-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,24 @@ try {
}
```

## Structured output with `generateText`
## Structured outputs with `generateText` and `streamText`

You can generate structured data with `generateText` and `streamText` by using the `experimental_output` setting.

<Note>
Some models, e.g. those by OpenAI, support structured outputs and tool calling
at the same time. This is only possible with `generateText` and `streamText`.
</Note>

<Note type="warning">
Structured output with `generateText` is experimental and may change in the
future.
Structured output generation with `generateText` and `streamText` is
experimental and may change in the future.
</Note>

You can also generate structured data with `generateText` by using the `experimental_output` setting.
This enables you to use structured outputs together with tool calling (for models that support it - currently only OpenAI).
### `generateText`

```ts highlight="1,3,4"
```ts highlight="2,4-18"
// experimental_output is a structured object that matches the schema:
const { experimental_output } = await generateText({
// ...
experimental_output: Output.object({
Expand All @@ -239,6 +246,31 @@ const { experimental_output } = await generateText({
});
```

### `streamText`

```ts highlight="2,4-18"
// experimental_partialOutputStream contains generated partial objects:
const { experimental_partialOutputStream } = await streamText({
// ...
experimental_output: Output.object({
schema: z.object({
name: z.string(),
age: z.number().nullable().describe('Age of the person.'),
contact: z.object({
type: z.literal('email'),
value: z.string(),
}),
occupation: z.object({
type: z.literal('employed'),
company: z.string(),
position: z.string(),
}),
}),
}),
prompt: 'Generate an example person for testing.',
});
```

## More Examples

You can see `generateObject` and `streamObject` in action using various frameworks in the following examples:
Expand Down
6 changes: 6 additions & 0 deletions content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,12 @@ To see `generateText` in action, check out [these examples](#examples).
description:
'Optional metadata from the provider. The outer key is the provider name. The inner values are the metadata. Details depend on the provider.',
},
{
name: 'experimental_output',
type: 'Output',
isOptional: true,
description: 'Experimental setting for generating structured outputs.',
},
{
name: 'steps',
type: 'Array<StepResult>',
Expand Down
41 changes: 41 additions & 0 deletions content/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,41 @@ To see `streamText` in action, check out [these examples](#examples).
},
],
},
{
name: 'experimental_output',
type: 'Output',
isOptional: true,
description: 'Experimental setting for generating structured outputs.',
properties: [
{
type: 'Output',
parameters: [
{
name: 'Output.text()',
type: 'Output',
description: 'Forward text output.',
},
{
name: 'Output.object()',
type: 'Output',
description: 'Generate a JSON object of type OBJECT.',
properties: [
{
type: 'Options',
parameters: [
{
name: 'schema',
type: 'Schema<OBJECT>',
description: 'The schema of the JSON object to generate.',
},
],
},
],
},
],
},
],
},
{
name: 'onStepFinish',
type: '(result: onStepFinishResult) => Promise<void> | void',
Expand Down Expand Up @@ -1578,6 +1613,12 @@ To see `streamText` in action, check out [these examples](#examples).
},
],
},
{
name: 'experimental_partialOutputStream',
type: 'AsyncIterableStream<PARTIAL_OUTPUT>',
description:
'A stream of partial outputs. It uses the `experimental_output` specification.',
},
{
name: 'pipeDataStreamToResponse',
type: '(response: ServerResponse, options: PipeDataStreamToResponseOptions } => void',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: AI_NoOutputSpecifiedError
description: Learn how to fix AI_NoOutputSpecifiedError
---

# AI_NoOutputSpecifiedError

This error occurs when no output format was specified for the AI response, and output-related methods are called.

## Properties

- `message`: The error message (defaults to 'No output specified.')

## Checking for this Error

You can check if an error is an instance of `AI_NoOutputSpecifiedError` using:

```typescript
import { NoOutputSpecifiedError } from 'ai';

if (NoOutputSpecifiedError.isInstance(error)) {
// Handle the error
}
```
1 change: 1 addition & 0 deletions content/docs/07-reference/05-ai-sdk-errors/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ collapsed: true
- [AI_MessageConversionError](/docs/reference/ai-sdk-errors/ai-message-conversion-error)
- [AI_NoContentGeneratedError](/docs/reference/ai-sdk-errors/ai-no-content-generated-error)
- [AI_NoObjectGeneratedError](/docs/reference/ai-sdk-errors/ai-no-object-generated-error)
- [AI_NoOutputSpecifiedError](/docs/reference/ai-sdk-errors/ai-no-output-specified-error)
- [AI_NoSuchModelError](/docs/reference/ai-sdk-errors/ai-no-such-model-error)
- [AI_NoSuchProviderError](/docs/reference/ai-sdk-errors/ai-no-such-provider-error)
- [AI_NoSuchToolError](/docs/reference/ai-sdk-errors/ai-no-such-tool-error)
Expand Down
45 changes: 45 additions & 0 deletions examples/ai-core/src/stream-text/openai-output-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { openai } from '@ai-sdk/openai';
import { Output, streamText, tool } from 'ai';
import 'dotenv/config';
import { z } from 'zod';

async function main() {
const { experimental_partialOutputStream: partialOutputStream } = streamText({
model: openai('gpt-4o-mini', { structuredOutputs: true }),
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: z.object({
location: z.string().describe('The location to get the weather for'),
}),
// location below is inferred to be a string:
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
experimental_output: Output.object({
schema: z.object({
elements: z.array(
z.object({
location: z.string(),
temperature: z.number(),
touristAttraction: z.string(),
}),
),
}),
}),
maxSteps: 2,
prompt:
'What is the weather and the main tourist attraction in San Francisco, London Paris, and Berlin?',
});

// [{ location: 'San Francisco', temperature: 81 }, ...]
for await (const partialOutput of partialOutputStream) {
console.clear();
console.log(partialOutput);
}
}

main().catch(console.error);
2 changes: 1 addition & 1 deletion packages/ai/core/generate-text/generate-text-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface GenerateTextResult<
readonly text: string;

/**
The generated output.
The generated structured output. It uses the `experimental_output` specification.
*/
readonly experimental_output: OUTPUT;

Expand Down
6 changes: 5 additions & 1 deletion packages/ai/core/generate-text/generate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ A result object that contains the generated text, the results of the tool calls,
export async function generateText<
TOOLS extends Record<string, CoreTool>,
OUTPUT = never,
OUTPUT_PARTIAL = never,
>({
model,
tools,
Expand Down Expand Up @@ -157,7 +158,10 @@ changing the tool call and result types in the result.
*/
experimental_activeTools?: Array<keyof TOOLS>;

experimental_output?: Output<OUTPUT>;
/**
Optional specification for parsing structured outputs from the LLM response.
*/
experimental_output?: Output<OUTPUT, OUTPUT_PARTIAL>;

/**
A function that attempts to repair a tool call that failed to parse.
Expand Down
49 changes: 45 additions & 4 deletions packages/ai/core/generate-text/output.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { safeParseJSON, safeValidateTypes } from '@ai-sdk/provider-utils';
import { asSchema, Schema } from '@ai-sdk/ui-utils';
import {
asSchema,
DeepPartial,
parsePartialJson,
Schema,
} from '@ai-sdk/ui-utils';
import { z } from 'zod';
import { NoObjectGeneratedError } from '../../errors';
import { injectJsonInstruction } from '../generate-object/inject-json-instruction';
Expand All @@ -10,15 +15,19 @@ import {
import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata';
import { LanguageModelUsage } from '../types/usage';

export interface Output<OUTPUT> {
export interface Output<OUTPUT, PARTIAL> {
readonly type: 'object' | 'text';
injectIntoSystemPrompt(options: {
system: string | undefined;
model: LanguageModel;
}): string | undefined;

responseFormat: (options: {
model: LanguageModel;
}) => LanguageModelV1CallOptions['responseFormat'];

parsePartial(options: { text: string }): { partial: PARTIAL } | undefined;

parseOutput(
options: { text: string },
context: {
Expand All @@ -28,12 +37,19 @@ export interface Output<OUTPUT> {
): OUTPUT;
}

export const text = (): Output<string> => ({
export const text = (): Output<string, string> => ({
type: 'text',

responseFormat: () => ({ type: 'text' }),

injectIntoSystemPrompt({ system }: { system: string | undefined }) {
return system;
},

parsePartial({ text }: { text: string }) {
return { partial: text };
},

parseOutput({ text }: { text: string }) {
return text;
},
Expand All @@ -43,15 +59,17 @@ export const object = <OUTPUT>({
schema: inputSchema,
}: {
schema: z.Schema<OUTPUT, z.ZodTypeDef, any> | Schema<OUTPUT>;
}): Output<OUTPUT> => {
}): Output<OUTPUT, DeepPartial<OUTPUT>> => {
const schema = asSchema(inputSchema);

return {
type: 'object',

responseFormat: ({ model }) => ({
type: 'json',
schema: model.supportsStructuredOutputs ? schema.jsonSchema : undefined,
}),

injectIntoSystemPrompt({ system, model }) {
// when the model supports structured outputs,
// we can use the system prompt as is:
Expand All @@ -62,6 +80,29 @@ export const object = <OUTPUT>({
schema: schema.jsonSchema,
});
},

parsePartial({ text }: { text: string }) {
const result = parsePartialJson(text);

switch (result.state) {
case 'failed-parse':
case 'undefined-input':
return undefined;

case 'repaired-parse':
case 'successful-parse':
return {
// Note: currently no validation of partial results:
partial: result.value as DeepPartial<OUTPUT>,
};

default: {
const _exhaustiveCheck: never = result.state;
throw new Error(`Unsupported parse state: ${_exhaustiveCheck}`);
}
}
},

parseOutput(
{ text }: { text: string },
context: {
Expand Down
10 changes: 9 additions & 1 deletion packages/ai/core/generate-text/stream-text-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import { ToolResultUnion } from './tool-result';
/**
A result object for accessing different stream types and additional information.
*/
export interface StreamTextResult<TOOLS extends Record<string, CoreTool>> {
export interface StreamTextResult<
TOOLS extends Record<string, CoreTool>,
PARTIAL_OUTPUT,
> {
/**
Warnings from the model provider (e.g. unsupported settings) for the first step.
*/
Expand Down Expand Up @@ -113,6 +116,11 @@ need to be added separately.
*/
readonly fullStream: AsyncIterableStream<TextStreamPart<TOOLS>>;

/**
A stream of partial outputs. It uses the `experimental_output` specification.
*/
readonly experimental_partialOutputStream: AsyncIterableStream<PARTIAL_OUTPUT>;

/**
Converts the result to a data stream.
Expand Down
Loading

0 comments on commit ae0485b

Please sign in to comment.