Class: Vangrail::Conversation
- Inherits:
-
Object
- Object
- Vangrail::Conversation
- Includes:
- ConversationTools
- Defined in:
- lib/vangrail/conversation.rb
Overview
A dialogue, so rails can see more than the turn in front of them.
Every rail so far reads one string. That is enough for the attacks that fit in one string, and it is exactly wrong for the ones built out of turns that are individually unremarkable: ask something harmless, ask for more detail about the part of the answer that helps, keep going until the thing you wanted is on screen. No single message in that sequence looks like an attack, because none of them is one.
So this holds the turns and threads them into the rail context as
:history, which the rail protocol has always carried and nothing has ever
filled in. A rail that ignores history behaves exactly as before.
convo = Vangrail::Conversation.new(engine)
verdict = convo.ask(question)
convo.answer(text) if verdict.allowed?
What it also does is remember the verdicts. A refusal is the most informative event in a dialogue: the next message is either an ordinary follow-up or the same request rewritten, and telling those apart is impossible without knowing a refusal happened.
Pass prior: and the same turns also feed a Session. One engine walk
per turn: assess when a session is present, otherwise check_input.
Escalation is not an assess term, so a retry after a refusal is
caught on the path without a session.
After ask and screen both tracks have turns. Name the channel;
block? is true if either would block.
convo = Vangrail::Conversation.new(engine, prior: 1e-3)
convo.ask(question)
convo.screen(documents)
convo.session.posterior(:attack)
convo.session.posterior(:contamination)
convo.session.block?
Defined Under Namespace
Classes: Turn
Constant Summary collapse
- DEFAULT_WINDOW =
How many turns of history the rails see. A dialogue that has been running for an hour is mostly irrelevant to whether this message is a retry, and an unbounded window makes the cost of a check grow with the session.
12
Instance Attribute Summary collapse
-
#admission ⇒ Object
readonly
Returns the value of attribute admission.
-
#capabilities ⇒ Object
readonly
Returns the value of attribute capabilities.
-
#engine ⇒ Object
readonly
Returns the value of attribute engine.
-
#invocations ⇒ Object
readonly
Returns the value of attribute invocations.
-
#monitor ⇒ Object
readonly
Returns the value of attribute monitor.
-
#plan ⇒ Object
readonly
Returns the value of attribute plan.
-
#profile ⇒ Object
readonly
Returns the value of attribute profile.
-
#retrieved ⇒ Object
readonly
Returns the value of attribute retrieved.
-
#session ⇒ Object
readonly
Returns the value of attribute session.
-
#tools ⇒ Object
readonly
Returns the value of attribute tools.
-
#turns ⇒ Object
readonly
Returns the value of attribute turns.
-
#window ⇒ Object
readonly
Returns the value of attribute window.
Instance Method Summary collapse
-
#admit?(capability, arguments: nil) ⇒ Boolean
included
from ConversationTools
Compatibility query for the coarse Admission API.
- #answer(text, **context) ⇒ Object
-
#ask(text, **context) ⇒ Object
Checks a question and records it, whatever the verdict.
- #blocked? ⇒ Boolean
- #blocked_turns ⇒ Object
- #child_env(source = ENV) ⇒ Object included from ConversationTools
- #confirm(call, actor:) ⇒ Object included from ConversationTools
-
#extract(pattern) ⇒ Object
A span pulled out of retrieved data.
-
#history ⇒ Object
The window the rails read: role and text, no Result objects, because a rail should not be reasoning about another rail's verdict text.
-
#initialize(engine, window: DEFAULT_WINDOW, session: nil, prior: nil, allow: {}, admission: nil, capabilities: nil, tools: nil, profile: nil, deny: [], hooks: {}, plan: nil, monitor: nil, **context) ⇒ Conversation
constructor
A new instance of Conversation.
-
#intend(*names) ⇒ Object
included
from ConversationTools
Names the tools this question is allowed to use before retrieved data is visible.
- #intended ⇒ Object included from ConversationTools
-
#invoke(target, arguments: nil, sink: nil, confirmation: nil, transaction: false, idempotency_key: nil, risk: nil) ⇒ Object
included
from ConversationTools
Runs a handler only after profile, plan, hook, and reference-monitor authorization agree.
- #invoked?(name) ⇒ Boolean included from ConversationTools
- #last_user_turn ⇒ Object
- #locked? ⇒ Boolean included from ConversationTools
-
#messages(system:, mode: :delimit, mark: Spotlight::DEFAULT_MARK) ⇒ Object
The only assembly this object will produce.
-
#screen(documents, **context) ⇒ Object
Screens retrieved documents with the dialogue in view, so a context rail can see which question they were fetched for.
- #to_h ⇒ Object
Constructor Details
#initialize(engine, window: DEFAULT_WINDOW, session: nil, prior: nil, allow: {}, admission: nil, capabilities: nil, tools: nil, profile: nil, deny: [], hooks: {}, plan: nil, monitor: nil, **context) ⇒ Conversation
Returns a new instance of Conversation.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/vangrail/conversation.rb', line 78 def initialize(engine, window: DEFAULT_WINDOW, session: nil, prior: nil, allow: {}, admission: nil, capabilities: nil, tools: nil, profile: nil, deny: [], hooks: {}, plan: nil, monitor: nil, **context) raise ArgumentError, 'pass session: or prior:, not both' if session && prior if plan && monitor && !monitor.plan.equal?(plan) raise ArgumentError, 'plan and monitor must refer to the same plan' end @engine = engine @window = window @base_context = context @turns = [] @retrieved = [] @invocations = [] @locked = false @pinned = false @hooks = hooks @tools = tools || Tools.new @profile = Profile.resolve(profile, allow: allow, deny: deny) @plan = plan || monitor&.plan || Plan.from_allow(@profile.allow, tools: @tools) @monitor = monitor || ReferenceMonitor.new(@plan) @intended = plan || monitor ? @plan.grants.map(&:tool).uniq : [] @capabilities = capabilities.nil? ? nil : Array(capabilities).map(&:to_sym).freeze @session = session || (prior && Session.new(engine: engine, prior: prior)) @admission = admission || Admission.new(allow: @profile.allow) end |
Instance Attribute Details
#admission ⇒ Object (readonly)
Returns the value of attribute admission.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def admission @admission end |
#capabilities ⇒ Object (readonly)
Returns the value of attribute capabilities.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def capabilities @capabilities end |
#engine ⇒ Object (readonly)
Returns the value of attribute engine.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def engine @engine end |
#invocations ⇒ Object (readonly)
Returns the value of attribute invocations.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def invocations @invocations end |
#monitor ⇒ Object (readonly)
Returns the value of attribute monitor.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def monitor @monitor end |
#plan ⇒ Object (readonly)
Returns the value of attribute plan.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def plan @plan end |
#profile ⇒ Object (readonly)
Returns the value of attribute profile.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def profile @profile end |
#retrieved ⇒ Object (readonly)
Returns the value of attribute retrieved.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def retrieved @retrieved end |
#session ⇒ Object (readonly)
Returns the value of attribute session.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def session @session end |
#tools ⇒ Object (readonly)
Returns the value of attribute tools.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def tools @tools end |
#turns ⇒ Object (readonly)
Returns the value of attribute turns.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def turns @turns end |
#window ⇒ Object (readonly)
Returns the value of attribute window.
75 76 77 |
# File 'lib/vangrail/conversation.rb', line 75 def window @window end |
Instance Method Details
#admit?(capability, arguments: nil) ⇒ Boolean Originally defined in module ConversationTools
Compatibility query for the coarse Admission API. Actual execution also requires a matching structured Grant from the ReferenceMonitor.
#answer(text, **context) ⇒ Object
128 129 130 131 132 133 134 135 |
# File 'lib/vangrail/conversation.rb', line 128 def answer(text, **context) result = engine.check_output(text, history: history, **@base_context, **context) turn = Turn.new(role: :assistant, text: content_of(result, text), result: result, origin: Origin.tool) @turns << turn @session&.fold(result, origin: turn.origin, side: :output) result end |
#ask(text, **context) ⇒ Object
Checks a question and records it, whatever the verdict. A blocked turn stays in the history: it is the part the next check needs most.
One engine walk: assess when a session is present, check_input otherwise. Assess does not run Escalation. That object is folded onto the Turn and the Session.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/vangrail/conversation.rb', line 112 def ask(text, **context) @pinned = true seen = history ctx = { history: seen, **@base_context, **context } result = if @session judgement = engine.assess(text, side: :input, origin: Origin.user, **session_assess, **ctx) @session.fold(judgement) result_from(judgement) else engine.check_input(text, **ctx) end @turns << Turn.new(role: :user, text: text.to_s, result: result, origin: Origin.user) result end |
#blocked? ⇒ Boolean
184 185 186 |
# File 'lib/vangrail/conversation.rb', line 184 def blocked? !blocked_turns.empty? end |
#blocked_turns ⇒ Object
180 181 182 |
# File 'lib/vangrail/conversation.rb', line 180 def blocked_turns turns.select { |t| t.user? && t.blocked? } end |
#child_env(source = ENV) ⇒ Object Originally defined in module ConversationTools
#confirm(call, actor:) ⇒ Object Originally defined in module ConversationTools
#extract(pattern) ⇒ Object
A span pulled out of retrieved data. The result is still data.
167 168 169 170 171 172 |
# File 'lib/vangrail/conversation.rb', line 167 def extract(pattern) retrieved.filter_map do |cell| match = cell.value[pattern] Cell.data(match) if match end end |
#history ⇒ Object
The window the rails read: role and text, no Result objects, because a rail should not be reasoning about another rail's verdict text.
176 177 178 |
# File 'lib/vangrail/conversation.rb', line 176 def history turns.last(window).map { |t| { role: t.role, text: t.text, blocked: t.blocked? } } end |
#intend(*names) ⇒ Object Originally defined in module ConversationTools
Names the tools this question is allowed to use before retrieved data is visible. Selecting a name never creates a Grant.
#intended ⇒ Object Originally defined in module ConversationTools
#invoke(target, arguments: nil, sink: nil, confirmation: nil, transaction: false, idempotency_key: nil, risk: nil) ⇒ Object Originally defined in module ConversationTools
Runs a handler only after profile, plan, hook, and reference-monitor authorization agree. Every refusal is recorded without calling the handler.
#invoked?(name) ⇒ Boolean Originally defined in module ConversationTools
#last_user_turn ⇒ Object
188 189 190 |
# File 'lib/vangrail/conversation.rb', line 188 def last_user_turn turns.reverse.detect(&:user?) end |
#locked? ⇒ Boolean Originally defined in module ConversationTools
#messages(system:, mode: :delimit, mark: Spotlight::DEFAULT_MARK) ⇒ Object
The only assembly this object will produce. The question is the last
user turn; the passages are the cells screen kept. A caller who
pastes retrieved text into system: or question: has to do it
without this method, which is the point.
158 159 160 161 162 163 164 |
# File 'lib/vangrail/conversation.rb', line 158 def (system:, mode: :delimit, mark: Spotlight::DEFAULT_MARK) turn = last_user_turn raise Error, 'ask before assembling a prompt' unless turn Spotlight.(system: system, question: Cell.user(turn.text), passages: retrieved, mode: mode, mark: mark) end |
#screen(documents, **context) ⇒ Object
Screens retrieved documents with the dialogue in view, so a context rail can see which question they were fetched for. A session, if any, records every judged page on the contamination track, rejected ones included: instruction-shaped data is poisoned retrieval, not a user attack. Retrieved cells stay the survivors.
142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/vangrail/conversation.rb', line 142 def screen(documents, **context) seen = history result = engine.screen(documents, history: seen, **@base_context, **context) @retrieved = result.cells @locked = true @intended.freeze Array(documents).each do |document| @session&.observe(Cell.text_of(document), side: :context, origin: :data, history: seen) end result end |
#to_h ⇒ Object
192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/vangrail/conversation.rb', line 192 def to_h { 'turns' => turns.map(&:to_h), 'blocked' => blocked_turns.size, 'invoked' => invocations.select { |row| row[:result].allowed? }.map { |row| row[:name].to_s }, 'intended' => @intended.map(&:to_s), 'locked' => locked?, 'plan' => plan.to_h, 'profile' => profile.name.to_s, 'session' => session&.to_h, }.compact end |