Skip to content

Commit 74802a1

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth into next
2 parents b60f041 + 9a94914 commit 74802a1

14 files changed

Lines changed: 9282 additions & 30 deletions

File tree

adminforth/commands/createApp/.env.hbs

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Add only sensitive local environment variables here; non-sensitive local variables should go to .env.local

adminforth/commands/createApp/templates/.env.local.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Add only non-sensitive local environment variables here so all team members can use them with minimal setup
2+
# For sensitive local environment variables, use .env and explain to team members how to set them, ideally with a .env.example
3+
14
ADMINFORTH_SECRET=123
25
NODE_ENV=development
36
DEBUG_LEVEL=info

adminforth/commands/createApp/templates/.env.prod.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Add only non-sensitive production environment variables here so deployed applications can use them
2+
# Deliver sensitive production environment variables via your deployment platform's process environment
3+
14
NODE_ENV=production
25
DATABASE_URL={{dbUrlProd}}
36
{{#if prismaDbUrlProd}}

adminforth/commands/createApp/utils.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
296296
dbUrlProd, prismaDbUrlProd, sqliteFile
297297
} = options;
298298
const packageManagerTemplateData = getPackageManagerTemplateData(useNpm, nodeMajor);
299+
const resolvedPrismaDbUrl = includePrismaMigrations ? prismaDbUrl : null;
300+
const resolvedPrismaDbUrlProd = includePrismaMigrations ? prismaDbUrlProd : null;
299301

300302
// Build a list of files to generate
301303
const templateTasks = [
@@ -322,22 +324,22 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
322324
{
323325
src: '.env.local.hbs',
324326
dest: '.env.local',
325-
data: { dbUrl: checkIfDatabaseLocal(dbUrl) ? dbUrl : null, prismaDbUrl },
327+
data: { dbUrl: checkIfDatabaseLocal(dbUrl) ? dbUrl : null, prismaDbUrl: resolvedPrismaDbUrl },
326328
},
327329
{
328330
src: '.env.prod.hbs',
329331
dest: '.env.prod',
330-
data: { prismaDbUrlProd, dbUrlProd },
332+
data: { prismaDbUrlProd: resolvedPrismaDbUrlProd, dbUrlProd },
331333
},
332334
{
333335
src: 'readme.md.hbs',
334336
dest: 'README.md',
335-
data: { dbUrl, prismaDbUrl, appName, sqliteFile },
337+
data: { dbUrl, prismaDbUrl: resolvedPrismaDbUrl, appName, sqliteFile },
336338
},
337339
{
338340
src: 'AGENTS.md.hbs',
339341
dest: 'AGENTS.md',
340-
data: { prismaDbUrl },
342+
data: { prismaDbUrl: resolvedPrismaDbUrl },
341343
},
342344
{
343345
src: 'CLAUDE.md.hbs',
@@ -347,7 +349,7 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
347349
{
348350
src: '.agents/skills/adminforth/SKILL.md.hbs',
349351
dest: '.agents/skills/adminforth/SKILL.md',
350-
data: { prismaDbUrl },
352+
data: { prismaDbUrl: resolvedPrismaDbUrl },
351353
},
352354
{
353355
src: '.agents/skills/adminforth-permissions/SKILL.md.hbs',
@@ -368,7 +370,7 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
368370
// We'll write .env using the same content as .env.sample
369371
src: '.env.hbs',
370372
dest: '.env',
371-
data: {dbUrl, prismaDbUrl},
373+
data: { dbUrl, prismaDbUrl: resolvedPrismaDbUrl },
372374
},
373375
{
374376
src: 'adminuser.ts.hbs',
@@ -504,6 +506,7 @@ function generateFinalInstructionsPnpm(skipPrismaSetup, options) {
504506
${chalk.dim('// Go to the project directory')}
505507
${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`;
506508

509+
if (options.includePrismaMigrations)
507510
instruction += `
508511
${chalk.dim('// Generate and apply initial migration')}
509512
${chalk.dim('$')}${chalk.cyan(' pnpm makemigration --name init && pnpm migrate:local')}\n`;
@@ -525,6 +528,7 @@ function generateFinalInstructionsNpm(skipPrismaSetup, options) {
525528
${chalk.dim('// Go to the project directory')}
526529
${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`;
527530

531+
if (options.includePrismaMigrations)
528532
instruction += `
529533
${chalk.dim('// Generate and apply initial migration')}
530534
${chalk.dim('$')}${chalk.cyan(' npm run makemigration -- --name init && npm run migrate:local')}\n`;

adminforth/dataConnectors/postgres.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,14 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
205205
if (!value) {
206206
return null;
207207
}
208-
if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
209-
return dayjs(value.replace(' ', 'T') + 'Z').toISOString();
208+
if (field._underlineType == 'timestamp') {
209+
if (typeof value == 'string') {
210+
const normalizedValue = value.includes(' ') ? `${value.replace(' ', 'T')}Z` : value;
211+
return dayjs(normalizedValue).toISOString();
212+
}
213+
return dayjs(value).toISOString();
214+
} else if (field._underlineType == 'int') {
215+
return dayjs.unix(+value).toISOString();
210216
} else if (field._underlineType == 'varchar') {
211217
return dayjs(value).toISOString();
212218
} else {

adminforth/documentation/docs/tutorial/08-Plugins/01-agent.md

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,216 @@ Each item in `modes` defines a user-selectable preset in the chat UI. The select
284284

285285
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.
286286

287+
## Debugging agent turns
288+
289+
Agent debug traces are optional and are intended for auditability and debugging. When enabled, they let you reconstruct why an agent produced a response or made a change by storing the full execution sequence for the turn: LLM steps, tool calls, tool inputs and outputs, token usage, and cache information.
290+
291+
By default, only the user prompt and agent response are persisted. Full debug traces are not stored unless you configure a `debugField`, because they can be large and may significantly increase database size.
292+
293+
Add a `debug` JSON column to the turns resource:
294+
295+
```ts title="./resources/agent_resources/turns.ts"
296+
import AdminForth, { AdminForthDataTypes } from 'adminforth';
297+
import type { AdminForthResourceInput, AdminUser } from 'adminforth';
298+
import { randomUUID } from 'crypto';
299+
300+
async function allowedForSuperAdmins({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
301+
return adminUser.dbUser.role === 'superadmin';
302+
}
303+
304+
export default {
305+
dataSource: 'maindb',
306+
table: 'turns',
307+
resourceId: 'turns',
308+
label: 'Turns',
309+
columns: [
310+
{
311+
name: 'id',
312+
primaryKey: true,
313+
type: AdminForthDataTypes.STRING,
314+
fillOnCreate: () => randomUUID(),
315+
showIn: {
316+
edit: false,
317+
create: false,
318+
},
319+
},
320+
{
321+
name: 'session_id',
322+
type: AdminForthDataTypes.STRING,
323+
},
324+
{
325+
name: 'created_at',
326+
type: AdminForthDataTypes.DATETIME,
327+
fillOnCreate: () => (new Date()).toISOString(),
328+
showIn: {
329+
edit: false,
330+
create: false,
331+
},
332+
},
333+
{
334+
name: 'prompt',
335+
type: AdminForthDataTypes.TEXT,
336+
},
337+
{
338+
name: 'response',
339+
type: AdminForthDataTypes.TEXT,
340+
},
341+
//diff-add
342+
{
343+
//diff-add
344+
name: 'debug',
345+
//diff-add
346+
type: AdminForthDataTypes.JSON,
347+
//diff-add
348+
components: {
349+
//diff-add
350+
show: {
351+
//diff-add
352+
file: '@@/TurnDebugShow.vue',
353+
//diff-add
354+
},
355+
//diff-add
356+
},
357+
//diff-add
358+
showIn: {
359+
//diff-add
360+
list: false,
361+
//diff-add
362+
show: true,
363+
//diff-add
364+
edit: false,
365+
//diff-add
366+
create: false,
367+
//diff-add
368+
filter: false,
369+
//diff-add
370+
},
371+
//diff-add
372+
},
373+
],
374+
options: {
375+
allowedActions: {
376+
list: allowedForSuperAdmins,
377+
show: allowedForSuperAdmins,
378+
create: false,
379+
edit: false,
380+
delete: false,
381+
},
382+
},
383+
} as AdminForthResourceInput;
384+
```
385+
386+
Add the matching field to your schema:
387+
388+
```prisma title="./schema.prisma"
389+
model turns {
390+
id String @id
391+
session_id String
392+
created_at DateTime
393+
prompt String?
394+
response String?
395+
debug Json? //diff-add
396+
}
397+
```
398+
399+
If you use SQLite with Prisma, store the same field as text:
400+
401+
```prisma title="./schema.prisma"
402+
model turns {
403+
id String @id
404+
session_id String
405+
created_at DateTime
406+
prompt String?
407+
response String?
408+
debug String? //diff-add
409+
}
410+
```
411+
412+
AdminForth should still define this resource column as `AdminForthDataTypes.JSON`; the SQLite connector serializes it into the text column and parses it back for the renderer.
413+
414+
Run migration:
415+
416+
```bash
417+
pnpm makemigration --name add-adminforth-agent-turn-debug ; pnpm migrate:local
418+
```
419+
420+
Tell the plugin where to store debug data:
421+
422+
```ts title="./resources/adminuser.ts"
423+
new AdminForthAgent({
424+
modes: [
425+
...
426+
],
427+
sessionResource: {
428+
resourceId: 'sessions',
429+
idField: 'id',
430+
titleField: 'title',
431+
turnsField: 'turns',
432+
askerIdField: 'asker_id',
433+
createdAtField: 'created_at',
434+
},
435+
turnResource: {
436+
resourceId: 'turns',
437+
idField: 'id',
438+
sessionIdField: 'session_id',
439+
createdAtField: 'created_at',
440+
promptField: 'prompt',
441+
responseField: 'response',
442+
//diff-add
443+
debugField: 'debug',
444+
},
445+
})
446+
```
447+
448+
The `debugField` value must match the turns resource column name. You can use another column name, but then use the same name in the resource, database schema, and `debugField`.
449+
450+
Create a renderer in your app custom folder:
451+
452+
```vue title="./custom/TurnDebugShow.vue"
453+
<template>
454+
<div class="space-y-3">
455+
<div class="rounded-lg bg-slate-50 p-3 text-sm text-slate-700 dark:bg-slate-800 dark:text-slate-200">
456+
<div class="font-semibold text-slate-900 dark:text-white">Agent Debug</div>
457+
<div class="mt-1">
458+
{{ debugSequences.length }} sequences,
459+
{{ totalToolCalls }} tool calls,
460+
{{ totalCachedTokens.toLocaleString() }} cached prompt tokens
461+
</div>
462+
</div>
463+
464+
<JsonViewer :value="debugSequences" :expandDepth="2" />
465+
</div>
466+
</template>
467+
468+
<script setup lang="ts">
469+
import { computed } from 'vue';
470+
import { JsonViewer } from '@/afcl';
471+
import type { AdminForthResourceColumnCommon } from '@/types/Common';
472+
473+
type DebugToolCall = {
474+
toolName: string;
475+
};
476+
477+
type DebugSequence = {
478+
cachedTokens: number;
479+
toolCalls: DebugToolCall[];
480+
};
481+
482+
const props = defineProps<{
483+
column: AdminForthResourceColumnCommon;
484+
record: Record<string, DebugSequence[]>;
485+
}>();
486+
487+
const debugSequences = computed(() => props.record[props.column.name] ?? []);
488+
const totalToolCalls = computed(() =>
489+
debugSequences.value.reduce((sum, sequence) => sum + sequence.toolCalls.length, 0),
490+
);
491+
const totalCachedTokens = computed(() =>
492+
debugSequences.value.reduce((sum, sequence) => sum + sequence.cachedTokens, 0),
493+
);
494+
</script>
495+
```
496+
287497
# Using with self-hosted models
288498

289499
`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/)
@@ -793,4 +1003,3 @@ services:
7931003
If Cloudflare returns a 403 response with `cf-mitigated: challenge` for `<baseURL>/adminapi/v1/agent/speech-response`, the request was blocked before it reached AdminForth. Create a WAF or bot rule exception for authenticated requests to this endpoint, because browser `fetch` calls with `multipart/form-data` cannot complete an HTML challenge page.
7941004

7951005
![alt text](image-6.png)
796-

0 commit comments

Comments
 (0)