Skip to content

Commit

Permalink
port: AgentSettings 4.14 changes to 4.17 (#4631)
Browse files Browse the repository at this point in the history
* Add circular structure detection and allow the agent settings to be customizable

* Fix port issues

* Fix lint
  • Loading branch information
sw-joelmut authored Mar 13, 2024
1 parent 5ee4dac commit d6e4d59
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 24 deletions.
13 changes: 8 additions & 5 deletions libraries/botbuilder-core/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import * as z from 'zod';
import { createHash } from 'crypto';
import { stringify } from 'botbuilder-stdlib';
import { TurnContext } from './turnContext';

/**
Expand Down Expand Up @@ -127,10 +129,11 @@ export function assertStoreItems(val: unknown, ..._args: unknown[]): asserts val
* @returns change hash string
*/
export function calculateChangeHash(item: StoreItem): string {
const cpy = { ...item };
if (cpy.eTag) {
delete cpy.eTag;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { eTag, ...rest } = item;

return JSON.stringify(cpy);
const result = stringify(rest);
const hash = createHash('sha256', { encoding: 'utf-8' });
const hashed = hash.update(result).digest('hex');
return hashed;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"botbuilder": "4.1.6",
"botbuilder-dialogs-adaptive-runtime": "4.1.6",
"botbuilder-dialogs-adaptive-runtime-core": "4.1.6",
"botframework-connector": "4.1.6",
"express": "^4.17.1",
"zod": "~1.11.17"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ActivityHandlerBase, BotFrameworkHttpAdapter, ChannelServiceRoutes
import type { Server } from 'http';
import type { ServiceCollection } from 'botbuilder-dialogs-adaptive-runtime-core';
import { Configuration, getRuntimeServices } from 'botbuilder-dialogs-adaptive-runtime';
import type { ConnectorClientOptions } from 'botframework-connector';
import { json, urlencoded } from 'body-parser';

// Explicitly fails checks for `""`
Expand Down Expand Up @@ -38,6 +39,12 @@ const TypedOptions = z.object({
* Path inside applicationRoot that should be served as static files
*/
staticDirectory: NonEmptyString,

/**
* Used when creating ConnectorClients.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
connectorClientOptions: z.object({}).nonstrict() as z.ZodObject<any, any, ConnectorClientOptions>,
});

/**
Expand All @@ -51,6 +58,7 @@ const defaultOptions: Options = {
skillsEndpointPrefix: '/api/skills',
port: 3978,
staticDirectory: 'wwwroot',
connectorClientOptions: {},
};

/**
Expand All @@ -65,7 +73,9 @@ export async function start(
settingsDirectory: string,
options: Partial<Options> = {}
): Promise<void> {
const [services, configuration] = await getRuntimeServices(applicationRoot, settingsDirectory);
const [services, configuration] = await getRuntimeServices(applicationRoot, settingsDirectory, {
connectorClientOptions: options.connectorClientOptions,
});
const [, listen] = await makeApp(services, configuration, applicationRoot, options);

listen();
Expand Down
14 changes: 11 additions & 3 deletions libraries/botbuilder-dialogs-adaptive-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/* eslint-disable @typescript-eslint/no-explicit-any */

import * as z from 'zod';
import fs from 'fs';
import path from 'path';
Expand Down Expand Up @@ -522,6 +524,7 @@ function registerQnAComponents(services: ServiceCollection, configuration: Confi
*
* @param applicationRoot absolute path to root of application
* @param settingsDirectory directory where settings files are located
* @param defaultServices services to use as default
* @returns service collection and configuration
*
* @remarks
Expand Down Expand Up @@ -560,27 +563,31 @@ function registerQnAComponents(services: ServiceCollection, configuration: Confi
*/
export async function getRuntimeServices(
applicationRoot: string,
settingsDirectory: string
settingsDirectory: string,
defaultServices?: Record<string, any>
): Promise<[ServiceCollection, Configuration]>;

/**
* Construct all runtime services.
*
* @param applicationRoot absolute path to root of application
* @param configuration a fully initialized configuration instance to use
* @param defaultServices services to use as default
* @returns service collection and configuration
*/
export async function getRuntimeServices(
applicationRoot: string,
configuration: Configuration
configuration: Configuration,
defaultServices?: Record<string, any>
): Promise<[ServiceCollection, Configuration]>;

/**
* @internal
*/
export async function getRuntimeServices(
applicationRoot: string,
configurationOrSettingsDirectory: Configuration | string
configurationOrSettingsDirectory: Configuration | string,
defaultServices: Record<string, any> = {}
): Promise<[ServiceCollection, Configuration]> {
// Resolve configuration
let configuration: Configuration;
Expand Down Expand Up @@ -612,6 +619,7 @@ export async function getRuntimeServices(
middlewares: new MiddlewareSet(),
pathResolvers: [],
serviceClientCredentialsFactory: undefined,
...defaultServices,
});

services.addFactory<ResourceExplorer, { declarativeTypes: ComponentDeclarativeTypes[] }>(
Expand Down
12 changes: 11 additions & 1 deletion libraries/botbuilder-dialogs-adaptive/src/actions/beginSkill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,17 @@ export class BeginSkill extends SkillDialog implements BeginSkillConfiguration {
* @param options Optional options used to configure the skill dialog.
*/
constructor(options?: SkillDialogOptions) {
super(Object.assign({ skill: {} } as SkillDialogOptions, options));
super(
Object.assign({ skill: {} } as SkillDialogOptions, options, {
// This is an alternative to the toJSON function because when the SkillDialogOptions are saved into the Storage,
// when the information is retrieved, it doesn't have the properties that were declared in the toJSON function.
_replacer(): Omit<SkillDialogOptions, 'conversationState' | 'skillClient' | 'conversationIdFactory'> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { conversationState, skillClient, conversationIdFactory, ...rest } = this;
return rest;
},
})
);
}

/**
Expand Down
1 change: 1 addition & 0 deletions libraries/botbuilder-stdlib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * as stringExt from './stringExt';
export { delay } from './delay';
export { maybeCast } from './maybeCast';
export { retry } from './retry';
export { stringify } from './stringify';
82 changes: 82 additions & 0 deletions libraries/botbuilder-stdlib/src/stringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/* eslint-disable @typescript-eslint/no-explicit-any */

/**
* Encapsulates JSON.stringify function to detect and handle different types of errors (eg. Circular Structure).
*
* @remarks
* Circular Structure:
* - It detects when the provided value has circular references and replaces them with [Circular *.{path to the value being referenced}].
*
* _replacer internal function:
* - Have similar functionality as the JSON.stringify internal toJSON function, but with the difference that only affects this stringify functionality.
*
* @example
* // Circular Structure:
* {
* "item": {
* "name": "parent",
* "parent": null,
* "child": {
* "name": "child",
* "parent": "[Circular *.item]" // => obj.item.child.parent = obj.item
* }
* }
* }
*
* @param value — A JavaScript value, usually an object or array, to be converted.
* @param replacer — A function that transforms the results.
* @param space — Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
* @returns {string} The converted JavaScript value to a JavaScript Object Notation (JSON) string.
*/
export function stringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string {
if (!value) {
return '';
}

try {
return JSON.stringify(value, stringifyReplacer(replacer), space);
} catch (error: any) {
if (!error?.message.includes('circular structure')) {
throw error;
}

const seen = new WeakMap();
return JSON.stringify(
value,
function stringifyCircularReplacer(key, val) {
if (val === null || val === undefined || typeof val !== 'object') {
return val;
}

if (key) {
const path = seen.get(val);
if (path) {
return `[Circular *.${path.join('.')}]`;
}

const parent = seen.get(this) ?? [];
seen.set(val, [...parent, key]);
}

const value = stringifyReplacer(replacer)(key, val);
return value;
},
space
);
}
}

function stringifyReplacer(replacer?: (key: string, value: any) => any) {
return function stringifyReplacerInternal(this: any, key: string, val: any) {
const replacerValue = replacer ? replacer.call(this, key, val) : val;
if (replacerValue === null || replacerValue === undefined || typeof replacerValue !== 'object') {
return replacerValue;
}

const result = replacerValue._replacer ? replacerValue._replacer(key) : replacerValue;
return result;
};
}
48 changes: 35 additions & 13 deletions libraries/botframework-connector/src/auth/botFrameworkClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,36 @@ import * as z from 'zod';
import axios from 'axios';
import { Activity, ChannelAccount, InvokeResponse, RoleTypes } from 'botframework-schema';
import { BotFrameworkClient } from '../skills';
import type { ConnectorClientOptions } from '../connectorApi/models';
import { ConversationIdHttpHeaderName } from '../conversationConstants';
import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory';
import { USER_AGENT } from './connectorFactoryImpl';
import { WebResource } from '@azure/ms-rest-js';
import { ok } from 'assert';

const botFrameworkClientFetchImpl: typeof fetch = async (input, init) => {
const url = z.string().parse(input);
const { body, headers } = z.object({ body: z.string(), headers: z.record(z.string()).optional() }).parse(init);

const response = await axios.post(url, JSON.parse(body), {
headers,
validateStatus: () => true,
const botFrameworkClientFetchImpl = (connectorClientOptions: ConnectorClientOptions): typeof fetch => {
const { http: httpAgent, https: httpsAgent } = connectorClientOptions?.agentSettings ?? {
http: undefined,
https: undefined,
};
const axiosInstance = axios.create({
httpAgent,
httpsAgent,
validateStatus: (): boolean => true,
});

return {
status: response.status,
json: async () => response.data,
} as Response;
return async (input, init?): Promise<Response> => {
const url = z.string().parse(input);
const { body, headers } = z.object({ body: z.string(), headers: z.record(z.string()).optional() }).parse(init);

const response = await axiosInstance.post(url, body, {
headers,
});
return {
status: response.status,
json: () => response.data,
} as Response;
};
};

/**
Expand All @@ -35,13 +46,24 @@ export class BotFrameworkClientImpl implements BotFrameworkClient {
* @param credentialsFactory A [ServiceClientCredentialsFactory](xref:botframework-connector.ServiceClientCredentialsFactory) instance.
* @param loginEndpoint The login url.
* @param botFrameworkClientFetch A custom Fetch implementation to be used in the [BotFrameworkClient](xref:botframework-connector.BotFrameworkClient).
* @param connectorClientOptions A [ConnectorClientOptions](xref:botframework-connector.ConnectorClientOptions) object.
*/
constructor(
private readonly credentialsFactory: ServiceClientCredentialsFactory,
private readonly loginEndpoint: string,
private readonly botFrameworkClientFetch = botFrameworkClientFetchImpl
private readonly botFrameworkClientFetch?: ReturnType<typeof botFrameworkClientFetchImpl>,
private readonly connectorClientOptions?: ConnectorClientOptions
) {
ok(typeof botFrameworkClientFetch === 'function');
this.botFrameworkClientFetch ??= botFrameworkClientFetchImpl(this.connectorClientOptions);

ok(typeof this.botFrameworkClientFetch === 'function');
}

private toJSON() {
// Ignore ConnectorClientOptions, as it could contain Circular Structure behavior.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { connectorClientOptions, ...rest } = this;
return rest;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent
return new BotFrameworkClientImpl(
this.credentialsFactory,
this.toChannelFromBotLoginUrl,
this.botFrameworkClientFetch
this.botFrameworkClientFetch,
this.connectorClientOptions
);
}

Expand Down

0 comments on commit d6e4d59

Please sign in to comment.