diff --git a/examples/extract-people-names.ts b/examples/extract-people-names.ts index b52baab..24ac57f 100644 --- a/examples/extract-people-names.ts +++ b/examples/extract-people-names.ts @@ -1,15 +1,14 @@ import 'dotenv/config'; import { ChatModel } from '@dexaai/dexter'; -import { createAIExtractFunction } from '@dexaai/dexter/ai-function'; +import { createExtractFunction } from '@dexaai/dexter/extract'; import { z } from 'zod'; /** A function to extract people names from text. */ -const extractPeopleNamesRunner = createAIExtractFunction({ - chatModel: new ChatModel({ params: { model: 'gpt-4-1106-preview' } }), - systemMessage: `You use functions to extract people names from a message.`, - name: 'log_people_names', - description: `Use this to log the full names of people from a message. Don't include duplicate names.`, +const extractPeopleNamesRunner = createExtractFunction({ + chatModel: new ChatModel({ params: { model: 'gpt-4o-mini' } }), + systemMessage: `You extract the names of people from unstructured text.`, + name: 'people_names', schema: z.object({ names: z.array( z diff --git a/package.json b/package.json index 0a71a32..81f762e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ "./ai-function": { "types": "./dist/ai-function/index.d.ts", "import": "./dist/ai-function/index.js" + }, + "./extract": { + "types": "./dist/extract/index.d.ts", + "import": "./dist/extract/index.js" } }, "sideEffects": false, @@ -50,6 +54,7 @@ "jsonrepair": "^3.8.1", "ky": "^1.7.2", "openai-fetch": "3.3.1", + "openai-zod-to-json-schema": "^1.0.2", "p-map": "^7.0.2", "p-throttle": "^6.2.0", "parse-json": "^8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4aafbb..711752b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: openai-fetch: specifier: 3.3.1 version: 3.3.1 + openai-zod-to-json-schema: + specifier: ^1.0.2 + version: 1.0.2(zod@3.23.8) p-map: specifier: ^7.0.2 version: 7.0.2 @@ -2317,6 +2320,12 @@ packages: resolution: {integrity: sha512-/b7rPeKLgS+3C2dxQHPiWDj4wOcbL/SF5L2dxktmJyfFza/VK6Mr3+rIldgGxRNpqsa3oonEowafPNx5Tdq9dA==} engines: {node: '>=18'} + openai-zod-to-json-schema@1.0.2: + resolution: {integrity: sha512-ow+YY1aLbGx5kmgIi5FD3Aw/shL/jHAkFBIWbmW8+mzB4u/FtemBqnNbt17vr++eNopbihhJxw8/ubfeaqe6ug==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -5710,6 +5719,10 @@ snapshots: dependencies: ky: 1.7.2 + openai-zod-to-json-schema@1.0.2(zod@3.23.8): + dependencies: + zod: 3.23.8 + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/readme.md b/readme.md index 6280df5..e723f26 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,8 @@ Dexter is a powerful TypeScript library for working with Large Language Models ( - **Advanced AI Function Utilities**: Tools for creating and managing AI functions, including `createAIFunction`, `createAIExtractFunction`, and `createAIRunner`, with Zod integration for schema validation. +- **Structured Data Extraction**: Dexter supports OpenAI's structured output feature through the `createExtractFunction`, which uses the `response_format` parameter with a JSON schema derived from a Zod schema. + - **Flexible Caching and Tokenization**: Built-in caching system with custom cache support, and advanced tokenization based on `tiktoken` for accurate token management. - **Robust Observability and Control**: Customizable telemetry system, comprehensive event hooks, and specialized error handling for enhanced monitoring and control. @@ -103,10 +105,41 @@ async function main() { main().catch(console.error); ``` +### Extracting Structured Data + +```typescript +import { ChatModel } from '@dexaai/dexter'; +import { createExtractFunction } from '@dexaai/dexter/extract'; +import { z } from 'zod'; + +const extractPeopleNames = createExtractFunction({ + chatModel: new ChatModel({ params: { model: 'gpt-4o-mini' } }), + systemMessage: `You extract the names of people from unstructured text.`, + name: 'people_names', + schema: z.object({ + names: z.array( + z.string().describe( + `The name of a person from the message. Normalize the name by removing suffixes, prefixes, and fixing capitalization` + ) + ), + }), +}); + +async function main() { + const peopleNames = await extractPeopleNames( + `Dr. Andrew Huberman interviewed Tony Hawk, an idol of Andrew Hubermans.` + ); + console.log('peopleNames', peopleNames); +} + +main().catch(console.error); +``` + ### Using AI Functions ```typescript -import { ChatModel, createAIFunction, MsgUtil } from '@dexaai/dexter'; +import { ChatModel, MsgUtil } from '@dexaai/dexter'; +import { createAIFunction } from '@dexaai/dexter/ai-function'; import { z } from 'zod'; const getWeather = createAIFunction( @@ -271,6 +304,22 @@ new SparseVectorModel(args: SparseVectorModelArgs) - `extend(args?: PartialSparseVectorModelArgs): SparseVectorModel` - Creates a new instance of the model with modified configuration +### Extract Functions + +#### createExtractFunction + +Creates a function to extract structured data from text using OpenAI's structured output feature. +This is a better way to extract structured data than using the legacy `createAIExtractFunction` function. + +```typescript +createExtractFunction>(args: { + chatModel: Model.Chat.Model; + name: string; + schema: Schema; + systemMessage: string; +}): (input: string | Msg) => Promise> +``` + ### AI Functions #### createAIFunction @@ -375,6 +424,8 @@ Dexter uses the `openai-fetch` library to interact with the OpenAI API. This cli 4. **Streaming Support**: The `openai-fetch` client supports streaming responses, which Dexter utilizes for real-time output in chat models. +5. **Structured Output**: Dexter supports OpenAI's structured output feature through the `createExtractFunction`, which uses the `response_format` parameter with a JSON schema derived from a Zod schema. + ### Message Types and MsgUtil Dexter defines a set of message types (`Msg`) that closely align with the OpenAI API's message formats but with some enhancements for better type safety and easier handling. The `MsgUtil` class provides methods for creating, checking, and asserting these message types. diff --git a/src/ai-function/ai-extract-function.ts b/src/ai-function/ai-extract-function.ts index c38e122..6c57119 100644 --- a/src/ai-function/ai-extract-function.ts +++ b/src/ai-function/ai-extract-function.ts @@ -7,6 +7,7 @@ import { type ExtractFunction } from './types.js'; /** * Use OpenAI function calling to extract data from a message. + * @deprecated Use `createExtractFunction()` from `@dexaai/dexter/extract` instead. */ export function createAIExtractFunction>( { diff --git a/src/extract/index.ts b/src/extract/index.ts new file mode 100644 index 0000000..d3cf59b --- /dev/null +++ b/src/extract/index.ts @@ -0,0 +1,52 @@ +import { zodToJsonSchema } from 'openai-zod-to-json-schema'; +import { type z } from 'zod'; + +import { type Model, type Msg, MsgUtil } from '../model/index.js'; + +/** + * Extract data using OpenAI structured outputs. + * + * Always returns an object satisfying the provided Zod schema. + */ +export function createExtractFunction>(args: { + /** The ChatModel used to make API calls. */ + chatModel: Model.Chat.Model; + /** A descriptive name for the object to extract. */ + name: string; + /** The Zod schema for the data to extract. */ + schema: Schema; + /** Add a system message to the beginning of the messages array. */ + systemMessage: string; +}): (input: string | Msg) => Promise> { + const { chatModel, schema, systemMessage } = args; + + async function runExtract(input: string | Msg): Promise> { + const inputVal = typeof input === 'string' ? input : (input.content ?? ''); + const messages: Msg[] = [ + MsgUtil.system(systemMessage), + MsgUtil.user(inputVal), + ]; + + const { message } = await chatModel.run({ + messages, + response_format: { + type: 'json_schema', + json_schema: { + name: args.name, + strict: true, + schema: zodToJsonSchema(schema, { + $refStrategy: 'none', + openaiStrictMode: true, + }), + }, + }, + }); + + MsgUtil.assertAssistant(message); + + const json = JSON.parse(message.content); + return schema.parse(json); + } + + return runExtract; +}