Feature/Add teams, gmail, outlook tools (#4577)

* add teams, gmail, outlook tools

* update docs link

* update credentials for oauth2

* add jira tool

* add google drive, google calendar, google sheets tools, powerpoint, excel, word doc loader

* update jira logo

* Refactor Gmail and Outlook tools to remove maxOutputLength parameter and enhance request handling. Update response formatting to include parameters in the output. Adjust Google Drive tools to simplify success messages by removing unnecessary parameter details.
This commit is contained in:
Henry Heng
2025-06-06 19:52:04 +01:00
committed by GitHub
parent 6dcb65cedb
commit 30c4180d97
62 changed files with 16832 additions and 144 deletions
@@ -0,0 +1,142 @@
import { TextSplitter } from 'langchain/text_splitter'
import { WordLoader } from './WordLoader'
import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
class MicrosoftWord_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Microsoft Word'
this.name = 'microsoftWord'
this.version = 1.0
this.type = 'Document'
this.icon = 'word.svg'
this.category = 'Document Loaders'
this.description = `Load data from Microsoft Word files`
this.baseClasses = [this.type]
this.inputs = [
{
label: 'Word File',
name: 'docxFile',
type: 'file',
fileType: '.docx, .doc'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
this.outputs = [
{
label: 'Document',
name: 'document',
description: 'Array of document objects containing metadata and pageContent',
baseClasses: [...this.baseClasses, 'json']
},
{
label: 'Text',
name: 'text',
description: 'Concatenated string from pageContent of documents',
baseClasses: ['string', 'json']
}
]
}
getFiles(nodeData: INodeData) {
const docxFileBase64 = nodeData.inputs?.docxFile as string
let files: string[] = []
let fromStorage: boolean = true
if (docxFileBase64.startsWith('FILE-STORAGE::')) {
const fileName = docxFileBase64.replace('FILE-STORAGE::', '')
if (fileName.startsWith('[') && fileName.endsWith(']')) {
files = JSON.parse(fileName)
} else {
files = [fileName]
}
} else {
if (docxFileBase64.startsWith('[') && docxFileBase64.endsWith(']')) {
files = JSON.parse(docxFileBase64)
} else {
files = [docxFileBase64]
}
fromStorage = false
}
return { files, fromStorage }
}
async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
if (fromStorage) {
return getFileFromStorage(file, orgId, chatflowid)
} else {
const splitDataURI = file.split(',')
splitDataURI.pop()
return Buffer.from(splitDataURI.pop() || '', 'base64')
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const output = nodeData.outputs?.output as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let docs: IDocument[] = []
const orgId = options.orgId
const chatflowid = options.chatflowid
const { files, fromStorage } = this.getFiles(nodeData)
for (const file of files) {
if (!file) continue
const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
const blob = new Blob([fileData])
const loader = new WordLoader(blob)
// use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
}
docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
return handleDocumentLoaderOutput(docs, output)
}
}
module.exports = { nodeClass: MicrosoftWord_DocumentLoaders }
@@ -0,0 +1,108 @@
import { Document } from '@langchain/core/documents'
import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
import { parseOfficeAsync } from 'officeparser'
/**
* Document loader that uses officeparser to load Word documents.
*
* The document is parsed into a single Document with metadata including
* document type and extracted text content.
*/
export class WordLoader extends BufferLoader {
attributes: { name: string; description: string; type: string }[] = []
constructor(filePathOrBlob: string | Blob) {
super(filePathOrBlob)
this.attributes = []
}
/**
* Parse Word document
*
* @param raw Raw data Buffer
* @param metadata Document metadata
* @returns Array of Documents
*/
async parse(raw: Buffer, metadata: Document['metadata']): Promise<Document[]> {
const result: Document[] = []
this.attributes = [
{ name: 'documentType', description: 'Type of document', type: 'string' },
{ name: 'pageCount', description: 'Number of pages/sections', type: 'number' }
]
try {
// Use officeparser to extract text from Word document
const data = await parseOfficeAsync(raw)
if (typeof data === 'string' && data.trim()) {
// Split content by common page/section separators
const sections = this.splitIntoSections(data)
sections.forEach((sectionContent, index) => {
if (sectionContent.trim()) {
result.push({
pageContent: sectionContent.trim(),
metadata: {
documentType: 'word',
pageNumber: index + 1,
...metadata
}
})
}
})
}
} catch (error) {
console.error('Error parsing Word file:', error)
throw new Error(`Failed to parse Word file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
return result
}
/**
* Split content into sections based on common patterns
* This is a heuristic approach since officeparser returns plain text
*/
private splitIntoSections(content: string): string[] {
// Try to split by common section patterns
const sectionPatterns = [
/\n\s*Page\s+\d+/gi,
/\n\s*Section\s+\d+/gi,
/\n\s*Chapter\s+\d+/gi,
/\n\s*\d+\.\s+/gi, // Numbered sections like "1. ", "2. "
/\n\s*[A-Z][A-Z\s]{2,}\n/g, // ALL CAPS headings
/\n\s*_{5,}/g, // Long underscores as separators
/\n\s*-{5,}/g // Long dashes as separators
]
let sections: string[] = []
// Try each pattern and use the one that creates the most reasonable splits
for (const pattern of sectionPatterns) {
const potentialSections = content.split(pattern)
if (potentialSections.length > 1 && potentialSections.length < 50) {
// Reasonable number of sections
sections = potentialSections
break
}
}
// If no good pattern found, split by multiple newlines as a fallback
if (sections.length === 0) {
sections = content.split(/\n\s*\n\s*\n\s*\n/)
}
// If still no good split, split by double newlines
if (sections.length === 0 || sections.every((section) => section.trim().length < 20)) {
sections = content.split(/\n\s*\n\s*\n/)
}
// If still no good split, treat entire content as one section
if (sections.length === 0 || sections.every((section) => section.trim().length < 10)) {
sections = [content]
}
return sections.filter((section) => section.trim().length > 0)
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="Q7XamDf1hnh~bz~vAO7C6a" x1="28" x2="28" y1="14.966" y2="6.45" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#42a3f2"/><stop offset="1" stop-color="#42a4eb"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6a)" d="M42,6H14c-1.105,0-2,0.895-2,2v7.003h32V8C44,6.895,43.105,6,42,6z"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6b" x1="28" x2="28" y1="42" y2="33.054" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#11408a"/><stop offset="1" stop-color="#103f8f"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6b)" d="M12,33.054V40c0,1.105,0.895,2,2,2h28c1.105,0,2-0.895,2-2v-6.946H12z"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6c" x1="28" x2="28" y1="-15.46" y2="-15.521" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3079d6"/><stop offset="1" stop-color="#297cd2"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6c)" d="M12,15.003h32v9.002H12V15.003z"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6d" x1="12" x2="44" y1="28.53" y2="28.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d59b3"/><stop offset="1" stop-color="#195bbc"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6d)" d="M12,24.005h32v9.05H12V24.005z"/><path d="M22.319,13H12v24h10.319C24.352,37,26,35.352,26,33.319V16.681C26,14.648,24.352,13,22.319,13z" opacity=".05"/><path d="M22.213,36H12V13.333h10.213c1.724,0,3.121,1.397,3.121,3.121v16.425 C25.333,34.603,23.936,36,22.213,36z" opacity=".07"/><path d="M22.106,35H12V13.667h10.106c1.414,0,2.56,1.146,2.56,2.56V32.44C24.667,33.854,23.52,35,22.106,35z" opacity=".09"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6e" x1="4.744" x2="23.494" y1="14.744" y2="33.493" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#256ac2"/><stop offset="1" stop-color="#1247ad"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6e)" d="M22,34H6c-1.105,0-2-0.895-2-2V16c0-1.105,0.895-2,2-2h16c1.105,0,2,0.895,2,2v16 C24,33.105,23.105,34,22,34z"/><path fill="#fff" d="M18.403,19l-1.546,7.264L15.144,19h-2.187l-1.767,7.489L9.597,19H7.641l2.344,10h2.352l1.713-7.689 L15.764,29h2.251l2.344-10H18.403z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB