Skip to content

Commit cc8a354

Browse files
committed
C2 WE ARE IN ORBIT I REPEAT WE ARE IN ORBIT
feat: add AI document flow and refactor context cleanup Introduce GenerateOutline, WriteContent, and AssembleDocument nodes and a main flow that uses Ollama and PocketFlow. Replace the old Value abstraction with StoredValue that holds an allocated pointer plus a per-value destructor; Context.set now allocates owned values and Context.deinit invokes destructors to free stored data. Extend Node.VTable with cleanup_prep and cleanup_exec and add Node wrappers; Flow.run now calls node cleanup functions. Adjust Ollama request body sending and tweak JSON parsing/escaping helpers.
1 parent bc4740e commit cc8a354

File tree

5 files changed

+454
-26
lines changed

5 files changed

+454
-26
lines changed

main.zig

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
const std = @import("std");
2+
const Allocator = std.mem.Allocator;
3+
4+
const ollama = @import("src/ollama.zig");
5+
const Ollama = ollama.Ollama;
6+
const pocketflow = @import("src/pocketflow.zig");
7+
const Node = pocketflow.Node;
8+
const BaseNode = pocketflow.BaseNode;
9+
const Context = pocketflow.Context;
10+
const Flow = pocketflow.Flow;
11+
12+
// --- Node Implementations ---
13+
14+
const GenerateOutlineNode = struct {
15+
base: BaseNode,
16+
17+
pub fn init(allocator: Allocator) *GenerateOutlineNode {
18+
const self = allocator.create(GenerateOutlineNode) catch @panic("oom");
19+
self.* = .{
20+
.base = BaseNode.init(allocator),
21+
};
22+
return self;
23+
}
24+
25+
pub fn deinit(self: *GenerateOutlineNode, allocator: Allocator) void {
26+
self.base.deinit();
27+
allocator.destroy(self);
28+
}
29+
30+
pub fn prep(_: *anyopaque, allocator: Allocator, context: *Context) !*anyopaque {
31+
const topic = context.get([]const u8, "topic") orelse {
32+
std.debug.print("ERROR: topic not found in context!\n", .{});
33+
@panic("topic not found");
34+
};
35+
std.debug.print("Prep: Generating outline for topic: '{s}' (len: {})\n", .{ topic, topic.len });
36+
const prep_result = allocator.create([]const u8) catch @panic("oom");
37+
prep_result.* = topic;
38+
return @ptrCast(prep_result);
39+
}
40+
41+
pub fn exec(_: *anyopaque, allocator: Allocator, prep_res: *anyopaque) !*anyopaque {
42+
const topic_ptr: *const []const u8 = @ptrCast(@alignCast(prep_res));
43+
const topic = topic_ptr.*;
44+
std.debug.print("Exec: Creating outline for topic: {s}...\n", .{topic});
45+
46+
// Call Ollama to generate outline
47+
var client = try Ollama.init(allocator, "http://localhost:11434");
48+
defer client.deinit();
49+
50+
const prompt = try std.fmt.allocPrint(allocator, "Create a simple outline with 3-4 main points for an article about: {s}. Return only the outline points, one per line, without numbers or bullets.", .{topic});
51+
defer allocator.free(prompt);
52+
53+
const options = Ollama.GenerateOptions{
54+
.model = "granite4:350m-h",
55+
.temperature = 0.7,
56+
.top_p = null,
57+
.top_k = null,
58+
.num_predict = 200,
59+
.stop = null,
60+
.seed = null,
61+
.stream = false,
62+
};
63+
64+
var response = client.generate(prompt, options) catch |err| {
65+
std.debug.print("Ollama generate failed: {}\n", .{err});
66+
// Fallback to default outline - need to allocate the strings
67+
const outline_literals = &[_][]const u8{ "Introduction", "Main Point 1", "Conclusion" };
68+
const outline_points = try allocator.alloc([]const u8, outline_literals.len);
69+
for (outline_literals, 0..) |literal, i| {
70+
outline_points[i] = try allocator.dupe(u8, literal);
71+
}
72+
const exec_result = allocator.create([][]const u8) catch @panic("oom");
73+
exec_result.* = outline_points;
74+
return @ptrCast(exec_result);
75+
};
76+
defer response.deinit();
77+
78+
// Parse the response into outline points
79+
var outline_list = std.ArrayListUnmanaged([]const u8){};
80+
var lines = std.mem.splitScalar(u8, response.response, '\n');
81+
while (lines.next()) |line| {
82+
const trimmed = std.mem.trim(u8, line, " \t\r");
83+
if (trimmed.len > 0) {
84+
const owned_line = try allocator.dupe(u8, trimmed);
85+
try outline_list.append(allocator, owned_line);
86+
}
87+
}
88+
89+
const exec_result = allocator.create([][]const u8) catch @panic("oom");
90+
exec_result.* = try outline_list.toOwnedSlice(allocator);
91+
return @ptrCast(exec_result);
92+
}
93+
94+
pub fn post(_: *anyopaque, _: Allocator, context: *Context, _: *anyopaque, exec_res: *anyopaque) ![]const u8 {
95+
const outline_ptr: *const [][]const u8 = @ptrCast(@alignCast(exec_res));
96+
const outline = outline_ptr.*;
97+
try context.set("outline", outline);
98+
std.debug.print("Post: Outline generated with {d} points.\n", .{outline.len});
99+
return "default";
100+
}
101+
102+
pub fn cleanup_prep(_: *anyopaque, allocator: Allocator, prep_res: *anyopaque) void {
103+
const topic_ptr: *const []const u8 = @ptrCast(@alignCast(prep_res));
104+
allocator.destroy(topic_ptr);
105+
}
106+
107+
pub fn cleanup_exec(_: *anyopaque, allocator: Allocator, exec_res: *anyopaque) void {
108+
// Don't free the outline data - it's stored in context and will be freed during context cleanup
109+
const outline_ptr: *const [][]const u8 = @ptrCast(@alignCast(exec_res));
110+
allocator.destroy(outline_ptr);
111+
}
112+
113+
pub const VTABLE = Node.VTable{
114+
.prep = prep,
115+
.exec = exec,
116+
.post = post,
117+
.cleanup_prep = cleanup_prep,
118+
.cleanup_exec = cleanup_exec,
119+
};
120+
};
121+
122+
const WriteContentNode = struct {
123+
base: BaseNode,
124+
125+
pub fn init(allocator: Allocator) *WriteContentNode {
126+
const self = allocator.create(WriteContentNode) catch @panic("oom");
127+
self.* = .{
128+
.base = BaseNode.init(allocator),
129+
};
130+
return self;
131+
}
132+
133+
pub fn deinit(self: *WriteContentNode, allocator: Allocator) void {
134+
self.base.deinit();
135+
allocator.destroy(self);
136+
}
137+
138+
pub fn prep(_: *anyopaque, allocator: Allocator, context: *Context) !*anyopaque {
139+
const outline = context.get([][]const u8, "outline").?;
140+
141+
std.debug.print("Prep: Writing content for {d} outline points.\n", .{outline.len});
142+
143+
const prep_result = allocator.create([][]const u8) catch @panic("oom");
144+
145+
prep_result.* = outline;
146+
147+
return @ptrCast(prep_result);
148+
}
149+
150+
pub fn exec(_: *anyopaque, allocator: Allocator, prep_res: *anyopaque) !*anyopaque {
151+
const outline_ptr: *const [][]const u8 = @ptrCast(@alignCast(prep_res));
152+
const outline = outline_ptr.*;
153+
std.debug.print("Exec: Generating content for each point...\n", .{});
154+
155+
var client = try Ollama.init(allocator, "http://localhost:11434");
156+
157+
defer client.deinit();
158+
159+
var content_map = std.StringHashMap([]const u8).init(allocator);
160+
161+
for (outline) |point| {
162+
std.debug.print(" Generating content for: {s}\n", .{point});
163+
164+
const prompt = try std.fmt.allocPrint(allocator, "Write 2-3 sentences of content for this section: {s}", .{point});
165+
defer allocator.free(prompt);
166+
167+
const options = Ollama.GenerateOptions{
168+
.model = "granite4:350m-h",
169+
.temperature = 0.7,
170+
.top_p = null,
171+
.top_k = null,
172+
.num_predict = 150,
173+
.stop = null,
174+
.seed = null,
175+
.stream = false,
176+
};
177+
178+
var response = client.generate(prompt, options) catch |err| {
179+
std.debug.print(" Ollama generate failed for '{s}': {}\n", .{ point, err });
180+
// Fallback content
181+
const content = try std.fmt.allocPrint(allocator, "This is the content for {s}.", .{point});
182+
try content_map.put(point, content);
183+
continue;
184+
};
185+
186+
const content = try allocator.dupe(u8, std.mem.trim(u8, response.response, " \t\r\n"));
187+
response.deinit();
188+
189+
try content_map.put(point, content);
190+
}
191+
192+
const exec_result = allocator.create(std.StringHashMap([]const u8)) catch @panic("oom");
193+
exec_result.* = content_map;
194+
return exec_result;
195+
}
196+
197+
pub fn post(_: *anyopaque, _: Allocator, context: *Context, _: *anyopaque, exec_res: *anyopaque) ![]const u8 {
198+
const content: *std.StringHashMap([]const u8) = @ptrCast(@alignCast(exec_res));
199+
try context.set("content", content.*);
200+
std.debug.print("Post: Content generated.\n", .{});
201+
return "default";
202+
}
203+
204+
pub fn cleanup_prep(_: *anyopaque, allocator: Allocator, prep_res: *anyopaque) void {
205+
const outline_ptr: *const [][]const u8 = @ptrCast(@alignCast(prep_res));
206+
allocator.destroy(outline_ptr);
207+
}
208+
209+
pub fn cleanup_exec(_: *anyopaque, allocator: Allocator, exec_res: *anyopaque) void {
210+
// Don't free the content map data - it's stored in context and will be freed during context cleanup
211+
const content_map: *std.StringHashMap([]const u8) = @ptrCast(@alignCast(exec_res));
212+
allocator.destroy(content_map);
213+
}
214+
215+
pub const VTABLE = Node.VTable{
216+
.prep = prep,
217+
.exec = exec,
218+
.post = post,
219+
.cleanup_prep = cleanup_prep,
220+
.cleanup_exec = cleanup_exec,
221+
};
222+
};
223+
224+
const AssembleDocumentNode = struct {
225+
base: BaseNode,
226+
227+
pub fn init(allocator: Allocator) *AssembleDocumentNode {
228+
const self = allocator.create(AssembleDocumentNode) catch @panic("oom");
229+
self.* = .{
230+
.base = BaseNode.init(allocator),
231+
};
232+
return self;
233+
}
234+
235+
pub fn deinit(self: *AssembleDocumentNode, allocator: Allocator) void {
236+
self.base.deinit();
237+
allocator.destroy(self);
238+
}
239+
240+
pub fn prep(_: *anyopaque, allocator: Allocator, context: *Context) !*anyopaque {
241+
std.debug.print("Prep: Assembling final document.\n", .{});
242+
const outline = context.get([][]const u8, "outline").?;
243+
const content = context.get(std.StringHashMap([]const u8), "content").?;
244+
245+
// Store both in a simple struct
246+
const PrepData = struct {
247+
outline: [][]const u8,
248+
content: std.StringHashMap([]const u8),
249+
};
250+
251+
const prep_result = try allocator.create(PrepData);
252+
prep_result.* = .{
253+
.outline = outline,
254+
.content = content,
255+
};
256+
return prep_result;
257+
}
258+
259+
pub fn exec(_: *anyopaque, allocator: Allocator, prep_res: *anyopaque) !*anyopaque {
260+
const PrepData = struct {
261+
outline: [][]const u8,
262+
content: std.StringHashMap([]const u8),
263+
};
264+
265+
const data: *PrepData = @ptrCast(@alignCast(prep_res));
266+
const outline = data.outline;
267+
const content = data.content;
268+
269+
std.debug.print("Exec: Combining outline and content...\n", .{});
270+
var document_parts = std.array_list.Managed(u8).init(allocator);
271+
defer document_parts.deinit();
272+
273+
const writer = document_parts.writer();
274+
275+
for (outline) |point| {
276+
try writer.print("## {s}\n", .{point});
277+
if (content.get(point)) |point_content| {
278+
try writer.print("{s}\n\n", .{point_content});
279+
}
280+
}
281+
282+
const final_document = try document_parts.toOwnedSlice();
283+
const exec_result = allocator.create([]const u8) catch @panic("oom");
284+
exec_result.* = final_document;
285+
286+
return @ptrCast(exec_result);
287+
}
288+
289+
pub fn post(_: *anyopaque, _: Allocator, context: *Context, _: *anyopaque, exec_res: *anyopaque) ![]const u8 {
290+
const document_ptr: *const []const u8 = @ptrCast(@alignCast(exec_res));
291+
const document = document_ptr.*;
292+
try context.set("document", document);
293+
std.debug.print("Post: Final document assembled.\n", .{});
294+
return "end";
295+
}
296+
297+
pub fn cleanup_prep(_: *anyopaque, allocator: Allocator, prep_res: *anyopaque) void {
298+
const PrepData = struct {
299+
outline: [][]const u8,
300+
content: std.StringHashMap([]const u8),
301+
};
302+
const data: *PrepData = @ptrCast(@alignCast(prep_res));
303+
allocator.destroy(data);
304+
}
305+
306+
pub fn cleanup_exec(_: *anyopaque, allocator: Allocator, exec_res: *anyopaque) void {
307+
// Don't free the document data - it's stored in context and will be freed during context cleanup
308+
const document_ptr: *const []const u8 = @ptrCast(@alignCast(exec_res));
309+
allocator.destroy(document_ptr);
310+
}
311+
312+
pub const VTABLE = Node.VTable{
313+
.prep = prep,
314+
.exec = exec,
315+
.post = post,
316+
.cleanup_prep = cleanup_prep,
317+
.cleanup_exec = cleanup_exec,
318+
};
319+
};
320+
321+
pub fn main() !void {
322+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
323+
defer _ = gpa.deinit();
324+
const allocator = gpa.allocator();
325+
326+
std.debug.print("\n=== PocketFlow: AI Document Generator ===\n\n", .{});
327+
328+
// --- Create Nodes ---
329+
const outline_node = GenerateOutlineNode.init(allocator);
330+
defer outline_node.deinit(allocator);
331+
const content_node = WriteContentNode.init(allocator);
332+
defer content_node.deinit(allocator);
333+
const assemble_node = AssembleDocumentNode.init(allocator);
334+
defer assemble_node.deinit(allocator);
335+
336+
// --- Create Node wrappers ---
337+
const outline_node_wrapper = Node{ .self = outline_node, .vtable = &GenerateOutlineNode.VTABLE };
338+
const content_node_wrapper = Node{ .self = content_node, .vtable = &WriteContentNode.VTABLE };
339+
const assemble_node_wrapper = Node{ .self = assemble_node, .vtable = &AssembleDocumentNode.VTABLE };
340+
341+
// --- Create the Flow ---
342+
outline_node.base.next("default", content_node_wrapper);
343+
content_node.base.next("default", assemble_node_wrapper);
344+
345+
var flow = Flow.init(allocator, outline_node_wrapper);
346+
347+
// --- Run the Flow ---
348+
var context = Context.init(allocator);
349+
defer {
350+
// Clean up context values (the actual data, not the wrappers)
351+
// Context.deinit() will free the pointer wrappers
352+
if (context.get([]const u8, "topic")) |topic| {
353+
allocator.free(topic);
354+
}
355+
if (context.get([][]const u8, "outline")) |outline| {
356+
for (outline) |point| {
357+
allocator.free(point);
358+
}
359+
allocator.free(outline);
360+
}
361+
if (context.get(std.StringHashMap([]const u8), "content")) |content| {
362+
var content_copy = content;
363+
var it = content_copy.iterator();
364+
while (it.next()) |entry| {
365+
allocator.free(entry.value_ptr.*);
366+
}
367+
content_copy.deinit();
368+
}
369+
if (context.get([]const u8, "document")) |document| {
370+
allocator.free(document);
371+
}
372+
context.deinit();
373+
}
374+
375+
// Store topic - duplicate the string so it's owned by the context
376+
const topic_str = try allocator.dupe(u8, "The Future of AI");
377+
try context.set("topic", topic_str);
378+
const test_topic = context.get([]const u8, "topic");
379+
std.debug.print("DEBUG: Stored topic, retrieved: '{?s}'\n", .{test_topic});
380+
381+
try flow.run(&context);
382+
383+
// --- Print Final Result ---
384+
if (context.get([]const u8, "document")) |document| {
385+
std.debug.print("\n=== FINAL DOCUMENT ===\n{s}\n", .{document});
386+
}
387+
}

0 commit comments

Comments
 (0)