diff options
Diffstat (limited to 'libawl/src/conversation.zig')
| -rw-r--r-- | libawl/src/conversation.zig | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/libawl/src/conversation.zig b/libawl/src/conversation.zig new file mode 100644 index 0000000..0e9a4e9 --- /dev/null +++ b/libawl/src/conversation.zig @@ -0,0 +1,185 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// A streaming text buffer used by content blocks. +/// Thin alias over ArrayList(u8) — amortized O(1) appends, +/// no O(n²) re-copying. +pub const TextualBlock = std.ArrayList(u8); + +/// Create a TextualBlock with initial content (copies the slice). +pub fn textualBlockFromSlice(alloc: Allocator, slice: []const u8) !TextualBlock { + var buf: TextualBlock = .empty; + try buf.appendSlice(alloc, slice); + return buf; +} + +pub const ToolUseBlock = struct { + id: []const u8, + name: []const u8, + input: TextualBlock = .empty, + + pub fn deinit(self: *ToolUseBlock, alloc: Allocator) void { + alloc.free(self.id); + alloc.free(self.name); + self.input.deinit(alloc); + } +}; + +pub const ToolResultBlock = struct { + tool_use_id: []const u8, + content: TextualBlock = .empty, + + pub fn deinit(self: *ToolResultBlock, alloc: Allocator) void { + alloc.free(self.tool_use_id); + self.content.deinit(alloc); + } +}; + +pub const ContentBlock = union(enum) { + Text: TextualBlock, + Thinking: TextualBlock, + ToolUse: ToolUseBlock, + ToolResult: ToolResultBlock, + + pub fn deinit(self: *ContentBlock, alloc: Allocator) void { + switch (self.*) { + inline else => |*b| b.deinit(alloc), + } + } +}; + +pub const MessageRole = enum { + system, + user, + assistant, +}; + +pub const Message = struct { + role: MessageRole, + content: std.ArrayList(ContentBlock) = .empty, + + pub fn deinit(self: *Message, alloc: Allocator) void { + for (self.content.items) |*block| { + block.deinit(alloc); + } + self.content.deinit(alloc); + } +}; + +pub const Conversation = struct { + messages: std.ArrayList(Message) = .empty, + allocator: Allocator, + + pub fn init(allocator: Allocator) Conversation { + return .{ + .allocator = allocator, + }; + } + + pub fn addSystemMessage(self: *Conversation, text: []const u8) !void { + const tb = try textualBlockFromSlice(self.allocator, text); + var content: std.ArrayList(ContentBlock) = .empty; + try content.append(self.allocator, .{ .Text = tb }); + try self.messages.append(self.allocator, .{ + .role = .system, + .content = content, + }); + } + + pub fn addUserMessage(self: *Conversation, text: []const u8) !void { + const tb = try textualBlockFromSlice(self.allocator, text); + var content: std.ArrayList(ContentBlock) = .empty; + try content.append(self.allocator, .{ .Text = tb }); + try self.messages.append(self.allocator, .{ + .role = .user, + .content = content, + }); + } + + /// Append an assistant message. Ownership of the blocks is transferred + /// to the conversation; the caller must not deinit them after this call. + pub fn addAssistantMessage(self: *Conversation, blocks: []const ContentBlock) !void { + var content: std.ArrayList(ContentBlock) = .empty; + try content.ensureTotalCapacity(self.allocator, blocks.len); + for (blocks) |block| { + content.appendAssumeCapacity(block); + } + try self.messages.append(self.allocator, .{ + .role = .assistant, + .content = content, + }); + } + + pub fn deinit(self: *Conversation) void { + for (self.messages.items) |*msg| { + msg.deinit(self.allocator); + } + self.messages.deinit(self.allocator); + } +}; + +test "Conversation - add messages and verify content" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("You are a helpful assistant."); + try conv.addUserMessage("Hello!"); + try conv.addAssistantMessage(&.{ + .{ .Text = try textualBlockFromSlice(allocator, "Hi there!") }, + }); + + try std.testing.expectEqual(@as(usize, 3), conv.messages.items.len); + + try std.testing.expectEqual(MessageRole.system, conv.messages.items[0].role); + try std.testing.expectEqualStrings( + "You are a helpful assistant.", + conv.messages.items[0].content.items[0].Text.items, + ); + + try std.testing.expectEqual(MessageRole.user, conv.messages.items[1].role); + try std.testing.expectEqualStrings("Hello!", conv.messages.items[1].content.items[0].Text.items); + + try std.testing.expectEqual(MessageRole.assistant, conv.messages.items[2].role); + try std.testing.expectEqualStrings("Hi there!", conv.messages.items[2].content.items[0].Text.items); +} + +test "TextualBlock - incremental append" { + const allocator = std.testing.allocator; + + var tb = TextualBlock.empty; + defer tb.deinit(allocator); + + try tb.appendSlice(allocator, "Hello"); + try tb.appendSlice(allocator, " world"); + try std.testing.expectEqualStrings("Hello world", tb.items); +} + +test "Conversation - deinit frees without leaks" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + try conv.addSystemMessage("system"); + try conv.addUserMessage("user message"); + try conv.addAssistantMessage(&.{ + .{ .Text = try textualBlockFromSlice(allocator, "response") }, + }); + conv.deinit(); +} + +test "ContentBlock - Thinking variant" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addAssistantMessage(&.{ + .{ .Thinking = try textualBlockFromSlice(allocator, "hmm...") }, + .{ .Text = try textualBlockFromSlice(allocator, "answer") }, + }); + + try std.testing.expectEqual(@as(usize, 2), conv.messages.items[0].content.items.len); + try std.testing.expectEqualStrings("hmm...", conv.messages.items[0].content.items[0].Thinking.items); + try std.testing.expectEqualStrings("answer", conv.messages.items[0].content.items[1].Text.items); +} |
