@adminforth/agent 1.44.2 → 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/agentTurnService.ts +526 -0
  2. package/build.log +3 -2
  3. package/chatSurfaceService.ts +189 -0
  4. package/custom/ChatSurfaceSettings.vue +125 -0
  5. package/custom/incremark_code_renderers/incremarkRenderer.ts +5 -5
  6. package/dist/agentTurnService.d.ts +70 -0
  7. package/dist/agentTurnService.js +453 -0
  8. package/dist/chatSurfaceService.d.ts +29 -0
  9. package/dist/chatSurfaceService.js +142 -0
  10. package/dist/custom/ChatSurfaceSettings.vue +125 -0
  11. package/dist/custom/incremark_code_renderers/incremarkRenderer.ts +5 -5
  12. package/dist/endpoints/chatSurfaces.d.ts +3 -0
  13. package/dist/endpoints/chatSurfaces.js +91 -0
  14. package/dist/endpoints/context.d.ts +30 -0
  15. package/dist/endpoints/context.js +1 -0
  16. package/dist/endpoints/core.d.ts +3 -0
  17. package/dist/endpoints/core.js +106 -0
  18. package/dist/endpoints/sessions.d.ts +3 -0
  19. package/dist/endpoints/sessions.js +177 -0
  20. package/dist/errors.d.ts +2 -0
  21. package/dist/errors.js +9 -0
  22. package/dist/index.d.ts +5 -42
  23. package/dist/index.js +50 -808
  24. package/dist/sessionStore.d.ts +19 -0
  25. package/dist/sessionStore.js +83 -0
  26. package/dist/types.d.ts +4 -0
  27. package/endpoints/chatSurfaces.ts +93 -0
  28. package/endpoints/context.ts +66 -0
  29. package/endpoints/core.ts +113 -0
  30. package/endpoints/sessions.ts +183 -0
  31. package/errors.ts +10 -0
  32. package/index.ts +60 -907
  33. package/package.json +2 -2
  34. package/sessionStore.ts +94 -0
  35. package/types.ts +5 -0
  36. package/agentResponseEvents.ts +0 -1
  37. package/dist/agentResponseEvents.d.ts +0 -1
  38. package/dist/agentResponseEvents.js +0 -1
@@ -0,0 +1,189 @@
1
+ import type {
2
+ AdminUser,
3
+ ChatSurfaceAdapter,
4
+ ChatSurfaceEventSink,
5
+ ChatSurfaceIncomingMessage,
6
+ IAdminForth,
7
+ } from "adminforth";
8
+ import { Filters } from "adminforth";
9
+ import { randomUUID } from "crypto";
10
+ import type { AgentEventEmitter } from "./agentEvents.js";
11
+ import type { HandleTurnInput } from "./agentTurnService.js";
12
+ import type { PluginOptions } from "./types.js";
13
+ import type { AgentSessionStore } from "./sessionStore.js";
14
+
15
+ type ChatSurfaceConnectAction = {
16
+ type: "url";
17
+ label: string;
18
+ url: string;
19
+ };
20
+
21
+ export type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
22
+ createConnectAction?(input: {
23
+ token: string;
24
+ }): ChatSurfaceConnectAction | Promise<ChatSurfaceConnectAction>;
25
+ };
26
+
27
+ type ChatSurfaceLinkTokenPayload = {
28
+ surface: string;
29
+ adminUserId: AdminUser["pk"];
30
+ expiresAt: number;
31
+ };
32
+
33
+ const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
34
+ const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
35
+
36
+ export class ChatSurfaceService {
37
+ private linkTokens = new Map<string, ChatSurfaceLinkTokenPayload>();
38
+
39
+ constructor(
40
+ private getAdminforth: () => IAdminForth,
41
+ private options: PluginOptions,
42
+ private sessionStore: AgentSessionStore,
43
+ private handleTurn: (input: HandleTurnInput) => Promise<unknown>,
44
+ ) {}
45
+
46
+ getConnectActionAdapters() {
47
+ return (this.options.chatSurfaceAdapters ?? [])
48
+ .map((adapter) => adapter as ChatSurfaceAdapterWithConnectAction)
49
+ .filter((adapter) => adapter.createConnectAction);
50
+ }
51
+
52
+ createLinkToken(surface: string, adminUser: AdminUser) {
53
+ for (const [token, payload] of this.linkTokens) {
54
+ if (payload.expiresAt <= Date.now()) {
55
+ this.linkTokens.delete(token);
56
+ }
57
+ }
58
+
59
+ const token = randomUUID();
60
+ this.linkTokens.set(token, {
61
+ surface,
62
+ adminUserId: adminUser.pk,
63
+ expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
64
+ });
65
+
66
+ return token;
67
+ }
68
+
69
+ private consumeLinkToken(surface: string, token: string) {
70
+ const payload = this.linkTokens.get(token);
71
+ this.linkTokens.delete(token);
72
+
73
+ if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
74
+ return null;
75
+ }
76
+
77
+ return payload;
78
+ }
79
+
80
+ private createEventEmitter(sink: ChatSurfaceEventSink): AgentEventEmitter {
81
+ return async (event) => {
82
+ if (event.type === "text-delta") {
83
+ await sink.emit({
84
+ type: "text_delta",
85
+ delta: event.delta,
86
+ });
87
+ return;
88
+ }
89
+
90
+ if (event.type === "response") {
91
+ await sink.emit({
92
+ type: "done",
93
+ text: event.text,
94
+ });
95
+ return;
96
+ }
97
+
98
+ if (event.type === "error") {
99
+ await sink.emit({
100
+ type: "error",
101
+ message: event.error,
102
+ });
103
+ }
104
+ };
105
+ }
106
+
107
+ private async handleLink(
108
+ incoming: ChatSurfaceIncomingMessage,
109
+ sink: ChatSurfaceEventSink,
110
+ ) {
111
+ if (typeof incoming.metadata?.startPayload !== "string") {
112
+ return false;
113
+ }
114
+
115
+ const payload = this.consumeLinkToken(incoming.surface, incoming.metadata.startPayload);
116
+ if (!payload) {
117
+ await sink.emit({
118
+ type: "error",
119
+ message: "This chat surface link is expired or invalid. Please start linking again from AdminForth.",
120
+ });
121
+ return true;
122
+ }
123
+ const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
124
+ const adminforth = this.getAdminforth();
125
+ const authResourceId = adminforth.config.auth!.usersResourceId!;
126
+ const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
127
+ const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
128
+ const adminUserRecord = await adminforth.resource(authResourceId).get([
129
+ Filters.EQ(primaryKeyField, payload.adminUserId),
130
+ ]);
131
+
132
+ await adminforth.resource(authResourceId).update(payload.adminUserId, {
133
+ [externalUserIdField]: {
134
+ ...(adminUserRecord[externalUserIdField] ?? {}),
135
+ [incoming.surface]: incoming.externalUserId,
136
+ },
137
+ });
138
+ await sink.emit({
139
+ type: "done",
140
+ text: `${incoming.surface} account connected to AdminForth.`,
141
+ });
142
+
143
+ return true;
144
+ }
145
+
146
+ async handleMessage(
147
+ adapter: ChatSurfaceAdapter,
148
+ incoming: ChatSurfaceIncomingMessage,
149
+ sink: ChatSurfaceEventSink,
150
+ ) {
151
+ if (await this.handleLink(incoming, sink)) {
152
+ return;
153
+ }
154
+
155
+ const adminforth = this.getAdminforth();
156
+ const authResourceId = adminforth.config.auth!.usersResourceId!;
157
+ const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
158
+ const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
159
+ const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
160
+ const adminUserRecord = (
161
+ await adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))
162
+ ).find((user) => user[externalUserIdField]?.[adapter.name] === incoming.externalUserId);
163
+
164
+ if (!adminUserRecord) {
165
+ await sink.emit({
166
+ type: "error",
167
+ message: "This chat account is not authorized to use AdminForth Agent.",
168
+ });
169
+ return;
170
+ }
171
+
172
+ const adminUser = {
173
+ pk: adminUserRecord[primaryKeyField],
174
+ username: adminUserRecord[adminforth.config.auth!.usernameField],
175
+ dbUser: adminUserRecord,
176
+ };
177
+
178
+ await this.handleTurn({
179
+ prompt: incoming.prompt,
180
+ sessionId: await this.sessionStore.getOrCreateChatSurfaceSession(incoming, adminUser),
181
+ modeName: incoming.modeName,
182
+ userTimeZone: incoming.userTimeZone ?? "UTC",
183
+ adminUser,
184
+ emit: this.createEventEmitter(sink),
185
+ failureLogMessage: `Agent ${incoming.surface} surface response failed`,
186
+ abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
187
+ });
188
+ }
189
+ }
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <div class="flex flex-col justify-center mr-6 md:mr-12">
3
+ <h2 class="flex items-start justify-start leading-none text-gray-800 dark:text-gray-50 text-3xl font-semibold">
4
+ {{ $t('Chat Surfaces') }}
5
+ </h2>
6
+ <p class="text-sm mt-3">
7
+ {{ $t('Connect external chat accounts to your AdminForth user') }}
8
+ </p>
9
+
10
+ <div class="mt-6 flex flex-wrap gap-4">
11
+ <div
12
+ v-for="surface in surfaces"
13
+ :key="surface.name"
14
+ class="flex flex-col w-full lg:w-72 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm"
15
+ >
16
+ <div class="flex items-center justify-between gap-3 mb-4">
17
+ <div class="min-w-0">
18
+ <p class="font-semibold text-gray-900 dark:text-white truncate">
19
+ {{ formatSurfaceName(surface.name) }}
20
+ </p>
21
+ <p class="text-xs text-gray-500 dark:text-gray-400">
22
+ {{ surface.externalUserId || $t('Not connected') }}
23
+ </p>
24
+ </div>
25
+ <span
26
+ class="shrink-0 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
27
+ :class="surface.externalUserId
28
+ ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
29
+ : 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
30
+ >
31
+ {{ surface.externalUserId ? $t('Active') : $t('Inactive') }}
32
+ </span>
33
+ </div>
34
+
35
+ <div class="grid gap-2 mt-auto" :class="surface.externalUserId ? 'grid-cols-2' : 'grid-cols-1'">
36
+ <Button
37
+ class="w-full"
38
+ :disabled="isSurfaceBusy(surface.name)"
39
+ :loader="connectingSurfaceName === surface.name"
40
+ @click="connectSurface(surface.name)"
41
+ >
42
+ {{ surface.externalUserId ? $t('Reconnect') : $t('Connect') }}
43
+ </Button>
44
+ <Button
45
+ v-if="surface.externalUserId"
46
+ class="w-full"
47
+ :disabled="isSurfaceBusy(surface.name)"
48
+ :loader="disconnectingSurfaceName === surface.name"
49
+ @click="disconnectSurface(surface.name)"
50
+ >
51
+ {{ $t('Disconnect') }}
52
+ </Button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { onMounted, ref } from 'vue';
61
+ import { Button } from '@/afcl';
62
+ import { callAdminForthApi } from '@/utils';
63
+
64
+ type ChatSurface = {
65
+ name: string;
66
+ externalUserId: string | null;
67
+ };
68
+
69
+ const surfaces = ref<ChatSurface[]>([]);
70
+ const connectingSurfaceName = ref<string | null>(null);
71
+ const disconnectingSurfaceName = ref<string | null>(null);
72
+
73
+ onMounted(loadSurfaces);
74
+
75
+ async function loadSurfaces() {
76
+ const response = await callAdminForthApi({
77
+ method: 'POST',
78
+ path: '/agent/surfaces/connectable',
79
+ body: {},
80
+ });
81
+
82
+ surfaces.value = response.surfaces;
83
+ }
84
+
85
+ async function connectSurface(surfaceName: string) {
86
+ connectingSurfaceName.value = surfaceName;
87
+
88
+ try {
89
+ const response = await callAdminForthApi({
90
+ method: 'POST',
91
+ path: `/agent/surface/${surfaceName}/connect-action`,
92
+ body: {},
93
+ });
94
+
95
+ if (response.action.type === 'url') {
96
+ window.open(response.action.url, '_blank', 'noopener,noreferrer');
97
+ }
98
+ } finally {
99
+ connectingSurfaceName.value = null;
100
+ }
101
+ }
102
+
103
+ async function disconnectSurface(surfaceName: string) {
104
+ disconnectingSurfaceName.value = surfaceName;
105
+
106
+ try {
107
+ await callAdminForthApi({
108
+ method: 'POST',
109
+ path: `/agent/surface/${surfaceName}/disconnect`,
110
+ body: {},
111
+ });
112
+ await loadSurfaces();
113
+ } finally {
114
+ disconnectingSurfaceName.value = null;
115
+ }
116
+ }
117
+
118
+ function isSurfaceBusy(surfaceName: string) {
119
+ return connectingSurfaceName.value === surfaceName || disconnectingSurfaceName.value === surfaceName;
120
+ }
121
+
122
+ function formatSurfaceName(surfaceName: string) {
123
+ return surfaceName.charAt(0).toUpperCase() + surfaceName.slice(1);
124
+ }
125
+ </script>
@@ -625,11 +625,11 @@ function sanitizeClassToken(value: string): string {
625
625
 
626
626
  function escapeHtml(value: string): string {
627
627
  return value
628
- .replaceAll('&', '&amp;')
629
- .replaceAll('<', '&lt;')
630
- .replaceAll('>', '&gt;')
631
- .replaceAll('"', '&quot;')
632
- .replaceAll("'", '&#39;');
628
+ .replace(/&/g, '&amp;')
629
+ .replace(/</g, '&lt;')
630
+ .replace(/>/g, '&gt;')
631
+ .replace(/"/g, '&quot;')
632
+ .replace(/'/g, '&#39;');
633
633
  }
634
634
 
635
635
  function escapeAttribute(value: string): string {
@@ -0,0 +1,70 @@
1
+ import type { AdminUser, AudioAdapter, IAdminForth } from "adminforth";
2
+ import type { BaseCheckpointSaver } from "@langchain/langgraph";
3
+ import type { AgentEventEmitter } from "./agentEvents.js";
4
+ import type { CurrentPageContext } from "./agent/tools/getUserLocation.js";
5
+ import type { AgentSessionStore } from "./sessionStore.js";
6
+ import type { PluginOptions } from "./types.js";
7
+ export type RunAndPersistAgentResponseInput = {
8
+ prompt: string;
9
+ sessionId: string;
10
+ modeName?: string | null;
11
+ userTimeZone: string;
12
+ currentPage?: CurrentPageContext;
13
+ abortSignal?: AbortSignal;
14
+ adminUser: AdminUser;
15
+ emit?: AgentEventEmitter;
16
+ failureLogMessage: string;
17
+ abortLogMessage: string;
18
+ };
19
+ export type RunAndPersistAgentResponseResult = {
20
+ text: string;
21
+ turnId: string;
22
+ aborted: boolean;
23
+ failed: boolean;
24
+ };
25
+ export type HandleTurnInput = Omit<RunAndPersistAgentResponseInput, "failureLogMessage" | "abortLogMessage"> & {
26
+ emit: AgentEventEmitter;
27
+ failureLogMessage?: string;
28
+ abortLogMessage?: string;
29
+ };
30
+ export type HandleSpeechTurnInput = Omit<HandleTurnInput, "prompt"> & {
31
+ audioAdapter: AudioAdapter;
32
+ audio: {
33
+ buffer: Buffer;
34
+ filename: string;
35
+ mimeType: string;
36
+ };
37
+ };
38
+ type AgentTurnServiceOptions = {
39
+ getAdminforth: () => IAdminForth;
40
+ getPluginInstanceId: () => string;
41
+ options: PluginOptions;
42
+ sessionStore: AgentSessionStore;
43
+ getCheckpointer: () => BaseCheckpointSaver;
44
+ getInternalAgentResourceIds: () => string[];
45
+ getAgentSystemPrompt: () => Promise<string>;
46
+ };
47
+ export declare class AgentTurnService {
48
+ private serviceOptions;
49
+ constructor(serviceOptions: AgentTurnServiceOptions);
50
+ private runAgentTurn;
51
+ runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput): Promise<{
52
+ text: string;
53
+ turnId: any;
54
+ aborted: boolean;
55
+ failed: boolean;
56
+ }>;
57
+ handleTurn(input: HandleTurnInput): Promise<{
58
+ text: string;
59
+ turnId: any;
60
+ aborted: boolean;
61
+ failed: boolean;
62
+ }>;
63
+ handleSpeechTurn(input: HandleSpeechTurnInput): Promise<{
64
+ text: string;
65
+ turnId: any;
66
+ aborted: boolean;
67
+ failed: boolean;
68
+ } | null>;
69
+ }
70
+ export {};