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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
0DEE5DC12BB40643004894AD /* SwiftOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 0DEE5DC02BB40643004894AD /* SwiftOpenAI */; };
0DF957842BB53BEF00DD2013 /* ServiceSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */; };
0DF957862BB543F100DD2013 /* AIProxyIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */; };
7B029E372C6893FD0025681A /* ChatStructuredOutputProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B029E362C6893FD0025681A /* ChatStructuredOutputProvider.swift */; };
7B029E392C68940D0025681A /* ChatStructuredOutputDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B029E382C68940D0025681A /* ChatStructuredOutputDemoView.swift */; };
7B1268052B08246400400694 /* AssistantConfigurationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1268042B08246400400694 /* AssistantConfigurationDemoView.swift */; };
7B1268072B08247C00400694 /* AssistantConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1268062B08247C00400694 /* AssistantConfigurationProvider.swift */; };
7B3DDCC52BAAA722004B5C96 /* AssistantsListDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3DDCC42BAAA722004B5C96 /* AssistantsListDemoView.swift */; };
Expand Down Expand Up @@ -88,6 +90,8 @@
/* Begin PBXFileReference section */
0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceSelectionView.swift; sourceTree = "<group>"; };
0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIProxyIntroView.swift; sourceTree = "<group>"; };
7B029E362C6893FD0025681A /* ChatStructuredOutputProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStructuredOutputProvider.swift; sourceTree = "<group>"; };
7B029E382C68940D0025681A /* ChatStructuredOutputDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStructuredOutputDemoView.swift; sourceTree = "<group>"; };
7B1268042B08246400400694 /* AssistantConfigurationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantConfigurationDemoView.swift; sourceTree = "<group>"; };
7B1268062B08247C00400694 /* AssistantConfigurationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantConfigurationProvider.swift; sourceTree = "<group>"; };
7B3DDCC42BAAA722004B5C96 /* AssistantsListDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantsListDemoView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -176,6 +180,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
7B029E352C6893BF0025681A /* ChatStructuredOutputs */ = {
isa = PBXGroup;
children = (
7B029E362C6893FD0025681A /* ChatStructuredOutputProvider.swift */,
7B029E382C68940D0025681A /* ChatStructuredOutputDemoView.swift */,
);
path = ChatStructuredOutputs;
sourceTree = "<group>";
};
7B1268032B08241200400694 /* Assistants */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -360,6 +373,7 @@
7BA788FA2AE23B27008825D5 /* AudioDemo */,
7B436B9F2AE2593D003CE281 /* ChatDemo */,
7B7239AC2AF9FEC300646679 /* ChatFunctionsCall */,
7B029E352C6893BF0025681A /* ChatStructuredOutputs */,
7B72399E2AF625B700646679 /* ChatStreamFluidConversationDemo */,
7B436BA42AE77EF9003CE281 /* EmbeddingsDemo */,
7B436BA92AE788CA003CE281 /* FineTuningDemo */,
Expand Down Expand Up @@ -630,7 +644,9 @@
7BA788FC2AE23B42008825D5 /* AudioDemoView.swift in Sources */,
7B99C2E72C0718DE00E701B3 /* FilesPicker.swift in Sources */,
7B1268072B08247C00400694 /* AssistantConfigurationProvider.swift in Sources */,
7B029E392C68940D0025681A /* ChatStructuredOutputDemoView.swift in Sources */,
7B436BBE2AE7ABDA003CE281 /* ModelsDemoView.swift in Sources */,
7B029E372C6893FD0025681A /* ChatStructuredOutputProvider.swift in Sources */,
7B436BA32AE25962003CE281 /* ChatDemoView.swift in Sources */,
7B7239A82AF6292100646679 /* LoadingView.swift in Sources */,
7B436B992AE25052003CE281 /* ContentLoader.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//
// ChatStructuredOutputDemoView.swift
// SwiftOpenAIExample
//
// Created by James Rochabrun on 8/10/24.
//

import Foundation
import SwiftOpenAI
import SwiftUI

/**
Schema demo

https://openai.com/index/introducing-structured-outputs-in-the-api/

"response_format": {
"type": "json_schema",
"json_schema": {
"name": "math_response",
"strict": true,
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"explanation": {
"type": "string"
},
"output": {
"type": "string"
}
},
"required": ["explanation", "output"],
"additionalProperties": false
}
},
"final_answer": {
"type": "string"
}
},
"required": ["steps", "final_answer"],
"additionalProperties": false
}
}
}
*/

// Steps to define the above Schema:

// 1: Define the Step schema object

let stepSchema = JSONSchema(
type: .object,
properties: [
"explanation": JSONSchema(type: .string),
"output": JSONSchema(
type: .string)
],
required: ["explanation", "output"],
additionalProperties: false
)

// 2. Define the steps Array schema.

let stepsArraySchema = JSONSchema(type: .array, items: stepSchema)

// 3. Define the final Answer schema.
let finalAnswerSchema = JSONSchema(type: .string)

// 4. Define the response format JSON schema.
let responseFormatSchema = JSONSchemaResponseFormat(
name: "math_response",
strict: true,
schema: JSONSchema(
type: .object,
properties: [
"steps": stepsArraySchema,
"final_answer": finalAnswerSchema
],
required: ["steps", "final_answer"],
additionalProperties: false
)
)

struct ChatStructuredOutputDemoView: View {

@State private var chatProvider: ChatStructuredOutputProvider
@State private var isLoading = false
@State private var prompt = ""
@State private var selectedSegment: ChatConfig = .chatCompeltionStream

enum ChatConfig {
case chatCompletion
case chatCompeltionStream
}

init(service: OpenAIService) {
_chatProvider = State(initialValue: ChatStructuredOutputProvider(service: service))
}

var body: some View {
ScrollView {
VStack {
picker
textArea
Text(chatProvider.errorMessage)
.foregroundColor(.red)
switch selectedSegment {
case .chatCompeltionStream:
streamedChatResultView
case .chatCompletion:
chatCompletionResultView
}
}
}
.overlay(
Group {
if isLoading {
ProgressView()
} else {
EmptyView()
}
}
)
}

var picker: some View {
Picker("Options", selection: $selectedSegment) {
Text("Chat Completion").tag(ChatConfig.chatCompletion)
Text("Chat Completion stream").tag(ChatConfig.chatCompeltionStream)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}

var textArea: some View {
HStack(spacing: 4) {
TextField("Enter prompt", text: $prompt, axis: .vertical)
.textFieldStyle(.roundedBorder)
.padding()
Button {
Task {
isLoading = true
defer { isLoading = false } // ensure isLoading is set to false when the

let content: ChatCompletionParameters.Message.ContentType = .text(prompt)
prompt = ""
let parameters = ChatCompletionParameters(
messages: [
.init(role: .system, content: .text("You are a helpful math tutor.")),
.init(
role: .user,
content: content)],
model: .gpt4o20240806,
responseFormat: .jsonSchema(responseFormatSchema))
switch selectedSegment {
case .chatCompletion:
try await chatProvider.startChat(parameters: parameters)
case .chatCompeltionStream:
try await chatProvider.startStreamedChat(parameters: parameters)
}
}
} label: {
Image(systemName: "paperplane")
}
.buttonStyle(.bordered)
}
.padding()
}

/// stream = `false`
var chatCompletionResultView: some View {
ForEach(Array(chatProvider.messages.enumerated()), id: \.offset) { idx, val in
VStack(spacing: 0) {
Text("\(val)")
}
}
}

/// stream = `true`
var streamedChatResultView: some View {
VStack {
Button("Cancel stream") {
chatProvider.cancelStream()
}
Text(chatProvider.message)

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// ChatStructuredOutputProvider.swift
// SwiftOpenAIExample
//
// Created by James Rochabrun on 8/10/24.
//

import Foundation
import SwiftOpenAI

@Observable
final class ChatStructuredOutputProvider {

private let service: OpenAIService
private var streamTask: Task<Void, Never>? = nil
var message: String = ""
var messages: [String] = []
var errorMessage: String = ""

// MARK: - Initializer

init(service: OpenAIService) {
self.service = service
}

// MARK: - Public Methods

func startChat(
parameters: ChatCompletionParameters) async throws
{
do {
let choices = try await service.startChat(parameters: parameters).choices
self.messages = choices.compactMap(\.message.content).map { $0.asJsonFormatted() }
assert(messages.count == 1)
self.errorMessage = choices.first?.message.refusal ?? ""
} catch APIError.responseUnsuccessful(let description, let statusCode) {
self.errorMessage = "Network error with status code: \(statusCode) and description: \(description)"
} catch {
self.errorMessage = error.localizedDescription
}
}

func startStreamedChat(
parameters: ChatCompletionParameters) async throws
{
streamTask = Task {
do {
let stream = try await service.startStreamedChat(parameters: parameters)
for try await result in stream {
let firstChoiceDelta = result.choices.first?.delta
let content = firstChoiceDelta?.refusal ?? firstChoiceDelta?.content ?? ""
self.message += content
if result.choices.first?.finishReason != nil {
self.message = self.message.asJsonFormatted()
}
}
} catch APIError.responseUnsuccessful(let description, let statusCode) {
self.errorMessage = "Network error with status code: \(statusCode) and description: \(description)"
} catch {
self.errorMessage = error.localizedDescription
}
}
}

func cancelStream() {
streamTask?.cancel()
}
}

/// Helper that allows to display the JSON Schema.
extension String {

func asJsonFormatted() -> String {
guard let data = self.data(using: .utf8) else { return self }
do {
// Parse JSON string to Any object
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
// Convert back to data with pretty-printing
let prettyPrintedData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys])

// Convert formatted data back to string
return String(data: prettyPrintedData, encoding: .utf8) ?? self
}
} catch {
print("Error formatting JSON: \(error)")
}
return self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct OptionsListView: View {
case chatHistoryConversation = "Chat History Conversation"
case chatFunctionCall = "Chat Functions call"
case chatFunctionsCallStream = "Chat Functions call (Stream)"
case chatStructuredOutput = "Chat Structured Output"
case configureAssistant = "Configure Assistant"

var id: String { rawValue }
Expand Down Expand Up @@ -71,6 +72,8 @@ struct OptionsListView: View {
ChatFunctionCallDemoView(service: openAIService)
case .chatFunctionsCallStream:
ChatFunctionsCalllStreamDemoView(service: openAIService)
case .chatStructuredOutput:
ChatStructuredOutputDemoView(service: openAIService)
case .configureAssistant:
AssistantConfigurationDemoView(service: openAIService)
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/OpenAI/Public/Parameters/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public enum Model {
case gpt4o20240513 // 128k context window with training data up to Oct 2023
/// The most affordable and intelligent small model for fast, lightweight tasks. GPT-4o mini is cheaper and more capable than GPT-3.5 Turbo. Currently points to gpt-4o-mini-2024-07-18.
case gpt4omini
/// Latest snapshot that supports [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs)/
case gpt4o20240806

case gpt35Turbo
case gpt35Turbo1106 // Most updated - Supports parallel function calls
Expand Down Expand Up @@ -62,6 +64,7 @@ public enum Model {
switch self {
case .gpt4o: return "gpt-4o"
case .gpt4o20240513: return "gpt-4o-2024-05-13"
case .gpt4o20240806: return "gpt-4o-2024-08-06"
case .gpt4omini: return "gpt-4o-mini"
case .gpt35Turbo: return "gpt-3.5-turbo"
case .gpt35Turbo1106: return "gpt-3.5-turbo-1106"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ public struct ChatCompletionChunkObject: Decodable {
public let functionCall: FunctionCall?
/// The role of the author of this message.
public let role: String?
/// The refusal message generated by the model.
public let refusal: String?

enum CodingKeys: String, CodingKey {
case content
case toolCalls = "tool_calls"
case functionCall = "function_call"
case role
case refusal
}
}

Expand Down