Skip to content

Commit 21a68b6

Browse files
authored
feat: ai agent (#12)
* feat: ai agent * chore: some fixes * chore: update
1 parent a23acc9 commit 21a68b6

9 files changed

Lines changed: 550 additions & 30 deletions

File tree

.npmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
auto-install-peers=true
2-
shamefully-hoist=true
2+
node-linker=hoisted

apps/web-app/.env.example

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
# Session password for Nuxt Auth Utils
2-
NUXT_SESSION_PASSWORD=""
2+
NUXT_SESSION_PASSWORD=
33

44
# Main database
5-
DATABASE_URL=""
5+
DATABASE_URL=
66

77
# S3 file storage
8-
NUXT_S3_BUCKET=""
9-
NUXT_S3_REGION=""
10-
NUXT_S3_ENDPOINT=""
11-
NUXT_S3_ACCESS_KEY_ID=""
12-
NUXT_S3_SECRET_ACCESS_KEY=""
8+
NUXT_S3_ACCESS_KEY_ID=
9+
NUXT_S3_BUCKET=
10+
NUXT_S3_ENDPOINT=
11+
NUXT_S3_REGION=
12+
NUXT_S3_SECRET_ACCESS_KEY=
1313

1414
# URL to media server (probably s3 bucket with static URL)
15-
NUXT_PUBLIC_MEDIA_URL=""
15+
NUXT_PUBLIC_MEDIA_URL=
16+
17+
# AI
18+
NUXT_AI_API_KEY=
19+
NUXT_AI_BASE_URL=
20+
NUXT_AI_MODEL=
21+
NUXT_AI_SERVICE_TOKEN=
1622

1723
# App version
18-
VERSION=""
24+
VERSION=

apps/web-app/app/components/Navigation.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ const menuItems = computed(() => [
8787
onSelect: () => {
8888
openDrawer.value = true
8989
},
90-
badge: 'Нисушка себе!',
9190
},
9291
{
9392
label: t('app.menu.products'),

apps/web-app/nuxt.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export default defineNuxtConfig({
1212
accessKeyId: '',
1313
secretAccessKey: '',
1414
},
15+
ai: {
16+
model: '',
17+
baseUrl: '',
18+
apiKey: '',
19+
serviceToken: '',
20+
},
1521
public: {
1622
mediaUrl: '',
1723
},

apps/web-app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@dicebear/collection": "catalog:",
1717
"@dicebear/core": "catalog:",
1818
"@neoconfetti/vue": "catalog:",
19+
"@openai/agents": "catalog:",
1920
"@pinia/nuxt": "catalog:",
2021
"@roll-stack/database": "workspace:*",
2122
"@roll-stack/ui": "workspace:*",
@@ -25,6 +26,7 @@
2526
"ioredis": "catalog:",
2627
"libphonenumber-js": "catalog:",
2728
"nuxt-tiptap-editor": "catalog:",
29+
"openai": "catalog:",
2830
"pinia": "catalog:",
2931
"sharp": "catalog:"
3032
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Buffer } from 'node:buffer'
2+
import { timingSafeEqual } from 'node:crypto'
3+
import { Agent, OpenAIChatCompletionsModel, run } from '@openai/agents'
4+
import OpenAI from 'openai'
5+
import { getPartnersByCityTool, getPartnersTool } from '~~/server/services/tools'
6+
7+
export default defineEventHandler(async (event) => {
8+
try {
9+
const { ai } = useRuntimeConfig()
10+
11+
// Requires bearer token
12+
const bearer = getHeader(event, 'authorization')
13+
if (!bearer?.startsWith('Bearer ')) {
14+
throw createError({
15+
statusCode: 401,
16+
message: 'Unauthorized',
17+
})
18+
}
19+
20+
const token = bearer.slice(7) // Remove 'Bearer ' prefix
21+
if (!ai.serviceToken || !timingSafeEqual(Buffer.from(token), Buffer.from(ai.serviceToken))) {
22+
throw createError({
23+
statusCode: 401,
24+
message: 'Unauthorized',
25+
})
26+
}
27+
28+
const body = await readBody(event)
29+
if (!body?.message) {
30+
throw createError({
31+
statusCode: 400,
32+
message: 'Message is required',
33+
})
34+
}
35+
36+
const client = new OpenAI({
37+
apiKey: ai.apiKey,
38+
baseURL: ai.baseUrl,
39+
})
40+
41+
const agent = new Agent({
42+
name: 'Дата агент сети доставок "Суши Love"',
43+
instructions: 'У тебя есть доступ к данным партнеров сети. Отвечай всегда на русском в мужском роде.',
44+
model: new OpenAIChatCompletionsModel(client, ai.model),
45+
tools: [
46+
getPartnersTool,
47+
getPartnersByCityTool,
48+
],
49+
})
50+
51+
const result = await run(agent, body.message)
52+
53+
return {
54+
ok: true,
55+
message: result.finalOutput,
56+
}
57+
} catch (error) {
58+
throw errorResolver(error)
59+
}
60+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { tool } from '@openai/agents'
2+
import { repository } from '@roll-stack/database'
3+
import { z } from 'zod'
4+
5+
export const getPartnersTool = tool({
6+
name: 'get_all_partners',
7+
description: 'Get all partners',
8+
needsApproval: false,
9+
parameters: z.object({}),
10+
execute: async () => {
11+
return repository.partner.list()
12+
},
13+
})
14+
15+
export const getPartnersByCityTool = tool({
16+
name: 'get_partners_by_city',
17+
description: 'Get partners filtered by city name using case-insensitive partial matching',
18+
needsApproval: false,
19+
parameters: z.object({
20+
city: z.string(),
21+
}),
22+
execute: async ({ city }) => {
23+
const partners = await repository.partner.list()
24+
25+
return partners.filter((partner) => partner.city?.toLowerCase().includes(city.toLowerCase()))
26+
},
27+
})

0 commit comments

Comments
 (0)