DEV Community

Mackross
Mackross

Posted on

Instruct LLMs to do what you want in Ruby ❤️

Introducing Instruct

https://github.com/instruct-rb/instruct

Instruct was inspired by Microsoft guidance with its natural interweaving of code and LLM completions, but it’s got Ruby flair and it’s own unique features.

Here’s just one example of how you can use instruct to easily create a multi-turn agent conversations.

  # Create two agents: Noel Gallagher and an interviewer with a system prompt.
  noel = p.system{"You're Noel Gallagher. Answer questions from an interviewer."}
  interviewer = p.system{"You're a skilled interviewer asking Noel Gallagher questions."}

  # We start a dynamic Q&A loop with the interviewer by kicking off the
  # interviewing agent and capturing the response under the :reply key.
  interviewer << p.user{"__Noel sits down in front of you.__"} + gen.capture(:reply)

  puts interviewer.captured(:reply) # => "Hello Noel, how are you today?"

  5.times do
    # Noel is sent the last value captured in the interviewer's transcript under the :reply key.
    # Similarly, we generate a response for Noel and capture it under the :reply key.
    noel << p.user{"<%= interviewer.captured(:reply) %>"} + gen.capture(:reply, list: :replies)

    # Noel's captured reply is now sent to the interviewer, who captures it in the same way.
    interviewer << p.user{"<%=  noel.captured(:reply) %>"} + gen.capture(:reply, list: :replies)
  end

  # After the conversation, we can access the list captured replies from both agents
  noel_said = noel.captured(:replies).map{ |r| "noel: #{r}" }
  interviewer_said = interviewer.captured(:replies).map{ |r| "interviewer: #{r}" }

  puts interviwer_said.zip(noel_said).flatten.join("\n\n")
  # => "noel: ... \n\n interviewer: ..., ..."

Enter fullscreen mode Exit fullscreen mode

I’ve been working on this gem part-time for a few months now. The API is not yet fully stable so I wouldn’t recommend for anything other than experimenting. Nevertheless, my company is using it in production (as of today :)), so it seemed like a nice time to share.

So why did I write yet another LLM prompting library?

I found the existing Ruby ones either too abstract — hiding the LLM’s capabilities behind unseen prompts, too low-level — leaving my classes hard to follow and littered with boilerplate managing prompts and responses, or they used class level abstractions — forcing me to create classes when I didn’t want to.

After reading an early version of Patterns of Application Development Using AI by Obie Fernandez and using Obie’s library raix, I felt inspired. The book has many great patterns, and raix’s transcript management and tool management were the first I’d used that felt ruby-ish. At the same time libraries in the python community such as guidance, DSPy, LangSmith, and TEXTGRAD had caught my eye. I also liked what the cross-platform BAML was doing too. I didn’t love the code generation and freemium aspects.

With motivation high, I set out to build an opinionated library of gems that improves my Ruby (and Rails) LLM developer experience.

The first gem is instruct. It is the flexible foundation that the other gems will build on. While the API is similar to guidance, it has a different architecture based around attributed strings and middleware which enables some unique features (like async guard rails, content filters, self-healing, auto-continuation, and native multi-modal support).

I’m currently working on a hopefully elegant API that makes requesting and handling streaming structured output easy (taking inspiration from BAML, but with automatic upgrades to json schema if the API supports it). Along with that, I’ve been working on a conversational memory middleware that automatically prunes historic irrelevant bits of the conversation transcript. I hope this keeps the model more steerable, but without loss of crucial details.

Thanks in advance for taking a look and providing any constructive feedback or ideas. Lastly, if you’re interested in contributing, please message me.

Top comments (0)