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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+168
View File
@@ -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',
},
},
}
+67
View File
@@ -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',
},
},
}
+118
View File
@@ -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' },
},
},
},
},
}
+123
View File
@@ -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)',
},
},
}
+31
View File
@@ -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,
}
+202
View File
@@ -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')
})
})
})
})
+144
View File
@@ -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',
},
},
}
+113
View File
@@ -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',
},
},
}
+159
View File
@@ -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',
},
},
}
+85
View File
@@ -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',
},
},
}
+210
View File
@@ -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 (1100). 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,
},
},
}
+56
View File
@@ -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',
},
},
}
+353
View File
@@ -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
}
+112
View File
@@ -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,
},
},
}
+120
View File
@@ -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,
},
},
}
+186
View File
@@ -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',
},
},
}