chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
inferDocumentFileInfo,
|
||||
type KnowledgeCreateDocumentResponse,
|
||||
} from '@/tools/knowledge/types'
|
||||
import { enrichKBTagsSchema } from '@/tools/schema-enrichers'
|
||||
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumentResponse> = {
|
||||
id: 'knowledge_create_document',
|
||||
name: 'Knowledge Create Document',
|
||||
description: 'Create a new document in a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the document',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content of the document',
|
||||
},
|
||||
documentTags: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document tags',
|
||||
},
|
||||
},
|
||||
|
||||
schemaEnrichment: {
|
||||
documentTags: {
|
||||
dependsOn: 'knowledgeBaseId',
|
||||
enrichSchema: enrichKBTagsSchema,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const workflowId = params._context?.workflowId
|
||||
const textContent = params.content?.trim()
|
||||
const documentName = params.name?.trim()
|
||||
|
||||
if (!documentName || documentName.length === 0) {
|
||||
throw new Error('Document name is required')
|
||||
}
|
||||
if (documentName.length > 255) {
|
||||
throw new Error('Document name must be 255 characters or less')
|
||||
}
|
||||
if (!textContent || textContent.length < 1) {
|
||||
throw new Error('Document content cannot be empty')
|
||||
}
|
||||
const utf8Bytes = new TextEncoder().encode(textContent)
|
||||
const contentBytes = utf8Bytes.length
|
||||
|
||||
if (contentBytes > 1_000_000) {
|
||||
throw new Error('Document content exceeds maximum size of 1MB')
|
||||
}
|
||||
|
||||
let base64Content: string
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
base64Content = Buffer.from(textContent, 'utf8').toString('base64')
|
||||
} else {
|
||||
let binary = ''
|
||||
for (let i = 0; i < utf8Bytes.length; i++) {
|
||||
binary += String.fromCharCode(utf8Bytes[i])
|
||||
}
|
||||
base64Content = btoa(binary)
|
||||
}
|
||||
|
||||
const { filename, mimeType } = inferDocumentFileInfo(documentName)
|
||||
const dataUri = `data:${mimeType};base64,${base64Content}`
|
||||
|
||||
const parsedTags = parseDocumentTags(params.documentTags)
|
||||
const tagData = formatDocumentTagsForAPI(parsedTags)
|
||||
|
||||
const documents = [
|
||||
{
|
||||
filename,
|
||||
fileUrl: dataUri,
|
||||
fileSize: contentBytes,
|
||||
mimeType,
|
||||
...tagData,
|
||||
},
|
||||
]
|
||||
|
||||
const requestBody = {
|
||||
documents: documents,
|
||||
processingOptions: {
|
||||
recipe: 'default',
|
||||
lang: 'en',
|
||||
},
|
||||
bulk: true,
|
||||
...(workflowId && { workflowId }),
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeCreateDocumentResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
const documentsCreated = data.documentsCreated || []
|
||||
|
||||
// Handle multiple documents response
|
||||
const uploadCount = documentsCreated.length
|
||||
const firstDocument = documentsCreated[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message:
|
||||
uploadCount > 1
|
||||
? `Successfully created ${uploadCount} documents in knowledge base`
|
||||
: `Successfully created document in knowledge base`,
|
||||
data: {
|
||||
documentId: firstDocument?.documentId || firstDocument?.id || '',
|
||||
documentName:
|
||||
uploadCount > 1 ? `${uploadCount} documents` : firstDocument?.filename || 'Unknown',
|
||||
type: 'document',
|
||||
enabled: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Information about the created document',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Document ID' },
|
||||
documentName: { type: 'string', description: 'Document name' },
|
||||
type: { type: 'string', description: 'Document type' },
|
||||
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Success or error message describing the operation result',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the created document',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { KnowledgeDeleteChunkResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeDeleteChunkTool: ToolConfig<any, KnowledgeDeleteChunkResponse> = {
|
||||
id: 'knowledge_delete_chunk',
|
||||
name: 'Knowledge Delete Chunk',
|
||||
description: 'Delete a chunk from a document in a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document containing the chunk',
|
||||
},
|
||||
chunkId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the chunk to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`,
|
||||
method: 'DELETE',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeDeleteChunkResponse> => {
|
||||
const result = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
chunkId: params?.chunkId ?? '',
|
||||
documentId: params?.documentId ?? '',
|
||||
message: result.data?.message ?? 'Chunk deleted successfully',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
chunkId: {
|
||||
type: 'string',
|
||||
description: 'ID of the deleted chunk',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the parent document',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation message',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { KnowledgeDeleteDocumentResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeDeleteDocumentTool: ToolConfig<any, KnowledgeDeleteDocumentResponse> = {
|
||||
id: 'knowledge_delete_document',
|
||||
name: 'Knowledge Delete Document',
|
||||
description: 'Delete a document from a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`,
|
||||
method: 'DELETE',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeDeleteDocumentResponse> => {
|
||||
const result = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
documentId: params?.documentId ?? '',
|
||||
message: result.data?.message ?? 'Document deleted successfully',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the deleted document',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation message',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { KnowledgeGetConnectorResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeGetConnectorTool: ToolConfig<any, KnowledgeGetConnectorResponse> = {
|
||||
id: 'knowledge_get_connector',
|
||||
name: 'Knowledge Get Connector',
|
||||
description:
|
||||
'Get detailed connector information including recent sync logs for monitoring sync health',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base the connector belongs to',
|
||||
},
|
||||
connectorId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the connector to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/connectors/${params.connectorId}`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeGetConnectorResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data || {}
|
||||
|
||||
return {
|
||||
success: result.success ?? true,
|
||||
output: {
|
||||
connector: {
|
||||
id: data.id,
|
||||
connectorType: data.connectorType,
|
||||
status: data.status,
|
||||
syncIntervalMinutes: data.syncIntervalMinutes,
|
||||
lastSyncAt: data.lastSyncAt ?? null,
|
||||
lastSyncError: data.lastSyncError ?? null,
|
||||
lastSyncDocCount: data.lastSyncDocCount ?? null,
|
||||
nextSyncAt: data.nextSyncAt ?? null,
|
||||
consecutiveFailures: data.consecutiveFailures ?? 0,
|
||||
createdAt: data.createdAt ?? null,
|
||||
updatedAt: data.updatedAt ?? null,
|
||||
},
|
||||
syncLogs: (data.syncLogs || []).map(
|
||||
(log: {
|
||||
id: string
|
||||
status: string
|
||||
startedAt: string | null
|
||||
completedAt: string | null
|
||||
docsAdded: number | null
|
||||
docsUpdated: number | null
|
||||
docsDeleted: number | null
|
||||
docsUnchanged: number | null
|
||||
errorMessage: string | null
|
||||
}) => ({
|
||||
id: log.id,
|
||||
status: log.status,
|
||||
startedAt: log.startedAt ?? null,
|
||||
completedAt: log.completedAt ?? null,
|
||||
docsAdded: log.docsAdded ?? null,
|
||||
docsUpdated: log.docsUpdated ?? null,
|
||||
docsDeleted: log.docsDeleted ?? null,
|
||||
docsUnchanged: log.docsUnchanged ?? null,
|
||||
errorMessage: log.errorMessage ?? null,
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
connector: {
|
||||
type: 'object',
|
||||
description: 'Connector details',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Connector ID' },
|
||||
connectorType: { type: 'string', description: 'Type of connector' },
|
||||
status: { type: 'string', description: 'Connector status (active, paused, syncing)' },
|
||||
syncIntervalMinutes: { type: 'number', description: 'Sync interval in minutes' },
|
||||
lastSyncAt: { type: 'string', description: 'Timestamp of last sync' },
|
||||
lastSyncError: { type: 'string', description: 'Error from last sync if failed' },
|
||||
lastSyncDocCount: { type: 'number', description: 'Docs synced in last sync' },
|
||||
nextSyncAt: { type: 'string', description: 'Next scheduled sync timestamp' },
|
||||
consecutiveFailures: { type: 'number', description: 'Consecutive sync failures' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
syncLogs: {
|
||||
type: 'array',
|
||||
description: 'Recent sync log entries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Sync log ID' },
|
||||
status: { type: 'string', description: 'Sync status' },
|
||||
startedAt: { type: 'string', description: 'Sync start time' },
|
||||
completedAt: { type: 'string', description: 'Sync completion time' },
|
||||
docsAdded: { type: 'number', description: 'Documents added' },
|
||||
docsUpdated: { type: 'number', description: 'Documents updated' },
|
||||
docsDeleted: { type: 'number', description: 'Documents deleted' },
|
||||
docsUnchanged: { type: 'number', description: 'Documents unchanged' },
|
||||
errorMessage: { type: 'string', description: 'Error message if sync failed' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { KnowledgeGetDocumentResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeGetDocumentTool: ToolConfig<any, KnowledgeGetDocumentResponse> = {
|
||||
id: 'knowledge_get_document',
|
||||
name: 'Knowledge Get Document',
|
||||
description:
|
||||
'Get full details of a single document including tags, connector metadata, and processing status',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base the document belongs to',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeGetDocumentResponse> => {
|
||||
const result = await response.json()
|
||||
const doc = result.data || {}
|
||||
|
||||
const tagSlots = [
|
||||
'tag1',
|
||||
'tag2',
|
||||
'tag3',
|
||||
'tag4',
|
||||
'tag5',
|
||||
'tag6',
|
||||
'tag7',
|
||||
'number1',
|
||||
'number2',
|
||||
'number3',
|
||||
'number4',
|
||||
'number5',
|
||||
'date1',
|
||||
'date2',
|
||||
'boolean1',
|
||||
'boolean2',
|
||||
'boolean3',
|
||||
]
|
||||
const tags: Record<string, unknown> = {}
|
||||
for (const slot of tagSlots) {
|
||||
if (doc[slot] !== null && doc[slot] !== undefined) {
|
||||
tags[slot] = doc[slot]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.success ?? true,
|
||||
output: {
|
||||
id: doc.id,
|
||||
filename: doc.filename,
|
||||
fileSize: doc.fileSize ?? 0,
|
||||
mimeType: doc.mimeType ?? null,
|
||||
enabled: doc.enabled ?? true,
|
||||
processingStatus: doc.processingStatus ?? null,
|
||||
processingError: doc.processingError ?? null,
|
||||
chunkCount: doc.chunkCount ?? 0,
|
||||
tokenCount: doc.tokenCount ?? 0,
|
||||
characterCount: doc.characterCount ?? 0,
|
||||
uploadedAt: doc.uploadedAt ?? null,
|
||||
updatedAt: doc.updatedAt ?? null,
|
||||
connectorId: doc.connectorId ?? null,
|
||||
sourceUrl: doc.sourceUrl ?? null,
|
||||
externalId: doc.externalId ?? null,
|
||||
tags,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Document ID' },
|
||||
filename: { type: 'string', description: 'Document filename' },
|
||||
fileSize: { type: 'number', description: 'File size in bytes' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the document' },
|
||||
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
|
||||
processingStatus: {
|
||||
type: 'string',
|
||||
description: 'Processing status (pending, processing, completed, failed)',
|
||||
},
|
||||
processingError: {
|
||||
type: 'string',
|
||||
description: 'Error message if processing failed',
|
||||
},
|
||||
chunkCount: { type: 'number', description: 'Number of chunks in the document' },
|
||||
tokenCount: { type: 'number', description: 'Total token count across chunks' },
|
||||
characterCount: { type: 'number', description: 'Total character count' },
|
||||
uploadedAt: { type: 'string', description: 'Upload timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
connectorId: {
|
||||
type: 'string',
|
||||
description: 'Connector ID if document was synced from an external source',
|
||||
},
|
||||
sourceUrl: {
|
||||
type: 'string',
|
||||
description: 'Original URL in the source system if synced from a connector',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
description: 'External ID from the source system',
|
||||
},
|
||||
tags: {
|
||||
type: 'object',
|
||||
description: 'Tag values keyed by tag slot (tag1-7, number1-5, date1-2, boolean1-3)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { knowledgeCreateDocumentTool } from '@/tools/knowledge/create_document'
|
||||
import { knowledgeDeleteChunkTool } from '@/tools/knowledge/delete_chunk'
|
||||
import { knowledgeDeleteDocumentTool } from '@/tools/knowledge/delete_document'
|
||||
import { knowledgeGetConnectorTool } from '@/tools/knowledge/get_connector'
|
||||
import { knowledgeGetDocumentTool } from '@/tools/knowledge/get_document'
|
||||
import { knowledgeListChunksTool } from '@/tools/knowledge/list_chunks'
|
||||
import { knowledgeListConnectorsTool } from '@/tools/knowledge/list_connectors'
|
||||
import { knowledgeListDocumentsTool } from '@/tools/knowledge/list_documents'
|
||||
import { knowledgeListTagsTool } from '@/tools/knowledge/list_tags'
|
||||
import { knowledgeSearchTool } from '@/tools/knowledge/search'
|
||||
import { knowledgeTriggerSyncTool } from '@/tools/knowledge/trigger_sync'
|
||||
import { knowledgeUpdateChunkTool } from '@/tools/knowledge/update_chunk'
|
||||
import { knowledgeUploadChunkTool } from '@/tools/knowledge/upload_chunk'
|
||||
import { knowledgeUpsertDocumentTool } from '@/tools/knowledge/upsert_document'
|
||||
|
||||
export {
|
||||
knowledgeSearchTool,
|
||||
knowledgeUploadChunkTool,
|
||||
knowledgeCreateDocumentTool,
|
||||
knowledgeListTagsTool,
|
||||
knowledgeListDocumentsTool,
|
||||
knowledgeDeleteDocumentTool,
|
||||
knowledgeGetDocumentTool,
|
||||
knowledgeListChunksTool,
|
||||
knowledgeUpdateChunkTool,
|
||||
knowledgeDeleteChunkTool,
|
||||
knowledgeListConnectorsTool,
|
||||
knowledgeGetConnectorTool,
|
||||
knowledgeTriggerSyncTool,
|
||||
knowledgeUpsertDocumentTool,
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*
|
||||
* Knowledge Tools Unit Tests
|
||||
*
|
||||
* Tests for knowledge_search and knowledge_upload_chunk tools,
|
||||
* specifically the cost restructuring in transformResponse.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { knowledgeSearchTool } from '@/tools/knowledge/search'
|
||||
import { knowledgeUploadChunkTool } from '@/tools/knowledge/upload_chunk'
|
||||
|
||||
/**
|
||||
* Creates a mock Response object for testing transformResponse
|
||||
*/
|
||||
function createMockResponse(data: unknown): Response {
|
||||
return {
|
||||
json: async () => data,
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response
|
||||
}
|
||||
|
||||
describe('Knowledge Tools', () => {
|
||||
describe('knowledgeSearchTool', () => {
|
||||
describe('transformResponse', () => {
|
||||
it('should restructure cost information for logging', async () => {
|
||||
const apiResponse = {
|
||||
data: {
|
||||
results: [{ content: 'test result', similarity: 0.95 }],
|
||||
query: 'test query',
|
||||
totalResults: 1,
|
||||
cost: {
|
||||
input: 0.00001042,
|
||||
output: 0,
|
||||
total: 0.00001042,
|
||||
tokens: {
|
||||
prompt: 521,
|
||||
completion: 0,
|
||||
total: 521,
|
||||
},
|
||||
model: 'text-embedding-3-small',
|
||||
pricing: {
|
||||
input: 0.02,
|
||||
output: 0,
|
||||
updatedAt: '2025-07-10',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const result = await knowledgeSearchTool.transformResponse!(createMockResponse(apiResponse))
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output).toEqual({
|
||||
results: [{ content: 'test result', similarity: 0.95 }],
|
||||
query: 'test query',
|
||||
totalResults: 1,
|
||||
cost: {
|
||||
input: 0.00001042,
|
||||
output: 0,
|
||||
total: 0.00001042,
|
||||
},
|
||||
tokens: {
|
||||
prompt: 521,
|
||||
completion: 0,
|
||||
total: 521,
|
||||
},
|
||||
model: 'text-embedding-3-small',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle response without cost information', async () => {
|
||||
const apiResponse = {
|
||||
data: {
|
||||
results: [],
|
||||
query: 'test query',
|
||||
totalResults: 0,
|
||||
},
|
||||
}
|
||||
|
||||
const result = await knowledgeSearchTool.transformResponse!(createMockResponse(apiResponse))
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output).toEqual({
|
||||
results: [],
|
||||
query: 'test query',
|
||||
totalResults: 0,
|
||||
})
|
||||
expect(result.output.cost).toBeUndefined()
|
||||
expect(result.output.tokens).toBeUndefined()
|
||||
expect(result.output.model).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle response with partial cost information', async () => {
|
||||
const apiResponse = {
|
||||
data: {
|
||||
results: [],
|
||||
query: 'test query',
|
||||
totalResults: 0,
|
||||
cost: {
|
||||
input: 0.001,
|
||||
output: 0,
|
||||
total: 0.001,
|
||||
// No tokens or model
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const result = await knowledgeSearchTool.transformResponse!(createMockResponse(apiResponse))
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.cost).toEqual({
|
||||
input: 0.001,
|
||||
output: 0,
|
||||
total: 0.001,
|
||||
})
|
||||
expect(result.output.tokens).toBeUndefined()
|
||||
expect(result.output.model).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('knowledgeUploadChunkTool', () => {
|
||||
describe('transformResponse', () => {
|
||||
it('should restructure cost information for logging', async () => {
|
||||
const apiResponse = {
|
||||
data: {
|
||||
id: 'chunk-123',
|
||||
chunkIndex: 0,
|
||||
content: 'test content',
|
||||
contentLength: 12,
|
||||
tokenCount: 3,
|
||||
enabled: true,
|
||||
documentId: 'doc-456',
|
||||
documentName: 'Test Document',
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
updatedAt: '2025-01-01T00:00:00Z',
|
||||
cost: {
|
||||
input: 0.00000521,
|
||||
output: 0,
|
||||
total: 0.00000521,
|
||||
tokens: {
|
||||
prompt: 260,
|
||||
completion: 0,
|
||||
total: 260,
|
||||
},
|
||||
model: 'text-embedding-3-small',
|
||||
pricing: {
|
||||
input: 0.02,
|
||||
output: 0,
|
||||
updatedAt: '2025-07-10',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const result = await knowledgeUploadChunkTool.transformResponse!(
|
||||
createMockResponse(apiResponse)
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.cost).toEqual({
|
||||
input: 0.00000521,
|
||||
output: 0,
|
||||
total: 0.00000521,
|
||||
})
|
||||
expect(result.output.tokens).toEqual({
|
||||
prompt: 260,
|
||||
completion: 0,
|
||||
total: 260,
|
||||
})
|
||||
expect(result.output.model).toBe('text-embedding-3-small')
|
||||
expect(result.output.data.chunkId).toBe('chunk-123')
|
||||
expect(result.output.documentId).toBe('doc-456')
|
||||
})
|
||||
|
||||
it('should handle response without cost information', async () => {
|
||||
const apiResponse = {
|
||||
data: {
|
||||
id: 'chunk-123',
|
||||
chunkIndex: 0,
|
||||
content: 'test content',
|
||||
documentId: 'doc-456',
|
||||
documentName: 'Test Document',
|
||||
},
|
||||
}
|
||||
|
||||
const result = await knowledgeUploadChunkTool.transformResponse!(
|
||||
createMockResponse(apiResponse)
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.cost).toBeUndefined()
|
||||
expect(result.output.tokens).toBeUndefined()
|
||||
expect(result.output.model).toBeUndefined()
|
||||
expect(result.output.data.chunkId).toBe('chunk-123')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { KnowledgeListChunksResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeListChunksTool: ToolConfig<any, KnowledgeListChunksResponse> = {
|
||||
id: 'knowledge_list_chunks',
|
||||
name: 'Knowledge List Chunks',
|
||||
description:
|
||||
'List chunks for a document in a knowledge base with optional filtering and pagination',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document to list chunks from',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query to filter chunks by content',
|
||||
},
|
||||
enabled: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by enabled status: "true", "false", or "all" (default: "all")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of chunks to return (1-100, default: 50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of chunks to skip for pagination (default: 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.search) queryParams.set('search', params.search)
|
||||
if (params.enabled) queryParams.set('enabled', params.enabled)
|
||||
if (params.limit)
|
||||
queryParams.set('limit', String(Math.max(1, Math.min(100, Number(params.limit)))))
|
||||
if (params.offset != null) queryParams.set('offset', String(params.offset))
|
||||
const qs = queryParams.toString()
|
||||
return `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeListChunksResponse> => {
|
||||
const result = await response.json()
|
||||
const chunks = result.data || []
|
||||
const pagination = result.pagination || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
knowledgeBaseId: params?.knowledgeBaseId ?? '',
|
||||
documentId: params?.documentId ?? '',
|
||||
chunks: chunks.map(
|
||||
(chunk: {
|
||||
id: string
|
||||
chunkIndex: number
|
||||
content: string
|
||||
contentLength: number
|
||||
tokenCount: number
|
||||
enabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}) => ({
|
||||
id: chunk.id,
|
||||
chunkIndex: chunk.chunkIndex ?? 0,
|
||||
content: chunk.content,
|
||||
contentLength: chunk.contentLength ?? 0,
|
||||
tokenCount: chunk.tokenCount ?? 0,
|
||||
enabled: chunk.enabled ?? true,
|
||||
createdAt: chunk.createdAt ?? null,
|
||||
updatedAt: chunk.updatedAt ?? null,
|
||||
})
|
||||
),
|
||||
totalChunks: pagination.total ?? chunks.length,
|
||||
limit: pagination.limit ?? 50,
|
||||
offset: pagination.offset ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the document',
|
||||
},
|
||||
chunks: {
|
||||
type: 'array',
|
||||
description: 'Array of chunks in the document',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Chunk ID' },
|
||||
chunkIndex: { type: 'number', description: 'Index of the chunk within the document' },
|
||||
content: { type: 'string', description: 'Chunk text content' },
|
||||
contentLength: { type: 'number', description: 'Content length in characters' },
|
||||
tokenCount: { type: 'number', description: 'Token count for the chunk' },
|
||||
enabled: { type: 'boolean', description: 'Whether the chunk is enabled' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalChunks: {
|
||||
type: 'number',
|
||||
description: 'Total number of chunks matching the filter',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Page size used',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Offset used for pagination',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { KnowledgeListConnectorsResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeListConnectorsTool: ToolConfig<any, KnowledgeListConnectorsResponse> = {
|
||||
id: 'knowledge_list_connectors',
|
||||
name: 'Knowledge List Connectors',
|
||||
description:
|
||||
'List all connectors for a knowledge base, showing sync status, type, and document counts',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base to list connectors for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/connectors`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeListConnectorsResponse> => {
|
||||
const result = await response.json()
|
||||
const connectors = result.data || []
|
||||
|
||||
return {
|
||||
success: result.success ?? true,
|
||||
output: {
|
||||
knowledgeBaseId: params?.knowledgeBaseId ?? '',
|
||||
connectors: connectors.map(
|
||||
(c: {
|
||||
id: string
|
||||
connectorType: string
|
||||
status: string
|
||||
syncIntervalMinutes: number
|
||||
lastSyncAt: string | null
|
||||
lastSyncError: string | null
|
||||
lastSyncDocCount: number | null
|
||||
nextSyncAt: string | null
|
||||
consecutiveFailures: number
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}) => ({
|
||||
id: c.id,
|
||||
connectorType: c.connectorType,
|
||||
status: c.status,
|
||||
syncIntervalMinutes: c.syncIntervalMinutes,
|
||||
lastSyncAt: c.lastSyncAt ?? null,
|
||||
lastSyncError: c.lastSyncError ?? null,
|
||||
lastSyncDocCount: c.lastSyncDocCount ?? null,
|
||||
nextSyncAt: c.nextSyncAt ?? null,
|
||||
consecutiveFailures: c.consecutiveFailures ?? 0,
|
||||
createdAt: c.createdAt ?? null,
|
||||
updatedAt: c.updatedAt ?? null,
|
||||
})
|
||||
),
|
||||
totalConnectors: connectors.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
connectors: {
|
||||
type: 'array',
|
||||
description: 'Array of connectors for the knowledge base',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Connector ID' },
|
||||
connectorType: {
|
||||
type: 'string',
|
||||
description: 'Type of connector (e.g. notion, github, confluence)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Connector status (active, paused, syncing)',
|
||||
},
|
||||
syncIntervalMinutes: {
|
||||
type: 'number',
|
||||
description: 'Sync interval in minutes (0 = manual only)',
|
||||
},
|
||||
lastSyncAt: { type: 'string', description: 'Timestamp of last sync' },
|
||||
lastSyncError: { type: 'string', description: 'Error from last sync if failed' },
|
||||
lastSyncDocCount: {
|
||||
type: 'number',
|
||||
description: 'Number of documents synced in last sync',
|
||||
},
|
||||
nextSyncAt: { type: 'string', description: 'Timestamp of next scheduled sync' },
|
||||
consecutiveFailures: {
|
||||
type: 'number',
|
||||
description: 'Number of consecutive sync failures',
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalConnectors: {
|
||||
type: 'number',
|
||||
description: 'Total number of connectors',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { KnowledgeListDocumentsResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeListDocumentsTool: ToolConfig<any, KnowledgeListDocumentsResponse> = {
|
||||
id: 'knowledge_list_documents',
|
||||
name: 'Knowledge List Documents',
|
||||
description: 'List documents in a knowledge base with optional filtering, search, and pagination',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base to list documents from',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query to filter documents by filename',
|
||||
},
|
||||
enabledFilter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by enabled status: "all", "enabled", or "disabled"',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of documents to return (default: 50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of documents to skip for pagination (default: 0)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.search) queryParams.set('search', params.search)
|
||||
if (params.enabledFilter) queryParams.set('enabledFilter', params.enabledFilter)
|
||||
if (params.limit != null) queryParams.set('limit', String(params.limit))
|
||||
if (params.offset != null) queryParams.set('offset', String(params.offset))
|
||||
const qs = queryParams.toString()
|
||||
return `/api/knowledge/${params.knowledgeBaseId}/documents${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeListDocumentsResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data || {}
|
||||
const documents = data.documents || []
|
||||
const pagination = data.pagination || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
knowledgeBaseId: params?.knowledgeBaseId ?? '',
|
||||
documents: documents.map(
|
||||
(doc: {
|
||||
id: string
|
||||
filename: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
enabled: boolean
|
||||
processingStatus: string
|
||||
chunkCount: number
|
||||
tokenCount: number
|
||||
uploadedAt: string
|
||||
updatedAt: string
|
||||
connectorId: string | null
|
||||
connectorType: string | null
|
||||
sourceUrl: string | null
|
||||
}) => ({
|
||||
id: doc.id,
|
||||
filename: doc.filename,
|
||||
fileSize: doc.fileSize ?? 0,
|
||||
mimeType: doc.mimeType ?? null,
|
||||
enabled: doc.enabled ?? true,
|
||||
processingStatus: doc.processingStatus ?? null,
|
||||
chunkCount: doc.chunkCount ?? 0,
|
||||
tokenCount: doc.tokenCount ?? 0,
|
||||
uploadedAt: doc.uploadedAt ?? null,
|
||||
updatedAt: doc.updatedAt ?? null,
|
||||
connectorId: doc.connectorId ?? null,
|
||||
connectorType: doc.connectorType ?? null,
|
||||
sourceUrl: doc.sourceUrl ?? null,
|
||||
})
|
||||
),
|
||||
totalDocuments: pagination.total ?? documents.length,
|
||||
limit: pagination.limit ?? 50,
|
||||
offset: pagination.offset ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
documents: {
|
||||
type: 'array',
|
||||
description: 'Array of documents in the knowledge base',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Document ID' },
|
||||
filename: { type: 'string', description: 'Document filename' },
|
||||
fileSize: { type: 'number', description: 'File size in bytes' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the document' },
|
||||
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
|
||||
processingStatus: {
|
||||
type: 'string',
|
||||
description: 'Processing status (pending, processing, completed, failed)',
|
||||
},
|
||||
chunkCount: { type: 'number', description: 'Number of chunks in the document' },
|
||||
tokenCount: { type: 'number', description: 'Total token count across chunks' },
|
||||
uploadedAt: { type: 'string', description: 'Upload timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
connectorId: {
|
||||
type: 'string',
|
||||
description: 'Connector ID if document was synced from an external source',
|
||||
},
|
||||
connectorType: {
|
||||
type: 'string',
|
||||
description: 'Connector type (e.g. notion, github, confluence) if synced',
|
||||
},
|
||||
sourceUrl: {
|
||||
type: 'string',
|
||||
description: 'Original URL in the source system if synced from a connector',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalDocuments: {
|
||||
type: 'number',
|
||||
description: 'Total number of documents matching the filter',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Page size used',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Offset used for pagination',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { KnowledgeListTagsResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeListTagsTool: ToolConfig<any, KnowledgeListTagsResponse> = {
|
||||
id: 'knowledge_list_tags',
|
||||
name: 'Knowledge List Tags',
|
||||
description: 'List all tag definitions for a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base to list tags for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/tag-definitions`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeListTagsResponse> => {
|
||||
const result = await response.json()
|
||||
const tags = result.data || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
knowledgeBaseId: params?.knowledgeBaseId ?? '',
|
||||
tags: tags.map(
|
||||
(tag: {
|
||||
id: string
|
||||
tagSlot: string
|
||||
displayName: string
|
||||
fieldType: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}) => ({
|
||||
id: tag.id,
|
||||
tagSlot: tag.tagSlot,
|
||||
displayName: tag.displayName,
|
||||
fieldType: tag.fieldType,
|
||||
createdAt: tag.createdAt ?? null,
|
||||
updatedAt: tag.updatedAt ?? null,
|
||||
})
|
||||
),
|
||||
totalTags: tags.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Array of tag definitions for the knowledge base',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Tag definition ID' },
|
||||
tagSlot: { type: 'string', description: 'Internal tag slot (e.g. tag1, number1)' },
|
||||
displayName: { type: 'string', description: 'Human-readable tag name' },
|
||||
fieldType: {
|
||||
type: 'string',
|
||||
description: 'Tag field type (text, number, date, boolean)',
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalTags: {
|
||||
type: 'number',
|
||||
description: 'Total number of tag definitions',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { DEFAULT_RERANKER_MODEL } from '@/lib/knowledge/reranker-models'
|
||||
import type { KnowledgeSearchResponse } from '@/tools/knowledge/types'
|
||||
import { enrichKBTagFiltersSchema } from '@/tools/schema-enrichers'
|
||||
import { parseTagFilters } from '@/tools/shared/tags'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
id: 'knowledge_search',
|
||||
name: 'Knowledge Search',
|
||||
description: 'Search for similar content in a knowledge base using vector similarity',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base to search in',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query text (optional when using tag filters)',
|
||||
},
|
||||
topK: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of most similar results to return (1-100)',
|
||||
},
|
||||
tagFilters: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of tag filters with tagName and tagValue properties',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tagName: { type: 'string' },
|
||||
tagValue: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
rerankerEnabled: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Whether to apply Cohere reranking to vector search results',
|
||||
},
|
||||
rerankerModel: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Cohere rerank model to use (one of: rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5)',
|
||||
},
|
||||
rerankerInputCount: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Number of vector results sent to the Cohere reranker (1–100). Defaults to topK × 4 capped at 100.',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cohere API key for reranker (self-hosted deployments only)',
|
||||
},
|
||||
},
|
||||
|
||||
schemaEnrichment: {
|
||||
tagFilters: {
|
||||
dependsOn: 'knowledgeBaseId',
|
||||
enrichSchema: enrichKBTagFiltersSchema,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/knowledge/search',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const workflowId = params._context?.workflowId
|
||||
|
||||
// Use single knowledge base ID
|
||||
const knowledgeBaseIds = [params.knowledgeBaseId]
|
||||
|
||||
// Parse tag filters from various formats (array, JSON string)
|
||||
const structuredFilters = parseTagFilters(params.tagFilters)
|
||||
|
||||
const rerankerEnabled = params.rerankerEnabled === true || params.rerankerEnabled === 'true'
|
||||
const rerankerModel =
|
||||
typeof params.rerankerModel === 'string' && params.rerankerModel.length > 0
|
||||
? params.rerankerModel
|
||||
: DEFAULT_RERANKER_MODEL
|
||||
const rerankerApiKey =
|
||||
typeof params.apiKey === 'string' && params.apiKey.length > 0 ? params.apiKey : undefined
|
||||
const rawInputCount =
|
||||
params.rerankerInputCount !== undefined &&
|
||||
params.rerankerInputCount !== null &&
|
||||
params.rerankerInputCount !== ''
|
||||
? Number(params.rerankerInputCount)
|
||||
: Number.NaN
|
||||
const rerankerInputCount = Number.isFinite(rawInputCount)
|
||||
? Math.max(1, Math.min(100, Math.floor(rawInputCount)))
|
||||
: undefined
|
||||
|
||||
const requestBody = {
|
||||
knowledgeBaseIds,
|
||||
query: params.query,
|
||||
topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10,
|
||||
...(structuredFilters.length > 0 && { tagFilters: structuredFilters }),
|
||||
...(rerankerEnabled && {
|
||||
rerankerEnabled: true,
|
||||
rerankerModel,
|
||||
...(rerankerInputCount !== undefined && { rerankerInputCount }),
|
||||
...(rerankerApiKey && { rerankerApiKey }),
|
||||
}),
|
||||
...(workflowId && { workflowId }),
|
||||
// The executor rolls this search's cost up at workflow completion, so
|
||||
// tell the route not to also meter it (avoids double-billing).
|
||||
skipUsageBilling: true,
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
transformResponse: async (response): Promise<KnowledgeSearchResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
|
||||
// Restructure cost: extract tokens/model to top level for logging
|
||||
let costFields: Record<string, unknown> = {}
|
||||
if (data.cost && typeof data.cost === 'object') {
|
||||
const {
|
||||
tokens,
|
||||
model,
|
||||
input,
|
||||
output: outputCost,
|
||||
total,
|
||||
rerankerCost,
|
||||
rerankerModel,
|
||||
rerankerSearchUnits,
|
||||
} = data.cost
|
||||
costFields = {
|
||||
cost: {
|
||||
input,
|
||||
output: outputCost,
|
||||
total,
|
||||
...(typeof rerankerCost === 'number' && { rerankerCost }),
|
||||
...(typeof rerankerModel === 'string' && { rerankerModel }),
|
||||
...(typeof rerankerSearchUnits === 'number' && { rerankerSearchUnits }),
|
||||
},
|
||||
...(tokens && { tokens }),
|
||||
...(model && { model }),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: data.results || [],
|
||||
query: data.query,
|
||||
totalResults: data.totalResults || 0,
|
||||
...costFields,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search results from the knowledge base',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Document ID' },
|
||||
documentName: { type: 'string', description: 'Document name' },
|
||||
sourceUrl: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description:
|
||||
'URL to the original source document (e.g., Confluence page, Google Doc, Notion page). Null for documents without an external source.',
|
||||
},
|
||||
content: { type: 'string', description: 'Content of the result' },
|
||||
chunkIndex: { type: 'number', description: 'Index of the chunk within the document' },
|
||||
similarity: { type: 'number', description: 'Similarity score of the result' },
|
||||
metadata: { type: 'object', description: 'Metadata of the result, including tags' },
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query that was executed',
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of results found',
|
||||
},
|
||||
cost: {
|
||||
type: 'object',
|
||||
description: 'Cost information for the search operation',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { KnowledgeTriggerSyncResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeTriggerSyncTool: ToolConfig<any, KnowledgeTriggerSyncResponse> = {
|
||||
id: 'knowledge_trigger_sync',
|
||||
name: 'Knowledge Trigger Sync',
|
||||
description: 'Trigger a manual sync for a knowledge base connector',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base the connector belongs to',
|
||||
},
|
||||
connectorId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the connector to trigger sync for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/connectors/${params.connectorId}/sync`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeTriggerSyncResponse> => {
|
||||
const result = await response.json()
|
||||
|
||||
return {
|
||||
success: result.success ?? true,
|
||||
output: {
|
||||
connectorId: params?.connectorId ?? '',
|
||||
message: result.message ?? 'Sync triggered',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
connectorId: {
|
||||
type: 'string',
|
||||
description: 'ID of the connector that was synced',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Status message from the sync trigger',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
getFileExtension,
|
||||
getMimeTypeFromExtension as getUploadMimeType,
|
||||
} from '@/lib/uploads/utils/file-utils'
|
||||
|
||||
const TEXT_COMPATIBLE_MIME_TYPES = new Set([
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'text/markdown',
|
||||
'text/csv',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'application/x-yaml',
|
||||
])
|
||||
|
||||
/**
|
||||
* Extracts extension from a filename and returns the normalized filename and MIME type.
|
||||
* If the extension maps to a recognized text-compatible MIME type, it is preserved.
|
||||
* Otherwise, the filename is normalized to `.txt` with `text/plain`.
|
||||
*/
|
||||
export function inferDocumentFileInfo(documentName: string): {
|
||||
filename: string
|
||||
mimeType: string
|
||||
} {
|
||||
const ext = getFileExtension(documentName)
|
||||
if (ext) {
|
||||
const mimeType = getUploadMimeType(ext)
|
||||
if (TEXT_COMPATIBLE_MIME_TYPES.has(mimeType)) {
|
||||
return { filename: documentName, mimeType }
|
||||
}
|
||||
}
|
||||
const base = ext ? documentName.slice(0, documentName.lastIndexOf('.')) : documentName
|
||||
return { filename: `${base || documentName}.txt`, mimeType: 'text/plain' }
|
||||
}
|
||||
|
||||
interface KnowledgeSearchResult {
|
||||
documentId: string
|
||||
documentName: string
|
||||
sourceUrl: string | null
|
||||
content: string
|
||||
chunkIndex: number
|
||||
metadata: Record<string, any>
|
||||
similarity: number
|
||||
rerankerScore?: number
|
||||
}
|
||||
|
||||
export interface KnowledgeSearchResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
results: KnowledgeSearchResult[]
|
||||
query: string
|
||||
totalResults: number
|
||||
cost?: {
|
||||
input: number
|
||||
output: number
|
||||
total: number
|
||||
rerankerCost?: number
|
||||
rerankerModel?: string
|
||||
rerankerSearchUnits?: number
|
||||
}
|
||||
tokens?: {
|
||||
prompt: number
|
||||
completion: number
|
||||
total: number
|
||||
}
|
||||
model?: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeSearchParams {
|
||||
knowledgeBaseIds: string | string[]
|
||||
query: string
|
||||
topK?: number
|
||||
}
|
||||
|
||||
interface KnowledgeUploadChunkResult {
|
||||
chunkId: string
|
||||
chunkIndex: number
|
||||
content: string
|
||||
contentLength: number
|
||||
tokenCount: number
|
||||
enabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface KnowledgeUploadChunkResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
data: KnowledgeUploadChunkResult
|
||||
message: string
|
||||
documentId: string
|
||||
documentName: string
|
||||
cost?: {
|
||||
input: number
|
||||
output: number
|
||||
total: number
|
||||
tokens: {
|
||||
prompt: number
|
||||
completion: number
|
||||
total: number
|
||||
}
|
||||
model: string
|
||||
pricing: {
|
||||
input: number
|
||||
output: number
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeUploadChunkParams {
|
||||
documentId: string
|
||||
content: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
interface KnowledgeCreateDocumentResult {
|
||||
documentId: string
|
||||
documentName: string
|
||||
type: string
|
||||
enabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface KnowledgeCreateDocumentResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
data: KnowledgeCreateDocumentResult
|
||||
message: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeTagDefinition {
|
||||
id: string
|
||||
tagSlot: string
|
||||
displayName: string
|
||||
fieldType: string
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
interface KnowledgeListTagsParams {
|
||||
knowledgeBaseId: string
|
||||
}
|
||||
|
||||
export interface KnowledgeListTagsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
knowledgeBaseId: string
|
||||
tags: KnowledgeTagDefinition[]
|
||||
totalTags: number
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeDocumentSummary {
|
||||
id: string
|
||||
filename: string
|
||||
fileSize: number
|
||||
mimeType: string | null
|
||||
enabled: boolean
|
||||
processingStatus: string | null
|
||||
chunkCount: number
|
||||
tokenCount: number
|
||||
uploadedAt: string | null
|
||||
updatedAt: string | null
|
||||
connectorId: string | null
|
||||
connectorType: string | null
|
||||
sourceUrl: string | null
|
||||
}
|
||||
|
||||
export interface KnowledgeListDocumentsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
knowledgeBaseId: string
|
||||
documents: KnowledgeDocumentSummary[]
|
||||
totalDocuments: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface KnowledgeDeleteDocumentResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
documentId: string
|
||||
message: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeChunkSummary {
|
||||
id: string
|
||||
chunkIndex: number
|
||||
content: string
|
||||
contentLength: number
|
||||
tokenCount: number
|
||||
enabled: boolean
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface KnowledgeListChunksResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
knowledgeBaseId: string
|
||||
documentId: string
|
||||
chunks: KnowledgeChunkSummary[]
|
||||
totalChunks: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface KnowledgeUpdateChunkResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
documentId: string
|
||||
id: string
|
||||
chunkIndex: number
|
||||
content: string
|
||||
contentLength: number
|
||||
tokenCount: number
|
||||
enabled: boolean
|
||||
updatedAt: string | null
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface KnowledgeDeleteChunkResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
chunkId: string
|
||||
documentId: string
|
||||
message: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface KnowledgeGetDocumentResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
filename: string
|
||||
fileSize: number
|
||||
mimeType: string | null
|
||||
enabled: boolean
|
||||
processingStatus: string | null
|
||||
processingError: string | null
|
||||
chunkCount: number
|
||||
tokenCount: number
|
||||
characterCount: number
|
||||
uploadedAt: string | null
|
||||
updatedAt: string | null
|
||||
connectorId: string | null
|
||||
sourceUrl: string | null
|
||||
externalId: string | null
|
||||
tags: Record<string, unknown>
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeConnectorSummary {
|
||||
id: string
|
||||
connectorType: string
|
||||
status: string
|
||||
syncIntervalMinutes: number
|
||||
lastSyncAt: string | null
|
||||
lastSyncError: string | null
|
||||
lastSyncDocCount: number | null
|
||||
nextSyncAt: string | null
|
||||
consecutiveFailures: number
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface KnowledgeListConnectorsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
knowledgeBaseId: string
|
||||
connectors: KnowledgeConnectorSummary[]
|
||||
totalConnectors: number
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface KnowledgeSyncLogEntry {
|
||||
id: string
|
||||
status: string
|
||||
startedAt: string | null
|
||||
completedAt: string | null
|
||||
docsAdded: number | null
|
||||
docsUpdated: number | null
|
||||
docsDeleted: number | null
|
||||
docsUnchanged: number | null
|
||||
errorMessage: string | null
|
||||
}
|
||||
|
||||
export interface KnowledgeGetConnectorResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
connector: KnowledgeConnectorSummary
|
||||
syncLogs: KnowledgeSyncLogEntry[]
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface KnowledgeTriggerSyncResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
connectorId: string
|
||||
message: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface KnowledgeUpsertDocumentParams {
|
||||
knowledgeBaseId: string
|
||||
name: string
|
||||
content: string
|
||||
documentId?: string
|
||||
documentTags?: Record<string, unknown>
|
||||
_context?: { workflowId?: string }
|
||||
}
|
||||
|
||||
interface KnowledgeUpsertDocumentResult {
|
||||
documentId: string
|
||||
documentName: string
|
||||
type: string
|
||||
enabled: boolean
|
||||
isUpdate: boolean
|
||||
previousDocumentId: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface KnowledgeUpsertDocumentResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
data: KnowledgeUpsertDocumentResult
|
||||
message: string
|
||||
documentId: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { KnowledgeUpdateChunkResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeUpdateChunkTool: ToolConfig<any, KnowledgeUpdateChunkResponse> = {
|
||||
id: 'knowledge_update_chunk',
|
||||
name: 'Knowledge Update Chunk',
|
||||
description: 'Update the content or enabled status of a chunk in a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document containing the chunk',
|
||||
},
|
||||
chunkId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the chunk to update',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New content for the chunk',
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the chunk should be enabled or disabled',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`,
|
||||
method: 'PUT',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.content !== undefined) body.content = params.content
|
||||
if (params.enabled !== undefined) body.enabled = params.enabled
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<KnowledgeUpdateChunkResponse> => {
|
||||
const result = await response.json()
|
||||
const chunk = result.data || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
documentId: params?.documentId ?? '',
|
||||
id: chunk.id ?? '',
|
||||
chunkIndex: chunk.chunkIndex ?? 0,
|
||||
content: chunk.content ?? '',
|
||||
contentLength: chunk.contentLength ?? 0,
|
||||
tokenCount: chunk.tokenCount ?? 0,
|
||||
enabled: chunk.enabled ?? true,
|
||||
updatedAt: chunk.updatedAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the parent document',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Chunk ID',
|
||||
},
|
||||
chunkIndex: {
|
||||
type: 'number',
|
||||
description: 'Index of the chunk within the document',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Updated chunk content',
|
||||
},
|
||||
contentLength: {
|
||||
type: 'number',
|
||||
description: 'Content length in characters',
|
||||
},
|
||||
tokenCount: {
|
||||
type: 'number',
|
||||
description: 'Token count for the chunk',
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the chunk is enabled',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
description: 'Last update timestamp',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { KnowledgeUploadChunkResponse } from '@/tools/knowledge/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkResponse> = {
|
||||
id: 'knowledge_upload_chunk',
|
||||
name: 'Knowledge Upload Chunk',
|
||||
description: 'Upload a new chunk to a document in a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document to upload the chunk to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content of the chunk to upload',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const workflowId = params._context?.workflowId
|
||||
|
||||
const requestBody = {
|
||||
content: params.content,
|
||||
enabled: true,
|
||||
...(workflowId && { workflowId }),
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeUploadChunkResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
|
||||
// Restructure cost: extract tokens/model to top level for logging
|
||||
let costFields: Record<string, unknown> = {}
|
||||
if (data.cost && typeof data.cost === 'object') {
|
||||
const { tokens, model, input, output: outputCost, total } = data.cost
|
||||
costFields = {
|
||||
cost: { input, output: outputCost, total },
|
||||
...(tokens && { tokens }),
|
||||
...(model && { model }),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: `Successfully uploaded chunk to document`,
|
||||
data: {
|
||||
chunkId: data.id,
|
||||
chunkIndex: data.chunkIndex || 0,
|
||||
content: data.content,
|
||||
contentLength: data.contentLength || data.content?.length || 0,
|
||||
tokenCount: data.tokenCount || 0,
|
||||
enabled: data.enabled !== undefined ? data.enabled : true,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
},
|
||||
documentId: data.documentId,
|
||||
documentName: data.documentName,
|
||||
...costFields,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Information about the uploaded chunk',
|
||||
properties: {
|
||||
chunkId: { type: 'string', description: 'Chunk ID' },
|
||||
chunkIndex: { type: 'number', description: 'Index of the chunk within the document' },
|
||||
content: { type: 'string', description: 'Content of the chunk' },
|
||||
contentLength: { type: 'number', description: 'Length of the content in characters' },
|
||||
tokenCount: { type: 'number', description: 'Number of tokens in the chunk' },
|
||||
enabled: { type: 'boolean', description: 'Whether the chunk is enabled' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Success or error message describing the operation result',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the document the chunk was added to',
|
||||
},
|
||||
documentName: {
|
||||
type: 'string',
|
||||
description: 'Name of the document the chunk was added to',
|
||||
},
|
||||
cost: {
|
||||
type: 'object',
|
||||
description: 'Cost information for the upload operation',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
inferDocumentFileInfo,
|
||||
type KnowledgeUpsertDocumentParams,
|
||||
type KnowledgeUpsertDocumentResponse,
|
||||
} from '@/tools/knowledge/types'
|
||||
import { enrichKBTagsSchema } from '@/tools/schema-enrichers'
|
||||
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeUpsertDocumentTool: ToolConfig<
|
||||
KnowledgeUpsertDocumentParams,
|
||||
KnowledgeUpsertDocumentResponse
|
||||
> = {
|
||||
id: 'knowledge_upsert_document',
|
||||
name: 'Knowledge Upsert Document',
|
||||
description:
|
||||
'Create or update a document in a knowledge base. If a document with the given ID or filename already exists, it will be replaced with the new content.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Optional ID of an existing document to update. If not provided, lookup is done by filename.',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the document',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content of the document',
|
||||
},
|
||||
documentTags: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document tags',
|
||||
},
|
||||
},
|
||||
|
||||
schemaEnrichment: {
|
||||
documentTags: {
|
||||
dependsOn: 'knowledgeBaseId',
|
||||
enrichSchema: enrichKBTagsSchema,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/upsert`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const workflowId = params._context?.workflowId
|
||||
const textContent = params.content?.trim()
|
||||
const documentName = params.name?.trim()
|
||||
|
||||
if (!documentName || documentName.length === 0) {
|
||||
throw new Error('Document name is required')
|
||||
}
|
||||
if (documentName.length > 255) {
|
||||
throw new Error('Document name must be 255 characters or less')
|
||||
}
|
||||
if (!textContent || textContent.length < 1) {
|
||||
throw new Error('Document content cannot be empty')
|
||||
}
|
||||
const utf8Bytes = new TextEncoder().encode(textContent)
|
||||
const contentBytes = utf8Bytes.length
|
||||
|
||||
if (contentBytes > 1_000_000) {
|
||||
throw new Error('Document content exceeds maximum size of 1MB')
|
||||
}
|
||||
let base64Content: string
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
base64Content = Buffer.from(textContent, 'utf8').toString('base64')
|
||||
} else {
|
||||
let binary = ''
|
||||
for (let i = 0; i < utf8Bytes.length; i++) {
|
||||
binary += String.fromCharCode(utf8Bytes[i])
|
||||
}
|
||||
base64Content = btoa(binary)
|
||||
}
|
||||
|
||||
const { filename, mimeType } = inferDocumentFileInfo(documentName)
|
||||
const dataUri = `data:${mimeType};base64,${base64Content}`
|
||||
|
||||
const parsedTags = parseDocumentTags(params.documentTags)
|
||||
const tagData = formatDocumentTagsForAPI(parsedTags)
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
filename,
|
||||
fileUrl: dataUri,
|
||||
fileSize: contentBytes,
|
||||
mimeType,
|
||||
...tagData,
|
||||
processingOptions: {
|
||||
recipe: 'default',
|
||||
lang: 'en',
|
||||
},
|
||||
...(workflowId && { workflowId }),
|
||||
}
|
||||
|
||||
if (params.documentId && String(params.documentId).trim().length > 0) {
|
||||
requestBody.documentId = String(params.documentId).trim()
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeUpsertDocumentResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data ?? result
|
||||
const documentsCreated = data.documentsCreated ?? []
|
||||
const firstDocument = documentsCreated[0]
|
||||
const isUpdate = data.isUpdate ?? false
|
||||
const previousDocumentId = data.previousDocumentId ?? null
|
||||
const documentId = firstDocument?.documentId ?? firstDocument?.id ?? ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: isUpdate
|
||||
? 'Successfully updated document in knowledge base'
|
||||
: 'Successfully created document in knowledge base',
|
||||
documentId,
|
||||
data: {
|
||||
documentId,
|
||||
documentName: firstDocument?.filename ?? 'Unknown',
|
||||
type: 'document',
|
||||
enabled: true,
|
||||
isUpdate,
|
||||
previousDocumentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Information about the upserted document',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Document ID' },
|
||||
documentName: { type: 'string', description: 'Document name' },
|
||||
type: { type: 'string', description: 'Document type' },
|
||||
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
|
||||
isUpdate: {
|
||||
type: 'boolean',
|
||||
description: 'Whether an existing document was replaced',
|
||||
},
|
||||
previousDocumentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the document that was replaced, if any',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Success or error message describing the operation result',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the upserted document',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user