GenAI Bundle#
Indexes resources into an external GenAI application - embedding and vector database - and asks that application questions. The GenAI technology itself is not part of the bundle, just as the Solr server is not part of the search bundle.
The bundle has two areas:
- Indexer — the GenAI application as a target of the index bundle
- Assistant — asking the GenAI application questions
Sources#
The sources can be accessed via the GitHub project https://github.com/sitepark/atoolo-genai-bundle.
Installation#
First add the Sitepark Flex Repository before installing the bundle.
composer require atoolo/genai-bundle
The bundle and the index bundle have to be registered in
config/bundles.php:
return [
// ...
Atoolo\Index\AtooloIndexBundle::class => ['all' => true],
Atoolo\GenAi\AtooloGenAiBundle::class => ['all' => true],
];
Configuration#
The connection is held as its parts, the same way the search bundle holds the Solr connection, so that each one can be set on its own:
| Environment variable | Default | Purpose |
|---|---|---|
GENAI_SCHEME |
http |
scheme of the GenAI application |
GENAI_HOST |
localhost |
host of the GenAI application |
GENAI_PORT |
8080 |
port of the GenAI application |
GENAI_PATH |
empty | path the GenAI application is served under |
GENAI_API_KEY |
empty | sent as X-API-Key, only when set |
GENAI_IDLE_TIMEOUT |
300 |
seconds to wait for the next byte of an answer |
Without any of them the bundle talks to http://localhost:8080. An
environment that knows the application as one address can set GENAI_URL
instead, e.g. https://genai.example.com:8443/genai; it is taken apart into
the parts, the same way SOLR_URL works for the search bundle.
Without an API key the GenAI application rejects the indexing, so
GENAI_API_KEY has to be set wherever the bundle indexes.
GENAI_IDLE_TIMEOUT is not the duration of a request but the time the bundle
waits for the application to send something. The application answers a bulk
update only once it has embedded every changed document of it, so the default
is generous.
Client ip#
The GenAI application limits the questions per ip address. Every question and
every feedback is therefore sent with the ip of the visitor in
X-Forwarded-For. It is the ip Symfony determines for the request, so if the
website runs behind a proxy or load balancer, it has to be configured as a
trusted proxy.
Otherwise every visitor is sent with the ip of the proxy and they all share
one limit.
A X-Forwarded-For the visitor sends along is not passed on, as it could be
forged to get around the limit. The GenAI application in turn should trust the
header only from the website.
Indexing#
The GenAI indexer reads the same resources as the Solr indexer and therefore
works under the same source, internal. It is told apart by its id genai,
which selects it on the console and in the schedule. The CMS configures it in
configs/indexer/genai.php, so it can be enabled separately from the Solr
indexer (internal.php); without that file it is not offered.
bin/console index:indexer --indexer genai
Resources the CMS publishes or depublishes reach the GenAI index through the resource change notification, the same way as the Solr index. See also Indexing.
A full run is a sync: every document carries a hash of its content, and the
GenAI application only embeds the documents whose hash changed. The others
just take over the process id of the run, and at the end of it everything the
run did not write is purged. The status of the indexer shows how many
documents were unchanged.
The index is the channel. The GenAI application separates its indices by the channel, the way Solr does by its cores. The name of the channel is the search index of the resource channel, the same for every language.
Only the channel language. For now only the resources in the language of the channel are indexed, translations are left out.
Warning
The GenAI index knows no access rights. The documents carry no access groups, so an answer may be built from protected content. Do not index a channel with protected content as long as that is the case.
The document#
The GenAI application knows two kinds of document, told apart by type:
article- a page, made of the sections the editor arranged: text blocks keep their HTML, link lists their links. The contact point and its opening hours become sections of their own. Next to theheadline, an article carries akicker- inherited from the navigation if the page has none - and anintro.media- a binary asset with the text the CMS extracted from it.
Both carry id, channel, source, processId, objectType, title,
url, date, categories, keywords and hash. The url is absolute, as the
application links it as the source of an answer. keywords are terms the
document is to be found by although its text may not contain them - the
keywords of the metadata, and the synonyms other bundles add. Fields that are
not set are left out.
To dump what an index run would send:
bin/console index:dump-document --indexer genai /path/to/resource.php
Custom Document Enricher#
DefaultGenAiDocumentEnricher fills the document from a SiteKit resource. Own
enricher are registered with the tag of this bundle:
services:
Atoolo\Examples\GenAi\Indexer\Enricher\CustomDocumentEnricher:
tags:
- { name: "atoolo_genai.indexer.document_enricher", priority: 10 }
To add terms without overwriting those of other enricher, use
GenAiDocument::addKeywords().
Scheduling#
Through the index bundle:
parameters:
atoolo_index.indexer.schedules:
genai: "0 3 * * *"
HTTP contract#
The bundle prepends nothing but {GENAI_URL} to the path, because the GenAI
application serves its parts under different roots:
| Purpose | Request |
|---|---|
| health | GET /actuator/health ({"status":"UP"}) |
| bulk update | POST /api/index/documents, body is a bare list of documents, answers {documents, chunks, unchanged} |
| delete by id | POST /api/index/documents/delete {channel, source, ids} |
| purge by process id | POST /api/index/purge {channel, source, keepProcessId} |
| ask | GraphQL POST /graphql, query question(query!, language!, channel!, categoryIds) |
| feedback | GraphQL POST /graphql, mutation answerFeedback(feedbackToken!, feedback) |
An empty bulk sends no request at all. The errors GraphQL reports with status
200 are treated as a failed request as well. The GraphQL requests carry the
client ip in X-Forwarded-For.
Assistant#
The assistant asks the GenAI application a question and passes on the
feedback of a user on the answer. Following the
errors as data pattern, the
result is modelled as the application delivers it: ask() returns a
QuestionResult, which is an Answer or one of the errors that say why the
question was not answered - NoDocumentsError, NoMatchingDocumentsError,
AnswerCutOffError or UnansweredError for an error of the application the
bundle does not know yet. Every result carries an id and a feedbackToken
to give feedback with. The sections of an answer are an
AnswerTextSection with html or an AnswerLinksSection with links, each
with its sources. Only a NoMatchingDocumentsError has hints how to ask
more precisely and suggestedQuestions.
$result = $assistant->ask(new Question(
'When is the next council meeting?',
ResourceLanguage::of('en'),
['10'], // category ids, optional
));
if ($result instanceof Answer) {
foreach ($result->sections as $section) {
echo $section->headline;
if ($section instanceof AnswerTextSection) {
echo $section->html;
}
if ($section instanceof AnswerLinksSection) {
foreach ($section->links as $link) {
echo $link->label . ': ' . $link->url;
}
}
foreach ($section->sources as $source) {
echo $source->url;
}
}
} elseif ($result instanceof NoMatchingDocumentsError) {
// $result->hints, $result->suggestedQuestions
}
if ($result->feedbackToken !== null) {
$assistant->feedback($result->feedbackToken, AnswerFeedback::GOOD);
}
A question that cannot be asked at all - not accepted by the application,
too many requests, the application not available - throws an
AssistantException with its AssistantErrorType.
The question is asked in the channel of the site. A question without a language is asked in the language of the channel.
GraphQL#
Asking and giving feedback are also offered as the fields genAiQuestion and
genAiAnswerFeedback of the GraphQL API. They are
only available when the GraphQL API is installed as well, see
GraphQL Search Bundle.
Console#
bin/console genai:ask "When is the next council meeting?" --lang en --category 10
--category can be given several times.