Docs / Automations

Answering questions from your content

Search returns passages. An answer is a passage plus a model that reads it, and the one rule that makes the difference between a useful assistant and a confident liar: answer only from what was retrieved.

Four functions ship ready to call. They are ordinary function nodes — read them, copy them, replace them with your own.

FunctionWhat it does
/lib/raisin/ai/search-documentsFinds passages. Returns the text plus where it came from
/lib/raisin/ai/askThe whole round trip: find passages, answer from them, cite them
/lib/raisin/ai/graph-contextStarts from what a question is about and follows the links out
/lib/raisin/ai/extract-entitiesReads a document and writes the entities it names into the graph

Asking a question

From an automation, use a Run a function step. From anywhere else, the function invoke endpoint:

curl -X POST "$RAISIN/api/functions/$REPO/ask/invoke" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"input": {"question": "How much notice do we have to give?"},
       "wait_for_completion": true}'
{
  "answer": "Either party may terminate on thirty days written notice [1].",
  "grounded": true,
  "citations": [
    { "marker": 1, "path": "/contracts/msa", "chunk_index": 0,
      "text_is_exact": true }
  ],
  "attempts": [{ "query": "How much notice do we have to give?", "passages": 8 }]
}

Three fields carry the weight:

grounded: false means the model was never asked. When retrieval finds nothing, ask says so instead of putting an empty context in front of a model. A model in that position answers from its own training — fluently, plausibly, and with nothing in the output to show it made the answer up. Refusing is the correct outcome, and it is the one that makes the rest of this trustworthy.

citations is the numbering the model was given, so [1] resolves to a document and a passage without parsing prose. Show them. An answer a reader can check is worth more than one they must believe.

attempts shows whether the question had to be re-asked. See below.

text_is_exact: false means only a short preview of that passage was available. Point a reader at the document with it; do not quote it as if it were complete.

Choosing the workspaces

Every retrieval states which workspaces it may draw from. It is one string, and it takes four forms:

FormExampleBehaviour
One name'stories'That workspace
A list'stories, handbook, policies'Each name; spaces are trimmed
A glob'content-*'Every matching name — one token, never mixed into a list
Everything readable'ALL READABLE'Every workspace the caller may read

A name is an assertion; a glob is a question. A listed workspace that does not exist is an error — you meant that name, so a typo is reported rather than quietly searching the rest. A glob that matches nothing is fine and returns no rows; you only hoped for a pattern.

'*' and 'ALL' are rejected on purpose. '*' reads as “unscoped” and the eye slides past it in review, while two uppercase words appear in no other context — so “which of our queries go repo-wide?” stays answerable with one search.

It is not the security boundary

Row-level security applies to the results either way: nobody is shown a node they may not read, whatever scope you pass. The scope is an intent, not a permission. It decides which corpus the answer is drawn from.

That distinction is easy to wave away and expensive on a public site. A visitor’s identity may legitimately read your published pages, so a chatbot left at “everything readable” is not leaking anything it should not — it is answering from a corpus nobody chose. The moment a question happens to match an internal handbook that the same identity can read, the bot paraphrases it back, fluently and with a citation.

So both search and ask require the scope. Say 'ALL READABLE' when that is genuinely what you mean, and name the workspaces when it is not.

It retries with better words

A first search is the user’s phrasing, which is often not the document’s. ask grades what came back, and when the passages do not answer the question it rewrites the query in the vocabulary the documents themselves use and searches once more:

"how much warning before we get kicked out?"   →  0 passages
      graded: insufficient
      rewritten: "termination written notice period"
"termination written notice period"            →  8 passages  →  answer

That single rewrite is most of the value. The loop is capped at two attempts, so a question with no answer in your content fails fast rather than expensively.

Bringing in the graph

"use_graph": true adds a second retrieval leg: the entities the question is about, and what those connect to. It is off by default because it answers a different question and costs a walk.

Directly, graph-context takes a question or explicit starting points and returns the neighbourhood:

{ "input": { "query": "who maintains the handbook?", "hops": 2 } }

Every returned node carries hop (how far from the starting point) and via (the type of the link), so an answer can say not just that two things are connected but how. The walk is bounded and reports truncated: true when it stops early — a neighbourhood that looked complete but was not is how an assistant concludes something is unconnected when it was simply never visited.

Keeping the graph current

extract-entities reads a document’s text, asks a model which entities it names and how they relate, and writes them as nodes joined by typed relations.

Run it as an automation on document change:

trigger:
  kind: node_event
  workspace: assets
  event: updated
steps:
  - id: extract
    kind: function
    function: /lib/raisin/ai/extract-entities
    input:
      path: "${trigger.node.path}"
      workspace: assets

It is safe to run on every change. A run records a fingerprint of the text it read, and a later run that finds the same fingerprint does nothing at all — no model call, no write. Without that, every re-save would pay for an extraction, mint a revision, and trigger the reindex that follows, forever.

Entities converge: two documents naming the same supplier attach to one node, so walking out from that supplier reaches both.

What it costs, and what to watch

  • Every ask is at least one model call, and up to three when the grader asks for a retry. search-documents on its own is free of model cost — reach for it when you want passages rather than prose.
  • extract-entities is one model call per changed document, not per document.
  • Embedding happens on write, once per chunk. A large import is the moment to check your provider’s rate limits.

Next

Pilot documentation. Studio is in a partner pilot. Screens and YAML shown here are current as of the version noted on each page; tell your Maravilla contact when something does not match what you see.