diff --git a/helper-apps/cortex-file-handler/package-lock.json b/helper-apps/cortex-file-handler/package-lock.json index 646374d2..71165792 100644 --- a/helper-apps/cortex-file-handler/package-lock.json +++ b/helper-apps/cortex-file-handler/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aj-archipelago/cortex-file-handler", - "version": "2.8.0", + "version": "2.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aj-archipelago/cortex-file-handler", - "version": "2.8.0", + "version": "2.8.1", "dependencies": { "@azure/storage-blob": "^12.13.0", "@distube/ytdl-core": "^4.14.3", diff --git a/helper-apps/cortex-file-handler/package.json b/helper-apps/cortex-file-handler/package.json index 88a26399..638ce7ba 100644 --- a/helper-apps/cortex-file-handler/package.json +++ b/helper-apps/cortex-file-handler/package.json @@ -1,6 +1,6 @@ { "name": "@aj-archipelago/cortex-file-handler", - "version": "2.8.0", + "version": "2.8.1", "description": "File handling service for Cortex - handles file uploads, media chunking, and document processing", "type": "module", "main": "src/index.js", diff --git a/helper-apps/cortex-file-handler/src/index.js b/helper-apps/cortex-file-handler/src/index.js index c7c12344..1c7fd85c 100644 --- a/helper-apps/cortex-file-handler/src/index.js +++ b/helper-apps/cortex-file-handler/src/index.js @@ -501,13 +501,34 @@ async function CortexFileHandler(context, req) { return; } + // Reconstruct missing filename from URL if needed (before creating response) + if (!hashResult.filename && hashResult.url) { + try { + const urlObj = new URL(hashResult.url); + const pathSegments = urlObj.pathname.split('/').filter(segment => segment.length > 0); + if (pathSegments.length > 0) { + // Extract filename from URL path (last segment) + const blobName = pathSegments[pathSegments.length - 1]; + // Remove query params if any got included + hashResult.filename = blobName.split('?')[0]; + } + } catch (error) { + context.log(`Error extracting filename from URL: ${error.message}`); + } + } + + // Ensure hash is set if missing + if (!hashResult.hash) { + hashResult.hash = hash; + } + // Create the response object const response = { - message: `File '${hashResult.filename}' uploaded successfully.`, + message: `File '${hashResult.filename || 'unknown'}' uploaded successfully.`, filename: hashResult.filename, url: hashResult.url, gcs: hashResult.gcs, - hash: hashResult.hash, + hash: hashResult.hash || hash, timestamp: new Date().toISOString(), }; @@ -625,6 +646,7 @@ async function CortexFileHandler(context, req) { // Update redis timestamp with current time // Note: setFileStoreMap will remove shortLivedUrl fields before storing + // hashResult has already been enriched with filename/hash above if missing await setFileStoreMap(hash, hashResult, resolvedContextId); context.res = { diff --git a/helper-apps/cortex-file-handler/src/redis.js b/helper-apps/cortex-file-handler/src/redis.js index 114b8493..b19d33c4 100644 --- a/helper-apps/cortex-file-handler/src/redis.js +++ b/helper-apps/cortex-file-handler/src/redis.js @@ -3,27 +3,6 @@ import { getDefaultContainerName } from "./constants.js"; const connectionString = process.env["REDIS_CONNECTION_STRING"]; -/** - * Get key for Redis storage. - * - * IMPORTANT: - * - We **never** write hash+container scoped keys anymore (legacy only). - * - We *do* support (optional) hash+contextId scoping for per-user/per-context storage. - * - For reads, we can fall back to legacy hash+container keys if they still exist in Redis. - * - * Key format: - * - No context: "" - * - With contextId: ":ctx:" - * - * @param {string} hash - The file hash - * @param {string|null} contextId - Optional context id - * @returns {string} The redis key for this hash/context - */ -export const getScopedHashKey = (hash, contextId = null) => { - if (!hash) return hash; - if (!contextId) return hash; - return `${hash}:ctx:${contextId}`; -}; const legacyContainerKey = (hash, containerName) => { if (!hash || !containerName) return null; @@ -304,10 +283,13 @@ const getFileStoreMap = async (hash, skipLazyCleanup = false, contextId = null) if (contextId) { const contextMapKey = `FileStoreMap:ctx:${contextId}`; value = await client.hget(contextMapKey, hash); - } - - // Fall back to unscoped map if not found - if (!value) { + // If contextId is provided, do NOT fall back to unscoped map + // This ensures proper context isolation and forces re-upload to current storage account + if (!value) { + return null; + } + } else { + // No contextId - check unscoped map value = await client.hget("FileStoreMap", hash); } @@ -315,21 +297,19 @@ const getFileStoreMap = async (hash, skipLazyCleanup = false, contextId = null) // If unscoped hash doesn't exist, fall back to legacy hash+container key (if still present). // SECURITY: Context-scoped lookups NEVER fall back - they must match exactly. if (!value && !contextId) { - const baseHash = hash; - // Only allow fallback for unscoped keys (not context-scoped) // Context-scoped keys are security-isolated and must match exactly - if (baseHash && !String(baseHash).includes(":")) { + if (hash && !String(hash).includes(":")) { const defaultContainerName = getDefaultContainerName(); - const legacyKey = legacyContainerKey(baseHash, defaultContainerName); + const legacyKey = legacyContainerKey(hash, defaultContainerName); if (legacyKey) { value = await client.hget("FileStoreMap", legacyKey); if (value) { console.log( - `Found legacy container-scoped key ${legacyKey} for hash ${baseHash}; migrating to unscoped key`, + `Found legacy container-scoped key ${legacyKey} for hash ${hash}; migrating to unscoped key`, ); // Migrate to unscoped key (we do NOT write legacy container-scoped keys) - await client.hset("FileStoreMap", baseHash, value); + await client.hset("FileStoreMap", hash, value); // Delete the legacy key after migration await client.hdel("FileStoreMap", legacyKey); console.log(`Deleted legacy key ${legacyKey} after migration`); @@ -415,62 +395,38 @@ const getFileStoreMap = async (hash, skipLazyCleanup = false, contextId = null) // Function to remove key from "FileStoreMap" hash map // If contextId is provided, removes from context-scoped map // Otherwise removes from unscoped map -// Hash can be either raw hash or scoped key format (hash:ctx:contextId) -// If scoped format is provided, extracts base hash and removes both scoped and legacy keys const removeFromFileStoreMap = async (hash, contextId = null) => { try { if (!hash) { return; } - // Extract base hash if hash is in scoped format (hash:ctx:contextId) - let baseHash = hash; - let extractedContextId = contextId; - if (String(hash).includes(":ctx:")) { - const parts = String(hash).split(":ctx:"); - baseHash = parts[0]; - if (parts.length > 1 && !extractedContextId) { - extractedContextId = parts[1]; - } - } - let result = 0; - // First, try to delete from unscoped map (in case scoped key was stored there) + // First, try to delete from unscoped map if (!contextId) { - // Remove from unscoped map (including scoped key format if present) result = await client.hdel("FileStoreMap", hash); - // Also try removing with base hash if hash was scoped - if (hash !== baseHash) { - const baseResult = await client.hdel("FileStoreMap", baseHash); - if (baseResult > 0) { - result = baseResult; - } - } } - // Also try to delete from context-scoped map if we extracted a contextId - if (extractedContextId) { - const contextMapKey = `FileStoreMap:ctx:${extractedContextId}`; - const contextResult = await client.hdel(contextMapKey, baseHash); + // Also try to delete from context-scoped map if contextId is provided + if (contextId) { + const contextMapKey = `FileStoreMap:ctx:${contextId}`; + const contextResult = await client.hdel(contextMapKey, hash); if (contextResult > 0) { result = contextResult; } - } else if (contextId) { - // If contextId was provided explicitly, delete from context-scoped map - const contextMapKey = `FileStoreMap:ctx:${contextId}`; - result = await client.hdel(contextMapKey, hash); } + if (result > 0) { console.log(`The hash ${hash} was removed successfully`); } // Always try to clean up legacy container-scoped entry as well. // This ensures we don't leave orphaned legacy keys behind. - // Only attempt legacy cleanup if baseHash doesn't contain a colon (not already scoped) - if (!String(baseHash).includes(":")) { + // Only attempt legacy cleanup if hash doesn't contain a colon + if (!String(hash).includes(":")) { const defaultContainerName = getDefaultContainerName(); - const legacyKey = legacyContainerKey(baseHash, defaultContainerName); + const legacyKey = legacyContainerKey(hash, defaultContainerName); if (legacyKey) { const legacyResult = await client.hdel("FileStoreMap", legacyKey); if (legacyResult > 0) { diff --git a/helper-apps/cortex-file-handler/tests/deleteOperations.test.js b/helper-apps/cortex-file-handler/tests/deleteOperations.test.js index b70f67c8..04467b0b 100644 --- a/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +++ b/helper-apps/cortex-file-handler/tests/deleteOperations.test.js @@ -533,28 +533,23 @@ test.serial("should handle backwards compatibility key removal correctly", async t.is(uploadResponse.status, 200, "Upload should succeed"); // Manually create a legacy unscoped key to test backwards compatibility - const { setFileStoreMap, getFileStoreMap, getScopedHashKey } = await import("../src/redis.js"); - const scopedHash = getScopedHashKey(testHash); - const hashResult = await getFileStoreMap(scopedHash); + const { setFileStoreMap, getFileStoreMap } = await import("../src/redis.js"); + const hashResult = await getFileStoreMap(testHash); if (hashResult) { - // Create legacy unscoped key + // Create legacy unscoped key (already exists from upload, but verify) await setFileStoreMap(testHash, hashResult); - // Verify both keys exist - const scopedExists = await getFileStoreMap(scopedHash); + // Verify key exists const legacyExists = await getFileStoreMap(testHash); - t.truthy(scopedExists, "Scoped key should exist"); t.truthy(legacyExists, "Legacy key should exist"); // Delete file - should remove both keys const deleteResponse = await deleteFileByHash(testHash); t.is(deleteResponse.status, 200, "Delete should succeed"); - // Verify both keys are removed - const scopedAfter = await getFileStoreMap(scopedHash); + // Verify key is removed const legacyAfter = await getFileStoreMap(testHash); - t.falsy(scopedAfter, "Scoped key should be removed"); t.falsy(legacyAfter, "Legacy key should be removed"); } diff --git a/helper-apps/cortex-file-handler/tests/redisMigration.test.js b/helper-apps/cortex-file-handler/tests/redisMigration.test.js index 42fbe417..991065a7 100644 --- a/helper-apps/cortex-file-handler/tests/redisMigration.test.js +++ b/helper-apps/cortex-file-handler/tests/redisMigration.test.js @@ -5,7 +5,6 @@ import { setFileStoreMap, getFileStoreMap, removeFromFileStoreMap, - getScopedHashKey, client, } from "../src/redis.js"; import { getDefaultContainerName } from "../src/constants.js"; @@ -15,7 +14,10 @@ import { getDefaultContainerName } from "../src/constants.js"; * * Key formats: * - Legacy: `:` (read-only, migrated on access) - * - Current: `` (unscoped) or `:ctx:` (context-scoped) + * - Current: `` (unscoped) stored in `FileStoreMap` hash map + * - Context-scoped: `` stored in `FileStoreMap:ctx:` hash map + * + * Note: Hashes are never scoped - scoping is at the Redis map level, not the hash level. * * Migration behavior: * - On read: If legacy key found, copy to new key, delete legacy key @@ -51,35 +53,6 @@ test.beforeEach(() => { // Tests use the mock Redis client automatically (NODE_ENV=test) }); -// ============================================================================= -// getScopedHashKey tests -// ============================================================================= - -test("getScopedHashKey - returns hash when no contextId", (t) => { - const hash = "abc123"; - const result = getScopedHashKey(hash); - t.is(result, "abc123"); -}); - -test("getScopedHashKey - returns hash when contextId is null", (t) => { - const hash = "abc123"; - const result = getScopedHashKey(hash, null); - t.is(result, "abc123"); -}); - -test("getScopedHashKey - returns context-scoped key when contextId provided", (t) => { - const hash = "abc123"; - const contextId = "user-456"; - const result = getScopedHashKey(hash, contextId); - t.is(result, "abc123:ctx:user-456"); -}); - -test("getScopedHashKey - handles empty hash", (t) => { - t.is(getScopedHashKey(""), ""); - t.is(getScopedHashKey(null), null); - t.is(getScopedHashKey(undefined), undefined); -}); - // ============================================================================= // Legacy key migration on READ // ============================================================================= @@ -146,15 +119,14 @@ test("getFileStoreMap - does not migrate when unscoped key already exists", asyn test("getFileStoreMap - context-scoped key does NOT fall back to unscoped hash (security)", async (t) => { const hash = `test-ctx-no-fallback-${uuidv4()}`; const contextId = "user-123"; - const contextKey = `${hash}:ctx:${contextId}`; const unscopedData = { url: "http://unscoped.com/file.txt", filename: "unscoped.txt" }; // Only set unscoped key (no context-scoped key) await client.hset("FileStoreMap", hash, JSON.stringify(unscopedData)); - // Read using context-scoped key - should NOT fall back for security - const result = await getFileStoreMap(contextKey, true); + // Read using contextId - should NOT fall back for security + const result = await getFileStoreMap(hash, true, contextId); // Should NOT return unscoped data (security isolation) t.is(result, null, "Should NOT fall back to unscoped data for security"); @@ -169,7 +141,6 @@ test("getFileStoreMap - context-scoped key does NOT fall back to unscoped hash ( test("getFileStoreMap - context-scoped key does NOT fall back through unscoped to legacy (security)", async (t) => { const hash = `test-ctx-legacy-no-fallback-${uuidv4()}`; const contextId = "user-456"; - const contextKey = `${hash}:ctx:${contextId}`; const containerName = getDefaultContainerName(); const legacyKey = `${hash}:${containerName}`; @@ -178,8 +149,8 @@ test("getFileStoreMap - context-scoped key does NOT fall back through unscoped t // Only set legacy key (no context-scoped or unscoped keys) await setLegacyKey(hash, containerName, legacyData); - // Read using context-scoped key - should NOT fall back for security - const result = await getFileStoreMap(contextKey, true); + // Read using contextId - should NOT fall back for security + const result = await getFileStoreMap(hash, true, contextId); // Should NOT return legacy data (security isolation) t.is(result, null, "Should NOT fall back to legacy data for security"); @@ -215,21 +186,22 @@ test("setFileStoreMap - writes to the key provided (unscoped)", async (t) => { test("setFileStoreMap - writes to context-scoped key when provided", async (t) => { const hash = `test-write-ctx-${uuidv4()}`; const contextId = "user-789"; - const contextKey = getScopedHashKey(hash, contextId); const testData = { url: "http://example.com/file.txt", filename: "file.txt" }; - await setFileStoreMap(contextKey, testData); + await setFileStoreMap(hash, testData, contextId); // Verify it was written to the context-scoped key - const result = await getRawKey(contextKey); + const contextMapKey = `FileStoreMap:ctx:${contextId}`; + const result = await client.hget(contextMapKey, hash); t.truthy(result); - t.is(result.url, testData.url); + const parsed = JSON.parse(result); + t.is(parsed.url, testData.url); // Unscoped key should NOT exist t.false(await keyExists(hash), "Unscoped key should not be created"); // Cleanup - await deleteRawKey(contextKey); + await client.hdel(contextMapKey, hash); }); // ============================================================================= @@ -283,23 +255,24 @@ test("removeFromFileStoreMap - deletes legacy key even when unscoped doesn't exi test("removeFromFileStoreMap - handles context-scoped key deletion", async (t) => { const hash = `test-delete-ctx-${uuidv4()}`; const contextId = "user-delete"; - const contextKey = `${hash}:ctx:${contextId}`; const containerName = getDefaultContainerName(); const legacyKey = `${hash}:${containerName}`; const testData = { url: "http://example.com/file.txt", filename: "file.txt" }; // Set up context-scoped key and legacy key - await client.hset("FileStoreMap", contextKey, JSON.stringify(testData)); + await setFileStoreMap(hash, testData, contextId); await setLegacyKey(hash, containerName, testData); - // Delete using context-scoped key - await removeFromFileStoreMap(contextKey); + // Delete using hash and contextId + await removeFromFileStoreMap(hash, contextId); // Context key should be deleted - t.false(await keyExists(contextKey), "Context-scoped key should be deleted"); + const contextMapKey = `FileStoreMap:ctx:${contextId}`; + const contextResult = await client.hget(contextMapKey, hash); + t.is(contextResult, null, "Context-scoped key should be deleted"); - // Legacy key should also be deleted (cleanup based on base hash) + // Legacy key should also be deleted (cleanup based on hash) t.false(await keyExists(legacyKey), "Legacy key should also be deleted"); }); @@ -347,20 +320,19 @@ test("migration - preserves all original data fields", async (t) => { }); test("migration - does not affect keys with colons in hash", async (t) => { - // Keys that already contain colons (like context-scoped keys) should not - // trigger legacy migration logic - const contextKey = `somehash:ctx:user123`; + // Hashes with colons should not trigger legacy migration logic + const hash = `somehash:with:colons`; const testData = { url: "http://example.com/file.txt", filename: "file.txt" }; - await client.hset("FileStoreMap", contextKey, JSON.stringify(testData)); + await client.hset("FileStoreMap", hash, JSON.stringify(testData)); // Reading should just return the data without trying legacy migration - const result = await getFileStoreMap(contextKey, true); + const result = await getFileStoreMap(hash, true); t.truthy(result); t.is(result.url, testData.url); // Cleanup - await deleteRawKey(contextKey); + await deleteRawKey(hash); }); // ============================================================================= @@ -370,7 +342,6 @@ test("migration - does not affect keys with colons in hash", async (t) => { test("getFileStoreMap - context-scoped file cannot be accessed without contextId", async (t) => { const hash = `test-security-${uuidv4()}`; const contextId = "user-secure"; - const contextKey = `${hash}:ctx:${contextId}`; const testData = { url: "http://example.com/secure-file.txt", filename: "secure-file.txt", @@ -378,30 +349,30 @@ test("getFileStoreMap - context-scoped file cannot be accessed without contextId }; // Write file with contextId - await setFileStoreMap(contextKey, testData); + await setFileStoreMap(hash, testData, contextId); // Verify context-scoped key exists - t.true(await keyExists(contextKey), "Context-scoped key should exist"); + const contextMapKey = `FileStoreMap:ctx:${contextId}`; + const exists = await client.hget(contextMapKey, hash); + t.truthy(exists, "Context-scoped key should exist"); // Try to read WITHOUT contextId - should NOT find it const unscopedResult = await getFileStoreMap(hash, true); t.is(unscopedResult, null, "Should NOT be able to read context-scoped file without contextId"); // Try to read WITH correct contextId - should find it - const scopedResult = await getFileStoreMap(contextKey, true); + const scopedResult = await getFileStoreMap(hash, true, contextId); t.truthy(scopedResult, "Should be able to read with correct contextId"); t.is(scopedResult.url, testData.url); // Cleanup - await deleteRawKey(contextKey); + await client.hdel(contextMapKey, hash); }); test("getFileStoreMap - context-scoped file cannot be accessed with wrong contextId", async (t) => { const hash = `test-security-wrong-${uuidv4()}`; const correctContextId = "user-correct"; const wrongContextId = "user-wrong"; - const correctKey = `${hash}:ctx:${correctContextId}`; - const wrongKey = `${hash}:ctx:${wrongContextId}`; const testData = { url: "http://example.com/secure-file.txt", filename: "secure-file.txt", @@ -409,24 +380,23 @@ test("getFileStoreMap - context-scoped file cannot be accessed with wrong contex }; // Write file with correct contextId - await setFileStoreMap(correctKey, testData); + await setFileStoreMap(hash, testData, correctContextId); // Try to read with wrong contextId - should NOT find it - const wrongResult = await getFileStoreMap(wrongKey, true); + const wrongResult = await getFileStoreMap(hash, true, wrongContextId); t.is(wrongResult, null, "Should NOT be able to read with wrong contextId"); // Verify correct contextId still works - const correctResult = await getFileStoreMap(correctKey, true); + const correctResult = await getFileStoreMap(hash, true, correctContextId); t.truthy(correctResult, "Should still be able to read with correct contextId"); // Cleanup - await deleteRawKey(correctKey); + await removeFromFileStoreMap(hash, correctContextId); }); test("removeFromFileStoreMap - context-scoped file cannot be deleted without contextId", async (t) => { const hash = `test-security-delete-${uuidv4()}`; const contextId = "user-delete-secure"; - const contextKey = `${hash}:ctx:${contextId}`; const testData = { url: "http://example.com/secure-file.txt", filename: "secure-file.txt", @@ -434,16 +404,20 @@ test("removeFromFileStoreMap - context-scoped file cannot be deleted without con }; // Write file with contextId - await setFileStoreMap(contextKey, testData); - t.true(await keyExists(contextKey), "Context-scoped key should exist"); + await setFileStoreMap(hash, testData, contextId); + const contextMapKey = `FileStoreMap:ctx:${contextId}`; + const exists = await client.hget(contextMapKey, hash); + t.truthy(exists, "Context-scoped key should exist"); // Try to delete WITHOUT contextId - should NOT delete context-scoped file await removeFromFileStoreMap(hash); - t.true(await keyExists(contextKey), "Context-scoped key should still exist after unscoped delete attempt"); + const stillExists = await client.hget(contextMapKey, hash); + t.truthy(stillExists, "Context-scoped key should still exist after unscoped delete attempt"); // Delete WITH correct contextId - should work - await removeFromFileStoreMap(contextKey); - t.false(await keyExists(contextKey), "Context-scoped key should be deleted with correct contextId"); + await removeFromFileStoreMap(hash, contextId); + const deleted = await client.hget(contextMapKey, hash); + t.is(deleted, null, "Context-scoped key should be deleted with correct contextId"); }); test("getFileStoreMap - unscoped file can be read without contextId", async (t) => { @@ -495,7 +469,6 @@ test("getFileStoreMap - unscoped file can fall back to legacy container-scoped k test("getFileStoreMap - context-scoped read does NOT fall back to unscoped or legacy", async (t) => { const hash = `test-no-fallback-${uuidv4()}`; const contextId = "user-no-fallback"; - const contextKey = `${hash}:ctx:${contextId}`; const containerName = getDefaultContainerName(); const legacyKey = `${hash}:${containerName}`; const unscopedData = { url: "http://example.com/unscoped.txt", filename: "unscoped.txt" }; @@ -506,7 +479,7 @@ test("getFileStoreMap - context-scoped read does NOT fall back to unscoped or le await setLegacyKey(hash, containerName, legacyData); // Try to read with contextId - should NOT find unscoped or legacy - const result = await getFileStoreMap(contextKey, true); + const result = await getFileStoreMap(hash, true, contextId); t.is(result, null, "Context-scoped read should NOT fall back to unscoped or legacy keys"); // Verify unscoped and legacy keys still exist diff --git a/helper-apps/cortex-file-handler/tests/setRetention.test.js b/helper-apps/cortex-file-handler/tests/setRetention.test.js index 356017e0..e513f1b5 100644 --- a/helper-apps/cortex-file-handler/tests/setRetention.test.js +++ b/helper-apps/cortex-file-handler/tests/setRetention.test.js @@ -14,8 +14,7 @@ import { } from "./testUtils.helper.js"; import { getFileStoreMap, - removeFromFileStoreMap, - getScopedHashKey + removeFromFileStoreMap } from "../src/redis.js"; const __filename = fileURLToPath(import.meta.url); diff --git a/server/executeWorkspace.js b/server/executeWorkspace.js index adcad20e..0a09a1a3 100644 --- a/server/executeWorkspace.js +++ b/server/executeWorkspace.js @@ -87,17 +87,14 @@ const executePathwayWithFallback = async (pathway, pathwayArgs, contextValue, in if (cortexPathwayName) { // Use the specific cortex pathway // Transform parameters for cortex pathway + // Spread all pathway args first (including contextId, contextKey, etc.), then override specific fields const cortexArgs = { + ...pathwayArgs, // Spread all pathway args (including contextId, contextKey, etc.) model: pathway.model || pathwayArgs.model || "labeeb-agent", // Use pathway model or default - chatHistory: [], - systemPrompt: pathway.systemPrompt + chatHistory: pathwayArgs.chatHistory ? JSON.parse(JSON.stringify(pathwayArgs.chatHistory)) : [], + systemPrompt: pathway.systemPrompt || pathwayArgs.systemPrompt }; - // If we have existing chatHistory, use it as base - if (pathwayArgs.chatHistory && pathwayArgs.chatHistory.length > 0) { - cortexArgs.chatHistory = JSON.parse(JSON.stringify(pathwayArgs.chatHistory)); - } - // If we have text parameter, we need to add it to the chatHistory if (pathwayArgs.text) { // Find the last user message or create a new one