diff options
| author | t <t@tjp.lol> | 2026-07-07 11:26:32 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-07-07 11:26:45 -0600 |
| commit | f83578fdc9264019a1a1cef8c5484a161167d3dd (patch) | |
| tree | 888f11767f944d61e5ca8eb92fa1b2dba295a4b8 /examples | |
initial commit, moved libpanto over from the pantograph repo
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/simple-agent-go/go.mod | 7 | ||||
| -rw-r--r-- | examples/simple-agent-go/main.go | 152 | ||||
| -rw-r--r-- | examples/simple-agent.lua | 103 |
3 files changed, 262 insertions, 0 deletions
diff --git a/examples/simple-agent-go/go.mod b/examples/simple-agent-go/go.mod new file mode 100644 index 0000000..376a3c9 --- /dev/null +++ b/examples/simple-agent-go/go.mod @@ -0,0 +1,7 @@ +module simple-agent-go + +go 1.24 + +require code.tjp.lol/libpantograph.git/libpanto-go v0.0.0 + +replace code.tjp.lol/libpantograph.git/libpanto-go => ../../libpanto-go diff --git a/examples/simple-agent-go/main.go b/examples/simple-agent-go/main.go new file mode 100644 index 0000000..8a02142 --- /dev/null +++ b/examples/simple-agent-go/main.go @@ -0,0 +1,152 @@ +// A tiny standalone libpanto-go app: one prompt in, streamed text out. +// +// Run with: +// +// PANTO_API_STYLE=openai_chat|anthropic_messages \ +// PANTO_API_KEY=... \ +// PANTO_BASE_URL=... \ +// PANTO_MODEL=... \ +// go run examples/simple-agent-go/main.go "list the files" + +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + + panto "code.tjp.lol/libpantograph.git/libpanto-go" +) + +func main() { + panto.Init() + defer panto.Deinit() + + apiStyle := env("PANTO_API_STYLE", "openai_chat") + apiKey := requireEnv("PANTO_API_KEY") + baseURL := requireEnv("PANTO_BASE_URL") + model := requireEnv("PANTO_MODEL") + maxTokens := uint32(4096) + if v := os.Getenv("PANTO_MAX_TOKENS"); v != "" { + fmt.Sscanf(v, "%d", &maxTokens) + } + + prompt := strings.Join(os.Args[1:], " ") + if prompt == "" { + fmt.Fprintln(os.Stderr, "usage: simple-agent-go <prompt>") + os.Exit(2) + } + + var cfg panto.Config + if strings.HasPrefix(apiStyle, "anthropic") { + cfg.Provider = panto.ProviderConfig{ + AnthropicMessages: &panto.AnthropicMessagesConfig{ + APIKey: apiKey, + BaseURL: baseURL, + Model: model, + MaxTokens: maxTokens, + }, + } + } else { + cfg.Provider = panto.ProviderConfig{ + OpenAIChat: &panto.OpenAIChatConfig{ + APIKey: apiKey, + BaseURL: baseURL, + Model: model, + MaxTokens: maxTokens, + }, + } + } + + agent, err := panto.NewAgent(cfg) + if err != nil { + panic(err) + } + defer agent.Close() + + agent.SetSystemPrompt("You are a very small coding agent.\nUse the bash tool when you need to inspect or change the local workspace.\nKeep final answers concise.") + + agent.RegisterTool(panto.Tool{ + Decl: panto.ToolDecl{ + Name: "bash", + Description: "Run a shell command with bash -lc and return combined stdout/stderr plus the exit code.", + SchemaJSON: `{ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The command to run." + } + }, + "required": ["command"] + }`, + }, + Invoke: func(input string) (panto.ResultParts, error) { + var params map[string]any + if err := json.Unmarshal([]byte(input), ¶ms); err != nil { + return nil, fmt.Errorf("invalid input: %w", err) + } + command, ok := params["command"].(string) + if !ok || command == "" { + return nil, fmt.Errorf("bash.command must be a non-empty string") + } + + out, err := runBash(command) + if err != nil { + return nil, err + } + return panto.ResultParts{{Text: out}}, nil + }, + }) + + stream, err := agent.Run(panto.UserMessage(panto.TextBlock(prompt))) + if err != nil { + panic(err) + } + defer stream.Close() + + for ev := range stream.Iter() { + switch ev := ev.(type) { + case panto.ContentDeltaEvent: + fmt.Print(ev.Delta) + os.Stdout.Sync() + case panto.ToolDispatchStartEvent: + fmt.Fprintf(os.Stderr, "\n[bash: running %d tool call(s)]\n", ev.Count) + case panto.ToolDispatchCompleteEvent: + fmt.Fprint(os.Stderr, "[bash: done]\n") + } + } + if err := stream.Err(); err != nil { + panic(err) + } + fmt.Println() +} + +func env(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func requireEnv(key string) string { + v := os.Getenv(key) + if v == "" { + panic("missing required env var: " + key) + } + return v +} + +func runBash(command string) (string, error) { + cmd := exec.Command("bash", "-lc", command) + out, err := cmd.CombinedOutput() + status := "unknown" + if exitErr, ok := err.(*exec.ExitError); ok { + status = fmt.Sprintf("%d", exitErr.ExitCode()) + } else if err != nil { + return "", err + } + return fmt.Sprintf("exit_code=%s\n%s", status, string(out)), nil +} diff --git a/examples/simple-agent.lua b/examples/simple-agent.lua new file mode 100644 index 0000000..baf00b5 --- /dev/null +++ b/examples/simple-agent.lua @@ -0,0 +1,103 @@ +#!/usr/bin/env lua +-- A tiny standalone libpanto-lua app: one prompt in, streamed text out. +-- +-- Run with Lua 5.4 after building/installing libpanto-lua, for example: +-- LUA_CPATH='./libpanto-lua/zig-out/lib/?.so;;' lua examples/simple-agent.lua "list the files" +-- +-- Required environment: +-- PANTO_API_STYLE=openai_chat|anthropic_messages +-- PANTO_API_KEY=... +-- PANTO_BASE_URL=... +-- PANTO_MODEL=... + +local panto = require("panto") + +local function getenv(name, default) + local value = os.getenv(name) + if value == nil or value == "" then return default end + return value +end + +local function require_env(name) + local value = getenv(name) + if value == nil then error("missing required env var: " .. name, 0) end + return value +end + +local function shell_quote(s) + return "'" .. tostring(s):gsub("'", "'\\''") .. "'" +end + +local function read_all(pipe) + local chunks = {} + while true do + local chunk = pipe:read(8192) + if chunk == nil then break end + chunks[#chunks + 1] = chunk + end + return table.concat(chunks) +end + +local function run_bash(command) + local wrapped = "bash -lc " .. shell_quote(command) .. " 2>&1; printf '\\n__panto_exit_code:%s\\n' $?" + local pipe = assert(io.popen(wrapped, "r")) + local output = read_all(pipe) + pipe:close() + + local body, code = output:match("^(.*)\n__panto_exit_code:(%-?%d+)\n$") + return string.format("exit_code=%s\n%s", code or "unknown", body or output) +end + +local prompt = table.concat(arg, " ") +if prompt == "" then + io.stderr:write("usage: lua examples/simple-agent.lua <prompt>\n") + os.exit(2) +end + +local agent = panto.agent { + api_style = require_env("PANTO_API_STYLE"), + api_key = require_env("PANTO_API_KEY"), + base_url = require_env("PANTO_BASE_URL"), + model = require_env("PANTO_MODEL"), + max_tokens = tonumber(getenv("PANTO_MAX_TOKENS", "4096")), +} + +agent:set_system_prompt(table.concat({ + "You are a very small coding agent.", + "Use the bash tool when you need to inspect or change the local workspace.", + "Keep final answers concise.", +}, "\n")) + +agent:register_tool { + name = "bash", + description = "Run a shell command with bash -lc and return combined stdout/stderr plus the exit code.", + schema = { + type = "object", + properties = { + command = { + type = "string", + description = "The command to run.", + }, + }, + required = { "command" }, + }, + handler = function(input) + if type(input.command) ~= "string" or input.command == "" then + error("bash.command must be a non-empty string") + end + return run_bash(input.command) + end, +} + +for ev in agent:run(prompt):events() do + if ev.type == "content_delta" then + io.write(ev.delta) + io.flush() + elseif ev.type == "tool_dispatch_start" then + io.stderr:write(string.format("\n[bash: running %d tool call(s)]\n", ev.count)) + elseif ev.type == "tool_dispatch_complete" then + io.stderr:write("[bash: done]\n") + end +end + +io.write("\n") |
