@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3043.1a796c3920
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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog.list_categories` + `catalog.get_category` (Phase 1 WS-C, Step 3.10).
|
|
3
|
+
*
|
|
4
|
+
* Read-only category tools scoped by tenant + organization. `parentId: null`
|
|
5
|
+
* returns root nodes; any concrete UUID restricts to direct children.
|
|
6
|
+
*/
|
|
7
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
10
|
+
import { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
11
|
+
import { E } from '#generated/entities.ids.generated'
|
|
12
|
+
import { CatalogProductCategory } from '../data/entities'
|
|
13
|
+
import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
|
|
14
|
+
|
|
15
|
+
function resolveEm(ctx: CatalogToolContext): EntityManager {
|
|
16
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildScope(ctx: CatalogToolContext, tenantId: string) {
|
|
20
|
+
return { tenantId, organizationId: ctx.organizationId }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const listCategoriesInput = z
|
|
24
|
+
.object({
|
|
25
|
+
parentId: z
|
|
26
|
+
.union([z.string().uuid(), z.null()])
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Parent category id; pass `null` to list root nodes. Omit to list every category in scope.'),
|
|
29
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
|
|
30
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
31
|
+
includeArchived: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('When true, include soft-deleted categories. Defaults to active-only.'),
|
|
35
|
+
})
|
|
36
|
+
.passthrough()
|
|
37
|
+
|
|
38
|
+
const listCategoriesTool: CatalogAiToolDefinition = {
|
|
39
|
+
name: 'catalog.list_categories',
|
|
40
|
+
displayName: 'List categories',
|
|
41
|
+
description:
|
|
42
|
+
'List catalog categories scoped to tenant + organization. Use `parentId: null` to list roots or a specific uuid to fetch direct children.',
|
|
43
|
+
inputSchema: listCategoriesInput,
|
|
44
|
+
requiredFeatures: ['catalog.categories.view'],
|
|
45
|
+
tags: ['read', 'catalog'],
|
|
46
|
+
handler: async (rawInput, ctx) => {
|
|
47
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
48
|
+
const input = listCategoriesInput.parse(rawInput)
|
|
49
|
+
const em = resolveEm(ctx)
|
|
50
|
+
const limit = input.limit ?? 50
|
|
51
|
+
const offset = input.offset ?? 0
|
|
52
|
+
const where: Record<string, unknown> = { tenantId }
|
|
53
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
54
|
+
if (!input.includeArchived) where.deletedAt = null
|
|
55
|
+
if ('parentId' in input) {
|
|
56
|
+
where.parentId = input.parentId ?? null
|
|
57
|
+
}
|
|
58
|
+
const [rows, total] = await Promise.all([
|
|
59
|
+
findWithDecryption<CatalogProductCategory>(
|
|
60
|
+
em,
|
|
61
|
+
CatalogProductCategory,
|
|
62
|
+
where as any,
|
|
63
|
+
{ limit, offset, orderBy: { depth: 'asc', name: 'asc' } as any } as any,
|
|
64
|
+
buildScope(ctx, tenantId),
|
|
65
|
+
),
|
|
66
|
+
em.count(CatalogProductCategory, where as any),
|
|
67
|
+
])
|
|
68
|
+
const filtered = rows.filter((row) => row.tenantId === tenantId)
|
|
69
|
+
return {
|
|
70
|
+
items: filtered.map((row) => ({
|
|
71
|
+
id: row.id,
|
|
72
|
+
name: row.name,
|
|
73
|
+
slug: row.slug ?? null,
|
|
74
|
+
description: row.description ?? null,
|
|
75
|
+
parentId: row.parentId ?? null,
|
|
76
|
+
rootId: row.rootId ?? null,
|
|
77
|
+
treePath: row.treePath ?? null,
|
|
78
|
+
depth: row.depth,
|
|
79
|
+
childIds: Array.isArray(row.childIds) ? row.childIds : [],
|
|
80
|
+
isActive: !!row.isActive,
|
|
81
|
+
organizationId: row.organizationId ?? null,
|
|
82
|
+
tenantId: row.tenantId ?? null,
|
|
83
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
84
|
+
})),
|
|
85
|
+
total,
|
|
86
|
+
limit,
|
|
87
|
+
offset,
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getCategoryInput = z.object({
|
|
93
|
+
categoryId: z.string().uuid().describe('Category id (UUID).'),
|
|
94
|
+
includeRelated: z
|
|
95
|
+
.boolean()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe(
|
|
98
|
+
'When true, include direct children (capped at 100) and inherited ancestor refs. Custom fields are always included.',
|
|
99
|
+
),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const getCategoryTool: CatalogAiToolDefinition = {
|
|
103
|
+
name: 'catalog.get_category',
|
|
104
|
+
displayName: 'Get category',
|
|
105
|
+
description:
|
|
106
|
+
'Fetch a catalog category by id with core fields and (optionally) children + ancestor inheritance + custom fields. Returns { found: false } when missing or cross-tenant.',
|
|
107
|
+
inputSchema: getCategoryInput,
|
|
108
|
+
requiredFeatures: ['catalog.categories.view'],
|
|
109
|
+
tags: ['read', 'catalog'],
|
|
110
|
+
handler: async (rawInput, ctx) => {
|
|
111
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
112
|
+
const input = getCategoryInput.parse(rawInput)
|
|
113
|
+
const em = resolveEm(ctx)
|
|
114
|
+
const where: Record<string, unknown> = {
|
|
115
|
+
id: input.categoryId,
|
|
116
|
+
tenantId,
|
|
117
|
+
deletedAt: null,
|
|
118
|
+
}
|
|
119
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
120
|
+
const category = await findOneWithDecryption<CatalogProductCategory>(
|
|
121
|
+
em,
|
|
122
|
+
CatalogProductCategory,
|
|
123
|
+
where as any,
|
|
124
|
+
undefined,
|
|
125
|
+
buildScope(ctx, tenantId),
|
|
126
|
+
)
|
|
127
|
+
if (!category || category.tenantId !== tenantId) {
|
|
128
|
+
return { found: false as const, categoryId: input.categoryId }
|
|
129
|
+
}
|
|
130
|
+
const customFieldValues = await loadCustomFieldValues({
|
|
131
|
+
em,
|
|
132
|
+
entityId: E.catalog.catalog_product_category,
|
|
133
|
+
recordIds: [category.id],
|
|
134
|
+
tenantIdByRecord: { [category.id]: category.tenantId ?? null },
|
|
135
|
+
organizationIdByRecord: { [category.id]: category.organizationId ?? null },
|
|
136
|
+
tenantFallbacks: [category.tenantId ?? tenantId].filter((value): value is string => !!value),
|
|
137
|
+
})
|
|
138
|
+
const customFields = customFieldValues[category.id] ?? {}
|
|
139
|
+
let related: Record<string, unknown> | null = null
|
|
140
|
+
if (input.includeRelated) {
|
|
141
|
+
const scope = buildScope(ctx, tenantId)
|
|
142
|
+
const children = await findWithDecryption<CatalogProductCategory>(
|
|
143
|
+
em,
|
|
144
|
+
CatalogProductCategory,
|
|
145
|
+
{ tenantId, parentId: category.id, deletedAt: null } as any,
|
|
146
|
+
{ limit: 100, orderBy: { name: 'asc' } as any } as any,
|
|
147
|
+
scope,
|
|
148
|
+
)
|
|
149
|
+
related = {
|
|
150
|
+
children: children
|
|
151
|
+
.filter((row) => row.tenantId === tenantId)
|
|
152
|
+
.map((row) => ({
|
|
153
|
+
id: row.id,
|
|
154
|
+
name: row.name,
|
|
155
|
+
slug: row.slug ?? null,
|
|
156
|
+
depth: row.depth,
|
|
157
|
+
isActive: !!row.isActive,
|
|
158
|
+
})),
|
|
159
|
+
ancestorIds: Array.isArray(category.ancestorIds) ? [...category.ancestorIds] : [],
|
|
160
|
+
descendantIds: Array.isArray(category.descendantIds) ? [...category.descendantIds] : [],
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
found: true as const,
|
|
165
|
+
category: {
|
|
166
|
+
id: category.id,
|
|
167
|
+
name: category.name,
|
|
168
|
+
slug: category.slug ?? null,
|
|
169
|
+
description: category.description ?? null,
|
|
170
|
+
parentId: category.parentId ?? null,
|
|
171
|
+
rootId: category.rootId ?? null,
|
|
172
|
+
treePath: category.treePath ?? null,
|
|
173
|
+
depth: category.depth,
|
|
174
|
+
childIds: Array.isArray(category.childIds) ? [...category.childIds] : [],
|
|
175
|
+
ancestorIds: Array.isArray(category.ancestorIds) ? [...category.ancestorIds] : [],
|
|
176
|
+
descendantIds: Array.isArray(category.descendantIds) ? [...category.descendantIds] : [],
|
|
177
|
+
metadata: category.metadata ?? null,
|
|
178
|
+
isActive: !!category.isActive,
|
|
179
|
+
organizationId: category.organizationId ?? null,
|
|
180
|
+
tenantId: category.tenantId ?? null,
|
|
181
|
+
createdAt: category.createdAt ? new Date(category.createdAt).toISOString() : null,
|
|
182
|
+
updatedAt: category.updatedAt ? new Date(category.updatedAt).toISOString() : null,
|
|
183
|
+
},
|
|
184
|
+
customFields,
|
|
185
|
+
related,
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const categoriesAiTools: CatalogAiToolDefinition[] = [listCategoriesTool, getCategoryTool]
|
|
191
|
+
|
|
192
|
+
export default categoriesAiTools
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog.list_option_schemas` + `catalog.list_unit_conversions` (Phase 1
|
|
3
|
+
* WS-C, Step 3.10).
|
|
4
|
+
*
|
|
5
|
+
* Product-configuration surface: option schemas (variant axes) and unit
|
|
6
|
+
* conversions (UoM factors).
|
|
7
|
+
*
|
|
8
|
+
* Phase 3c of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
|
|
9
|
+
* both tools are now API-backed wrappers over the documented CRUD list
|
|
10
|
+
* routes (`GET /api/catalog/option-schemas` and
|
|
11
|
+
* `GET /api/catalog/product-unit-conversions`). Tool names, schemas,
|
|
12
|
+
* requiredFeatures, and output shapes are unchanged.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
|
|
16
|
+
import type {
|
|
17
|
+
AiApiOperationRequest,
|
|
18
|
+
AiToolExecutionContext,
|
|
19
|
+
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
|
|
20
|
+
import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
|
|
21
|
+
|
|
22
|
+
const listOptionSchemasInput = z
|
|
23
|
+
.object({
|
|
24
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
|
|
25
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
26
|
+
})
|
|
27
|
+
.passthrough()
|
|
28
|
+
|
|
29
|
+
type ListOptionSchemasInput = z.infer<typeof listOptionSchemasInput>
|
|
30
|
+
|
|
31
|
+
type ListOptionSchemasApiItem = {
|
|
32
|
+
id?: string
|
|
33
|
+
code?: string | null
|
|
34
|
+
name?: string | null
|
|
35
|
+
description?: string | null
|
|
36
|
+
schema?: unknown
|
|
37
|
+
metadata?: unknown
|
|
38
|
+
is_active?: boolean | null
|
|
39
|
+
isActive?: boolean | null
|
|
40
|
+
organization_id?: string | null
|
|
41
|
+
organizationId?: string | null
|
|
42
|
+
tenant_id?: string | null
|
|
43
|
+
tenantId?: string | null
|
|
44
|
+
created_at?: string | null
|
|
45
|
+
createdAt?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ListOptionSchemasApiResponse = {
|
|
49
|
+
items?: ListOptionSchemasApiItem[]
|
|
50
|
+
total?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type ListOptionSchemasOutput = {
|
|
54
|
+
items: Array<Record<string, unknown>>
|
|
55
|
+
total: number
|
|
56
|
+
limit: number
|
|
57
|
+
offset: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const listOptionSchemasTool = defineApiBackedAiTool<
|
|
61
|
+
ListOptionSchemasInput,
|
|
62
|
+
ListOptionSchemasApiResponse,
|
|
63
|
+
ListOptionSchemasOutput
|
|
64
|
+
>({
|
|
65
|
+
name: 'catalog.list_option_schemas',
|
|
66
|
+
displayName: 'List option schemas',
|
|
67
|
+
description:
|
|
68
|
+
'List product option schemas (variant axes, e.g. size/color definitions) for the caller tenant + organization.',
|
|
69
|
+
inputSchema: listOptionSchemasInput,
|
|
70
|
+
requiredFeatures: ['catalog.products.view'],
|
|
71
|
+
toOperation: (input, ctx) => {
|
|
72
|
+
assertTenantScope(ctx as unknown as CatalogToolContext)
|
|
73
|
+
const limit = input.limit ?? 50
|
|
74
|
+
const offset = input.offset ?? 0
|
|
75
|
+
const page = Math.floor(offset / limit) + 1
|
|
76
|
+
const operation: AiApiOperationRequest = {
|
|
77
|
+
method: 'GET',
|
|
78
|
+
path: '/catalog/option-schemas',
|
|
79
|
+
query: { page, pageSize: limit },
|
|
80
|
+
}
|
|
81
|
+
return operation
|
|
82
|
+
},
|
|
83
|
+
mapResponse: (response, input) => {
|
|
84
|
+
const limit = input.limit ?? 50
|
|
85
|
+
const offset = input.offset ?? 0
|
|
86
|
+
const data = (response.data ?? {}) as ListOptionSchemasApiResponse
|
|
87
|
+
const rawItems: ListOptionSchemasApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
88
|
+
return {
|
|
89
|
+
items: rawItems.map((row) => {
|
|
90
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
91
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
92
|
+
return {
|
|
93
|
+
id: row.id,
|
|
94
|
+
code: row.code,
|
|
95
|
+
name: row.name,
|
|
96
|
+
description: row.description ?? null,
|
|
97
|
+
schema: row.schema,
|
|
98
|
+
metadata: row.metadata ?? null,
|
|
99
|
+
isActive: !!(row.is_active ?? row.isActive),
|
|
100
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
101
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
102
|
+
createdAt,
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
total: typeof data.total === 'number' ? data.total : 0,
|
|
106
|
+
limit,
|
|
107
|
+
offset,
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}) as unknown as CatalogAiToolDefinition
|
|
111
|
+
|
|
112
|
+
const listUnitConversionsInput = z
|
|
113
|
+
.object({
|
|
114
|
+
productId: z.string().uuid().optional().describe('Restrict to unit conversions for this product.'),
|
|
115
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
|
|
116
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
117
|
+
})
|
|
118
|
+
.passthrough()
|
|
119
|
+
|
|
120
|
+
type ListUnitConversionsInput = z.infer<typeof listUnitConversionsInput>
|
|
121
|
+
|
|
122
|
+
type ListUnitConversionsApiItem = {
|
|
123
|
+
id?: string
|
|
124
|
+
product_id?: string | null
|
|
125
|
+
productId?: string | null
|
|
126
|
+
unit_code?: string | null
|
|
127
|
+
unitCode?: string | null
|
|
128
|
+
to_base_factor?: string | number | null
|
|
129
|
+
toBaseFactor?: string | number | null
|
|
130
|
+
sort_order?: number | null
|
|
131
|
+
sortOrder?: number | null
|
|
132
|
+
is_active?: boolean | null
|
|
133
|
+
isActive?: boolean | null
|
|
134
|
+
metadata?: unknown
|
|
135
|
+
organization_id?: string | null
|
|
136
|
+
organizationId?: string | null
|
|
137
|
+
tenant_id?: string | null
|
|
138
|
+
tenantId?: string | null
|
|
139
|
+
created_at?: string | null
|
|
140
|
+
createdAt?: string | null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type ListUnitConversionsApiResponse = {
|
|
144
|
+
items?: ListUnitConversionsApiItem[]
|
|
145
|
+
total?: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type ListUnitConversionsOutput = {
|
|
149
|
+
items: Array<Record<string, unknown>>
|
|
150
|
+
total: number
|
|
151
|
+
limit: number
|
|
152
|
+
offset: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const listUnitConversionsTool = defineApiBackedAiTool<
|
|
156
|
+
ListUnitConversionsInput,
|
|
157
|
+
ListUnitConversionsApiResponse,
|
|
158
|
+
ListUnitConversionsOutput
|
|
159
|
+
>({
|
|
160
|
+
name: 'catalog.list_unit_conversions',
|
|
161
|
+
displayName: 'List unit conversions',
|
|
162
|
+
description:
|
|
163
|
+
'List product unit conversions (alternate units with `toBaseFactor`) for the caller tenant + organization. Optionally narrow by product.',
|
|
164
|
+
inputSchema: listUnitConversionsInput,
|
|
165
|
+
requiredFeatures: ['catalog.products.view'],
|
|
166
|
+
toOperation: (input, ctx) => {
|
|
167
|
+
assertTenantScope(ctx as unknown as CatalogToolContext)
|
|
168
|
+
const limit = input.limit ?? 50
|
|
169
|
+
const offset = input.offset ?? 0
|
|
170
|
+
const page = Math.floor(offset / limit) + 1
|
|
171
|
+
const query: Record<string, string | number | boolean | null | undefined> = {
|
|
172
|
+
page,
|
|
173
|
+
pageSize: limit,
|
|
174
|
+
}
|
|
175
|
+
if (input.productId) query.productId = input.productId
|
|
176
|
+
const operation: AiApiOperationRequest = {
|
|
177
|
+
method: 'GET',
|
|
178
|
+
path: '/catalog/product-unit-conversions',
|
|
179
|
+
query,
|
|
180
|
+
}
|
|
181
|
+
return operation
|
|
182
|
+
},
|
|
183
|
+
mapResponse: (response, input) => {
|
|
184
|
+
const limit = input.limit ?? 50
|
|
185
|
+
const offset = input.offset ?? 0
|
|
186
|
+
const data = (response.data ?? {}) as ListUnitConversionsApiResponse
|
|
187
|
+
const rawItems: ListUnitConversionsApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
188
|
+
return {
|
|
189
|
+
items: rawItems.map((row) => {
|
|
190
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
191
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
192
|
+
const toBaseFactor = row.to_base_factor ?? row.toBaseFactor ?? null
|
|
193
|
+
return {
|
|
194
|
+
id: row.id,
|
|
195
|
+
unitCode: row.unit_code ?? row.unitCode ?? null,
|
|
196
|
+
toBaseFactor: toBaseFactor === null ? null : String(toBaseFactor),
|
|
197
|
+
sortOrder: row.sort_order ?? row.sortOrder ?? 0,
|
|
198
|
+
isActive: !!(row.is_active ?? row.isActive),
|
|
199
|
+
productId: row.product_id ?? row.productId ?? null,
|
|
200
|
+
metadata: row.metadata ?? null,
|
|
201
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
202
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
203
|
+
createdAt,
|
|
204
|
+
}
|
|
205
|
+
}),
|
|
206
|
+
total: typeof data.total === 'number' ? data.total : 0,
|
|
207
|
+
limit,
|
|
208
|
+
offset,
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}) as unknown as CatalogAiToolDefinition
|
|
212
|
+
|
|
213
|
+
export const configurationAiTools: CatalogAiToolDefinition[] = [
|
|
214
|
+
listOptionSchemasTool,
|
|
215
|
+
listUnitConversionsTool,
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
export default configurationAiTools
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog.list_product_media` + `catalog.list_product_tags` (Phase 1 WS-C,
|
|
3
|
+
* Step 3.10).
|
|
4
|
+
*
|
|
5
|
+
* Media tool returns metadata only — bytes flow through the Step 3.7
|
|
6
|
+
* attachment bridge, not this enumeration. Tags tool mirrors the existing
|
|
7
|
+
* `/api/catalog/tags` GET surface (feature-gated the same way).
|
|
8
|
+
*/
|
|
9
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
|
+
import { E } from '#generated/entities.ids.generated'
|
|
13
|
+
import { Attachment } from '@open-mercato/core/modules/attachments/data/entities'
|
|
14
|
+
import { CatalogProductTag, CatalogProductTagAssignment } from '../data/entities'
|
|
15
|
+
import { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'
|
|
16
|
+
|
|
17
|
+
function resolveEm(ctx: CatalogToolContext): EntityManager {
|
|
18
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildScope(ctx: CatalogToolContext, tenantId: string) {
|
|
22
|
+
return { tenantId, organizationId: ctx.organizationId }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const listProductMediaInput = z
|
|
26
|
+
.object({
|
|
27
|
+
productId: z.string().uuid().describe('Catalog product id (UUID).'),
|
|
28
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 50, max 100).'),
|
|
29
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
30
|
+
})
|
|
31
|
+
.passthrough()
|
|
32
|
+
|
|
33
|
+
const listProductMediaTool: CatalogAiToolDefinition = {
|
|
34
|
+
name: 'catalog.list_product_media',
|
|
35
|
+
displayName: 'List product media',
|
|
36
|
+
description:
|
|
37
|
+
'Enumerate media attachments (metadata only) associated with a catalog product. Use the attachment bridge (Step 3.7) to fetch bytes.',
|
|
38
|
+
inputSchema: listProductMediaInput,
|
|
39
|
+
requiredFeatures: ['catalog.products.view'],
|
|
40
|
+
tags: ['read', 'catalog'],
|
|
41
|
+
handler: async (rawInput, ctx) => {
|
|
42
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
43
|
+
const input = listProductMediaInput.parse(rawInput)
|
|
44
|
+
const em = resolveEm(ctx)
|
|
45
|
+
const limit = input.limit ?? 50
|
|
46
|
+
const offset = input.offset ?? 0
|
|
47
|
+
const where: Record<string, unknown> = {
|
|
48
|
+
tenantId,
|
|
49
|
+
entityId: E.catalog.catalog_product,
|
|
50
|
+
recordId: input.productId,
|
|
51
|
+
}
|
|
52
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
53
|
+
const [rows, total] = await Promise.all([
|
|
54
|
+
findWithDecryption<Attachment>(
|
|
55
|
+
em,
|
|
56
|
+
Attachment,
|
|
57
|
+
where as any,
|
|
58
|
+
{ limit, offset, orderBy: { createdAt: 'asc' } as any } as any,
|
|
59
|
+
buildScope(ctx, tenantId),
|
|
60
|
+
),
|
|
61
|
+
em.count(Attachment, where as any),
|
|
62
|
+
])
|
|
63
|
+
const filtered = rows.filter((row) => (row.tenantId ?? null) === tenantId)
|
|
64
|
+
return {
|
|
65
|
+
items: filtered.map((row) => ({
|
|
66
|
+
id: row.id,
|
|
67
|
+
fileName: row.fileName,
|
|
68
|
+
mimeType: row.mimeType,
|
|
69
|
+
fileSize: row.fileSize,
|
|
70
|
+
url: row.url,
|
|
71
|
+
storageDriver: row.storageDriver,
|
|
72
|
+
partitionCode: row.partitionCode,
|
|
73
|
+
entityId: row.entityId,
|
|
74
|
+
recordId: row.recordId,
|
|
75
|
+
organizationId: row.organizationId ?? null,
|
|
76
|
+
tenantId: row.tenantId ?? null,
|
|
77
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
78
|
+
})),
|
|
79
|
+
total,
|
|
80
|
+
limit,
|
|
81
|
+
offset,
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const listProductTagsInput = z
|
|
87
|
+
.object({
|
|
88
|
+
productId: z.string().uuid().describe('Catalog product id (UUID).'),
|
|
89
|
+
})
|
|
90
|
+
.passthrough()
|
|
91
|
+
|
|
92
|
+
const listProductTagsTool: CatalogAiToolDefinition = {
|
|
93
|
+
name: 'catalog.list_product_tags',
|
|
94
|
+
displayName: 'List product tags',
|
|
95
|
+
description:
|
|
96
|
+
'Enumerate tags assigned to a catalog product (label + slug). Returns { items, total }.',
|
|
97
|
+
inputSchema: listProductTagsInput,
|
|
98
|
+
requiredFeatures: ['catalog.products.view'],
|
|
99
|
+
tags: ['read', 'catalog'],
|
|
100
|
+
handler: async (rawInput, ctx) => {
|
|
101
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
102
|
+
const input = listProductTagsInput.parse(rawInput)
|
|
103
|
+
const em = resolveEm(ctx)
|
|
104
|
+
const where: Record<string, unknown> = { tenantId, product: input.productId }
|
|
105
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
106
|
+
const assignments = await findWithDecryption<CatalogProductTagAssignment>(
|
|
107
|
+
em,
|
|
108
|
+
CatalogProductTagAssignment,
|
|
109
|
+
where as any,
|
|
110
|
+
{ populate: ['tag'] as any } as any,
|
|
111
|
+
buildScope(ctx, tenantId),
|
|
112
|
+
)
|
|
113
|
+
const filtered = assignments.filter((assignment) => assignment.tenantId === tenantId)
|
|
114
|
+
const items = filtered
|
|
115
|
+
.map((assignment) => {
|
|
116
|
+
const tag = (assignment as any).tag as CatalogProductTag | string | null
|
|
117
|
+
if (!tag || typeof tag === 'string') return null
|
|
118
|
+
return { id: tag.id, label: tag.label, slug: tag.slug }
|
|
119
|
+
})
|
|
120
|
+
.filter((value): value is { id: string; label: string; slug: string } => value !== null)
|
|
121
|
+
return { items, total: items.length }
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const mediaTagsAiTools: CatalogAiToolDefinition[] = [listProductMediaTool, listProductTagsTool]
|
|
126
|
+
|
|
127
|
+
export default mediaTagsAiTools
|