Skip to content

Commit c2d668c

Browse files
authored
feat(copilot): stats tracking (#1227)
* Add copilot stats table schema * Move db to agent * Lint * Fix tests
1 parent 1a5d5dd commit c2d668c

File tree

12 files changed

+283
-71
lines changed

12 files changed

+283
-71
lines changed

apps/sim/app/api/copilot/chat/route.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ describe('Copilot Chat API Route', () => {
224224
stream: true,
225225
streamToolCalls: true,
226226
mode: 'agent',
227+
messageId: 'mock-uuid-1234-5678',
227228
depth: 0,
228229
}),
229230
})
@@ -286,6 +287,7 @@ describe('Copilot Chat API Route', () => {
286287
stream: true,
287288
streamToolCalls: true,
288289
mode: 'agent',
290+
messageId: 'mock-uuid-1234-5678',
289291
depth: 0,
290292
}),
291293
})
@@ -337,6 +339,7 @@ describe('Copilot Chat API Route', () => {
337339
stream: true,
338340
streamToolCalls: true,
339341
mode: 'agent',
342+
messageId: 'mock-uuid-1234-5678',
340343
depth: 0,
341344
}),
342345
})
@@ -425,6 +428,7 @@ describe('Copilot Chat API Route', () => {
425428
stream: true,
426429
streamToolCalls: true,
427430
mode: 'ask',
431+
messageId: 'mock-uuid-1234-5678',
428432
depth: 0,
429433
}),
430434
})

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export async function POST(req: NextRequest) {
108108
conversationId,
109109
contexts,
110110
} = ChatMessageSchema.parse(body)
111+
// Ensure we have a consistent user message ID for this request
112+
const userMessageIdToUse = userMessageId || crypto.randomUUID()
111113
try {
112114
logger.info(`[${tracker.requestId}] Received chat POST`, {
113115
hasContexts: Array.isArray(contexts),
@@ -369,6 +371,7 @@ export async function POST(req: NextRequest) {
369371
stream: stream,
370372
streamToolCalls: true,
371373
mode: mode,
374+
messageId: userMessageIdToUse,
372375
...(providerConfig ? { provider: providerConfig } : {}),
373376
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
374377
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}),
@@ -414,7 +417,7 @@ export async function POST(req: NextRequest) {
414417
if (stream && simAgentResponse.body) {
415418
// Create user message to save
416419
const userMessage = {
417-
id: userMessageId || crypto.randomUUID(), // Use frontend ID if provided
420+
id: userMessageIdToUse, // Consistent ID used for request and persistence
418421
role: 'user',
419422
content: message,
420423
timestamp: new Date().toISOString(),
@@ -810,7 +813,7 @@ export async function POST(req: NextRequest) {
810813
// Save messages if we have a chat
811814
if (currentChat && responseData.content) {
812815
const userMessage = {
813-
id: userMessageId || crypto.randomUUID(), // Use frontend ID if provided
816+
id: userMessageIdToUse, // Consistent ID used for request and persistence
814817
role: 'user',
815818
content: message,
816819
timestamp: new Date().toISOString(),
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
3+
import {
4+
authenticateCopilotRequestSessionOnly,
5+
createBadRequestResponse,
6+
createInternalServerErrorResponse,
7+
createRequestTracker,
8+
createUnauthorizedResponse,
9+
} from '@/lib/copilot/auth'
10+
import { env } from '@/lib/env'
11+
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
12+
13+
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
14+
15+
const BodySchema = z
16+
.object({
17+
// Do NOT send id; messageId is the unique correlator
18+
userId: z.string().optional(),
19+
chatId: z.string().uuid().optional(),
20+
messageId: z.string().optional(),
21+
depth: z.number().int().nullable().optional(),
22+
maxEnabled: z.boolean().nullable().optional(),
23+
createdAt: z.union([z.string().datetime(), z.date()]).optional(),
24+
diffCreated: z.boolean().nullable().optional(),
25+
diffAccepted: z.boolean().nullable().optional(),
26+
duration: z.number().int().nullable().optional(),
27+
inputTokens: z.number().int().nullable().optional(),
28+
outputTokens: z.number().int().nullable().optional(),
29+
aborted: z.boolean().nullable().optional(),
30+
})
31+
.passthrough()
32+
33+
export async function POST(req: NextRequest) {
34+
const tracker = createRequestTracker()
35+
try {
36+
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
37+
if (!isAuthenticated || !userId) {
38+
return createUnauthorizedResponse()
39+
}
40+
41+
const json = await req.json().catch(() => ({}))
42+
const parsed = BodySchema.safeParse(json)
43+
if (!parsed.success) {
44+
return createBadRequestResponse('Invalid request body for copilot stats')
45+
}
46+
const body = parsed.data as any
47+
48+
// Build outgoing payload for Sim Agent; do not include id
49+
const payload: Record<string, any> = {
50+
...body,
51+
userId: body.userId || userId,
52+
createdAt: body.createdAt || new Date().toISOString(),
53+
}
54+
payload.id = undefined
55+
56+
const agentRes = await fetch(`${SIM_AGENT_API_URL}/api/stats`, {
57+
method: 'POST',
58+
headers: {
59+
'Content-Type': 'application/json',
60+
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
61+
},
62+
body: JSON.stringify(payload),
63+
})
64+
65+
// Prefer not to block clients; still relay status
66+
let agentJson: any = null
67+
try {
68+
agentJson = await agentRes.json()
69+
} catch {}
70+
71+
if (!agentRes.ok) {
72+
const message = (agentJson && (agentJson.error || agentJson.message)) || 'Upstream error'
73+
return NextResponse.json({ success: false, error: message }, { status: 400 })
74+
}
75+
76+
return NextResponse.json({ success: true })
77+
} catch (error) {
78+
return createInternalServerErrorResponse('Failed to forward copilot stats')
79+
}
80+
}

apps/sim/db/migrations/meta/0080_snapshot.json

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5747,21 +5747,6 @@
57475747
"concurrently": false,
57485748
"method": "btree",
57495749
"with": {}
5750-
},
5751-
"workspace_environment_workspace_id_idx": {
5752-
"name": "workspace_environment_workspace_id_idx",
5753-
"columns": [
5754-
{
5755-
"expression": "workspace_id",
5756-
"isExpression": false,
5757-
"asc": true,
5758-
"nulls": "last"
5759-
}
5760-
],
5761-
"isUnique": false,
5762-
"concurrently": false,
5763-
"method": "btree",
5764-
"with": {}
57655750
}
57665751
},
57675752
"foreignKeys": {

apps/sim/db/migrations/meta/0081_snapshot.json

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5747,21 +5747,6 @@
57475747
"concurrently": false,
57485748
"method": "btree",
57495749
"with": {}
5750-
},
5751-
"workspace_environment_workspace_id_idx": {
5752-
"name": "workspace_environment_workspace_id_idx",
5753-
"columns": [
5754-
{
5755-
"expression": "workspace_id",
5756-
"isExpression": false,
5757-
"asc": true,
5758-
"nulls": "last"
5759-
}
5760-
],
5761-
"isUnique": false,
5762-
"concurrently": false,
5763-
"method": "btree",
5764-
"with": {}
57655750
}
57665751
},
57675752
"foreignKeys": {

apps/sim/db/migrations/meta/0082_snapshot.json

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5872,21 +5872,6 @@
58725872
"concurrently": false,
58735873
"method": "btree",
58745874
"with": {}
5875-
},
5876-
"workspace_environment_workspace_id_idx": {
5877-
"name": "workspace_environment_workspace_id_idx",
5878-
"columns": [
5879-
{
5880-
"expression": "workspace_id",
5881-
"isExpression": false,
5882-
"asc": true,
5883-
"nulls": "last"
5884-
}
5885-
],
5886-
"isUnique": false,
5887-
"concurrently": false,
5888-
"method": "btree",
5889-
"with": {}
58905875
}
58915876
},
58925877
"foreignKeys": {

apps/sim/db/migrations/meta/0083_snapshot.json

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5872,21 +5872,6 @@
58725872
"concurrently": false,
58735873
"method": "btree",
58745874
"with": {}
5875-
},
5876-
"workspace_environment_workspace_id_idx": {
5877-
"name": "workspace_environment_workspace_id_idx",
5878-
"columns": [
5879-
{
5880-
"expression": "workspace_id",
5881-
"isExpression": false,
5882-
"asc": true,
5883-
"nulls": "last"
5884-
}
5885-
],
5886-
"isUnique": false,
5887-
"concurrently": false,
5888-
"method": "btree",
5889-
"with": {}
58905875
}
58915876
},
58925877
"foreignKeys": {

apps/sim/lib/copilot/tools/client/workflow/build-workflow.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@ export class BuildWorkflowClientTool extends BaseClientTool {
9595
// Populate diff preview immediately (without marking complete yet)
9696
try {
9797
const diffStore = useWorkflowDiffStore.getState()
98+
// Send early stats upsert with the triggering user message id if available
99+
try {
100+
const { useCopilotStore } = await import('@/stores/copilot/store')
101+
const { currentChat, currentUserMessageId, agentDepth, agentPrefetch } =
102+
useCopilotStore.getState() as any
103+
if (currentChat?.id && currentUserMessageId) {
104+
fetch('/api/copilot/stats', {
105+
method: 'POST',
106+
headers: { 'Content-Type': 'application/json' },
107+
body: JSON.stringify({
108+
chatId: currentChat.id,
109+
messageId: currentUserMessageId,
110+
depth: agentDepth,
111+
maxEnabled: agentDepth >= 2 && !agentPrefetch,
112+
diffCreated: true,
113+
}),
114+
}).catch(() => {})
115+
}
116+
} catch {}
98117
await diffStore.setProposedChanges(result.yamlContent)
99118
logger.info('diff proposed changes set')
100119
} catch (e) {

apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,25 @@ export class EditWorkflowClientTool extends BaseClientTool {
151151
try {
152152
if (!this.hasAppliedDiff) {
153153
const diffStore = useWorkflowDiffStore.getState()
154+
// Send early stats upsert with the triggering user message id if available
155+
try {
156+
const { useCopilotStore } = await import('@/stores/copilot/store')
157+
const { currentChat, currentUserMessageId, agentDepth, agentPrefetch } =
158+
useCopilotStore.getState() as any
159+
if (currentChat?.id && currentUserMessageId) {
160+
fetch('/api/copilot/stats', {
161+
method: 'POST',
162+
headers: { 'Content-Type': 'application/json' },
163+
body: JSON.stringify({
164+
chatId: currentChat.id,
165+
messageId: currentUserMessageId,
166+
depth: agentDepth,
167+
maxEnabled: agentDepth >= 2 && !agentPrefetch,
168+
diffCreated: true,
169+
}),
170+
}).catch(() => {})
171+
}
172+
} catch {}
154173
await diffStore.setProposedChanges(result.yamlContent)
155174
logger.info('diff proposed changes set for edit_workflow')
156175
this.hasAppliedDiff = true

0 commit comments

Comments
 (0)