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

Added Outlook integration toolkit #3465

Open
wants to merge 74 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
12dd2a9
outlook integration
hahahafafa Nov 14, 2023
3388d62
Update outlookIntegration.ts
SimonLi1020 Nov 15, 2023
2694daf
Merge pull request #1 from hahahafafa/develop
hahahafafa Nov 21, 2023
884bab7
get message tips integrated
hahahafafa Nov 22, 2023
929f9f8
added some tests and added getToken for getting Access token
oscarchen178 Nov 27, 2023
44928c1
move our integration to outlook folder, extract authentication to oth…
oscarchen178 Nov 29, 2023
ba52666
add get credentials from env, remove unused package
oscarchen178 Nov 29, 2023
106868d
create entrypoints for outlook, create read and send tool extending b…
oscarchen178 Nov 29, 2023
3b54b6f
Update index.ts
oscarchen178 Nov 29, 2023
5c259c7
reformat the read email
Qi123123Li Nov 30, 2023
3d123c5
now our outlook tools depend on abstract authFlowBase not concrete au…
oscarchen178 Nov 30, 2023
df52718
added two authentication flow, make outlook tool construct more simpl…
oscarchen178 Nov 30, 2023
5fc9972
change the code to fit standard lint rules
Qi123123Li Nov 30, 2023
43388c7
add test case for send email
Qi123123Li Nov 30, 2023
497e486
fix some small bugs
Qi123123Li Nov 30, 2023
e29c32d
Merge branch 'langchain-ai:main' into main
hahahafafa Nov 30, 2023
11d91fc
add invalid token test
oscarchen178 Nov 30, 2023
d19b446
Merge branch 'dev2'
hahahafafa Nov 30, 2023
7091118
Update .env.example
SimonLi1020 Nov 30, 2023
d48ab61
Merge branch 'main' into main
SimonLi1020 Nov 30, 2023
6234417
Merge branch 'main' into main
SimonLi1020 Nov 30, 2023
d38f9f6
use typedoc
Qi123123Li Dec 1, 2023
1bc7761
Merge branch 'main' into run-typedoc
Qi123123Li Dec 1, 2023
0ff7f6d
Merge pull request #4 from hahahafafa/run-typedoc
Qi123123Li Dec 1, 2023
12d160f
add http and url in package
Qi123123Li Dec 1, 2023
cf52d46
Merge branch 'run-typedoc'
Qi123123Li Dec 1, 2023
7c03901
Merge branch 'main' into main
Qi123123Li Dec 1, 2023
083a8ae
Merge branch 'main' into main
oscarchen178 Dec 4, 2023
d70da41
remove optional deps, remove modules using opt depd from index entryp…
oscarchen178 Dec 4, 2023
918ad47
Merge branch 'main' into main
oscarchen178 Dec 4, 2023
9a6ef37
Merge branch 'main' into main
oscarchen178 Dec 5, 2023
43ee152
add to create-entrypoints
oscarchen178 Dec 5, 2023
d4bf536
change from openurl to open package
oscarchen178 Dec 5, 2023
d21fc40
Merge branch 'main' into main
oscarchen178 Dec 5, 2023
960d082
remove url from dependencies
oscarchen178 Dec 6, 2023
f4861fe
log the url for manually open instead of using open package
oscarchen178 Dec 6, 2023
bdb51ab
Merge branch 'main' into main
oscarchen178 Dec 6, 2023
c518e9f
Update langchain/src/tools/outlook/authFlowREST.ts
Qi123123Li Dec 6, 2023
3212a3c
Update langchain/src/tools/outlook/descriptions.ts
Qi123123Li Dec 6, 2023
767acbc
Update langchain/src/tools/outlook/descriptions.ts
Qi123123Li Dec 6, 2023
216c3cc
Update langchain/src/tools/tests/outlookIntegration.test.ts
Qi123123Li Dec 6, 2023
c94886e
fix the format
Qi123123Li Dec 7, 2023
c3f6c74
add the jsdoc
Qi123123Li Dec 7, 2023
3cec11e
add jsdoc
Qi123123Li Dec 7, 2023
9b1d3f4
change OutlookBase to abstract; remove `state` in rest; apply camel c…
oscarchen178 Dec 10, 2023
d92e771
Merge branch 'main' into main
oscarchen178 Dec 10, 2023
6ebb241
Merge branch 'main' of https://github.com/hwchase17/langchainjs into …
jacoblee93 Dec 12, 2023
da61a07
Merge
jacoblee93 Dec 12, 2023
ed41abe
Skip test
jacoblee93 Dec 12, 2023
ba2ec31
Merge branch 'main' into main
oscarchen178 Dec 20, 2023
d9ca68d
Merge branch 'main' into main
oscarchen178 Dec 21, 2023
04a7221
Merge branch 'main' into main
oscarchen178 Dec 22, 2023
23271fd
Merge branch 'main' of https://github.com/hwchase17/langchainjs into …
jacoblee93 Dec 22, 2023
562df12
Move/rename files around
jacoblee93 Dec 22, 2023
a9ba549
add a docs page for outlook toolkit
oscarchen178 Dec 22, 2023
7fa8d85
Update outlook.mdx
jacoblee93 Dec 23, 2023
c2a9eba
Update outlook.mdx
oscarchen178 Dec 23, 2023
c7eb5c6
Update outlook.mdx
oscarchen178 Dec 23, 2023
6718669
add getRefreshToken for AuthFlowREST
oscarchen178 Dec 23, 2023
4cbdeb9
Merge branch 'main' into main
Qi123123Li Dec 26, 2023
b300654
Merge branch 'main' into main
oscarchen178 Dec 28, 2023
1fb53e0
move getAuth from outlook tool to authFLow
oscarchen178 Dec 29, 2023
4719a52
format
oscarchen178 Dec 29, 2023
cdde1d4
update docs
oscarchen178 Dec 29, 2023
ab49c28
Merge branch 'main' into main
oscarchen178 Dec 29, 2023
175324c
Merge branch 'main' into main
oscarchen178 Jan 4, 2024
8751341
Merge branch 'main' into main
oscarchen178 Jan 6, 2024
42189f8
format outlook.mdx
oscarchen178 Jan 6, 2024
f94225a
Merge branch 'main' into main
oscarchen178 Jan 6, 2024
d1e961e
putting example in the examples/
oscarchen178 Jan 18, 2024
569b34e
Update outlook.mdx
oscarchen178 Jan 19, 2024
981242e
Update outlook.ts
oscarchen178 Jan 19, 2024
1aa5ee3
Merge remote-tracking branch 'upstream/main'
oscarchen178 Jan 20, 2024
d03bd77
Merge branch 'main' into main
oscarchen178 Jan 23, 2024
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
5 changes: 5 additions & 0 deletions langchain/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,8 @@ NEO4J_USERNAME=ADD_YOURS_HERE
NEO4J_PASSWORD=ADD_YOURS_HERE
CLOSEVECTOR_API_KEY=ADD_YOURS_HERE
CLOSEVECTOR_API_SECRET=ADD_YOURS_HERE
OUTLOOK_CLIENT_ID=ADD_YOURS_HERE
OUTLOOK_CLIENT_SECRET=ADD_YOURS_HERE
OUTLOOK_REDIRECT_URI=ADD_YOURS_HERE
OUTLOOK_REFRESH_TOKEN=ADD_YOURS_HERE
OUTLOOK_ACCESS_TOKEN=ADD_YOURS_HERE
3 changes: 3 additions & 0 deletions langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@
"@types/jsdom": "^21.1.1",
"@types/lodash": "^4",
"@types/mozilla-readability": "^0.2.1",
"@types/openurl": "^1",
"@types/pdf-parse": "^1.1.1",
"@types/pg": "^8",
"@types/pg-copy-streams": "^1.2.2",
Expand Down Expand Up @@ -1410,6 +1411,8 @@
"ml-distance": "^4.0.0",
"openai": "^4.19.0",
"openapi-types": "^12.1.3",
"openurl": "^1.1.1",
"p-queue": "^6.6.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra deps

"p-retry": "4",
"uuid": "^9.0.0",
"yaml": "^2.2.1",
Expand Down
1 change: 1 addition & 0 deletions langchain/scripts/create-entrypoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const entrypoints = {
"tools/sql": "tools/sql",
"tools/webbrowser": "tools/webbrowser",
"tools/google_calendar": "tools/google_calendar/index",
"tools/outlook": "tools/outlook/index",
// chains
chains: "chains/index",
"chains/combine_documents/reduce": "chains/combine_documents/reduce",
Expand Down
1 change: 1 addition & 0 deletions langchain/src/load/import_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * as agents__openai__output_parser from "../agents/openai/output_parser.j
export * as base_language from "../base_language/index.js";
export * as tools from "../tools/index.js";
export * as tools__render from "../tools/render.js";
export * as tools__outlook from "../tools/outlook/index.js";
export * as chains from "../chains/index.js";
export * as chains__combine_documents__reduce from "../chains/combine_documents/reduce.js";
export * as chains__openai_functions from "../chains/openai_functions/index.js";
Expand Down
2 changes: 2 additions & 0 deletions langchain/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ export {
formatToOpenAIFunction,
formatToOpenAITool,
} from "./convert_to_openai.js";
// export { OutlookIntegration } from "./outlook/outlookIntegration.js";
export { OutlookSendMailTool, OutlookReadMailTool } from "./outlook/index.js";
13 changes: 13 additions & 0 deletions langchain/src/tools/outlook/authFlowBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export abstract class AuthFlowBase {
protected clientId: string;

protected accessToken = "";

constructor(clientId: string) {
this.clientId = clientId;
}

public abstract getAccessToken(): Promise<string>;

public abstract refreshAccessToken(): Promise<string>;
}
189 changes: 189 additions & 0 deletions langchain/src/tools/outlook/authFlowREST.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import * as http from "http";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a new HTTP request using the fetch function in the getAccessToken and refreshAccessToken methods. This comment is flagging the change for maintainers to review.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds code that explicitly accesses and reads environment variables via the getEnvironmentVariable function, which should be reviewed by maintainers to ensure proper handling and usage of environment variables.

import * as url from "url";
import * as openurl from "openurl";
import { AuthFlowBase } from "./authFlowBase.js";
import { getEnvironmentVariable } from "../../util/env.js";

interface AccessTokenResponse {
access_token: string;
refresh_token: string;
}

export class AuthFlowREST extends AuthFlowBase {
private clientSecret: string;

private redirectUri: string;

private port: number;

private pathname: string;

private refreshToken = "";

constructor({ clientId, clientSecret, redirectUri }: { clientId?: string, clientSecret?: string, redirectUri?: string } = {}) {
let id = clientId;
let secret = clientSecret;
let uri = redirectUri;
if (!id || !secret || !uri) {
id = getEnvironmentVariable("OUTLOOK_CLIENT_ID");
secret = getEnvironmentVariable("OUTLOOK_CLIENT_SECRET");
uri = getEnvironmentVariable("OUTLOOK_REDIRECT_URI");
}
Qi123123Li marked this conversation as resolved.
Show resolved Hide resolved
if (!id || !secret || !uri) {
throw new Error("Missing clientId, clientSecret or redirectUri.");
}
super(id);
this.clientSecret = secret;
this.redirectUri = uri;
const parsedUrl = new URL(this.redirectUri);
this.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 3000;
this.pathname = parsedUrl.pathname || "";
}

// Function to construct the OAuth URL
private openAuthUrl(): string {
const loginEndpoint =
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
const { clientId } = this; // client ID regestered in Azure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do object destructuring here, since it's only used in one place it's clearer to just call this.clientId

const response_type = "code";
const response_mode = "query";
const redirectUri = encodeURIComponent(this.redirectUri); // redirect URI regestered in Azure
const scope = encodeURIComponent(
"openid offline_access https://graph.microsoft.com/.default"
);
const state = "12345";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking this is the correct value?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just found that this is optional so I will remove this


const url = [
`${loginEndpoint}?client_id=${clientId}`,
`&response_type=${response_type}`,
`&response_mode=${response_mode}`,
`&redirect_uri=${redirectUri}`,
`&scope=${scope}`,
`&state=${state}`,
].join("");
openurl.open(url);
return url;
}

// Function to start the server
private startServer(): Promise<http.Server> {
return new Promise((resolve, reject) => {
const server = http.createServer();

server.listen(this.port, () => {
console.log(`Server listening at http://localhost:${this.port}`);
resolve(server);
});

server.on("error", (err) => {
reject(err);
});
});
}

// Function to listen for the authorization code
private async listenForCode(server: http.Server): Promise<string> {
return new Promise((resolve, reject) => {
server.on("request", (req, res) => {
try {
const reqUrl = url.parse(req.url || "", true);

if (reqUrl.pathname === this.pathname) {
const authCode = reqUrl.query.code as string;

res.writeHead(200, { "Content-Type": "text/html" });
res.end("Authorization code received. You can close this window.");

server.close();
console.log("Server closed");
resolve(authCode); // Resolve the Promise with the authorization code
} else {
res.writeHead(404);
res.end("404 Not Found");
}
} catch (err) {
res.writeHead(500);
res.end("Server error");
reject(err);
}
});
});
}

// Main function to run the auth flow
private async getCode(): Promise<string> {
// check credentials
if (!this.clientId || !this.redirectUri) {
throw new Error("Missing clientId or redirectUri.");
}

const server = await this.startServer();
this.openAuthUrl();
const code = await this.listenForCode(server);
return code;
}

// Function to get the token using the code and client credentials
public async getAccessToken(): Promise<string> {
// fetch auth code from user login
const code = await this.getCode();
// fetch access token using auth code
const req_body =
`client_id=${encodeURIComponent(this.clientId)}&` +
`client_secret=${encodeURIComponent(this.clientSecret)}&` +
`scope=${encodeURIComponent("https://graph.microsoft.com/.default")}&` +
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` +
`grant_type=authorization_code&` +
`code=${encodeURIComponent(code)}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be cleaner to use something like URLSearchParams


const response = await fetch(
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: req_body,
}
);

if (!response.ok) {
throw new Error(`fetch token error! response: ${response.status}`);
}
// save access token and refresh token
const json = (await response.json()) as AccessTokenResponse;
this.accessToken = json.access_token;
this.refreshToken = json.refresh_token;
return this.accessToken;
}

public async refreshAccessToken(): Promise<string> {
// fetch new access token using refresh token
const req_body =
`client_id=${encodeURIComponent(this.clientId)}&` +
`client_secret=${encodeURIComponent(this.clientSecret)}&` +
`scope=${encodeURIComponent("https://graph.microsoft.com/.default")}&` +
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` +
`grant_type=refresh_token&` +
`refresh_token=${encodeURIComponent(this.refreshToken)}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same search params comment here


const response = await fetch(
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: req_body,
}
);

if (!response.ok) {
throw new Error(`fetch token error! response: ${response.status}`);
}
// save new access token
const json = (await response.json()) as AccessTokenResponse;
this.accessToken = json.access_token;
return this.accessToken;
}
}
103 changes: 103 additions & 0 deletions langchain/src/tools/outlook/authFlowToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { AuthFlowBase } from "./authFlowBase.js";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a new fetch request in the AuthFlowRefresh class to refresh the access token using the refresh token. This comment is flagging the change for maintainers to review the addition of this new request.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on the PR! I've flagged a change in the code that requires an environment variable using the getEnvironmentVariable function. Please review this change to ensure it aligns with the project's requirements.

import { getEnvironmentVariable } from "../../util/env.js";

interface AccessTokenResponse {
access_token: string;
refresh_token: string;
}

// if you have the token, and no need to refresh it, warning: token expires in 1 hour
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert to JSDoc and give more of an overview as to what this class can be used for

export class AuthFlowToken extends AuthFlowBase {
constructor(accessToken?: string) {
let token = accessToken;
if (!token) {
token = getEnvironmentVariable("OUTLOOK_ACCESS_TOKEN");
}
if (!token) {
throw new Error("Missing access_token.");
}
super("");
this.accessToken = token;
}

public async refreshAccessToken(): Promise<string> {
return this.accessToken;
}

public async getAccessToken(): Promise<string> {
return this.accessToken;
}
}

// if you have the refresh token and other credentials
export class AuthFlowRefresh extends AuthFlowBase {
private clientSecret: string;

private redirectUri: string;

private refreshToken: string;

constructor(
clientId?: string,
clientSecret?: string,
redirectUri?: string,
refreshToken?: string
) {
let id = clientId;
let secret = clientSecret;
let uri = redirectUri;
let token = refreshToken;
if (!id || !secret || !uri || !token) {
id = getEnvironmentVariable("OUTLOOK_CLIENT_ID");
secret = getEnvironmentVariable("OUTLOOK_CLIENT_SECRET");
uri = getEnvironmentVariable("OUTLOOK_REDIRECT_URI");
token = getEnvironmentVariable("OUTLOOK_REFRESH_TOKEN");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here around not overriding all.
Some more context:
If a user passes the id, secret and uri but not token then this will run. However if they are missing id / secret / uri in env vars it'll reassign them to undefined and the error below will throw.

if (!id || !secret || !uri || !token) {
throw new Error(
"Missing clientId, clientSecret, redirectUri or refreshToken."
);
}
super(id);
this.clientSecret = secret;
this.redirectUri = uri;
this.refreshToken = token;
}

public async refreshAccessToken(): Promise<string> {
// fetch new access token using refresh token
const req_body =
`client_id=${encodeURIComponent(this.clientId)}&` +
`client_secret=${encodeURIComponent(this.clientSecret)}&` +
`scope=${encodeURIComponent("https://graph.microsoft.com/.default")}&` +
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` +
`grant_type=refresh_token&` +
`refresh_token=${encodeURIComponent(this.refreshToken)}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment


const response = await fetch(
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: req_body,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, prefer camel case. Please refactor here and in the other places where snake case is used.

}
);

if (!response.ok) {
throw new Error(`fetch token error! response: ${response.status}`);
}
// save new access token
const json = (await response.json()) as AccessTokenResponse;
this.accessToken = json.access_token;
return this.accessToken;
}

// Function to get the token using the code and client credentials
public async getAccessToken(): Promise<string> {
const accessToken = await this.refreshAccessToken();
this.accessToken = accessToken;
return accessToken;
}
}
Loading