Skip to content

Extract RubyLLM::Prompt from Agent's private prompt rendering#675

Open
kryzhovnik wants to merge 3 commits into
crmne:mainfrom
kryzhovnik:extract-prompt-class
Open

Extract RubyLLM::Prompt from Agent's private prompt rendering#675
kryzhovnik wants to merge 3 commits into
crmne:mainfrom
kryzhovnik:extract-prompt-class

Conversation

@kryzhovnik

Copy link
Copy Markdown
Contributor

Moves prompt root resolution, path construction, and ERB rendering into a standalone RubyLLM::Prompt class so prompts can be rendered without subclassing Agent.

Closes #667

What this does

Extracts RubyLLM::Prompt from Agent's private prompt_path_for and prompt_root methods. This lets users render prompts following the gem's own app/prompts/ convention without subclassing Agent:

RubyLLM::Prompt.render('friend', name: 'Andrey')

Agent's render_prompt now delegates to Prompt, preserving its existing class-name scoping and runtime context resolution.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Performance improvement

Scope check

  • I read the Contributing Guide
  • This aligns with RubyLLM's focus on LLM communication
  • This isn't application-specific logic that belongs in user code
  • This benefits most users, not just my specific use case

Required for new features

PRs for new features or enhancements without a prior approved issue will be closed.

Quality check

  • I ran overcommit --install and all hooks pass
  • I tested my changes thoroughly
    • For provider changes: Re-recorded VCR cassettes with bundle exec rake vcr:record[provider_name]
    • All tests pass: bundle exec rspec
  • I updated documentation if needed
  • I didn't modify auto-generated files manually (models.json, aliases.json)

AI-generated code

  • I used AI tools to help write this code
  • I have reviewed and understand all generated code (required if above is checked)

API changes

  • Breaking change
  • New public methods/classes
  • Changed method signatures
  • No API changes

kryzhovnik and others added 2 commits March 12, 2026 09:20
Moves prompt root resolution, path construction, and ERB rendering
into a standalone RubyLLM::Prompt class so prompts can be rendered
without subclassing Agent.

Closes crmne#667

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.06%. Comparing base (4942d6c) to head (5cfe000).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #675      +/-   ##
==========================================
+ Coverage   87.05%   87.06%   +0.01%     
==========================================
  Files         119      120       +1     
  Lines        5594     5599       +5     
  Branches     1407     1406       -1     
==========================================
+ Hits         4870     4875       +5     
  Misses        724      724              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@crmne crmne left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the extraction here. The thing I would change is the public API shape.

Rather than documenting RubyLLM::Prompt.render(...), I think the public API should be:

RubyLLM.render_prompt("friend", name: "Andrey")
RubyLLM.render_prompt("work_assistant/instructions", user: current_user)

That matches the existing RubyLLM style of top-level verbs for common actions: RubyLLM.chat, RubyLLM.embed, RubyLLM.paint, RubyLLM.transcribe, etc. It also keeps RubyLLM::Prompt as an internal implementation object instead of asking users to think in a service class for a one-shot render.

I would avoid RubyLLM.prompt(...) because "prompt" is overloaded in an LLM library. render_prompt is explicit and already matches the Agent naming.

Implementation-wise, that could be as small as adding this to lib/ruby_llm.rb:

module RubyLLM
  class << self
    def render_prompt(name, **locals)
      Prompt.render(name, **locals)
    end
  end
end

Then Agent can delegate through the public entrypoint:

RubyLLM.render_prompt("#{prompt_agent_path}/#{name}", **resolved_locals)

One small load-order issue: lib/ruby_llm/prompt.rb should require its own dependencies:

require "erb"
require "pathname"

Right now standalone rendering can fail with uninitialized constant RubyLLM::Prompt::ERB if Agent has not already loaded ERB.

Add a top-level RubyLLM.render_prompt(name, **locals) verb alongside
chat/embed/paint/transcribe, keeping RubyLLM::Prompt as internal
implementation. Agent.render_prompt now delegates through it.

Require erb and pathname in prompt.rb so standalone rendering works when
Agent has not been loaded; drop the now-unused erb/pathname requires from
agent.rb since rendering moved to Prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kryzhovnik

Copy link
Copy Markdown
Contributor Author

Reworked it as suggested

One caveat: a bare require "ruby_llm/prompt" in full isolation still won't work, because PromptNotFoundError isn't loaded there. The error class lives in error.rb, which can't load on its own (it registers a Faraday::Middleware at file scope). Maybe it's worth splitting the plain error classes into a Faraday-free file to decouple them from Faraday?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Extract RubyLLM::Prompt from Agent's existing prompt rendering internals

2 participants