Skip to content

Commit 50ecb86

Browse files
author
Michel-André Chirita
authored
feat: adds format support (#43)
1 parent 95d4249 commit 50ecb86

File tree

6 files changed

+209
-34
lines changed

6 files changed

+209
-34
lines changed

Playground/OKPlayground.xcodeproj/project.pbxproj

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
04A2EC422D22A513009C9AED /* ChatWithFormatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A2EC412D22A513009C9AED /* ChatWithFormatView.swift */; };
11+
04C1CC092D21E8B8005362B0 /* OllamaKit in Frameworks */ = {isa = PBXBuildFile; productRef = 04C1CC082D21E8B8005362B0 /* OllamaKit */; };
1012
0A5648C42C1468E0008FB5F6 /* OKPlaygroundApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5648C32C1468E0008FB5F6 /* OKPlaygroundApp.swift */; };
1113
0A5648C82C1468E1008FB5F6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A5648C72C1468E1008FB5F6 /* Assets.xcassets */; };
1214
0A5648CB2C1468E1008FB5F6 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A5648CA2C1468E1008FB5F6 /* Preview Assets.xcassets */; };
@@ -25,6 +27,7 @@
2527
/* End PBXBuildFile section */
2628

2729
/* Begin PBXFileReference section */
30+
04A2EC412D22A513009C9AED /* ChatWithFormatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWithFormatView.swift; sourceTree = "<group>"; };
2831
0A5648C02C1468E0008FB5F6 /* OKPlayground.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OKPlayground.app; sourceTree = BUILT_PRODUCTS_DIR; };
2932
0A5648C32C1468E0008FB5F6 /* OKPlaygroundApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OKPlaygroundApp.swift; sourceTree = "<group>"; };
3033
0A5648C72C1468E1008FB5F6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -49,6 +52,7 @@
4952
files = (
5053
0A5648EE2C150C66008FB5F6 /* OllamaKit in Frameworks */,
5154
0AEA9B902CFCFD0100227D01 /* OllamaKit in Frameworks */,
55+
04C1CC092D21E8B8005362B0 /* OllamaKit in Frameworks */,
5256
);
5357
runOnlyForDeploymentPostprocessing = 0;
5458
};
@@ -104,6 +108,7 @@
104108
0A5648E72C14E00E008FB5F6 /* ChatView.swift */,
105109
0A5648E92C1504CD008FB5F6 /* GenerateView.swift */,
106110
0A7B752C2C55E79400624336 /* ChatWithToolsView.swift */,
111+
04A2EC412D22A513009C9AED /* ChatWithFormatView.swift */,
107112
);
108113
path = Views;
109114
sourceTree = "<group>";
@@ -151,6 +156,7 @@
151156
packageProductDependencies = (
152157
0A5648ED2C150C66008FB5F6 /* OllamaKit */,
153158
0AEA9B8F2CFCFD0100227D01 /* OllamaKit */,
159+
04C1CC082D21E8B8005362B0 /* OllamaKit */,
154160
);
155161
productName = OKPlayground;
156162
productReference = 0A5648C02C1468E0008FB5F6 /* OKPlayground.app */;
@@ -181,7 +187,7 @@
181187
);
182188
mainGroup = 0A5648B72C1468E0008FB5F6;
183189
packageReferences = (
184-
0AEA9B8E2CFCFD0100227D01 /* XCLocalSwiftPackageReference "../../OllamaKit" */,
190+
04C1CC072D21E8B8005362B0 /* XCLocalSwiftPackageReference "../../OllamaKit" */,
185191
);
186192
productRefGroup = 0A5648C12C1468E0008FB5F6 /* Products */;
187193
projectDirPath = "";
@@ -220,6 +226,7 @@
220226
0A5648E42C14D342008FB5F6 /* ModelInfoView.swift in Sources */,
221227
0A5648E22C14C7E1008FB5F6 /* ViewModel.swift in Sources */,
222228
0A5648DF2C14C583008FB5F6 /* EmbeddingsView.swift in Sources */,
229+
04A2EC422D22A513009C9AED /* ChatWithFormatView.swift in Sources */,
223230
);
224231
runOnlyForDeploymentPostprocessing = 0;
225232
};
@@ -429,13 +436,17 @@
429436
/* End XCConfigurationList section */
430437

431438
/* Begin XCLocalSwiftPackageReference section */
432-
0AEA9B8E2CFCFD0100227D01 /* XCLocalSwiftPackageReference "../../OllamaKit" */ = {
439+
04C1CC072D21E8B8005362B0 /* XCLocalSwiftPackageReference "../../OllamaKit" */ = {
433440
isa = XCLocalSwiftPackageReference;
434441
relativePath = ../../OllamaKit;
435442
};
436443
/* End XCLocalSwiftPackageReference section */
437444

438445
/* Begin XCSwiftPackageProductDependency section */
446+
04C1CC082D21E8B8005362B0 /* OllamaKit */ = {
447+
isa = XCSwiftPackageProductDependency;
448+
productName = OllamaKit;
449+
};
439450
0A5648ED2C150C66008FB5F6 /* OllamaKit */ = {
440451
isa = XCSwiftPackageProductDependency;
441452
productName = OllamaKit;

Playground/OKPlayground.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 0 additions & 23 deletions
This file was deleted.

Playground/OKPlayground/Views/AppView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ struct AppView: View {
4444
NavigationLink("Chat with Tools") {
4545
ChatWithToolsView()
4646
}
47-
47+
48+
NavigationLink("Chat with Format") {
49+
ChatWithFormatView()
50+
}
51+
4852
NavigationLink("Generate") {
4953
GenerateView()
5054
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//
2+
// ChatWithFormatView.swift
3+
// OKPlayground
4+
//
5+
// Created by Michel-Andre Chirita on 30/12/2024.
6+
//
7+
8+
import Combine
9+
import OllamaKit
10+
import SwiftUI
11+
12+
struct ChatWithFormatView: View {
13+
14+
enum ViewState {
15+
case idle
16+
case loading
17+
case error(String)
18+
}
19+
20+
@Environment(ViewModel.self) private var viewModel
21+
22+
@State private var model: String? = nil
23+
/// TIP: be sure to include "return as JSON" in your prompt
24+
@State private var prompt = "Lists of the 10 biggest countries in the world with their iso code as id, name and capital, return as JSON"
25+
@State private var cancellables = Set<AnyCancellable>()
26+
@State private var viewState: ViewState = .idle
27+
28+
@State private var responseItems: [ResponseItem] = []
29+
30+
var body: some View {
31+
NavigationStack {
32+
Form {
33+
Section {
34+
Picker("Model", selection: $model) {
35+
ForEach(viewModel.models, id: \.self) { model in
36+
Text(model)
37+
.tag(model as String?)
38+
}
39+
}
40+
41+
TextField("Prompt", text: $prompt, axis: .vertical)
42+
.lineLimit(5)
43+
}
44+
45+
Section {
46+
Button("Chat Async", action: actionAsync)
47+
Button("Chat Combine", action: actionCombine)
48+
}
49+
50+
switch viewState {
51+
case .idle:
52+
EmptyView()
53+
54+
case .loading:
55+
ProgressView()
56+
.id(UUID())
57+
58+
case .error(let error):
59+
Text(error)
60+
.foregroundStyle(.red)
61+
}
62+
63+
Section("Response") {
64+
ForEach(responseItems) { item in
65+
Text("Country: " + item.country + ", capital: " + item.capital)
66+
}
67+
}
68+
}
69+
.navigationTitle("Chat with Format")
70+
.navigationBarTitleDisplayMode(.inline)
71+
.onAppear {
72+
model = viewModel.models.first
73+
}
74+
}
75+
}
76+
77+
func actionAsync() {
78+
clearResponse()
79+
80+
guard let model = model else { return }
81+
let messages = [OKChatRequestData.Message(role: .user, content: prompt)]
82+
var data = OKChatRequestData(model: model, messages: messages, format: getFormat())
83+
data.options = OKCompletionOptions(temperature: 0) /// TIP: better results with temperature = 0
84+
self.viewState = .loading
85+
86+
Task {
87+
do {
88+
var message: String = ""
89+
for try await chunk in viewModel.ollamaKit.chat(data: data) {
90+
if let content = chunk.message?.content {
91+
message.append(content)
92+
}
93+
if chunk.done {
94+
self.viewState = .idle
95+
decodeResponse(message)
96+
}
97+
}
98+
} catch {
99+
print("Error:", error.localizedDescription)
100+
self.viewState = .error(error.localizedDescription)
101+
}
102+
}
103+
}
104+
105+
func actionCombine() {
106+
clearResponse()
107+
108+
guard let model = model else { return }
109+
let messages = [OKChatRequestData.Message(role: .user, content: prompt)]
110+
var data = OKChatRequestData(model: model, messages: messages, format: getFormat())
111+
data.options = OKCompletionOptions(temperature: 0) /// TIP: better results with temperature = 0
112+
self.viewState = .loading
113+
114+
var message: String = ""
115+
viewModel.ollamaKit.chat(data: data)
116+
.compactMap { $0.message?.content }
117+
.scan("", { result, nextChunk in
118+
result + nextChunk
119+
})
120+
.sink { completion in
121+
switch completion {
122+
case .finished:
123+
print("Finished")
124+
decodeResponse(message)
125+
self.viewState = .idle
126+
case .failure(let error):
127+
print("Error:", error.localizedDescription)
128+
self.viewState = .error(error.localizedDescription)
129+
}
130+
} receiveValue: { value in
131+
message = value
132+
}
133+
.store(in: &cancellables)
134+
}
135+
136+
private func getFormat() -> OKJSONValue {
137+
return
138+
.object(["type": .string("array"),
139+
"items": .object([
140+
"type" : .string("object"),
141+
"properties": .object([
142+
"id": .object(["type" : .string("string")]),
143+
"country": .object(["type" : .string("string")]),
144+
"capital": .object(["type" : .string("string")]),
145+
]),
146+
"required": .array([.string("id"), .string("country"), .string("capital")])
147+
])
148+
])
149+
}
150+
151+
private func decodeResponse(_ content: String) {
152+
do {
153+
guard let data = content.data(using: .utf8) else { return }
154+
let response = try JSONDecoder().decode([ResponseItem].self, from: data)
155+
self.responseItems = response
156+
} catch {
157+
print("Error message: \(error)")
158+
self.viewState = .error(error.localizedDescription)
159+
}
160+
}
161+
162+
private func clearResponse() {
163+
self.responseItems = []
164+
}
165+
}
166+
167+
struct ResponseItem: Identifiable, Codable {
168+
let id: String
169+
let country: String
170+
let capital: String
171+
}

Sources/OllamaKit/RequestData/OKChatRequestData.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@ public struct OKChatRequestData: Sendable {
1919

2020
/// An optional array of ``OKJSONValue`` representing the tools available for tool calling in the chat.
2121
public let tools: [OKJSONValue]?
22-
22+
23+
/// Optional ``OKJSONValue`` representing the JSON schema for the response.
24+
/// Be sure to also include "return as JSON" in your prompt
25+
public let format: OKJSONValue?
26+
2327
/// Optional ``OKCompletionOptions`` providing additional configuration for the chat request.
2428
public var options: OKCompletionOptions?
2529

26-
public init(model: String, messages: [Message], tools: [OKJSONValue]? = nil) {
30+
public init(model: String, messages: [Message], tools: [OKJSONValue]? = nil, format: OKJSONValue? = nil) {
2731
self.stream = tools == nil
2832
self.model = model
2933
self.messages = messages
3034
self.tools = tools
35+
self.format = format
3136
}
3237

3338
/// A structure that represents a single message in the chat request.
@@ -68,13 +73,14 @@ extension OKChatRequestData: Encodable {
6873
try container.encode(model, forKey: .model)
6974
try container.encode(messages, forKey: .messages)
7075
try container.encodeIfPresent(tools, forKey: .tools)
71-
76+
try container.encodeIfPresent(format, forKey: .format)
77+
7278
if let options {
7379
try options.encode(to: encoder)
7480
}
7581
}
7682

7783
private enum CodingKeys: String, CodingKey {
78-
case stream, model, messages, tools
84+
case stream, model, messages, tools, format
7985
}
8086
}

Sources/OllamaKit/RequestData/OKGenerateRequestData.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ public struct OKGenerateRequestData: Sendable {
1717
/// A string containing the initial input or prompt.
1818
public let prompt: String
1919

20-
/// /// An optional array of base64-encoded images.
20+
/// An optional array of base64-encoded images.
2121
public let images: [String]?
22-
22+
23+
/// Optional ``OKJSONValue`` representing the JSON schema for the response.
24+
/// Be sure to also include "return as JSON" in your prompt
25+
public let format: OKJSONValue?
26+
2327
/// An optional string specifying the system message.
2428
public var system: String?
2529

@@ -29,11 +33,12 @@ public struct OKGenerateRequestData: Sendable {
2933
/// Optional ``OKCompletionOptions`` providing additional configuration for the generation request.
3034
public var options: OKCompletionOptions?
3135

32-
public init(model: String, prompt: String, images: [String]? = nil) {
36+
public init(model: String, prompt: String, images: [String]? = nil, format: OKJSONValue? = nil) {
3337
self.stream = true
3438
self.model = model
3539
self.prompt = prompt
3640
self.images = images
41+
self.format = format
3742
}
3843
}
3944

@@ -44,6 +49,7 @@ extension OKGenerateRequestData: Encodable {
4449
try container.encode(model, forKey: .model)
4550
try container.encode(prompt, forKey: .prompt)
4651
try container.encodeIfPresent(images, forKey: .images)
52+
try container.encodeIfPresent(format, forKey: .format)
4753
try container.encodeIfPresent(system, forKey: .system)
4854
try container.encodeIfPresent(context, forKey: .context)
4955

@@ -53,6 +59,6 @@ extension OKGenerateRequestData: Encodable {
5359
}
5460

5561
private enum CodingKeys: String, CodingKey {
56-
case stream, model, prompt, images, system, context
62+
case stream, model, prompt, images, format, system, context
5763
}
5864
}

0 commit comments

Comments
 (0)