Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.3.8
- Added write-capable `create_model_record` tool to the fixture OpenAI agent with DataAccessor permission checks.
- Updated agent instructions to use JSON envelopes and documented the new command format.
- Switched the fixture configuration to the single `openai-data` model so reads and writes share one assistant.

## 4.3.7
- Added `DataAccessor.describeAccessibleFields()` to expose per-action field metadata for AI and form builders.
- Extended the fixture OpenAI agent with schema introspection, payload sanitisation, and required-field validation when creating records.
Expand Down
56 changes: 30 additions & 26 deletions docs/AiAssistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,45 @@ dependencies.

## Fixture OpenAI Agent

The fixture now also registers an `openai` model that executes structured commands against the database. The agent expects JSON instructions and uses `DataAccessor` under the hood, so every operation is filtered by the requesting user's permissions.
The fixture now registers a single `openai-data` model that executes structured commands against the database. The agent expects
JSON envelopes describing which tool to call and relies on `DataAccessor`, so every operation is filtered by the requesting
user's permissions.

Example payload for creating a record:
Supported commands share the following shape:

```json
{
"action": "create",
"entity": "Example",
"data": {
"title": "Hello from the assistant",
"description": "Generated through the OpenAI agent"
"tool": "query_model_records",
"parameters": {
"model": "Example",
"filter": "{\"title\": \"Test\"}",
"fields": ["id", "title"],
"limit": 5
}
}
```

If the user lacks the required access token (for example, `create-example-model`), the agent responds with an authorization error instead of touching the database. The `openai` fixture user (`login: openai`, `password: openai`) belongs to the administrators group, granting full access for experimentation. Regular users can be granted permissions by assigning the `ai-assistant-openai` token to their groups.

### Discovering available fields

Before issuing a `create` command the agent can now describe the exact payload shape that is accepted for the chosen model. This
is achieved through the `fields` action which asks `DataAccessor` for the list of writable fields, their types, requirements, and
association hints. The agent trims any values that are not allowed and will stop execution if mandatory properties are missing.

Example request for the schema:

```json
{
"action": "fields",
"entity": "Example"
}
```
* `query_model_records` performs read operations using the caller's `list` permissions. Filters are expressed as JSON strings
that match the model criteria. Field projections and result limits are optional.
* `create_model_record` performs write operations through the caller's `add` permissions:

```json
{
"tool": "create_model_record",
"parameters": {
"model": "Example",
"data": {
"title": "Hello from the assistant",
"description": "Generated through the OpenAI agent"
}
}
}
```

The response enumerates each accessible field, including required flags, optional descriptions (taken from field tooltips),
allowed enums, and association targets. When a `create` command is executed afterwards the agent automatically reuses this
metadata to validate the payload and report missing values instead of failing with a generic database error.
If the user lacks the required access token (for example, `add-example-model`), the agent responds with an authorization error
instead of touching the database. The `openai` fixture user (`login: openai`, `password: openai`) belongs to the administrators
group, granting full access for experimentation. Regular users can be granted permissions by assigning the
`ai-assistant-openai-data` token to their groups.

## Backend Overview

Expand Down
4 changes: 2 additions & 2 deletions fixture/adminizerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,8 @@ const config: AdminpanelConfig = {
},
aiAssistant: {
enabled: true,
defaultModel: 'openai',
models: ['openai'],
defaultModel: 'openai-data',
models: ['openai-data'],
},
routePrefix: routePrefix,
// routePrefix: "/admin",
Expand Down
78 changes: 63 additions & 15 deletions fixture/helpers/ai/OpenAiDataAgentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,33 +77,33 @@ export class OpenAiDataAgentService extends AbstractAiModelService {

const dataQueryTool = tool({
name: 'query_model_records',
description: 'Query Adminizer models using DataAccessor. Provide the model name from the admin panel configuration.',
description: 'Read Adminizer records through DataAccessor with the active user\'s permissions.',
parameters: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Model name as defined in the Adminizer configuration',
minLength: 1
description: 'Model name as defined in the Adminizer configuration.',
minLength: 1,
},
filter: {
type: 'string',
description: 'Optional filter as a JSON string matching the model criteria'
description: 'Optional filter as a JSON string matching the model criteria.',
},
fields: {
type: 'array',
items: { type: 'string', minLength: 1 },
description: 'Optional list of fields to include in the response'
items: {type: 'string', minLength: 1},
description: 'Optional list of fields to include in the response.',
},
limit: {
type: 'number',
minimum: 1,
maximum: 50,
description: 'Maximum number of records to return (default 10).'
}
description: 'Maximum number of records to return (default 10).',
},
},
required: ['model', 'filter', 'fields', 'limit'],
additionalProperties: false
required: ['model'],
additionalProperties: false,
},
execute: async (input: any, runContext?: RunContext<AgentContext>) => {
const activeUser = runContext?.context?.user ?? user;
Expand All @@ -127,7 +127,8 @@ export class OpenAiDataAgentService extends AbstractAiModelService {
}
}
const records = await entity.model.find(criteria, accessor);
const limited = records.slice(0, input.limit ?? 10);
const limit = typeof input.limit === 'number' ? input.limit : 10;
const limited = records.slice(0, limit);
const projected = input.fields && input.fields.length > 0
? limited.map((record) => this.pickFields(record, input.fields ?? []))
: limited;
Expand All @@ -140,19 +141,66 @@ export class OpenAiDataAgentService extends AbstractAiModelService {
},
});

const dataCreateTool = tool({
name: 'create_model_record',
description: 'Create Adminizer records through DataAccessor. Provide a JSON payload with the field values for the new record.',
parameters: {
type: 'object',
properties: {
model: {
type: 'string',
description: 'Model name as defined in the Adminizer configuration.',
minLength: 1,
},
data: {
type: 'object',
description: 'Field values for the new record that comply with the model schema.',
},
},
required: ['model', 'data'],
additionalProperties: false,
},
execute: async (input: any, runContext?: RunContext<AgentContext>) => {
const activeUser = runContext?.context?.user ?? user;

if (!input.model) {
throw new Error('Model name is required');
}

if (!input.data || typeof input.data !== 'object' || Array.isArray(input.data)) {
throw new Error('Data must be a JSON object with field values.');
}

const entity = this.resolveEntity(input.model);
if (!entity.model) {
throw new Error(`Model "${input.model}" is not registered in Adminizer.`);
}

const accessor = new DataAccessor(this.adminizer, activeUser, entity, 'add');
const created = await entity.model.create(input.data, accessor);

return JSON.stringify({
model: entity.name,
record: created,
}, null, 2);
},
});

return new Agent<AgentContext>({
name: 'Adminizer data agent',
instructions: [
'You are an assistant that answers questions using Adminizer data.',
'Always rely on the provided tool to inspect database records.',
'Only include fields that are relevant to the question.',
'Summaries should explain how the answer was derived from the data.',
'Respond with JSON commands that describe which tool to invoke and the required parameters.',
'Use "query_model_records" to read data. Example: {"tool": "query_model_records", "parameters": {"model": "Example", "filter": "{\\"id\\": 1}"}}',
'Use "create_model_record" to add data. Example: {"tool": "create_model_record", "parameters": {"model": "Example", "data": {"title": "Test"}}}',
'Only include fields that are relevant to the user\'s request and confirm successful writes with the created payload.',
'Summaries should explain how the answer was derived from the data or acknowledge that a record was created.',
'',
'Accessible models:',
modelSummary,
].join('\n'),
handoffDescription: 'Retrieves Adminizer records using DataAccessor with full permission checks.',
tools: [dataQueryTool],
tools: [dataQueryTool, dataCreateTool],
model: this.model,
});
}
Expand Down