Conversation
A host that drives RubyLLM `acts_as_chat` conversations and runs `ActiveAgent::Evals` suites against them had to write two pieces itself: a Judge whose completions come from a RubyLLM chat, and a Replay built from the messages a conversation stored. Both are generic, so they move into the gem as `ActiveAgent::Evals::RubyLLM`. `judge(label:, model:, provider:, context:, correlation:)` returns a Judge that answers from `context.chat(...)`, where `context` is RubyLLM itself or a `RubyLLM.context` built with the host's keys, and traces each call through `Correlation#judge` under the kind it serves when a correlation is given. `replay(messages)` reads the tool calls in id order, marks a call errored when its tool result holds JSON with an `"error"` key (how an MCP tool failure is reported), sums tokens over the assistant messages from RubyLLM 1.x's columns or 2.x's `tokens`, and takes the last assistant message as the answer. The file is loaded by an explicit `require "active_agent/evals/ruby_llm"` and requires `ruby_llm` itself, so `require "active_agent/evals"` stays free of the gem; the standalone load test now checks both directions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2Eg7xu5GJ5UsSw8GmGNix
…gine
The keys an owner saves under Settings -> Provider API Keys were only
reachable through `for_owner(...).find_by(provider:)`, which every host
calling models itself was re-deriving. `ProviderKey.credentials_for(owner)`
returns `{ "openai" => "sk-..." }` for the owner's saved API-key rows, and
`ProviderKey.apply_to(config, owner:)` writes each through
`<provider>_api_key=` on whatever it is handed: a `RubyLLM.context`
config block, or any object with those writers, so the engine takes on
no RubyLLM dependency.
A credential that no longer decrypts is skipped with a warning naming
the error class, never the value, rather than raised, so one stale row
does not take every provider down.
Documents both next to the provider-key sections, and adds the
CHANGELOG entries for this and the evaluation module's RubyLLM glue.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V2Eg7xu5GJ5UsSw8GmGNix
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A host app that drives RubyLLM
acts_as_chatconversations and runsActiveAgent::Evalssuites against them, with theactionagentdashboard mounted, currently carries three pieces of generic RubyLLM-to-ActiveAgent glue.ActiveAgent::Evals::Correlation(1.6.3) already covered trace correlation; this PR moves the remaining two into the gems so every such host stops re-deriving them:Judgewhose completions come from a RubyLLM chat, traced through the correlation;Replaybuilt from the messages a conversation stored (tool calls with MCP-style errors, tokens, answer);RubyLLM.contextconfig block.API
ActiveAgent::Evals::RubyLLM(activeagent)Loaded by an explicit
require "active_agent/evals/ruby_llm", which requiresruby_llmitself.require "active_agent/evals"alone still loads without the gem; the standalone load test now checks both directions.replay(messages, answer: nil, duration_ms: nil, error: nil, metadata: {})accepts an Array or a relation (to_a) ofacts_as_chatrecords orRubyLLM::Messagevalues:tool_calls: every message's calls in id order,{ "name", "arguments", "error", "detail" }; thetoolmessage answering a call (matched ontool_call_id) whose content is JSON with an"error"key marks it errored with that error asdetail.input_tokens/output_tokens: summed over assistant messages frominput_tokens/output_tokens(RubyLLM 1.x) ortokens.input/tokens.output(RubyLLM 2.x).answer: the last assistant message's content unless given.rolemay be a String or a Symbol;tool_callsmay be a has_many, a Hash keyed by call id, or nil.ActionAgent::ProviderKey(actionagent)Only
KEY_PROVIDERSrows with a present credential;ollamahosts are left out.configis duck-typed (anything with<provider>_api_key=writers), so the engine gains no RubyLLM dependency. A credential that cannot be decrypted is skipped with a warning naming the error class only, never the value.Migrating a host
extract_tool_calls/ token-summing / replay-building code withActiveAgent::Evals::RubyLLM.replay(messages, duration_ms:, metadata:).ActiveAgent::Evals::RubyLLM.judge(label:, model:, provider:, context:, correlation:).ProviderKey.for_owner(owner).where(...)loop that builds aRubyLLM.contextwithRubyLLM.context { |config| ActionAgent::ProviderKey.apply_to(config, owner: owner) }.require "active_agent/evals/ruby_llm"where the suite is run.Docs and changelog
docs/framework/evaluations.md: new "RubyLLM hosts" section showingCorrelation+judge+replaytogether.docs/framework/self-hosted-observability.md: "Using an owner's provider keys outside the engine" next to the provider-key description; a pointer from the RubyLLM dashboard guide.Validation
bin/test test/evals/*_test.rb actionagent/test/credentials_test.rb actionagent/test/dashboard_assistant_service_test.rb actionagent/test/provider_key_test.rb: 182 runs, 0 failures (includes the 16 new tests).rubocop --config .rubocop.ymlon every changed Ruby file: no offenses.npx vitepress build docs: builds; the new anchors resolve.🤖 Generated with Claude Code
https://claude.ai/code/session_01V2Eg7xu5GJ5UsSw8GmGNix
Generated by Claude Code