|
| 1 | +> TODO: this plugin tutorial is in progress, some information might be missing, we are actively working on it now. If you have any questions regarding this plugin, please reach out to us in GitHub issues |
| 2 | +
|
| 3 | + |
| 4 | +# Agent Plugin |
| 5 | + |
| 6 | +This plugin adds an AI agent with a chat surface to AdminForth which is capable of default skills like searching/editing data and extending with custom skills. |
| 7 | + |
| 8 | +It stores session history in your own resources and uses any AdminForth completion adapter to generate responses. |
| 9 | + |
| 10 | +## Installation |
| 11 | + |
| 12 | +```bash |
| 13 | +pnpm i adminforth-agent --save |
| 14 | +pnpm i @adminforth/completion-adapter-open-ai-chat-gpt --save |
| 15 | +``` |
| 16 | + |
| 17 | +Add your LLM credentials to `.env`: |
| 18 | + |
| 19 | +```env title=.env |
| 20 | +... |
| 21 | +OPENAI_API_KEY=your_key |
| 22 | +``` |
| 23 | + |
| 24 | +You can replace the OpenAI adapter with any completion adapter from [List of adapters](/docs/tutorial/ListOfAdapters/). |
| 25 | + |
| 26 | +## Setup |
| 27 | + |
| 28 | +First create two resources for sessions and turns: |
| 29 | + |
| 30 | +```ts title="./resources/agent_resources/sessions.ts" |
| 31 | +import AdminForth, { AdminForthDataTypes } from 'adminforth'; |
| 32 | +import type { AdminForthResourceInput } from 'adminforth'; |
| 33 | +import { randomUUID } from 'crypto'; |
| 34 | + |
| 35 | +export default { |
| 36 | + dataSource: 'sqlite', |
| 37 | + table: 'sessions', |
| 38 | + resourceId: 'sessions', |
| 39 | + label: 'Sessions', |
| 40 | + columns: [ |
| 41 | + { |
| 42 | + name: 'id', |
| 43 | + primaryKey: true, |
| 44 | + type: AdminForthDataTypes.STRING, |
| 45 | + fillOnCreate: () => randomUUID(), |
| 46 | + showIn: { |
| 47 | + edit: false, |
| 48 | + create: false, |
| 49 | + }, |
| 50 | + }, |
| 51 | + { |
| 52 | + name: 'title', |
| 53 | + type: AdminForthDataTypes.STRING, |
| 54 | + }, |
| 55 | + { |
| 56 | + name: 'turns', |
| 57 | + type: AdminForthDataTypes.INTEGER, |
| 58 | + }, |
| 59 | + { |
| 60 | + name: 'asker_id', |
| 61 | + type: AdminForthDataTypes.STRING, |
| 62 | + }, |
| 63 | + { |
| 64 | + name: 'created_at', |
| 65 | + type: AdminForthDataTypes.DATETIME, |
| 66 | + fillOnCreate: () => (new Date()).toISOString(), |
| 67 | + showIn: { |
| 68 | + edit: false, |
| 69 | + create: false, |
| 70 | + }, |
| 71 | + }, |
| 72 | + ], |
| 73 | +} as AdminForthResourceInput; |
| 74 | +``` |
| 75 | + |
| 76 | +```ts title="./resources/agent_resources/turns.ts" |
| 77 | +import AdminForth, { AdminForthDataTypes } from 'adminforth'; |
| 78 | +import type { AdminForthResourceInput } from 'adminforth'; |
| 79 | +import { randomUUID } from 'crypto'; |
| 80 | + |
| 81 | +export default { |
| 82 | + dataSource: 'sqlite', |
| 83 | + table: 'turns', |
| 84 | + resourceId: 'turns', |
| 85 | + label: 'Turns', |
| 86 | + columns: [ |
| 87 | + { |
| 88 | + name: 'id', |
| 89 | + primaryKey: true, |
| 90 | + type: AdminForthDataTypes.STRING, |
| 91 | + fillOnCreate: () => randomUUID(), |
| 92 | + showIn: { |
| 93 | + edit: false, |
| 94 | + create: false, |
| 95 | + }, |
| 96 | + }, |
| 97 | + { |
| 98 | + name: 'session_id', |
| 99 | + type: AdminForthDataTypes.STRING, |
| 100 | + }, |
| 101 | + { |
| 102 | + name: 'created_at', |
| 103 | + type: AdminForthDataTypes.DATE, |
| 104 | + fillOnCreate: () => (new Date()).toISOString(), |
| 105 | + showIn: { |
| 106 | + edit: false, |
| 107 | + create: false, |
| 108 | + }, |
| 109 | + }, |
| 110 | + { |
| 111 | + name: 'prompt', |
| 112 | + type: AdminForthDataTypes.TEXT, |
| 113 | + }, |
| 114 | + { |
| 115 | + name: 'response', |
| 116 | + type: AdminForthDataTypes.TEXT, |
| 117 | + }, |
| 118 | + ], |
| 119 | +} as AdminForthResourceInput; |
| 120 | +``` |
| 121 | + |
| 122 | +`asker_id` must store the current admin user's primary key, and `created_at` should be filled automatically because the plugin sorts sessions and turns by it. The `turns` field can stay nullable, but the plugin configuration still expects it. |
| 123 | + |
| 124 | +Add matching tables to your schema: |
| 125 | + |
| 126 | +```prisma title='./schema.prisma' |
| 127 | +model sessions { |
| 128 | + id String @id |
| 129 | + title String |
| 130 | + turns Int? |
| 131 | + asker_id String |
| 132 | + created_at DateTime |
| 133 | +} |
| 134 | +
|
| 135 | +model turns { |
| 136 | + id String @id |
| 137 | + session_id String |
| 138 | + created_at DateTime |
| 139 | + prompt String? |
| 140 | + response String? |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +Run migration: |
| 145 | + |
| 146 | +```bash |
| 147 | +pnpm makemigration --name add-adminforth-agent-tables ; pnpm migrate:local |
| 148 | +``` |
| 149 | + |
| 150 | +Register both resources in your app: |
| 151 | + |
| 152 | +```ts title="./index.ts" |
| 153 | +import sessions_resource from './resources/agent_resources/sessions.js'; |
| 154 | +import turns_resource from './resources/agent_resources/turns.js'; |
| 155 | + |
| 156 | +export const admin = new AdminForth({ |
| 157 | + ... |
| 158 | + resources: [ |
| 159 | + ... |
| 160 | + sessions_resource, |
| 161 | + turns_resource, |
| 162 | + ], |
| 163 | + ... |
| 164 | +}); |
| 165 | +``` |
| 166 | + |
| 167 | +Then attach the plugin once, usually to your `adminuser` resource: |
| 168 | + |
| 169 | +```ts title="./resources/adminuser.ts" |
| 170 | +import AdminForthAgent from 'adminforth-agent'; |
| 171 | +import CompletionAdapterOpenAIChatGPT from '@adminforth/completion-adapter-open-ai-chat-gpt'; |
| 172 | + |
| 173 | +... |
| 174 | + |
| 175 | +plugins: [ |
| 176 | + ... |
| 177 | + new AdminForthAgent({ |
| 178 | + completionAdapter: new CompletionAdapterOpenAIChatGPT({ |
| 179 | + openAiApiKey: process.env.OPENAI_API_KEY as string, |
| 180 | + model: 'gpt-5.4-mini', |
| 181 | + }), |
| 182 | + maxTokens: 10000, |
| 183 | + reasoning: 'none', |
| 184 | + sessionResource: { |
| 185 | + resourceId: 'sessions', |
| 186 | + idField: 'id', |
| 187 | + titleField: 'title', |
| 188 | + turnsField: 'turns', |
| 189 | + askerIdField: 'asker_id', |
| 190 | + createdAtField: 'created_at', |
| 191 | + }, |
| 192 | + turnResource: { |
| 193 | + resourceId: 'turns', |
| 194 | + idField: 'id', |
| 195 | + sessionIdField: 'session_id', |
| 196 | + createdAtField: 'created_at', |
| 197 | + promptField: 'prompt', |
| 198 | + responseField: 'response', |
| 199 | + // optional |
| 200 | + // debugField: 'debug', |
| 201 | + }, |
| 202 | + }), |
| 203 | +] |
| 204 | +``` |
| 205 | + |
| 206 | +The plugin adds a chat surface to the admin UI and keeps session history per admin user. |
| 207 | + |
| 208 | +## Reverse proxy and CDN configuration for streaming |
| 209 | + |
| 210 | +The agent streams responses from `<baseURL>/adminapi/v1/agent/response` using server-sent events, where `<baseURL>` is your AdminForth base path or an empty string when deployed at the domain root. If your proxy buffers responses, the UI will receive the answer only after generation is finished. |
| 211 | + |
| 212 | +For Nginx, disable response buffering on this endpoint. The critical line is `proxy_buffering off;`. |
| 213 | + |
| 214 | +```nginx |
| 215 | +location <baseURL>/adminapi/v1/agent/response { |
| 216 | + proxy_http_version 1.1; |
| 217 | + proxy_read_timeout 600s; |
| 218 | + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 219 | + proxy_set_header Host $http_host; |
| 220 | + proxy_set_header Connection ""; |
| 221 | + proxy_buffering off; # required for streaming |
| 222 | + proxy_pass http://127.0.0.1:3500; |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +Traefik forwards streaming responses immediately by default. The line that must stay off this route is any buffering middleware attachment such as `traefik.http.routers.adminforth-agent.middlewares=buffering@docker`. If your main router uses extra middlewares, create a dedicated router for the agent stream endpoint and do not attach buffering to it: |
| 227 | + |
| 228 | +```yaml title='./compose.yml' |
| 229 | +services: |
| 230 | + adminforth: |
| 231 | + labels: |
| 232 | + - "traefik.enable=true" |
| 233 | + - "traefik.http.services.adminforth.loadbalancer.server.port=3500" |
| 234 | + |
| 235 | + - "traefik.http.routers.adminforth.rule=PathPrefix(`/`)" |
| 236 | + - "traefik.http.routers.adminforth.tls=true" |
| 237 | + - "traefik.http.routers.adminforth.tls.certresolver=myresolver" |
| 238 | + - "traefik.http.routers.adminforth.middlewares=secure-headers,buffering@docker" |
| 239 | + |
| 240 | + - "traefik.http.routers.adminforth-agent.rule=Path(`<baseURL>/adminapi/v1/agent/response`)" |
| 241 | + - "traefik.http.routers.adminforth-agent.priority=100" |
| 242 | + - "traefik.http.routers.adminforth-agent.service=adminforth" |
| 243 | + - "traefik.http.routers.adminforth-agent.tls=true" |
| 244 | + - "traefik.http.routers.adminforth-agent.tls.certresolver=myresolver" |
| 245 | + # keep buffering OFF for SSE |
| 246 | + # do not add: |
| 247 | + # - "traefik.http.routers.adminforth-agent.middlewares=buffering@docker" |
| 248 | +``` |
| 249 | + |
| 250 | + Replace `<baseURL>` with the same base path you use for AdminForth. For example, when `ADMIN_BASE_URL = '/admin/'`, the endpoint becomes `/admin/adminapi/v1/agent/response`. |
| 251 | + |
| 252 | +### CDN |
| 253 | + |
| 254 | + Cloudflare by default buffers responses, which breaks streaming. To fix it, create a page rule for your domain with a "Response Body Buffering" setting turned off for the agent stream endpoint (`<baseURL>/adminapi/v1/agent/response`). |
| 255 | + |
| 256 | + |
| 257 | + |
| 258 | + |
| 259 | +## Custom skills and tools |
| 260 | + |
| 261 | + |
| 262 | +Place you skills in `custom/skills/<skill_name>/SKILL.md` file. The plugin will pick them up automatically and make available in agent's toolbox. |
| 263 | + |
| 264 | + |
| 265 | +To define custom tools, create api endpoints, prefer `admin.express.withSchema(...)` from [Custom Pages / API docs](/docs/tutorial/Customization/customPages/). That exposes machine-readable request and response schemas the agent can use. |
| 266 | + |
| 267 | +In skills markdown file, merge which tool exactlu agent should load. |
| 268 | + |
| 269 | + |
| 270 | +Skill example: |
| 271 | + |
| 272 | +// TODO |
| 273 | + |
| 274 | + |
0 commit comments