Skip to content

Commit 42828f8

Browse files
committed
Merge branch 'main' of https://github.com/wannabespace/conar into feat/context-implementation
2 parents 6479813 + 5681a4f commit 42828f8

50 files changed

Lines changed: 798 additions & 345 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lint-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Setup pnpm
2525
uses: pnpm/action-setup@v4
2626
with:
27-
version: 10.27.0
27+
version: 10.28.0
2828

2929
- name: Restore cached dependencies
3030
id: cache-restore

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Install pnpm
2020
uses: pnpm/action-setup@v2
2121
with:
22-
version: 10.27.0
22+
version: 10.28.0
2323

2424
- name: Install Dependencies
2525
run: pnpm install --frozen-lockfile

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ Conar is an AI-powered open-source project that simplifies database interactions
4444
- Drizzle ORM
4545
- Better Auth
4646
- AI SDK with Anthropic, OpenAI, Gemini and XAI
47-
- Supabase
4847
- Railway
4948
- PostHog
5049
- Resend
5150
- ToDesktop
51+
- Stripe
5252

5353
## Development Setup
5454

apps/api/src/drizzle/schema/chats.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AppUIMessage } from '~/ai-tools'
2-
import { createSelectSchema } from 'drizzle-arktype'
2+
import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-arktype'
33
import { relations } from 'drizzle-orm'
44
import { index, pgTable, text, uuid } from 'drizzle-orm/pg-core'
55
import { baseTable } from '../base-table'
@@ -19,6 +19,8 @@ export const chats = pgTable('chats', {
1919
])
2020

2121
export const chatsSelectSchema = createSelectSchema(chats)
22+
export const chatsInsertSchema = createInsertSchema(chats)
23+
export const chatsUpdateSchema = createUpdateSchema(chats)
2224

2325
export const chatsMessages = pgTable('chats_messages', {
2426
...baseTable,
@@ -32,6 +34,8 @@ export const chatsMessages = pgTable('chats_messages', {
3234
])
3335

3436
export const chatsMessagesSelectSchema = createSelectSchema(chatsMessages)
37+
export const chatsMessagesInsertSchema = createInsertSchema(chatsMessages)
38+
export const chatsMessagesUpdateSchema = createUpdateSchema(chatsMessages)
3539

3640
export const chatsRelations = relations(chats, ({ one, many }) => ({
3741
user: one(users, {

apps/api/src/orpc/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,19 @@ export const optionalAuthMiddleware = orpc.middleware(async ({ context, next })
5353
})
5454

5555
export async function getSubscription(userId: string) {
56+
const userSubscriptions = await db.select().from(subscriptions).where(eq(subscriptions.userId, userId))
57+
58+
return userSubscriptions.find(s => ACTIVE_SUBSCRIPTION_STATUSES.includes(s.status as typeof ACTIVE_SUBSCRIPTION_STATUSES[number]) && !s.cancelAt) ?? null
59+
}
60+
61+
async function getSubscriptionCached(userId: string) {
5662
const cachedSubscription = await redis.get(`subscription:${userId}`)
5763

5864
if (cachedSubscription) {
59-
return JSON.parse(cachedSubscription) as typeof subscriptions.$inferSelect
65+
return JSON.parse(cachedSubscription) as NonNullable<Awaited<ReturnType<typeof getSubscription>>>
6066
}
6167

62-
const userSubscriptions = await db.select().from(subscriptions).where(eq(subscriptions.userId, userId))
63-
64-
const subscription = userSubscriptions.find(s => ACTIVE_SUBSCRIPTION_STATUSES.includes(s.status as typeof ACTIVE_SUBSCRIPTION_STATUSES[number]) && !s.cancelAt) ?? null
68+
const subscription = await getSubscription(userId)
6569

6670
if (subscription) {
6771
await redis.setex(
@@ -77,7 +81,7 @@ export async function getSubscription(userId: string) {
7781
export const requireSubscriptionMiddleware = orpc.middleware(async ({ context, next }) => {
7882
const session = await getSession(context.headers)
7983
const minorVersion = context.minorVersion ?? 0
80-
const subscription = await getSubscription(session.user.id)
84+
const subscription = await getSubscriptionCached(session.user.id)
8185

8286
if (!subscription) {
8387
throw new ORPCError('FORBIDDEN', {
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
import { challenge } from './challenge'
2-
import { invoices } from './invoices'
3-
import { subscription } from './subscription'
4-
5-
export const account = {
6-
invoices,
7-
subscription,
8-
challenge,
9-
}
1+
export { challenge } from './challenge'
2+
export { invoices } from './invoices'
3+
export { subscription } from './subscription'

apps/api/src/orpc/routers/ai/ask.ts renamed to apps/api/src/orpc/routers/ai/ask__legacy.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { context7ToolDescriptions, convertToAppUIMessage, getAllTools, tools } f
1515
import { chats, chatsMessages, db } from '~/drizzle'
1616
import { withPosthog } from '~/lib/posthog'
1717
import { orpc, requireSubscriptionMiddleware } from '~/orpc'
18-
import { streamContext } from './resume-stream'
1918

2019
const chatInputType = type({
2120
'id': 'string.uuid.v7',
@@ -33,8 +32,8 @@ const chatInputType = type({
3332
const mainModel = createRetryable({
3433
model: anthropic('claude-sonnet-4-5'),
3534
retries: [
36-
{ model: anthropic('claude-opus-4-1') },
37-
{ model: google('gemini-2.5-pro') },
35+
anthropic('claude-opus-4-5'),
36+
google('gemini-2.5-pro'),
3837
],
3938
})
4039

@@ -271,15 +270,6 @@ export const ask = orpc
271270
},
272271
})
273272

274-
try {
275-
const streamId = v7()
276-
await streamContext.createNewResumableStream(streamId, () => result.textStream)
277-
await db.update(chats).set({ activeStreamId: streamId }).where(eq(chats.id, input.id))
278-
}
279-
catch (error) {
280-
consola.error('error on createNewResumableStream', error)
281-
}
282-
283273
return streamToEventIterator(stream)
284274
}
285275
catch (error) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { AppUIMessage } from '~/ai-tools'
2+
import { anthropic } from '@ai-sdk/anthropic'
3+
import { google } from '@ai-sdk/google'
4+
import { DatabaseType } from '@conar/shared/enums/database-type'
5+
import { streamToEventIterator } from '@orpc/server'
6+
import { convertToModelMessages, smoothStream, stepCountIs, streamText } from 'ai'
7+
import { createRetryable } from 'ai-retry'
8+
import { type } from 'arktype'
9+
import { consola } from 'consola'
10+
import { v7 } from 'uuid'
11+
import { tools } from '~/ai-tools'
12+
import { withPosthog } from '~/lib/posthog'
13+
import { orpc, requireSubscriptionMiddleware } from '~/orpc'
14+
15+
const model = createRetryable({
16+
model: anthropic('claude-sonnet-4-5'),
17+
retries: [
18+
anthropic('claude-opus-4-5'),
19+
google('gemini-2.5-pro'),
20+
],
21+
})
22+
23+
function handleError(error: unknown) {
24+
if (typeof error === 'object' && (error as { type?: string }).type === 'overloaded_error') {
25+
return 'Sorry, I was unable to generate a response due to high load. Please try again later.'
26+
}
27+
if (typeof error === 'object' && (error as { message?: string }).message?.includes('prompt is too long')) {
28+
return 'Sorry, I was unable to generate a response. Currently I cannot handle larger chats like yours. Please create a new chat.'
29+
}
30+
return 'Sorry, I was unable to generate a response due to an error. Please try again.'
31+
}
32+
33+
export const chat = orpc
34+
.use(requireSubscriptionMiddleware)
35+
.use(async ({ context, next }) => {
36+
context.setHeader('Transfer-Encoding', 'chunked')
37+
context.setHeader('Connection', 'keep-alive')
38+
39+
return next()
40+
})
41+
.input(type({
42+
id: 'string.uuid.v7',
43+
type: type.valueOf(DatabaseType),
44+
context: 'string',
45+
createdAt: 'Date',
46+
updatedAt: 'Date',
47+
messages: 'object[]' as type.cast<AppUIMessage[]>,
48+
}))
49+
.handler(async ({ input, context, signal }) => {
50+
consola.info('messages', JSON.stringify(input.messages.map(message => ({
51+
id: message.id,
52+
chatId: input.id,
53+
role: message.role,
54+
partsCount: message.parts.length,
55+
})), null, 2))
56+
57+
const result = streamText({
58+
messages: [
59+
{
60+
role: 'system',
61+
content: [
62+
`You are an SQL tool that generates valid SQL code for ${input.type} database.`,
63+
'You can use several tools to improve response.',
64+
'You can generate select queries using the tools to get data directly from the database.',
65+
'You can also search the web for information when the user asks about external resources, provides URLs, or needs current information beyond the database schema.',
66+
'',
67+
'Requirements:',
68+
`- Ensure the SQL is 100% valid and optimized for ${input.type} database`,
69+
'- Use proper table and column names exactly as provided in the context',
70+
'- Use 2 spaces for indentation and consistent formatting',
71+
'- Consider performance implications for complex queries',
72+
'- The SQL code will be executed directly in a production database editor',
73+
'- Generate SQL query only for the provided schemas, tables, columns and enums',
74+
'- Answer in markdown and paste the SQL code in a code block, each query in a separate code block, do not use headings',
75+
'- Answer in the same language as the user\'s message',
76+
'- Use quotes for table and column names to prevent SQL errors with case sensitivity',
77+
'- If a user asks to change specific lines generate SQL only for the lines, not for whole SQL',
78+
'',
79+
'Additional information:',
80+
`- Current date and time: ${new Date().toISOString()}`,
81+
'',
82+
'You can use the following tools to help you generate the SQL code:',
83+
`- ${Object.entries(tools).map(([tool, { description }]) => `${tool}: ${description}`).join('\n')}`,
84+
'',
85+
'User provided context:',
86+
input.context,
87+
].join('\n'),
88+
},
89+
...(await convertToModelMessages(input.messages)),
90+
],
91+
stopWhen: stepCountIs(Number.POSITIVE_INFINITY),
92+
abortSignal: signal,
93+
model: withPosthog(model, {
94+
chatId: input.id,
95+
userId: context.user.id,
96+
}),
97+
experimental_transform: smoothStream(),
98+
tools,
99+
})
100+
101+
const stream = result.toUIMessageStream({
102+
originalMessages: input.messages,
103+
generateMessageId: () => v7(),
104+
sendSources: true,
105+
onFinish: async (result) => {
106+
consola.info('stream finished', JSON.stringify({
107+
...result.responseMessage,
108+
parts: result.responseMessage.parts.map(part => part.type),
109+
}, null, 2))
110+
},
111+
onError: (error) => {
112+
consola.error('error toUIMessageStream onError', error)
113+
114+
return handleError(error)
115+
},
116+
})
117+
118+
return streamToEventIterator(stream)
119+
})

apps/api/src/orpc/routers/ai/enhance-prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { generateText } from 'ai'
44
import { type } from 'arktype'
55
import { withPosthog } from '~/lib/posthog'
66
import { orpc, requireSubscriptionMiddleware } from '~/orpc'
7-
import { getMessages } from './ask'
7+
import { getMessages } from './ask__legacy'
88

99
export const enhancePrompt = orpc
1010
.use(requireSubscriptionMiddleware)
Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
import { ask } from './ask'
2-
import { enhancePrompt } from './enhance-prompt'
3-
import { filters } from './filters'
4-
import { fixSQL } from './fix-sql'
5-
import { generateTitle } from './generate-title'
6-
import { resumeStream } from './resume-stream'
7-
import { updateSQL } from './update-sql'
8-
9-
export const ai = {
10-
ask,
11-
enhancePrompt,
12-
filters,
13-
generateTitle,
14-
updateSQL,
15-
fixSQL,
16-
resumeStream,
17-
}
1+
export { ask } from './ask__legacy'
2+
export { chat } from './chat'
3+
export { enhancePrompt } from './enhance-prompt'
4+
export { filters } from './filters'
5+
export { fixSQL } from './fix-sql'
6+
export { generateTitle } from './generate-title'
7+
export { updateSQL } from './update-sql'

0 commit comments

Comments
 (0)