Skip to content

Commit 04f56d1

Browse files
committed
docs: add debugging support for agent turns with optional debug traces and schema updates
1 parent 67aaa74 commit 04f56d1

1 file changed

Lines changed: 210 additions & 1 deletion

File tree

  • adminforth/documentation/docs/tutorial/08-Plugins

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)