Skip to content

Commit 37122fd

Browse files
committed
refactor: better LLM output parsing (#68)
* fix * clean-up * clean-up
1 parent 856ef11 commit 37122fd

File tree

5 files changed

+21
-97
lines changed

5 files changed

+21
-97
lines changed

packages/apple-llm/ios/AppleLLM.mm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ - (void)generateText:(nonnull NSArray *)messages
114114
@"maxTokens": options.maxTokens().has_value() ? @(options.maxTokens().value()) : [NSNull null],
115115
@"topP": options.topP().has_value() ? @(options.topP().value()) : [NSNull null],
116116
@"topK": options.topK().has_value() ? @(options.topK().value()) : [NSNull null],
117-
@"schema": options.schema(),
118-
@"tools": options.tools()
117+
@"schema": options.schema() ?: [NSNull null],
118+
@"tools": options.tools() ?: [NSNull null]
119119
};
120120

121121
auto callToolBlock = ^(NSString *toolName, id parameters, void (^completion)(id, NSError *)) {
@@ -136,7 +136,7 @@ - (nonnull NSString *)generateStream:(nonnull NSArray *)messages options:(JS::Na
136136
@"maxTokens": options.maxTokens().has_value() ? @(options.maxTokens().value()) : [NSNull null],
137137
@"topP": options.topP().has_value() ? @(options.topP().value()) : [NSNull null],
138138
@"topK": options.topK().has_value() ? @(options.topK().value()) : [NSNull null],
139-
@"schema": options.schema()
139+
@"schema": options.schema() ?: [NSNull null]
140140
};
141141

142142
NSError *error;

packages/apple-llm/ios/AppleLLMImpl.swift

Lines changed: 15 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ public class AppleLLMImpl: NSObject {
2020

2121
private var streamTasks: [String: Task<Void, Never>] = [:]
2222

23-
// MARK: - Constants
24-
25-
private static let supportedStringFormats: Set<String> = [
26-
"date-time", "time", "date", "duration", "email", "hostname", "ipv4", "ipv6", "uuid"
27-
]
28-
2923
@objc
3024
public func isAvailable() -> Bool {
3125
#if canImport(FoundationModels)
@@ -42,7 +36,7 @@ public class AppleLLMImpl: NSObject {
4236
@objc
4337
public func generateText(
4438
_ messages: [[String: Any]],
45-
options: [String: Any]?,
39+
options: [String: Any],
4640
resolve: @escaping (Any?) -> Void,
4741
reject: @escaping (String, String, Error?) -> Void,
4842
toolInvoker: @escaping ToolInvoker
@@ -59,7 +53,7 @@ public class AppleLLMImpl: NSObject {
5953
}
6054
Task {
6155
do {
62-
let tools = try self.createTools(from: options ?? [:], toolInvoker: toolInvoker)
56+
let tools = try self.createTools(from: options, toolInvoker: toolInvoker)
6357
let (transcript, userPrompt) = try self.createTranscriptAndPrompt(from: messages, tools: tools)
6458

6559
let session = LanguageModelSession.init(
@@ -69,13 +63,14 @@ public class AppleLLMImpl: NSObject {
6963
transcript: transcript
7064
)
7165

72-
let generationOptions = try self.createGenerationOptions(from: options ?? [:])
66+
let generationOptions = try self.createGenerationOptions(from: options)
7367

74-
if let schemaObj = options?["schema"] {
75-
let generationSchema = try AppleLLMSchemaParser.createGenerationSchema(from: schemaObj)
68+
if let schemaObj = options["schema"], !(schemaObj is NSNull),
69+
let schema = schemaObj as? [String: Any] {
70+
let generationSchema = try AppleLLMSchemaParser.createGenerationSchema(from: schema)
7671
let response = try await session.respond(to: userPrompt, schema: generationSchema, includeSchemaInPrompt: true, options: generationOptions)
7772

78-
resolve(try response.rawValue())
73+
resolve(try response.content.toDictionary(using: schema))
7974
} else {
8075
let response = try await session.respond(to: userPrompt, options: generationOptions)
8176
resolve(response.content)
@@ -97,7 +92,7 @@ public class AppleLLMImpl: NSObject {
9792
@objc
9893
public func generateStream(
9994
_ messages: [[String: Any]],
100-
options: [String: Any]?,
95+
options: [String: Any],
10196
onUpdate: @escaping (String, String) -> Void,
10297
onComplete: @escaping (String) -> Void,
10398
onError: @escaping (String, String) -> Void
@@ -121,9 +116,9 @@ public class AppleLLMImpl: NSObject {
121116
transcript: transcript
122117
)
123118

124-
let generationOptions = try self.createGenerationOptions(from: options ?? [:])
119+
let generationOptions = try self.createGenerationOptions(from: options)
125120

126-
if let schemaOption = options?["schema"] {
121+
if let schemaOption = options["schema"] as? [String: Any] {
127122
let generationSchema = try AppleLLMSchemaParser.createGenerationSchema(from: schemaOption)
128123
let responseStream = session.streamResponse(
129124
to: userPrompt,
@@ -175,30 +170,14 @@ public class AppleLLMImpl: NSObject {
175170
}
176171
}
177172

178-
@objc
179-
public func isModelAvailable(
180-
_ modelId: String,
181-
resolve: @escaping (Any?) -> Void,
182-
reject: @escaping (String, String, Error?) -> Void
183-
) {
184-
#if canImport(FoundationModels)
185-
if #available(iOS 26, *) {
186-
resolve(SystemLanguageModel.default.availability == .available)
187-
} else {
188-
resolve(false)
189-
}
190-
#else
191-
resolve(false)
192-
#endif
193-
}
194-
195173
// MARK: - Private Methods
196174
#if canImport(FoundationModels)
197175

198176
@available(iOS 26, *)
199177
private func createTools(from options: [String: Any], toolInvoker: @escaping ToolInvoker) throws -> [any Tool] {
200-
guard let toolsDict = options["tools"] as? [String: [String: Any]] else {
201-
throw AppleLLMError.invalidSchema("Tools must be an object with tool definitions")
178+
guard let toolsObj = options["tools"], !(toolsObj is NSNull),
179+
let toolsDict = toolsObj as? [String: [String: Any]] else {
180+
return []
202181
}
203182

204183
var tools: [any Tool] = []
@@ -345,16 +324,7 @@ public class AppleLLMImpl: NSObject {
345324

346325
@available(iOS 26, *)
347326
struct AppleLLMSchemaParser {
348-
349-
// MARK: - Constants
350-
private static let supportedStringFormats: Set<String> = [
351-
"date-time", "time", "date", "duration", "email", "hostname", "ipv4", "ipv6", "uuid"
352-
]
353-
354-
static func createGenerationSchema(from schemaObj: Any) throws -> GenerationSchema {
355-
guard let schemaDict = schemaObj as? [String: Any] else {
356-
throw AppleLLMError.invalidSchema("Schema must be an object")
357-
}
327+
static func createGenerationSchema(from schemaDict: [String: Any]) throws -> GenerationSchema {
358328
let dynamicSchemas = try parseDynamicSchema(from: schemaDict)
359329
return try GenerationSchema(root: dynamicSchemas, dependencies: [])
360330
}
@@ -529,49 +499,8 @@ public class AppleLLMImpl: NSObject {
529499

530500
#endif
531501
}
532-
533-
#if canImport(FoundationModels)
534502

535-
@available(iOS 26, *)
536-
extension LanguageModelSession.Response<GeneratedContent> {
537-
enum RawValueExtractionError: Error {
538-
case noTranscriptEntries
539-
case notAResponseEntry
540-
case noSegments
541-
case notAStructuredSegment
542-
case rawValueNotFound
543-
}
544-
545-
func rawValue() throws -> String {
546-
guard let lastEntry = transcriptEntries.last else {
547-
throw RawValueExtractionError.noTranscriptEntries
548-
}
549-
550-
guard case let .response(res) = lastEntry else {
551-
throw RawValueExtractionError.notAResponseEntry
552-
}
553-
554-
guard let lastSegment = res.segments.last else {
555-
throw RawValueExtractionError.noSegments
556-
}
557-
558-
if case let .text(textSegment) = lastSegment {
559-
return textSegment.content
560-
}
561-
562-
guard case let .structure(structureSegment) = lastSegment else {
563-
throw RawValueExtractionError.notAStructuredSegment
564-
}
565-
566-
for child in Mirror(reflecting: structureSegment).children {
567-
if child.label == "rawValue", let rawValue = child.value as? String {
568-
return rawValue
569-
}
570-
}
571-
572-
throw RawValueExtractionError.rawValueNotFound
573-
}
574-
}
503+
#if canImport(FoundationModels)
575504

576505
@available(iOS 26, *)
577506
extension GeneratedContent {

packages/apple-llm/src/NativeAppleLLM.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface Spec extends TurboModule {
3838
generateText(
3939
messages: AppleMessage[],
4040
options: AppleGenerationOptions
41-
): Promise<string>
41+
): Promise<string | UnsafeObject>
4242
generateStream(
4343
messages: AppleMessage[],
4444
options: AppleGenerationOptions

packages/apple-llm/src/ai-sdk.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ export class AppleLLMChatLanguageModel implements LanguageModelV1 {
3636
async doGenerate(options: LanguageModelV1CallOptions) {
3737
const messages = this.convertMessages(options.prompt)
3838

39-
const text = await NativeAppleLLM.generateText(messages, {
39+
const response = await NativeAppleLLM.generateText(messages, {
4040
maxTokens: options.maxTokens,
4141
temperature: options.temperature,
4242
topP: options.topP,
4343
topK: options.topK,
4444
})
4545

4646
return {
47-
text,
47+
text: typeof response === 'string' ? response : JSON.stringify(response),
4848
finishReason: 'stop' as const,
4949
// Apple LLM doesn't provide token counts.
5050
// We will have to handle this ourselves in the future to avoid errors.

packages/apple-llm/src/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,6 @@ async function generateText(
7171
generationOptions
7272
)
7373

74-
if (schema) {
75-
const parsed = schema.parse(JSON.parse(response))
76-
return parsed
77-
}
78-
7974
return response
8075
}
8176

0 commit comments

Comments
 (0)