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.
| Function | What it does |
|---|---|
/lib/raisin/ai/search-documents | Finds passages. Returns the text plus where it came from |
/lib/raisin/ai/ask | The whole round trip: find passages, answer from them, cite them |
/lib/raisin/ai/graph-context | Starts from what a question is about and follows the links out |
/lib/raisin/ai/extract-entities | Reads 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:
| Form | Example | Behaviour |
|---|---|---|
| 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
askis at least one model call, and up to three when the grader asks for a retry.search-documentson its own is free of model cost — reach for it when you want passages rather than prose. extract-entitiesis 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
- Build a chatbot — the same functions behind a conversation.
- Search, indexing and the knowledge graph — what is indexed and how documents are split.
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.