|
1 | 1 | --- |
2 | 2 | title: Agent |
3 | 3 | slug: /tutorial/Plugins/agent |
| 4 | +sidebar_position: 0 |
4 | 5 | --- |
5 | 6 |
|
6 | 7 | > 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 |
@@ -282,6 +283,202 @@ Each item in `modes` defines a user-selectable preset in the chat UI. The select |
282 | 283 |
|
283 | 284 | The plugin adds a chat surface to the admin UI, keeps session history per admin user, and shows a mode picker when `modes` are configured. |
284 | 285 |
|
| 286 | +# Using with self-hosted models |
| 287 | + |
| 288 | +`CompletionAdapterOpenAIResponses` when works with agent plugin, under the hood uses the LangChain internal proxy called `OpenAIChat` (in LangChain they call it "provider"). This proxy is capable with a fresh versions of OpenAI-compatible Responses APIs, for example [self-hosted latest versions of vLLM installations](https://devforth.io/insights/self-hosted-gpt-real-response-time-token-throughput-and-cost-on-l4-l40s-and-h100-for-gpt-oss-20b/) |
| 289 | +To use them, just point the adapter to your local vLLM server: |
| 290 | + |
| 291 | + |
| 292 | +```ts |
| 293 | +completionAdapter: new CompletionAdapterOpenAIResponses({ |
| 294 | + openAiApiKey: process.env.MY_API_KEY as string, // if you use authorization |
| 295 | + baseUrl: 'http://my_local_vllm_server:8000/v1', |
| 296 | + model: 'gpt-oss-120b', |
| 297 | +}) |
| 298 | +``` |
| 299 | + |
| 300 | +However some of 3rd party providers might serve outdated vLLM and still don't fully support the Responses API needed for langchain internal implmentation, for example [OVH AI Endpoints](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/) in Responses mode still don't play well with langchain proxy (25 Apr 2026) |
| 301 | + |
| 302 | +In that case you can try to use the OpenAI Complition API mode of the plugin, which is less efficient but more compatible with older APIs, you can force Chat Completions API mode with `useComplitionApi: true`: |
| 303 | + |
| 304 | +```ts |
| 305 | +completionAdapter: new CompletionAdapterOpenAIResponses({ |
| 306 | + openAiApiKey: process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN as string, |
| 307 | + baseUrl: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', |
| 308 | + model: 'gpt-oss-120b', |
| 309 | + useComplitionApi: true, |
| 310 | +}) |
| 311 | +``` |
| 312 | + |
| 313 | +OVH AI Endpoints still does not fully support the OpenAI `responses` API, so `useComplitionApi: false` may work unstably there. |
| 314 | + |
| 315 | + |
| 316 | + |
| 317 | + |
| 318 | +## Writing own skills |
| 319 | + |
| 320 | +You can write own skills by following [SKILL.md Agennt Skills](https://agentskills.io/) |
| 321 | + |
| 322 | +For example we will write a skill which can summarizes backoffice data stats like record counts by resources and send it to user email. |
| 323 | + |
| 324 | +It may handle prompts like |
| 325 | + |
| 326 | +``` |
| 327 | +Please send record counts for all resources to my email |
| 328 | +``` |
| 329 | + |
| 330 | +Or: |
| 331 | + |
| 332 | +``` |
| 333 | +Please send record counts to all admin users |
| 334 | +``` |
| 335 | + |
| 336 | +### Writing custom tools |
| 337 | + |
| 338 | +To define a custom tool you should simply define an express API route in your app using `admin.express.withSchema` wrapper which makes the route available for the agent (clear and predictable schema is a crucial part of making the tool work well). |
| 339 | + |
| 340 | +If you are using `admin.express.authorize` wrapper for authorization, adminuser will be injected atomatically from user which sits on the surface and controls the agent. In other words all permissions and access rights of the agent are defined by the admin user which is controlling this agent. At the same time all actions done by agent are automatically attributed in the audit log to the admin user which is controlling the agent. |
| 341 | + |
| 342 | +This example uses the same email adapter pattern shown in the Email Invite and Email Password Reset plugins. The transport below uses Mailgun only to keep the snippet short; you can replace it with SES or any other adapter from [List of adapters](/docs/tutorial/ListOfAdapters/). |
| 343 | + |
| 344 | +```ts title="./api.ts" |
| 345 | +import type { Express, Response } from 'express'; |
| 346 | +import { Filters, type IAdminForth, type IAdminUserExpressRequest } from 'adminforth'; |
| 347 | +import * as z from 'zod'; |
| 348 | +import EmailAdapterMailgun from '@adminforth/email-adapter-mailgun'; |
| 349 | + |
| 350 | +const agentEmailAdapter = new EmailAdapterMailgun({ |
| 351 | + apiKey: process.env.MAILGUN_API_KEY as string, |
| 352 | + domain: process.env.MAILGUN_DOMAIN as string, |
| 353 | + baseUrl: process.env.MAILGUN_REGION_URL || 'api.mailgun.net', |
| 354 | +}); |
| 355 | +const agentSendFrom = process.env.AGENT_SEND_FROM_EMAIL as string; |
| 356 | + |
| 357 | +function escapeHtml(value: string) { |
| 358 | + return value |
| 359 | + .replace(/&/g, '&') |
| 360 | + .replace(/</g, '<') |
| 361 | + .replace(/>/g, '>'); |
| 362 | +} |
| 363 | + |
| 364 | +function renderEmailHtml(body: string) { |
| 365 | + return `<html><body><pre style="font-family: sans-serif; white-space: pre-wrap;">${escapeHtml(body)}</pre></body></html>`; |
| 366 | +} |
| 367 | + |
| 368 | +export function initApi(app: Express, admin: IAdminForth) { |
| 369 | + app.post('/send_email_to_user', |
| 370 | + admin.express.withSchema( |
| 371 | + { |
| 372 | + description: 'Send an email to one AdminForth user by id. Use this after the user row is resolved.', |
| 373 | + request: z.object({ |
| 374 | + userId: z.string(), |
| 375 | + subject: z.string().min(1), |
| 376 | + body: z.string().min(1), |
| 377 | + }), |
| 378 | + response: z.object({ |
| 379 | + ok: z.boolean(), |
| 380 | + email: z.string().email().optional(), |
| 381 | + error: z.string().optional(), |
| 382 | + }), |
| 383 | + }, |
| 384 | + admin.express.authorize( |
| 385 | + async (req: IAdminUserExpressRequest, res: Response) => { |
| 386 | + const { userId, subject, body } = req.body as { |
| 387 | + userId: string; |
| 388 | + subject: string; |
| 389 | + body: string; |
| 390 | + }; |
| 391 | + |
| 392 | + await agentEmailAdapter.validate(); |
| 393 | + |
| 394 | + const user = await admin.resource('adminuser').get( |
| 395 | + Filters.EQ('id', userId), |
| 396 | + ); |
| 397 | + |
| 398 | + if (!user || typeof user.email !== 'string' || !user.email) { |
| 399 | + res.status(404).json({ ok: false, error: 'User not found' }); |
| 400 | + return; |
| 401 | + } |
| 402 | + |
| 403 | + const result = await agentEmailAdapter.sendEmail( |
| 404 | + agentSendFrom, |
| 405 | + user.email, |
| 406 | + body, |
| 407 | + renderEmailHtml(body), |
| 408 | + subject, |
| 409 | + ); |
| 410 | + |
| 411 | + if (!result.ok) { |
| 412 | + res.status(500).json({ ok: false, error: result.error ?? 'Failed to send email' }); |
| 413 | + return; |
| 414 | + } |
| 415 | + |
| 416 | + res.json({ ok: true, email: user.email }); |
| 417 | + } |
| 418 | + ) |
| 419 | + ) |
| 420 | + ); |
| 421 | +} |
| 422 | +``` |
| 423 | + |
| 424 | +## Define skills instructions |
| 425 | + |
| 426 | +Custom skills live in your app's custom directory. The agent loads project skills from: |
| 427 | + |
| 428 | +- `custom/skills/<skill_name>/SKILL.md` |
| 429 | + |
| 430 | +Also it can load skills from plugins, it allows other plugins to expose their own skills and tools: |
| 431 | + |
| 432 | +- `custom/plugins/adminforth-agent/skills/<skill_name>/SKILL.md` |
| 433 | + |
| 434 | +Each skill needs YAML frontmatter with `name` and `description`. The `description` is the discovery surface, so include the phrases the admin is likely to type in chat. Skills are not loaded automcatically, agent loads them |
| 435 | +on demand once understands that `description` of the skill matches the user intent, so keep the description clear and concise. |
| 436 | + |
| 437 | +Tools are also not loaded automatically, the agent loads them only if they are mentioned in the skill instructions. Then agent calls `fetch_tool_schema` meta tool to load actual schema of your custom tool. |
| 438 | + |
| 439 | +Tool names are derived from route paths. If you want the tool name to be exactly `send_email_to_user`, register the route as `/send_email_to_user`. |
| 440 | + |
| 441 | +The example below creates a minimal user skill which: |
| 442 | + |
| 443 | +- resolves a user row |
| 444 | +- reads the `total` count for each resource with `get_resource_data` |
| 445 | +- sends the final report by email with `send_email_to_user` |
| 446 | + |
| 447 | +Create the skill in your app custom folder: |
| 448 | + |
| 449 | +```md title="./custom/skills/email_resource_counts/SKILL.md" |
| 450 | +--- |
| 451 | +name: email_resource_counts |
| 452 | +description: Email record counts for each AdminForth resource to a user. Use when the user asks to send resource counts or all-record statistics by email. |
| 453 | +--- |
| 454 | + |
| 455 | +# Involved tools |
| 456 | + |
| 457 | +Use `send_email_to_user` to send the final report after you have one exact target user row. |
| 458 | + |
| 459 | +# Instructions |
| 460 | + |
| 461 | +- For each resource in system use fetch data default skill to collect total count of records in each resource. |
| 462 | +- Create html report in format `Resource Label (resourceId): count` for each resource on a new line, sort resources by count in descending order. |
| 463 | +- Use modern, stylish but compatible html formatting in the email. |
| 464 | +- Call `send_email_to_user` with the resolved user primary key, the final subject, and the final plain text body. |
| 465 | +- After the tool succeeds, tell the user the email was sent and include a short summary in chat. |
| 466 | +``` |
| 467 | + |
| 468 | +This way allows to extend your agent with literally any custom instructions and tools and make it do complex tasks related to your backoffice data and operations. |
| 469 | + |
| 470 | + |
| 471 | +## Standard skills |
| 472 | + |
| 473 | +The plugin ships with bundled skills from `plugins/adminforth-agent/custom/skills/`. These are available out of the box and can be combined with your own custom skills. |
| 474 | + |
| 475 | +| Folder | Skill name | Description | |
| 476 | +| --- | --- | --- | |
| 477 | +| `analyze_data` | `analyze_data` | Analyze AdminForth resource data, summarize trends, and create charts from fetched rows. Prefer server-side aggregation when possible and return Vega-Lite specs for charts. | |
| 478 | +| `fetch_data` | `fetch_data` | Fetch one or more exact records with filters after inspecting the resource schema. This skill is for finding rows, not for aggregations. | |
| 479 | +| `mutate_data` | `mutate_data` | Create, update, delete, or run actions on records. Before any mutation it must show the exact target row and ask the user for confirmation. | |
| 480 | + |
| 481 | + |
285 | 482 | ## Persistent checkpointer |
286 | 483 |
|
287 | 484 | If you do not configure `checkpointResource`, the plugin falls back to an in-memory `MemorySaver`. This is fine for local testing, but checkpoints are lost on process restart. |
@@ -545,17 +742,3 @@ services: |
545 | 742 |  |
546 | 743 |
|
547 | 744 |
|
548 | | -## Custom skills and tools |
549 | | - |
550 | | - |
551 | | -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. |
552 | | - |
553 | | - |
554 | | -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. |
555 | | - |
556 | | -In skills markdown file, merge which tool exactlu agent should load. |
557 | | - |
558 | | - |
559 | | -Skill example: |
560 | | - |
561 | | -// TODO |
|
0 commit comments