@aj-archipelago/cortex 1.3.59 → 1.3.60

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.
@@ -12,7 +12,21 @@
12
12
  "requestsPerSecond": 10,
13
13
  "maxTokenLength": 2000
14
14
  },
15
-
15
+ "azure-bing-agent": {
16
+ "type": "AZURE-FOUNDRY-AGENTS",
17
+ "url": "https://archipelago-foundry-resource.services.ai.azure.com/api/projects/archipelago-foundry",
18
+ "agentId": "asst_assistantid",
19
+ "headers": {
20
+ "Content-Type": "application/json"
21
+ },
22
+ "params": {
23
+ "api-version": "2025-05-01"
24
+ },
25
+ "requestsPerSecond": 10,
26
+ "maxTokenLength": 32768,
27
+ "maxReturnTokens": 4096,
28
+ "supportsStreaming": false
29
+ },
16
30
  "gemini-pro-chat": {
17
31
  "type": "GEMINI-CHAT",
18
32
  "url": "https://us-central1-aiplatform.googleapis.com/v1/projects/project-id/locations/us-central1/publishers/google/models/gemini-pro:streamGenerateContent",
package/config.js CHANGED
@@ -4,6 +4,7 @@ import HandleBars from './lib/handleBars.js';
4
4
  import fs from 'fs';
5
5
  import { fileURLToPath, pathToFileURL } from 'url';
6
6
  import GcpAuthTokenHelper from './lib/gcpAuthTokenHelper.js';
7
+ import AzureAuthTokenHelper from './lib/azureAuthTokenHelper.js';
7
8
  import logger from './lib/logger.js';
8
9
  import PathwayManager from './lib/pathwayManager.js';
9
10
  import { readdir } from 'fs/promises';
@@ -128,6 +129,12 @@ var config = convict({
128
129
  env: 'GCP_SERVICE_ACCOUNT_KEY',
129
130
  sensitive: true
130
131
  },
132
+ azureServicePrincipalCredentials: {
133
+ format: String,
134
+ default: null,
135
+ env: 'AZURE_SERVICE_PRINCIPAL_CREDENTIALS',
136
+ sensitive: true
137
+ },
131
138
  models: {
132
139
  format: Object,
133
140
  default: {
@@ -183,6 +190,36 @@ var config = convict({
183
190
  },
184
191
  "maxTokenLength": 8192,
185
192
  },
193
+ "oai-gpt5": {
194
+ "type": "OPENAI-REASONING-VISION",
195
+ "url": "https://api.openai.com/v1/chat/completions",
196
+ "headers": {
197
+ "Authorization": "Bearer {{OPENAI_API_KEY}}",
198
+ "Content-Type": "application/json"
199
+ },
200
+ "params": {
201
+ "model": "gpt-5"
202
+ },
203
+ "requestsPerSecond": 50,
204
+ "maxTokenLength": 1000000,
205
+ "maxReturnTokens": 16384,
206
+ "supportsStreaming": true
207
+ },
208
+ "oai-gpt5-mini": {
209
+ "type": "OPENAI-REASONING-VISION",
210
+ "url": "https://api.openai.com/v1/chat/completions",
211
+ "headers": {
212
+ "Authorization": "Bearer {{OPENAI_API_KEY}}",
213
+ "Content-Type": "application/json"
214
+ },
215
+ "params": {
216
+ "model": "gpt-5-mini"
217
+ },
218
+ "requestsPerSecond": 50,
219
+ "maxTokenLength": 1000000,
220
+ "maxReturnTokens": 16384,
221
+ "supportsStreaming": true
222
+ },
186
223
  "oai-gpt4o": {
187
224
  "type": "OPENAI-VISION",
188
225
  "url": "https://api.openai.com/v1/chat/completions",
@@ -614,6 +651,11 @@ if (config.get('gcpServiceAccountKey')) {
614
651
  config.set('gcpAuthTokenHelper', gcpAuthTokenHelper);
615
652
  }
616
653
 
654
+ if (config.get('azureServicePrincipalCredentials')) {
655
+ const azureAuthTokenHelper = new AzureAuthTokenHelper(config.getProperties());
656
+ config.set('azureAuthTokenHelper', azureAuthTokenHelper);
657
+ }
658
+
617
659
  // Load dynamic pathways from JSON file or cloud storage
618
660
  const createDynamicPathwayManager = async (config, basePathway) => {
619
661
  const { dynamicPathwayConfig } = config.getProperties();
@@ -0,0 +1,78 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ class AzureAuthTokenHelper {
4
+ constructor(config) {
5
+ // Parse Azure credentials from config
6
+ const azureCredentials = config.azureServicePrincipalCredentials ? JSON.parse(config.azureServicePrincipalCredentials) : null;
7
+
8
+ if (!azureCredentials) {
9
+ throw new Error('AZURE_SERVICE_PRINCIPAL_CREDENTIALS is missing or undefined');
10
+ }
11
+
12
+ // Extract required fields
13
+ this.tenantId = azureCredentials.tenant_id || azureCredentials.tenantId;
14
+ this.clientId = azureCredentials.client_id || azureCredentials.clientId;
15
+ this.clientSecret = azureCredentials.client_secret || azureCredentials.clientSecret;
16
+ this.scope = azureCredentials.scope || 'https://ai.azure.com/.default';
17
+
18
+ if (!this.tenantId || !this.clientId || !this.clientSecret) {
19
+ throw new Error('Azure credentials must include tenant_id, client_id, and client_secret');
20
+ }
21
+
22
+ this.token = null;
23
+ this.expiry = null;
24
+ this.tokenUrl = `https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/token`;
25
+ }
26
+
27
+ async getAccessToken() {
28
+ if (!this.token || !this.isTokenValid()) {
29
+ await this.refreshToken();
30
+ }
31
+ return this.token;
32
+ }
33
+
34
+ isTokenValid() {
35
+ // Check if token is still valid with a 5-minute buffer
36
+ return !!(this.expiry && Date.now() < this.expiry.getTime() - 5 * 60 * 1000);
37
+ }
38
+
39
+ async refreshToken() {
40
+ try {
41
+ const formData = new URLSearchParams();
42
+ formData.append('client_id', this.clientId);
43
+ formData.append('client_secret', this.clientSecret);
44
+ formData.append('scope', this.scope);
45
+ formData.append('grant_type', 'client_credentials');
46
+
47
+ const response = await fetch(this.tokenUrl, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/x-www-form-urlencoded',
51
+ },
52
+ body: formData,
53
+ });
54
+
55
+ if (!response.ok) {
56
+ const errorText = await response.text();
57
+ throw new Error(`Azure token request failed: ${response.status} ${response.statusText} - ${errorText}`);
58
+ }
59
+
60
+ const tokenData = await response.json();
61
+
62
+ if (!tokenData.access_token) {
63
+ throw new Error('Azure token response missing access_token');
64
+ }
65
+
66
+ this.token = tokenData.access_token;
67
+
68
+ // Calculate expiry time (expires_in is in seconds)
69
+ const expiresInMs = (tokenData.expires_in || 3600) * 1000;
70
+ this.expiry = new Date(Date.now() + expiresInMs);
71
+
72
+ } catch (error) {
73
+ throw new Error(`Failed to refresh Azure token: ${error.message}`);
74
+ }
75
+ }
76
+ }
77
+
78
+ export default AzureAuthTokenHelper;
@@ -28,11 +28,12 @@ Your responses should be in {{language}} unless the user has expressed another p
28
28
 
29
29
  AI_TOOLS: `# Tool Instructions
30
30
 
31
- You have an extensive toolkit. Each time you call tool(s) you will get the result(s), evaluate, decide what's next, and chain as many steps as needed. Always honor user requests to use specific tools.
31
+ You have an extensive toolkit. Each time you call tool(s) you will get the result(s), evaluate, decide what's next, and chain as many steps as needed. Always honor user requests to use specific tools. You must always search if you are being asked questions about current events, news, fact-checking, or information requiring citation. Your search tools work best when called in parallel to save time so if you know you will need multiple searches, call the search tool(s) in parallel.
32
32
 
33
33
  1. Search deeply & verify rigorously:
34
- - Start broad and consult multiple sources, running searches in parallel where possible.
34
+ - Start broad and consult multiple sources, running all searches in parallel to save time.
35
35
  - Consult all available sources and cross-reference with specific searches before responding.
36
+ - If a tool fails or has a technical difficulty, try the backup tool automatically before giving up or reporting the error.
36
37
 
37
38
  2. Plan & sequence before acting:
38
39
  - Review the toolset first.
@@ -61,7 +62,7 @@ When searching for news, you must complete the following steps:
61
62
 
62
63
  1. Triangulate
63
64
  - Run multiple, parallel queries across all applicable sources.
64
- - Request about double the number of results you want to share, then select the best results.
65
+ - Request at least double the number of results you want to share, then select the best results.
65
66
  - Confirm that multiple sources tell the same story.
66
67
 
67
68
  2. Check Freshness
@@ -102,7 +103,7 @@ token1 | token2 (OR operator - either token may appear (also the default
102
103
 
103
104
  # AI Search Syntax
104
105
 
105
- When creating a query string for your non-Bing search tools, you can use the following AI Search syntax. Important: these tools do not support AND, OR, or NOT strings as operators - you MUST use the syntax below. E.g. you cannot use "term1 AND term2", you must use "term1 + term2".
106
+ When creating a query string for your non-Bing, index-based search tools, you can use the following AI Search syntax. Important: these tools do not support AND, OR, or NOT strings as operators - you MUST use the syntax below. E.g. you cannot use "term1 AND term2", you must use "term1 + term2".
106
107
 
107
108
  token1 + token2 (AND operator - both tokens must appear)
108
109
  token1 | token2 (OR operator - either token may appear (also the default if no operator is specified))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.59",
3
+ "version": "1.3.60",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -0,0 +1,13 @@
1
+ // bing_afagent.js
2
+ // Web search tool
3
+
4
+ export default {
5
+ inputParameters: {
6
+ text: ``,
7
+ },
8
+ timeout: 400,
9
+ enableDuplicateRequests: false,
10
+ model: 'azure-bing-agent',
11
+ useInputChunking: false
12
+ };
13
+
@@ -17,4 +17,8 @@ export default {
17
17
  useInputChunking: false,
18
18
  enableDuplicateRequests: false,
19
19
  timeout: 600,
20
+ geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
21
+ {category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
22
+ {category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
23
+ {category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
20
24
  }
@@ -61,7 +61,7 @@ export default {
61
61
  const errorMessages = Array.isArray(resolver.errors)
62
62
  ? resolver.errors.map(err => err.message || err)
63
63
  : [resolver.errors.message || resolver.errors];
64
- return JSON.stringify({ _type: "SearchError", value: errorMessages });
64
+ return JSON.stringify({ _type: "SearchError", value: errorMessages, recoveryMessage: "This tool failed. You should try the backup tool for this function." });
65
65
  }
66
66
 
67
67
  const parsedResponse = JSON.parse(response);
@@ -0,0 +1,141 @@
1
+ // sys_tool_bing_search_afagent.js
2
+ // Tool pathway that handles Bing web search functionality with minimal parsing
3
+ import { callPathway } from '../../../../lib/pathwayTools.js';
4
+ import logger from '../../../../lib/logger.js';
5
+ import { config } from '../../../../config.js';
6
+ import { getSearchResultId } from '../../../../lib/util.js';
7
+
8
+ export default {
9
+ prompt: [],
10
+ timeout: 300,
11
+ toolDefinition: {
12
+ type: "function",
13
+ icon: "🕸️",
14
+ function: {
15
+ name: "SearchInternetBackup",
16
+ description: "This tool allows you to search sources on the internet by calling another agent that has Bing search access. Use this for current events, news, fact-checking, and information requiring citation. This is a backup tool for when the other internet search tools fail - it is slower so try to use the other tools first and always call this tool in parallel if you have several searches to do.",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ text: {
21
+ type: "string",
22
+ description: "The complete natural language prompt describing what you want to search for. This is going to an AI agent that has Bing search access - you can be as detailed or general as you want."
23
+ },
24
+ userMessage: {
25
+ type: "string",
26
+ description: "A user-friendly message that describes what you're doing with this tool"
27
+ }
28
+ },
29
+ required: ["text", "userMessage"]
30
+ }
31
+ }
32
+ },
33
+
34
+ executePathway: async ({args, runAllPrompts, resolver}) => {
35
+
36
+ // Check if Bing API key is available
37
+ const servicePricipalAvailable = !!config.getEnv()["AZURE_SERVICE_PRINCIPAL_CREDENTIALS"];
38
+ if (!servicePricipalAvailable) {
39
+ throw new Error("Service Principal for Bing Search Agent is not available!");
40
+ }
41
+
42
+ try {
43
+ // Call the Bing search pathway
44
+ //remove model from args as bing_afagent has model in its own
45
+ const { model, ...restArgs } = args;
46
+ const rawResponse = await callPathway('bing_afagent', {
47
+ ...restArgs,
48
+ }, resolver);
49
+
50
+ // Add error handling for malformed JSON
51
+ let response;
52
+ try {
53
+ response = JSON.parse(rawResponse);
54
+ } catch (parseError) {
55
+ logger.error(`Failed to parse bing_afagent response as JSON: ${parseError.message}`);
56
+ logger.error(`Raw response: ${rawResponse}`);
57
+ throw new Error(`Invalid JSON response from bing_afagent: ${parseError.message}`);
58
+ }
59
+
60
+ if (resolver.errors && resolver.errors.length > 0) {
61
+ const errorMessages = Array.isArray(resolver.errors)
62
+ ? resolver.errors.map(err => err.message || err)
63
+ : [resolver.errors.message || resolver.errors];
64
+ return JSON.stringify({ _type: "SearchError", value: errorMessages });
65
+ }
66
+
67
+ // Transform response to match expected SearchResponse format
68
+ function transformToSearchResponse(response) {
69
+ let valueText = response.value || '';
70
+ const annotations = response.annotations || [];
71
+
72
+ // Create a mapping from citation text to search result IDs
73
+ const citationToIdMap = new Map();
74
+ const citationPattern = /【\d+:\d+†source】/g;
75
+
76
+ // Replace citation markers with search result IDs
77
+ valueText = valueText.replace(citationPattern, (match) => {
78
+ if (!citationToIdMap.has(match)) {
79
+ citationToIdMap.set(match, getSearchResultId());
80
+ }
81
+ return `:cd_source[${citationToIdMap.get(match)}]`;
82
+ });
83
+
84
+ // Transform annotations to search result objects
85
+ const searchResults = annotations.map(annotation => {
86
+ if (annotation.type === "url_citation" && annotation.url_citation) {
87
+ const citationText = annotation.text;
88
+ const searchResultId = citationToIdMap.get(citationText) || getSearchResultId();
89
+
90
+ return {
91
+ searchResultId: searchResultId,
92
+ title: annotation.url_citation.title || '',
93
+ url: annotation.url_citation.url || '',
94
+ content: annotation.url_citation.title || annotation.url_citation.url || '', // Individual result content
95
+ path: '',
96
+ wireid: '',
97
+ source: '',
98
+ slugline: '',
99
+ date: ''
100
+ };
101
+ }
102
+ return null;
103
+ }).filter(result => result !== null);
104
+
105
+ // If no annotations, create a single search result with the content
106
+ if (searchResults.length === 0) {
107
+ searchResults.push({
108
+ searchResultId: getSearchResultId(),
109
+ title: '',
110
+ url: '',
111
+ content: valueText, // Use the full transformed text as content
112
+ path: '',
113
+ wireid: '',
114
+ source: '',
115
+ slugline: '',
116
+ date: ''
117
+ });
118
+ }
119
+
120
+ return {
121
+ transformedText: valueText, // The full text with citations replaced
122
+ searchResults: searchResults // Individual search results for citation extraction
123
+ };
124
+ }
125
+
126
+ const transformedData = transformToSearchResponse(response);
127
+
128
+ resolver.tool = JSON.stringify({ toolUsed: "SearchInternetAgent2" });
129
+
130
+ // Return the full transformed text as the main result, and include search results for citation extraction
131
+ return JSON.stringify({
132
+ _type: "SearchResponse",
133
+ value: transformedData.searchResults,
134
+ text: transformedData.transformedText // The full transformed text with citations
135
+ });
136
+ } catch (e) {
137
+ logger.error(`Error in Bing search: ${e}`);
138
+ throw e;
139
+ }
140
+ }
141
+ };
@@ -12,7 +12,7 @@ export default {
12
12
  icon: "🌎",
13
13
  function: {
14
14
  name: "FetchWebPageContentJina",
15
- description: "This tool allows you to fetch and extract the text content from any webpage using the Jina API. This is a great fallback for web page content if you don't get a good enough response from your other browser tool.",
15
+ description: "This tool allows you to fetch and extract the text content from any webpage using the Jina API. This is a great backup tool for web page content if you don't get a good enough response from your other browser tool or are blocked by a website.",
16
16
  parameters: {
17
17
  type: "object",
18
18
  properties: {
@@ -22,6 +22,10 @@ export default {
22
22
  useInputChunking: false,
23
23
  enableDuplicateRequests: false,
24
24
  timeout: 600,
25
+ geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
26
+ {category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
27
+ {category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
28
+ {category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
25
29
  toolDefinition: [{
26
30
  type: "function",
27
31
  icon: "📄",
@@ -182,6 +182,10 @@ export default {
182
182
  },
183
183
  // model: 'oai-gpt41',
184
184
  model: 'gemini-pro-25-vision',
185
+ geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
186
+ {category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
187
+ {category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
188
+ {category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
185
189
  timeout: 600,
186
190
  stream: true,
187
191
  }
@@ -62,6 +62,10 @@ export default {
62
62
  },
63
63
  timeout: 3600, // in seconds
64
64
  enableDuplicateRequests: false,
65
+ geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
66
+ {category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
67
+ {category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
68
+ {category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
65
69
 
66
70
  executePathway: async ({args, runAllPrompts, resolver}) => {
67
71
  let intervalId;
@@ -33,6 +33,7 @@ import ApptekTranslatePlugin from './plugins/apptekTranslatePlugin.js';
33
33
  import GoogleTranslatePlugin from './plugins/googleTranslatePlugin.js';
34
34
  import GroqChatPlugin from './plugins/groqChatPlugin.js';
35
35
  import VeoVideoPlugin from './plugins/veoVideoPlugin.js';
36
+ import AzureFoundryAgentsPlugin from './plugins/azureFoundryAgentsPlugin.js';
36
37
 
37
38
  class ModelExecutor {
38
39
  constructor(pathway, model) {
@@ -133,6 +134,9 @@ class ModelExecutor {
133
134
  case 'VEO-VIDEO':
134
135
  plugin = new VeoVideoPlugin(pathway, model);
135
136
  break;
137
+ case 'AZURE-FOUNDRY-AGENTS':
138
+ plugin = new AzureFoundryAgentsPlugin(pathway, model);
139
+ break;
136
140
  default:
137
141
  throw new Error(`Unsupported model type: ${model.type}`);
138
142
  }
@@ -0,0 +1,372 @@
1
+ // AzureFoundryAgentsPlugin.js
2
+ import ModelPlugin from './modelPlugin.js';
3
+ import logger from '../../lib/logger.js';
4
+ import axios from 'axios';
5
+
6
+ class AzureFoundryAgentsPlugin extends ModelPlugin {
7
+ constructor(pathway, model) {
8
+ super(pathway, model);
9
+ this.agentId = model.agentId;
10
+ this.projectUrl = model.url;
11
+ }
12
+
13
+ // Convert to Azure Foundry Agents messages array format
14
+ convertToAzureFoundryMessages(context, examples, messages) {
15
+ let azureMessages = [];
16
+
17
+ // Add context as a system message if provided
18
+ if (context) {
19
+ azureMessages.push({
20
+ role: 'system',
21
+ content: context,
22
+ });
23
+ }
24
+
25
+ // Add examples to the messages array
26
+ if (examples && examples.length > 0) {
27
+ examples.forEach(example => {
28
+ azureMessages.push({
29
+ role: example.input.author || 'user',
30
+ content: example.input.content,
31
+ });
32
+ azureMessages.push({
33
+ role: example.output.author || 'assistant',
34
+ content: example.output.content,
35
+ });
36
+ });
37
+ }
38
+
39
+ // Add remaining messages to the messages array
40
+ messages.forEach(message => {
41
+ azureMessages.push({
42
+ role: message.author,
43
+ content: message.content,
44
+ });
45
+ });
46
+
47
+ return azureMessages;
48
+ }
49
+
50
+ // Set up parameters specific to the Azure Foundry Agents API
51
+ getRequestParameters(text, parameters, prompt) {
52
+ const { modelPromptText, modelPromptMessages, tokenLength, modelPrompt } = this.getCompiledPrompt(text, parameters, prompt);
53
+ const { stream } = parameters;
54
+
55
+ // Define the model's max token length
56
+ const modelTargetTokenLength = this.getModelMaxPromptTokens();
57
+
58
+ let requestMessages = modelPromptMessages || [{ "role": "user", "content": modelPromptText }];
59
+
60
+ // Check if the messages are in Palm format and convert them to Azure format if necessary
61
+ const isPalmFormat = requestMessages.some(message => 'author' in message);
62
+ if (isPalmFormat) {
63
+ const context = modelPrompt.context || '';
64
+ const examples = modelPrompt.examples || [];
65
+ requestMessages = this.convertToAzureFoundryMessages(context, examples, modelPromptMessages);
66
+ }
67
+
68
+ // Check if the token length exceeds the model's max token length
69
+ if (tokenLength > modelTargetTokenLength && this.promptParameters?.manageTokenLength) {
70
+ // Remove older messages until the token length is within the model's limit
71
+ requestMessages = this.truncateMessagesToTargetLength(requestMessages, modelTargetTokenLength);
72
+ }
73
+
74
+ const requestParameters = {
75
+ assistant_id: this.agentId,
76
+ thread: {
77
+ messages: requestMessages
78
+ },
79
+ stream: stream || false,
80
+ // Add any additional parameters that might be needed
81
+ ...(parameters.tools && { tools: parameters.tools }),
82
+ ...(parameters.tool_resources && { tool_resources: parameters.tool_resources }),
83
+ ...(parameters.metadata && { metadata: parameters.metadata }),
84
+ ...(parameters.instructions && { instructions: parameters.instructions }),
85
+ ...(parameters.model && { model: parameters.model }),
86
+ ...(parameters.temperature && { temperature: parameters.temperature }),
87
+ ...(parameters.max_tokens && { max_tokens: parameters.max_tokens }),
88
+ ...(parameters.top_p && { top_p: parameters.top_p }),
89
+ ...(parameters.tool_choice && { tool_choice: parameters.tool_choice }),
90
+ ...(parameters.response_format && { response_format: parameters.response_format }),
91
+ ...(parameters.parallel_tool_calls && { parallel_tool_calls: parameters.parallel_tool_calls }),
92
+ ...(parameters.truncation_strategy && { truncation_strategy: parameters.truncation_strategy })
93
+ };
94
+
95
+ return requestParameters;
96
+ }
97
+
98
+ // Assemble and execute the request to the Azure Foundry Agents API
99
+ async execute(text, parameters, prompt, cortexRequest) {
100
+ const requestParameters = this.getRequestParameters(text, parameters, prompt);
101
+
102
+ // Set up the request for Azure Foundry Agents
103
+ cortexRequest.url = this.requestUrl();
104
+ cortexRequest.data = requestParameters;
105
+ cortexRequest.params = { 'api-version': '2025-05-01' }; // Azure API version
106
+
107
+ // Get authentication token and add to headers
108
+ const azureAuthTokenHelper = this.config.get('azureAuthTokenHelper');
109
+ let authToken = null;
110
+ if (azureAuthTokenHelper) {
111
+ try {
112
+ authToken = await azureAuthTokenHelper.getAccessToken();
113
+ } catch (error) {
114
+ logger.warn(`[Azure Foundry Agent] Failed to get auth token: ${error.message}`);
115
+ // Continue without auth token
116
+ }
117
+ }
118
+
119
+ cortexRequest.headers = {
120
+ 'Content-Type': 'application/json',
121
+ ...cortexRequest.headers,
122
+ ...(authToken && { 'Authorization': `Bearer ${authToken}` })
123
+ };
124
+
125
+ // Execute the initial request to create the run
126
+ const runResponse = await this.executeRequest(cortexRequest);
127
+
128
+ // If we got a run response, poll for completion and get messages
129
+ if (runResponse && runResponse.id && runResponse.thread_id) {
130
+ return await this.pollForCompletion(runResponse.thread_id, runResponse.id, cortexRequest);
131
+ }
132
+
133
+ return runResponse;
134
+ }
135
+
136
+ // Poll for run completion and retrieve messages
137
+ async pollForCompletion(threadId, runId, cortexRequest) {
138
+ const maxPollingAttempts = 60; // 60 seconds max
139
+ const pollingInterval = 1000; // 1 second
140
+ let attempts = 0;
141
+
142
+ while (attempts < maxPollingAttempts) {
143
+ attempts++;
144
+
145
+ // Wait before polling
146
+ await new Promise(resolve => setTimeout(resolve, pollingInterval));
147
+
148
+ try {
149
+ // Add authentication token if available
150
+ const azureAuthTokenHelper = this.config.get('azureAuthTokenHelper');
151
+ let authToken = null;
152
+ if (azureAuthTokenHelper) {
153
+ try {
154
+ authToken = await azureAuthTokenHelper.getAccessToken();
155
+ } catch (error) {
156
+ logger.warn(`[Azure Foundry Agent] Failed to get auth token for polling: ${error.message}`);
157
+ // Continue without auth token
158
+ }
159
+ }
160
+
161
+ const pollUrl = this.constructAzureUrl(`/threads/${threadId}/runs/${runId}`);
162
+ const pollResponse = await axios.get(pollUrl, {
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ ...this.model.headers,
166
+ ...(authToken && { 'Authorization': `Bearer ${authToken}` })
167
+ },
168
+ params: { 'api-version': '2025-05-01' }
169
+ });
170
+ const runStatus = pollResponse?.data;
171
+
172
+ if (!runStatus) {
173
+ logger.warn(`[Azure Foundry Agent] No run status received for run: ${runId}`);
174
+ continue;
175
+ }
176
+
177
+ // Check if run is completed
178
+ if (runStatus.status === 'completed') {
179
+ logger.info(`[Azure Foundry Agent] Run completed successfully: ${runId}`);
180
+ return await this.retrieveMessages(threadId, cortexRequest);
181
+ }
182
+
183
+ // Check if run failed
184
+ if (runStatus.status === 'failed') {
185
+ logger.error(`[Azure Foundry Agent] Run failed: ${runId}`, runStatus.lastError);
186
+ return null;
187
+ }
188
+
189
+ // Check if run was cancelled
190
+ if (runStatus.status === 'cancelled') {
191
+ logger.warn(`[Azure Foundry Agent] Run was cancelled: ${runId}`);
192
+ return null;
193
+ }
194
+
195
+ // Continue polling for queued or in_progress status
196
+ if (runStatus.status === 'queued' || runStatus.status === 'in_progress') {
197
+ continue;
198
+ }
199
+
200
+ // Unknown status
201
+ logger.warn(`[Azure Foundry Agent] Unknown run status: ${runStatus.status}`);
202
+ break;
203
+
204
+ } catch (error) {
205
+ logger.error(`[Azure Foundry Agent] Error polling run status: ${error.message}`);
206
+ break;
207
+ }
208
+ }
209
+
210
+ logger.error(`[Azure Foundry Agent] Polling timeout after ${maxPollingAttempts} attempts for run: ${runId}`);
211
+ return null;
212
+ }
213
+
214
+ // Retrieve messages from the completed thread
215
+ async retrieveMessages(threadId, cortexRequest) {
216
+ try {
217
+ // Add authentication token if available
218
+ const azureAuthTokenHelper = this.config.get('azureAuthTokenHelper');
219
+ let authToken = null;
220
+ if (azureAuthTokenHelper) {
221
+ try {
222
+ authToken = await azureAuthTokenHelper.getAccessToken();
223
+ } catch (error) {
224
+ logger.warn(`[Azure Foundry Agent] Failed to get auth token for messages: ${error.message}`);
225
+ // Continue without auth token
226
+ }
227
+ }
228
+
229
+ const messagesUrl = this.constructAzureUrl(`/threads/${threadId}/messages`);
230
+ const axiosResponse = await axios.get(messagesUrl, {
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ ...this.model.headers,
234
+ ...(authToken && { 'Authorization': `Bearer ${authToken}` })
235
+ },
236
+ params: { 'api-version': '2025-05-01', order: 'asc' }
237
+ });
238
+ const messagesResponse = axiosResponse?.data;
239
+
240
+ if (!messagesResponse || !messagesResponse.data) {
241
+ logger.warn(`[Azure Foundry Agent] No messages received from thread: ${threadId}`);
242
+ return null;
243
+ }
244
+
245
+ // Find the last assistant message
246
+ const messages = messagesResponse.data;
247
+ for (let i = messages.length - 1; i >= 0; i--) {
248
+ const message = messages[i];
249
+ if (message.role === 'assistant' && message.content && Array.isArray(message.content)) {
250
+ const textContent = message.content.find(c => c.type === 'text' && c.text && c.text.value);
251
+ if (textContent) {
252
+ return JSON.stringify(textContent.text);
253
+ }
254
+ }
255
+ }
256
+
257
+ logger.warn(`[Azure Foundry Agent] No assistant messages found in thread: ${threadId}`);
258
+ return null;
259
+
260
+ } catch (error) {
261
+ logger.error(`[Azure Foundry Agent] Error retrieving messages: ${error.message}`);
262
+ return null;
263
+ }
264
+ }
265
+
266
+ // Parse the response from the Azure Foundry Agents API
267
+ parseResponse(data) {
268
+ if (!data) return "";
269
+
270
+ // If data is already a string (the final message content), return it
271
+ if (typeof data === 'string') {
272
+ return data;
273
+ }
274
+
275
+ // Handle the run response format (for backward compatibility)
276
+ if (data.id && data.status) {
277
+ // This is a run response, we need to handle the status
278
+ if (data.status === 'completed') {
279
+ // The run completed successfully, but we need to get the messages
280
+ // This would typically be handled by polling for messages
281
+ return data;
282
+ } else if (data.status === 'failed') {
283
+ logger.error(`Azure Foundry Agent run failed: ${data.lastError?.message || data.last_error?.message || 'Unknown error'}`);
284
+ return null;
285
+ } else {
286
+ // Still in progress
287
+ return data;
288
+ }
289
+ }
290
+
291
+ // Handle direct message response
292
+ if (data.messages && Array.isArray(data.messages)) {
293
+ const lastMessage = data.messages[data.messages.length - 1];
294
+ if (lastMessage && lastMessage.content && Array.isArray(lastMessage.content)) {
295
+ const textContent = lastMessage.content.find(c => c.type === 'text');
296
+ if (textContent && textContent.text) {
297
+ // Support both object { value: string } and string shapes
298
+ if (typeof textContent.text === 'string') {
299
+ return textContent.text;
300
+ }
301
+ if (typeof textContent.text.value === 'string') {
302
+ return textContent.text.value;
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ // Fallback to returning the entire response
309
+ return data;
310
+ }
311
+
312
+ // Override the logging function to display the messages and responses
313
+ logRequestData(data, responseData, prompt) {
314
+ const { stream, thread } = data;
315
+
316
+ if (thread && thread.messages && thread.messages.length > 1) {
317
+ logger.info(`[Azure Foundry Agent request sent containing ${thread.messages.length} messages]`);
318
+ let totalLength = 0;
319
+ let totalUnits;
320
+
321
+ thread.messages.forEach((message, index) => {
322
+ const content = message.content === undefined ? JSON.stringify(message) :
323
+ (Array.isArray(message.content) ? message.content.map(item => {
324
+ return JSON.stringify(item);
325
+ }).join(', ') : message.content);
326
+ const { length, units } = this.getLength(content);
327
+ const displayContent = this.shortenContent(content);
328
+
329
+ logger.verbose(`message ${index + 1}: role: ${message.role}, ${units}: ${length}, content: "${displayContent}"`);
330
+ totalLength += length;
331
+ totalUnits = units;
332
+ });
333
+ logger.info(`[Azure Foundry Agent request contained ${totalLength} ${totalUnits}]`);
334
+ } else if (thread && thread.messages && thread.messages.length === 1) {
335
+ const message = thread.messages[0];
336
+ const content = Array.isArray(message.content) ? message.content.map(item => {
337
+ return JSON.stringify(item);
338
+ }).join(', ') : message.content;
339
+ const { length, units } = this.getLength(content);
340
+ logger.info(`[Azure Foundry Agent request sent containing ${length} ${units}]`);
341
+ logger.verbose(`${this.shortenContent(content)}`);
342
+ }
343
+
344
+ if (stream) {
345
+ logger.info(`[Azure Foundry Agent response received as an SSE stream]`);
346
+ } else {
347
+ const responseText = this.parseResponse(responseData);
348
+ if (responseText && typeof responseText === 'string') {
349
+ const { length, units } = this.getLength(responseText);
350
+ logger.info(`[Azure Foundry Agent response received containing ${length} ${units}]`);
351
+ logger.verbose(`${this.shortenContent(responseText)}`);
352
+ } else {
353
+ logger.info(`[Azure Foundry Agent response received: ${JSON.stringify(responseData)}]`);
354
+ }
355
+ }
356
+
357
+ prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
358
+ }
359
+
360
+ // Override the request URL to use the Azure Foundry Agents endpoint
361
+ requestUrl() {
362
+ // The URL should be constructed as: {projectUrl}/threads/runs
363
+ return `${this.projectUrl}/threads/runs`;
364
+ }
365
+
366
+ // Helper method to construct Azure Foundry Agents URLs
367
+ constructAzureUrl(path) {
368
+ return `${this.projectUrl}${path}`;
369
+ }
370
+ }
371
+
372
+ export default AzureFoundryAgentsPlugin;
@@ -124,9 +124,9 @@ class Gemini15ChatPlugin extends ModelPlugin {
124
124
  topP: parameters.topP || 0.95,
125
125
  topK: parameters.topK || 40,
126
126
  },
127
- safety_settings: geminiSafetySettings || undefined,
128
- systemInstruction: system,
129
- tools: geminiTools || undefined
127
+ ...(geminiSafetySettings ? {safety_settings: geminiSafetySettings} : {}),
128
+ ...(system ? {systemInstruction: system} : {}),
129
+ ...(geminiTools ? {tools: geminiTools} : {})
130
130
  };
131
131
 
132
132
  return requestParameters;
@@ -0,0 +1,150 @@
1
+ import test from 'ava';
2
+ import AzureAuthTokenHelper from '../lib/azureAuthTokenHelper.js';
3
+
4
+ test('should initialize with valid credentials', (t) => {
5
+ const mockConfig = {
6
+ azureServicePrincipalCredentials: JSON.stringify({
7
+ tenant_id: '648085d9-6878-44a9-a76b-0223882f8268',
8
+ client_id: '8796f9a6-cd80-45dd-91b8-9ddf384ee42c',
9
+ client_secret: 'test-secret',
10
+ scope: 'https://ai.azure.com/.default'
11
+ })
12
+ };
13
+
14
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
15
+
16
+ t.is(azureHelper.tenantId, '648085d9-6878-44a9-a76b-0223882f8268');
17
+ t.is(azureHelper.clientId, '8796f9a6-cd80-45dd-91b8-9ddf384ee42c');
18
+ t.is(azureHelper.clientSecret, 'test-secret');
19
+ t.is(azureHelper.scope, 'https://ai.azure.com/.default');
20
+ t.is(azureHelper.tokenUrl, 'https://login.microsoftonline.com/648085d9-6878-44a9-a76b-0223882f8268/oauth2/v2.0/token');
21
+ });
22
+
23
+ test('should throw error when azureCredentials is missing', (t) => {
24
+ t.throws(() => {
25
+ new AzureAuthTokenHelper({});
26
+ }, { message: 'AZURE_SERVICE_PRINCIPAL_CREDENTIALS is missing or undefined' });
27
+ });
28
+
29
+ test('should throw error when required fields are missing', (t) => {
30
+ const invalidConfig = {
31
+ azureServicePrincipalCredentials: JSON.stringify({
32
+ tenant_id: 'test-tenant'
33
+ // missing client_id and client_secret
34
+ })
35
+ };
36
+
37
+ t.throws(() => {
38
+ new AzureAuthTokenHelper(invalidConfig);
39
+ }, { message: 'Azure credentials must include tenant_id, client_id, and client_secret' });
40
+ });
41
+
42
+ test('should support both snake_case and camelCase field names', (t) => {
43
+ const camelCaseConfig = {
44
+ azureServicePrincipalCredentials: JSON.stringify({
45
+ tenantId: '648085d9-6878-44a9-a76b-0223882f8268',
46
+ clientId: '8796f9a6-cd80-45dd-91b8-9ddf384ee42c',
47
+ clientSecret: 'test-secret',
48
+ scope: 'https://ai.azure.com/.default'
49
+ })
50
+ };
51
+
52
+ const azureHelper = new AzureAuthTokenHelper(camelCaseConfig);
53
+
54
+ t.is(azureHelper.tenantId, '648085d9-6878-44a9-a76b-0223882f8268');
55
+ t.is(azureHelper.clientId, '8796f9a6-cd80-45dd-91b8-9ddf384ee42c');
56
+ t.is(azureHelper.clientSecret, 'test-secret');
57
+ });
58
+
59
+ test('isTokenValid should return false when no token exists', (t) => {
60
+ const mockConfig = {
61
+ azureServicePrincipalCredentials: JSON.stringify({
62
+ tenant_id: 'test-tenant',
63
+ client_id: 'test-client',
64
+ client_secret: 'test-secret'
65
+ })
66
+ };
67
+
68
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
69
+ t.false(azureHelper.isTokenValid());
70
+ });
71
+
72
+ test('isTokenValid should return false when token is expired', (t) => {
73
+ const mockConfig = {
74
+ azureServicePrincipalCredentials: JSON.stringify({
75
+ tenant_id: 'test-tenant',
76
+ client_id: 'test-client',
77
+ client_secret: 'test-secret'
78
+ })
79
+ };
80
+
81
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
82
+ azureHelper.token = 'test-token';
83
+ azureHelper.expiry = new Date(Date.now() - 1000); // expired 1 second ago
84
+ t.false(azureHelper.isTokenValid());
85
+ });
86
+
87
+ test('isTokenValid should return true when token is valid with buffer', (t) => {
88
+ const mockConfig = {
89
+ azureServicePrincipalCredentials: JSON.stringify({
90
+ tenant_id: 'test-tenant',
91
+ client_id: 'test-client',
92
+ client_secret: 'test-secret'
93
+ })
94
+ };
95
+
96
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
97
+ azureHelper.token = 'test-token';
98
+ azureHelper.expiry = new Date(Date.now() + 10 * 60 * 1000); // expires in 10 minutes
99
+ t.true(azureHelper.isTokenValid());
100
+ });
101
+
102
+ test('isTokenValid should return false when token expires within buffer time', (t) => {
103
+ const mockConfig = {
104
+ azureServicePrincipalCredentials: JSON.stringify({
105
+ tenant_id: 'test-tenant',
106
+ client_id: 'test-client',
107
+ client_secret: 'test-secret'
108
+ })
109
+ };
110
+
111
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
112
+ azureHelper.token = 'test-token';
113
+ azureHelper.expiry = new Date(Date.now() + 3 * 60 * 1000); // expires in 3 minutes (within 5-minute buffer)
114
+ t.false(azureHelper.isTokenValid());
115
+ });
116
+
117
+ test('getAccessToken should return existing token if valid', async (t) => {
118
+ const mockConfig = {
119
+ azureServicePrincipalCredentials: JSON.stringify({
120
+ tenant_id: 'test-tenant',
121
+ client_id: 'test-client',
122
+ client_secret: 'test-secret'
123
+ })
124
+ };
125
+
126
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
127
+ azureHelper.token = 'existing-token';
128
+ azureHelper.expiry = new Date(Date.now() + 10 * 60 * 1000);
129
+
130
+ const token = await azureHelper.getAccessToken();
131
+ t.is(token, 'existing-token');
132
+ });
133
+
134
+ test('getAccessToken should throw error when token is invalid and no network available', async (t) => {
135
+ const mockConfig = {
136
+ azureServicePrincipalCredentials: JSON.stringify({
137
+ tenant_id: 'test-tenant',
138
+ client_id: 'test-client',
139
+ client_secret: 'test-secret'
140
+ })
141
+ };
142
+
143
+ const azureHelper = new AzureAuthTokenHelper(mockConfig);
144
+ // No token set, so it will try to refresh and fail
145
+
146
+ await t.throwsAsync(
147
+ azureHelper.getAccessToken(),
148
+ { message: /Failed to refresh Azure token/ }
149
+ );
150
+ });
@@ -0,0 +1,212 @@
1
+ // azureFoundryAgents.test.js
2
+ import test from 'ava';
3
+ import AzureFoundryAgentsPlugin from '../server/plugins/azureFoundryAgentsPlugin.js';
4
+
5
+ test.beforeEach(t => {
6
+ const mockPathway = {
7
+ name: 'test-pathway',
8
+ temperature: 0.7,
9
+ prompt: {
10
+ context: 'You are a helpful assistant.',
11
+ examples: []
12
+ }
13
+ };
14
+
15
+ const mockModel = {
16
+ name: 'azure-foundry-agents',
17
+ type: 'AZURE-FOUNDRY-AGENTS',
18
+ url: 'https://archipelago-foundry-resource.services.ai.azure.com/api/projects/archipelago-foundry',
19
+ agentId: 'asst_testid',
20
+ headers: {
21
+ 'Content-Type': 'application/json'
22
+ },
23
+ maxTokenLength: 32768,
24
+ maxReturnTokens: 4096,
25
+ supportsStreaming: true
26
+ };
27
+
28
+ t.context.plugin = new AzureFoundryAgentsPlugin(mockPathway, mockModel);
29
+ t.context.mockPathway = mockPathway;
30
+ t.context.mockModel = mockModel;
31
+ });
32
+
33
+ test('should initialize with correct agent ID and project URL', t => {
34
+ const { plugin } = t.context;
35
+ t.is(plugin.agentId, 'asst_testid');
36
+ t.is(plugin.projectUrl, 'https://archipelago-foundry-resource.services.ai.azure.com/api/projects/archipelago-foundry');
37
+ });
38
+
39
+ test('should convert Palm format messages to Azure format', t => {
40
+ const { plugin } = t.context;
41
+ const context = 'You are a helpful assistant.';
42
+ const examples = [
43
+ {
44
+ input: { author: 'user', content: 'Hello' },
45
+ output: { author: 'assistant', content: 'Hi there!' }
46
+ }
47
+ ];
48
+ const messages = [
49
+ { author: 'user', content: 'How are you?' }
50
+ ];
51
+
52
+ const result = plugin.convertToAzureFoundryMessages(context, examples, messages);
53
+
54
+ t.deepEqual(result, [
55
+ { role: 'system', content: 'You are a helpful assistant.' },
56
+ { role: 'user', content: 'Hello' },
57
+ { role: 'assistant', content: 'Hi there!' },
58
+ { role: 'user', content: 'How are you?' }
59
+ ]);
60
+ });
61
+
62
+ test('should handle empty examples and context', t => {
63
+ const { plugin } = t.context;
64
+ const messages = [
65
+ { author: 'user', content: 'Hello' }
66
+ ];
67
+
68
+ const result = plugin.convertToAzureFoundryMessages('', [], messages);
69
+
70
+ t.deepEqual(result, [
71
+ { role: 'user', content: 'Hello' }
72
+ ]);
73
+ });
74
+
75
+ test('should create correct request parameters', t => {
76
+ const { plugin } = t.context;
77
+ const text = 'Hello, can you help me?';
78
+ const parameters = { stream: false };
79
+ const prompt = {
80
+ context: 'You are helpful.',
81
+ examples: [],
82
+ messages: [{ role: 'user', content: text }]
83
+ };
84
+
85
+ const result = plugin.getRequestParameters(text, parameters, prompt);
86
+
87
+ t.is(result.assistant_id, 'asst_testid');
88
+ t.deepEqual(result.thread.messages, [{ role: 'user', content: text }]);
89
+ t.is(result.stream, false);
90
+ });
91
+
92
+ test('should parse completed run response', t => {
93
+ const { plugin } = t.context;
94
+ const mockResponse = {
95
+ id: 'run_123',
96
+ status: 'completed',
97
+ thread_id: 'thread_456'
98
+ };
99
+
100
+ const result = plugin.parseResponse(mockResponse);
101
+ t.deepEqual(result, mockResponse);
102
+ });
103
+
104
+ test('should parse string response (final message content)', t => {
105
+ const { plugin } = t.context;
106
+ const mockResponse = 'Hello! How can I help you?';
107
+
108
+ const result = plugin.parseResponse(mockResponse);
109
+ t.is(result, 'Hello! How can I help you?');
110
+ });
111
+
112
+ test('should handle failed run response', t => {
113
+ const { plugin } = t.context;
114
+ const mockResponse = {
115
+ id: 'run_123',
116
+ status: 'failed',
117
+ lastError: { message: 'Something went wrong' }
118
+ };
119
+
120
+ const result = plugin.parseResponse(mockResponse);
121
+ t.is(result, null);
122
+ });
123
+
124
+ test('should parse message content from response', t => {
125
+ const { plugin } = t.context;
126
+ const mockResponse = {
127
+ messages: [
128
+ {
129
+ role: 'assistant',
130
+ content: [
131
+ {
132
+ type: 'text',
133
+ text: { value: 'Hello! How can I help you?' }
134
+ }
135
+ ]
136
+ }
137
+ ]
138
+ };
139
+
140
+ const result = plugin.parseResponse(mockResponse);
141
+ t.is(result, 'Hello! How can I help you?');
142
+ });
143
+
144
+ test('should return empty string for null response', t => {
145
+ const { plugin } = t.context;
146
+ const result = plugin.parseResponse(null);
147
+ t.is(result, '');
148
+ });
149
+
150
+ test('should return correct Azure Foundry Agents endpoint', t => {
151
+ const { plugin } = t.context;
152
+ const url = plugin.requestUrl();
153
+ t.is(url, 'https://archipelago-foundry-resource.services.ai.azure.com/api/projects/archipelago-foundry/threads/runs');
154
+ });
155
+
156
+ test('should be able to access azureAuthTokenHelper from config', (t) => {
157
+ // Mock config with azureAuthTokenHelper
158
+ const mockConfig = {
159
+ get: (key) => {
160
+ if (key === 'azureAuthTokenHelper') {
161
+ return {
162
+ getAccessToken: async () => 'mock-token'
163
+ };
164
+ }
165
+ return null;
166
+ }
167
+ };
168
+
169
+ // Mock pathway and model
170
+ const mockPathway = {};
171
+ const mockModel = {
172
+ url: 'https://test.azure.com/api/projects/test',
173
+ agentId: 'test-agent-id',
174
+ headers: { 'Content-Type': 'application/json' }
175
+ };
176
+
177
+ // Create plugin instance
178
+ const plugin = new AzureFoundryAgentsPlugin(mockPathway, mockModel);
179
+
180
+ // Mock the config property
181
+ plugin.config = mockConfig;
182
+
183
+ // Test that we can access the auth helper
184
+ const authHelper = plugin.config.get('azureAuthTokenHelper');
185
+ t.truthy(authHelper);
186
+ t.is(typeof authHelper.getAccessToken, 'function');
187
+ });
188
+
189
+ test('should handle missing azureAuthTokenHelper gracefully', (t) => {
190
+ // Mock config without azureAuthTokenHelper
191
+ const mockConfig = {
192
+ get: (key) => null
193
+ };
194
+
195
+ // Mock pathway and model
196
+ const mockPathway = {};
197
+ const mockModel = {
198
+ url: 'https://test.azure.com/api/projects/test',
199
+ agentId: 'test-agent-id',
200
+ headers: { 'Content-Type': 'application/json' }
201
+ };
202
+
203
+ // Create plugin instance
204
+ const plugin = new AzureFoundryAgentsPlugin(mockPathway, mockModel);
205
+
206
+ // Mock the config property
207
+ plugin.config = mockConfig;
208
+
209
+ // Test that we can access the auth helper (should be null)
210
+ const authHelper = plugin.config.get('azureAuthTokenHelper');
211
+ t.is(authHelper, null);
212
+ });