Skip to content

Commit 2f58bb9

Browse files
committed
Skills
1 parent 58de276 commit 2f58bb9

10 files changed

Lines changed: 378 additions & 239 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,16 @@ function isChatContext(value: unknown): value is ChatContext {
370370
return typeof value.fileId === 'string'
371371
case 'folder':
372372
return typeof value.folderId === 'string'
373+
case 'filefolder':
374+
return typeof value.fileFolderId === 'string'
373375
case 'docs':
374376
return true
375377
case 'slash_command':
376378
return typeof value.command === 'string'
379+
case 'integration':
380+
return typeof value.blockType === 'string'
381+
case 'skill':
382+
return typeof value.skillId === 'string'
377383
default:
378384
return false
379385
}

apps/sim/lib/copilot/chat/payload.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface BuildPayloadParams {
2525
mode: string
2626
model: string
2727
provider?: string
28-
contexts?: Array<{ type: string; content: string }>
28+
contexts?: Array<{ type: string; content: string; tag?: string; path?: string }>
2929
fileAttachments?: Array<{ id: string; key: string; size: number; [key: string]: unknown }>
3030
commands?: string[]
3131
chatId?: string
@@ -266,7 +266,7 @@ export async function buildCopilotRequestPayload(
266266
const transportMode = effectiveMode === 'build' ? 'agent' : effectiveMode
267267

268268
// Track uploaded files in the DB and build context tags instead of base64 inlining
269-
const uploadContexts: Array<{ type: string; content: string }> = []
269+
const uploadContexts: Array<{ type: string; content: string; tag?: string; path?: string }> = []
270270
if (chatId && params.workspaceId && fileAttachments && fileAttachments.length > 0) {
271271
for (const f of fileAttachments) {
272272
const filename = (f.filename ?? f.name ?? 'file') as string

apps/sim/lib/copilot/chat/post.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,31 @@ describe('handleUnifiedChatPost', () => {
243243
)
244244
})
245245

246+
it('accepts tagged skill contexts and forwards them to context resolution', async () => {
247+
const response = await handleUnifiedChatPost(
248+
new NextRequest('http://localhost/api/copilot/chat', {
249+
method: 'POST',
250+
body: JSON.stringify({
251+
message: 'Hello',
252+
workspaceId: 'ws-1',
253+
createNewChat: true,
254+
contexts: [{ kind: 'skill', skillId: 'sk-1', label: 'my-skill' }],
255+
}),
256+
})
257+
)
258+
259+
expect(response.status).toBe(200)
260+
expect(processContextsServer).toHaveBeenCalledWith(
261+
expect.arrayContaining([
262+
expect.objectContaining({ kind: 'skill', skillId: 'sk-1', label: 'my-skill' }),
263+
]),
264+
'user-1',
265+
'Hello',
266+
'ws-1',
267+
expect.anything()
268+
)
269+
})
270+
246271
it('persists cancelled partial responses from the server lifecycle', async () => {
247272
await handleUnifiedChatPost(
248273
new NextRequest('http://localhost/api/copilot/chat', {

apps/sim/lib/copilot/chat/post.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const ChatContextSchema = z.object({
109109
'folder',
110110
'filefolder',
111111
'integration',
112+
'skill',
112113
]),
113114
label: z.string(),
114115
chatId: z.string().optional(),
@@ -121,6 +122,7 @@ const ChatContextSchema = z.object({
121122
fileId: z.string().optional(),
122123
folderId: z.string().optional(),
123124
fileFolderId: z.string().optional(),
125+
skillId: z.string().optional(),
124126
})
125127

126128
const ChatMessageSchema = z.object({
@@ -163,7 +165,7 @@ type UnifiedChatBranch =
163165
userId: string
164166
userMessageId: string
165167
chatId?: string
166-
contexts: Array<{ type: string; content: string }>
168+
contexts: Array<{ type: string; content: string; tag?: string; path?: string }>
167169
fileAttachments?: UnifiedChatRequest['fileAttachments']
168170
userPermission?: string
169171
userTimezone?: string
@@ -198,7 +200,7 @@ type UnifiedChatBranch =
198200
userId: string
199201
userMessageId: string
200202
chatId?: string
201-
contexts: Array<{ type: string; content: string }>
203+
contexts: Array<{ type: string; content: string; tag?: string; path?: string }>
202204
fileAttachments?: UnifiedChatRequest['fileAttachments']
203205
userPermission?: string
204206
userTimezone?: string
@@ -234,10 +236,10 @@ async function resolveAgentContexts(params: {
234236
workspaceId?: string
235237
chatId?: string
236238
requestId: string
237-
}): Promise<Array<{ type: string; content: string }>> {
239+
}): Promise<Array<{ type: string; content: string; tag?: string; path?: string }>> {
238240
const { contexts, resourceAttachments, userId, message, workspaceId, chatId, requestId } = params
239241

240-
let agentContexts: Array<{ type: string; content: string }> = []
242+
let agentContexts: Array<{ type: string; content: string; tag?: string; path?: string }> = []
241243

242244
if (Array.isArray(contexts) && contexts.length > 0) {
243245
try {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import type { ChatContext } from '@/stores/panel'
7+
8+
const { getSkillById } = vi.hoisted(() => ({ getSkillById: vi.fn() }))
9+
10+
vi.mock('@sim/db', () => ({ db: {} }))
11+
vi.mock('@sim/db/schema', () => ({ document: {}, knowledgeBase: {} }))
12+
vi.mock('@/lib/workflows/skills/operations', () => ({ getSkillById }))
13+
14+
import { processContextsServer } from './process-contents'
15+
16+
describe('processContextsServer - skill contexts', () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks()
19+
})
20+
21+
it('resolves a tagged skill to full content + encoded VFS path', async () => {
22+
getSkillById.mockResolvedValue({
23+
id: 'sk-1',
24+
name: 'My Skill — PostHog',
25+
description: 'desc',
26+
content: '# My Skill\n\nDo the thing.',
27+
})
28+
29+
const result = await processContextsServer(
30+
[{ kind: 'skill', skillId: 'sk-1', label: 'My Skill — PostHog' } as ChatContext],
31+
'user-1',
32+
'hello',
33+
'ws-1'
34+
)
35+
36+
expect(getSkillById).toHaveBeenCalledWith({ skillId: 'sk-1', workspaceId: 'ws-1' })
37+
expect(result).toEqual([
38+
{
39+
type: 'skill',
40+
tag: '@My Skill — PostHog',
41+
content: '# My Skill\n\nDo the thing.',
42+
path: 'agent/skills/My%20Skill%20%E2%80%94%20PostHog.json',
43+
},
44+
])
45+
})
46+
47+
it('drops a skill that does not resolve (unknown or cross-workspace)', async () => {
48+
getSkillById.mockResolvedValue(null)
49+
50+
const result = await processContextsServer(
51+
[{ kind: 'skill', skillId: 'missing', label: 'x' } as ChatContext],
52+
'user-1',
53+
'hello',
54+
'ws-1'
55+
)
56+
57+
expect(result).toEqual([])
58+
})
59+
60+
it('drops a skill when no workspace is in scope', async () => {
61+
const result = await processContextsServer(
62+
[{ kind: 'skill', skillId: 'sk-1', label: 'x' } as ChatContext],
63+
'user-1',
64+
'hello',
65+
undefined
66+
)
67+
68+
expect(getSkillById).not.toHaveBeenCalled()
69+
expect(result).toEqual([])
70+
})
71+
})

0 commit comments

Comments
 (0)