Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
auto-install-peers=true
shamefully-hoist=true
node-linker=hoisted
24 changes: 15 additions & 9 deletions apps/web-app/.env.example
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_ACCESS_KEY_ID=
NUXT_S3_BUCKET=
NUXT_S3_ENDPOINT=
NUXT_S3_REGION=
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=
1 change: 0 additions & 1 deletion apps/web-app/app/components/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ const menuItems = computed(() => [
onSelect: () => {
openDrawer.value = true
},
badge: 'Нисушка себе!',
},
{
label: t('app.menu.products'),
Expand Down
6 changes: 6 additions & 0 deletions apps/web-app/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export default defineNuxtConfig({
accessKeyId: '',
secretAccessKey: '',
},
ai: {
model: '',
baseUrl: '',
apiKey: '',
serviceToken: '',
},
public: {
mediaUrl: '',
},
Expand Down
2 changes: 2 additions & 0 deletions apps/web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@dicebear/collection": "catalog:",
"@dicebear/core": "catalog:",
"@neoconfetti/vue": "catalog:",
"@openai/agents": "catalog:",
"@pinia/nuxt": "catalog:",
"@roll-stack/database": "workspace:*",
"@roll-stack/ui": "workspace:*",
Expand All @@ -25,6 +26,7 @@
"ioredis": "catalog:",
"libphonenumber-js": "catalog:",
"nuxt-tiptap-editor": "catalog:",
"openai": "catalog:",
"pinia": "catalog:",
"sharp": "catalog:"
},
Expand Down
60 changes: 60 additions & 0 deletions apps/web-app/server/api/agent/index.post.ts
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 message but doesn't validate its type, length, or content.

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.data

Then use message instead of body.message on line 41.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = await readBody(event)
if (!body?.message) {
throw createError({
statusCode: 400,
message: 'Message is required',
})
}
import { z } from 'zod'
const requestSchema = z.object({
message: z.string().min(1).max(1000).trim(),
})
const body = await readBody(event)
const validation = requestSchema.safeParse(body)
if (!validation.success) {
throw createError({
statusCode: 400,
message: 'Invalid input: ' + validation.error.issues.map(i => i.message).join(', '),
})
}
const { message } = validation.data
// …use `message` below instead of `body.message`…
🤖 Prompt for AI Agents
In apps/web-app/server/api/agent/index.post.ts around lines 18 to 24, the
current input validation only checks if the message exists but does not validate
its type, length, or content. To fix this, define a zod schema that validates
the message field for type (string), minimum and maximum length, and any other
content rules needed. Use this schema to parse and validate the request body
instead of just checking presence. After validation, use the parsed message
variable instead of accessing body.message directly on line 41.


const client = new OpenAI({
apiKey: ai.apiKey,
baseURL: ai.baseUrl,
})
Comment on lines +36 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
In apps/web-app/server/api/agent/index.post.ts around lines 26 to 29, the OpenAI
client is instantiated without validating the presence of required configuration
values like apiKey and baseURL. Add validation checks before creating the client
to ensure ai.apiKey and ai.baseUrl are defined and valid. If any required
configuration is missing or invalid, throw an appropriate error or handle it
gracefully to prevent runtime errors.


const agent = new Agent({
name: 'Дата агент сети доставок "Суши Love"',
instructions: 'У тебя есть доступ к данным партнеров сети. Отвечай всегда на русском в мужском роде.',
model: new OpenAIChatCompletionsModel(client, ai.model),
tools: [
getPartnersTool,
getPartnersByCityTool,
],
})
Comment on lines +41 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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)
+  )
+])

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web-app/server/api/agent/index.post.ts around lines 31 to 39, the Agent
instantiation lacks rate limiting and request timeout controls, which can cause
resource exhaustion or hanging requests. To fix this, implement a rate limiting
mechanism that restricts the number of requests per user or IP within a time
window before creating the Agent instance. Additionally, add a request timeout
feature that cancels or aborts the agent's execution if it exceeds a predefined
duration, ensuring the system remains responsive and stable.


const result = await run(agent, body.message)

return {
ok: true,
message: result.finalOutput,
}
} catch (error) {
throw errorResolver(error)
}
})
27 changes: 27 additions & 0 deletions apps/web-app/server/services/tools.ts
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getPartnersTool = tool({
name: 'get_all_partners',
description: 'Get all partners',
needsApproval: false,
parameters: z.object({}),
execute: async () => {
return repository.partner.list()
},
})
export const getPartnersTool = tool({
name: 'get_all_partners',
description: 'Get paginated list of partners',
needsApproval: false,
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 })
},
})
🤖 Prompt for AI Agents
In apps/web-app/server/services/tools.ts around lines 5 to 13, the
getPartnersTool currently returns all partner data without pagination or access
control, risking performance and security issues. Modify the tool to accept
pagination parameters (like page number and page size) in its input schema, and
implement logic in the execute function to fetch only the requested page of
partners. Additionally, add access control checks to ensure only authorized
users can retrieve partner data.


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()))
},
})
Loading