Skip to content

Commit ef681d8

Browse files
committed
Greptile fixes
1 parent 5c487f5 commit ef681d8

File tree

24 files changed

+625
-204
lines changed

24 files changed

+625
-204
lines changed

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

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { type NextRequest, NextResponse } from 'next/server'
22
import { z } from 'zod'
33
import { getSession } from '@/lib/auth'
44
import {
5+
type CopilotChat,
6+
type CopilotMessage,
57
createChat,
68
generateChatTitle,
79
generateDocsResponse,
@@ -52,8 +54,8 @@ export async function POST(req: NextRequest) {
5254
})
5355

5456
// Handle chat context
55-
let currentChat: any = null
56-
let conversationHistory: any[] = []
57+
let currentChat: CopilotChat | null = null
58+
let conversationHistory: CopilotMessage[] = []
5759

5860
if (chatId) {
5961
// Load existing chat
@@ -126,53 +128,75 @@ export async function POST(req: NextRequest) {
126128
controller.enqueue(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`))
127129
}
128130

129-
// Save conversation to database after streaming completes
130-
if (currentChat) {
131-
const userMessage = {
132-
id: crypto.randomUUID(),
133-
role: 'user',
134-
content: query,
135-
timestamp: new Date().toISOString(),
136-
}
137-
138-
const assistantMessage = {
139-
id: crypto.randomUUID(),
140-
role: 'assistant',
141-
content: accumulatedResponse,
142-
timestamp: new Date().toISOString(),
143-
citations: result.sources.map((source, index) => ({
144-
id: index + 1,
145-
title: source.title,
146-
url: source.url,
147-
})),
148-
}
149-
150-
const updatedMessages = [...conversationHistory, userMessage, assistantMessage]
151-
152-
// Generate title if this is the first message
153-
let updatedTitle = currentChat.title
154-
if (!updatedTitle && conversationHistory.length === 0) {
155-
updatedTitle = await generateChatTitle(query)
156-
}
157-
158-
// Update the chat in database
159-
await updateChat(currentChat.id, session.user.id, {
160-
title: updatedTitle,
161-
messages: updatedMessages,
162-
})
131+
// Send completion marker first to unblock the user
132+
controller.enqueue(encoder.encode(`data: {"type":"done"}\n\n`))
163133

164-
logger.info(`[${requestId}] Updated chat ${currentChat.id} with new docs messages`)
134+
// Save conversation to database asynchronously (non-blocking)
135+
if (currentChat) {
136+
// Fire-and-forget database save to avoid blocking stream completion
137+
Promise.resolve()
138+
.then(async () => {
139+
try {
140+
const userMessage: CopilotMessage = {
141+
id: crypto.randomUUID(),
142+
role: 'user',
143+
content: query,
144+
timestamp: new Date().toISOString(),
145+
}
146+
147+
const assistantMessage: CopilotMessage = {
148+
id: crypto.randomUUID(),
149+
role: 'assistant',
150+
content: accumulatedResponse,
151+
timestamp: new Date().toISOString(),
152+
citations: result.sources.map((source, index) => ({
153+
id: index + 1,
154+
title: source.title,
155+
url: source.url,
156+
})),
157+
}
158+
159+
const updatedMessages = [
160+
...conversationHistory,
161+
userMessage,
162+
assistantMessage,
163+
]
164+
165+
// Generate title if this is the first message
166+
let updatedTitle = currentChat.title ?? undefined
167+
if (!updatedTitle && conversationHistory.length === 0) {
168+
updatedTitle = await generateChatTitle(query)
169+
}
170+
171+
// Update the chat in database
172+
await updateChat(currentChat.id, session.user.id, {
173+
title: updatedTitle,
174+
messages: updatedMessages,
175+
})
176+
177+
logger.info(
178+
`[${requestId}] Updated chat ${currentChat.id} with new docs messages`
179+
)
180+
} catch (dbError) {
181+
logger.error(`[${requestId}] Failed to save chat to database:`, dbError)
182+
// Database errors don't affect the user's streaming experience
183+
}
184+
})
185+
.catch((error) => {
186+
logger.error(`[${requestId}] Unexpected error in async database save:`, error)
187+
})
165188
}
166-
167-
// Send completion marker
168-
controller.enqueue(encoder.encode(`data: {"type":"done"}\n\n`))
169189
} catch (error) {
170190
logger.error(`[${requestId}] Docs streaming error:`, error)
171-
const errorChunk = {
172-
type: 'error',
173-
error: 'Streaming failed',
191+
try {
192+
const errorChunk = {
193+
type: 'error',
194+
error: 'Streaming failed',
195+
}
196+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`))
197+
} catch (enqueueError) {
198+
logger.error(`[${requestId}] Failed to enqueue error response:`, enqueueError)
174199
}
175-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorChunk)}\n\n`))
176200
} finally {
177201
controller.close()
178202
}
@@ -193,14 +217,14 @@ export async function POST(req: NextRequest) {
193217

194218
// Save conversation to database if we have a chat
195219
if (currentChat) {
196-
const userMessage = {
220+
const userMessage: CopilotMessage = {
197221
id: crypto.randomUUID(),
198222
role: 'user',
199223
content: query,
200224
timestamp: new Date().toISOString(),
201225
}
202226

203-
const assistantMessage = {
227+
const assistantMessage: CopilotMessage = {
204228
id: crypto.randomUUID(),
205229
role: 'assistant',
206230
content: typeof result.response === 'string' ? result.response : '[Streaming Response]',
@@ -215,7 +239,7 @@ export async function POST(req: NextRequest) {
215239
const updatedMessages = [...conversationHistory, userMessage, assistantMessage]
216240

217241
// Generate title if this is the first message
218-
let updatedTitle = currentChat.title
242+
let updatedTitle = currentChat.title ?? undefined
219243
if (!updatedTitle && conversationHistory.length === 0) {
220244
updatedTitle = await generateChatTitle(query)
221245
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import { createLogger } from '@/lib/logs/console-logger'
1414

1515
const logger = createLogger('CopilotAPI')
1616

17+
// Interface for StreamingExecution response
18+
interface StreamingExecution {
19+
stream: ReadableStream
20+
execution: Promise<any>
21+
}
22+
1723
// Schema for sending messages
1824
const SendMessageSchema = z.object({
1925
message: z.string().min(1, 'Message is required'),
@@ -135,7 +141,8 @@ export async function POST(req: NextRequest) {
135141
) {
136142
// Handle StreamingExecution (from providers with tool calls)
137143
logger.info(`[${requestId}] StreamingExecution detected`)
138-
streamToRead = (result.response as any).stream
144+
const streamingExecution = result.response as StreamingExecution
145+
streamToRead = streamingExecution.stream
139146

140147
// No need to extract citations - LLM generates direct markdown links
141148
}

apps/sim/app/api/docs/search/route.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,73 @@ import { createLogger } from '@/lib/logs/console-logger'
44

55
const logger = createLogger('DocsSearchAPI')
66

7-
export async function POST(request: NextRequest) {
7+
// Request and response type definitions
8+
interface DocsSearchRequest {
9+
query: string
10+
topK?: number
11+
}
12+
13+
interface DocsSearchResult {
14+
id: number
15+
title: string
16+
url: string
17+
content: string
18+
similarity: number
19+
}
20+
21+
interface DocsSearchSuccessResponse {
22+
success: true
23+
results: DocsSearchResult[]
24+
query: string
25+
totalResults: number
26+
searchTime?: number
27+
}
28+
29+
interface DocsSearchErrorResponse {
30+
success: false
31+
error: string
32+
}
33+
34+
export async function POST(
35+
request: NextRequest
36+
): Promise<NextResponse<DocsSearchSuccessResponse | DocsSearchErrorResponse>> {
837
try {
9-
const { query, topK = 5 } = await request.json()
38+
const requestBody: DocsSearchRequest = await request.json()
39+
const { query, topK = 5 } = requestBody
1040

1141
if (!query) {
12-
return NextResponse.json({ error: 'Query is required' }, { status: 400 })
42+
const errorResponse: DocsSearchErrorResponse = {
43+
success: false,
44+
error: 'Query is required',
45+
}
46+
return NextResponse.json(errorResponse, { status: 400 })
1347
}
1448

1549
logger.info('Executing documentation search', { query, topK })
1650

51+
const startTime = Date.now()
1752
const results = await searchDocumentation(query, { topK })
53+
const searchTime = Date.now() - startTime
1854

1955
logger.info(`Found ${results.length} documentation results`, { query })
2056

21-
return NextResponse.json({
57+
const successResponse: DocsSearchSuccessResponse = {
2258
success: true,
2359
results,
2460
query,
2561
totalResults: results.length,
26-
})
62+
searchTime,
63+
}
64+
65+
return NextResponse.json(successResponse)
2766
} catch (error) {
2867
logger.error('Documentation search API failed', error)
29-
return NextResponse.json(
30-
{
31-
success: false,
32-
error: `Documentation search failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
33-
},
34-
{ status: 500 }
35-
)
68+
69+
const errorResponse: DocsSearchErrorResponse = {
70+
success: false,
71+
error: `Documentation search failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
72+
}
73+
74+
return NextResponse.json(errorResponse, { status: 500 })
3675
}
3776
}

apps/sim/app/api/workflows/[id]/state/route.ts

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,103 @@ import { getUserEntityPermissions } from '@/lib/permissions/utils'
77
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
88
import { db } from '@/db'
99
import { workflow } from '@/db/schema'
10+
import type { WorkflowState } from '@/stores/workflows/workflow/types'
1011

1112
const logger = createLogger('WorkflowStateAPI')
1213

14+
// Zod schemas for workflow state validation
15+
const PositionSchema = z.object({
16+
x: z.number(),
17+
y: z.number(),
18+
})
19+
20+
const BlockDataSchema = z.object({
21+
parentId: z.string().optional(),
22+
extent: z.literal('parent').optional(),
23+
width: z.number().optional(),
24+
height: z.number().optional(),
25+
collection: z.unknown().optional(),
26+
count: z.number().optional(),
27+
loopType: z.enum(['for', 'forEach']).optional(),
28+
parallelType: z.enum(['collection', 'count']).optional(),
29+
type: z.string().optional(),
30+
})
31+
32+
const SubBlockStateSchema = z.object({
33+
id: z.string(),
34+
type: z.string(),
35+
value: z.union([z.string(), z.number(), z.array(z.array(z.string())), z.null()]),
36+
})
37+
38+
const BlockOutputSchema = z.any()
39+
40+
const BlockStateSchema = z.object({
41+
id: z.string(),
42+
type: z.string(),
43+
name: z.string(),
44+
position: PositionSchema,
45+
subBlocks: z.record(SubBlockStateSchema),
46+
outputs: z.record(BlockOutputSchema),
47+
enabled: z.boolean(),
48+
horizontalHandles: z.boolean().optional(),
49+
isWide: z.boolean().optional(),
50+
height: z.number().optional(),
51+
advancedMode: z.boolean().optional(),
52+
data: BlockDataSchema.optional(),
53+
})
54+
55+
const EdgeSchema = z.object({
56+
id: z.string(),
57+
source: z.string(),
58+
target: z.string(),
59+
sourceHandle: z.string().optional(),
60+
targetHandle: z.string().optional(),
61+
type: z.string().optional(),
62+
animated: z.boolean().optional(),
63+
style: z.record(z.any()).optional(),
64+
data: z.record(z.any()).optional(),
65+
label: z.string().optional(),
66+
labelStyle: z.record(z.any()).optional(),
67+
labelShowBg: z.boolean().optional(),
68+
labelBgStyle: z.record(z.any()).optional(),
69+
labelBgPadding: z.array(z.number()).optional(),
70+
labelBgBorderRadius: z.number().optional(),
71+
markerStart: z.string().optional(),
72+
markerEnd: z.string().optional(),
73+
})
74+
75+
const LoopSchema = z.object({
76+
id: z.string(),
77+
nodes: z.array(z.string()),
78+
iterations: z.number(),
79+
loopType: z.enum(['for', 'forEach']),
80+
forEachItems: z.union([z.array(z.any()), z.record(z.any()), z.string()]).optional(),
81+
})
82+
83+
const ParallelSchema = z.object({
84+
id: z.string(),
85+
nodes: z.array(z.string()),
86+
distribution: z.union([z.array(z.any()), z.record(z.any()), z.string()]).optional(),
87+
count: z.number().optional(),
88+
parallelType: z.enum(['count', 'collection']).optional(),
89+
})
90+
91+
const DeploymentStatusSchema = z.object({
92+
id: z.string(),
93+
status: z.enum(['deploying', 'deployed', 'failed', 'stopping', 'stopped']),
94+
deployedAt: z.date().optional(),
95+
error: z.string().optional(),
96+
})
97+
1398
const WorkflowStateSchema = z.object({
14-
blocks: z.record(z.any()),
15-
edges: z.array(z.any()),
16-
loops: z.record(z.any()).optional(),
17-
parallels: z.record(z.any()).optional(),
99+
blocks: z.record(BlockStateSchema),
100+
edges: z.array(EdgeSchema),
101+
loops: z.record(LoopSchema).optional(),
102+
parallels: z.record(ParallelSchema).optional(),
18103
lastSaved: z.number().optional(),
19104
isDeployed: z.boolean().optional(),
20105
deployedAt: z.date().optional(),
21-
deploymentStatuses: z.record(z.any()).optional(),
106+
deploymentStatuses: z.record(DeploymentStatusSchema).optional(),
22107
hasActiveSchedule: z.boolean().optional(),
23108
hasActiveWebhook: z.boolean().optional(),
24109
})
@@ -100,7 +185,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
100185
hasActiveWebhook: state.hasActiveWebhook || false,
101186
}
102187

103-
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
188+
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState as any)
104189

105190
if (!saveResult.success) {
106191
logger.error(`[${requestId}] Failed to save workflow ${workflowId} state:`, saveResult.error)
@@ -131,7 +216,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
131216
},
132217
{ status: 200 }
133218
)
134-
} catch (error: any) {
219+
} catch (error: unknown) {
135220
const elapsed = Date.now() - startTime
136221
if (error instanceof z.ZodError) {
137222
logger.warn(`[${requestId}] Invalid workflow state data for ${workflowId}`, {

0 commit comments

Comments
 (0)