@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/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
+ }