@sf-explorer/agentforce-service 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/README.md +173 -0
- package/bin/cli.js +43 -0
- package/dist/client/assets/index-4BXb_5Lv.css +1 -0
- package/dist/client/assets/index-CCE64qe5.js +43 -0
- package/dist/client/index.html +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +54 -0
- package/dist/openapi.d.ts +644 -0
- package/dist/openapi.js +475 -0
- package/dist/org.d.ts +21 -0
- package/dist/org.js +45 -0
- package/dist/routes/agents.d.ts +3 -0
- package/dist/routes/agents.js +181 -0
- package/dist/routes/context.d.ts +15 -0
- package/dist/routes/context.js +11 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.js +13 -0
- package/dist/routes/session.d.ts +3 -0
- package/dist/routes/session.js +195 -0
- package/dist/routes/tests.d.ts +3 -0
- package/dist/routes/tests.js +211 -0
- package/dist/routes.d.ts +14 -0
- package/dist/routes.js +534 -0
- package/package.json +52 -0
package/dist/routes.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { file, directory } from "@polycuber/script.cli";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Agent, ScriptAgent } from "@salesforce/agents";
|
|
6
|
+
import { SfProject } from "@salesforce/core";
|
|
7
|
+
import { getOrgInfoFromCli, createJsforceConnection, createSfConnection, } from "./org.js";
|
|
8
|
+
/** Session ID -> agent instance (for preview sendMessage) */
|
|
9
|
+
const sessionAgents = new Map();
|
|
10
|
+
/** Initialize org connection (jsforce + @salesforce/core). Call at server startup. */
|
|
11
|
+
export async function initializeContext(context) {
|
|
12
|
+
if (context.orgInfo)
|
|
13
|
+
return;
|
|
14
|
+
context.orgInfo = getOrgInfoFromCli(context.orgAlias, context.projectDir);
|
|
15
|
+
context.jsforceConn = createJsforceConnection(context.orgInfo);
|
|
16
|
+
context.sfConn = await createSfConnection(context.orgInfo);
|
|
17
|
+
}
|
|
18
|
+
export function createRoutes(context) {
|
|
19
|
+
const router = Router();
|
|
20
|
+
async function ensureInitialized() {
|
|
21
|
+
if (!context.orgInfo) {
|
|
22
|
+
await initializeContext(context);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* POST /startSession
|
|
27
|
+
* Start an agent preview session using @salesforce/agents
|
|
28
|
+
* Body: { agentBundleName } for script agent OR { apiNameOrId } for production agent
|
|
29
|
+
*/
|
|
30
|
+
router.post("/startSession", async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
await ensureInitialized();
|
|
33
|
+
const agentBundleName = req.body.agentBundleName ?? req.body.aabName;
|
|
34
|
+
const apiNameOrId = req.body.apiNameOrId;
|
|
35
|
+
if (!agentBundleName && !apiNameOrId) {
|
|
36
|
+
res.status(400).json({
|
|
37
|
+
error: "Missing required field: agentBundleName (script agent) or apiNameOrId (production agent)",
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
42
|
+
const agent = agentBundleName
|
|
43
|
+
? await Agent.init({
|
|
44
|
+
connection: context.sfConn,
|
|
45
|
+
project,
|
|
46
|
+
aabName: agentBundleName,
|
|
47
|
+
})
|
|
48
|
+
: await Agent.init({
|
|
49
|
+
connection: context.sfConn,
|
|
50
|
+
project,
|
|
51
|
+
apiNameOrId: apiNameOrId,
|
|
52
|
+
});
|
|
53
|
+
const startResponse = await agent.preview.start();
|
|
54
|
+
sessionAgents.set(startResponse.sessionId, agent);
|
|
55
|
+
res.json(startResponse);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
res.status(500).json({
|
|
59
|
+
error: err instanceof Error ? err.message : "Start session failed",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* POST /sendmessage
|
|
65
|
+
* Send a message using @salesforce/agents agent.preview.send()
|
|
66
|
+
* Body: { sessionId, message }
|
|
67
|
+
*/
|
|
68
|
+
router.post("/sendmessage", async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { sessionId, message } = req.body;
|
|
71
|
+
if (!sessionId || !message) {
|
|
72
|
+
res.status(400).json({
|
|
73
|
+
error: "Missing required fields: sessionId, message",
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const agent = sessionAgents.get(sessionId);
|
|
78
|
+
if (!agent) {
|
|
79
|
+
res.status(404).json({
|
|
80
|
+
error: "Session not found. Call startSession first.",
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const sendResponse = await agent.preview.send(message);
|
|
85
|
+
res.json(sendResponse);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
res.status(500).json({
|
|
89
|
+
error: err instanceof Error ? err.message : "Send message failed",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* POST /trace
|
|
95
|
+
* Get the plan trace for a given planId from an active session.
|
|
96
|
+
* Body: { sessionId, planId }
|
|
97
|
+
* planId comes from sendmessage response messages[].planId
|
|
98
|
+
*/
|
|
99
|
+
router.post("/trace", async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const { sessionId, planId } = req.body;
|
|
102
|
+
if (!sessionId || !planId) {
|
|
103
|
+
res.status(400).json({
|
|
104
|
+
error: "Missing required fields: sessionId, planId",
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const agent = sessionAgents.get(sessionId);
|
|
109
|
+
if (!agent) {
|
|
110
|
+
res.status(404).json({
|
|
111
|
+
error: "Session not found. Call startSession first.",
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const trace = await agent.getTrace(planId);
|
|
116
|
+
res.json(trace ?? null);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
res.status(500).json({
|
|
120
|
+
error: err instanceof Error ? err.message : "Get trace failed",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
125
|
+
* POST /endSession
|
|
126
|
+
* End an agent preview session using @salesforce/agents
|
|
127
|
+
* Body: { sessionId }
|
|
128
|
+
*/
|
|
129
|
+
router.post("/endSession", async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { sessionId } = req.body;
|
|
132
|
+
if (!sessionId) {
|
|
133
|
+
res.status(400).json({ error: "Missing required field: sessionId" });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const agent = sessionAgents.get(sessionId);
|
|
137
|
+
if (!agent) {
|
|
138
|
+
res.status(404).json({
|
|
139
|
+
error: "Session not found. Call startSession first.",
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const endResponse = await agent.preview.end();
|
|
144
|
+
sessionAgents.delete(sessionId);
|
|
145
|
+
res.json(endResponse);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
res.status(500).json({
|
|
149
|
+
error: err instanceof Error ? err.message : "End session failed",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* POST /compile
|
|
155
|
+
* Compile an AgentScript from an aiAuthoringBundle
|
|
156
|
+
* Body: { agentBundleName }
|
|
157
|
+
*/
|
|
158
|
+
router.post("/compile", async (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
await ensureInitialized();
|
|
161
|
+
const agentBundleName = req.body.agentBundleName ?? req.body.aabName;
|
|
162
|
+
if (!agentBundleName) {
|
|
163
|
+
res.status(400).json({ error: "Missing required field: agentBundleName" });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
167
|
+
const scriptAgent = await Agent.init({
|
|
168
|
+
connection: context.sfConn,
|
|
169
|
+
project,
|
|
170
|
+
aabName: agentBundleName,
|
|
171
|
+
});
|
|
172
|
+
if (!(scriptAgent instanceof ScriptAgent)) {
|
|
173
|
+
res.status(400).json({
|
|
174
|
+
error: "Agent is not a ScriptAgent (compile only supports script agents)",
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const result = await scriptAgent.compile();
|
|
179
|
+
res.json(result);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
res.status(500).json({
|
|
183
|
+
error: err instanceof Error ? err.message : "Compile failed",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* GET /agents
|
|
189
|
+
* Retrieve list of agents. Query: ?source=local|remote|all (default: all)
|
|
190
|
+
* - local: Agent.list(project) - aabNames in project
|
|
191
|
+
* - remote: Agent.listRemote(connection) - bots in org
|
|
192
|
+
* - all: Agent.listPreviewable(connection, project) - combined
|
|
193
|
+
*/
|
|
194
|
+
router.get("/agents", async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
await ensureInitialized();
|
|
197
|
+
const source = req.query.source ?? "all";
|
|
198
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
199
|
+
if (source === "local") {
|
|
200
|
+
const aabNames = await Agent.list(project);
|
|
201
|
+
res.json({ agents: aabNames, source: "local" });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (source === "remote") {
|
|
205
|
+
const bots = await Agent.listRemote(context.sfConn);
|
|
206
|
+
res.json({ agents: bots, source: "remote" });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const previewable = await Agent.listPreviewable(context.sfConn, project);
|
|
210
|
+
res.json({ agents: previewable, source: "all" });
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
res.status(500).json({
|
|
214
|
+
error: err instanceof Error ? err.message : "Retrieve agents failed",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const bundlesDir = join(context.projectDir, "force-app", "main", "default", "aiAuthoringBundles");
|
|
219
|
+
/**
|
|
220
|
+
* GET /agent/:developerName
|
|
221
|
+
* Read agent metadata (AiAuthoringBundle) directly from disk.
|
|
222
|
+
* Path: force-app/main/default/aiAuthoringBundles/{developerName}/
|
|
223
|
+
*/
|
|
224
|
+
router.get("/agent/:developerName", (req, res) => {
|
|
225
|
+
try {
|
|
226
|
+
const { developerName } = req.params;
|
|
227
|
+
const bundlePath = join(bundlesDir, developerName);
|
|
228
|
+
const result = {};
|
|
229
|
+
const agentPath = join(bundlePath, `${developerName}.agent`);
|
|
230
|
+
const metaPath = join(bundlePath, `${developerName}.bundle-meta.xml`);
|
|
231
|
+
if (file.exists(agentPath)) {
|
|
232
|
+
const content = file.read.text(agentPath);
|
|
233
|
+
if (content)
|
|
234
|
+
result[`aiAuthoringBundles/${developerName}/${developerName}.agent`] = content;
|
|
235
|
+
}
|
|
236
|
+
if (file.exists(metaPath)) {
|
|
237
|
+
const content = file.read.text(metaPath);
|
|
238
|
+
if (content)
|
|
239
|
+
result[`aiAuthoringBundles/${developerName}/${developerName}.bundle-meta.xml`] = content;
|
|
240
|
+
}
|
|
241
|
+
if (Object.keys(result).length === 0) {
|
|
242
|
+
res.status(404).json({
|
|
243
|
+
error: `Agent not found: ${developerName}`,
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
res.json(result);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
res.status(500).json({
|
|
251
|
+
error: err instanceof Error ? err.message : "Get agent failed",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* POST /agent
|
|
257
|
+
* Create a new agent by writing files to disk.
|
|
258
|
+
* Body: { developerName, agentContent, bundleMetaContent? }
|
|
259
|
+
* Path: force-app/main/default/aiAuthoringBundles/{developerName}/
|
|
260
|
+
*/
|
|
261
|
+
router.post("/agent", (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
const { developerName, agentContent, bundleMetaContent } = req.body;
|
|
264
|
+
if (!developerName || !agentContent) {
|
|
265
|
+
res.status(400).json({
|
|
266
|
+
error: "Missing required fields: developerName, agentContent",
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const bundlePath = join(bundlesDir, developerName);
|
|
271
|
+
if (directory.exists(bundlePath)) {
|
|
272
|
+
res.status(409).json({
|
|
273
|
+
error: `Agent already exists: ${developerName}`,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
directory.make(bundlePath);
|
|
278
|
+
file.write.text(join(bundlePath, `${developerName}.agent`), agentContent);
|
|
279
|
+
if (bundleMetaContent) {
|
|
280
|
+
file.write.text(join(bundlePath, `${developerName}.bundle-meta.xml`), bundleMetaContent);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const defaultMeta = `<?xml version="1.0" encoding="UTF-8"?>
|
|
284
|
+
<AiAuthoringBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
285
|
+
<bundleType>AGENT</bundleType>
|
|
286
|
+
</AiAuthoringBundle>`;
|
|
287
|
+
file.write.text(join(bundlePath, `${developerName}.bundle-meta.xml`), defaultMeta);
|
|
288
|
+
}
|
|
289
|
+
res.status(201).json({ success: true, developerName });
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
res.status(500).json({
|
|
293
|
+
error: err instanceof Error ? err.message : "Create agent failed",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
/**
|
|
298
|
+
* PUT /agent/:developerName
|
|
299
|
+
* Update agent by writing files directly to disk.
|
|
300
|
+
* Body: { agentContent, bundleMetaContent? }
|
|
301
|
+
* Path: force-app/main/default/aiAuthoringBundles/{developerName}/
|
|
302
|
+
*/
|
|
303
|
+
router.put("/agent/:developerName", (req, res) => {
|
|
304
|
+
try {
|
|
305
|
+
const { developerName } = req.params;
|
|
306
|
+
const { agentContent, bundleMetaContent } = req.body;
|
|
307
|
+
if (!agentContent) {
|
|
308
|
+
res.status(400).json({
|
|
309
|
+
error: "Missing required field: agentContent",
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const bundlePath = join(bundlesDir, developerName);
|
|
314
|
+
if (!directory.exists(bundlePath)) {
|
|
315
|
+
res.status(404).json({
|
|
316
|
+
error: `Agent not found: ${developerName}. Use POST /agent to create.`,
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
file.write.text(join(bundlePath, `${developerName}.agent`), agentContent);
|
|
321
|
+
if (bundleMetaContent) {
|
|
322
|
+
file.write.text(join(bundlePath, `${developerName}.bundle-meta.xml`), bundleMetaContent);
|
|
323
|
+
}
|
|
324
|
+
res.json({ success: true, developerName });
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
res.status(500).json({
|
|
328
|
+
error: err instanceof Error ? err.message : "Update agent failed",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
const apiVersion = "v63.0";
|
|
333
|
+
/**
|
|
334
|
+
* GET /tests/runs/:runId
|
|
335
|
+
* Retrieve test run status. Query: ?results=true for full results.
|
|
336
|
+
* Must be registered before /tests/:filename so "runs" is not captured.
|
|
337
|
+
*/
|
|
338
|
+
router.get("/tests/runs/:runId", async (req, res) => {
|
|
339
|
+
try {
|
|
340
|
+
await ensureInitialized();
|
|
341
|
+
const { runId } = req.params;
|
|
342
|
+
const includeResults = req.query.results === "true";
|
|
343
|
+
const path = includeResults
|
|
344
|
+
? `/services/data/${apiVersion}/einstein/ai-evaluations/runs/${runId}/results`
|
|
345
|
+
: `/services/data/${apiVersion}/einstein/ai-evaluations/runs/${runId}`;
|
|
346
|
+
const result = await context.jsforceConn.request({
|
|
347
|
+
method: "GET",
|
|
348
|
+
url: path,
|
|
349
|
+
});
|
|
350
|
+
res.json(result);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
res.status(500).json({
|
|
354
|
+
error: err instanceof Error ? err.message : "Retrieve test run failed",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
/**
|
|
359
|
+
* GET /tests
|
|
360
|
+
* List available test definitions (local JSON files with "test" in name).
|
|
361
|
+
* Query: ?dir=root|example|all (default: all) - which dirs to scan.
|
|
362
|
+
*/
|
|
363
|
+
router.get("/tests", (req, res) => {
|
|
364
|
+
try {
|
|
365
|
+
const dirFilter = req.query.dir ?? "all";
|
|
366
|
+
const tests = [];
|
|
367
|
+
const dirsToScan = dirFilter === "example"
|
|
368
|
+
? [join(context.projectDir, "example")]
|
|
369
|
+
: dirFilter === "root"
|
|
370
|
+
? [context.projectDir]
|
|
371
|
+
: [context.projectDir, join(context.projectDir, "example")];
|
|
372
|
+
for (const dir of dirsToScan) {
|
|
373
|
+
if (!directory.exists(dir))
|
|
374
|
+
continue;
|
|
375
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
376
|
+
for (const e of entries) {
|
|
377
|
+
if (e.isFile() && e.name.endsWith(".json") && /test/i.test(e.name)) {
|
|
378
|
+
tests.push({
|
|
379
|
+
name: e.name,
|
|
380
|
+
path: join(dir, e.name),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
res.json({ tests: tests.map((t) => ({ name: t.name, path: t.path })) });
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
res.status(500).json({
|
|
389
|
+
error: err instanceof Error ? err.message : "List tests failed",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
/**
|
|
394
|
+
* GET /tests/:filename
|
|
395
|
+
* Retrieve test definition content by filename.
|
|
396
|
+
*/
|
|
397
|
+
router.get("/tests/:filename", (req, res) => {
|
|
398
|
+
try {
|
|
399
|
+
const { filename } = req.params;
|
|
400
|
+
if (!filename || !/^[\w.-]+\.json$/i.test(filename)) {
|
|
401
|
+
res.status(400).json({ error: "Invalid filename" });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const candidates = [
|
|
405
|
+
join(context.projectDir, filename),
|
|
406
|
+
join(context.projectDir, "example", filename),
|
|
407
|
+
];
|
|
408
|
+
for (const p of candidates) {
|
|
409
|
+
if (file.exists(p)) {
|
|
410
|
+
const content = file.read.text(p);
|
|
411
|
+
const parsed = JSON.parse(content ?? "[]");
|
|
412
|
+
return res.json(parsed);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
res.status(404).json({ error: `Test not found: ${filename}` });
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
res.status(500).json({
|
|
419
|
+
error: err instanceof Error ? err.message : "Retrieve test failed",
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
/**
|
|
424
|
+
* POST /tests/run
|
|
425
|
+
* Run a test. Body: { aiEvaluationDefinitionName } for Salesforce AI Evaluation,
|
|
426
|
+
* or { testFile, agentBundleName } for local run via preview session.
|
|
427
|
+
*/
|
|
428
|
+
router.post("/tests/run", async (req, res) => {
|
|
429
|
+
try {
|
|
430
|
+
await ensureInitialized();
|
|
431
|
+
const { aiEvaluationDefinitionName, aiEvaluationDefinitionId, testFile, agentBundleName, } = req.body;
|
|
432
|
+
if (aiEvaluationDefinitionName || aiEvaluationDefinitionId) {
|
|
433
|
+
if (aiEvaluationDefinitionName && aiEvaluationDefinitionId) {
|
|
434
|
+
res.status(400).json({
|
|
435
|
+
error: "Provide exactly one: aiEvaluationDefinitionName or aiEvaluationDefinitionId",
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const body = aiEvaluationDefinitionName
|
|
440
|
+
? { aiEvaluationDefinitionName }
|
|
441
|
+
: { aiEvaluationDefinitionId: aiEvaluationDefinitionId };
|
|
442
|
+
const result = (await context.jsforceConn.request({
|
|
443
|
+
method: "POST",
|
|
444
|
+
url: `/services/data/${apiVersion}/einstein/ai-evaluations/runs`,
|
|
445
|
+
body: JSON.stringify(body),
|
|
446
|
+
}));
|
|
447
|
+
res.json({ runId: result.runId });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (testFile && agentBundleName) {
|
|
451
|
+
const candidates = [
|
|
452
|
+
join(context.projectDir, testFile),
|
|
453
|
+
join(context.projectDir, "example", testFile),
|
|
454
|
+
];
|
|
455
|
+
let content;
|
|
456
|
+
for (const p of candidates) {
|
|
457
|
+
if (file.exists(p)) {
|
|
458
|
+
content = file.read.text(p);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!content) {
|
|
463
|
+
res.status(404).json({ error: `Test file not found: ${testFile}` });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const parsed = JSON.parse(content);
|
|
467
|
+
const cases = Array.isArray(parsed)
|
|
468
|
+
? parsed
|
|
469
|
+
: parsed?.testSuite?.testCases ?? parsed?.testCases ?? [];
|
|
470
|
+
const flatCases = cases.flatMap((c) => {
|
|
471
|
+
const obj = c;
|
|
472
|
+
return Array.isArray(obj?.testCases) ? obj.testCases : [c];
|
|
473
|
+
});
|
|
474
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
475
|
+
const agent = await Agent.init({
|
|
476
|
+
connection: context.sfConn,
|
|
477
|
+
project,
|
|
478
|
+
aabName: agentBundleName,
|
|
479
|
+
});
|
|
480
|
+
const startRes = await agent.preview.start();
|
|
481
|
+
const sessionId = startRes.sessionId;
|
|
482
|
+
sessionAgents.set(sessionId, agent);
|
|
483
|
+
const results = [];
|
|
484
|
+
try {
|
|
485
|
+
for (let i = 0; i < flatCases.length; i++) {
|
|
486
|
+
const tc = flatCases[i];
|
|
487
|
+
const utterance = tc["utterance"] ??
|
|
488
|
+
tc.utterance ??
|
|
489
|
+
tc.inputs?.utterance ??
|
|
490
|
+
(Array.isArray(tc.interactions)
|
|
491
|
+
? tc.interactions
|
|
492
|
+
.map((a) => a.message)
|
|
493
|
+
.join(" ")
|
|
494
|
+
: "");
|
|
495
|
+
if (!utterance)
|
|
496
|
+
continue;
|
|
497
|
+
try {
|
|
498
|
+
await agent.preview.send(utterance);
|
|
499
|
+
results.push({ index: i + 1, utterance, passed: true });
|
|
500
|
+
}
|
|
501
|
+
catch (e) {
|
|
502
|
+
results.push({
|
|
503
|
+
index: i + 1,
|
|
504
|
+
utterance,
|
|
505
|
+
passed: false,
|
|
506
|
+
error: e instanceof Error ? e.message : String(e),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
await agent.preview.end();
|
|
513
|
+
sessionAgents.delete(sessionId);
|
|
514
|
+
}
|
|
515
|
+
res.json({
|
|
516
|
+
total: flatCases.length,
|
|
517
|
+
passed: results.filter((r) => r.passed).length,
|
|
518
|
+
failed: results.filter((r) => !r.passed).length,
|
|
519
|
+
results,
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
res.status(400).json({
|
|
524
|
+
error: "Provide aiEvaluationDefinitionName (or aiEvaluationDefinitionId) for SF run, or testFile+agentBundleName for local run",
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
res.status(500).json({
|
|
529
|
+
error: err instanceof Error ? err.message : "Run test failed",
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return router;
|
|
534
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sf-explorer/agentforce-service",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Express server for Agentforce agent APIs - sendMessage, compile, get/update agent metadata",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agentforce-service": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "npm run build:client || true && tsc",
|
|
12
|
+
"build:client": "cd ../client && npm run build",
|
|
13
|
+
"start": "node bin/cli.js",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"type-check": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"salesforce",
|
|
19
|
+
"agentforce",
|
|
20
|
+
"agents",
|
|
21
|
+
"express"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@polycuber/script.cli": "^1.0.5",
|
|
27
|
+
"@salesforce/agents": "^0.23.0",
|
|
28
|
+
"@salesforce/core": "^8.26.0",
|
|
29
|
+
"adm-zip": "^0.5.16",
|
|
30
|
+
"cors": "^2.8.6",
|
|
31
|
+
"express": "^4.21.0",
|
|
32
|
+
"jsforce": "^2.0.0-beta.22",
|
|
33
|
+
"swagger-ui-express": "^5.0.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/adm-zip": "^0.5.7",
|
|
37
|
+
"@types/cors": "^2.8.19",
|
|
38
|
+
"@types/express": "^4.17.21",
|
|
39
|
+
"@types/node": "^20.11.0",
|
|
40
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
41
|
+
"tsx": "^4.7.0",
|
|
42
|
+
"typescript": "^5.3.3"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"bin"
|
|
50
|
+
],
|
|
51
|
+
"prepare": "npm run build"
|
|
52
|
+
}
|