Skip to content

Commit 6d447e0

Browse files
ifeelBALANCEDbalancedletstri
authored
feat(ai): add Context7 documentation tools integration (#299)
Co-authored-by: balanced <ifeelbalanced@gmail.com> Co-authored-by: Valerii Strilets <valerii.strilets@gmail.com>
1 parent 936cb40 commit 6d447e0

22 files changed

Lines changed: 567 additions & 563 deletions

File tree

apps/api/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ GITHUB_CLIENT_ID=
2222
GITHUB_CLIENT_SECRET=
2323
EXA_API_KEY=
2424
GITHUB_TOKEN=
25+
CONTEXT7_API_KEY=
2526
TODESKTOP_WEBHOOK_SECRET=

apps/api/.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ GITHUB_CLIENT_ID=
2222
GITHUB_CLIENT_SECRET=
2323
EXA_API_KEY=
2424
GITHUB_TOKEN=
25+
CONTEXT7_API_KEY=
2526
TODESKTOP_WEBHOOK_SECRET=

apps/api/ai-tools.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.

apps/api/ai/tools/helpers.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { DynamicToolUIPart, InferUITools, ToolUIPart as ToolUIPartAi, UIDataTypes, UIMessage } from 'ai'
2+
import type { tools } from '.'
3+
import { isToolUIPart as isToolUIPartAi } from 'ai'
4+
5+
export type AppUIMessage = UIMessage<
6+
{
7+
updatedAt?: Date
8+
createdAt?: Date
9+
},
10+
UIDataTypes,
11+
InferUITools<typeof tools>
12+
>
13+
14+
export function convertToAppUIMessage(message: UIMessage): AppUIMessage {
15+
return message as AppUIMessage
16+
}
17+
18+
export type ToolUIPart = ToolUIPartAi<InferUITools<typeof tools>> | DynamicToolUIPart
19+
20+
export function isToolUIPart(part: UIMessage['parts'][number]): part is ToolUIPart {
21+
return isToolUIPartAi(part)
22+
}

apps/api/ai/tools/index.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { SQL_FILTERS_LIST } from '@conar/shared/filters/sql'
2+
import { webSearch } from '@exalabs/ai-sdk'
3+
import { queryDocs, resolveLibraryId } from '@upstash/context7-tools-ai-sdk'
4+
import { tool } from 'ai'
5+
import { type } from 'arktype'
6+
import { env } from '~/env'
7+
8+
export const tools = {
9+
columns: tool({
10+
description: 'Use this tool if you need to get the list of columns in a table.',
11+
inputSchema: type({
12+
tableAndSchema: type({
13+
tableName: 'string',
14+
schemaName: 'string',
15+
}),
16+
}),
17+
outputSchema: type({
18+
isEditable: 'boolean',
19+
isNullable: 'boolean',
20+
table: 'string',
21+
id: 'string',
22+
type: 'string',
23+
default: 'string | null',
24+
}).array(),
25+
}),
26+
enums: tool({
27+
description: 'Use this tool if you need to get the list of enums in a database',
28+
inputSchema: type({}),
29+
outputSchema: type({
30+
schema: 'string',
31+
name: 'string',
32+
value: 'string',
33+
}).array(),
34+
}),
35+
select: tool({
36+
description: [
37+
'Use this tool to select data from the database to improve your response.',
38+
'Do not abuse this tool, unless you are 100% sure that the data will help to answer the question.',
39+
'Do not select any sensitive data, like password, token, secret, card number, etc.',
40+
'Mask sensitive data with asterisks if need to select to answer the question.',
41+
'Do not use any tables and schemas that are not provided in the input.',
42+
'tableName and schemaName will be concatenated to "schemaName.tableName".',
43+
'For tableName use only table without schema prefix.',
44+
].join('\n'),
45+
inputSchema: type({
46+
whereConcatOperator: type('"AND" | "OR"').describe('The operator to use to concatenate the where clauses'),
47+
whereFilters: type({
48+
column: 'string',
49+
operator: type
50+
.enumerated(...SQL_FILTERS_LIST.map(filter => filter.operator))
51+
.describe('The operator to use in the where clause'),
52+
values: 'string[]',
53+
})
54+
.array()
55+
.describe('The columns to use in the where clause'),
56+
select: type('string[] | null').describe('The columns to select. If not provided, all columns will be selected'),
57+
limit: 'number',
58+
offset: 'number',
59+
orderBy: 'Record<string, "ASC" | "DESC"> | null',
60+
tableAndSchema: type({
61+
tableName: 'string',
62+
schemaName: 'string',
63+
}).describe('The name of the table and schema to query'),
64+
}).describe('Input schema for database select query with filters, ordering, and pagination'),
65+
outputSchema: type('unknown'),
66+
}),
67+
...(env.EXA_API_KEY && { webSearch: webSearch({ apiKey: env.EXA_API_KEY }) }),
68+
...(env.CONTEXT7_API_KEY && {
69+
resolveLibraryId: resolveLibraryId({ apiKey: env.CONTEXT7_API_KEY }),
70+
queryDocs: queryDocs({ apiKey: env.CONTEXT7_API_KEY }),
71+
}),
72+
}

apps/api/drizzle/schema/chats.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AppUIMessage } from '~/ai-tools'
1+
import type { AppUIMessage } from '~/ai/tools/helpers'
22
import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-arktype'
33
import { relations } from 'drizzle-orm'
44
import { index, pgTable, text, uuid } from 'drizzle-orm/pg-core'

apps/api/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const envType = type({
2929
GITHUB_CLIENT_SECRET: 'string',
3030
BANNER_TEXT: 'string?',
3131
EXA_API_KEY: 'string',
32+
CONTEXT7_API_KEY: 'string',
3233
GITHUB_TOKEN: 'string',
3334
TODESKTOP_WEBHOOK_SECRET: 'string',
3435
})
@@ -51,6 +52,7 @@ const devOptionalEnvs = [
5152
'GITHUB_CLIENT_ID',
5253
'GITHUB_CLIENT_SECRET',
5354
'EXA_API_KEY',
55+
'CONTEXT7_API_KEY',
5456
'GITHUB_TOKEN',
5557
'TODESKTOP_WEBHOOK_SECRET',
5658
] satisfies (keyof typeof envType.infer)[]

apps/api/orpc/routers/ai/ask__legacy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { LanguageModelV3 } from '@ai-sdk/provider'
2-
import type { AppUIMessage } from '~/ai-tools'
2+
import type { AppUIMessage } from '~/ai/tools/helpers'
33
import { anthropic } from '@ai-sdk/anthropic'
44
import { google } from '@ai-sdk/google'
55
import { DatabaseType } from '@conar/shared/enums/database-type'
@@ -10,7 +10,8 @@ import { type } from 'arktype'
1010
import { consola } from 'consola'
1111
import { asc, eq } from 'drizzle-orm'
1212
import { v7 } from 'uuid'
13-
import { convertToAppUIMessage, tools } from '~/ai-tools'
13+
import { tools } from '~/ai/tools'
14+
import { convertToAppUIMessage } from '~/ai/tools/helpers'
1415
import { chats, chatsMessages, db } from '~/drizzle'
1516
import { withPosthog } from '~/lib/posthog'
1617
import { orpc, requireSubscriptionMiddleware } from '~/orpc'
@@ -100,7 +101,7 @@ async function generateStream({
100101
`- Current date and time: ${new Date().toISOString()}`,
101102
'',
102103
'You can use the following tools to help you generate the SQL code:',
103-
`- ${Object.entries(tools).map(([tool, { description }]) => `${tool}: ${description}`).join('\n')}`,
104+
Object.entries(tools).map(([tool, { description }]) => `- ${tool}: ${description}`).join('\n'),
104105
'',
105106
'User provided context:',
106107
context,

apps/api/orpc/routers/ai/chat.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AppUIMessage } from '~/ai-tools'
1+
import type { AppUIMessage } from '~/ai/tools/helpers'
22
import { anthropic } from '@ai-sdk/anthropic'
33
import { google } from '@ai-sdk/google'
44
import { DatabaseType } from '@conar/shared/enums/database-type'
@@ -8,7 +8,7 @@ import { createRetryable } from 'ai-retry'
88
import { type } from 'arktype'
99
import { consola } from 'consola'
1010
import { v7 } from 'uuid'
11-
import { tools } from '~/ai-tools'
11+
import { tools } from '~/ai/tools'
1212
import { withPosthog } from '~/lib/posthog'
1313
import { orpc, requireSubscriptionMiddleware } from '~/orpc'
1414

@@ -75,12 +75,13 @@ export const chat = orpc
7575
'- Answer in the same language as the user\'s message',
7676
'- Use quotes for table and column names to prevent SQL errors with case sensitivity',
7777
'- If a user asks to change specific lines generate SQL only for the lines, not for whole SQL',
78+
'- Always use Context7 MCP when I need library/API documentation, code generation, setup or configuration steps without me having to explicitly ask.',
7879
'',
7980
'Additional information:',
8081
`- Current date and time: ${new Date().toISOString()}`,
8182
'',
8283
'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+
Object.entries(tools).map(([tool, { description }]) => `- ${tool}: ${description}`).join('\n'),
8485
'',
8586
'User provided context:',
8687
input.context,
Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { google } from '@ai-sdk/google'
22
import { SQL_FILTERS_GROUPED, SQL_FILTERS_LIST } from '@conar/shared/filters/sql'
3-
import { generateObject } from 'ai'
3+
import { generateText, Output } from 'ai'
44
import { type } from 'arktype'
55
import { consola } from 'consola'
6-
import { z } from 'zod'
76
import { withPosthog } from '~/lib/posthog'
87
import { authMiddleware, orpc } from '~/orpc'
98

@@ -16,7 +15,7 @@ export const filters = orpc
1615
.handler(async ({ input, signal, context }) => {
1716
consola.info('[SQL FILTERS] input', input.prompt)
1817

19-
const { object } = await generateObject({
18+
const { output: result } = await generateText({
2019
model: withPosthog(google('gemini-2.0-flash'), {
2120
prompt: input.prompt,
2221
context: input.context,
@@ -38,13 +37,13 @@ export const filters = orpc
3837
'- If context already contains a filter, you can use it as reference to generate a new filter',
3938
'- User can paste only the value, you should try to understand to which column the value belongs',
4039
'- Try to generate at least one filter unless the prompt is completely unclear',
41-
// '',
42-
// ' Ordering:',
43-
// ' - If the user requests sorting or ordering (e.g., "sort by date descending", "order by name ascending"), generate an orderBy object.',
44-
// ' - Use the exact column names from the context for ordering.',
45-
// ' - The orderBy object should have the column name as the key and the direction as the value ("ASC" for ascending, "DESC" for descending).',
46-
// ' - If no ordering is specified in the prompt, you may omit the orderBy object.',
4740
'',
41+
// 'Ordering:',
42+
// '- If the user requests sorting or ordering (e.g., "sort by date descending", "order by name ascending"), generate an orderBy object.',
43+
// '- Use the exact column names from the context for ordering.',
44+
// '- The orderBy object should have the column name as the key and the direction as the value ("ASC" for ascending, "DESC" for descending).',
45+
// '- If no ordering is specified in the prompt, you may omit the orderBy object.',
46+
// '',
4847
`Current time: ${new Date().toISOString()}`,
4948
`Available operators: ${JSON.stringify(SQL_FILTERS_GROUPED, null, 2)}`,
5049
'',
@@ -53,32 +52,23 @@ export const filters = orpc
5352
].join('\n'),
5453
prompt: input.prompt,
5554
abortSignal: signal,
56-
schema: z.object({
57-
// orderBy: z
58-
// .object({})
59-
// .catchall(
60-
// z.enum(['ASC', 'DESC']).describe('The direction to order by: ASC for ascending, DESC for descending'),
61-
// )
62-
// .optional()
63-
// .describe('An optional object specifying the order of the results, where each key is a column name and the value is the order direction (ASC or DESC). The object can be empty.'),
64-
filters: z
65-
.object({
66-
column: z.string().describe('The column name to filter by'),
67-
operator: z
68-
.enum(SQL_FILTERS_LIST.map(filter => filter.operator))
55+
output: Output.object({
56+
schema: type({
57+
'orderBy?': type('Record<string, "ASC" | "DESC">')
58+
.describe('An optional object specifying the order of the results, where each key is a column name and the value is the order direction (ASC or DESC). The object can be empty.'),
59+
'filters': type({
60+
column: 'string',
61+
operator: type
62+
.enumerated(...SQL_FILTERS_LIST.map(filter => filter.operator))
6963
.describe('The operator to use for the filter, must be one of the available SQL operators'),
70-
values: z
71-
.array(z.string().describe('A value to filter by for the specified column and operator'))
72-
.describe('The values to filter by for the given column and operator'),
73-
})
74-
.array()
75-
.describe('An array of filter objects, each specifying a column, operator, and values to filter by'),
76-
}).describe('An object with a single property "filters" that is an array of filters'),
77-
schemaDescription: 'An array of objects with the following properties: column, operator, value where the operator is one of the SQL operators available',
78-
output: 'object',
64+
values: 'string[]',
65+
}).array(),
66+
}).describe('An object with filters array and optional orderBy object; each filter has column, operator, and values; orderBy maps column names to sort direction.'),
67+
description: 'An array of objects with the following properties: column, operator, values where the operator is one of the SQL operators available',
68+
}),
7969
})
8070

81-
consola.info('[SQL FILTERS] response', JSON.stringify(object, null, 2))
71+
consola.info('[SQL FILTERS] response', JSON.stringify(result, null, 2))
8272

83-
return object
73+
return result
8474
})

0 commit comments

Comments
 (0)