mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
refactor: rename buildJson to stringifyJson and clean up logic
- Rename `Command.buildJson` to `stringifyJson` for clarity. - Flatten tool call recording loop in `Agent.zig` to reduce nesting. - Extract `parseValue` helper in `tools.zig` to reduce duplication. - Optimize `substituteEnvVars` by skipping redundant string scans.
This commit is contained in:
@@ -923,15 +923,13 @@ fn processUserMessage(self: *Self, user_input: []const u8, record_comment: ?[]co
|
||||
if (self.recorder.file != null) {
|
||||
var recorded_any = false;
|
||||
for (result.tool_calls_made) |tc| {
|
||||
if (!tc.is_error) {
|
||||
if (Command.fromToolCall(ma, tc.name, tc.arguments)) |cmd| {
|
||||
if (!recorded_any) {
|
||||
if (record_comment) |c| self.recorder.recordComment(c);
|
||||
recorded_any = true;
|
||||
}
|
||||
self.recorder.record(cmd);
|
||||
}
|
||||
if (tc.is_error) continue;
|
||||
const cmd = Command.fromToolCall(ma, tc.name, tc.arguments) orelse continue;
|
||||
if (!recorded_any) {
|
||||
if (record_comment) |c| self.recorder.recordComment(c);
|
||||
recorded_any = true;
|
||||
}
|
||||
self.recorder.record(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -421,26 +421,26 @@ pub fn noSubstitute(_: std.mem.Allocator, input: []const u8) []const u8 {
|
||||
pub fn toToolCall(arena: std.mem.Allocator, cmd: Command, substitute: SubstituteFn) ?ToolCall {
|
||||
const Action = lp.tools.Action;
|
||||
return switch (cmd) {
|
||||
.goto => |url| .{ .name = @tagName(Action.goto), .args_json = buildJson(arena, .{ .url = substitute(arena, url) }) },
|
||||
.click => |sel| .{ .name = @tagName(Action.click), .args_json = buildJson(arena, .{ .selector = substitute(arena, sel) }) },
|
||||
.type_cmd => |args| .{ .name = @tagName(Action.fill), .args_json = buildJson(arena, .{
|
||||
.goto => |url| .{ .name = @tagName(Action.goto), .args_json = stringifyJson(arena, .{ .url = substitute(arena, url) }) },
|
||||
.click => |sel| .{ .name = @tagName(Action.click), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, sel) }) },
|
||||
.type_cmd => |args| .{ .name = @tagName(Action.fill), .args_json = stringifyJson(arena, .{
|
||||
.selector = substitute(arena, args.selector),
|
||||
.value = args.value,
|
||||
}) },
|
||||
.wait => |sel| .{ .name = @tagName(Action.waitForSelector), .args_json = buildJson(arena, .{ .selector = sel }) },
|
||||
.scroll => |args| .{ .name = @tagName(Action.scroll), .args_json = buildJson(arena, .{ .x = args.x, .y = args.y }) },
|
||||
.hover => |sel| .{ .name = @tagName(Action.hover), .args_json = buildJson(arena, .{ .selector = substitute(arena, sel) }) },
|
||||
.select => |args| .{ .name = @tagName(Action.selectOption), .args_json = buildJson(arena, .{
|
||||
.wait => |sel| .{ .name = @tagName(Action.waitForSelector), .args_json = stringifyJson(arena, .{ .selector = sel }) },
|
||||
.scroll => |args| .{ .name = @tagName(Action.scroll), .args_json = stringifyJson(arena, .{ .x = args.x, .y = args.y }) },
|
||||
.hover => |sel| .{ .name = @tagName(Action.hover), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, sel) }) },
|
||||
.select => |args| .{ .name = @tagName(Action.selectOption), .args_json = stringifyJson(arena, .{
|
||||
.selector = substitute(arena, args.selector),
|
||||
.value = substitute(arena, args.value),
|
||||
}) },
|
||||
.check => |args| .{ .name = @tagName(Action.setChecked), .args_json = buildJson(arena, .{
|
||||
.check => |args| .{ .name = @tagName(Action.setChecked), .args_json = stringifyJson(arena, .{
|
||||
.selector = substitute(arena, args.selector),
|
||||
.checked = args.checked,
|
||||
}) },
|
||||
.tree => .{ .name = @tagName(Action.tree), .args_json = "" },
|
||||
.markdown => .{ .name = @tagName(Action.markdown), .args_json = "" },
|
||||
.eval_js => |script| .{ .name = @tagName(Action.eval), .args_json = buildJson(arena, .{ .script = script }) },
|
||||
.eval_js => |script| .{ .name = @tagName(Action.eval), .args_json = stringifyJson(arena, .{ .script = script }) },
|
||||
.extract, .natural_language, .comment, .login, .accept_cookies => null,
|
||||
};
|
||||
}
|
||||
@@ -501,7 +501,7 @@ fn getJsonString(o: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn buildJson(arena: std.mem.Allocator, value: anytype) []const u8 {
|
||||
pub fn stringifyJson(arena: std.mem.Allocator, value: anytype) []const u8 {
|
||||
var aw: std.Io.Writer.Allocating = .init(arena);
|
||||
std.json.Stringify.value(value, .{}, &aw.writer) catch return "{}";
|
||||
return aw.written();
|
||||
|
||||
@@ -23,6 +23,10 @@ pub const ExecResult = struct {
|
||||
failed: bool,
|
||||
};
|
||||
|
||||
/// Caller contract: `cmd` must not be `.natural_language`, `.comment`,
|
||||
/// `.login`, or `.accept_cookies` — those are filtered upstream (see
|
||||
/// `Agent.runRepl`) because they have no tool mapping and would hit the
|
||||
/// `unreachable` arm below.
|
||||
pub fn executeWithResult(self: *Self, arena: std.mem.Allocator, cmd: Command.Command) ExecResult {
|
||||
if (cmd == .extract) return self.execExtract(arena, cmd.extract);
|
||||
|
||||
@@ -64,7 +68,7 @@ fn execExtract(self: *Self, arena: std.mem.Allocator, raw_selector: []const u8)
|
||||
const script = std.fmt.allocPrint(
|
||||
arena,
|
||||
"JSON.stringify(Array.from(document.querySelectorAll({s})).map(el => el.textContent.trim()))",
|
||||
.{Command.buildJson(arena, selector)},
|
||||
.{Command.stringifyJson(arena, selector)},
|
||||
) catch return .{ .output = "failed to build extract script", .failed = true };
|
||||
|
||||
const result = self.tool_executor.callEval(arena, script);
|
||||
|
||||
@@ -67,7 +67,7 @@ fn queryElementProperty(self: *Self, arena: std.mem.Allocator, selector: []const
|
||||
const script = std.fmt.allocPrint(
|
||||
arena,
|
||||
"(function(){{ var el = document.querySelector({s}); return el ? {s} : null; }})()",
|
||||
.{ Command.buildJson(arena, selector), js_property },
|
||||
.{ Command.stringifyJson(arena, selector), js_property },
|
||||
) catch return null;
|
||||
const result = self.tool_executor.callEval(arena, script);
|
||||
if (result.is_error) return null;
|
||||
|
||||
@@ -967,23 +967,22 @@ fn resolveBySelector(session: *lp.Session, selector: []const u8) ToolError!NodeA
|
||||
|
||||
const ParseArgsError = error{ OutOfMemory, InvalidParams };
|
||||
|
||||
/// For tools where every field is optional. Missing args → default `T`;
|
||||
/// wrong-typed args still error (don't silently default).
|
||||
fn parseArgsOrDefault(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) ParseArgsError!T {
|
||||
const args_raw = arguments orelse return .{};
|
||||
return std.json.parseFromValueLeaky(T, arena, args_raw, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {
|
||||
fn parseValue(comptime T: type, arena: std.mem.Allocator, value: std.json.Value) ParseArgsError!T {
|
||||
return std.json.parseFromValueLeaky(T, arena, value, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => error.InvalidParams,
|
||||
};
|
||||
}
|
||||
|
||||
/// For tools where every field is optional. Missing args → default `T`;
|
||||
/// wrong-typed args still error (don't silently default).
|
||||
fn parseArgsOrDefault(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) ParseArgsError!T {
|
||||
return parseValue(T, arena, arguments orelse return .{});
|
||||
}
|
||||
|
||||
/// Required-args parse: missing or malformed both surface as `InvalidParams`.
|
||||
fn parseArgs(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) ParseArgsError!T {
|
||||
const args_raw = arguments orelse return error.InvalidParams;
|
||||
return std.json.parseFromValueLeaky(T, arena, args_raw, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => error.InvalidParams,
|
||||
};
|
||||
return parseValue(T, arena, arguments orelse return error.InvalidParams);
|
||||
}
|
||||
|
||||
pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 {
|
||||
@@ -991,10 +990,10 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8
|
||||
// Pages routinely contain `$5.99`-style content where `$` is incidental.
|
||||
// Lowercase `$lp_…` falls through here too — `std.posix.getenv` is
|
||||
// case-sensitive on Linux, so it would never resolve anyway.
|
||||
if (std.mem.indexOf(u8, input, "$LP_") == null) return input;
|
||||
const first_lp = std.mem.indexOf(u8, input, "$LP_") orelse return input;
|
||||
|
||||
var result: std.ArrayList(u8) = .empty;
|
||||
var i: usize = 0;
|
||||
var i: usize = first_lp;
|
||||
var last_copy: usize = 0;
|
||||
while (std.mem.indexOfScalarPos(u8, input, i, '$')) |dollar| {
|
||||
const var_start = dollar + 1;
|
||||
|
||||
Reference in New Issue
Block a user