diff --git a/packages/agents-a365-runtime/README.md b/packages/agents-a365-runtime/README.md index 42d42e0..b696299 100644 --- a/packages/agents-a365-runtime/README.md +++ b/packages/agents-a365-runtime/README.md @@ -13,6 +13,53 @@ npm install @microsoft/agents-a365-runtime ## Usage +### Agent Settings Service + +The Agent Settings Service provides methods to manage agent settings templates and instance-specific settings: + +```typescript +import { + AgentSettingsService, + PowerPlatformApiDiscovery +} from '@microsoft/agents-a365-runtime'; + +// Initialize the service +const apiDiscovery = new PowerPlatformApiDiscovery('prod'); +const tenantId = 'your-tenant-id'; +const service = new AgentSettingsService(apiDiscovery, tenantId); + +// Get agent setting template by agent type +const template = await service.getAgentSettingTemplate( + 'my-agent-type', + accessToken +); + +// Set agent setting template +await service.setAgentSettingTemplate( + { + agentType: 'my-agent-type', + settings: { key1: 'value1', key2: 'value2' } + }, + accessToken +); + +// Get agent settings by instance +const settings = await service.getAgentSettings( + 'agent-instance-id', + accessToken +); + +// Set agent settings by instance +await service.setAgentSettings( + { + agentInstanceId: 'agent-instance-id', + agentType: 'my-agent-type', + settings: { instanceKey: 'instanceValue' } + }, + accessToken +); +``` + For detailed usage examples and implementation guidance, see the [Microsoft Agent 365 Developer Documentation](https://learn.microsoft.com/microsoft-agent-365/developer/?tabs=nodejs). ## Support diff --git a/packages/agents-a365-runtime/src/agent-settings-service.ts b/packages/agents-a365-runtime/src/agent-settings-service.ts new file mode 100644 index 0000000..f7cb591 --- /dev/null +++ b/packages/agents-a365-runtime/src/agent-settings-service.ts @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { PowerPlatformApiDiscovery } from './power-platform-api-discovery'; + +/** + * Represents an agent setting template. + */ +export interface AgentSettingTemplate { + /** + * The agent type identifier. + */ + agentType: string; + + /** + * The settings template as a key-value dictionary. + */ + settings: Record; + + /** + * Optional metadata about the template. + */ + metadata?: Record; +} + +/** + * Represents agent settings for a specific instance. + */ +export interface AgentSettings { + /** + * The agent instance identifier. + */ + agentInstanceId: string; + + /** + * The agent type identifier. + */ + agentType: string; + + /** + * The settings as a key-value dictionary. + */ + settings: Record; + + /** + * Optional metadata about the settings. + */ + metadata?: Record; +} + +/** + * Service for managing agent settings templates and instance-specific settings. + */ +export class AgentSettingsService { + private readonly apiDiscovery: PowerPlatformApiDiscovery; + private readonly tenantId: string; + + /** + * Creates a new instance of AgentSettingsService. + * @param apiDiscovery The Power Platform API discovery service. + * @param tenantId The tenant identifier. + */ + constructor(apiDiscovery: PowerPlatformApiDiscovery, tenantId: string) { + this.apiDiscovery = apiDiscovery; + this.tenantId = tenantId; + } + + /** + * Gets the base endpoint for agent settings API. + * @returns The base endpoint URL. + */ + private getBaseEndpoint(): string { + const tenantEndpoint = this.apiDiscovery.getTenantEndpoint(this.tenantId); + return `https://${tenantEndpoint}/agents/v1.0`; + } + + /** + * Gets the endpoint for agent setting templates. + * @param agentType The agent type identifier. + * @returns The endpoint URL for the agent type template. + */ + public getAgentSettingTemplateEndpoint(agentType: string): string { + return `${this.getBaseEndpoint()}/settings/templates/${encodeURIComponent(agentType)}`; + } + + /** + * Gets the endpoint for agent instance settings. + * @param agentInstanceId The agent instance identifier. + * @returns The endpoint URL for the agent instance settings. + */ + public getAgentSettingsEndpoint(agentInstanceId: string): string { + return `${this.getBaseEndpoint()}/settings/instances/${encodeURIComponent(agentInstanceId)}`; + } + + /** + * Retrieves an agent setting template by agent type. + * @param agentType The agent type identifier. + * @param accessToken The access token for authentication. + * @returns A promise that resolves to the agent setting template. + */ + public async getAgentSettingTemplate( + agentType: string, + accessToken: string + ): Promise { + const endpoint = this.getAgentSettingTemplateEndpoint(agentType); + + const response = await fetch(endpoint, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to get agent setting template for type '${agentType}': ${response.status} ${response.statusText}` + ); + } + + return await response.json(); + } + + /** + * Sets an agent setting template for a specific agent type. + * @param template The agent setting template to set. + * @param accessToken The access token for authentication. + * @returns A promise that resolves to the updated agent setting template. + */ + public async setAgentSettingTemplate( + template: AgentSettingTemplate, + accessToken: string + ): Promise { + const endpoint = this.getAgentSettingTemplateEndpoint(template.agentType); + + const response = await fetch(endpoint, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(template), + }); + + if (!response.ok) { + throw new Error( + `Failed to set agent setting template for type '${template.agentType}': ${response.status} ${response.statusText}` + ); + } + + return await response.json(); + } + + /** + * Retrieves agent settings for a specific agent instance. + * @param agentInstanceId The agent instance identifier. + * @param accessToken The access token for authentication. + * @returns A promise that resolves to the agent settings. + */ + public async getAgentSettings( + agentInstanceId: string, + accessToken: string + ): Promise { + const endpoint = this.getAgentSettingsEndpoint(agentInstanceId); + + const response = await fetch(endpoint, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to get agent settings for instance '${agentInstanceId}': ${response.status} ${response.statusText}` + ); + } + + return await response.json(); + } + + /** + * Sets agent settings for a specific agent instance. + * @param settings The agent settings to set. + * @param accessToken The access token for authentication. + * @returns A promise that resolves to the updated agent settings. + */ + public async setAgentSettings( + settings: AgentSettings, + accessToken: string + ): Promise { + const endpoint = this.getAgentSettingsEndpoint(settings.agentInstanceId); + + const response = await fetch(endpoint, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(settings), + }); + + if (!response.ok) { + throw new Error( + `Failed to set agent settings for instance '${settings.agentInstanceId}': ${response.status} ${response.statusText}` + ); + } + + return await response.json(); + } +} diff --git a/packages/agents-a365-runtime/src/index.ts b/packages/agents-a365-runtime/src/index.ts index c6a9d97..4bc9903 100644 --- a/packages/agents-a365-runtime/src/index.ts +++ b/packages/agents-a365-runtime/src/index.ts @@ -1,4 +1,5 @@ export * from './power-platform-api-discovery'; export * from './agentic-authorization-service'; export * from './environment-utils'; -export * from './utility'; \ No newline at end of file +export * from './utility'; +export * from './agent-settings-service'; \ No newline at end of file diff --git a/tests/runtime/agent-settings-service.test.ts b/tests/runtime/agent-settings-service.test.ts new file mode 100644 index 0000000..2192d52 --- /dev/null +++ b/tests/runtime/agent-settings-service.test.ts @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { + AgentSettingsService, + AgentSettingTemplate, + AgentSettings, + PowerPlatformApiDiscovery, + ClusterCategory, +} from '@microsoft/agents-a365-runtime'; + +// Mock fetch globally +const mockFetch = jest.fn() as jest.MockedFunction; +global.fetch = mockFetch; + +describe('AgentSettingsService', () => { + const testTenantId = 'e3064512-cc6d-4703-be71-a2ecaecaa98a'; + const testAccessToken = 'test-access-token-123'; + const testAgentType = 'test-agent-type'; + const testAgentInstanceId = 'test-agent-instance-123'; + let service: AgentSettingsService; + let apiDiscovery: PowerPlatformApiDiscovery; + + beforeEach(() => { + apiDiscovery = new PowerPlatformApiDiscovery('prod' as ClusterCategory); + service = new AgentSettingsService(apiDiscovery, testTenantId); + mockFetch.mockClear(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getAgentSettingTemplateEndpoint', () => { + it('should return correct endpoint for agent setting template', () => { + const endpoint = service.getAgentSettingTemplateEndpoint(testAgentType); + expect(endpoint).toContain('/agents/v1.0/settings/templates/'); + expect(endpoint).toContain(encodeURIComponent(testAgentType)); + expect(endpoint).toMatch(/^https:\/\//); + }); + + it('should encode special characters in agent type', () => { + const agentTypeWithSpecialChars = 'agent/type with spaces'; + const endpoint = service.getAgentSettingTemplateEndpoint(agentTypeWithSpecialChars); + expect(endpoint).toContain(encodeURIComponent(agentTypeWithSpecialChars)); + }); + }); + + describe('getAgentSettingsEndpoint', () => { + it('should return correct endpoint for agent instance settings', () => { + const endpoint = service.getAgentSettingsEndpoint(testAgentInstanceId); + expect(endpoint).toContain('/agents/v1.0/settings/instances/'); + expect(endpoint).toContain(encodeURIComponent(testAgentInstanceId)); + expect(endpoint).toMatch(/^https:\/\//); + }); + + it('should encode special characters in agent instance id', () => { + const instanceIdWithSpecialChars = 'instance/id with spaces'; + const endpoint = service.getAgentSettingsEndpoint(instanceIdWithSpecialChars); + expect(endpoint).toContain(encodeURIComponent(instanceIdWithSpecialChars)); + }); + }); + + describe('getAgentSettingTemplate', () => { + const mockTemplate: AgentSettingTemplate = { + agentType: testAgentType, + settings: { + setting1: 'value1', + setting2: 42, + }, + metadata: { + version: '1.0', + }, + }; + + it('should successfully retrieve agent setting template', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockTemplate, + } as Response); + + const result = await service.getAgentSettingTemplate(testAgentType, testAccessToken); + + expect(result).toEqual(mockTemplate); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/settings/templates/${encodeURIComponent(testAgentType)}`), + expect.objectContaining({ + method: 'GET', + headers: { + Authorization: `Bearer ${testAccessToken}`, + 'Content-Type': 'application/json', + }, + }) + ); + }); + + it('should throw error when API returns non-ok status', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + } as Response); + + await expect( + service.getAgentSettingTemplate(testAgentType, testAccessToken) + ).rejects.toThrow(`Failed to get agent setting template for type '${testAgentType}': 404 Not Found`); + }); + + it('should include authorization header with access token', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockTemplate, + } as Response); + + await service.getAgentSettingTemplate(testAgentType, testAccessToken); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${testAccessToken}`, + }), + }) + ); + }); + }); + + describe('setAgentSettingTemplate', () => { + const mockTemplate: AgentSettingTemplate = { + agentType: testAgentType, + settings: { + setting1: 'value1', + setting2: 42, + }, + metadata: { + version: '1.0', + }, + }; + + it('should successfully set agent setting template', async () => { + const updatedTemplate = { ...mockTemplate, metadata: { version: '1.1' } }; + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => updatedTemplate, + } as Response); + + const result = await service.setAgentSettingTemplate(mockTemplate, testAccessToken); + + expect(result).toEqual(updatedTemplate); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/settings/templates/${encodeURIComponent(testAgentType)}`), + expect.objectContaining({ + method: 'PUT', + headers: { + Authorization: `Bearer ${testAccessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mockTemplate), + }) + ); + }); + + it('should throw error when API returns non-ok status', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 400, + statusText: 'Bad Request', + } as Response); + + await expect( + service.setAgentSettingTemplate(mockTemplate, testAccessToken) + ).rejects.toThrow(`Failed to set agent setting template for type '${testAgentType}': 400 Bad Request`); + }); + + it('should send template data in request body', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockTemplate, + } as Response); + + await service.setAgentSettingTemplate(mockTemplate, testAccessToken); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: JSON.stringify(mockTemplate), + }) + ); + }); + }); + + describe('getAgentSettings', () => { + const mockSettings: AgentSettings = { + agentInstanceId: testAgentInstanceId, + agentType: testAgentType, + settings: { + instanceSetting1: 'value1', + instanceSetting2: 100, + }, + metadata: { + lastUpdated: '2024-01-01T00:00:00Z', + }, + }; + + it('should successfully retrieve agent settings', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockSettings, + } as Response); + + const result = await service.getAgentSettings(testAgentInstanceId, testAccessToken); + + expect(result).toEqual(mockSettings); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/settings/instances/${encodeURIComponent(testAgentInstanceId)}`), + expect.objectContaining({ + method: 'GET', + headers: { + Authorization: `Bearer ${testAccessToken}`, + 'Content-Type': 'application/json', + }, + }) + ); + }); + + it('should throw error when API returns non-ok status', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 403, + statusText: 'Forbidden', + } as Response); + + await expect( + service.getAgentSettings(testAgentInstanceId, testAccessToken) + ).rejects.toThrow(`Failed to get agent settings for instance '${testAgentInstanceId}': 403 Forbidden`); + }); + + it('should include authorization header with access token', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockSettings, + } as Response); + + await service.getAgentSettings(testAgentInstanceId, testAccessToken); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${testAccessToken}`, + }), + }) + ); + }); + }); + + describe('setAgentSettings', () => { + const mockSettings: AgentSettings = { + agentInstanceId: testAgentInstanceId, + agentType: testAgentType, + settings: { + instanceSetting1: 'value1', + instanceSetting2: 100, + }, + metadata: { + lastUpdated: '2024-01-01T00:00:00Z', + }, + }; + + it('should successfully set agent settings', async () => { + const updatedSettings = { ...mockSettings, settings: { ...mockSettings.settings, newSetting: true } }; + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => updatedSettings, + } as Response); + + const result = await service.setAgentSettings(mockSettings, testAccessToken); + + expect(result).toEqual(updatedSettings); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining(`/settings/instances/${encodeURIComponent(testAgentInstanceId)}`), + expect.objectContaining({ + method: 'PUT', + headers: { + Authorization: `Bearer ${testAccessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mockSettings), + }) + ); + }); + + it('should throw error when API returns non-ok status', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response); + + await expect( + service.setAgentSettings(mockSettings, testAccessToken) + ).rejects.toThrow( + `Failed to set agent settings for instance '${testAgentInstanceId}': 500 Internal Server Error` + ); + }); + + it('should send settings data in request body', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockSettings, + } as Response); + + await service.setAgentSettings(mockSettings, testAccessToken); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: JSON.stringify(mockSettings), + }) + ); + }); + }); + + describe('different cluster categories', () => { + it.each<{ cluster: ClusterCategory; expectedDomain: string }>([ + { cluster: 'prod', expectedDomain: 'api.powerplatform.com' }, + { cluster: 'gov', expectedDomain: 'api.gov.powerplatform.microsoft.us' }, + { cluster: 'high', expectedDomain: 'api.high.powerplatform.microsoft.us' }, + ])('should construct endpoint with correct domain for $cluster cluster', ({ cluster, expectedDomain }) => { + const discovery = new PowerPlatformApiDiscovery(cluster); + const testService = new AgentSettingsService(discovery, testTenantId); + + const endpoint = testService.getAgentSettingTemplateEndpoint(testAgentType); + + expect(endpoint).toContain(expectedDomain); + expect(endpoint).toContain('/agents/v1.0/settings/templates/'); + }); + }); +});