Apply AI action — DGS integration with LLM services
Overview
The AIConnector plugin extends the DGS with AI-capable fields: fields that implement IStoreAIField (e.g. AITextareaField, AIFileField) can define a prompt and show an "Apply AI" button. Clicking it sends the prompt (with placeholders replaced by form values) to an LLM service (e.g. ChatGPT API, Claude, Ollama) and applies the result back to the form. The connection to the LLM is configurable via Core and DGS options.
Typical uses: fix grammar or spelling, rewrite text, extract structured data from a field or from an uploaded file (e.g. CV parsing).
1. Configuration (Core / DGS)
The connection to the LLM service is configured in Core. Example (e.g. in your config):
$GLOBALS['config']['ai'] = [
'provider' => 'openai', // provider: openai, claude, ollama
'model' => 'gpt-4', // model ID
'key' => 'sk-proj-XXXX', // API key
'org_id' => 'org-xxxxxxxx', // optional; e.g. OpenAI org
'endpoint' => null, // optional; override API base URL
];
ApplyAIAction reads this section via Core::getInstance()->getConfigValue('ai') and validates it through AIProviderConfig.
Attributes:
- provider — AI provider:
openai,claude, orollama. Can be overridden per table. - model — Model ID (e.g.
gpt-4,claude-3-opus). Can be overridden per table. - key — API key for the LLM provider.
- org_id — Optional organization ID (e.g. OpenAI).
- endpoint — Optional; leave
nullto use the provider default.
Table-level overrides (DGS): in the table definition you can set a default model and/or provider for that store:
<table name="candidates"
primaryKey="id"
aiModel="gpt-4"
aiProvider="openai"
...>
- aiModel — Model ID for this table. Use empty or omit to use global config.
- aiProvider — AI provider for this table. Use empty or omit to use global config.
2. AI-capable field types and attributes
AI-capable field types (e.g. aiTextarea, aiFile) extend the standard DGS field types and support the prompt attribute. Add a prompt to enable the "Apply AI" button next to the field. The prompt is a template: use %fieldName% for current form values (and, in event handlers, custom placeholders). Clicking "Apply AI" sends the prepared prompt to the LLM and applies the returned value(s) to the form.
Example — aiTextarea:
<field type="Plugins\AIConnector\Domain\Store\Field\AITextareaField"
name="summary"
caption="<?php echo __('Summary'); ?>"
prompt="Summarize in 2 sentences: %summary%"
aiFormat="text" />
Example — aiFile:
<field type="Plugins\AIConnector\Domain\Store\Field\AIFileField"
name="cv"
caption="<?php echo __('CV'); ?>"
prompt="Extract skills and experience from the document. Return JSON with keys: skills, experience."
aiFormat="json"
logTag="cv_extract" />
Example — single field, grammar (prompt with placeholders):
<field type="Plugins\AIConnector\Domain\Store\Field\AITextareaField"
name="description"
prompt="Fix grammar in: %description%. Add context from id: %id%. Return only the corrected text."
aiFormat="text"
caption="<?php echo __('Description'); ?>" />
Field attributes (AI options):
| Option | XML attribute | Description |
|---|---|---|
| Prompt | prompt |
Required for Apply AI. Template for the LLM; use %fieldName% for form values. |
| Format | aiFormat |
text (default) or json. Use json for structured output and mapping to multiple fields. |
| Log tag | logTag |
Optional tag for log entries in festi_ai_logs (filtering, analytics by use case). |
Placeholders like %description% and %id% are replaced with the current form snapshot. For file fields or custom logic, use BeforePromptPrepareEvent to add virtual placeholders (e.g. extracted file text).
3. Enable the Apply AI action in the store
In the table XML, declare the action so the store and the "Apply AI" button use it:
<actions>
<action type="<?php echo \Plugins\AIConnector\Domain\Store\Action\ApplyAIAction::class; ?>"
caption="<?php echo __('Apply AI'); ?>" />
</actions>
4. Frontend (Apply AI button and script)
The plugin provides static/js/ai-connector.js (exposes window.AIConnector). It:
- Binds the "Apply AI" button next to fields that have a
prompt. - Sends the current form (and optional file) to the backend Apply AI action.
- Applies the returned result to the form and supports undo.
Include this script on store form views where the store has the Apply AI action and fields with prompt.
5. Event hooks (optional)
For advanced behaviour (custom placeholders, JSON schema, mapping AI output to multiple fields), subscribe to these events on the store that runs Apply AI. All event classes are in Plugins\AIConnector\Domain\Store\Event\*.
Events (in execution order):
BeforePromptPrepareEvent— before placeholder substitution on the prompt template.BeforeAIRequestEvent— before the HTTP request to the LLM (override model, system instructions, JSON schema, register extractors).BeforeResolvedValuesPrepareEvent— between response processing and resolution (recover malformed JSON, add warnings, provide custom field mapping).AfterAICompleteEvent— after the full flow finishes (logging, analytics, response data access).
BeforePromptPrepareEvent::EVENT_TYPE
Fired before placeholder substitution runs on the prompt template. Use it to inject virtual placeholders (e.g. extracted file text) or to replace the prepared prompt entirely.
Event data:
- getVirtualPlaceholders(): array (by reference) — add entries here; they are merged with form values before %placeholder% substitution.
- setPreparedPrompt(string) / getPreparedPrompt() — set the final prompt string directly, bypassing substitution.
- setPromptTemplate(string) / getPromptTemplate() — change the template before substitution runs.
- getField(), getContext(), getValues(), getFile() — read-only context from the request.
Listening:
use Plugins\AIConnector\Domain\Store\Event\BeforePromptPrepareEvent;
$this->addEventListener(
BeforePromptPrepareEvent::EVENT_TYPE,
function (BeforePromptPrepareEvent &$event) {
$file = $event->getFile();
$cvText = $this->_extractTextFromFile($file);
$placeholders = &$event->getVirtualPlaceholders();
$placeholders['cv_text'] = $cvText;
}
);
BeforeAIRequestEvent::EVENT_TYPE
Fired once per Apply AI call, immediately before the HTTP request is sent to the LLM. Use it to override request parameters or register document extractors on the client.
Event data:
- setMaxTokens(int) / getMaxTokens() — override the token limit for this request.
- setSystemInstructions(string) / getSystemInstructions() — set the system message prepended to the conversation.
- setJsonSchema(array) / getJsonSchema() — pass a JSON schema to enforce structured output (OpenAI only).
- setAIModel(string) / getAIModel() — override the model for this request.
- setAIFormat(string) / getAIFormat() — override the response format (text or json).
- getClient(): OpenAIClient|ClaudeClient|OllamaClient|null — the active AI client. For OpenAI and Claude, you can register document extractors (e.g. DOCX, PDF).
Listening:
use AI\Utils\Extractor\IDocumentExtraction;
use Plugins\AIConnector\Domain\Store\Event\BeforeAIRequestEvent;
use libs\AI\Extractor\PhpWordExtractor;
$this->addEventListener(
BeforeAIRequestEvent::EVENT_TYPE,
function (BeforeAIRequestEvent &$event) {
$event->setMaxTokens(1200);
$event->setSystemInstructions('You are a CV extraction assistant. Respond only in JSON.');
$client = $event->getClient();
if (!$client instanceof IDocumentExtraction) {
return;
}
$extractor = new PhpWordExtractor();
$client->registerDocumentExtractor($extractor);
}
);
BeforeResolvedValuesPrepareEvent::EVENT_TYPE
Fired after the AI response is processed (parsed, truncated) but before the default resolver maps it to form fields. Use it to recover malformed JSON, add warnings, or provide custom resolved values (e.g. competency matching, country resolution for CV parsing).
Event data:
- getContent(): string — raw LLM response text.
- getData(): ?array — parsed JSON (or null if decode failed).
- setData(?array) — replace or recover the parsed data (e.g. after custom JSON recovery).
- getWarnings(): array (by reference) — warnings collected so far.
- setWarnings(array) / addWarning(string) — add warnings to show the user.
- getResolvedValues(): ?AIActionValues — custom resolved values, or null to use default resolver.
- setResolvedValues(AIActionValues) — provide custom mapping; bypasses the default resolver when set.
Listening:
use Plugins\AIConnector\Domain\Store\Event\BeforeResolvedValuesPrepareEvent;
use Plugins\AIConnector\Domain\Store\Action\Data\AIActionValues;
$this->addEventListener(
BeforeResolvedValuesPrepareEvent::EVENT_TYPE,
function (BeforeResolvedValuesPrepareEvent &$event) {
$field = $event->getField();
if ($event->getTable() !== 'candidates' || $field->getName() !== 'cv') {
return;
}
$data = $event->getData();
if (!$data) {
$content = $event->getContent();
$recovered = $this->_recoverJson($content);
if ($recovered) {
$event->setData($recovered);
$event->addWarning(__('AI returned invalid JSON; some data was recovered.'));
} else {
$event->addWarning(__('AI response could not be read. Try again.'));
return;
}
}
$data = $event->getData();
$values = $this->_mapToStoreFields($data);
$resolvedValues = new AIActionValues($values['scalar'], $values['many2many']);
$event->setResolvedValues($resolvedValues);
}
);
AfterAICompleteEvent::EVENT_TYPE
Fired after the full Apply AI flow finishes, whether successful or not. Intended for logging, analytics, and post-processing.
Event data:
- getError(): ?string — error message, or null on success.
- getLogTag(): ?string — the logTag attribute from the field definition (for filtering log entries).
- getPrompt(): ?Prompt — the final prompt object that was sent.
- getContent(): string — raw LLM response text.
- getResponseData(): ?array — parsed JSON response (or null).
- getResolvedValues(): ?AIActionValues — the values that were applied to the form (or null on error).
- getWarnings(): array (by reference) — any warnings collected during the flow.
Listening:
use Plugins\AIConnector\Domain\Store\Event\AfterAICompleteEvent;
$this->addEventListener(
AfterAICompleteEvent::EVENT_TYPE,
function (AfterAICompleteEvent &$event) {
$this->_logAIResult(
$event->getLogTag(),
$event->getError(),
$event->getPrompt(),
$event->getResolvedValues()?->toArray()
);
}
);
6. Use cases
Simple (AI text field): use an AI-capable field (e.g. aiTextarea), add prompt and optionally aiFormat="text". User clicks Apply AI, LLM result is applied to the field. No events required.
Structured output / multiple fields: set aiFormat="json", then use BeforeResolvedValuesPrepareEvent to map the parsed JSON to multiple store fields (or AfterAICompleteEvent for logging/analytics only).
File-based extraction (e.g. CV): use a file field with prompt and aiFormat="json". In BeforeAIRequestEvent, register a document extractor (e.g. PhpWordExtractor) via $client->registerDocumentExtractor() when using OpenAI or Claude so DOCX files are extracted. In BeforeResolvedValuesPrepareEvent, recover malformed JSON if needed and map the AI response to store fields (competency matching, country resolution, etc.). In BeforePromptPrepareEvent, you can also extract file content and set a virtual placeholder (e.g. %cv_text%).
7. Creating AI connections outside Store context
For using AI outside the DGS Store context, use the plugin's createConnection() method:
use Plugins\AIConnector\Domain\Store\Action\Client\AIProviderConfig;
$plugin = Core::getInstance()->getPluginByName('AIConnector');
// Use global config
$connection = $plugin->createConnection();
// Or use custom config with extractors
$customConfig = new AIProviderConfig('claude', 'claude-3-opus', $apiKey);
$extractor = new PhpWordExtractor();
$extractors = [
$extractor,
];
$connection = $plugin->createConnection($customConfig, $extractors);
// Use the connection
$connection->connect();
$response = $connection->ask($prompt);