diff --git a/Sources/LLMMacrosImplementation/GeneratableMacro.swift b/Sources/LLMMacrosImplementation/GeneratableMacro.swift index af1e0be..5dd7c6a 100644 --- a/Sources/LLMMacrosImplementation/GeneratableMacro.swift +++ b/Sources/LLMMacrosImplementation/GeneratableMacro.swift @@ -33,12 +33,15 @@ public struct GeneratableMacro: MemberMacro, ExtensionMacro { let schemaExpr = schema(for: unwrappedType, in: declaration, context: context) return (name, schemaExpr, !isOptional) } + let emptyStringToken="\"\"" let propertyExprsString = props.map { "\"\\\"" + $0.name + "\\\": \" + " + $0.schema }.joined(separator: " + \",\" + ") + let propertyExprsStringFinal = propertyExprsString.isEmpty ? emptyStringToken : propertyExprsString let requiredExprString = props.filter { $0.isRequired }.map { "\"\\\"" + $0.name + "\\\"\"" }.joined(separator: " + \",\" + ") + let requiredExprStringFinal = requiredExprString.isEmpty ? emptyStringToken : requiredExprString return [ """ public static var jsonSchema: String { - return "{ \\"type\\": \\"object\\", \\"properties\\": {" + \(raw: propertyExprsString) + "}, \\"required\\": [" + \(raw: requiredExprString) + "] }" + return "{ \\"type\\": \\"object\\", \\"properties\\": {" + \(raw: propertyExprsStringFinal) + "}, \\"required\\": [" + \(raw: requiredExprStringFinal) + "] }" } """ ] @@ -149,4 +152,4 @@ enum MacroError: Error, CustomStringConvertible { case .notAStruct: "Can only be applied to a struct." } } -} \ No newline at end of file +} diff --git a/Tests/LLMTests/LLMTests.swift b/Tests/LLMTests/LLMTests.swift index 60d66c6..d8fd94b 100644 --- a/Tests/LLMTests/LLMTests.swift +++ b/Tests/LLMTests/LLMTests.swift @@ -779,4 +779,56 @@ final class LLMTests { #expect(assignee["name"] is String) #expect(assignee["age"] is Int) } + + @Generatable + struct EmptyTestStruct { } + + @Test + func testEmptyGeneratable() async throws { + let bot = try await LLM(from: model)! + + let result = try await bot.respond( + to: "Create the void", + as: EmptyTestStruct.self + ) + let project = result.value + let output = result.rawOutput + + print("Project: \(project)") + print("Raw output: \(output)") + + let jsonData = output.data(using: String.Encoding.utf8)! + let parsed = try JSONSerialization.jsonObject(with: jsonData) as! [String: Any] + #expect(parsed.isEmpty) + } + + @Generatable + struct AllNullableAttributes { + let first: Int? + let second: Int? + } + + @Test + func testStructWithNullableAttributes() async throws { + let bot = try await LLM(from: model)! + + print(AllNullableAttributes.jsonSchema) + + // Note: it seems it is hard to tell this LLM to return an empty JSON or drop an attribute, + // so stick to just simple type checking + let result = try await bot.respond( + to: "Return something", + as: AllNullableAttributes.self + ) + let project = result.value + let output = result.rawOutput + + print("Project: \(project)") + print("Raw output: \(output)") + + let jsonData = output.data(using: String.Encoding.utf8)! + let parsed = try JSONSerialization.jsonObject(with: jsonData) as! [String: Any] + #expect(parsed["first"] is Int?) + #expect(parsed["second"] is Int?) + } }