@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { file, directory } from "@polycuber/script.cli";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Agent, ScriptAgent } from "@salesforce/agents";
|
|
4
|
+
import { SfProject } from "@salesforce/core";
|
|
5
|
+
import { initializeContext } from "./context.js";
|
|
6
|
+
export function registerAgentsRoutes(router, context) {
|
|
7
|
+
const bundlesDir = join(context.projectDir, "force-app", "main", "default", "aiAuthoringBundles");
|
|
8
|
+
async function ensureInitialized() {
|
|
9
|
+
if (!context.orgInfo) {
|
|
10
|
+
await initializeContext(context);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* POST /compile
|
|
15
|
+
* Body: { agentBundleName }
|
|
16
|
+
*/
|
|
17
|
+
router.post("/compile", async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
await ensureInitialized();
|
|
20
|
+
const agentBundleName = req.body.agentBundleName ?? req.body.aabName;
|
|
21
|
+
if (!agentBundleName) {
|
|
22
|
+
res.status(400).json({ error: "Missing required field: agentBundleName" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
26
|
+
const scriptAgent = await Agent.init({
|
|
27
|
+
connection: context.sfConn,
|
|
28
|
+
project,
|
|
29
|
+
aabName: agentBundleName,
|
|
30
|
+
});
|
|
31
|
+
if (!(scriptAgent instanceof ScriptAgent)) {
|
|
32
|
+
res.status(400).json({
|
|
33
|
+
error: "Agent is not a ScriptAgent (compile only supports script agents)",
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const result = await scriptAgent.compile();
|
|
38
|
+
res.json(result);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
res.status(500).json({
|
|
42
|
+
error: err instanceof Error ? err.message : "Compile failed",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* GET /agents
|
|
48
|
+
* Query: ?source=local|remote|all (default: all)
|
|
49
|
+
*/
|
|
50
|
+
router.get("/agents", async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
await ensureInitialized();
|
|
53
|
+
const source = req.query.source ?? "all";
|
|
54
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
55
|
+
if (source === "local") {
|
|
56
|
+
const aabNames = await Agent.list(project);
|
|
57
|
+
res.json({ agents: aabNames, source: "local" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (source === "remote") {
|
|
61
|
+
const bots = await Agent.listRemote(context.sfConn);
|
|
62
|
+
res.json({ agents: bots, source: "remote" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const previewable = await Agent.listPreviewable(context.sfConn, project);
|
|
66
|
+
res.json({ agents: previewable, source: "all" });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
res.status(500).json({
|
|
70
|
+
error: err instanceof Error ? err.message : "Retrieve agents failed",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* GET /agent/:developerName
|
|
76
|
+
*/
|
|
77
|
+
router.get("/agent/:developerName", (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const { developerName } = req.params;
|
|
80
|
+
const bundlePath = join(bundlesDir, developerName);
|
|
81
|
+
const result = {};
|
|
82
|
+
const agentPath = join(bundlePath, `${developerName}.agent`);
|
|
83
|
+
const metaPath = join(bundlePath, `${developerName}.bundle-meta.xml`);
|
|
84
|
+
if (file.exists(agentPath)) {
|
|
85
|
+
const content = file.read.text(agentPath);
|
|
86
|
+
if (content)
|
|
87
|
+
result[`aiAuthoringBundles/${developerName}/${developerName}.agent`] = content;
|
|
88
|
+
}
|
|
89
|
+
if (file.exists(metaPath)) {
|
|
90
|
+
const content = file.read.text(metaPath);
|
|
91
|
+
if (content)
|
|
92
|
+
result[`aiAuthoringBundles/${developerName}/${developerName}.bundle-meta.xml`] = content;
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(result).length === 0) {
|
|
95
|
+
res.status(404).json({
|
|
96
|
+
error: `Agent not found: ${developerName}`,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.json(result);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
res.status(500).json({
|
|
104
|
+
error: err instanceof Error ? err.message : "Get agent failed",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* POST /agent
|
|
110
|
+
* Body: { developerName, agentContent, bundleMetaContent? }
|
|
111
|
+
*/
|
|
112
|
+
router.post("/agent", (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const { developerName, agentContent, bundleMetaContent } = req.body;
|
|
115
|
+
if (!developerName || !agentContent) {
|
|
116
|
+
res.status(400).json({
|
|
117
|
+
error: "Missing required fields: developerName, agentContent",
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const bundlePath = join(bundlesDir, developerName);
|
|
122
|
+
if (directory.exists(bundlePath)) {
|
|
123
|
+
res.status(409).json({
|
|
124
|
+
error: `Agent already exists: ${developerName}`,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
directory.make(bundlePath);
|
|
129
|
+
file.write.text(join(bundlePath, `${developerName}.agent`), agentContent);
|
|
130
|
+
if (bundleMetaContent) {
|
|
131
|
+
file.write.text(join(bundlePath, `${developerName}.bundle-meta.xml`), bundleMetaContent);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const defaultMeta = `<?xml version="1.0" encoding="UTF-8"?>
|
|
135
|
+
<AiAuthoringBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
136
|
+
<bundleType>AGENT</bundleType>
|
|
137
|
+
</AiAuthoringBundle>`;
|
|
138
|
+
file.write.text(join(bundlePath, `${developerName}.bundle-meta.xml`), defaultMeta);
|
|
139
|
+
}
|
|
140
|
+
res.status(201).json({ success: true, developerName });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
res.status(500).json({
|
|
144
|
+
error: err instanceof Error ? err.message : "Create agent failed",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* PUT /agent/:developerName
|
|
150
|
+
* Body: { agentContent, bundleMetaContent? }
|
|
151
|
+
*/
|
|
152
|
+
router.put("/agent/:developerName", (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
const { developerName } = req.params;
|
|
155
|
+
const { agentContent, bundleMetaContent } = req.body;
|
|
156
|
+
if (!agentContent) {
|
|
157
|
+
res.status(400).json({
|
|
158
|
+
error: "Missing required field: agentContent",
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const bundlePath = join(bundlesDir, developerName);
|
|
163
|
+
if (!directory.exists(bundlePath)) {
|
|
164
|
+
res.status(404).json({
|
|
165
|
+
error: `Agent not found: ${developerName}. Use POST /agent to create.`,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
file.write.text(join(bundlePath, `${developerName}.agent`), agentContent);
|
|
170
|
+
if (bundleMetaContent) {
|
|
171
|
+
file.write.text(join(bundlePath, `${developerName}.bundle-meta.xml`), bundleMetaContent);
|
|
172
|
+
}
|
|
173
|
+
res.json({ success: true, developerName });
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
res.status(500).json({
|
|
177
|
+
error: err instanceof Error ? err.message : "Update agent failed",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentInstance } from "@salesforce/agents";
|
|
2
|
+
import type { Connection as JSForceConnection } from "jsforce";
|
|
3
|
+
import type { Connection as SfConnection } from "@salesforce/core";
|
|
4
|
+
import { type OrgInfo } from "../org.js";
|
|
5
|
+
/** Session ID -> agent instance (for preview sendMessage, trace, endSession) */
|
|
6
|
+
export declare const sessionAgents: Map<string, AgentInstance>;
|
|
7
|
+
export interface ServerContext {
|
|
8
|
+
projectDir: string;
|
|
9
|
+
orgAlias?: string;
|
|
10
|
+
orgInfo?: OrgInfo;
|
|
11
|
+
jsforceConn?: JSForceConnection;
|
|
12
|
+
sfConn?: SfConnection;
|
|
13
|
+
}
|
|
14
|
+
/** Initialize org connection (jsforce + @salesforce/core). Call at server startup. */
|
|
15
|
+
export declare function initializeContext(context: ServerContext): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getOrgInfoFromCli, createJsforceConnection, createSfConnection, } from "../org.js";
|
|
2
|
+
/** Session ID -> agent instance (for preview sendMessage, trace, endSession) */
|
|
3
|
+
export const sessionAgents = new Map();
|
|
4
|
+
/** Initialize org connection (jsforce + @salesforce/core). Call at server startup. */
|
|
5
|
+
export async function initializeContext(context) {
|
|
6
|
+
if (context.orgInfo)
|
|
7
|
+
return;
|
|
8
|
+
context.orgInfo = getOrgInfoFromCli(context.orgAlias, context.projectDir);
|
|
9
|
+
context.jsforceConn = createJsforceConnection(context.orgInfo);
|
|
10
|
+
context.sfConn = await createSfConnection(context.orgInfo);
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { registerSessionRoutes } from "./session.js";
|
|
3
|
+
import { registerAgentsRoutes } from "./agents.js";
|
|
4
|
+
import { registerTestsRoutes } from "./tests.js";
|
|
5
|
+
export { initializeContext } from "./context.js";
|
|
6
|
+
export function createRoutes(context) {
|
|
7
|
+
const router = Router();
|
|
8
|
+
// All routes on same router - avoids sub-router mounting issues
|
|
9
|
+
registerSessionRoutes(router, context);
|
|
10
|
+
registerAgentsRoutes(router, context);
|
|
11
|
+
registerTestsRoutes(router, context);
|
|
12
|
+
return router;
|
|
13
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Agent } from "@salesforce/agents";
|
|
2
|
+
import { SfProject } from "@salesforce/core";
|
|
3
|
+
import { initializeContext } from "./context.js";
|
|
4
|
+
import { sessionAgents } from "./context.js";
|
|
5
|
+
/** Normalize API response to PlannerResponse. Handles AgentTraceResponse format. */
|
|
6
|
+
function normalizeTrace(raw) {
|
|
7
|
+
if (!raw || typeof raw !== "object")
|
|
8
|
+
return null;
|
|
9
|
+
const obj = raw;
|
|
10
|
+
// API may return { actions: [{ returnValue: { planId, sessionId, intent, topic, plan } }] }
|
|
11
|
+
const actions = obj.actions;
|
|
12
|
+
if (Array.isArray(actions) && actions.length > 0) {
|
|
13
|
+
const first = actions[0];
|
|
14
|
+
const rv = first?.returnValue;
|
|
15
|
+
if (rv && typeof rv.planId === "string") {
|
|
16
|
+
return {
|
|
17
|
+
type: "PlanSuccessResponse",
|
|
18
|
+
planId: rv.planId,
|
|
19
|
+
sessionId: rv.sessionId ?? "",
|
|
20
|
+
intent: rv.intent ?? "",
|
|
21
|
+
topic: rv.topic ?? "",
|
|
22
|
+
plan: Array.isArray(rv.plan) ? rv.plan : [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Already PlannerResponse format
|
|
27
|
+
if (typeof obj.planId === "string" && Array.isArray(obj.plan)) {
|
|
28
|
+
return obj;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export function registerSessionRoutes(router, context) {
|
|
33
|
+
async function ensureInitialized() {
|
|
34
|
+
if (!context.orgInfo) {
|
|
35
|
+
await initializeContext(context);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* POST /startSession
|
|
40
|
+
* Body: { agentBundleName } for script agent OR { apiNameOrId } for production agent
|
|
41
|
+
* Optional: { useMock } - for script agents only. true = simulated (default), false = live Apex/flows.
|
|
42
|
+
*/
|
|
43
|
+
router.post("/startSession", async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
await ensureInitialized();
|
|
46
|
+
const agentBundleName = req.body.agentBundleName ?? req.body.aabName;
|
|
47
|
+
const apiNameOrId = req.body.apiNameOrId;
|
|
48
|
+
const useMock = req.body.useMock !== false; // default true (simulated)
|
|
49
|
+
if (!agentBundleName && !apiNameOrId) {
|
|
50
|
+
res.status(400).json({
|
|
51
|
+
error: "Missing required field: agentBundleName (script agent) or apiNameOrId (production agent)",
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
56
|
+
const agent = agentBundleName
|
|
57
|
+
? await Agent.init({
|
|
58
|
+
connection: context.sfConn,
|
|
59
|
+
project,
|
|
60
|
+
aabName: agentBundleName,
|
|
61
|
+
})
|
|
62
|
+
: await Agent.init({
|
|
63
|
+
connection: context.sfConn,
|
|
64
|
+
project,
|
|
65
|
+
apiNameOrId: apiNameOrId,
|
|
66
|
+
});
|
|
67
|
+
const mockMode = agentBundleName ? (useMock ? "Mock" : "Live Test") : undefined;
|
|
68
|
+
const startResponse = await agent.preview.start(mockMode);
|
|
69
|
+
sessionAgents.set(startResponse.sessionId, agent);
|
|
70
|
+
res.json(startResponse);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
res.status(500).json({
|
|
74
|
+
error: err instanceof Error ? err.message : "Start session failed",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* POST /sendmessage
|
|
80
|
+
* Body: { sessionId, message }
|
|
81
|
+
*/
|
|
82
|
+
router.post("/sendmessage", async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const { sessionId, message } = req.body;
|
|
85
|
+
if (!sessionId || !message) {
|
|
86
|
+
res.status(400).json({
|
|
87
|
+
error: "Missing required fields: sessionId, message",
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const agent = sessionAgents.get(sessionId);
|
|
92
|
+
if (!agent) {
|
|
93
|
+
res.status(404).json({
|
|
94
|
+
error: "Session not found. Call startSession first.",
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const sendResponse = await agent.preview.send(message);
|
|
99
|
+
res.json(sendResponse);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
res.status(500).json({
|
|
103
|
+
error: err instanceof Error ? err.message : "Send message failed",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* POST /trace
|
|
109
|
+
* Get the plan trace for a given planId from an active session.
|
|
110
|
+
* Body: { sessionId, planId }
|
|
111
|
+
*/
|
|
112
|
+
router.post("/trace", async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const { sessionId, planId } = req.body;
|
|
115
|
+
if (!sessionId || !planId) {
|
|
116
|
+
res.status(400).json({
|
|
117
|
+
error: "Missing required fields: sessionId, planId",
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const agent = sessionAgents.get(sessionId);
|
|
122
|
+
if (!agent) {
|
|
123
|
+
res.status(404).json({
|
|
124
|
+
error: "Session not found. Call startSession first.",
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let rawTrace;
|
|
129
|
+
try {
|
|
130
|
+
rawTrace = await agent.getTrace(planId);
|
|
131
|
+
}
|
|
132
|
+
catch (apiErr) {
|
|
133
|
+
console.warn(`[trace] getTrace API failed for planId=${planId}:`, apiErr);
|
|
134
|
+
rawTrace = undefined;
|
|
135
|
+
}
|
|
136
|
+
let trace = null;
|
|
137
|
+
if (rawTrace !== undefined && rawTrace !== null) {
|
|
138
|
+
trace = normalizeTrace(rawTrace);
|
|
139
|
+
}
|
|
140
|
+
// Fallback: read from disk (script agents write traces during sendMessage)
|
|
141
|
+
if (!trace && agent.preview.getAllTraces) {
|
|
142
|
+
try {
|
|
143
|
+
const allTraces = await agent.preview.getAllTraces();
|
|
144
|
+
for (const t of allTraces) {
|
|
145
|
+
const normalized = normalizeTrace(t) ?? t;
|
|
146
|
+
if (normalized.planId === planId) {
|
|
147
|
+
trace = normalized;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// getAllTraces may throw if historyDir doesn't exist
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!trace) {
|
|
157
|
+
console.warn(`[trace] No trace for planId=${planId} (session=${sessionId}). Trace is only available for script agents (agentBundleName), not published agents (apiNameOrId).`);
|
|
158
|
+
}
|
|
159
|
+
res.json(trace);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
res.status(500).json({
|
|
163
|
+
error: err instanceof Error ? err.message : "Get trace failed",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
/**
|
|
168
|
+
* POST /endSession
|
|
169
|
+
* Body: { sessionId }
|
|
170
|
+
*/
|
|
171
|
+
router.post("/endSession", async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const { sessionId } = req.body;
|
|
174
|
+
if (!sessionId) {
|
|
175
|
+
res.status(400).json({ error: "Missing required field: sessionId" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const agent = sessionAgents.get(sessionId);
|
|
179
|
+
if (!agent) {
|
|
180
|
+
res.status(404).json({
|
|
181
|
+
error: "Session not found. Call startSession first.",
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const endResponse = await agent.preview.end();
|
|
186
|
+
sessionAgents.delete(sessionId);
|
|
187
|
+
res.json(endResponse);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
res.status(500).json({
|
|
191
|
+
error: err instanceof Error ? err.message : "End session failed",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { file, directory } from "@polycuber/script.cli";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Agent } from "@salesforce/agents";
|
|
5
|
+
import { SfProject } from "@salesforce/core";
|
|
6
|
+
import { initializeContext } from "./context.js";
|
|
7
|
+
import { sessionAgents } from "./context.js";
|
|
8
|
+
const API_VERSION = "v63.0";
|
|
9
|
+
export function registerTestsRoutes(router, context) {
|
|
10
|
+
async function ensureInitialized() {
|
|
11
|
+
if (!context.orgInfo) {
|
|
12
|
+
await initializeContext(context);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* GET /tests/runs/:runId
|
|
17
|
+
* Must be registered before /tests/:filename so "runs" is not captured.
|
|
18
|
+
*/
|
|
19
|
+
router.get("/tests/runs/:runId", async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
await ensureInitialized();
|
|
22
|
+
const { runId } = req.params;
|
|
23
|
+
const includeResults = req.query.results === "true";
|
|
24
|
+
const path = includeResults
|
|
25
|
+
? `/services/data/${API_VERSION}/einstein/ai-evaluations/runs/${runId}/results`
|
|
26
|
+
: `/services/data/${API_VERSION}/einstein/ai-evaluations/runs/${runId}`;
|
|
27
|
+
const result = await context.jsforceConn.request({
|
|
28
|
+
method: "GET",
|
|
29
|
+
url: path,
|
|
30
|
+
});
|
|
31
|
+
res.json(result);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
res.status(500).json({
|
|
35
|
+
error: err instanceof Error ? err.message : "Retrieve test run failed",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* GET /tests
|
|
41
|
+
* Query: ?dir=root|example|all (default: all)
|
|
42
|
+
*/
|
|
43
|
+
router.get("/tests", (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const dirFilter = req.query.dir ?? "all";
|
|
46
|
+
const tests = [];
|
|
47
|
+
const dirsToScan = dirFilter === "example"
|
|
48
|
+
? [join(context.projectDir, "example")]
|
|
49
|
+
: dirFilter === "root"
|
|
50
|
+
? [context.projectDir]
|
|
51
|
+
: [context.projectDir, join(context.projectDir, "example")];
|
|
52
|
+
for (const dir of dirsToScan) {
|
|
53
|
+
if (!directory.exists(dir))
|
|
54
|
+
continue;
|
|
55
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
if (e.isFile() && e.name.endsWith(".json") && /test/i.test(e.name)) {
|
|
58
|
+
tests.push({
|
|
59
|
+
name: e.name,
|
|
60
|
+
path: join(dir, e.name),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
res.json({ tests: tests.map((t) => ({ name: t.name, path: t.path })) });
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
res.status(500).json({
|
|
69
|
+
error: err instanceof Error ? err.message : "List tests failed",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* GET /tests/:filename
|
|
75
|
+
*/
|
|
76
|
+
router.get("/tests/:filename", (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { filename } = req.params;
|
|
79
|
+
if (!filename || !/^[\w.-]+\.json$/i.test(filename)) {
|
|
80
|
+
res.status(400).json({ error: "Invalid filename" });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const candidates = [
|
|
84
|
+
join(context.projectDir, filename),
|
|
85
|
+
join(context.projectDir, "example", filename),
|
|
86
|
+
];
|
|
87
|
+
for (const p of candidates) {
|
|
88
|
+
if (file.exists(p)) {
|
|
89
|
+
const content = file.read.text(p);
|
|
90
|
+
const parsed = JSON.parse(content ?? "[]");
|
|
91
|
+
return res.json(parsed);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
res.status(404).json({ error: `Test not found: ${filename}` });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
res.status(500).json({
|
|
98
|
+
error: err instanceof Error ? err.message : "Retrieve test failed",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* POST /tests/run
|
|
104
|
+
* Body: { aiEvaluationDefinitionName } or { testFile, agentBundleName }
|
|
105
|
+
*/
|
|
106
|
+
router.post("/tests/run", async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
await ensureInitialized();
|
|
109
|
+
const { aiEvaluationDefinitionName, aiEvaluationDefinitionId, testFile, agentBundleName, } = req.body;
|
|
110
|
+
if (aiEvaluationDefinitionName || aiEvaluationDefinitionId) {
|
|
111
|
+
if (aiEvaluationDefinitionName && aiEvaluationDefinitionId) {
|
|
112
|
+
res.status(400).json({
|
|
113
|
+
error: "Provide exactly one: aiEvaluationDefinitionName or aiEvaluationDefinitionId",
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const body = aiEvaluationDefinitionName
|
|
118
|
+
? { aiEvaluationDefinitionName }
|
|
119
|
+
: { aiEvaluationDefinitionId: aiEvaluationDefinitionId };
|
|
120
|
+
const result = (await context.jsforceConn.request({
|
|
121
|
+
method: "POST",
|
|
122
|
+
url: `/services/data/${API_VERSION}/einstein/ai-evaluations/runs`,
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
}));
|
|
125
|
+
res.json({ runId: result.runId });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (testFile && agentBundleName) {
|
|
129
|
+
const candidates = [
|
|
130
|
+
join(context.projectDir, testFile),
|
|
131
|
+
join(context.projectDir, "example", testFile),
|
|
132
|
+
];
|
|
133
|
+
let content;
|
|
134
|
+
for (const p of candidates) {
|
|
135
|
+
if (file.exists(p)) {
|
|
136
|
+
content = file.read.text(p);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!content) {
|
|
141
|
+
res.status(404).json({ error: `Test file not found: ${testFile}` });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const parsed = JSON.parse(content);
|
|
145
|
+
const cases = Array.isArray(parsed)
|
|
146
|
+
? parsed
|
|
147
|
+
: parsed?.testSuite?.testCases ?? parsed?.testCases ?? [];
|
|
148
|
+
const flatCases = cases.flatMap((c) => {
|
|
149
|
+
const obj = c;
|
|
150
|
+
return Array.isArray(obj?.testCases) ? obj.testCases : [c];
|
|
151
|
+
});
|
|
152
|
+
const project = await SfProject.resolve(context.projectDir);
|
|
153
|
+
const agent = await Agent.init({
|
|
154
|
+
connection: context.sfConn,
|
|
155
|
+
project,
|
|
156
|
+
aabName: agentBundleName,
|
|
157
|
+
});
|
|
158
|
+
const startRes = await agent.preview.start();
|
|
159
|
+
const sessionId = startRes.sessionId;
|
|
160
|
+
sessionAgents.set(sessionId, agent);
|
|
161
|
+
const results = [];
|
|
162
|
+
try {
|
|
163
|
+
for (let i = 0; i < flatCases.length; i++) {
|
|
164
|
+
const tc = flatCases[i];
|
|
165
|
+
const utterance = tc["utterance"] ??
|
|
166
|
+
tc.utterance ??
|
|
167
|
+
tc.inputs?.utterance ??
|
|
168
|
+
(Array.isArray(tc.interactions)
|
|
169
|
+
? tc.interactions
|
|
170
|
+
.map((a) => a.message)
|
|
171
|
+
.join(" ")
|
|
172
|
+
: "");
|
|
173
|
+
if (!utterance)
|
|
174
|
+
continue;
|
|
175
|
+
try {
|
|
176
|
+
await agent.preview.send(utterance);
|
|
177
|
+
results.push({ index: i + 1, utterance, passed: true });
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
results.push({
|
|
181
|
+
index: i + 1,
|
|
182
|
+
utterance,
|
|
183
|
+
passed: false,
|
|
184
|
+
error: e instanceof Error ? e.message : String(e),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await agent.preview.end();
|
|
191
|
+
sessionAgents.delete(sessionId);
|
|
192
|
+
}
|
|
193
|
+
res.json({
|
|
194
|
+
total: flatCases.length,
|
|
195
|
+
passed: results.filter((r) => r.passed).length,
|
|
196
|
+
failed: results.filter((r) => !r.passed).length,
|
|
197
|
+
results,
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
res.status(400).json({
|
|
202
|
+
error: "Provide aiEvaluationDefinitionName (or aiEvaluationDefinitionId) for SF run, or testFile+agentBundleName for local run",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
res.status(500).json({
|
|
207
|
+
error: err instanceof Error ? err.message : "Run test failed",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import type { Connection as JSForceConnection } from "jsforce";
|
|
3
|
+
import type { Connection as SfConnection } from "@salesforce/core";
|
|
4
|
+
import { type OrgInfo } from "./org.js";
|
|
5
|
+
export interface ServerContext {
|
|
6
|
+
projectDir: string;
|
|
7
|
+
orgAlias?: string;
|
|
8
|
+
orgInfo?: OrgInfo;
|
|
9
|
+
jsforceConn?: JSForceConnection;
|
|
10
|
+
sfConn?: SfConnection;
|
|
11
|
+
}
|
|
12
|
+
/** Initialize org connection (jsforce + @salesforce/core). Call at server startup. */
|
|
13
|
+
export declare function initializeContext(context: ServerContext): Promise<void>;
|
|
14
|
+
export declare function createRoutes(context: ServerContext): Router;
|