Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JS] feat: File download support, vision support, and vision sample #913

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/.nycrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"**/coverage/**",
"**/*.d.ts",
"**/*.spec.ts",
"packages/**/src/index.ts"
"packages/**/src/**/index.ts"
],
"reporter": ["html", "text"],
"all": true,
Expand Down
2 changes: 1 addition & 1 deletion js/packages/teams-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@microsoft/teams-ai",
"author": "Microsoft Corp.",
"description": "SDK focused on building AI based applications for Microsoft Teams.",
"version": "1.0.0-preview.1",
"version": "1.0.0-preview.2",
"license": "MIT",
"keywords": [
"botbuilder",
Expand Down
12 changes: 1 addition & 11 deletions js/packages/teams-ai/src/AI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,17 +453,6 @@ export class AI<TState extends TurnState = TurnState> {
start_time?: number,
step_count?: number
): Promise<boolean> {
// Populate {{$temp.input}}
if (typeof state.temp.input != 'string') {
// Use the received activity text
state.temp.input = context.activity.text;
}

// Initialize {{$allOutputs}}
if (state.temp.actionOutputs == undefined) {
state.temp.actionOutputs = {};
}

// Initialize start time and action count
const { max_steps, max_time } = this._options;
if (start_time === undefined) {
Expand Down Expand Up @@ -549,6 +538,7 @@ export class AI<TState extends TurnState = TurnState> {
// Copy the actions output to the input
state.temp.lastOutput = output;
state.temp.input = output;
state.temp.inputFiles = [];
}

// Check for looping
Expand Down
29 changes: 28 additions & 1 deletion js/packages/teams-ai/src/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ import { ReadReceiptInfo } from 'botframework-connector';

import { AdaptiveCards, AdaptiveCardsOptions } from './AdaptiveCards';
import { AI, AIOptions } from './AI';
import { Meetings } from './Meetings';
import { MessageExtensions } from './MessageExtensions';
import { TaskModules, TaskModulesOptions } from './TaskModules';
import { AuthenticationManager, AuthenticationOptions } from './authentication/Authentication';
import { TurnState } from './TurnState';
import { InputFileDownloader } from './InputFileDownloader';
import {
deleteUserInSignInFlow,
setTokenInState,
setUserInSignInFlow,
userInSignInFlow
} from './authentication/BotAuthenticationBase';
import { Meetings } from './Meetings';

/**
* @private
Expand Down Expand Up @@ -133,6 +134,11 @@ export interface ApplicationOptions<TState extends TurnState> {
* Optional. Factory used to create a custom turn state instance.
*/
turnStateFactory: () => TState;

/**
* Optional. Array of input file download plugins to use.
*/
fileDownloaders?: InputFileDownloader<TState>[];
}

/**
Expand Down Expand Up @@ -668,6 +674,27 @@ export class Application<TState extends TurnState = TurnState> {
return false;
}

// Populate {{$temp.input}}
if (typeof state.temp.input != 'string') {
// Use the received activity text
state.temp.input = context.activity.text;
}

// Download any input files
if (Array.isArray(this._options.fileDownloaders) && this._options.fileDownloaders.length > 0) {
const inputFiles = state.temp.inputFiles ?? [];
for (let i = 0; i < this._options.fileDownloaders.length; i++) {
const files = await this._options.fileDownloaders[i].downloadFiles(context, state);
inputFiles.push(...files);
}
state.temp.inputFiles = inputFiles;
}

// Initialize {{$allOutputs}}
if (state.temp.actionOutputs == undefined) {
state.temp.actionOutputs = {};
}

// Run any RouteSelectors in this._invokeRoutes first if the incoming Teams activity.type is "Invoke".
// Invoke Activities from Teams need to be responded to in less than 5 seconds.
if (context.activity.type === ActivityTypes.Invoke) {
Expand Down
43 changes: 43 additions & 0 deletions js/packages/teams-ai/src/InputFileDownloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @module teams-ai
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { TurnContext } from 'botbuilder';
import { TurnState } from './TurnState';

/**
* A plugin responsible for downloading files relative to the current user's input.
* @template TState Optional. Type of application state.
*/
export interface InputFileDownloader<TState extends TurnState = TurnState> {
/**
* Download any files relative to the current user's input.
* @param context Context for the current turn of conversation.
* @param state Application state for the current turn of conversation.
*/
downloadFiles(context: TurnContext, state: TState): Promise<InputFile[]>;
}

/**
* A file sent by the user to the bot.
*/
export interface InputFile {
/**
* The downloaded content of the file.
*/
content: Buffer;

/**
* The content type of the file.
*/
contentType: string;

/**
* Optional. URL to the content of the file.
*/
contentUrl?: string;
}
129 changes: 129 additions & 0 deletions js/packages/teams-ai/src/TeamsAttachmentDownloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @module teams-ai
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import axios, { AxiosInstance } from 'axios';
import { Attachment, TurnContext } from 'botbuilder';
import { TurnState } from './TurnState';
import { InputFile, InputFileDownloader } from './InputFileDownloader';

/**
* Options for the `TeamsAttachmentDownloader` class.
*/
export interface TeamsAttachmentDownloaderOptions {
/**
* The Microsoft App ID of the bot.
*/
botAppId: string;

/**
* The Microsoft App Password of the bot.
*/
botAppPassword: string;
}

/**
* Downloads attachments from Teams using the bots access token.
*/
export class TeamsAttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
private readonly _options: TeamsAttachmentDownloaderOptions;
private _httpClient: AxiosInstance;

/**
* Creates a new instance of the `TeamsAttachmentDownloader` class.
* @param options Options for the `TeamsAttachmentDownloader` class.
*/
public constructor(options: TeamsAttachmentDownloaderOptions) {
this._options = options;
this._httpClient = axios.create();
}

/**
* Download any files relative to the current user's input.
* @param context Context for the current turn of conversation.
* @param state Application state for the current turn of conversation.
*/
public async downloadFiles(context: TurnContext, state: TState): Promise<InputFile[]> {
// Filter out HTML attachments
const attachments = context.activity.attachments?.filter((a) => !a.contentType.startsWith('text/html'));
if (!attachments || attachments.length === 0) {
return Promise.resolve([]);
}

// Download all attachments
const accessToken = await this.getAccessToken();
const files: InputFile[] = [];
for (const attachment of attachments) {
const file = await this.downloadFile(attachment, accessToken);
if (file) {
files.push(file);
}
}

return files;
}

/**
* @private
*/
private async downloadFile(attachment: Attachment, accessToken: string): Promise<InputFile> {
if (attachment.contentUrl && attachment.contentUrl.startsWith('https://')) {
// Download file
const headers = {
'Authorization': `Bearer ${accessToken}`
};
const response = await this._httpClient.get(attachment.contentUrl, {
headers,
responseType: 'arraybuffer'
});

// Convert to a buffer
const content = Buffer.from(response.data, 'binary');

// Fixup content type
let contentType = attachment.contentType;
if (contentType === 'image/*') {
contentType = 'image/png';
}

// Return file
return {
content,
contentType,
contentUrl: attachment.contentUrl,
};
} else {
return {
content: Buffer.from(attachment.content),
contentType: attachment.contentType,
contentUrl: attachment.contentUrl,
};
}
}

/**
* @private
*/
private async getAccessToken(): Promise<string> {
const headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const body = `grant_type=client_credentials&client_id=${encodeURI(this._options.botAppId)}&client_secret=${encodeURI(this._options.botAppPassword)}&scope=https%3A%2F%2Fapi.botframework.com%2F.default`;
const token = await this._httpClient.post<JWTToken>('https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token', body, { headers });
return token.data.access_token;
}
}

/**
* @private
*/
interface JWTToken {
token_type: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
}
7 changes: 4 additions & 3 deletions js/packages/teams-ai/src/TurnState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { TurnContext, Storage, StoreItems } from 'botbuilder';
import { Memory } from './MemoryFork';
import { InputFile } from './InputFileDownloader';

/**
* @private
Expand Down Expand Up @@ -50,14 +51,14 @@ export interface DefaultUserState {}
*/
export interface DefaultTempState {
/**
* Input passed to an AI prompt
* Input passed from the user to the AI Library
*/
input: string;

/**
* Formatted conversation history for embedding in an AI prompt
* Downloaded files passed by the user to the AI Library
*/
history: string;
inputFiles: InputFile[];

/**
* Output returned from the last executed action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('Authentication', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down Expand Up @@ -291,7 +291,7 @@ describe('AuthenticationManager', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('AdaptiveCardAuthenticaion', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('BotAuthentication', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('OAuthPromptMessageExtensionAuthentication', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('TeamsSsoBotAuthentication', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('TeamsSsoMessageExtensionAuthentication', () => {
await state.load(context);
state.temp = {
input: '',
history: '',
inputFiles: [],
lastOutput: '',
actionOutputs: {},
authTokens: {}
Expand Down
2 changes: 2 additions & 0 deletions js/packages/teams-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ export * from './validators';
export * from './AdaptiveCards';
export * from './AI';
export * from './Application';
export * from './InputFileDownloader';
export * from './MemoryFork';
export * from './MessageExtensions';
export * from './TaskModules';
export * from './TeamsAttachmentDownloader';
export * from './TestTurnState';
export * from './TurnState';
export * from './Utilities';
Expand Down
Loading