Powered by AppSignal & Oban Pro

PTC-Lisp Playground

livebooks/ptc_runner_playground.livemd

PTC-Lisp Playground

# For local dev: run `mix deps.get` in the project root first
repo_root = Path.expand("..", __DIR__)

deps =
  if File.exists?(Path.join(repo_root, "mix.exs")) do
    [{:ptc_runner, path: repo_root}]
  else
    [{:ptc_runner, "~> 0.4.0"}]
  end

Mix.install(deps, consolidate_protocols: false)

Introduction

PTC-Lisp is a small, safe subset of Clojure designed for Programmatic Tool Calling. Programs run in sandboxed BEAM processes with resource limits (1s timeout, 10MB memory).

Key concepts:

  • ->> threads data through a pipeline (like Elixir’s |>)
  • :keyword accesses map fields (converted to string keys internally)
  • where builds predicates for filtering
  • ctx/tool-name calls external tools

Basic Example

Filter expenses and sum amounts:

tools = %{
  "get-expenses" => fn _args ->
    [
      %{"category" => "travel", "amount" => 500},
      %{"category" => "food", "amount" => 50},
      %{"category" => "travel", "amount" => 200}
    ]
  end
}

program = ~S|(->> (ctx/get-expenses) (filter (where :category = "travel")) (sum-by :amount))|

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)

IO.puts("Travel expenses: #{step.return}")

Step-by-step breakdown

  1. (ctx/get-expenses) - calls the tool, returns list of expense maps
  2. (filter (where :category = "travel")) - keeps only travel expenses
  3. (sum-by :amount) - sums the amount field

Working with Variables

Use let to bind intermediate results:

program = ~S"""
(let [expenses (ctx/get-expenses)
      travel (filter (where :category = "travel") expenses)]
  {:count (count travel)
   :total (sum-by :amount travel)
   :avg (avg-by :amount travel)})
"""

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)
step.return

Error Handling

PTC-Lisp provides helpful error messages with hints:

# Missing operator in where clause
bad_program = ~S|(filter (where :status "active") items)|

case PtcRunner.Lisp.run(bad_program, tools: %{}) do
  {:error, error} -> IO.puts("Error: #{inspect(error)}")
  {:ok, step} -> step.return
end
# Type error - sum-by needs a collection
bad_program = ~S|(sum-by :amount "not a list")|

case PtcRunner.Lisp.run(bad_program, tools: %{}) do
  {:error, error} -> IO.puts("Error: #{inspect(error)}")
  {:ok, step} -> step.return
end

Advanced: Data Transformation

Transform and join data from multiple sources:

tools = %{
  "get-users" => fn _args ->
    [
      %{"id" => 1, "name" => "Alice", "email" => "alice@example.com"},
      %{"id" => 2, "name" => "Bob", "email" => "bob@example.com"}
    ]
  end,
  "get-orders" => fn _args ->
    [
      %{"user-id" => 1, "product" => "Laptop", "total" => 1200},
      %{"user-id" => 2, "product" => "Mouse", "total" => 25},
      %{"user-id" => 1, "product" => "Keyboard", "total" => 150}
    ]
  end
}

program = ~S"""
(let [users (ctx/get-users)
      orders (ctx/get-orders)
      high-value (filter (where :total > 100) orders)]
  (->> high-value
       (mapv (fn [order]
               (let [user (find (where :id = (:user-id order)) users)]
                 {:customer (:name user)
                  :product (:product order)
                  :total (:total order)})))))
"""

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)
step.return

Advanced: Grouping and Aggregation

Group expenses by category and compute totals:

expenses_tools = %{
  "get-expenses" => fn _args ->
    [
      %{"category" => "travel", "amount" => 500},
      %{"category" => "food", "amount" => 50},
      %{"category" => "travel", "amount" => 200},
      %{"category" => "food", "amount" => 75}
    ]
  end
}

program = ~S"""
(let [expenses (ctx/get-expenses)
      by-category (group-by :category expenses)]
  (->> (keys by-category)
       (mapv (fn [cat]
               {:category cat
                :total (sum-by :amount (get by-category cat))
                :count (count (get by-category cat))}))))
"""

{:ok, step} = PtcRunner.Lisp.run(program, tools: expenses_tools)
step.return

Learn More