From 4b39d98a9fbbb4d03b56b78e096e88b0e48a839a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20J=C3=A4ggi?= Date: Tue, 9 Jul 2024 07:45:35 +0200 Subject: [PATCH 1/2] feat: firefall conversation support (WIP) --- .../src/clients/firefall-client.js | 109 +++++++++++------- .../test/clients/firefall-client.test.js | 74 ++++++++++++ 2 files changed, 144 insertions(+), 39 deletions(-) diff --git a/packages/spacecat-shared-gpt-client/src/clients/firefall-client.js b/packages/spacecat-shared-gpt-client/src/clients/firefall-client.js index f6ecca34..fa0d80ef 100644 --- a/packages/spacecat-shared-gpt-client/src/clients/firefall-client.js +++ b/packages/spacecat-shared-gpt-client/src/clients/firefall-client.js @@ -25,6 +25,10 @@ function validateFirefallResponse(response) { || !hasText(response.generations[0][0].text)); } +function templatePrompt(prompt, context) { + return prompt.replace(/\{\{(\w+)\}\}/g, (_, key) => context[key] || ''); +} + export default class FirefallClient { static createFrom(context) { const { log = console } = context; @@ -71,6 +75,7 @@ export default class FirefallClient { */ constructor(config, log) { this.config = config; + this.apiBaseUrl = `${config.apiEndpoint}/v2/`; this.log = log; this.imsClient = config.imsClient; this.apiAuth = null; @@ -89,15 +94,9 @@ export default class FirefallClient { this.log.debug(`${message}: took ${duration}ms`); } - async #submitJob(prompt) { + async #apiCall(path, method, body = null) { const apiAuth = await this.#getApiAuth(); - - const body = JSON.stringify({ - input: prompt, - capability_name: this.config.capabilityName, - }); - - const url = createUrl(`${this.config.apiEndpoint}/v2/capability_execution/job`); + const url = `${this.apiBaseUrl}${path}`; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${apiAuth}`, @@ -105,53 +104,47 @@ export default class FirefallClient { 'x-gw-ims-org-id': this.config.imsOrg, }; + const options = { + method, + headers, + }; + + if (body) { + options.body = JSON.stringify(body); + } + this.log.info(`URL: ${url}, Headers: ${JSON.stringify(headers)}`); - const response = await httpFetch(url, { - method: 'POST', - headers, - body, - }); + const response = await httpFetch(createUrl(url), options); if (!response.ok) { - throw new Error(`Job submission failed with status code ${response.status}`); + const msg = await response.text(); + throw new Error(`API call failed with status code ${response.status}: ${msg}`); } return response.json(); } + async #submitJob(prompt) { + const path = 'capability_execution/job'; + const body = { + input: prompt, + capability_name: this.config.capabilityName, + }; + + return this.#apiCall(path, 'POST', body); + } + /* eslint-disable no-await-in-loop */ async #pollJobStatus(jobId) { - const apiAuth = await this.#getApiAuth(); - let jobStatusResponse; do { await new Promise( (resolve) => { setTimeout(resolve, this.config.pollInterval); }, - ); // Wait for 2 seconds before polling - - const url = `${this.config.apiEndpoint}/v2/capability_execution/job/${jobId}`; - const headers = { - Authorization: `Bearer ${apiAuth}`, - 'x-api-key': this.config.apiKey, - 'x-gw-ims-org-id': this.config.imsOrg, - }; - - this.log.info(`URL: ${url}, Headers: ${JSON.stringify(headers)}`); - - const response = await httpFetch( - createUrl(url), - { - method: 'GET', - headers, - }, - ); - - if (!response.ok) { - throw new Error(`Job polling failed with status code ${response.status}`); - } + ); // Wait for pollInterval before polling - jobStatusResponse = await response.json(); + const url = `capability_execution/job/${jobId}`; + jobStatusResponse = await this.#apiCall(url, 'GET'); } while (jobStatusResponse.status === 'PROCESSING' || jobStatusResponse.status === 'WAITING'); if (jobStatusResponse.status !== 'SUCCEEDED') { @@ -161,6 +154,44 @@ export default class FirefallClient { return jobStatusResponse; } + async #createConversationSession() { + const url = 'conversation'; + const body = { capability_name: this.config.capabilityName, conversation_name: 'spacecat' }; + + const response = await this.#apiCall(url, 'POST', body); + return response.conversation_id; + } + + async #submitPromptToConversation(sessionId, prompt) { + const path = 'query'; + const body = { dialogue: { question: prompt }, conversation_id: sessionId }; + + return this.#apiCall(path, 'POST', body); + } + + async executePromptChain(chainConfig) { + const sessionId = await this.#createConversationSession(); + + let context = {}; + for (const step of chainConfig.steps) { + const prompt = templatePrompt(step.prompt, context); + const response = await this.#submitPromptToConversation(sessionId, prompt); + const { answer } = response.dialogue; + + if (step.onResponse) { + const result = step.onResponse(answer, context); + if (result.abort) { + this.log.info('Prompt chain aborted.'); + break; + } + context = { ...context, ...result.context }; + } else { + context = { ...context, ...response }; + } + } + return context; + } + async fetch(prompt) { if (!hasText(prompt)) { throw new Error('Invalid prompt received'); diff --git a/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js b/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js index 5b1a943a..77ee9e60 100644 --- a/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js +++ b/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js @@ -178,4 +178,78 @@ describe('FirefallClient', () => { await expect(client.fetch('Test prompt')).to.be.rejectedWith('Invalid response format.'); }); }); + + describe('executePromptChain', async () => { + let client; + + beforeEach(() => { + client = FirefallClient.createFrom(mockContext); + }); + + it('executes a prompt chain successfully using conversation API', async () => { + const chainConfig = { + steps: [ + { + prompt: 'What is the capital of France?', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onResponse: (response) => ({ context: { capital: 'Paris' } }), + }, + { + prompt: 'What is the population of {{capital}}?', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onResponse: (response) => ({ context: { population: '2 million' } }), + }, + ], + }; + + nock(mockContext.env.FIREFALL_API_ENDPOINT) + .post('/v2/conversations') + .reply(200, { conversation_id: 'sessionId' }) + .post('/v2/conversations/sessionId/messages') + .reply(200, { messages: [{ id: 'messageId1', content: 'Paris', status: 'SUCCEEDED' }] }) + .get('/v2/conversations/sessionId/messages/messageId1') + .reply(200, { messages: [{ content: 'Paris' }], status: 'SUCCEEDED' }) + .post('/v2/conversations/sessionId/messages') + .reply(200, { messages: [{ id: 'messageId2', content: '2 million', status: 'SUCCEEDED' }] }) + .get('/v2/conversations/sessionId/messages/messageId2') + .reply(200, { messages: [{ content: '2 million' }], status: 'SUCCEEDED' }); + + const result = await client.executePromptChain(chainConfig); + expect(result).to.eql({ capital: 'Paris', population: '2 million' }); + }); + + it('works', async () => { + const chainConfig = { + steps: [ + { + prompt: 'What is the capital of France?', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onResponse: (response) => ({ context: { capital: 'Paris' } }), + }, + { + prompt: 'What is the population of {{capital}}?', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onResponse: (response) => ({ context: { population: '2 million' } }), + }, + ], + }; + + const ctx = { + env: { + FIREFALL_API_ENDPOINT: 'https://firefall-stage.adobe.io', + FIREFALL_API_KEY: 'D729BFC2-8D8A-418A-92B4-624E1E8D6F07', + IMS_HOST: 'ims-na1-stg1.adobelogin.com', + IMS_CLIENT_ID: 'spacecat-firefall-dev', + IMS_CLIENT_CODE: 'eyJhbGciOiJSUzI1NiIsIng1dSI6Imltc19uYTEtc3RnMS1rZXktcGFjLTEuY2VyIiwia2lkIjoiaW1zX25hMS1zdGcxLWtleS1wYWMtMSIsIml0dCI6InBhYyJ9.eyJpZCI6InNwYWNlY2F0LWZpcmVmYWxsLWRldl9zdGciLCJ0eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwiY2xpZW50X2lkIjoic3BhY2VjYXQtZmlyZWZhbGwtZGV2IiwidXNlcl9pZCI6InNwYWNlY2F0LWZpcmVmYWxsLWRldkBBZG9iZVNlcnZpY2UiLCJhcyI6Imltcy1uYTEtc3RnMSIsIm90byI6ZmFsc2UsImNyZWF0ZWRfYXQiOiIxNzA3MjEwOTYyOTg3Iiwic2NvcGUiOiJzeXN0ZW0ifQ.PUUVTXY8_a2iPrkmHuc4H5RSeYmX1Bgf6Qfw0QTSlpBoNc2Qnyh86jVUoKmJa7bbVt6VPXz0PjNL5TCz-m7IEWMB7lWbNuxOcO7TFkeNdJA6KYf6rnDyfFB8HmssYpJTa79zV5Px0lXLcQ-x_5MUe_BudIpQxBD482j7Jklg-3ec8TbO34BXSixmLEFLTqz1akVmmxoPYrsGkbK9huFtlRJJnzTZKRapsTC3e0sjqYcGkOhSUIarW7DUFxNThA1JpeJSkm9NixZb3V4kuhm84mLyE2DpG1hqZPG3FqquiPbh0D0egFAExIRugTgs0OE8ULkvl_IJn-AnrPrsLcciSA', + IMS_CLIENT_SECRET: 's8e-zrHCq1fRAS3xxbi9s23zPnXY25cmf0AF', + }, + log: console, + }; + const ffClient = FirefallClient.createFrom(ctx); + + const result = await ffClient.executePromptChain(chainConfig); + + expect(result).to.eql({ capital: 'Paris', population: '2 million' }); + }); + }); }); From 6d9d8a3dd12f2f5019cd2dad0c3e253e62f78266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20J=C3=A4ggi?= Date: Tue, 9 Jul 2024 07:48:06 +0200 Subject: [PATCH 2/2] fix: remove token --- .../test/clients/firefall-client.test.js | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js b/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js index 77ee9e60..bce6c704 100644 --- a/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js +++ b/packages/spacecat-shared-gpt-client/test/clients/firefall-client.test.js @@ -217,39 +217,5 @@ describe('FirefallClient', () => { const result = await client.executePromptChain(chainConfig); expect(result).to.eql({ capital: 'Paris', population: '2 million' }); }); - - it('works', async () => { - const chainConfig = { - steps: [ - { - prompt: 'What is the capital of France?', - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onResponse: (response) => ({ context: { capital: 'Paris' } }), - }, - { - prompt: 'What is the population of {{capital}}?', - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onResponse: (response) => ({ context: { population: '2 million' } }), - }, - ], - }; - - const ctx = { - env: { - FIREFALL_API_ENDPOINT: 'https://firefall-stage.adobe.io', - FIREFALL_API_KEY: 'D729BFC2-8D8A-418A-92B4-624E1E8D6F07', - IMS_HOST: 'ims-na1-stg1.adobelogin.com', - IMS_CLIENT_ID: 'spacecat-firefall-dev', - IMS_CLIENT_CODE: 'eyJhbGciOiJSUzI1NiIsIng1dSI6Imltc19uYTEtc3RnMS1rZXktcGFjLTEuY2VyIiwia2lkIjoiaW1zX25hMS1zdGcxLWtleS1wYWMtMSIsIml0dCI6InBhYyJ9.eyJpZCI6InNwYWNlY2F0LWZpcmVmYWxsLWRldl9zdGciLCJ0eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwiY2xpZW50X2lkIjoic3BhY2VjYXQtZmlyZWZhbGwtZGV2IiwidXNlcl9pZCI6InNwYWNlY2F0LWZpcmVmYWxsLWRldkBBZG9iZVNlcnZpY2UiLCJhcyI6Imltcy1uYTEtc3RnMSIsIm90byI6ZmFsc2UsImNyZWF0ZWRfYXQiOiIxNzA3MjEwOTYyOTg3Iiwic2NvcGUiOiJzeXN0ZW0ifQ.PUUVTXY8_a2iPrkmHuc4H5RSeYmX1Bgf6Qfw0QTSlpBoNc2Qnyh86jVUoKmJa7bbVt6VPXz0PjNL5TCz-m7IEWMB7lWbNuxOcO7TFkeNdJA6KYf6rnDyfFB8HmssYpJTa79zV5Px0lXLcQ-x_5MUe_BudIpQxBD482j7Jklg-3ec8TbO34BXSixmLEFLTqz1akVmmxoPYrsGkbK9huFtlRJJnzTZKRapsTC3e0sjqYcGkOhSUIarW7DUFxNThA1JpeJSkm9NixZb3V4kuhm84mLyE2DpG1hqZPG3FqquiPbh0D0egFAExIRugTgs0OE8ULkvl_IJn-AnrPrsLcciSA', - IMS_CLIENT_SECRET: 's8e-zrHCq1fRAS3xxbi9s23zPnXY25cmf0AF', - }, - log: console, - }; - const ffClient = FirefallClient.createFrom(ctx); - - const result = await ffClient.executePromptChain(chainConfig); - - expect(result).to.eql({ capital: 'Paris', population: '2 million' }); - }); }); });