Skip to content

Commit

Permalink
feat: Option to include file search results in assistants API (#543)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmigloz authored Aug 28, 2024
1 parent 1099725 commit e916ad3
Show file tree
Hide file tree
Showing 18 changed files with 2,171 additions and 129 deletions.
134 changes: 100 additions & 34 deletions packages/openai_dart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ Unofficial Dart client for [OpenAI](https://platform.openai.com/docs/api-referen

**Supported endpoints:**

- Chat (with tools and streaming support)
- Chat (with structured outputs, tools and streaming support)
- Completions (legacy)
- Embeddings
- Fine-tuning
- Batch
- Images
- Models
- Moderations
- Assistants v2 (with tools and streaming support) `beta`
- Assistants v2 (with structured outputs, tools and streaming support) `beta`
* Threads
* Messages
* Runs
Expand Down Expand Up @@ -97,14 +97,14 @@ final client = OpenAIClient(

Given a list of messages comprising a conversation, the model will return a response.

Related guide: [Chat Completions](https://platform.openai.com/docs/guides/text-generation)
Related guide: [Chat Completions](https://platform.openai.com/docs/guides/chat-completions)

**Create chat completion:**

```dart
final res = await client.createChatCompletion(
request: CreateChatCompletionRequest(
model: ChatCompletionModel.modelId('gpt-4'),
model: ChatCompletionModel.modelId('gpt-4o'),
messages: [
ChatCompletionMessage.system(
content: 'You are a helpful assistant.',
Expand All @@ -121,28 +121,28 @@ print(res.choices.first.message.content);
```

`ChatCompletionModel` is a sealed class that offers two ways to specify the model:
- `ChatCompletionModel.modelId('model-id')`: the model ID as string (e.g. `'gpt-4'` or your fine-tuned model ID).
- `ChatCompletionModel.model(ChatCompletionModels.gpt4)`: a value from `ChatCompletionModels` enum which lists all of the available models.
- `ChatCompletionModel.modelId('model-id')`: the model ID as string (e.g. `'gpt-4o'` or your fine-tuned model ID).
- `ChatCompletionModel.model(ChatCompletionModels.gpt4o)`: a value from `ChatCompletionModels` enum which lists all of the available models.

`ChatCompletionMessage` is a sealed class that supports the following message types:
- `ChatCompletionMessage.system()`: a system message.
- `ChatCompletionMessage.user()`: a user message.
- `ChatCompletionMessage.assistant()`: an assistant message.
- `ChatCompletionMessage.tool()`: a tool message.
- `ChatCompletionMessage.function()`: a function message.
- `ChatCompletionMessage.function()`: a function message (deprecated in favor of tools).

`ChatCompletionMessage.user()` takes a `ChatCompletionUserMessageContent` object that supports the following content types:
- `ChatCompletionUserMessageContent.string('content')`: string content.
- `ChatCompletionUserMessageContent.parts([...])`: multi-modal content (check the 'Multi-modal prompt' section below).
* `ChatCompletionMessageContentPart.text('content')`: text content.
* `ChatCompletionMessageContentPart.image(imageUrl: ...)`: image content.
* `ChatCompletionMessageContentPart.image(...)`: image content (URL or base64-encoded image).

**Stream chat completion:**

```dart
final stream = client.createChatCompletionStream(
request: CreateChatCompletionRequest(
model: ChatCompletionModel.modelId('gpt-4-turbo'),
model: ChatCompletionModel.modelId('gpt-4o'),
messages: [
ChatCompletionMessage.system(
content:
Expand All @@ -167,6 +167,8 @@ await for (final res in stream) {

**Multi-modal prompt:** ([docs](https://platform.openai.com/docs/guides/vision))

You can either provide the image URL:

```dart
final res = await client.createChatCompletion(
request: CreateChatCompletionRequest(
Expand Down Expand Up @@ -198,37 +200,31 @@ print(res.choices.first.message.content);
// The fruit in the image is an apple.
```

**JSON mode:** ([docs](https://platform.openai.com/docs/guides/structured-outputs/json-mode))

Or provide the base64-encoded image:
```dart
final res = await client.createChatCompletion(
request: CreateChatCompletionRequest(
model: ChatCompletionModel.model(
ChatCompletionModels.gpt41106Preview,
),
messages: [
ChatCompletionMessage.system(
content:
'You are a helpful assistant. That extracts names from text '
'and returns them in a JSON array.',
//...
ChatCompletionMessage.user(
content: ChatCompletionUserMessageContent.parts(
[
ChatCompletionMessageContentPart.text(
text: 'What fruit is this?',
),
ChatCompletionMessage.user(
content: ChatCompletionUserMessageContent.string(
'John, Mary, and Peter.',
ChatCompletionMessageContentPart.image(
imageUrl: ChatCompletionMessageImageUrl(
url: '/9j/4AAQSkZJRgABAQAAAQABAAD/2wB...P3s/XHQ8cE/nmiupbL0+fz/r/MjnSbsr69/Rdu1j//2Q==',
detail: ChatCompletionMessageImageDetail.high,
),
),
],
temperature: 0,
responseFormat: ChatCompletionResponseFormat(
type: ChatCompletionResponseFormatType.jsonObject,
),
),
);
// { "names": ["John", "Mary", "Peter"] }
),
//...
```

**Structured output: ([docs](https://platform.openai.com/docs/guides/structured-outputs))**

Structured Outputs is a feature that ensures the model will always generate responses that adhere to your supplied JSON Schema.

```dart
final res = await client.createChatCompletion(
request: CreateChatCompletionRequest(
Expand All @@ -237,8 +233,7 @@ final res = await client.createChatCompletion(
),
messages: [
ChatCompletionMessage.system(
content:
'You are a helpful assistant. That extracts names from text.',
content: 'You are a helpful assistant. That extracts names from text.',
),
ChatCompletionMessage.user(
content: ChatCompletionUserMessageContent.string(
Expand Down Expand Up @@ -272,8 +267,41 @@ final res = await client.createChatCompletion(
// {"names":["John","Mary","Peter"]}
```

**JSON mode:** ([docs](https://platform.openai.com/docs/guides/structured-outputs/json-mode))

> JSON mode is a more basic version of the Structured Outputs feature. While JSON mode ensures that model output is valid JSON, Structured Outputs reliably matches the model's output to the schema you specify. It us recommended to use Structured Outputs if it is supported for your use case.
```dart
final res = await client.createChatCompletion(
request: CreateChatCompletionRequest(
model: ChatCompletionModel.model(
ChatCompletionModels.gpt41106Preview,
),
messages: [
ChatCompletionMessage.system(
content:
'You are a helpful assistant. That extracts names from text '
'and returns them in a JSON array.',
),
ChatCompletionMessage.user(
content: ChatCompletionUserMessageContent.string(
'John, Mary, and Peter.',
),
),
],
temperature: 0,
responseFormat: ChatCompletionResponseFormat(
type: ChatCompletionResponseFormatType.jsonObject,
),
),
);
// { "names": ["John", "Mary", "Peter"] }
```

**Tools:** ([docs](https://platform.openai.com/docs/guides/function-calling))

Tool calling allows you to connect models to external tools and systems.

```dart
const function = FunctionObject(
name: 'get_current_weather',
Expand Down Expand Up @@ -301,7 +329,7 @@ const tool = ChatCompletionTool(
final res1 = await client.createChatCompletion(
request: CreateChatCompletionRequest(
model: const ChatCompletionModel.model(
model: ChatCompletionModel.model(
ChatCompletionModels.gpt4oMini,
),
messages: [
Expand Down Expand Up @@ -353,6 +381,8 @@ final answer = res2.choices.first.message.content;
// The weather in Boston right now is sunny with a temperature of 22°C
```

You can enable Structured Outputs for your tools by setting `strict: true` in your `FunctionObject` definition. Structured Outputs ensures that the arguments generated by the model for a tool call exactly match the JSON Schema you provided in the tool definition.

**Function calling:** (deprecated in favor of tools)

```dart
Expand Down Expand Up @@ -813,7 +843,7 @@ final res = await client.createThreadMessage(
),
MessageContent.imageUrl(
imageUrl: MessageContentImageUrl(
url: 'https://example.com/image.jpg',
url: 'https://example.com/image.jpg', // or base64-encoded image
),
),
]),
Expand Down Expand Up @@ -867,6 +897,42 @@ final res = await client.createThreadRun(
);
```

You can also use Structured Outputs to ensure that the model-generated responses adhere to a specific JSON schema:

```dart
final res = await client.createThreadRun(
threadId: threadId,
request: CreateRunRequest(
assistantId: assistantId,
instructions: 'You are a helpful assistant that extracts names from text.',
model: CreateRunRequestModel.modelId('gpt-4o'),
responseFormat: CreateRunRequestResponseFormat.responseFormat(
ResponseFormat.jsonSchema(
jsonSchema: JsonSchemaObject(
name: 'Names',
description: 'A list of names',
strict: true,
schema: {
'type': 'object',
'properties': {
'names': {
'type': 'array',
'items': {
'type': 'string',
},
},
},
'additionalProperties': false,
'required': ['names'],
},
),
)
)
),
);
```

**Create run: (streaming)**

```dart
Expand Down
2 changes: 1 addition & 1 deletion packages/openai_dart/lib/openai_dart.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// Dart client for the OpenAI API. Supports completions (GPT-3.5 Turbo), chat (GPT-4o, etc.), embeddings (Embedding v3), images (DALL·E 3), assistants v2 (threads, runs, vector stores, etc.) batch, fine-tuning, etc.
/// Dart client for the OpenAI API. Supports chat (GPT-4o, etc.), completions, embeddings, images (DALL·E 3), assistants (threads, runs, vector stores, etc.), batch, fine-tuning, etc.
library;

export 'src/client.dart';
Expand Down
16 changes: 16 additions & 0 deletions packages/openai_dart/lib/src/generated/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1175,11 +1175,14 @@ class OpenAIClient {
///
/// `threadId`: The ID of the thread to run.
///
/// `include`: A list of additional fields to include in the response. Currently the only supported value is `step_details.tool_calls[*].file_search.results[*].content` to fetch the file search result content. See the [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings) for more information.
///
/// `request`: Request object for the Create run endpoint.
///
/// `POST` `https://api.openai.com/v1/threads/{thread_id}/runs`
Future<RunObject> createThreadRun({
required String threadId,
String? include,
required CreateRunRequest request,
}) async {
final r = await makeRequest(
Expand All @@ -1190,6 +1193,9 @@ class OpenAIClient {
requestType: 'application/json',
responseType: 'application/json',
body: request,
queryParams: {
if (include != null) 'include': include,
},
);
return RunObject.fromJson(_jsonDecode(r));
}
Expand Down Expand Up @@ -1324,6 +1330,8 @@ class OpenAIClient {
///
/// `before`: A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list.
///
/// `include`: A list of additional fields to include in the response. Currently the only supported value is `step_details.tool_calls[*].file_search.results[*].content` to fetch the file search result content. See the [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings) for more information.
///
/// `GET` `https://api.openai.com/v1/threads/{thread_id}/runs/{run_id}/steps`
Future<ListRunStepsResponse> listThreadRunSteps({
required String threadId,
Expand All @@ -1332,6 +1340,7 @@ class OpenAIClient {
String order = 'desc',
String? after,
String? before,
String? include,
}) async {
final r = await makeRequest(
baseUrl: 'https://api.openai.com/v1',
Expand All @@ -1345,6 +1354,7 @@ class OpenAIClient {
'order': order,
if (after != null) 'after': after,
if (before != null) 'before': before,
if (include != null) 'include': include,
},
);
return ListRunStepsResponse.fromJson(_jsonDecode(r));
Expand All @@ -1362,11 +1372,14 @@ class OpenAIClient {
///
/// `stepId`: The ID of the run step to retrieve.
///
/// `include`: A list of additional fields to include in the response. Currently the only supported value is `step_details.tool_calls[*].file_search.results[*].content` to fetch the file search result content. See the [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings) for more information.
///
/// `GET` `https://api.openai.com/v1/threads/{thread_id}/runs/{run_id}/steps/{step_id}`
Future<RunStepObject> getThreadRunStep({
required String threadId,
required String runId,
required String stepId,
String? include,
}) async {
final r = await makeRequest(
baseUrl: 'https://api.openai.com/v1',
Expand All @@ -1375,6 +1388,9 @@ class OpenAIClient {
isMultipart: false,
requestType: '',
responseType: 'application/json',
queryParams: {
if (include != null) 'include': include,
},
);
return RunStepObject.fromJson(_jsonDecode(r));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ sealed class AssistantStreamEvent with _$AssistantStreamEvent {
// UNION: RunStepStreamEvent
// ------------------------------------------

/// Occurs when a new [run step](https://platform.openai.com/docs/api-reference/runs/step-object) changes state.
/// Occurs when a new [run step](https://platform.openai.com/docs/api-reference/run-steps/step-object) changes state.
const factory AssistantStreamEvent.runStepStreamEvent({
/// The type of the event.
required EventType event,
Expand All @@ -74,7 +74,7 @@ sealed class AssistantStreamEvent with _$AssistantStreamEvent {
// UNION: RunStepStreamDeltaEvent
// ------------------------------------------

/// Occurs when a new [run step](https://platform.openai.com/docs/api-reference/runs/step-object) changes state.
/// Occurs when a new [run step](https://platform.openai.com/docs/api-reference/run-steps/step-object) changes state.
const factory AssistantStreamEvent.runStepStreamDeltaEvent({
/// The type of the event.
required EventType event,
Expand Down
18 changes: 15 additions & 3 deletions packages/openai_dart/lib/src/generated/schema/assistant_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,17 @@ class AssistantToolsFileSearchFileSearch
/// The maximum number of results the file search tool should output. The default is 20 for `gpt-4*` models
/// and 5 for gpt-3.5-turbo. This number should be between 1 and 50 inclusive.
///
/// Note that the file search tool may output fewer than `max_num_results` results. See the [file search
/// tool documentation](https://platform.openai.com/docs/assistants/tools/file-search/number-of-chunks-returned) for more information.
/// Note that the file search tool may output fewer than `max_num_results` results. See the
/// [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings)
/// for more information.
@JsonKey(name: 'max_num_results', includeIfNull: false) int? maxNumResults,

/// The ranking options for the file search.
///
/// See the [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings)
/// for more information.
@JsonKey(name: 'ranking_options', includeIfNull: false)
FileSearchRankingOptions? rankingOptions,
}) = _AssistantToolsFileSearchFileSearch;

/// Object construction from a JSON representation
Expand All @@ -94,7 +102,10 @@ class AssistantToolsFileSearchFileSearch
_$AssistantToolsFileSearchFileSearchFromJson(json);

/// List of all property names of schema
static const List<String> propertyNames = ['max_num_results'];
static const List<String> propertyNames = [
'max_num_results',
'ranking_options'
];

/// Validation constants
static const maxNumResultsMinValue = 1;
Expand All @@ -115,6 +126,7 @@ class AssistantToolsFileSearchFileSearch
Map<String, dynamic> toMap() {
return {
'max_num_results': maxNumResults,
'ranking_options': rankingOptions,
};
}
}
Loading

0 comments on commit e916ad3

Please sign in to comment.