Module: Brainchat::Chat

Defined in:
lib/brainchat/chat.rb,
sig/brainchat.rbs

Overview

Answers a question with an LLM, using retrieved knowledge-brain chunks as the only context. Chunks are numbered in the prompt and the model is asked to cite them as [n]; the CLI prints the [n] -> path:lines mapping after the answer, so every citation is resolvable to a file.

Constant Summary collapse

OLLAMA_DEFAULT_BASE =

Ollama's conventional local endpoint, so --provider ollama needs no setup.

"http://localhost:11434/v1"
FAILURES =

A missing API key or an unknown model id raise off a different branch of RubyLLM's hierarchy than provider/HTTP faults, and they are the two most likely failures here, so both branches are caught.

[RubyLLM::Error, RubyLLM::ConfigurationError,
RubyLLM::ModelNotFoundError, Faraday::Error].freeze
SYSTEM_PROMPT =
"You answer questions about the user's knowledge-brain: a vault of notes,\nADRs, plans, commit history and docs, retrieved for you by a hybrid\nBM25 + cosine search. The retrieved chunks are numbered and included in\nthe user's message.\n\nAnswer only from those chunks. When the chunks do not contain the answer,\nsay so plainly instead of guessing. Cite every claim with the chunk\nnumber in square brackets, e.g. [1] or [2][3]. Keep the answer tight;\nquote file paths only via the citation numbers.\n"

Class Method Summary collapse

Class Method Details

.call(question, chunks, on_chunk: nil, **chat_options) ⇒ Object

Asks question with chunks as context. Streams each content delta to on_chunk when given. Returns the final RubyLLM::Message.

Parameters:

  • question (String)
  • chunks (Array[Retriever::Chunk])
  • on_chunk: (^(String) -> void) (defaults to: nil)
  • chat_options (Object)

Returns:

  • (Object)


36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/brainchat/chat.rb', line 36

def call(question, chunks, on_chunk: nil, **chat_options)
  configure
  chat = RubyLLM.chat(**chat_options.compact)
                .with_instructions(SYSTEM_PROMPT)
  if on_chunk
    chat.ask(prompt_for(question, chunks)) { |chunk| on_chunk.call(chunk.content) }
  else
    chat.ask(prompt_for(question, chunks))
  end
rescue *FAILURES => e
  raise Error, "chat failed: #{e.class}: #{e.message}"
end

.configure ⇒ Object

RubyLLM reads no provider credentials from the environment on its own, and a CLI has nowhere else to get them.



51
52
53
54
55
56
57
# File 'lib/brainchat/chat.rb', line 51

def configure
  RubyLLM.configure do |config|
    config.anthropic_api_key = ENV.fetch("ANTHROPIC_API_KEY", nil)
    config.openai_api_key = ENV.fetch("OPENAI_API_KEY", nil)
    config.ollama_api_base = ENV.fetch("OLLAMA_API_BASE", OLLAMA_DEFAULT_BASE)
  end
end

.prompt_for(question, chunks) ⇒ Object

Chunks go over as a numbered list; the citation number the model emits is the position in this list, and the CLI prints the same numbering.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/brainchat/chat.rb', line 61

def prompt_for(question, chunks)
  body = chunks.each_with_index.map do |chunk, index|
    "      [\#{index + 1}] \#{chunk.location} (\#{chunk.source_type}/\#{chunk.repo})\n      \#{chunk.text}\n    CHUNK\n  end.join(\"\\n\")\n\n  <<~PROMPT\n    Retrieved chunks:\n\n    \#{body}\n\n    Question: \#{question}\n  PROMPT\nend\n"