Backlinks
The built-in assistant: how questions become Web-Algebra plans, how it is set up, and how conversations are stored
The assistant turns a question into a Web-Algebra plan, shows the plan, and runs it when the reader says so. It runs in the browser as part of the client stylesheet and talks to a web-algebra service beside the platform, which is optional and off by default — see Setup. The user guide covers using it. To work with LinkedDataHub from an agent of your own instead, see Connect an agent.
How it works
- The client sends the question to /webalgebra/plans on the dataspace's own origin, with the current document, the dataspace's SPARQL endpoint and ontology, and the last three turns of the conversation that ran, each with its plan, its outcome and up to a hundred of the rows it returned.
- nginx forwards /webalgebra to the web-algebra service together with the client certificate of the connection. A request without a client certificate is answered with 401, since the service has nobody to act for.
- The service asks the model for a plan, checks it against its executor, and answers with the plan, a one-sentence summary and the operations it uses. The client shows them as a card.
- On Execute, the client submits the plan to /webalgebra. The service answers 202 with an execution id, and the client polls /webalgebra/{id}/result once a second, showing each step as the executor reports it.
- When the plan has run, the client sends the result to /webalgebra/answers for a sentence that sums it up, then stores the turn in the conversation's document and reloads the documents the plan wrote.
A plan that only reads may run as soon as it arrives. One that writes always waits for the reader to press Execute.
Access control
The service runs every plan as the reader who asked. It presents the secretary's WebID
certificate to the platform, with an
On-Behalf-Of header naming the reader's WebID. The platform honours it because every agent's WebID
document that the platform creates,
at sign-up or at the first Google or ORCID login, states that the secretary delegates that agent
(<secretary> acl:delegates <agent>). Plans are therefore subject to the reader's own
authorizations: a plan can read and write what the reader can, and nothing else.
nginx clears any On-Behalf-Of and Client-Cert headers arriving from outside, so identity comes only from the TLS
connection. The web-algebra service is reachable only through nginx.
On a deployment that enables the assistant, the Assistant button appears for a signed-in agent with append access to the document, and a conversation accepts new questions under the same condition.
Setup
The web-algebra service runs the ghcr.io/atomgraph/webalgebra-server-ldh image, which is proprietary: AtomGraph publishes the image but not its source, and LinkedDataHub's license does not cover it. The assistant is therefore opt-in.
docker-compose.yml declares the web-algebra service under the assistant Compose profile, so it does not start unless the profile is active, and the Assistant button stays hidden until the platform is told that the service runs. Writing plans from questions also needs an OpenAI API key:
- Enable the profile in .env:
The profile starts the service, and the platform reads the same variable: it then publishes
COMPOSE_PROFILES=assistant
lds:assistant truefor every dataspace in its /context, which is what the button is shown on. Set it in .env rather than with docker compose --profile, which the platform does not see. - Put the key in secrets/openai_api_key.txt
- Declare the secret and hand it to the service in docker-compose.override.yml:
secrets: openai_api_key: file: ./secrets/openai_api_key.txt services: web-algebra: environment: - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key secrets: - secretary_cert_password - openai_api_key - Recreate the services: make up
Without a key the service still runs plans, but declines to write them: a question is answered with a message saying so, shown in the card. The sentence that sums up a result also comes from the model; without it, the card shows the rows alone.
The service's variables are listed in the configuration reference.
Running without the service
Without the profile, the rest of the stack runs as usual: nginx looks the service up per request instead of at startup, and answers /webalgebra with 502. Conversations stored while the assistant was enabled are still drawn from the store, but a new question in them fails with an error in its card.
Data model
A conversation is an ldh:Chat resource, placed in a document by an object block
like a view or a chart. Its turns are ldh:ChatTurn resources in the same document, and the chat's rdf:_1,
rdf:_2, … members in the order they were asked. The first question writes the chat, the
object block and the document's next
rdf:_N for the block in one request; the chat and its block share a fragment suffix, and
the chat's title is the first question, cut near
80 characters. Each turn is written when its answer has landed, or has failed to.
| Property | Value |
|---|---|
ldh:question |
What the reader asked |
ldh:answer |
What the plan did, read by the model, as an rdf:XMLLiteral: an XHTML div of paragraphs, lists, emphasis, code and links to what the plan returned, when the
model gave one. Other markup is dropped, and so are links that are not http(s). Turns
stored before answers were XHTML hold a plain string |
ldh:plan |
The Web-Algebra plan as an rdf:XMLLiteral: its summary, its operations and the plan itself |
ldh:execution |
What running the plan reported, as an rdf:XMLLiteral: the steps, the documents written, and the first hundred rows or resources of the
result |
ldh:outcome |
How the turn went, in one line: rows or resources returned, documents written, no results, the failure's message, or declined when the service answered in words instead of with a plan |
dct:created |
When the turn was stored |
A stored conversation is drawn from these resources when the document is rendered again, so it shows the result as it was when the turn ran; nothing is run again. Since the chat is an ordinary resource, its object block can also be placed in another document, and questions asked there are stored in the chat's own document.
@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<https://localhost:4443/customers/> rdf:_3 <https://localhost:4443/customers/#block-91c0…> .
<https://localhost:4443/customers/#block-91c0…> a ldh:Object ;
rdf:value <https://localhost:4443/customers/#chat-91c0…> .
<https://localhost:4443/customers/#chat-91c0…> a ldh:Chat ;
dct:title "Which five customers ordered the most in 1997?" ;
rdf:_1 <https://localhost:4443/customers/#turn-5d7e…> .
<https://localhost:4443/customers/#turn-5d7e…> a ldh:ChatTurn ;
ldh:question "Which five customers ordered the most in 1997?" ;
ldh:answer "<div xmlns="http://www.w3.org/1999/xhtml"><p>The five customers with the most orders in 1997 were:</p><ol><li><a href="…">Save-a-lot Markets</a>: 17 orders</li>…</ol></div>"^^rdf:XMLLiteral ;
ldh:plan "<wa:plan …>…</wa:plan>"^^rdf:XMLLiteral ;
ldh:execution "<wa:execution …>…</wa:execution>"^^rdf:XMLLiteral ;
ldh:outcome "5 rows" ;
dct:created "2026-10-17T09:12:44Z"^^xsd:dateTime .