diff --git a/example-servers/sveltekit/package-lock.json b/example-servers/sveltekit/package-lock.json index ba3c676fe..71b0d6e5f 100644 --- a/example-servers/sveltekit/package-lock.json +++ b/example-servers/sveltekit/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "dependencies": { "deep-chat": "^2.2.2", - "eventsource-parser": "^1.1.2" + "eventsource-parser": "^1.1.2", + "salesforce-agent-api-client": "^1.0.1" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.1", @@ -1261,6 +1262,27 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/eventsource-client": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/eventsource-client/-/eventsource-client-1.1.3.tgz", + "integrity": "sha512-6GJGePDMxin/6VH1Dex7RqnZRguweO1BVKhXpBYwKcQbVLOZPLfeDr9vYSb9ra88kjs0NpT8FaEDz5rExedDkg==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-client/node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/eventsource-parser": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", @@ -2119,6 +2141,15 @@ } ] }, + "node_modules/salesforce-agent-api-client": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/salesforce-agent-api-client/-/salesforce-agent-api-client-1.0.1.tgz", + "integrity": "sha512-Q6zoq2mNH/Ai++rixXtQmecTuN0pAgkK3xZc55xla8QnUAqZ1F8o+d2aRPUiSto1k/KhrvqWrUSc2m5TXHjMWQ==", + "license": "CC0-1.0", + "dependencies": { + "eventsource-client": "^1.1.3" + } + }, "node_modules/sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", diff --git a/example-servers/sveltekit/package.json b/example-servers/sveltekit/package.json index fa305ee11..b5a31409b 100644 --- a/example-servers/sveltekit/package.json +++ b/example-servers/sveltekit/package.json @@ -22,6 +22,7 @@ "type": "module", "dependencies": { "deep-chat": "^2.2.2", - "eventsource-parser": "^1.1.2" + "eventsource-parser": "^1.1.2", + "salesforce-agent-api-client": "^1.0.1" } } diff --git a/example-servers/sveltekit/src/routes/+page.svelte b/example-servers/sveltekit/src/routes/+page.svelte index 77ea9479b..3d0326a8e 100644 --- a/example-servers/sveltekit/src/routes/+page.svelte +++ b/example-servers/sveltekit/src/routes/+page.svelte @@ -232,5 +232,32 @@ errorMessages={{displayServiceErrorMessages: true}} /> +

Server for Salesforce Agentforce

+ + {'Title + +

Make sure to set the SALESFORCE_INSTANCE_URL, SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET and SALESFORCE_AGENT_ID environment variable in your server

+
+
+ + +
{/if} diff --git a/example-servers/sveltekit/src/routes/api/salesforce/agentforce-stream/+server.ts b/example-servers/sveltekit/src/routes/api/salesforce/agentforce-stream/+server.ts new file mode 100644 index 000000000..4869bd599 --- /dev/null +++ b/example-servers/sveltekit/src/routes/api/salesforce/agentforce-stream/+server.ts @@ -0,0 +1,128 @@ +import type { DeepChatTextRequestBody } from "../../../types/deepChatTextRequestBody"; +import type { RequestHandler } from "@sveltejs/kit"; +import AgentApiClient from "salesforce-agent-api-client"; +import { writable } from 'svelte/store'; + +export const config = { + runtime: "edge", + // this is used to enable streaming + dynamic: 'force-dynamic' +}; + +// Make sure to set the SALESFORCE_INSTANCE_URL, SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET and SALESFORCE_AGENT_ID environment variable + +interface StreamEvent { + data: string; + event: string; +} + +const salesforceSession = writable(null); +let sessionStore: string | null = null; +salesforceSession.subscribe(value => { + sessionStore = value; +}); + +export const POST: RequestHandler = async ({ request }) => { + // Load config from .env file + const config = { + instanceUrl: process.env.SALESFORCE_INSTANCE_URL || "", + clientId: process.env.SALESFORCE_CLIENT_ID || "", + clientSecret: process.env.SALESFORCE_CLIENT_SECRET || "", + agentId: process.env.SALESFORCE_AGENT_ID || "", + }; + + // Configure Agent API client + const client = new AgentApiClient(config); + const responseStream = new TransformStream(); + const writer = responseStream.writable.getWriter(); + const encoder = new TextEncoder(); + + try { + // Authenticate + await client.authenticate(); + + // Get the message from the request + const messageRequestBody = (await request.json()) as DeepChatTextRequestBody; + const message = messageRequestBody.messages[messageRequestBody.messages.length - 1].text; + + // Validate that a message was provided in the request + if (!message) { + throw new Error("No message provided"); + } + + let sessionId: string; + + // Session Management + if (sessionStore) { + sessionId = sessionStore; + } else { + sessionId = await client.createSession(); + salesforceSession.set(sessionId); + } + + // Stream event handler + function streamEventHandler({ data, event }: StreamEvent) { + const eventData = JSON.parse(data); + // console.log('Event:', event); + + switch (event) { + case 'TEXT_CHUNK': + // Handle TEXT_CHUNK event + // Write the event data to the stream in Deep Chat format + console.log('TEXT_CHUNK:', eventData.message?.message); + writer.write(encoder.encode(`data: ${JSON.stringify({ text: eventData.message?.message || '' })}\n\n`)); + break; + case 'END_OF_TURN': + // Handle END_OF_TURN event + writer.close(); + break; + case 'INFORM': + // do nothing for INFORM event + break; + default: + console.log('Unknown event:', eventData); + } + + } + + // Stream disconnect handler + async function streamDisconnectHandler() { + if (!writer.closed) { + writer.close(); + } + } + + // Send the streaming message + client.sendStreamingMessage( + sessionId, + message, + [], // Empty array for variables/context + streamEventHandler, + streamDisconnectHandler + ); + + return new Response(responseStream.readable, { + headers: { + 'Content-Type': 'text/event-stream', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache, no-transform', + }, + }); + + } catch (error) { + console.error("Salesforce Agent API Error:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + + // Write error to stream and close it + writer.write(encoder.encode(`data: ${JSON.stringify({ text: "Error: " + errorMessage })}\n\n`)); + writer.close(); + + return new Response(responseStream.readable, { + headers: { + 'Content-Type': 'text/event-stream', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache, no-transform', + }, + }); + } +}; diff --git a/example-servers/sveltekit/src/routes/api/salesforce/agentforce/+server.ts b/example-servers/sveltekit/src/routes/api/salesforce/agentforce/+server.ts new file mode 100644 index 000000000..51c409f13 --- /dev/null +++ b/example-servers/sveltekit/src/routes/api/salesforce/agentforce/+server.ts @@ -0,0 +1,89 @@ +import type { DeepChatTextRequestBody } from "../../../types/deepChatTextRequestBody"; +import type { RequestHandler } from "@sveltejs/kit"; +import AgentApiClient from "salesforce-agent-api-client"; +import { writable } from 'svelte/store'; + +export const config = { + runtime: "edge", +}; + +// Make sure to set the SALESFORCE_INSTANCE_URL, SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET and SALESFORCE_AGENT_ID environment variable + +// Create a writable store to manage Salesforce session state +// This store holds the session ID for maintaining conversation context +// Initial value is null, indicating no active session +const salesforceSession = writable(null); +let sessionStore: string | null = null; +salesforceSession.subscribe(value => { + sessionStore = value; +}); + +export const POST: RequestHandler = async ({ request }) => { + // Load config from env + const config = { + instanceUrl: process.env.SALESFORCE_INSTANCE_URL || "", + clientId: process.env.SALESFORCE_CLIENT_ID || "", + clientSecret: process.env.SALESFORCE_CLIENT_SECRET || "", + agentId: process.env.SALESFORCE_AGENT_ID || "", + }; + + // Configure Agent API client + const client = new AgentApiClient(config); + + try { + // Authenticate + await client.authenticate(); + + // Get the message from the request + const messageRequestBody = (await request.json()) as DeepChatTextRequestBody; + const message = messageRequestBody.messages[messageRequestBody.messages.length - 1].text; + + // Validate that a message was provided in the request + if (!message) { + throw new Error("No message provided"); + } + + let sessionId: string; + + // Session Management: + // We either reuse an existing session from the store or create a new one. + // This helps maintain conversation context and reduces unnecessary session creation. + if (sessionStore) { + // Reuse the existing session ID from the store + sessionId = sessionStore; + } else { + // If no session exists, create a new one and store it + sessionId = await client.createSession(); + salesforceSession.set(sessionId); + } + + // Send the message to Salesforce Agent API and wait for the synchronous response + // The empty array parameter is for additional context that might be needed in future + const syncResponse = await client.sendSyncMessage(sessionId, message, []); + + // Format and return the agent's response + // If no message is received, provide a fallback response + return new Response(JSON.stringify({ + text: syncResponse.messages[0].message || "No response received from the Salesforce agent", + }), { + headers: { "content-type": "application/json" }, + }); + + } catch (error) { + // Log the full error for debugging purposes + console.error("Salesforce Agent API Error:", error); + // Extract error message safely, providing a fallback for non-Error objects + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + return new Response( + JSON.stringify({ + text: "Error communicating with Salesforce Agent API: " + errorMessage, + }), + { + status: 500, + headers: { + "content-type": "application/json", + }, + } + ); + } +}; diff --git a/website/static/img/salesforceLogo.png b/website/static/img/salesforceLogo.png new file mode 100644 index 000000000..8aaef6686 Binary files /dev/null and b/website/static/img/salesforceLogo.png differ