@adminforth/agent 1.0.0
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.
- package/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +46 -0
- package/.woodpecker/release.yml +57 -0
- package/agent/middleware/apiBasedTools.ts +109 -0
- package/agent/middleware/sequenceDebug.ts +302 -0
- package/agent/simpleAgent.ts +291 -0
- package/agent/skills/registry.ts +135 -0
- package/agent/systemPrompt.ts +69 -0
- package/agent/toolCallEvents.ts +17 -0
- package/agent/tools/apiTool.ts +99 -0
- package/agent/tools/fetchSkill.ts +58 -0
- package/agent/tools/fetchToolSchema.ts +50 -0
- package/agent/tools/index.ts +26 -0
- package/apiBasedTools.ts +625 -0
- package/build.log +30 -0
- package/custom/ChatSurface.vue +184 -0
- package/custom/ConversationArea.vue +175 -0
- package/custom/Message.vue +206 -0
- package/custom/SessionsHistory.vue +93 -0
- package/custom/ToolRenderer.vue +131 -0
- package/custom/ToolsGroup.vue +67 -0
- package/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
- package/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
- package/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
- package/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
- package/custom/package.json +26 -0
- package/custom/pnpm-lock.yaml +1467 -0
- package/custom/skills/fetch_data/SKILL.md +15 -0
- package/custom/skills/mutate_data/SKILL.md +108 -0
- package/custom/tsconfig.json +16 -0
- package/custom/types.ts +34 -0
- package/custom/useAgentStore.ts +349 -0
- package/dist/agent/middleware/apiBasedTools.js +91 -0
- package/dist/agent/middleware/sequenceDebug.js +210 -0
- package/dist/agent/simpleAgent.js +173 -0
- package/dist/agent/skills/registry.js +108 -0
- package/dist/agent/systemPrompt.js +64 -0
- package/dist/agent/toolCallEvents.js +1 -0
- package/dist/agent/tools/apiTool.js +93 -0
- package/dist/agent/tools/fetchSkill.js +51 -0
- package/dist/agent/tools/fetchToolSchema.js +36 -0
- package/dist/agent/tools/index.js +28 -0
- package/dist/apiBasedTools.js +412 -0
- package/dist/custom/ChatSurface.vue +184 -0
- package/dist/custom/ConversationArea.vue +175 -0
- package/dist/custom/Message.vue +206 -0
- package/dist/custom/SessionsHistory.vue +93 -0
- package/dist/custom/ToolRenderer.vue +131 -0
- package/dist/custom/ToolsGroup.vue +67 -0
- package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
- package/dist/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
- package/dist/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
- package/dist/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
- package/dist/custom/package.json +26 -0
- package/dist/custom/pnpm-lock.yaml +1467 -0
- package/dist/custom/skills/fetch_data/SKILL.md +15 -0
- package/dist/custom/skills/mutate_data/SKILL.md +108 -0
- package/dist/custom/tsconfig.json +16 -0
- package/dist/custom/types.ts +34 -0
- package/dist/custom/useAgentStore.ts +349 -0
- package/dist/index.js +415 -0
- package/dist/types.js +1 -0
- package/index.ts +457 -0
- package/package.json +58 -0
- package/tsconfig.json +13 -0
- package/types.ts +45 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
serializeApiBasedTool,
|
|
5
|
+
type ApiBasedTool,
|
|
6
|
+
} from "../../apiBasedTools.js";
|
|
7
|
+
|
|
8
|
+
const fetchToolSchemaSchema = z.object({
|
|
9
|
+
toolName: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Name of the API-based tool to load, for example get_resource."),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export async function createFetchToolSchemaTool(
|
|
15
|
+
apiBasedTools: Record<string, ApiBasedTool>,
|
|
16
|
+
) {
|
|
17
|
+
return tool(
|
|
18
|
+
async ({ toolName }) => {
|
|
19
|
+
const toolDefinition = apiBasedTools[toolName];
|
|
20
|
+
|
|
21
|
+
if (!toolDefinition) {
|
|
22
|
+
return JSON.stringify(
|
|
23
|
+
{
|
|
24
|
+
status: 404,
|
|
25
|
+
error: "TOOL_NOT_FOUND",
|
|
26
|
+
message: `Tool "${toolName}" not found.`,
|
|
27
|
+
},
|
|
28
|
+
null,
|
|
29
|
+
2,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
status: 200,
|
|
36
|
+
name: toolName,
|
|
37
|
+
...serializeApiBasedTool(toolDefinition),
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2,
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "fetch_tool_schema",
|
|
45
|
+
description:
|
|
46
|
+
"Fetch the schema for an API-based AdminForth tool by name and load it for later use.",
|
|
47
|
+
schema: fetchToolSchemaSchema,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ClientTool } from "@langchain/core/tools";
|
|
2
|
+
import { createFetchSkillTool } from "./fetchSkill.js";
|
|
3
|
+
import { createFetchToolSchemaTool } from "./fetchToolSchema.js";
|
|
4
|
+
import type { ApiBasedTool } from "../../apiBasedTools.js";
|
|
5
|
+
import { createApiTool } from "./apiTool.js";
|
|
6
|
+
|
|
7
|
+
export const ALWAYS_AVAILABLE_API_TOOL_NAMES = ["get_resource"] as const;
|
|
8
|
+
|
|
9
|
+
export async function createAgentTools(
|
|
10
|
+
customComponentsDir: string,
|
|
11
|
+
apiBasedTools: Record<string, ApiBasedTool>,
|
|
12
|
+
): Promise<ClientTool[]> {
|
|
13
|
+
return [
|
|
14
|
+
...ALWAYS_AVAILABLE_API_TOOL_NAMES.map((toolName) => {
|
|
15
|
+
const apiBasedTool = apiBasedTools[toolName];
|
|
16
|
+
|
|
17
|
+
if (!apiBasedTool) {
|
|
18
|
+
throw new Error(`Required base API tool "${toolName}" is missing.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return createApiTool(toolName, apiBasedTool);
|
|
22
|
+
}),
|
|
23
|
+
await createFetchSkillTool(customComponentsDir),
|
|
24
|
+
await createFetchToolSchemaTool(apiBasedTools),
|
|
25
|
+
];
|
|
26
|
+
}
|
package/apiBasedTools.ts
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AdminForthDataTypes,
|
|
3
|
+
type AdminUser,
|
|
4
|
+
type HttpExtra,
|
|
5
|
+
type IAdminForth,
|
|
6
|
+
type IAdminForthHttpResponse,
|
|
7
|
+
type IHttpServer,
|
|
8
|
+
} from 'adminforth';
|
|
9
|
+
import dayjs from 'dayjs';
|
|
10
|
+
import timezone from 'dayjs/plugin/timezone.js';
|
|
11
|
+
import utc from 'dayjs/plugin/utc.js';
|
|
12
|
+
import { PassThrough } from 'stream';
|
|
13
|
+
import { inspect } from 'util';
|
|
14
|
+
import YAML from 'yaml';
|
|
15
|
+
|
|
16
|
+
dayjs.extend(utc);
|
|
17
|
+
dayjs.extend(timezone);
|
|
18
|
+
|
|
19
|
+
type CookieItem = {
|
|
20
|
+
key: string;
|
|
21
|
+
value: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type CapturedEndpointHandlerInput = {
|
|
25
|
+
body: Record<string, unknown>;
|
|
26
|
+
adminUser?: AdminUser;
|
|
27
|
+
query: Record<string, string>;
|
|
28
|
+
headers: Record<string, any>;
|
|
29
|
+
cookies: CookieItem[];
|
|
30
|
+
response: IAdminForthHttpResponse;
|
|
31
|
+
requestUrl: string;
|
|
32
|
+
abortSignal: AbortSignal;
|
|
33
|
+
_raw_express_req: any;
|
|
34
|
+
_raw_express_res: any;
|
|
35
|
+
tr: (
|
|
36
|
+
msg: string,
|
|
37
|
+
category: string,
|
|
38
|
+
params: any,
|
|
39
|
+
pluralizationNumber?: number,
|
|
40
|
+
) => Promise<string>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type EndpointWithSchemas = {
|
|
44
|
+
method: string;
|
|
45
|
+
noAuth?: boolean;
|
|
46
|
+
path: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
request_schema?: unknown;
|
|
49
|
+
response_schema?: unknown;
|
|
50
|
+
responce_schema?: unknown;
|
|
51
|
+
handler: (input: CapturedEndpointHandlerInput) => Promise<any> | any;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type CapturedEndpoint = EndpointWithSchemas & {
|
|
55
|
+
normalizedResponseSchema?: unknown;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ToolHttpResponse = IAdminForthHttpResponse & {
|
|
59
|
+
headers: Array<[string, string]>;
|
|
60
|
+
jsonPayload?: unknown;
|
|
61
|
+
status: number;
|
|
62
|
+
message?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type ToolOverrideCallParams = Pick<ApiBasedToolCallParams, 'httpExtra' | 'inputs' | 'userTimeZone'>;
|
|
66
|
+
|
|
67
|
+
type ToolOverrideContext = {
|
|
68
|
+
output: unknown;
|
|
69
|
+
adminUser?: AdminUser;
|
|
70
|
+
httpExtra?: Partial<HttpExtra>;
|
|
71
|
+
inputs?: Record<string, unknown>;
|
|
72
|
+
userTimeZone?: string;
|
|
73
|
+
invokeTool: (toolName: string, params?: ToolOverrideCallParams) => Promise<unknown>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type ToolOverride = {
|
|
77
|
+
wipe_frontend_specific_data?: readonly string[];
|
|
78
|
+
post_process_response?: (params: ToolOverrideContext) => Promise<unknown> | unknown;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type GetResourceToolResponse = {
|
|
82
|
+
resource: {
|
|
83
|
+
columns: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
type?: string;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type GetResourceDataToolResponse = {
|
|
91
|
+
data: Array<Record<string, unknown>>;
|
|
92
|
+
total?: number;
|
|
93
|
+
options?: Record<string, unknown>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const DEFAULT_USER_TIME_ZONE = 'UTC';
|
|
97
|
+
|
|
98
|
+
const TOOL_OVERRIDES: Record<string, ToolOverride> = {
|
|
99
|
+
get_resource: {
|
|
100
|
+
wipe_frontend_specific_data: [
|
|
101
|
+
'resource.columns[].filterOptions',
|
|
102
|
+
'resource.columns[].components',
|
|
103
|
+
'resource.options.actions[].customComponent',
|
|
104
|
+
'resource.options.pageInjections',
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
get_resource_data: {
|
|
108
|
+
post_process_response: async ({ output, inputs, invokeTool, userTimeZone }) => {
|
|
109
|
+
if (hasToolError(output)) {
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resourceId = inputs?.resourceId as string;
|
|
114
|
+
const getResourceOutput = await invokeTool('get_resource', {
|
|
115
|
+
inputs: { resourceId },
|
|
116
|
+
});
|
|
117
|
+
const dateTimeColumnNames = getDateTimeColumnNames(getResourceOutput);
|
|
118
|
+
|
|
119
|
+
if (dateTimeColumnNames.length === 0) {
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const localizedTimeZone = userTimeZone ?? DEFAULT_USER_TIME_ZONE;
|
|
124
|
+
const response = output as GetResourceDataToolResponse;
|
|
125
|
+
formatDateTimeColumns(response.data, dateTimeColumnNames, localizedTimeZone);
|
|
126
|
+
|
|
127
|
+
return response;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type ApiBasedToolCallParams = {
|
|
133
|
+
adminUser?: AdminUser;
|
|
134
|
+
adminuser?: AdminUser;
|
|
135
|
+
inputs?: Record<string, unknown>;
|
|
136
|
+
httpExtra?: Partial<HttpExtra>;
|
|
137
|
+
userTimeZone?: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type ApiBasedTool = {
|
|
141
|
+
description?: string;
|
|
142
|
+
input_schema?: unknown;
|
|
143
|
+
input_schma?: unknown;
|
|
144
|
+
output_schema?: unknown;
|
|
145
|
+
call: (params?: ApiBasedToolCallParams) => Promise<string>;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function sanitizeForYaml(
|
|
149
|
+
value: unknown,
|
|
150
|
+
): unknown {
|
|
151
|
+
const traversalStack: object[] = [];
|
|
152
|
+
const serialized = JSON.stringify(value, function (this: unknown, _key: string, nestedValue: unknown) {
|
|
153
|
+
if (typeof nestedValue === 'function' || typeof nestedValue === 'symbol' || nestedValue === undefined) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof nestedValue === 'bigint') {
|
|
158
|
+
return nestedValue.toString();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof nestedValue !== 'object' || nestedValue === null) {
|
|
162
|
+
return nestedValue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (nestedValue instanceof Map) {
|
|
166
|
+
return Object.fromEntries(nestedValue);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (nestedValue instanceof Set) {
|
|
170
|
+
return Array.from(nestedValue.values());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
while (traversalStack.length > 0 && traversalStack[traversalStack.length - 1] !== this) {
|
|
174
|
+
traversalStack.pop();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (traversalStack.includes(nestedValue)) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
traversalStack.push(nestedValue);
|
|
182
|
+
return nestedValue;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (serialized === undefined) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return JSON.parse(serialized);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function serializeUnknownError(error: unknown): Record<string, unknown> {
|
|
193
|
+
if (error instanceof Error) {
|
|
194
|
+
const errorWithCause = error as Error & { cause?: unknown };
|
|
195
|
+
const errorRecord = error as unknown as Record<string, unknown>;
|
|
196
|
+
const serialized: Record<string, unknown> = {
|
|
197
|
+
name: error.name,
|
|
198
|
+
message: error.message,
|
|
199
|
+
stack: error.stack,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (errorWithCause.cause !== undefined) {
|
|
203
|
+
serialized.cause = serializeUnknownError(errorWithCause.cause);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
207
|
+
if (key in serialized) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
serialized[key] = errorRecord[key];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return serialized;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof error === 'object' && error !== null) {
|
|
218
|
+
return {
|
|
219
|
+
type: error.constructor?.name ?? 'Object',
|
|
220
|
+
inspected: inspect(error, { depth: 6, breakLength: 120 }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
type: typeof error,
|
|
226
|
+
value: error,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function wipePath(target: unknown, pathParts: string[]): void {
|
|
231
|
+
if (!target || typeof target !== 'object' || pathParts.length === 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const [currentPart, ...rest] = pathParts;
|
|
236
|
+
const isArrayTraversal = currentPart.endsWith('[]');
|
|
237
|
+
const key = isArrayTraversal ? currentPart.slice(0, -2) : currentPart;
|
|
238
|
+
const targetRecord = target as Record<string, unknown>;
|
|
239
|
+
|
|
240
|
+
if (!(key in targetRecord)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (rest.length === 0) {
|
|
245
|
+
delete targetRecord[key];
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const nextValue = targetRecord[key];
|
|
250
|
+
|
|
251
|
+
if (isArrayTraversal) {
|
|
252
|
+
if (!Array.isArray(nextValue)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const item of nextValue) {
|
|
257
|
+
wipePath(item, rest);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
wipePath(nextValue, rest);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function hasToolError(output: unknown): output is { error: unknown } {
|
|
267
|
+
return typeof output === 'object' && output !== null && 'error' in output;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getDateTimeColumnNames(output: unknown): string[] {
|
|
271
|
+
const resource = (output as GetResourceToolResponse).resource;
|
|
272
|
+
|
|
273
|
+
return resource.columns
|
|
274
|
+
.filter((column) => column.type === AdminForthDataTypes.DATETIME)
|
|
275
|
+
.map((column) => column.name);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatGmtOffset(offsetMinutes: number): string {
|
|
279
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
280
|
+
const absoluteOffsetMinutes = Math.abs(offsetMinutes);
|
|
281
|
+
const hours = Math.floor(absoluteOffsetMinutes / 60);
|
|
282
|
+
const minutes = absoluteOffsetMinutes % 60;
|
|
283
|
+
|
|
284
|
+
if (minutes === 0) {
|
|
285
|
+
return `GMT${sign}${hours}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return `GMT${sign}${hours}:${String(minutes).padStart(2, '0')}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatDateTimeValue(value: string, userTimeZone: string): string {
|
|
292
|
+
const localizedValue = dayjs.utc(value).tz(userTimeZone);
|
|
293
|
+
return `${localizedValue.format('DD MMM YYYY, HH:mm:ss.SSS')} (${formatGmtOffset(localizedValue.utcOffset())})`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatDateTimeColumns(
|
|
297
|
+
rows: Array<Record<string, unknown>>,
|
|
298
|
+
dateTimeColumnNames: string[],
|
|
299
|
+
userTimeZone: string,
|
|
300
|
+
): void {
|
|
301
|
+
for (const row of rows) {
|
|
302
|
+
for (const columnName of dateTimeColumnNames) {
|
|
303
|
+
const value = row[columnName];
|
|
304
|
+
|
|
305
|
+
if (typeof value === 'string' && value) {
|
|
306
|
+
row[columnName] = formatDateTimeValue(value, userTimeZone);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function applyToolOverride(params: {
|
|
313
|
+
adminforth: IAdminForth;
|
|
314
|
+
adminUser?: AdminUser;
|
|
315
|
+
capturedEndpointsByToolName: Record<string, CapturedEndpoint>;
|
|
316
|
+
httpExtra?: Partial<HttpExtra>;
|
|
317
|
+
inputs?: Record<string, unknown>;
|
|
318
|
+
output: unknown;
|
|
319
|
+
toolName: string;
|
|
320
|
+
userTimeZone?: string;
|
|
321
|
+
}): Promise<unknown> {
|
|
322
|
+
const {
|
|
323
|
+
adminforth,
|
|
324
|
+
adminUser,
|
|
325
|
+
capturedEndpointsByToolName,
|
|
326
|
+
httpExtra,
|
|
327
|
+
inputs,
|
|
328
|
+
output,
|
|
329
|
+
toolName,
|
|
330
|
+
userTimeZone,
|
|
331
|
+
} = params;
|
|
332
|
+
const sanitizedOutput = sanitizeForYaml(output);
|
|
333
|
+
const override = TOOL_OVERRIDES[toolName];
|
|
334
|
+
|
|
335
|
+
if (!override) {
|
|
336
|
+
return sanitizedOutput;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const path of override.wipe_frontend_specific_data ?? []) {
|
|
340
|
+
wipePath(sanitizedOutput, path.split('.'));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!override.post_process_response) {
|
|
344
|
+
return sanitizedOutput;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const postProcessedOutput = await override.post_process_response({
|
|
348
|
+
output: sanitizedOutput,
|
|
349
|
+
adminUser,
|
|
350
|
+
httpExtra,
|
|
351
|
+
inputs,
|
|
352
|
+
userTimeZone,
|
|
353
|
+
invokeTool: async (nestedToolName, nestedParams = {}) => {
|
|
354
|
+
const nestedEndpoint = capturedEndpointsByToolName[nestedToolName];
|
|
355
|
+
|
|
356
|
+
if (!nestedEndpoint) {
|
|
357
|
+
throw new Error(`Tool ${nestedToolName} is not registered`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const nestedInputs = nestedParams.inputs ?? inputs;
|
|
361
|
+
const nestedHttpExtra = nestedParams.httpExtra ?? httpExtra;
|
|
362
|
+
const nestedUserTimeZone = nestedParams.userTimeZone ?? userTimeZone;
|
|
363
|
+
const nestedOutput = await callCapturedEndpoint({
|
|
364
|
+
adminforth,
|
|
365
|
+
endpoint: nestedEndpoint,
|
|
366
|
+
adminUser,
|
|
367
|
+
inputs: nestedInputs,
|
|
368
|
+
httpExtra: nestedHttpExtra,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return applyToolOverride({
|
|
372
|
+
adminforth,
|
|
373
|
+
adminUser,
|
|
374
|
+
capturedEndpointsByToolName,
|
|
375
|
+
httpExtra: nestedHttpExtra,
|
|
376
|
+
inputs: nestedInputs,
|
|
377
|
+
output: nestedOutput,
|
|
378
|
+
toolName: nestedToolName,
|
|
379
|
+
userTimeZone: nestedUserTimeZone,
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return sanitizeForYaml(postProcessedOutput);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function endpointPathToToolName(path: string) {
|
|
388
|
+
return path
|
|
389
|
+
.replace(/^\/+/, '')
|
|
390
|
+
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
|
391
|
+
.replace(/^_+|_+$/g, '');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function normalizeCookies(cookies?: Partial<HttpExtra>['cookies']): CookieItem[] {
|
|
395
|
+
if (!cookies) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (Array.isArray(cookies)) {
|
|
400
|
+
return cookies;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return Object.entries(cookies).map(([key, value]) => ({ key, value }));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function createToolResponse(baseResponse?: IAdminForthHttpResponse): ToolHttpResponse {
|
|
407
|
+
return {
|
|
408
|
+
headers: [],
|
|
409
|
+
status: 200,
|
|
410
|
+
message: undefined,
|
|
411
|
+
setHeader(name, value) {
|
|
412
|
+
this.headers.push([name, value]);
|
|
413
|
+
baseResponse?.setHeader(name, value);
|
|
414
|
+
},
|
|
415
|
+
setStatus(code, message) {
|
|
416
|
+
this.status = code;
|
|
417
|
+
this.message = message;
|
|
418
|
+
baseResponse?.setStatus(code, message);
|
|
419
|
+
},
|
|
420
|
+
blobStream() {
|
|
421
|
+
return baseResponse?.blobStream() ?? new PassThrough();
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function createRawExpressRequest(params: {
|
|
427
|
+
adminUser?: AdminUser;
|
|
428
|
+
body: Record<string, unknown>;
|
|
429
|
+
cookies: CookieItem[];
|
|
430
|
+
headers: Record<string, any>;
|
|
431
|
+
method: string;
|
|
432
|
+
query: Record<string, string>;
|
|
433
|
+
requestUrl: string;
|
|
434
|
+
}) {
|
|
435
|
+
const cookieHeader = params.cookies
|
|
436
|
+
.map(({ key, value }) => `${key}=${value}`)
|
|
437
|
+
.join('; ');
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
adminUser: params.adminUser,
|
|
441
|
+
body: params.body,
|
|
442
|
+
destroyed: false,
|
|
443
|
+
headers: {
|
|
444
|
+
...params.headers,
|
|
445
|
+
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
|
446
|
+
},
|
|
447
|
+
method: params.method.toUpperCase(),
|
|
448
|
+
on: () => undefined,
|
|
449
|
+
query: params.query,
|
|
450
|
+
url: params.requestUrl,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function createRawExpressResponse(response: ToolHttpResponse) {
|
|
455
|
+
const rawResponse = {
|
|
456
|
+
destroyed: false,
|
|
457
|
+
on: () => undefined,
|
|
458
|
+
setHeader(name: string, value: string) {
|
|
459
|
+
response.setHeader(name, value);
|
|
460
|
+
return rawResponse;
|
|
461
|
+
},
|
|
462
|
+
status(code: number) {
|
|
463
|
+
response.status = code;
|
|
464
|
+
return rawResponse;
|
|
465
|
+
},
|
|
466
|
+
send(message: string) {
|
|
467
|
+
response.message = message;
|
|
468
|
+
return rawResponse;
|
|
469
|
+
},
|
|
470
|
+
json(payload: unknown) {
|
|
471
|
+
response.jsonPayload = payload;
|
|
472
|
+
response.message = JSON.stringify(payload);
|
|
473
|
+
return rawResponse;
|
|
474
|
+
},
|
|
475
|
+
write: () => true,
|
|
476
|
+
writeHead: () => rawResponse,
|
|
477
|
+
writableEnded: false,
|
|
478
|
+
end: () => rawResponse,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
return rawResponse;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function callCapturedEndpoint(params: {
|
|
485
|
+
adminforth: IAdminForth;
|
|
486
|
+
adminUser?: AdminUser;
|
|
487
|
+
endpoint: CapturedEndpoint;
|
|
488
|
+
httpExtra?: Partial<HttpExtra>;
|
|
489
|
+
inputs?: Record<string, unknown>;
|
|
490
|
+
}) {
|
|
491
|
+
const { adminforth, adminUser, endpoint, httpExtra, inputs } = params;
|
|
492
|
+
const response = createToolResponse(httpExtra?.response);
|
|
493
|
+
const headers = {
|
|
494
|
+
'content-type': 'application/json',
|
|
495
|
+
...(httpExtra?.headers ?? {}),
|
|
496
|
+
};
|
|
497
|
+
const body = (inputs ?? httpExtra?.body ?? {}) as Record<string, unknown>;
|
|
498
|
+
const query = httpExtra?.query ?? {};
|
|
499
|
+
const cookies = normalizeCookies(httpExtra?.cookies);
|
|
500
|
+
const requestUrl = httpExtra?.requestUrl ?? `${adminforth.config.baseUrl}/adminapi/v1${endpoint.path}`;
|
|
501
|
+
const abortController = new AbortController();
|
|
502
|
+
const rawRequest = createRawExpressRequest({
|
|
503
|
+
adminUser,
|
|
504
|
+
body,
|
|
505
|
+
cookies,
|
|
506
|
+
headers,
|
|
507
|
+
method: endpoint.method,
|
|
508
|
+
query,
|
|
509
|
+
requestUrl,
|
|
510
|
+
});
|
|
511
|
+
const rawResponse = createRawExpressResponse(response);
|
|
512
|
+
const acceptLanguage = headers['accept-language'];
|
|
513
|
+
const tr = (
|
|
514
|
+
msg: string,
|
|
515
|
+
category: string = 'default',
|
|
516
|
+
translationParams: any,
|
|
517
|
+
pluralizationNumber?: number,
|
|
518
|
+
) => adminforth.tr(msg, category, acceptLanguage, translationParams, pluralizationNumber);
|
|
519
|
+
|
|
520
|
+
const output = await endpoint.handler({
|
|
521
|
+
body,
|
|
522
|
+
adminUser,
|
|
523
|
+
query,
|
|
524
|
+
headers,
|
|
525
|
+
cookies,
|
|
526
|
+
response,
|
|
527
|
+
requestUrl,
|
|
528
|
+
abortSignal: abortController.signal,
|
|
529
|
+
_raw_express_req: rawRequest,
|
|
530
|
+
_raw_express_res: rawResponse,
|
|
531
|
+
tr,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (output !== undefined && output !== null) {
|
|
535
|
+
return output;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (response.jsonPayload !== undefined) {
|
|
539
|
+
return response.jsonPayload;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (response.message !== undefined) {
|
|
543
|
+
return response.message;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
headers: response.headers,
|
|
548
|
+
status: response.status,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function prepareApiBasedTools(adminforth: IAdminForth): Record<string, ApiBasedTool> {
|
|
553
|
+
const capturedEndpoints: CapturedEndpoint[] = [];
|
|
554
|
+
|
|
555
|
+
const captureServer: IHttpServer = {
|
|
556
|
+
setupSpaServer() {},
|
|
557
|
+
endpoint: ((options: EndpointWithSchemas) => {
|
|
558
|
+
const normalizedResponseSchema = options.response_schema ?? options.responce_schema;
|
|
559
|
+
if (!options.request_schema && !normalizedResponseSchema) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
capturedEndpoints.push({
|
|
564
|
+
...options,
|
|
565
|
+
response_schema: normalizedResponseSchema,
|
|
566
|
+
normalizedResponseSchema,
|
|
567
|
+
});
|
|
568
|
+
}) as IHttpServer['endpoint'],
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
adminforth.setupEndpoints(captureServer);
|
|
572
|
+
|
|
573
|
+
const apiBasedTools: Record<string, ApiBasedTool> = {};
|
|
574
|
+
const capturedEndpointsByToolName = Object.fromEntries(
|
|
575
|
+
capturedEndpoints.map((endpoint) => [endpointPathToToolName(endpoint.path), endpoint]),
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
for (const endpoint of capturedEndpoints) {
|
|
579
|
+
const toolName = endpointPathToToolName(endpoint.path);
|
|
580
|
+
apiBasedTools[toolName] = {
|
|
581
|
+
description: endpoint.description,
|
|
582
|
+
input_schema: endpoint.request_schema,
|
|
583
|
+
input_schma: endpoint.request_schema,
|
|
584
|
+
output_schema: endpoint.normalizedResponseSchema,
|
|
585
|
+
call: async ({ adminUser, adminuser, inputs, httpExtra, userTimeZone } = {}) => {
|
|
586
|
+
const output = await callCapturedEndpoint({
|
|
587
|
+
adminforth,
|
|
588
|
+
endpoint,
|
|
589
|
+
adminUser: adminUser ?? adminuser,
|
|
590
|
+
inputs,
|
|
591
|
+
httpExtra,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const processedOutput = await applyToolOverride({
|
|
595
|
+
adminforth,
|
|
596
|
+
adminUser: adminUser ?? adminuser,
|
|
597
|
+
capturedEndpointsByToolName,
|
|
598
|
+
httpExtra,
|
|
599
|
+
inputs,
|
|
600
|
+
output,
|
|
601
|
+
toolName,
|
|
602
|
+
userTimeZone,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
return YAML.stringify(processedOutput);
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return apiBasedTools;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function serializeApiBasedTool(tool: ApiBasedTool | undefined) {
|
|
614
|
+
if (!tool) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
description: tool.description,
|
|
620
|
+
input_schema: tool.input_schema,
|
|
621
|
+
input_schma: tool.input_schma,
|
|
622
|
+
output_schema: tool.output_schema,
|
|
623
|
+
call: '[Function]',
|
|
624
|
+
};
|
|
625
|
+
}
|