{"id":23721902,"url":"https://github.com/instruct-rb/instruct","last_synced_at":"2025-09-03T22:31:44.109Z","repository":{"id":261569366,"uuid":"884687895","full_name":"instruct-rb/instruct","owner":"instruct-rb","description":"Instruct LLMs to do what you want in Ruby","archived":false,"fork":false,"pushed_at":"2025-02-07T04:04:28.000Z","size":251,"stargazers_count":51,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"development","last_synced_at":"2025-08-15T06:55:32.665Z","etag":null,"topics":["ai","gem","ruby","rubyml"],"latest_commit_sha":null,"homepage":"https://instruct-rb.com","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/instruct-rb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-07T07:59:11.000Z","updated_at":"2025-05-30T10:24:40.000Z","dependencies_parsed_at":"2024-12-19T02:27:33.256Z","dependency_job_id":"f9e5c153-03b7-4588-afff-6c2597e55106","html_url":"https://github.com/instruct-rb/instruct","commit_stats":null,"previous_names":["mackross/instruct-rb","instruct-rb/instruct"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/instruct-rb/instruct","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instruct-rb%2Finstruct","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instruct-rb%2Finstruct/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instruct-rb%2Finstruct/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instruct-rb%2Finstruct/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/instruct-rb","download_url":"https://codeload.github.com/instruct-rb/instruct/tar.gz/refs/heads/development","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instruct-rb%2Finstruct/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273522950,"owners_count":25120857,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-03T02:00:09.631Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","gem","ruby","rubyml"],"created_at":"2024-12-30T23:01:16.879Z","updated_at":"2025-09-03T22:31:43.837Z","avatar_url":"https://github.com/instruct-rb.png","language":"Ruby","readme":"# Instruct\n*Instruct LLMs to do what you want in Ruby.*\n\nCombine **code**, **prompts**, and **completions** in a natural and intuitive\nway for programmers. Inspired by libraries like\n[Guidance](https://github.com/guidance-ai/guidance) and rack, Instruct strips\naway boilerplate code while providing a flexible and powerful interface that\ndoesn't abstract away control over the LLM.\n\n\n## Features\n\n* **Natural and Intuitive API**\n\n  Using LLMs with instruct is not too different from plain old string\n  manipulation. This lets you think about your prompts and completions in a way\n  that intuitively makes sense to most programmers.\n* **Safe Prompting**\n\n  The ERB `#p{}`rompt helper can be used to generate prompts with dynamic input in an\n  familiar way. Dynamic input is automatically marked as unsafe and can be\n  handled differently by middleware (for example to check for prompt\n  injections). Use `.prompt_safe` to mark part of the prompt as safe.\n* **Flexible Middleware Stack**\n\n  Middleware can be used to add features like structured output, conversation\n  pruning, RAG integrations, retries, auto-continuation, guard-rails, monitoring\n  and more. The middleware stack also provides a common way to transform a prompt for\n  different LLM models with different capabilities.\n* **Streaming Support**\n\n  Both middleware and callers can process completion responses as the chunks\n  arrive. This can be used to display a completion in real time, or to validate\n  or parse the output of an LLM call as it's being generated.\n* **Rails Integration**\n\n  Prompts, completions and models can be serialized and stored on ActiveRecord\n  with custom attributes and will automatically serialize when passed to an\n  ActiveJob. Enabling easy background processing of LLM calls.\n\n---\n🏗️ **This gem is still undergoing active development and is not yet ready for use\nbeyond experimentation.**\n\nI'm making this public to get feedback and to see if there is any interest in\nfrom the community to help develop this further.\n\n---\n## Usage\n\n### The gen function\n\n`gen` is used to **gen**erate completions from an LLM.\n\n### Simple Usage\n\nGetting a single completion from an LLM is as simple as calling `gen` with a prompt.\n\nWhen a prompt is a present as the first argument the gen call immediately\nretrieves the completion from the LLM.\n\nThe model can be set with the model keyword argument if no default model has been\nset. Similarly all arguments that `ruby-openai` and `anthropic` can be configured with\ncan be passed into the gen call.\n```ruby\ncompletion = gen(\"The capital of France is \", stop_chars: \"\\n ,.\", model: 'gpt-3-5-turbo-instruct')\n\nputs completion # =\u003e \"Paris\"\n```\n\n### Deferred Completions\n\nThe gen function can also create deferred completions. This is used to create\nprompts that can be called multiple times or passed around as an argument.\n\n```ruby\n\n  # Adding gen to a string creates a deferred completion. This is indicated by\n  # the 💬 emoji.\n  prompt = \"The capital of France is \" + gen(stop_chars: \"\\n ,;.\") + \"!\"\n\n  puts prompt # =\u003e \"The capital of France is 💬!\"\n\n  # Each time the prompt is called, a new completion is generated and returned.\n  completion = prompt.call\n\n  puts completion # =\u003e \"Paris\"\n\n  # When a completion is added to the prompt that generated it, a new\n  # prompt is created with the completion replacing the deferred completion.\n  puts prompt + completion # =\u003e \"The capital of France is Paris!\"\n\n  # Note the exclamation mark is still present and comes after the completion.\n```\n\n### Appending to an existing prompt\n\nThe double angle bracket operator `\u003c\u003c` can be used to quickly append objects\nto a prompt. This can be used to modify and build up a prompt in place.\n\nUnlike the `+=` and `concat` operators, the `\u003c\u003c` operator will immediately call any\ndeferred completions and append them to the prompt.\n\n```ruby\n  string = Instruct::Prompt.new\n  string \u003c\u003c \"The capital of France is \"\n  string \u003c\u003c gen(stop_chars: \"\\n ,;.\") + \"!\"\n\n  puts string # =\u003e \"The capital of France is Paris!\"\n\n  string \u003c\u003c \" It is widely known for \" + gen(stop_chars: \".\") + \".\"\n  puts string # =\u003e \"The capital of France is Paris! It is widely known for its fashion, art and culture.\"\n```\n\n### Capturing Generated Completion\n\nBecause it's quite common to want to access a completion, but not break apart\nthe prompt and completion into separate components, instruct provides `capture`\ncaptures the result of a completion from a deferred generation and makes it\naccessible from the prompt with `captured`.\n\n```ruby\n  string = Instruct::Prompt.new\n  string \u003c\u003c \"The capital of France is \" + gen(stop: '\\n','.').capture(:capital)\n\n  puts string.captured(:capital) # =\u003e \"Paris\"\n```\n\nPassing a `list: :key` keyword argument will capture an array of completions under the same key.\n\n### Creating a Prompt Transcript\n\nMost modern LLMs are designed for conversational style completions. The chat\ncompletion middleware transforms a prompt formatted like a transcript into an\nobject that can be used with these APIs.\n```ruby\n  # Roles can be added to a prompt transcript by a new line with the role name\n  # followed by a colon and then a space.\n  transcript = p{\"\n    system: You're an expert geographer that speaks only French\n    user: What is the capital of Australia?\n  \"} + gen(prompt, stop_chars: \"\\n ,;.\", model: 'gpt-4o')\n\n\n  # Note the returned or captured completion does not include any role prefix.\n  completion = transcript.call\n  puts completion # =\u003e \"le capital de l'Australie est Canberra\"\n\n  # However, when the completion is added to the transcript, the `assistant: `\n  # prefix is automatically prepended (if required), enabling a new user prompt\n  # to be appended immediately after.\n  puts transcript + completion\n  # =\u003e \"system: You're an expert geographer that speaks only French\n  #     user: What is the capital of Australia?\n  #     assistant: le capital de l'Australie est Canberra\"\n```\n\nIf you want to be more explicit about adding roles in to a prompt, instruct provides\n`#p.system`, `#p.user`, and `#p.assistant` helper methods. There is nothing\nspecial about these methods, they just prepend the role prefix to the string\n\n```ruby\n  transcript = p.system{\"You're an expert geographer that speaks only French\"}\n  transcript += p.user{\"What is the capital of Australia?\"}\n  transcript += gen(stop_chars: \"\\n ,;.\", model: 'gpt-4o')\n```\n\n### The p(rompt) Block ERB Helper\n\n`#p{}` (shown above) allows for dynamic prompt templating using ERB tags\n`\u003c%= %\u003e` with automatic handling of safe and unsafe content similar to HTML\ntemplating.\n\nThis safety mechanism provides a way for both programmer and middleware to tell\nthe difference between user, LLM, and prompt template text. In the case of the\nChat Completion Middleware, role switches cannot occur in unsafe text.\n\nSimilarly, guard middleware might be added to check unsafe content for prompt\ninjections or innapropriate content.\n\nERB heredoc blocks combined with the `p` helper provide syntax highlighting\nin most editors making long dynamic prompts easy to read. The following prompt\nshows how to use a chomped ERB heredoc to generate larger prompts with both\n\"safe\" and \"unsafe\" content.\n\n```ruby\n  p = p{\u003c\u003c~ERB.chomp\n      This is a longer prompt, if the included content might include\n      injections use the normal ERB tags like so: \u003c%= unsafe_user_content %\u003e.\n\n      If we know that something doesn't include prompt injections, add it\n      as: \u003c%= raw some_safe_content %\u003e, #{some_safe_content} or \u003c%=\n      some_safe_string.prompt_safe %\u003e.\n\n      By default generated LLM responses as \u003c%= gen %\u003e or #{ gen } will be added\n      to the prompt as unsafe. To add it as safe we cannot use the ERB\n      method, we instead need to call .prompt_safe on the completion befored\n      appending it.\n    ERB\n  }\n```\n\nNote that if you call `#p` with without a block and with arguments, it will pass\nthrough to `Kernel#p`. That is you can still use `p obj` to inspect objects.\n\nAlternatively, if you don't include the `Instruct::Helpers` module, you can use\nthe module directly with `Instruct.p`.\n\n\n### A More Complex Example: Multi-Turn Conversations Between Agents\n\nHere we put together all the features so far to show how you can easily manage multi-turn\ninteractions between two different agents.\n\n```ruby\n  # Create two agents: Noel Gallagher and an interviewer with a system prompt.\n  noel = p.system{\"You're Noel Gallagher. Answer questions from an interviewer.\"}\n  interviewer = p.system{\"You're a skilled interviewer asking Noel Gallagher questions.\"}\n\n  # We start a dynamic Q\u0026A loop with the interviewer by kicking off the\n  # interviewing agent and capturing the response under the :reply key.\n  interviewer \u003c\u003c p.user{\"__Noel sits down in front of you.__\"} + gen.capture(:reply)\n\n  puts interviewer.captured(:reply) # =\u003e \"Hello Noel, how are you today?\"\n\n  5.times do\n    # Noel is sent the last value captured in the interviewer's transcript under the :reply key.\n    # Similarly, we generate a response for Noel and capture it under the :reply key.\n    noel \u003c\u003c p.user{\"\u003c%= interviewer.captured(:reply) %\u003e\"} + gen.capture(:reply, list: :replies)\n\n    # Noel's captured reply is now sent to the interviewer, who captures it in the same way.\n    interviewer \u003c\u003c p.user{\"\u003c%=  noel.captured(:reply) %\u003e\"} + gen.capture(:reply, list: :replies)\n  end\n\n  # After the conversation, we can access the list captured replies from both agents\n  noel_said = noel.captured(:replies).map{ |r| \"noel: #{r}\" }\n  interviewer_said = interviewer.captured(:replies).map{ |r| \"interviewer: #{r}\" }\n\n  puts interviwer_said.zip(noel_said).flatten.join(\"\\n\\n\")\n  # =\u003e \"noel: ... \\n\\n interviewer: ..., ...\"\n```\n\n## The Prompt\nThe following examples illustrate how the Prompt can be manipulated in\ndifferent ways.\n``` ruby\n  Instruct::Prompt.new \u003c\u003c \"The capital of France is\" + gen(stop: '\\n','.')\n  # =\u003e \"The capital of France is Paris\"\n\n  prompt = \"The capital of France is\" + gen(stop: '\\n','.')\n  # =\u003e \"The capital of France is 💬\"\n  # Note that a prompt can just be created by adding a string and a gen\n  # call. However, the gen call is deferred (as indicated by the 💬).\n\n  prompt.class\n  # =\u003e Instruct::Prompt\n\n  result = prompt.call do |response|\n    # This optional block on the call method can be used for streaming\n    # The response is called after each chunk is processed by the middleware,\n    # the response is the entire buffer so Paris as three chunks might look like\n    # \"P\", \"Par\", \"Paris\". It's possible that middleware could change the\n    # response, so it's best not to treat these as final until after the call is\n    # finished.\n  end\n  # =\u003e \"Paris\"\n\n  result.class\n  # =\u003e Instruct::Prompt::Completion\n\n  result.prompt\n  # =\u003e \"The capital of France is 💬\"\n\n\n  result.prompt == prompt\n  # =\u003e true\n\n  together = prompt + result\n  # =\u003e \"The capital of France is Paris\"\n  # Adding a completion to a prompt will return the prompt with the completion appended.\n  # If this completion was generated using the same prompt, it will also update the prompts\n  # content with any additional changes that were made by middleware during\n  # the call that produced the completion. This includes transferring the captured values.\n\n  together.class\n  # =\u003e Instruct::Prompt\n\n  # This does nothing as there are no deferred calls.\n  together.call\n  # =\u003e nil\n\n  prompt = \"The capital of Germany is\" + gen(stop: '\\n','.') + \", which is in the region of \" + gen(stop: '\\n','.')\n  # =\u003e \"The capital of Germany is 💬, which is in the region of 💬\"\n\n  result = prompt.call\n  # =\u003e [ \"Berlin\", \"Europe\" ] # Array\u003cInstruct::Prompt::Completion\u003e\n\n  new_prompt == Instruct::Serializer.load(Instruct::Serializer.dump(prompt))\n  # =\u003e \"The capital of Germany is 💬, which is in the region of 💬\"\n\n  new_prompt == prompt\n  # =\u003e true\n\n  # The results are joined together with the prompt in the order they were returned.\n  together = new_prompt + result\n  # =\u003e \"The capital of Germany is Berlin, which is in the region of Europe\"\n\n  # The interpolation only occurs if the prompt that generated the completion(s)\n  # is equal to the prompt that is being added or concatenated to. In all other\n  # cases the completion is added to the end of the prompt.\n```\n\n## Installation\n\nThis gem won't be published again to RubyGems until it's more stable. For now, you\nshould add these lines to your application's Gemfile to experiment with Instruct:\n\n```ruby\n  gem \"instruct\", github: \"instruct-rb/instruct\", branch: \"development\"\n  gem \"attributed-string\", github: \"instruct-rb/attributed-string\", branch: \"main\"\n```\n\nInclude the helpers and refinements in the modules or classes where you want to\nuse Instruct.\n\n```ruby\n  include Instruct::Helpers\n  using Instruct::Refinements\n```\n\nInstruct supports the ruby-openai gem and anthropic out of the box, simply include the\none or both gems in your Gemfile.\n\n```ruby\n  gem \"ruby-openai\"\n  gem \"anthropic\"\n```\n\nFor more info on setting up the OpenAI or Anthropic clients, see the docs for\n[OpenAI Usage](docs/openai-usage.md) and [Anthropic Usage](docs/anthropic-usage.md).\n\n### Logging Setup\n`Instruct.error_logger` and `Instruct.logger` can be set to any ruby `Logger`\nclass. By default they are configured to log warn and error messages. Set the `INSTRUCT_LOG_LEVEL`\nenvironment variable to `debug`, `info`, `warn`, `error`, `fatal`, `unknown` to change the\nthe log level, or change the log level directly on the logger instance.\n\n```ruby\n# logs errors and warnings to STDERR by default, by default all warnings and\n# errors are logged\nInstruct.err_logger.sev_threshold = :warn\n\n# logs all debug and info messages to STDOUT, by default nothing is logged as\n# the default is warn.\nInstruct.logger.sev_threshold = :warn\n```\n\n\n## What's missing\nThis gem is still in development and is missing many features before a 1.0,\nplease feel free to get in touch if you would like to contribute or have any\nideas.\n\n- Middleware\n  - [ ] Constraint based validation with automatic retries\n  - [ ] Improvments to chat completion middleware\n    - [ ] Allow role switching on the same line but then in the updated prompt fix it\n    - [ ] New Conversation middleware with default to user with system kw arg or assistant kw arg (maybe its one and the same?)\n  - [ ] Conversation management (prune long running conversations)\n  - [ ] Async support (waiting on async support in ruby-openai). This enables\n        the use of async calls to the LLM and the use of async middleware.\n  - [ ] Streaming structured output (similar to BAML or a CFG)\n    - [ ] Self healing\n  - [ ] Guard-rails (middleware that checks for prompt injections/high perplexity)\n  - [ ] Auto-continuation (middleware that adds prompts to continue a conversation)\n  - [ ] Support transform attachments in the prompt intos multi-modal input\n  - [ ] Anthropic caching\n  - [ ] Visualize streaming prompt as a tree in web interface (dependent on forking)\n  - [ ] Standardize finish reasons, and shared call arguments\n- Models\n  - [x] OpenAI API model selection\n  - [x] Anthropic API model selection\n  - [ ] Gemini models\n  - [ ] Local models\n    - [ ] Constrained inference like Guidance\n    - [ ] Token healing\n- Core\n  - [ ] Track forking path\n  - [ ] Change middleware by passing it into the gen or call methods\n  - [ ] Tool calling\n    - [ ] Develop an intuitive API for calling tools\n  - [ ] Batch APIs\n  - [ ] Improve attributed string API with a visitor style presenter\n    - [ ] Update middleware and printers to use the new presenters\n  - [x] Serialization of prompts (Consider migrations / upgrades) for storage\n    - [x] Register ActiveJob serializer for prompts so that they can be added to the job queue\n    - [ ] Register ActiveRecord serializer for prompts so that they can be stored in the database\n  - [x] `stop_chars` and `stop`\n","funding_links":[],"categories":["Recently Updated","Ruby","Machine Learning Libraries"],"sub_categories":["[Dec 27, 2024](/content/2024/12/27/README.md)","Frameworks"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finstruct-rb%2Finstruct","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finstruct-rb%2Finstruct","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finstruct-rb%2Finstruct/lists"}