These helpers call Guava's server-side LLM and RAG endpoints — no third-party API keys required. They pair naturally with the handlers in Handlers.

DocumentQA (server mode)

Index documents on the Guava server and answer questions over them. Ideal for on_question.

client = Guava.Client.new()

qa = Guava.DocumentQA.new(client,
  documents: [policy_text, faq_text],
  # namespace: "policies",   # scope this instance's docs when running several
  # instructions: "Answer only from the documents; be concise."
)

{:ok, answer} = Guava.DocumentQA.ask(qa, "What is the return window?")

Documents are content-addressed, so re-running with the same set skips re-uploading and prunes ones you dropped. Manage them incrementally:

qa = Guava.DocumentQA.upsert_document(qa, "return-policy", new_text)
qa = Guava.DocumentQA.add_document(qa, some_text)   # content-addressed key
qa = Guava.DocumentQA.delete_document(qa, "return-policy")
qa = Guava.DocumentQA.clear(qa)

(Each returns an updated DocumentQA — thread it through, functional-style.)

Wire it up:

@impl true
def handle_question(_call, question, state), do: {:reply, Guava.DocumentQA.ask!(qa(), question), state}

Local mode (bring your own vector store)

Pass a :store implementing Guava.RAG.VectorStore and a :generation_model implementing Guava.RAG.GenerationModel, each as a {module, state} tuple. Server mode is the default and needs no setup.

IntentRecognizer

Classify a caller request into one of a fixed set of choices — perfect for on_action_request.

recognizer = Guava.IntentRecognizer.new(client, %{
  "sales" => "purchases, pricing, availability, order status",
  "support" => "problems, returns, warranty, account help",
  "other" => "anything else"
})

Guava.IntentRecognizer.classify!(recognizer, "my order arrived damaged")
# => [%Guava.SuggestedAction{key: "support", description: "problems, returns, ..."}]

Pass a plain list of strings instead of a map if you don't need descriptions. Returns a list ordered by likelihood (multiple entries when ambiguous), or nil when nothing matches.

DatetimeFilter

Filter ISO-8601 datetime slots by a natural-language query — pairs with a searchable calendar_slot field via on_search_query.

filter = Guava.DatetimeFilter.new(client, ["2026-07-03T10:00", "2026-07-03T14:00", "2026-07-04T09:00"])
{matched, other} = Guava.DatetimeFilter.filter!(filter, "Friday afternoon", 5)

Returns {matching, fallback} slot lists (each capped at max_results), drawn only from the source list.

DateRangeParser

Turn phrases like "next Tuesday" or "the week of the 15th" into a concrete date range, clamped to today..today+1yr.

parser = Guava.DateRangeParser.new(client)
{start_date, end_date} = Guava.DateRangeParser.parse!(parser, "next week", 1)

LLM.generate

The low-level primitive the helpers build on (tuple + ! bang):

{:ok, text} = Guava.LLM.generate(client, "Summarize: ...")
Guava.LLM.generate(client, prompt, %{"type" => "object", "properties" => %{...}})  # JSON-schema-constrained

Next: Testing.