@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,291 @@
|
|
|
1
|
+
import { createAgent, summarizationMiddleware } from "langchain";
|
|
2
|
+
import { logger, type AdminUser, type CompletionAdapter } from "adminforth";
|
|
3
|
+
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
|
4
|
+
import { MemorySaver, type Messages } from "@langchain/langgraph";
|
|
5
|
+
import type { LLMResult } from "@langchain/core/outputs";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
8
|
+
import { createAgentTools } from "./tools/index.js";
|
|
9
|
+
import { createApiBasedToolsMiddleware } from "./middleware/apiBasedTools.js";
|
|
10
|
+
import {
|
|
11
|
+
createSequenceDebugMiddleware,
|
|
12
|
+
type SequenceDebugModelCallSink,
|
|
13
|
+
} from "./middleware/sequenceDebug.js";
|
|
14
|
+
import type { ApiBasedTool } from "../apiBasedTools.js";
|
|
15
|
+
import type { ToolCallEventSink } from "./toolCallEvents.js";
|
|
16
|
+
|
|
17
|
+
const checkpointer = new MemorySaver();
|
|
18
|
+
|
|
19
|
+
export const contextSchema = z.object({
|
|
20
|
+
adminUser: z.custom<AdminUser>(),
|
|
21
|
+
userTimeZone: z.string(),
|
|
22
|
+
sessionId: z.string(),
|
|
23
|
+
turnId: z.string(),
|
|
24
|
+
emitToolCallEvent: z.custom<ToolCallEventSink>(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
type AgentReasoning =
|
|
28
|
+
| "none"
|
|
29
|
+
| "minimal"
|
|
30
|
+
| "low"
|
|
31
|
+
| "medium"
|
|
32
|
+
| "high"
|
|
33
|
+
| "xhigh";
|
|
34
|
+
|
|
35
|
+
type OpenAIBackedCompletionAdapter = CompletionAdapter & {
|
|
36
|
+
options?: {
|
|
37
|
+
openAiApiKey?: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
baseURL?: string;
|
|
40
|
+
baseUrl?: string;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
extraRequestBodyParameters?: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type LlmOutputTokenUsage = {
|
|
47
|
+
promptTokens?: unknown;
|
|
48
|
+
completionTokens?: unknown;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type MessageUsageMetadata = {
|
|
52
|
+
input_tokens?: unknown;
|
|
53
|
+
output_tokens?: unknown;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type PendingLlmRun = {
|
|
57
|
+
startedAt: number;
|
|
58
|
+
firstTokenAt?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function getFiniteNumber(value: unknown) {
|
|
62
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
63
|
+
? value
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractTokenUsage(output: LLMResult) {
|
|
68
|
+
const llmOutputTokenUsage = (
|
|
69
|
+
output.llmOutput as { tokenUsage?: LlmOutputTokenUsage } | undefined
|
|
70
|
+
)?.tokenUsage;
|
|
71
|
+
|
|
72
|
+
const promptTokens = getFiniteNumber(llmOutputTokenUsage?.promptTokens);
|
|
73
|
+
const completionTokens = getFiniteNumber(
|
|
74
|
+
llmOutputTokenUsage?.completionTokens,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (promptTokens !== undefined || completionTokens !== undefined) {
|
|
78
|
+
return {
|
|
79
|
+
InputTokens: promptTokens ?? 0,
|
|
80
|
+
outputTokens: completionTokens ?? 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let InputTokens = 0;
|
|
85
|
+
let outputTokens = 0;
|
|
86
|
+
|
|
87
|
+
for (const generationBatch of output.generations) {
|
|
88
|
+
for (const generation of generationBatch) {
|
|
89
|
+
if (!("message" in generation) || !generation.message) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const message = generation.message as {
|
|
94
|
+
usage_metadata?: MessageUsageMetadata;
|
|
95
|
+
response_metadata?: {
|
|
96
|
+
tokenUsage?: LlmOutputTokenUsage;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
InputTokens +=
|
|
101
|
+
getFiniteNumber(message.usage_metadata?.input_tokens) ??
|
|
102
|
+
getFiniteNumber(message.response_metadata?.tokenUsage?.promptTokens) ??
|
|
103
|
+
0;
|
|
104
|
+
outputTokens +=
|
|
105
|
+
getFiniteNumber(message.usage_metadata?.output_tokens) ??
|
|
106
|
+
getFiniteNumber(
|
|
107
|
+
message.response_metadata?.tokenUsage?.completionTokens,
|
|
108
|
+
) ??
|
|
109
|
+
0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { InputTokens, outputTokens };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class AgentLlmMetricsLogger extends BaseCallbackHandler {
|
|
117
|
+
name = "AgentLlmMetricsLogger";
|
|
118
|
+
lc_prefer_streaming = true;
|
|
119
|
+
|
|
120
|
+
private readonly pendingRuns = new Map<string, PendingLlmRun>();
|
|
121
|
+
|
|
122
|
+
async handleLLMStart(_llm: unknown, _prompts: string[], runId: string) {
|
|
123
|
+
this.pendingRuns.set(runId, { startedAt: Date.now() });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async handleLLMNewToken(
|
|
127
|
+
_token: string,
|
|
128
|
+
_chunk: unknown,
|
|
129
|
+
runId: string,
|
|
130
|
+
) {
|
|
131
|
+
const pendingRun = this.pendingRuns.get(runId);
|
|
132
|
+
|
|
133
|
+
if (!pendingRun || pendingRun.firstTokenAt !== undefined) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pendingRun.firstTokenAt = Date.now();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async handleLLMEnd(output: LLMResult, runId: string) {
|
|
141
|
+
const pendingRun = this.pendingRuns.get(runId);
|
|
142
|
+
|
|
143
|
+
if (!pendingRun) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.pendingRuns.delete(runId);
|
|
148
|
+
|
|
149
|
+
const finishedAt = Date.now();
|
|
150
|
+
const rtt = finishedAt - pendingRun.startedAt;
|
|
151
|
+
const ttft =
|
|
152
|
+
pendingRun.firstTokenAt === undefined
|
|
153
|
+
? rtt
|
|
154
|
+
: pendingRun.firstTokenAt - pendingRun.startedAt;
|
|
155
|
+
const { InputTokens, outputTokens } = extractTokenUsage(output);
|
|
156
|
+
|
|
157
|
+
logger.info(
|
|
158
|
+
{ InputTokens, outputTokens, ttft, rtt },
|
|
159
|
+
"LLM call finished",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async handleLLMError(_error: unknown, runId: string) {
|
|
164
|
+
this.pendingRuns.delete(runId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function createAgentLlmMetricsLogger() {
|
|
169
|
+
return new AgentLlmMetricsLogger();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeReasoning(reasoning: AgentReasoning) {
|
|
173
|
+
if (reasoning === "none") {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
effort: reasoning as "minimal" | "low" | "medium" | "high" | "xhigh",
|
|
179
|
+
summary: "auto" as const,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createAgentChatModel(params: {
|
|
184
|
+
adapter: CompletionAdapter;
|
|
185
|
+
maxTokens: number;
|
|
186
|
+
reasoning: AgentReasoning;
|
|
187
|
+
modelName?: string;
|
|
188
|
+
}) {
|
|
189
|
+
const adapter = params.adapter as OpenAIBackedCompletionAdapter;
|
|
190
|
+
const options = adapter.options ?? {};
|
|
191
|
+
|
|
192
|
+
if (!options.openAiApiKey) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"CompletionAdapter must expose options.openAiApiKey for ChatOpenAI",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const model = params.modelName ?? options.model ?? "gpt-5-nano";
|
|
199
|
+
const baseURL = options.baseURL ?? options.baseUrl;
|
|
200
|
+
const reasoning = normalizeReasoning(params.reasoning);
|
|
201
|
+
|
|
202
|
+
return new ChatOpenAI({
|
|
203
|
+
apiKey: options.openAiApiKey,
|
|
204
|
+
model,
|
|
205
|
+
maxTokens: params.maxTokens,
|
|
206
|
+
useResponsesApi: true,
|
|
207
|
+
outputVersion: "v1",
|
|
208
|
+
...(reasoning ? { reasoning } : {}),
|
|
209
|
+
...(typeof options.timeoutMs === "number"
|
|
210
|
+
? { timeout: options.timeoutMs }
|
|
211
|
+
: {}),
|
|
212
|
+
...(baseURL
|
|
213
|
+
? {
|
|
214
|
+
configuration: {
|
|
215
|
+
baseURL,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function callAgent(params: {
|
|
223
|
+
name: string;
|
|
224
|
+
model: ChatOpenAI;
|
|
225
|
+
summaryModel: ChatOpenAI;
|
|
226
|
+
messages: Messages;
|
|
227
|
+
adminUser: AdminUser;
|
|
228
|
+
apiBasedTools: Record<string, ApiBasedTool>;
|
|
229
|
+
customComponentsDir: string;
|
|
230
|
+
sessionId: string;
|
|
231
|
+
turnId: string;
|
|
232
|
+
userTimeZone: string;
|
|
233
|
+
emitToolCallEvent: ToolCallEventSink;
|
|
234
|
+
sequenceDebugSink: SequenceDebugModelCallSink;
|
|
235
|
+
}) {
|
|
236
|
+
const {
|
|
237
|
+
name,
|
|
238
|
+
model,
|
|
239
|
+
summaryModel,
|
|
240
|
+
messages,
|
|
241
|
+
adminUser,
|
|
242
|
+
apiBasedTools,
|
|
243
|
+
customComponentsDir,
|
|
244
|
+
sessionId,
|
|
245
|
+
turnId,
|
|
246
|
+
userTimeZone,
|
|
247
|
+
emitToolCallEvent,
|
|
248
|
+
sequenceDebugSink,
|
|
249
|
+
} = params;
|
|
250
|
+
|
|
251
|
+
const tools = await createAgentTools(customComponentsDir, apiBasedTools);
|
|
252
|
+
const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools);
|
|
253
|
+
const sequenceDebugMiddleware = createSequenceDebugMiddleware(
|
|
254
|
+
sequenceDebugSink,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const middleware = [
|
|
258
|
+
apiBasedToolsMiddleware,
|
|
259
|
+
sequenceDebugMiddleware,
|
|
260
|
+
summarizationMiddleware({
|
|
261
|
+
model: summaryModel,
|
|
262
|
+
trigger: { tokens: 1024 * 8 },
|
|
263
|
+
keep: { messages: 10 },
|
|
264
|
+
}),
|
|
265
|
+
] as const;
|
|
266
|
+
|
|
267
|
+
const agent = createAgent<undefined, typeof contextSchema, typeof middleware>({
|
|
268
|
+
name,
|
|
269
|
+
model,
|
|
270
|
+
checkpointer,
|
|
271
|
+
tools,
|
|
272
|
+
contextSchema,
|
|
273
|
+
middleware,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return await agent.stream({ messages } as any, {
|
|
277
|
+
streamMode: "messages",
|
|
278
|
+
recursionLimit: 50,
|
|
279
|
+
callbacks: [createAgentLlmMetricsLogger()],
|
|
280
|
+
configurable: {
|
|
281
|
+
thread_id: sessionId,
|
|
282
|
+
},
|
|
283
|
+
context: {
|
|
284
|
+
adminUser,
|
|
285
|
+
userTimeZone,
|
|
286
|
+
sessionId,
|
|
287
|
+
turnId,
|
|
288
|
+
emitToolCallEvent,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readdir, readFile } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
|
|
6
|
+
const PLUGIN_SKILLS_DIRECTORY_PATH = fileURLToPath(
|
|
7
|
+
new URL("../../custom/skills/", import.meta.url),
|
|
8
|
+
);
|
|
9
|
+
const SKILL_MARKDOWN_FILENAME = "SKILL.md";
|
|
10
|
+
const SKILL_FRONTMATTER_SEPARATOR = "\n---\n";
|
|
11
|
+
|
|
12
|
+
export interface AgentSkillManifest {
|
|
13
|
+
directoryName: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
instructions: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseSkillManifest(directoryName: string, markdown: string): AgentSkillManifest {
|
|
20
|
+
const [frontmatterBlock, instructions = ""] = markdown.split("\r\n").join("\n").split(
|
|
21
|
+
SKILL_FRONTMATTER_SEPARATOR,
|
|
22
|
+
2,
|
|
23
|
+
);
|
|
24
|
+
const metadata = parseYaml(frontmatterBlock) as {
|
|
25
|
+
name?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
directoryName,
|
|
31
|
+
name: metadata.name ?? directoryName,
|
|
32
|
+
description: metadata.description ?? "",
|
|
33
|
+
instructions: instructions.trim(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readSkillManifest(skillsDirectoryPath: string, directoryName: string) {
|
|
38
|
+
const markdown = await readFile(
|
|
39
|
+
path.join(skillsDirectoryPath, directoryName, SKILL_MARKDOWN_FILENAME),
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return parseSkillManifest(directoryName, markdown);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function listDirectorySkillManifests(skillsDirectoryPath: string) {
|
|
47
|
+
try {
|
|
48
|
+
const entries = await readdir(skillsDirectoryPath, { withFileTypes: true });
|
|
49
|
+
|
|
50
|
+
return await Promise.all(
|
|
51
|
+
entries
|
|
52
|
+
.filter((entry) => entry.isDirectory())
|
|
53
|
+
.map((entry) => entry.name)
|
|
54
|
+
.sort()
|
|
55
|
+
.map((directoryName) => readSkillManifest(skillsDirectoryPath, directoryName)),
|
|
56
|
+
);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mergeSkillManifests(skillGroups: AgentSkillManifest[][]) {
|
|
67
|
+
return Array.from(
|
|
68
|
+
new Map(
|
|
69
|
+
skillGroups.flat().map((skill) => [
|
|
70
|
+
`${skill.name}:${skill.directoryName}`,
|
|
71
|
+
skill,
|
|
72
|
+
]),
|
|
73
|
+
).values(),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getProjectSkillsDirectoryPath(customComponentsDir: string) {
|
|
78
|
+
return path.resolve(customComponentsDir, "skills");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function listBundledSkillManifests() {
|
|
82
|
+
return await listDirectorySkillManifests(PLUGIN_SKILLS_DIRECTORY_PATH);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function listProjectSkillManifests(customComponentsDir: string) {
|
|
86
|
+
return await listDirectorySkillManifests(
|
|
87
|
+
getProjectSkillsDirectoryPath(customComponentsDir),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function listSkillManifests(customComponentsDir: string) {
|
|
92
|
+
return mergeSkillManifests([
|
|
93
|
+
await listProjectSkillManifests(customComponentsDir),
|
|
94
|
+
await listBundledSkillManifests(),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function loadSkillManifest(skillName: string, customComponentsDir: string) {
|
|
99
|
+
const manifests = await listSkillManifests(customComponentsDir);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
manifests.find(
|
|
103
|
+
(manifest) =>
|
|
104
|
+
manifest.name === skillName || manifest.directoryName === skillName,
|
|
105
|
+
) ?? null
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function loadSkillMarkdown(skillName: string, customComponentsDir: string) {
|
|
110
|
+
const manifest = await loadSkillManifest(skillName, customComponentsDir);
|
|
111
|
+
|
|
112
|
+
if (!manifest) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const directories = [
|
|
117
|
+
getProjectSkillsDirectoryPath(customComponentsDir),
|
|
118
|
+
PLUGIN_SKILLS_DIRECTORY_PATH,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const skillsDirectoryPath of directories) {
|
|
122
|
+
try {
|
|
123
|
+
return await readFile(
|
|
124
|
+
path.join(skillsDirectoryPath, manifest.directoryName, SKILL_MARKDOWN_FILENAME),
|
|
125
|
+
"utf8",
|
|
126
|
+
);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { AdminForthResource, IAdminForth } from "adminforth";
|
|
2
|
+
import {
|
|
3
|
+
listBundledSkillManifests,
|
|
4
|
+
listProjectSkillManifests,
|
|
5
|
+
type AgentSkillManifest,
|
|
6
|
+
} from "./skills/registry.js";
|
|
7
|
+
import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "./tools/index.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_AGENT_SYSTEM_PROMPT = [
|
|
10
|
+
"You are helpful AI Assistant for Admin Panel.",
|
|
11
|
+
|
|
12
|
+
// about admin
|
|
13
|
+
"Admin panel has resources which represent some physical data storage (e.g. table/collection), each resource defines list of columns.",
|
|
14
|
+
"Each resource stores data records. Record represents a data item of resource.",
|
|
15
|
+
|
|
16
|
+
//about user
|
|
17
|
+
"Assume user is not technical so does not talk to him in terms of API calls, databases/sql/json etc.",
|
|
18
|
+
|
|
19
|
+
// prevent extra talk
|
|
20
|
+
"Try to achieve user's goal with as few steps as possible. Talk with him only when you need some important decision, otherwise act immediately and call tools asap",
|
|
21
|
+
|
|
22
|
+
// tone of voice
|
|
23
|
+
"Be warm, friendly, and sincere.",
|
|
24
|
+
"Keep responses short, clear, and practical.",
|
|
25
|
+
"Answer only what is needed.",
|
|
26
|
+
"Do not add extra explanations or suggestions unless the user asks.",
|
|
27
|
+
"Adapt to the user's tone and style of speaking, mirroring their vibe and wording.",
|
|
28
|
+
"if the user speaks casually, you should respond casually too",
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
].join(" ");
|
|
32
|
+
|
|
33
|
+
function formatResources(resources: AdminForthResource[]) {
|
|
34
|
+
return resources
|
|
35
|
+
.map((resource) => `- resourceId: ${resource.resourceId}\n label: ${resource.label}`)
|
|
36
|
+
.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatSkills(skills: AgentSkillManifest[], label: "skill_name" | "tool_name") {
|
|
40
|
+
return skills
|
|
41
|
+
.map((skill) => `- ${label}: ${skill.name}\n description: ${skill.description}`)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function buildAgentSystemPrompt(adminforth: IAdminForth) {
|
|
46
|
+
const [primarySkills, defaultSkills] = await Promise.all([
|
|
47
|
+
listProjectSkillManifests(adminforth.config.customization.customComponentsDir),
|
|
48
|
+
listBundledSkillManifests(),
|
|
49
|
+
]);
|
|
50
|
+
const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", ");
|
|
51
|
+
const sections = [
|
|
52
|
+
DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
53
|
+
`BASE_URL: ${adminforth.config.baseUrl}`,
|
|
54
|
+
`List of resources:\n${formatResources(adminforth.config.resources)}`,
|
|
55
|
+
`You have always-available base tools: ${alwaysAvailableTools}.`,
|
|
56
|
+
primarySkills.length > 0
|
|
57
|
+
? `You have primary skills set:\n${formatSkills(primarySkills, "skill_name")}`
|
|
58
|
+
: "",
|
|
59
|
+
"You have next default skills which you can fallback to if primary skill set does not provide a good skill:\n" +
|
|
60
|
+
formatSkills(defaultSkills, "skill_name"),
|
|
61
|
+
"Before using any skill, call fetch_skill to load its full instructions.",
|
|
62
|
+
"You can use get_resource immediately to inspect resource structure and column names.",
|
|
63
|
+
"Only call fetch_tool_schema for tool names that are explicitly mentioned in a fetched skill and are not already available as base tools.",
|
|
64
|
+
"When fetch_tool_schema succeeds, that tool becomes available on the next step.",
|
|
65
|
+
"Try to call as many tools as possible in parallel in one step.",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
return sections.filter(Boolean).join("\n\n");
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ToolCallEvent =
|
|
2
|
+
| {
|
|
3
|
+
toolCallId: string;
|
|
4
|
+
toolName: string;
|
|
5
|
+
phase: "start";
|
|
6
|
+
input: string;
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
toolCallId: string;
|
|
10
|
+
toolName: string;
|
|
11
|
+
phase: "end";
|
|
12
|
+
durationMs: number;
|
|
13
|
+
output: string | null;
|
|
14
|
+
error: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ToolCallEventSink = (event: ToolCallEvent) => void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import type { ApiBasedTool } from "../../apiBasedTools.js";
|
|
5
|
+
import { serializeUnknownError } from "../../apiBasedTools.js";
|
|
6
|
+
|
|
7
|
+
const emptyToolSchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {},
|
|
10
|
+
additionalProperties: true,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
function normalizeToolInputSchema(inputSchema: unknown) {
|
|
14
|
+
if (!inputSchema || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
15
|
+
return emptyToolSchema;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const schema = JSON.parse(JSON.stringify(inputSchema)) as Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
if (schema.type !== "object") {
|
|
21
|
+
return emptyToolSchema;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const notes: string[] = [];
|
|
25
|
+
|
|
26
|
+
if ("if" in schema || "then" in schema || "else" in schema || "allOf" in schema) {
|
|
27
|
+
delete schema.if;
|
|
28
|
+
delete schema.then;
|
|
29
|
+
delete schema.else;
|
|
30
|
+
delete schema.allOf;
|
|
31
|
+
notes.push("Runtime applies additional conditional validation rules.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ("oneOf" in schema || "anyOf" in schema || "not" in schema || "enum" in schema) {
|
|
35
|
+
delete schema.oneOf;
|
|
36
|
+
delete schema.anyOf;
|
|
37
|
+
delete schema.not;
|
|
38
|
+
delete schema.enum;
|
|
39
|
+
notes.push("Top-level composite validation rules are omitted for tool compatibility.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (notes.length > 0) {
|
|
43
|
+
schema.description = typeof schema.description === "string"
|
|
44
|
+
? `${schema.description}\n\n${notes.join(" ")}`
|
|
45
|
+
: notes.join(" ");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return schema;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createApiTool(toolName: string, apiBasedTool: ApiBasedTool) {
|
|
52
|
+
return tool(
|
|
53
|
+
async (input, runtime) => {
|
|
54
|
+
const normalizedInput = (input ?? {}) as Record<string, unknown>;
|
|
55
|
+
const toolCallId = randomUUID();
|
|
56
|
+
const startedAt = Date.now();
|
|
57
|
+
runtime.context.emitToolCallEvent({
|
|
58
|
+
toolCallId,
|
|
59
|
+
toolName,
|
|
60
|
+
phase: "start",
|
|
61
|
+
input: YAML.stringify(normalizedInput),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const output = await apiBasedTool.call({
|
|
66
|
+
adminUser: runtime.context.adminUser,
|
|
67
|
+
inputs: normalizedInput,
|
|
68
|
+
userTimeZone: runtime.context.userTimeZone,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
runtime.context.emitToolCallEvent({
|
|
72
|
+
toolCallId,
|
|
73
|
+
toolName,
|
|
74
|
+
phase: "end",
|
|
75
|
+
durationMs: Date.now() - startedAt,
|
|
76
|
+
output,
|
|
77
|
+
error: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return output;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
runtime.context.emitToolCallEvent({
|
|
83
|
+
toolCallId,
|
|
84
|
+
toolName,
|
|
85
|
+
phase: "end",
|
|
86
|
+
durationMs: Date.now() - startedAt,
|
|
87
|
+
output: null,
|
|
88
|
+
error: YAML.stringify(serializeUnknownError(error)),
|
|
89
|
+
});
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: toolName,
|
|
95
|
+
description: apiBasedTool.description ?? `${toolName} tool`,
|
|
96
|
+
schema: normalizeToolInputSchema(apiBasedTool.input_schema),
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
listSkillManifests,
|
|
5
|
+
loadSkillMarkdown,
|
|
6
|
+
type AgentSkillManifest,
|
|
7
|
+
} from "../skills/registry.js";
|
|
8
|
+
|
|
9
|
+
const fetchSkillSchema = z.object({
|
|
10
|
+
skillName: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe(
|
|
13
|
+
"Name of the custom AdminForth skill to load, for example fetch_data or mutate_data.",
|
|
14
|
+
),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function serializeSkillManifests(skillManifests: AgentSkillManifest[]) {
|
|
18
|
+
return skillManifests.map((skill) => ({
|
|
19
|
+
name: skill.name,
|
|
20
|
+
description: skill.description,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function createFetchSkillTool(customComponentsDir: string) {
|
|
25
|
+
const availableSkills = await listSkillManifests(customComponentsDir);
|
|
26
|
+
const availableSkillNames = availableSkills.map((skill) => skill.name);
|
|
27
|
+
|
|
28
|
+
return tool(
|
|
29
|
+
async ({ skillName }) => {
|
|
30
|
+
try {
|
|
31
|
+
const skillMarkdown = await loadSkillMarkdown(skillName, customComponentsDir);
|
|
32
|
+
|
|
33
|
+
if (!skillMarkdown) {
|
|
34
|
+
return [
|
|
35
|
+
`Skill "${skillName}" not found.`,
|
|
36
|
+
"Available skills:",
|
|
37
|
+
JSON.stringify(serializeSkillManifests(availableSkills), null, 2),
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return skillMarkdown;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return `Failed to load skill "${skillName}": ${
|
|
44
|
+
error instanceof Error ? error.message : String(error)
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "fetch_skill",
|
|
50
|
+
description: `Fetch the raw SKILL.md content for a custom AdminForth skill by name.${
|
|
51
|
+
availableSkillNames.length > 0
|
|
52
|
+
? ` Available skills: ${availableSkillNames.join(", ")}.`
|
|
53
|
+
: ""
|
|
54
|
+
}`,
|
|
55
|
+
schema: fetchSkillSchema,
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|