GitHub - pcarolan/magic: An LLMs best friend.

6 min read Original article โ†—

๐Ÿช„ Magic

OOLLM: Think it, call it, chain it: Magic lets you call and compose any method you can think of using fluent Ruby.

Tests

Setup

  1. Set OPENAI_API_KEY to a valid openai key
  2. Install ruby >= ruby 3.3.4 (that's it! no other dependencies)

Usage

  1. Run tests ruby test_magic.rb to make sure everything's working properly
  2. Run an interactive session irb (from the root directory)

Roadmap

  • single method execution with and without arguments
  • method chaining with and without arguments
  • strong types
  • recursion
  • caching

Try it

$ irb
DEBUG is set to true
magic is ready ๐Ÿช„, type `@magic = Magic.new` to get started

>> @magic = Magic.new
>> @magic.emoji_getter('happy camper')

๐Ÿ”ฎ Step 1: emoji_getter("happy camper")
   โ†’ "๐Ÿ•๏ธ๐Ÿ™‚"
=> ๐Ÿ•๏ธ๐Ÿ™‚

Examples

Single Method Calls

@magic = Magic.new

result = @magic.random_number(0..1000)
result
=> 746

result.class
=> Magic


# Other examples:
@magic.state_capital('Michigan', 'USA')
=> Lansing

@magic.types_of_cheese_in_geo('world')
=> Cheddar, Mozzarella, Parmesan, Brie, Gouda, Swiss, Blue, Feta, Provolone, Monterey Jack, Camembert, Colby, Havarti, Manchego, Ricotta, Gorgonzola, Gruyรจre, Roquefort, Emmental, Asiago

@magic.types_of_cheese_in_geo('france')
=> Brie, Camembert, Roquefort, Comtรฉ, Reblochon, Munster, Emmental, Beaufort, Cantal, Saint-Nectaire, Pont-lโ€™ร‰vรชque, Livarot, Chรจvre (various goat cheeses), Tomme de Savoie, Bleu dโ€™Auvergne, Bleu de Gex, Boursin, Neufchรขtel, Ossau-Iraty, Morbier

Method Chaining

Magic enables fluent API method chaining. Each method call makes an immediate API request and passes the previous result as context to the next call.

Example 1: Chaining Two Methods

>> result = @magic.random_number.multiply_by(5)

๐Ÿ”ฎ Step 1: random_number
   โ†’ "271"

๐Ÿ”ฎ Step 2: multiply_by(5)
   โ†’ "1355"
=> 1355

puts "History length: #{result.instance_variable_get(:@history).length} steps"
# => History length: 2 steps

Example 2: Chaining Three Methods

>> result = @magic.random_number.multiply_by(5).add(10)

๐Ÿ”ฎ Step 1: random_number
   โ†’ "70628"

๐Ÿ”ฎ Step 2: multiply_by(5)
   โ†’ "353140"

๐Ÿ”ฎ Step 3: add(10)
   โ†’ "353150"
=> 353150

Example 3: Accessing Intermediate Results

>> step1 = @magic.get_number

๐Ÿ”ฎ Step 1: get_number
   โ†’ "42"
=> 42
>> puts "Step 1 result: #{step1.result}"
Step 1 result: 42

>> step2 = step1.double_it

๐Ÿ”ฎ Step 2: double_it
   โ†’ "84"
=> 84

>> step3 = step2.add(100)

๐Ÿ”ฎ Step 3: add(100)
   โ†’ "184"
=> 184

>> puts "Full chain inspect: #{step3.inspect}"
Full chain inspect: #<Magic history=3 steps, result="184">

Key Features:

  • Each method call makes an immediate API request
  • Previous result is passed as context to next call
  • Magic instances are immutable (functional style)
  • Auto-executes on puts/string interpolation via to_s
  • Access raw result via .result method
  • View chain history via .inspect method

Pipeline Processing & Nested Data Navigation

Magic enables powerful data transformation pipelines through its context-aware chaining. Magic's chaining allows for sequential transformations where each step receives context from previous operations.

Example 1: Data Pipeline Processing

# Transform data through multiple steps
>> result = @magic.list_us_presidents
>>   .take_first(5)
>>   .get_birthplaces
>>   .find_common_state

๐Ÿ”ฎ Step 1: list_us_presidents
   โ†’ "George Washington\nJohn Adams\nThomas Jefferson\nJames Madison\nJames Monroe\nJohn Quincy Adams\nAndrew Jackson\nMartin Van Buren\nWilliam Henry Harrison\nJohn Tyler\nJames K. Polk\nZachary Taylor\nMillard Fillmore\nFranklin Pierce\nJames Buchanan\nAbraham Lincoln\nAndrew Johnson\nUlysses S. Grant\nRutherford B. Hayes\nJames A. Garfield\nChester A. Arthur\nGrover Cleveland\nBenjamin Harrison\nGrover Cleveland\nWilliam McKinley\nTheodore Roosevelt\nWilliam Howard Taft\nWoodrow Wilson\nWarren G. Harding\nCalvin Coolidge\nHerbert Hoover\nFranklin D. Roosevelt\nHarry S. Truman\nDwight D. Eisenhower\nJohn F. Kennedy\nLyndon B. Johnson\nRichard Nixon\nGerald Ford\nJimmy Carter\nRonald Reagan\nGeorge H. W. Bush\nBill Clinton\nGeorge W. Bush\nBarack Obama\nDonald Trump\nJoe Biden"

๐Ÿ”ฎ Step 2: take_first(5)
   โ†’ "George Washington\nJohn Adams\nThomas Jefferson\nJames Madison\nJames Monroe"

๐Ÿ”ฎ Step 3: get_birthplaces
   โ†’ "Westmoreland County, Virginia\nBraintree, Massachusetts\nShadwell, Virginia\nPort Conway, Virginia\nWestmoreland County, Virginia"

๐Ÿ”ฎ Step 4: find_common_state
   โ†’ "Virginia"
=> Virginia

Example 2: Nested Object Navigation

# Drill down through nested data structures
>> result = @magic.countries_in('Europe')
>>   .get_details('France')
>>   .largest_city
>>   .population

๐Ÿ”ฎ Step 1: countries_in("Europe")
   โ†’ "Albania\nAndorra\nArmenia\nAustria\nAzerbaijan\nBelarus\nBelgium\nBosnia and Herzegovina\nBulgaria\nCroatia\nCyprus\nCzechia\nDenmark\nEstonia\nFinland\nFrance\nGeorgia\nGermany\nGreece\nHungary\nIceland\nIreland\nItaly\nKazakhstan\nKosovo\nLatvia\nLiechtenstein\nLithuania\nLuxembourg\nMalta\nMoldova\nMonaco\nMontenegro\nNetherlands\nNorth Macedonia\nNorway\nPoland\nPortugal\nRomania\nRussia\nSan Marino\nSerbia\nSlovakia\nSlovenia\nSpain\nSweden\nSwitzerland\nTurkey\nUkraine\nUnited Kingdom\nVatican City"

๐Ÿ”ฎ Step 2: get_details("France")
   โ†’ "Capital: Paris\nPopulation: ~68 million\nOfficial language: French\nGovernment: Unitary semi-presidential republic\nCurrency: Euro (โ‚ฌ)\nArea: ~551,700 kmยฒ (metropolitan France)\nContinent/Region: Western Europe\nEU member: Yes (founding member)\nNotable: Largest country in the EU by area; major global center for culture, fashion, cuisine, and wine; key role in EU and international politics."

๐Ÿ”ฎ Step 3: largest_city
   โ†’ "Paris"

๐Ÿ”ฎ Step 4: population
   โ†’ "2165423"
=> 2165423

Example 3: Computational Pipelines

# Chain mathematical operations
>> result = @magic.factorial(5)
>>   .multiply_by(2)
>>   .add(10)

๐Ÿ”ฎ Step 1: factorial(5)
   โ†’ "120"

๐Ÿ”ฎ Step 2: multiply_by(2)
   โ†’ "240"

๐Ÿ”ฎ Step 3: add(10)
   โ†’ "250"
=> 250

Example 4: Context-Aware Operations

# Operations that can reference previous context
>> result = @magic.number(10)
>>   .double_it      # 10 * 2 = 20
>>   .add_previous   # 20 + 10 = 30 (LLM can access original context)
>>   .square         # 30^2 = 900

๐Ÿ”ฎ Step 1: number(10)
   โ†’ "10"

๐Ÿ”ฎ Step 2: double_it
   โ†’ "20"

๐Ÿ”ฎ Step 3: add_previous
   โ†’ "20"

๐Ÿ”ฎ Step 4: square
   โ†’ "400"
=> 400

Webserver / example page

There is a tiny example webserver (server.rb) and an ERB template (index.html.erb) that demonstrates embedding Magic output into a web page.

  • server: server.rb โ€” a small WEBrick server (port 3000) that renders index.html.erb.
  • template: index.html.erb โ€” calls @magic.generate_html(...) and inserts the returned HTML into the page.

How to run

# set your OpenAI key first (required)
export OPENAI_API_KEY="sk-..."

# start the example server
ruby server.rb

# then open http://localhost:3000 in your browser

What the template does

The index.html.erb file demonstrates a simple call to the Magic object:

<%= @magic.generate_html(
  tag: 'body', 
  theme: 'wu tang clan', 
  mode: 'dark',
  generate_content: "fan fiction",
  looks_like: "wu tang clan"
) %>

The example passes a few options to generate_html โ€” these are just demonstration inputs (tag, visual theme, content type and style). Magic will return HTML (a string) that the template inserts into the page. This is a minimal demo of using Magic results inside a web UI โ€” no frameworks or external gems required (only Ruby standard library: WEBrick + ERB).

alt text Notes

  • The server makes real LLM requests, so ensure OPENAI_API_KEY is set and be mindful of API usage.
  • This example is intentionally minimal โ€” use it as a starting point for building a small experiment or integrating Magic into your own web view.

How It Works

Magic's pipeline processing uses sequential execution with context passing:

  1. Context Passing: Each chained call receives the previous result in the prompt:

    # Previous result is automatically included
    "Previous result: {previous_value}"
  2. Chain History: The @history array maintains a complete audit trail of all operations

  3. LLM Reasoning: The LLM can reason about nested structures and relationships since it has access to full context

  4. Sequential Execution: Each step:

    • Executes independently: Makes its own API call
    • Receives context: Gets previous result as input
    • Passes forward: Sends result to next step
    • LLM-powered: Intelligence comes from the LLM understanding data relationships

References