-
Notifications
You must be signed in to change notification settings - Fork 0
feat: ai agent #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: ai agent #12
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| auto-install-peers=true | ||
| shamefully-hoist=true | ||
| node-linker=hoisted |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,24 @@ | ||
| # Session password for Nuxt Auth Utils | ||
| NUXT_SESSION_PASSWORD="" | ||
| NUXT_SESSION_PASSWORD= | ||
|
|
||
| # Main database | ||
| DATABASE_URL="" | ||
| DATABASE_URL= | ||
|
|
||
| # S3 file storage | ||
| NUXT_S3_BUCKET="" | ||
| NUXT_S3_REGION="" | ||
| NUXT_S3_ENDPOINT="" | ||
| NUXT_S3_ACCESS_KEY_ID="" | ||
| NUXT_S3_SECRET_ACCESS_KEY="" | ||
| NUXT_S3_BUCKET= | ||
| NUXT_S3_REGION= | ||
| NUXT_S3_ENDPOINT= | ||
| NUXT_S3_ACCESS_KEY_ID= | ||
| NUXT_S3_SECRET_ACCESS_KEY= | ||
|
|
||
| # URL to media server (probably s3 bucket with static URL) | ||
| NUXT_PUBLIC_MEDIA_URL="" | ||
| NUXT_PUBLIC_MEDIA_URL= | ||
|
|
||
| # AI | ||
| NUXT_AI_API_KEY= | ||
| NUXT_AI_BASE_URL= | ||
| NUXT_AI_MODEL= | ||
| NUXT_AI_SERVICE_TOKEN= | ||
|
|
||
| # App version | ||
| VERSION="" | ||
| VERSION= |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Buffer } from 'node:buffer' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { timingSafeEqual } from 'node:crypto' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Agent, OpenAIChatCompletionsModel, run } from '@openai/agents' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import OpenAI from 'openai' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getPartnersByCityTool, getPartnersTool } from '~~/server/services/tools' | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export default defineEventHandler(async (event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { ai } = useRuntimeConfig() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Requires bearer token | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const bearer = getHeader(event, 'authorization') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!bearer?.startsWith('Bearer ')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 401, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Unauthorized', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = bearer.slice(7) // Remove 'Bearer ' prefix | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ai.serviceToken || !timingSafeEqual(Buffer.from(token), Buffer.from(ai.serviceToken))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 401, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Unauthorized', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await readBody(event) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!body?.message) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 400, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Message is required', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add comprehensive input validation. The current validation only checks for the presence of Add proper input validation using zod: +import { z } from 'zod'
+const requestSchema = z.object({
+ message: z.string().min(1).max(1000).trim(),
+})
const body = await readBody(event)
-if (!body?.message) {
+
+const validation = requestSchema.safeParse(body)
+if (!validation.success) {
throw createError({
statusCode: 400,
- message: 'Message is required',
+ message: 'Invalid input: ' + validation.error.issues.map(i => i.message).join(', '),
})
}
+const { message } = validation.dataThen use 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = new OpenAI({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apiKey: ai.apiKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| baseURL: ai.baseUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add OpenAI client configuration validation. The OpenAI client is created without validating the required configuration values, which could lead to runtime errors. Add configuration validation: +if (!ai.apiKey || !ai.baseUrl || !ai.model) {
+ throw createError({
+ statusCode: 500,
+ message: 'AI service configuration is incomplete',
+ })
+}
const client = new OpenAI({
apiKey: ai.apiKey,
baseURL: ai.baseUrl,
})🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const agent = new Agent({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'Дата агент сети доставок "Суши Love"', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| instructions: 'У тебя есть доступ к данным партнеров сети. Отвечай всегда на русском в мужском роде.', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| model: new OpenAIChatCompletionsModel(client, ai.model), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tools: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getPartnersTool, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getPartnersByCityTool, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add rate limiting and request timeout. The agent execution doesn't have rate limiting or timeout controls, which could lead to resource exhaustion or hanging requests. Consider adding timeout and basic rate limiting: const agent = new Agent({
name: 'Дата агент сети доставок "Суши Love"',
instructions: 'У тебя есть доступ к данным партнеров сети. Отвечай всегда на русском в мужском роде.',
model: new OpenAIChatCompletionsModel(client, ai.model),
tools: [
getPartnersTool,
getPartnersByCityTool,
],
+ maxToolCalls: 10, // Limit tool calls to prevent infinite loops
})Also consider implementing request timeout: -const result = await run(agent, body.message)
+const result = await Promise.race([
+ run(agent, message),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Request timeout')), 30000)
+ )
+])
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await run(agent, body.message) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ok: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| message: result.finalOutput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw errorResolver(error) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { tool } from '@openai/agents' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { repository } from '@roll-stack/database' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const getPartnersTool = tool({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'get_all_partners', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: 'Get all partners', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| needsApproval: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parameters: z.object({}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return repository.partner.list() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider adding pagination and access control. The tool exposes all partner data without pagination or access control, which could lead to performance issues and potential data leaks. Consider adding pagination parameters and access control: export const getPartnersTool = tool({
name: 'get_all_partners',
- description: 'Get all partners',
+ description: 'Get paginated list of partners',
needsApproval: false,
- parameters: z.object({}),
- execute: async () => {
- return repository.partner.list()
+ parameters: z.object({
+ limit: z.number().min(1).max(100).optional().default(50),
+ offset: z.number().min(0).optional().default(0),
+ }),
+ execute: async ({ limit, offset }) => {
+ return repository.partner.list({ limit, offset })
},
})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const getPartnersByCityTool = tool({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'get_partners_by_city', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: 'Get partners filtered by city name using case-insensitive partial matching', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| needsApproval: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parameters: z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| city: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async ({ city }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const partners = await repository.partner.list() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return partners.filter((partner) => partner.city?.toLowerCase().includes(city.toLowerCase())) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Optimize database query for city filtering. The current implementation fetches all partners and then filters in memory, which is inefficient and doesn't scale well. Move the filtering to the database layer: export const getPartnersByCityTool = tool({
name: 'get_partners_by_city',
description: 'Get partners by provided city name. The list returned may be longer than needed, requires rechecking',
needsApproval: false,
parameters: z.object({
city: z.string(),
}),
- execute: async ({ city }) => {
- const partners = await repository.partner.list()
-
- return partners.filter((partner) => partner.city?.toLowerCase().includes(city.toLowerCase()))
+ execute: async ({ city }) => {
+ return repository.partner.list({
+ where: {
+ city: {
+ contains: city,
+ mode: 'insensitive'
+ }
+ }
+ })
},
})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.