diff options
Diffstat (limited to 'libpanto/src/anthropic_messages_json.zig')
| -rw-r--r-- | libpanto/src/anthropic_messages_json.zig | 83 |
1 files changed, 77 insertions, 6 deletions
diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig index c54db86..8e33539 100644 --- a/libpanto/src/anthropic_messages_json.zig +++ b/libpanto/src/anthropic_messages_json.zig @@ -232,8 +232,39 @@ fn writeBlock(s: *std.json.Stringify, block: conversation.ContentBlock) !void { try s.write("tool_result"); try s.objectField("tool_use_id"); try s.write(tr.tool_use_id); + // Anthropic accepts `content` as an array of typed blocks: + // text, image (base64 source), and document (PDF). try s.objectField("content"); - try s.write(tr.content.items); + try s.beginArray(); + for (tr.parts.items) |part| { + switch (part) { + .text => |tb| { + try s.beginObject(); + try s.objectField("type"); + try s.write("text"); + try s.objectField("text"); + try s.write(tb.items); + try s.endObject(); + }, + .media => |m| { + const is_pdf = std.mem.eql(u8, m.media_type, "application/pdf"); + try s.beginObject(); + try s.objectField("type"); + try s.write(if (is_pdf) "document" else "image"); + try s.objectField("source"); + try s.beginObject(); + try s.objectField("type"); + try s.write("base64"); + try s.objectField("media_type"); + try s.write(m.media_type); + try s.objectField("data"); + try s.write(m.data.items); + try s.endObject(); + try s.endObject(); + }, + } + } + try s.endArray(); try s.endObject(); }, // System blocks never reach here: `serializeRequest` filters out @@ -865,7 +896,7 @@ test "parseStreamEvent - unknown type" { const tool_mod = @import("tool.zig"); const StaticToolVT = struct { - fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror![]u8 { + fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror![]tool_mod.ResultPart { return error.NotImplementedInTest; } fn deinit_(_: *anyopaque, _: Allocator) void {} @@ -999,11 +1030,11 @@ test "serializeRequest - user ToolResult becomes tool_result content block" { defer conv.deinit(); const id = try allocator.dupe(u8, "tu_1"); - var content: conversation.TextualBlock = .empty; - try content.appendSlice(allocator, "the answer is 42"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "the answer is 42") }); var blocks: std.ArrayList(conversation.ContentBlock) = .empty; - try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .content = content } }); + try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .parts = parts } }); try conv.messages.append(allocator, .{ .role = .user, .content = blocks }); var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); @@ -1022,7 +1053,47 @@ test "serializeRequest - user ToolResult becomes tool_result content block" { try testing.expectEqual(@as(usize, 1), arr.len); try testing.expectEqualStrings("tool_result", arr[0].object.get("type").?.string); try testing.expectEqualStrings("tu_1", arr[0].object.get("tool_use_id").?.string); - try testing.expectEqualStrings("the answer is 42", arr[0].object.get("content").?.string); + const tr_content = arr[0].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), tr_content.len); + try testing.expectEqualStrings("text", tr_content[0].object.get("type").?.string); + try testing.expectEqualStrings("the answer is 42", tr_content[0].object.get("text").?.string); +} + +test "serializeRequest - tool result with image part emits image block" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "tu_img"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "the file:") }); + try parts.append(allocator, .{ .media = .{ + .media_type = try allocator.dupe(u8, "image/png"), + .data = try conversation.textualBlockFromSlice(allocator, "iVBOR=="), + } }); + var blocks: std.ArrayList(conversation.ContentBlock) = .empty; + try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .parts = parts } }); + try conv.messages.append(allocator, .{ .role = .user, .content = blocks }); + + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0].object; + const arr = msg.get("content").?.array.items[0].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 2), arr.len); + try testing.expectEqualStrings("text", arr[0].object.get("type").?.string); + try testing.expectEqualStrings("image", arr[1].object.get("type").?.string); + const src = arr[1].object.get("source").?.object; + try testing.expectEqualStrings("base64", src.get("type").?.string); + try testing.expectEqualStrings("image/png", src.get("media_type").?.string); + try testing.expectEqualStrings("iVBOR==", src.get("data").?.string); } test "serializeRequest - empty ToolUse input becomes {} not invalid JSON" { |
