@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +13 -1
- package/dist/helpers/integration/api.js +29 -16
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/auth.js +11 -6
- package/dist/helpers/integration/auth.js.map +3 -3
- package/dist/modules/auth/commands/roles.js +9 -12
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/catalog/ai-agents-context.js +147 -0
- package/dist/modules/catalog/ai-agents-context.js.map +7 -0
- package/dist/modules/catalog/ai-agents.js +383 -0
- package/dist/modules/catalog/ai-agents.js.map +7 -0
- package/dist/modules/catalog/ai-tools/_shared.js +318 -0
- package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
- package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/types.js +10 -0
- package/dist/modules/catalog/ai-tools/types.js.map +7 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools.js +28 -0
- package/dist/modules/catalog/ai-tools.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
- package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
- package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
- package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
- package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/events.js +7 -4
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/catalog/widgets/injection-table.js +13 -1
- package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/ai-agents-context.js +96 -0
- package/dist/modules/customers/ai-agents-context.js.map +7 -0
- package/dist/modules/customers/ai-agents.js +244 -0
- package/dist/modules/customers/ai-agents.js.map +7 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
- package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
- package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/people-pack.js +261 -0
- package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
- package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/types.js +10 -0
- package/dist/modules/customers/ai-tools/types.js.map +7 -0
- package/dist/modules/customers/ai-tools.js +20 -0
- package/dist/modules/customers/ai-tools.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection-table.js +26 -0
- package/dist/modules/customers/widgets/injection-table.js.map +7 -0
- package/dist/modules/inbox_ops/ai-tools.js +4 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
- package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
- package/dist/modules/notifications/setup.js +13 -0
- package/dist/modules/notifications/setup.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.setup.ts +18 -0
- package/package.json +5 -3
- package/src/helpers/integration/api.ts +38 -16
- package/src/helpers/integration/auth.ts +13 -6
- package/src/modules/auth/commands/roles.ts +10 -12
- package/src/modules/catalog/AGENTS.md +11 -0
- package/src/modules/catalog/ai-agents-context.ts +239 -0
- package/src/modules/catalog/ai-agents.ts +525 -0
- package/src/modules/catalog/ai-tools/_shared.ts +487 -0
- package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
- package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
- package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
- package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
- package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
- package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
- package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
- package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
- package/src/modules/catalog/ai-tools/types.ts +81 -0
- package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
- package/src/modules/catalog/ai-tools.ts +78 -0
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
- package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
- package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
- package/src/modules/catalog/events.ts +7 -4
- package/src/modules/catalog/i18n/de.json +17 -0
- package/src/modules/catalog/i18n/en.json +17 -0
- package/src/modules/catalog/i18n/es.json +17 -0
- package/src/modules/catalog/i18n/pl.json +17 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
- package/src/modules/catalog/widgets/injection-table.ts +12 -0
- package/src/modules/customer_accounts/i18n/de.json +5 -0
- package/src/modules/customer_accounts/i18n/en.json +5 -0
- package/src/modules/customer_accounts/i18n/es.json +5 -0
- package/src/modules/customer_accounts/i18n/pl.json +5 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
- package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
- package/src/modules/customers/AGENTS.md +13 -0
- package/src/modules/customers/ai-agents-context.ts +150 -0
- package/src/modules/customers/ai-agents.ts +355 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
- package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
- package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
- package/src/modules/customers/ai-tools/people-pack.ts +369 -0
- package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
- package/src/modules/customers/ai-tools/types.ts +76 -0
- package/src/modules/customers/ai-tools.ts +34 -0
- package/src/modules/customers/i18n/de.json +25 -0
- package/src/modules/customers/i18n/en.json +25 -0
- package/src/modules/customers/i18n/es.json +25 -0
- package/src/modules/customers/i18n/pl.json +25 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
- package/src/modules/customers/widgets/injection-table.ts +41 -0
- package/src/modules/inbox_ops/ai-tools.ts +4 -0
- package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
- package/src/modules/notifications/setup.ts +11 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog.list_products` + `catalog.get_product` (Phase 1 WS-C, Step 3.10).
|
|
3
|
+
*
|
|
4
|
+
* Read-only tools scoped to `ctx.tenantId` + `ctx.organizationId`. Mutation
|
|
5
|
+
* tools are deferred to Step 5.14 under the pending-action contract.
|
|
6
|
+
*
|
|
7
|
+
* Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
|
|
8
|
+
* `catalog.list_products` is now an API-backed wrapper over
|
|
9
|
+
* `GET /api/catalog/products`. Tool name, schema, requiredFeatures, and
|
|
10
|
+
* output shape are unchanged.
|
|
11
|
+
*/
|
|
12
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
|
|
15
|
+
import type {
|
|
16
|
+
AiApiOperationRequest,
|
|
17
|
+
AiToolExecutionContext,
|
|
18
|
+
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
|
|
19
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
20
|
+
import { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
21
|
+
import { E } from '#generated/entities.ids.generated'
|
|
22
|
+
import { Attachment } from '@open-mercato/core/modules/attachments/data/entities'
|
|
23
|
+
import {
|
|
24
|
+
CatalogProduct,
|
|
25
|
+
CatalogProductCategoryAssignment,
|
|
26
|
+
CatalogProductTagAssignment,
|
|
27
|
+
CatalogProductTag,
|
|
28
|
+
CatalogProductVariant,
|
|
29
|
+
CatalogProductPrice,
|
|
30
|
+
CatalogProductUnitConversion,
|
|
31
|
+
} from '../data/entities'
|
|
32
|
+
import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
|
|
33
|
+
|
|
34
|
+
function resolveEm(ctx: CatalogToolContext | AiToolExecutionContext): EntityManager {
|
|
35
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildScope(ctx: CatalogToolContext | AiToolExecutionContext, tenantId: string) {
|
|
39
|
+
return { tenantId, organizationId: ctx.organizationId }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const listProductsInput = z
|
|
43
|
+
.object({
|
|
44
|
+
q: z.string().trim().min(1).optional().describe('Optional search text matched against title / subtitle / sku / handle.'),
|
|
45
|
+
limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),
|
|
46
|
+
offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),
|
|
47
|
+
categoryId: z.string().uuid().optional().describe('Restrict to products assigned to this catalog category.'),
|
|
48
|
+
tagIds: z.array(z.string().uuid()).optional().describe('Restrict to products carrying at least one of these tag ids.'),
|
|
49
|
+
active: z.boolean().optional().describe('When true, only active (not archived) products are returned.'),
|
|
50
|
+
})
|
|
51
|
+
.passthrough()
|
|
52
|
+
|
|
53
|
+
type ListProductsInput = z.infer<typeof listProductsInput>
|
|
54
|
+
|
|
55
|
+
type ListProductsApiItem = {
|
|
56
|
+
id?: string
|
|
57
|
+
title?: string | null
|
|
58
|
+
subtitle?: string | null
|
|
59
|
+
sku?: string | null
|
|
60
|
+
handle?: string | null
|
|
61
|
+
product_type?: string | null
|
|
62
|
+
productType?: string | null
|
|
63
|
+
status_entry_id?: string | null
|
|
64
|
+
statusEntryId?: string | null
|
|
65
|
+
primary_currency_code?: string | null
|
|
66
|
+
primaryCurrencyCode?: string | null
|
|
67
|
+
default_media_id?: string | null
|
|
68
|
+
defaultMediaId?: string | null
|
|
69
|
+
default_media_url?: string | null
|
|
70
|
+
defaultMediaUrl?: string | null
|
|
71
|
+
is_active?: boolean | null
|
|
72
|
+
isActive?: boolean | null
|
|
73
|
+
is_configurable?: boolean | null
|
|
74
|
+
isConfigurable?: boolean | null
|
|
75
|
+
organization_id?: string | null
|
|
76
|
+
organizationId?: string | null
|
|
77
|
+
tenant_id?: string | null
|
|
78
|
+
tenantId?: string | null
|
|
79
|
+
created_at?: string | null
|
|
80
|
+
createdAt?: string | null
|
|
81
|
+
updated_at?: string | null
|
|
82
|
+
updatedAt?: string | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type ListProductsApiResponse = {
|
|
86
|
+
items?: ListProductsApiItem[]
|
|
87
|
+
total?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type ListProductsOutput = {
|
|
91
|
+
items: Array<Record<string, unknown>>
|
|
92
|
+
total: number
|
|
93
|
+
limit: number
|
|
94
|
+
offset: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const listProductsTool = defineApiBackedAiTool<
|
|
98
|
+
ListProductsInput,
|
|
99
|
+
ListProductsApiResponse,
|
|
100
|
+
ListProductsOutput
|
|
101
|
+
>({
|
|
102
|
+
name: 'catalog.list_products',
|
|
103
|
+
displayName: 'List products',
|
|
104
|
+
description:
|
|
105
|
+
'Search / list catalog products for the caller tenant + organization. Returns { items, total, limit, offset }.',
|
|
106
|
+
inputSchema: listProductsInput,
|
|
107
|
+
requiredFeatures: ['catalog.products.view'],
|
|
108
|
+
toOperation: (input, ctx) => {
|
|
109
|
+
assertTenantScope(ctx as unknown as CatalogToolContext)
|
|
110
|
+
const limit = input.limit ?? 50
|
|
111
|
+
const offset = input.offset ?? 0
|
|
112
|
+
const page = Math.floor(offset / limit) + 1
|
|
113
|
+
|
|
114
|
+
const query: Record<string, string | number | boolean | null | undefined> = {
|
|
115
|
+
page,
|
|
116
|
+
pageSize: limit,
|
|
117
|
+
}
|
|
118
|
+
if (input.q?.trim()) query.search = input.q.trim()
|
|
119
|
+
if (input.categoryId) query.categoryIds = input.categoryId
|
|
120
|
+
if (input.tagIds && input.tagIds.length > 0) query.tagIds = input.tagIds.join(',')
|
|
121
|
+
if (input.active === true) query.isActive = 'true'
|
|
122
|
+
|
|
123
|
+
const operation: AiApiOperationRequest = {
|
|
124
|
+
method: 'GET',
|
|
125
|
+
path: '/catalog/products',
|
|
126
|
+
query,
|
|
127
|
+
}
|
|
128
|
+
return operation
|
|
129
|
+
},
|
|
130
|
+
mapResponse: (response, input) => {
|
|
131
|
+
const limit = input.limit ?? 50
|
|
132
|
+
const offset = input.offset ?? 0
|
|
133
|
+
const data = (response.data ?? {}) as ListProductsApiResponse
|
|
134
|
+
const rawItems: ListProductsApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
135
|
+
return {
|
|
136
|
+
items: rawItems.map((row) => {
|
|
137
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
138
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
139
|
+
const updatedAtRaw = row.updated_at ?? row.updatedAt ?? null
|
|
140
|
+
const updatedAt = updatedAtRaw ? new Date(String(updatedAtRaw)).toISOString() : null
|
|
141
|
+
return {
|
|
142
|
+
id: row.id,
|
|
143
|
+
title: row.title ?? null,
|
|
144
|
+
subtitle: row.subtitle ?? null,
|
|
145
|
+
sku: row.sku ?? null,
|
|
146
|
+
handle: row.handle ?? null,
|
|
147
|
+
productType: row.product_type ?? row.productType ?? null,
|
|
148
|
+
statusEntryId: row.status_entry_id ?? row.statusEntryId ?? null,
|
|
149
|
+
primaryCurrencyCode: row.primary_currency_code ?? row.primaryCurrencyCode ?? null,
|
|
150
|
+
defaultMediaId: row.default_media_id ?? row.defaultMediaId ?? null,
|
|
151
|
+
defaultMediaUrl: row.default_media_url ?? row.defaultMediaUrl ?? null,
|
|
152
|
+
imageUrl: row.default_media_url ?? row.defaultMediaUrl ?? null,
|
|
153
|
+
isActive: !!(row.is_active ?? row.isActive),
|
|
154
|
+
isConfigurable: !!(row.is_configurable ?? row.isConfigurable),
|
|
155
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
156
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
157
|
+
createdAt,
|
|
158
|
+
updatedAt,
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
161
|
+
total: typeof data.total === 'number' ? data.total : 0,
|
|
162
|
+
limit,
|
|
163
|
+
offset,
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}) as unknown as CatalogAiToolDefinition
|
|
167
|
+
|
|
168
|
+
const getProductInput = z.object({
|
|
169
|
+
productId: z.string().uuid().describe('Catalog product id (UUID).'),
|
|
170
|
+
includeRelated: z
|
|
171
|
+
.boolean()
|
|
172
|
+
.optional()
|
|
173
|
+
.describe(
|
|
174
|
+
'When true, include categories, tags, variants, prices (base + offers), media (metadata only), unit conversions, and custom fields (each related list capped at 100).',
|
|
175
|
+
),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const getProductTool: CatalogAiToolDefinition = {
|
|
179
|
+
name: 'catalog.get_product',
|
|
180
|
+
displayName: 'Get product',
|
|
181
|
+
description:
|
|
182
|
+
'Fetch a catalog product by id with core fields and (optionally) categories, tags, variants, prices, media metadata, unit conversions, and custom fields. Returns { found: false } when the record is outside tenant/org scope or missing.',
|
|
183
|
+
inputSchema: getProductInput,
|
|
184
|
+
requiredFeatures: ['catalog.products.view'],
|
|
185
|
+
tags: ['read', 'catalog'],
|
|
186
|
+
handler: async (rawInput, ctx) => {
|
|
187
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
188
|
+
const input = getProductInput.parse(rawInput)
|
|
189
|
+
const em = resolveEm(ctx)
|
|
190
|
+
const where: Record<string, unknown> = {
|
|
191
|
+
id: input.productId,
|
|
192
|
+
tenantId,
|
|
193
|
+
deletedAt: null,
|
|
194
|
+
}
|
|
195
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
196
|
+
const product = await findOneWithDecryption<CatalogProduct>(
|
|
197
|
+
em,
|
|
198
|
+
CatalogProduct,
|
|
199
|
+
where as any,
|
|
200
|
+
undefined,
|
|
201
|
+
buildScope(ctx, tenantId),
|
|
202
|
+
)
|
|
203
|
+
if (!product || product.tenantId !== tenantId) {
|
|
204
|
+
return { found: false as const, productId: input.productId }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let related: Record<string, unknown> | null = null
|
|
208
|
+
let customFields: Record<string, unknown> = {}
|
|
209
|
+
if (input.includeRelated) {
|
|
210
|
+
const scope = buildScope(ctx, tenantId)
|
|
211
|
+
const [
|
|
212
|
+
categoryAssignments,
|
|
213
|
+
tagAssignments,
|
|
214
|
+
variants,
|
|
215
|
+
prices,
|
|
216
|
+
mediaAttachments,
|
|
217
|
+
unitConversions,
|
|
218
|
+
customFieldValues,
|
|
219
|
+
] = await Promise.all([
|
|
220
|
+
findWithDecryption<CatalogProductCategoryAssignment>(
|
|
221
|
+
em,
|
|
222
|
+
CatalogProductCategoryAssignment,
|
|
223
|
+
{ tenantId, product: product.id } as any,
|
|
224
|
+
{ limit: 100, populate: ['category'] as any } as any,
|
|
225
|
+
scope,
|
|
226
|
+
),
|
|
227
|
+
findWithDecryption<CatalogProductTagAssignment>(
|
|
228
|
+
em,
|
|
229
|
+
CatalogProductTagAssignment,
|
|
230
|
+
{ tenantId, product: product.id } as any,
|
|
231
|
+
{ limit: 100, populate: ['tag'] as any } as any,
|
|
232
|
+
scope,
|
|
233
|
+
),
|
|
234
|
+
findWithDecryption<CatalogProductVariant>(
|
|
235
|
+
em,
|
|
236
|
+
CatalogProductVariant,
|
|
237
|
+
{ tenantId, product: product.id, deletedAt: null } as any,
|
|
238
|
+
{ limit: 100, orderBy: { createdAt: 'asc' } as any } as any,
|
|
239
|
+
scope,
|
|
240
|
+
),
|
|
241
|
+
findWithDecryption<CatalogProductPrice>(
|
|
242
|
+
em,
|
|
243
|
+
CatalogProductPrice,
|
|
244
|
+
{ tenantId, product: product.id } as any,
|
|
245
|
+
{ limit: 100, orderBy: { createdAt: 'asc' } as any } as any,
|
|
246
|
+
scope,
|
|
247
|
+
),
|
|
248
|
+
findWithDecryption<Attachment>(
|
|
249
|
+
em,
|
|
250
|
+
Attachment,
|
|
251
|
+
{ tenantId, entityId: E.catalog.catalog_product, recordId: product.id } as any,
|
|
252
|
+
{ limit: 100, orderBy: { createdAt: 'asc' } as any } as any,
|
|
253
|
+
scope,
|
|
254
|
+
),
|
|
255
|
+
findWithDecryption<CatalogProductUnitConversion>(
|
|
256
|
+
em,
|
|
257
|
+
CatalogProductUnitConversion,
|
|
258
|
+
{ tenantId, product: product.id, deletedAt: null } as any,
|
|
259
|
+
{ limit: 100, orderBy: { sortOrder: 'asc', createdAt: 'asc' } as any } as any,
|
|
260
|
+
scope,
|
|
261
|
+
),
|
|
262
|
+
loadCustomFieldValues({
|
|
263
|
+
em,
|
|
264
|
+
entityId: E.catalog.catalog_product,
|
|
265
|
+
recordIds: [product.id],
|
|
266
|
+
tenantIdByRecord: { [product.id]: product.tenantId ?? null },
|
|
267
|
+
organizationIdByRecord: { [product.id]: product.organizationId ?? null },
|
|
268
|
+
tenantFallbacks: [product.tenantId ?? tenantId].filter((value): value is string => !!value),
|
|
269
|
+
}),
|
|
270
|
+
])
|
|
271
|
+
customFields = customFieldValues[product.id] ?? {}
|
|
272
|
+
related = {
|
|
273
|
+
categories: categoryAssignments
|
|
274
|
+
.map((assignment) => {
|
|
275
|
+
const category = (assignment as any).category
|
|
276
|
+
if (!category || typeof category === 'string') {
|
|
277
|
+
const fallbackId = typeof category === 'string' ? category : null
|
|
278
|
+
return fallbackId ? { id: fallbackId, name: null, slug: null } : null
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
id: category.id,
|
|
282
|
+
name: category.name ?? null,
|
|
283
|
+
slug: category.slug ?? null,
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
.filter((value): value is { id: string; name: string | null; slug: string | null } => value !== null),
|
|
287
|
+
tags: tagAssignments
|
|
288
|
+
.map((assignment) => {
|
|
289
|
+
const tag = (assignment as any).tag as CatalogProductTag | string | null
|
|
290
|
+
if (!tag || typeof tag === 'string') return null
|
|
291
|
+
return { id: tag.id, label: tag.label, slug: tag.slug }
|
|
292
|
+
})
|
|
293
|
+
.filter((value): value is { id: string; label: string; slug: string } => value !== null),
|
|
294
|
+
variants: variants.map((variant) => ({
|
|
295
|
+
id: variant.id,
|
|
296
|
+
name: variant.name ?? null,
|
|
297
|
+
sku: variant.sku ?? null,
|
|
298
|
+
barcode: variant.barcode ?? null,
|
|
299
|
+
optionValues: variant.optionValues ?? null,
|
|
300
|
+
defaultMediaId: variant.defaultMediaId ?? null,
|
|
301
|
+
defaultMediaUrl: variant.defaultMediaUrl ?? null,
|
|
302
|
+
isDefault: !!variant.isDefault,
|
|
303
|
+
isActive: !!variant.isActive,
|
|
304
|
+
})),
|
|
305
|
+
prices: prices.map((price) => ({
|
|
306
|
+
id: price.id,
|
|
307
|
+
priceKindId: (price as any).priceKind && typeof (price as any).priceKind === 'object'
|
|
308
|
+
? (price as any).priceKind.id
|
|
309
|
+
: (price as any).priceKind ?? null,
|
|
310
|
+
currencyCode: price.currencyCode,
|
|
311
|
+
kind: price.kind,
|
|
312
|
+
minQuantity: price.minQuantity,
|
|
313
|
+
maxQuantity: price.maxQuantity ?? null,
|
|
314
|
+
unitPriceNet: price.unitPriceNet ?? null,
|
|
315
|
+
unitPriceGross: price.unitPriceGross ?? null,
|
|
316
|
+
channelId: price.channelId ?? null,
|
|
317
|
+
offerId: (price as any).offer && typeof (price as any).offer === 'object'
|
|
318
|
+
? (price as any).offer.id
|
|
319
|
+
: (price as any).offer ?? null,
|
|
320
|
+
variantId: (price as any).variant && typeof (price as any).variant === 'object'
|
|
321
|
+
? (price as any).variant.id
|
|
322
|
+
: (price as any).variant ?? null,
|
|
323
|
+
startsAt: price.startsAt ? new Date(price.startsAt).toISOString() : null,
|
|
324
|
+
endsAt: price.endsAt ? new Date(price.endsAt).toISOString() : null,
|
|
325
|
+
})),
|
|
326
|
+
media: mediaAttachments.map((attachment) => ({
|
|
327
|
+
id: attachment.id,
|
|
328
|
+
fileName: attachment.fileName,
|
|
329
|
+
mimeType: attachment.mimeType,
|
|
330
|
+
fileSize: attachment.fileSize,
|
|
331
|
+
url: attachment.url,
|
|
332
|
+
})),
|
|
333
|
+
unitConversions: unitConversions.map((row) => ({
|
|
334
|
+
id: row.id,
|
|
335
|
+
unitCode: row.unitCode,
|
|
336
|
+
toBaseFactor: row.toBaseFactor,
|
|
337
|
+
sortOrder: row.sortOrder,
|
|
338
|
+
isActive: !!row.isActive,
|
|
339
|
+
})),
|
|
340
|
+
customFields,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
found: true as const,
|
|
346
|
+
product: {
|
|
347
|
+
id: product.id,
|
|
348
|
+
title: product.title,
|
|
349
|
+
subtitle: product.subtitle ?? null,
|
|
350
|
+
description: product.description ?? null,
|
|
351
|
+
sku: product.sku ?? null,
|
|
352
|
+
handle: product.handle ?? null,
|
|
353
|
+
productType: product.productType,
|
|
354
|
+
statusEntryId: product.statusEntryId ?? null,
|
|
355
|
+
primaryCurrencyCode: product.primaryCurrencyCode ?? null,
|
|
356
|
+
taxRate: product.taxRate ?? null,
|
|
357
|
+
taxRateId: product.taxRateId ?? null,
|
|
358
|
+
defaultUnit: product.defaultUnit ?? null,
|
|
359
|
+
defaultSalesUnit: product.defaultSalesUnit ?? null,
|
|
360
|
+
defaultSalesUnitQuantity: product.defaultSalesUnitQuantity ?? null,
|
|
361
|
+
unitPriceEnabled: !!product.unitPriceEnabled,
|
|
362
|
+
unitPriceReferenceUnit: product.unitPriceReferenceUnit ?? null,
|
|
363
|
+
unitPriceBaseQuantity: product.unitPriceBaseQuantity ?? null,
|
|
364
|
+
defaultMediaId: product.defaultMediaId ?? null,
|
|
365
|
+
defaultMediaUrl: product.defaultMediaUrl ?? null,
|
|
366
|
+
imageUrl: product.defaultMediaUrl ?? null,
|
|
367
|
+
weightValue: product.weightValue ?? null,
|
|
368
|
+
weightUnit: product.weightUnit ?? null,
|
|
369
|
+
dimensions: product.dimensions ?? null,
|
|
370
|
+
metadata: product.metadata ?? null,
|
|
371
|
+
customFieldsetCode: product.customFieldsetCode ?? null,
|
|
372
|
+
isConfigurable: !!product.isConfigurable,
|
|
373
|
+
isActive: !!product.isActive,
|
|
374
|
+
organizationId: product.organizationId ?? null,
|
|
375
|
+
tenantId: product.tenantId ?? null,
|
|
376
|
+
createdAt: product.createdAt ? new Date(product.createdAt).toISOString() : null,
|
|
377
|
+
updatedAt: product.updatedAt ? new Date(product.updatedAt).toISOString() : null,
|
|
378
|
+
},
|
|
379
|
+
customFields,
|
|
380
|
+
related,
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const productsAiTools: CatalogAiToolDefinition[] = [listProductsTool, getProductTool]
|
|
386
|
+
|
|
387
|
+
export default productsAiTools
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog.show_stats` — example dynamic UI-part tool.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the generic UI-part contract: any tool can return a JSON
|
|
5
|
+
* envelope `{ uiPart: { componentId, payload } }` (or a `uiParts: [...]`
|
|
6
|
+
* array) and the chat client surfaces it inline. The catalog stats card
|
|
7
|
+
* is the canonical dynamic example; module authors copy this file +
|
|
8
|
+
* `components/CatalogStatsCard.tsx` to ship their own cards.
|
|
9
|
+
*
|
|
10
|
+
* Read-only — no `prepareMutation` gate, no DB writes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { CatalogProduct, CatalogProductCategory, CatalogProductTag } from '../data/entities'
|
|
16
|
+
import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
|
|
17
|
+
|
|
18
|
+
function resolveEm(ctx: CatalogToolContext): EntityManager {
|
|
19
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const showStatsInput = z
|
|
23
|
+
.object({
|
|
24
|
+
note: z
|
|
25
|
+
.string()
|
|
26
|
+
.max(160)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
'Optional one-line note to render below the stats grid (e.g. "as of today" or a quick observation).',
|
|
30
|
+
),
|
|
31
|
+
})
|
|
32
|
+
.passthrough()
|
|
33
|
+
|
|
34
|
+
const showStatsTool: CatalogAiToolDefinition = {
|
|
35
|
+
name: 'catalog.show_stats',
|
|
36
|
+
displayName: 'Show catalog stats',
|
|
37
|
+
description:
|
|
38
|
+
'Displays a compact "Catalog overview" card in the chat with live counts: total products, active products, categories, and tags for the current tenant. Use this when the operator asks for a high-level snapshot of the catalog (e.g. "give me catalog stats", "how many products do we have", "show overview"). Returns a `uiPart` envelope so the registered `catalog.stats-card` component renders inline — no fenced code block needed.',
|
|
39
|
+
inputSchema: showStatsInput,
|
|
40
|
+
requiredFeatures: ['catalog.products.view'],
|
|
41
|
+
tags: ['read', 'catalog', 'stats', 'ui'],
|
|
42
|
+
isMutation: false,
|
|
43
|
+
handler: async (rawInput, ctx) => {
|
|
44
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
45
|
+
const input = showStatsInput.parse(rawInput)
|
|
46
|
+
const em = resolveEm(ctx)
|
|
47
|
+
|
|
48
|
+
// CatalogProductTag has no soft-delete column, so the `deletedAt: null`
|
|
49
|
+
// filter (used for products + categories which DO have it) would throw
|
|
50
|
+
// `Trying to query by not existing property CatalogProductTag.deletedAt`.
|
|
51
|
+
// Build per-entity scopes that only include the fields that actually exist.
|
|
52
|
+
const tenantScope: Record<string, unknown> = { tenantId }
|
|
53
|
+
if (ctx.organizationId) tenantScope.organizationId = ctx.organizationId
|
|
54
|
+
const softDeleteScope: Record<string, unknown> = { ...tenantScope, deletedAt: null }
|
|
55
|
+
|
|
56
|
+
const [products, activeProducts, categories, tags] = await Promise.all([
|
|
57
|
+
em.count(CatalogProduct, softDeleteScope as never),
|
|
58
|
+
em.count(CatalogProduct, { ...softDeleteScope, isActive: true } as never),
|
|
59
|
+
em.count(CatalogProductCategory, softDeleteScope as never),
|
|
60
|
+
em.count(CatalogProductTag, tenantScope as never),
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
uiPart: {
|
|
65
|
+
componentId: 'catalog.stats-card',
|
|
66
|
+
payload: {
|
|
67
|
+
products,
|
|
68
|
+
activeProducts,
|
|
69
|
+
categories,
|
|
70
|
+
tags,
|
|
71
|
+
generatedAt: new Date().toISOString(),
|
|
72
|
+
note: input.note,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
// Plain-text mirror so the model can summarize what it just rendered
|
|
76
|
+
// without parsing the UI part envelope itself.
|
|
77
|
+
summary: `Catalog snapshot: ${products} products (${activeProducts} active), ${categories} categories, ${tags} tags.`,
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const statsAiTools: CatalogAiToolDefinition[] = [showStatsTool]
|
|
83
|
+
|
|
84
|
+
export default statsAiTools
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local AI tool shape for the catalog module (Phase 1 WS-C, Step 3.10).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the pattern established by the customers pack (Step 3.9): the
|
|
5
|
+
* catalog module declares its read-only tools as plain objects whose shape
|
|
6
|
+
* is a strict subset of `AiToolDefinition` from `@open-mercato/ai-assistant`.
|
|
7
|
+
* Keeping the shape local avoids pulling the ai-assistant package into the
|
|
8
|
+
* core module graph for jest and sidesteps a cross-package `moduleNameMapper`.
|
|
9
|
+
* The generator walks every module root for a default / `aiTools` export with
|
|
10
|
+
* this shape.
|
|
11
|
+
*/
|
|
12
|
+
import type { z } from 'zod'
|
|
13
|
+
import type { AwilixContainer } from 'awilix'
|
|
14
|
+
|
|
15
|
+
export interface CatalogToolContext {
|
|
16
|
+
tenantId: string | null
|
|
17
|
+
organizationId: string | null
|
|
18
|
+
userId: string | null
|
|
19
|
+
container: AwilixContainer
|
|
20
|
+
userFeatures: string[]
|
|
21
|
+
isSuperAdmin: boolean
|
|
22
|
+
apiKeySecret?: string
|
|
23
|
+
sessionId?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shape returned by `loadBeforeRecord` on a single-record mutation tool.
|
|
28
|
+
* Mirrors `AiToolLoadBeforeSingleRecord` from `@open-mercato/ai-assistant/lib/types`.
|
|
29
|
+
*/
|
|
30
|
+
export interface CatalogToolLoadBeforeSingleRecord {
|
|
31
|
+
recordId: string
|
|
32
|
+
entityType: string
|
|
33
|
+
recordVersion: string | null
|
|
34
|
+
before: Record<string, unknown>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shape returned by `loadBeforeRecords` on a bulk mutation tool. Mirrors
|
|
39
|
+
* `AiToolLoadBeforeRecord` from `@open-mercato/ai-assistant/lib/types` — the
|
|
40
|
+
* Step 5.6 `prepareMutation` runtime wraps this into the `records[]` array on
|
|
41
|
+
* the emitted `mutation-preview-card`.
|
|
42
|
+
*/
|
|
43
|
+
export interface CatalogToolLoadBeforeRecord {
|
|
44
|
+
recordId: string
|
|
45
|
+
entityType: string
|
|
46
|
+
label: string
|
|
47
|
+
recordVersion: string | null
|
|
48
|
+
before: Record<string, unknown>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CatalogAiToolDefinition<TInput = unknown, TOutput = unknown> {
|
|
52
|
+
name: string
|
|
53
|
+
displayName?: string
|
|
54
|
+
description: string
|
|
55
|
+
inputSchema: z.ZodType<TInput>
|
|
56
|
+
requiredFeatures?: string[]
|
|
57
|
+
tags?: string[]
|
|
58
|
+
isMutation?: boolean
|
|
59
|
+
isBulk?: boolean
|
|
60
|
+
maxCallsPerTurn?: number
|
|
61
|
+
supportsAttachments?: boolean
|
|
62
|
+
handler: (input: TInput, context: CatalogToolContext) => Promise<TOutput>
|
|
63
|
+
loadBeforeRecord?: (
|
|
64
|
+
input: TInput,
|
|
65
|
+
context: CatalogToolContext,
|
|
66
|
+
) => Promise<CatalogToolLoadBeforeSingleRecord | null>
|
|
67
|
+
loadBeforeRecords?: (
|
|
68
|
+
input: TInput,
|
|
69
|
+
context: CatalogToolContext,
|
|
70
|
+
) => Promise<CatalogToolLoadBeforeRecord[]>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function assertTenantScope(ctx: CatalogToolContext): {
|
|
74
|
+
tenantId: string
|
|
75
|
+
organizationId: string | null
|
|
76
|
+
} {
|
|
77
|
+
if (!ctx.tenantId) {
|
|
78
|
+
throw new Error('Tenant context is required for catalog.* tools')
|
|
79
|
+
}
|
|
80
|
+
return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
81
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog.list_variants` (Phase 1 WS-C, Step 3.10).
|
|
3
|
+
*
|
|
4
|
+
* Enumerate variants for a single product with option values + media refs.
|
|
5
|
+
*
|
|
6
|
+
* Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
|
|
7
|
+
* `catalog.list_variants` is now an API-backed wrapper over
|
|
8
|
+
* `GET /api/catalog/variants`. Tool name, schema, requiredFeatures, and
|
|
9
|
+
* output shape are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
|
|
13
|
+
import type {
|
|
14
|
+
AiApiOperationRequest,
|
|
15
|
+
AiToolExecutionContext,
|
|
16
|
+
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
|
|
17
|
+
import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
|
|
18
|
+
|
|
19
|
+
const listVariantsInput = z
|
|
20
|
+
.object({
|
|
21
|
+
productId: z.string().uuid().describe('Parent product id (UUID).'),
|
|
22
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
|
|
23
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
24
|
+
})
|
|
25
|
+
.passthrough()
|
|
26
|
+
|
|
27
|
+
type ListVariantsInput = z.infer<typeof listVariantsInput>
|
|
28
|
+
|
|
29
|
+
type ListVariantsApiItem = {
|
|
30
|
+
id?: string
|
|
31
|
+
product_id?: string | null
|
|
32
|
+
productId?: string | null
|
|
33
|
+
name?: string | null
|
|
34
|
+
sku?: string | null
|
|
35
|
+
barcode?: string | null
|
|
36
|
+
status_entry_id?: string | null
|
|
37
|
+
statusEntryId?: string | null
|
|
38
|
+
option_values?: unknown
|
|
39
|
+
optionValues?: unknown
|
|
40
|
+
default_media_id?: string | null
|
|
41
|
+
defaultMediaId?: string | null
|
|
42
|
+
default_media_url?: string | null
|
|
43
|
+
defaultMediaUrl?: string | null
|
|
44
|
+
weight_value?: string | number | null
|
|
45
|
+
weightValue?: string | number | null
|
|
46
|
+
weight_unit?: string | null
|
|
47
|
+
weightUnit?: string | null
|
|
48
|
+
dimensions?: unknown
|
|
49
|
+
tax_rate?: string | number | null
|
|
50
|
+
taxRate?: string | number | null
|
|
51
|
+
tax_rate_id?: string | null
|
|
52
|
+
taxRateId?: string | null
|
|
53
|
+
is_default?: boolean | null
|
|
54
|
+
isDefault?: boolean | null
|
|
55
|
+
is_active?: boolean | null
|
|
56
|
+
isActive?: boolean | null
|
|
57
|
+
organization_id?: string | null
|
|
58
|
+
organizationId?: string | null
|
|
59
|
+
tenant_id?: string | null
|
|
60
|
+
tenantId?: string | null
|
|
61
|
+
created_at?: string | null
|
|
62
|
+
createdAt?: string | null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type ListVariantsApiResponse = {
|
|
66
|
+
items?: ListVariantsApiItem[]
|
|
67
|
+
total?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ListVariantsOutput = {
|
|
71
|
+
items: Array<Record<string, unknown>>
|
|
72
|
+
total: number
|
|
73
|
+
limit: number
|
|
74
|
+
offset: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const listVariantsTool = defineApiBackedAiTool<
|
|
78
|
+
ListVariantsInput,
|
|
79
|
+
ListVariantsApiResponse,
|
|
80
|
+
ListVariantsOutput
|
|
81
|
+
>({
|
|
82
|
+
name: 'catalog.list_variants',
|
|
83
|
+
displayName: 'List variants',
|
|
84
|
+
description:
|
|
85
|
+
'List the variants of a catalog product (including option values, SKU, barcode, default media ref). Returns { items, total, limit, offset }.',
|
|
86
|
+
inputSchema: listVariantsInput,
|
|
87
|
+
requiredFeatures: ['catalog.products.view'],
|
|
88
|
+
toOperation: (input, ctx) => {
|
|
89
|
+
assertTenantScope(ctx as unknown as CatalogToolContext)
|
|
90
|
+
const limit = input.limit ?? 50
|
|
91
|
+
const offset = input.offset ?? 0
|
|
92
|
+
const page = Math.floor(offset / limit) + 1
|
|
93
|
+
|
|
94
|
+
const query: Record<string, string | number | boolean | null | undefined> = {
|
|
95
|
+
page,
|
|
96
|
+
pageSize: limit,
|
|
97
|
+
productId: input.productId,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const operation: AiApiOperationRequest = {
|
|
101
|
+
method: 'GET',
|
|
102
|
+
path: '/catalog/variants',
|
|
103
|
+
query,
|
|
104
|
+
}
|
|
105
|
+
return operation
|
|
106
|
+
},
|
|
107
|
+
mapResponse: (response, input) => {
|
|
108
|
+
const limit = input.limit ?? 50
|
|
109
|
+
const offset = input.offset ?? 0
|
|
110
|
+
const data = (response.data ?? {}) as ListVariantsApiResponse
|
|
111
|
+
const rawItems: ListVariantsApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
112
|
+
return {
|
|
113
|
+
items: rawItems.map((row) => {
|
|
114
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
115
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
116
|
+
return {
|
|
117
|
+
id: row.id,
|
|
118
|
+
name: row.name ?? null,
|
|
119
|
+
sku: row.sku ?? null,
|
|
120
|
+
barcode: row.barcode ?? null,
|
|
121
|
+
statusEntryId: row.status_entry_id ?? row.statusEntryId ?? null,
|
|
122
|
+
optionValues: row.option_values ?? row.optionValues ?? null,
|
|
123
|
+
defaultMediaId: row.default_media_id ?? row.defaultMediaId ?? null,
|
|
124
|
+
defaultMediaUrl: row.default_media_url ?? row.defaultMediaUrl ?? null,
|
|
125
|
+
weightValue: row.weight_value ?? row.weightValue ?? null,
|
|
126
|
+
weightUnit: row.weight_unit ?? row.weightUnit ?? null,
|
|
127
|
+
dimensions: row.dimensions ?? null,
|
|
128
|
+
taxRate: row.tax_rate ?? row.taxRate ?? null,
|
|
129
|
+
taxRateId: row.tax_rate_id ?? row.taxRateId ?? null,
|
|
130
|
+
isDefault: !!(row.is_default ?? row.isDefault),
|
|
131
|
+
isActive: !!(row.is_active ?? row.isActive),
|
|
132
|
+
productId: row.product_id ?? row.productId ?? null,
|
|
133
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
134
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
135
|
+
createdAt,
|
|
136
|
+
}
|
|
137
|
+
}),
|
|
138
|
+
total: typeof data.total === 'number' ? data.total : 0,
|
|
139
|
+
limit,
|
|
140
|
+
offset,
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
}) as unknown as CatalogAiToolDefinition
|
|
144
|
+
|
|
145
|
+
export const variantsAiTools: CatalogAiToolDefinition[] = [listVariantsTool]
|
|
146
|
+
|
|
147
|
+
export default variantsAiTools
|