Skip to content

Commit a5b148e

Browse files
committed
Native kb connectors
1 parent 9665f49 commit a5b148e

File tree

6 files changed

+472
-5
lines changed

6 files changed

+472
-5
lines changed

apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { db } from '@sim/db'
2+
import { knowledgeConnector } from '@sim/db/schema'
13
import { createLogger } from '@sim/logger'
4+
import { and, eq, isNull } from 'drizzle-orm'
5+
import { generateInternalToken } from '@/lib/auth/internal'
26
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
37
import type { KnowledgeBaseArgs, KnowledgeBaseResult } from '@/lib/copilot/tools/shared/schemas'
8+
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
49
import { createSingleDocument, processDocumentAsync } from '@/lib/knowledge/documents/service'
510
import { generateSearchEmbedding } from '@/lib/knowledge/embeddings'
611
import {
@@ -543,10 +548,179 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
543548
}
544549
}
545550

551+
case 'add_connector': {
552+
if (!args.knowledgeBaseId) {
553+
return { success: false, message: 'Knowledge base ID is required for add_connector' }
554+
}
555+
if (!args.connectorType) {
556+
return { success: false, message: 'connectorType is required for add_connector' }
557+
}
558+
if (!args.credentialId) {
559+
return {
560+
success: false,
561+
message:
562+
'credentialId is required for add_connector. Read environment/credentials.json to find credential IDs.',
563+
}
564+
}
565+
566+
const createBody: Record<string, unknown> = {
567+
connectorType: args.connectorType,
568+
credentialId: args.credentialId,
569+
sourceConfig: args.sourceConfig ?? {},
570+
syncIntervalMinutes: args.syncIntervalMinutes ?? 1440,
571+
}
572+
573+
if (args.disabledTagIds?.length) {
574+
;(createBody.sourceConfig as Record<string, unknown>).disabledTagIds =
575+
args.disabledTagIds
576+
}
577+
578+
const createRes = await connectorApiCall(
579+
context.userId,
580+
`/api/knowledge/${args.knowledgeBaseId}/connectors`,
581+
'POST',
582+
createBody
583+
)
584+
585+
if (!createRes.success) {
586+
return { success: false, message: createRes.error }
587+
}
588+
589+
const connector = createRes.data
590+
logger.info('Connector created via copilot', {
591+
connectorId: connector.id,
592+
connectorType: args.connectorType,
593+
knowledgeBaseId: args.knowledgeBaseId,
594+
userId: context.userId,
595+
})
596+
597+
return {
598+
success: true,
599+
message: `Connector "${args.connectorType}" added to knowledge base. Initial sync started.`,
600+
data: {
601+
id: connector.id,
602+
connectorType: connector.connectorType ?? connector.connector_type,
603+
status: connector.status,
604+
knowledgeBaseId: args.knowledgeBaseId,
605+
},
606+
}
607+
}
608+
609+
case 'update_connector': {
610+
if (!args.connectorId) {
611+
return { success: false, message: 'connectorId is required for update_connector' }
612+
}
613+
614+
const kbId = await resolveKnowledgeBaseId(args.connectorId)
615+
if (!kbId) {
616+
return { success: false, message: `Connector "${args.connectorId}" not found` }
617+
}
618+
619+
const updateBody: Record<string, unknown> = {}
620+
if (args.sourceConfig !== undefined) updateBody.sourceConfig = args.sourceConfig
621+
if (args.syncIntervalMinutes !== undefined)
622+
updateBody.syncIntervalMinutes = args.syncIntervalMinutes
623+
if (args.connectorStatus !== undefined) updateBody.status = args.connectorStatus
624+
625+
if (Object.keys(updateBody).length === 0) {
626+
return {
627+
success: false,
628+
message:
629+
'At least one of sourceConfig, syncIntervalMinutes, or connectorStatus is required',
630+
}
631+
}
632+
633+
const updateRes = await connectorApiCall(
634+
context.userId,
635+
`/api/knowledge/${kbId}/connectors/${args.connectorId}`,
636+
'PATCH',
637+
updateBody
638+
)
639+
640+
if (!updateRes.success) {
641+
return { success: false, message: updateRes.error }
642+
}
643+
644+
logger.info('Connector updated via copilot', {
645+
connectorId: args.connectorId,
646+
userId: context.userId,
647+
})
648+
649+
return {
650+
success: true,
651+
message: 'Connector updated successfully',
652+
data: { id: args.connectorId, ...updateBody },
653+
}
654+
}
655+
656+
case 'delete_connector': {
657+
if (!args.connectorId) {
658+
return { success: false, message: 'connectorId is required for delete_connector' }
659+
}
660+
661+
const deleteKbId = await resolveKnowledgeBaseId(args.connectorId)
662+
if (!deleteKbId) {
663+
return { success: false, message: `Connector "${args.connectorId}" not found` }
664+
}
665+
666+
const deleteRes = await connectorApiCall(
667+
context.userId,
668+
`/api/knowledge/${deleteKbId}/connectors/${args.connectorId}`,
669+
'DELETE'
670+
)
671+
672+
if (!deleteRes.success) {
673+
return { success: false, message: deleteRes.error }
674+
}
675+
676+
logger.info('Connector deleted via copilot', {
677+
connectorId: args.connectorId,
678+
userId: context.userId,
679+
})
680+
681+
return {
682+
success: true,
683+
message: 'Connector deleted successfully. Associated documents have been removed.',
684+
data: { id: args.connectorId },
685+
}
686+
}
687+
688+
case 'sync_connector': {
689+
if (!args.connectorId) {
690+
return { success: false, message: 'connectorId is required for sync_connector' }
691+
}
692+
693+
const syncKbId = await resolveKnowledgeBaseId(args.connectorId)
694+
if (!syncKbId) {
695+
return { success: false, message: `Connector "${args.connectorId}" not found` }
696+
}
697+
698+
const syncRes = await connectorApiCall(
699+
context.userId,
700+
`/api/knowledge/${syncKbId}/connectors/${args.connectorId}/sync`,
701+
'POST'
702+
)
703+
704+
if (!syncRes.success) {
705+
return { success: false, message: syncRes.error }
706+
}
707+
708+
logger.info('Connector sync triggered via copilot', {
709+
connectorId: args.connectorId,
710+
userId: context.userId,
711+
})
712+
713+
return {
714+
success: true,
715+
message: 'Sync triggered. Documents will be updated in the background.',
716+
data: { id: args.connectorId },
717+
}
718+
}
719+
546720
default:
547721
return {
548722
success: false,
549-
message: `Unknown operation: ${operation}. Supported operations: create, get, query, add_file, update, delete, list_tags, create_tag, update_tag, delete_tag, get_tag_usage`,
723+
message: `Unknown operation: ${operation}. Supported operations: create, get, query, add_file, update, delete, list_tags, create_tag, update_tag, delete_tag, get_tag_usage, add_connector, update_connector, delete_connector, sync_connector`,
550724
}
551725
}
552726
} catch (error) {
@@ -564,3 +738,43 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
564738
}
565739
},
566740
}
741+
742+
async function connectorApiCall(
743+
userId: string,
744+
path: string,
745+
method: string,
746+
body?: Record<string, unknown>
747+
): Promise<{ success: boolean; data?: any; error?: string }> {
748+
const token = await generateInternalToken(userId)
749+
const baseUrl = getInternalApiBaseUrl()
750+
751+
const res = await fetch(`${baseUrl}${path}`, {
752+
method,
753+
headers: {
754+
'Content-Type': 'application/json',
755+
Authorization: `Bearer ${token}`,
756+
},
757+
...(body ? { body: JSON.stringify(body) } : {}),
758+
})
759+
760+
const json = await res.json().catch(() => ({}))
761+
762+
if (!res.ok) {
763+
return {
764+
success: false,
765+
error: json.error || `API returned ${res.status}`,
766+
}
767+
}
768+
769+
return { success: true, data: json.data }
770+
}
771+
772+
async function resolveKnowledgeBaseId(connectorId: string): Promise<string | null> {
773+
const rows = await db
774+
.select({ knowledgeBaseId: knowledgeConnector.knowledgeBaseId })
775+
.from(knowledgeConnector)
776+
.where(and(eq(knowledgeConnector.id, connectorId), isNull(knowledgeConnector.deletedAt)))
777+
.limit(1)
778+
779+
return rows[0]?.knowledgeBaseId ?? null
780+
}

apps/sim/lib/copilot/tools/shared/schemas.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export const KnowledgeBaseArgsSchema = z.object({
3333
'update_tag',
3434
'delete_tag',
3535
'get_tag_usage',
36+
'add_connector',
37+
'update_connector',
38+
'delete_connector',
39+
'sync_connector',
3640
]),
3741
args: z
3842
.object({
@@ -42,7 +46,7 @@ export const KnowledgeBaseArgsSchema = z.object({
4246
description: z.string().optional(),
4347
/** Workspace ID to associate with (required for create, optional for list) */
4448
workspaceId: z.string().optional(),
45-
/** Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage) */
49+
/** Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage, add_connector) */
4650
knowledgeBaseId: z.string().optional(),
4751
/** Workspace file path to add as a document (required for add_file). Example: "files/report.pdf" */
4852
filePath: z.string().optional(),
@@ -64,6 +68,20 @@ export const KnowledgeBaseArgsSchema = z.object({
6468
tagDisplayName: z.string().optional(),
6569
/** Tag field type: text, number, date, boolean (optional for create_tag, defaults to text) */
6670
tagFieldType: z.enum(['text', 'number', 'date', 'boolean']).optional(),
71+
/** Connector type from registry, e.g. "confluence" (required for add_connector) */
72+
connectorType: z.string().optional(),
73+
/** OAuth credential ID from environment/credentials.json (required for add_connector) */
74+
credentialId: z.string().optional(),
75+
/** Connector-specific config matching the schema in knowledgebases/connectors/{type}.json */
76+
sourceConfig: z.record(z.unknown()).optional(),
77+
/** Sync interval: 60, 360, 1440, 10080, or 0 for manual only (optional for add_connector, defaults to 1440) */
78+
syncIntervalMinutes: z.number().int().min(0).optional(),
79+
/** Connector ID (required for update_connector, delete_connector, sync_connector) */
80+
connectorId: z.string().optional(),
81+
/** Connector status: "active" or "paused" (optional for update_connector) */
82+
connectorStatus: z.enum(['active', 'paused']).optional(),
83+
/** Tag definition IDs to disable (optional for add_connector) */
84+
disabledTagIds: z.array(z.string()).optional(),
6785
})
6886
.optional(),
6987
})

0 commit comments

Comments
 (0)