@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
|
@@ -153,12 +153,19 @@ export async function login(page: Page, role: Role = 'admin'): Promise<void> {
|
|
|
153
153
|
const apiLoginForm = new URLSearchParams();
|
|
154
154
|
apiLoginForm.set('email', creds.email);
|
|
155
155
|
apiLoginForm.set('password', creds.password);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
// Retry-on-429 against the auth rate limit (5/60s per email). Capped
|
|
157
|
+
// exponential backoff: 1s, 2s, 4s — worst-case ~7s.
|
|
158
|
+
let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;
|
|
159
|
+
for (let retry = 0; retry < 4; retry += 1) {
|
|
160
|
+
apiLoginResponse = await page.request.post('/api/auth/login', {
|
|
161
|
+
headers: {
|
|
162
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
163
|
+
},
|
|
164
|
+
data: apiLoginForm.toString(),
|
|
165
|
+
}).catch(() => null);
|
|
166
|
+
if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));
|
|
168
|
+
}
|
|
162
169
|
if (apiLoginResponse?.ok()) {
|
|
163
170
|
const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;
|
|
164
171
|
const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;
|
|
@@ -53,11 +53,14 @@ type RoleSnapshots = {
|
|
|
53
53
|
|
|
54
54
|
const RESERVED_ROLE_NAMES = new Set(['superadmin', 'admin'])
|
|
55
55
|
|
|
56
|
-
function
|
|
57
|
-
if (typeof name !== 'string') return
|
|
56
|
+
function isReservedRoleName(name: string | undefined | null): boolean {
|
|
57
|
+
if (typeof name !== 'string') return false
|
|
58
58
|
const normalized = name.trim().toLowerCase()
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
return normalized.length > 0 && RESERVED_ROLE_NAMES.has(normalized)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertRoleNameAllowed(name: string | undefined | null) {
|
|
63
|
+
if (isReservedRoleName(name)) {
|
|
61
64
|
throw new CrudHttpError(400, { error: 'Role name is reserved' })
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -220,22 +223,17 @@ const updateRoleCommand: CommandHandler<Record<string, unknown>, Role> = {
|
|
|
220
223
|
async execute(rawInput, ctx) {
|
|
221
224
|
const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput)
|
|
222
225
|
const em = (ctx.container.resolve('em') as EntityManager)
|
|
226
|
+
const current = await findOneWithDecryption(em, Role, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
|
|
227
|
+
if (!current) throw new CrudHttpError(404, { error: 'Role not found' })
|
|
223
228
|
if (parsed.name !== undefined) {
|
|
224
|
-
assertRoleNameAllowed(parsed.name)
|
|
225
|
-
const current = await findOneWithDecryption(em, Role, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
|
|
226
|
-
if (!current) throw new CrudHttpError(404, { error: 'Role not found' })
|
|
227
|
-
assertRoleNameAllowed(current.name)
|
|
228
229
|
const nextName = parsed.name
|
|
230
|
+
if (nextName !== current.name) assertRoleNameAllowed(nextName)
|
|
229
231
|
if (nextName !== current.name) {
|
|
230
232
|
const assignments = await em.count(UserRole, { role: current, deletedAt: null })
|
|
231
233
|
if (assignments > 0) {
|
|
232
234
|
throw new CrudHttpError(400, { error: 'Role name cannot be changed while users are assigned' })
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
|
-
} else {
|
|
236
|
-
const current = await findOneWithDecryption(em, Role, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
|
|
237
|
-
if (!current) throw new CrudHttpError(404, { error: 'Role not found' })
|
|
238
|
-
assertRoleNameAllowed(current.name)
|
|
239
237
|
}
|
|
240
238
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
241
239
|
const role = await de.updateOrmEntity({
|
|
@@ -64,3 +64,14 @@ The default pipeline emits `catalog.pricing.resolve.before|after` events.
|
|
|
64
64
|
Key events follow the standard pattern in `events.ts`:
|
|
65
65
|
- `catalog.product.created/updated/deleted` — CRUD events
|
|
66
66
|
- `catalog.pricing.resolve.before/after` — pricing lifecycle (excluded from workflow triggers)
|
|
67
|
+
|
|
68
|
+
## AI Agents in This Module
|
|
69
|
+
|
|
70
|
+
Two typed AI agents ship from `ai-agents.ts`. See `/framework/ai-assistant/agents` for the full guide. Copy `packages/core/src/modules/customers/ai-agents.ts` + `ai-tools.ts` first; use this module's agents as the catalog-specific reference.
|
|
71
|
+
|
|
72
|
+
| Agent ID | Mode | Policy | Purpose |
|
|
73
|
+
|----------|------|--------|---------|
|
|
74
|
+
| `catalog.catalog_assistant` | chat | `read-only` | General operator explorer for products, categories, variants, prices, offers, product media, tags, option schemas, and unit conversions via the base catalog tool pack + general-purpose packs. |
|
|
75
|
+
| `catalog.merchandising_assistant` | chat | `read-only` (mutation-capable via per-tenant override unlocking `catalog.update_product`, `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`, `catalog.update_product_media_descriptions`) | D18 demo agent: proposes descriptions, attribute extractions, title variants, and price adjustments for the current selection on the products list page. |
|
|
76
|
+
|
|
77
|
+
The merchandising assistant is the Phase 2 D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation-capable authoring tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-context hydration helpers for the catalog agents
|
|
3
|
+
* (Phase 3 WS-A, Step 5.2).
|
|
4
|
+
*
|
|
5
|
+
* Two flavors:
|
|
6
|
+
*
|
|
7
|
+
* 1. `hydrateCatalogAssistantContext` — `catalog.catalog_assistant`. Loads
|
|
8
|
+
* a lightweight product summary for a single UUID (`catalog.product`),
|
|
9
|
+
* or a batch of up to 10 summaries when the request carries a
|
|
10
|
+
* comma-separated UUID list keyed as `catalog.products.list`.
|
|
11
|
+
*
|
|
12
|
+
* 2. `hydrateMerchandisingAssistantContext` —
|
|
13
|
+
* `catalog.merchandising_assistant`. Loads the full
|
|
14
|
+
* `catalog.get_product_bundle` aggregate for a single product, or a
|
|
15
|
+
* capped-at-10 selection via `catalog.list_selected_products`. When
|
|
16
|
+
* the request carries the products-list page view, the incoming
|
|
17
|
+
* `pageContext.extra.filter` is pretty-printed into the context block
|
|
18
|
+
* so the agent can reason about the narrowed set even when no
|
|
19
|
+
* selection is active.
|
|
20
|
+
*
|
|
21
|
+
* Both helpers route every read through an existing tool-pack handler
|
|
22
|
+
* (Step 3.10 base pack + Step 3.11 D18 pack) so the agent-reachable
|
|
23
|
+
* surface and the hydration surface stay in lock-step. Tenant + org
|
|
24
|
+
* scope is enforced by the tool handlers themselves; cross-tenant ids
|
|
25
|
+
* surface as `{ found: false }` / `missingIds`, which we translate to a
|
|
26
|
+
* silent null return (the runtime then proceeds without hydration).
|
|
27
|
+
*
|
|
28
|
+
* Error swallowing is required by the Step 3.2 runtime contract — a
|
|
29
|
+
* hydration fault MUST NEVER break the chat request.
|
|
30
|
+
*/
|
|
31
|
+
import type { AwilixContainer } from 'awilix'
|
|
32
|
+
import catalogAiTools from './ai-tools'
|
|
33
|
+
import type {
|
|
34
|
+
CatalogAiToolDefinition,
|
|
35
|
+
CatalogToolContext,
|
|
36
|
+
} from './ai-tools/types'
|
|
37
|
+
|
|
38
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
39
|
+
|
|
40
|
+
const SELECTION_CAP = 10
|
|
41
|
+
|
|
42
|
+
function isUuid(value: unknown): value is string {
|
|
43
|
+
return typeof value === 'string' && UUID_REGEX.test(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseSelectionIds(raw: string): string[] {
|
|
47
|
+
if (!raw) return []
|
|
48
|
+
const unique = new Set<string>()
|
|
49
|
+
for (const token of raw.split(',')) {
|
|
50
|
+
const trimmed = token.trim()
|
|
51
|
+
if (isUuid(trimmed)) unique.add(trimmed)
|
|
52
|
+
if (unique.size >= SELECTION_CAP) break
|
|
53
|
+
}
|
|
54
|
+
return Array.from(unique)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findTool(name: string): CatalogAiToolDefinition | null {
|
|
58
|
+
return (
|
|
59
|
+
(catalogAiTools as CatalogAiToolDefinition[]).find((tool) => tool.name === name) ?? null
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildToolContext(
|
|
64
|
+
container: AwilixContainer,
|
|
65
|
+
tenantId: string,
|
|
66
|
+
organizationId: string | null,
|
|
67
|
+
): CatalogToolContext {
|
|
68
|
+
return {
|
|
69
|
+
tenantId,
|
|
70
|
+
organizationId,
|
|
71
|
+
userId: null,
|
|
72
|
+
container,
|
|
73
|
+
userFeatures: [],
|
|
74
|
+
isSuperAdmin: true,
|
|
75
|
+
apiKeySecret: undefined,
|
|
76
|
+
sessionId: undefined,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderContextBlock(label: string, payload: unknown): string {
|
|
81
|
+
return `## Page context — ${label}\n${JSON.stringify(payload, null, 2)}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface HydrateCatalogContextInput {
|
|
85
|
+
entityType: string
|
|
86
|
+
recordId: string
|
|
87
|
+
container: AwilixContainer
|
|
88
|
+
tenantId: string | null
|
|
89
|
+
organizationId: string | null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SINGLE_PRODUCT_ENTITY_TYPES = new Set([
|
|
93
|
+
'product',
|
|
94
|
+
'catalog.product',
|
|
95
|
+
'catalog:catalog_product',
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
const PRODUCTS_LIST_ENTITY_TYPES = new Set([
|
|
99
|
+
'catalog.products.list',
|
|
100
|
+
'catalog.products.selection',
|
|
101
|
+
'products.list',
|
|
102
|
+
'products.selection',
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
async function invokeTool(
|
|
106
|
+
toolName: string,
|
|
107
|
+
args: Record<string, unknown>,
|
|
108
|
+
toolContext: CatalogToolContext,
|
|
109
|
+
reasonPrefix: string,
|
|
110
|
+
): Promise<unknown | null> {
|
|
111
|
+
const tool = findTool(toolName)
|
|
112
|
+
if (!tool) {
|
|
113
|
+
console.warn(`[${reasonPrefix}] resolvePageContext: tool "${toolName}" not registered`)
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const result = await tool.handler(args as never, toolContext)
|
|
118
|
+
return result ?? null
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`[${reasonPrefix}] resolvePageContext: tool "${toolName}" failed (reason="hydration_error"); skipping`,
|
|
122
|
+
error instanceof Error ? error.message : error,
|
|
123
|
+
)
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -----------------------------------------------------------------------------
|
|
129
|
+
// catalog.catalog_assistant hydration
|
|
130
|
+
// -----------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export async function hydrateCatalogAssistantContext(
|
|
133
|
+
input: HydrateCatalogContextInput,
|
|
134
|
+
): Promise<string | null> {
|
|
135
|
+
const tenantId = input.tenantId
|
|
136
|
+
if (!tenantId) return null
|
|
137
|
+
const entityType = input.entityType.trim().toLowerCase()
|
|
138
|
+
if (!entityType) return null
|
|
139
|
+
const toolContext = buildToolContext(input.container, tenantId, input.organizationId)
|
|
140
|
+
|
|
141
|
+
if (SINGLE_PRODUCT_ENTITY_TYPES.has(entityType)) {
|
|
142
|
+
if (!isUuid(input.recordId)) return null
|
|
143
|
+
const result = await invokeTool(
|
|
144
|
+
'catalog.get_product',
|
|
145
|
+
{ productId: input.recordId },
|
|
146
|
+
toolContext,
|
|
147
|
+
'catalog.catalog_assistant',
|
|
148
|
+
)
|
|
149
|
+
if (!result || typeof result !== 'object') return null
|
|
150
|
+
if ((result as { found?: boolean }).found === false) return null
|
|
151
|
+
return renderContextBlock(`Product ${input.recordId}`, result)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (PRODUCTS_LIST_ENTITY_TYPES.has(entityType)) {
|
|
155
|
+
const ids = parseSelectionIds(input.recordId)
|
|
156
|
+
if (ids.length === 0) return null
|
|
157
|
+
// Reuse the D18 merchandising bundle tool — its result carries
|
|
158
|
+
// summaries inside full bundles. For the base catalog_assistant we
|
|
159
|
+
// keep the payload lightweight by projecting each bundle onto the
|
|
160
|
+
// summary subset the agent cares about.
|
|
161
|
+
const result = await invokeTool(
|
|
162
|
+
'catalog.list_selected_products',
|
|
163
|
+
{ productIds: ids },
|
|
164
|
+
toolContext,
|
|
165
|
+
'catalog.catalog_assistant',
|
|
166
|
+
)
|
|
167
|
+
if (!result || typeof result !== 'object') return null
|
|
168
|
+
const { items, missingIds } = result as {
|
|
169
|
+
items?: Array<{ product?: unknown }>
|
|
170
|
+
missingIds?: string[]
|
|
171
|
+
}
|
|
172
|
+
const summaries = Array.isArray(items)
|
|
173
|
+
? items
|
|
174
|
+
.map((item) => (item && typeof item === 'object' ? (item as { product?: unknown }).product ?? null : null))
|
|
175
|
+
.filter((value) => value !== null)
|
|
176
|
+
: []
|
|
177
|
+
if (summaries.length === 0) return null
|
|
178
|
+
return renderContextBlock(
|
|
179
|
+
`Products selection (${summaries.length} of ${ids.length})`,
|
|
180
|
+
{ items: summaries, missingIds: missingIds ?? [] },
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// -----------------------------------------------------------------------------
|
|
188
|
+
// catalog.merchandising_assistant hydration
|
|
189
|
+
// -----------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export async function hydrateMerchandisingAssistantContext(
|
|
192
|
+
input: HydrateCatalogContextInput,
|
|
193
|
+
): Promise<string | null> {
|
|
194
|
+
const tenantId = input.tenantId
|
|
195
|
+
if (!tenantId) return null
|
|
196
|
+
const entityType = input.entityType.trim().toLowerCase()
|
|
197
|
+
if (!entityType) return null
|
|
198
|
+
const toolContext = buildToolContext(input.container, tenantId, input.organizationId)
|
|
199
|
+
|
|
200
|
+
if (SINGLE_PRODUCT_ENTITY_TYPES.has(entityType)) {
|
|
201
|
+
if (!isUuid(input.recordId)) return null
|
|
202
|
+
const result = await invokeTool(
|
|
203
|
+
'catalog.get_product_bundle',
|
|
204
|
+
{ productId: input.recordId },
|
|
205
|
+
toolContext,
|
|
206
|
+
'catalog.merchandising_assistant',
|
|
207
|
+
)
|
|
208
|
+
if (!result || typeof result !== 'object') return null
|
|
209
|
+
if ((result as { found?: boolean }).found === false) return null
|
|
210
|
+
return renderContextBlock(`Product bundle ${input.recordId}`, result)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (PRODUCTS_LIST_ENTITY_TYPES.has(entityType)) {
|
|
214
|
+
const ids = parseSelectionIds(input.recordId)
|
|
215
|
+
if (ids.length === 0) return null
|
|
216
|
+
const result = await invokeTool(
|
|
217
|
+
'catalog.list_selected_products',
|
|
218
|
+
{ productIds: ids },
|
|
219
|
+
toolContext,
|
|
220
|
+
'catalog.merchandising_assistant',
|
|
221
|
+
)
|
|
222
|
+
if (!result || typeof result !== 'object') return null
|
|
223
|
+
const { items, missingIds } = result as {
|
|
224
|
+
items?: unknown[]
|
|
225
|
+
missingIds?: string[]
|
|
226
|
+
}
|
|
227
|
+
const bundles = Array.isArray(items) ? items : []
|
|
228
|
+
if (bundles.length === 0) return null
|
|
229
|
+
return renderContextBlock(
|
|
230
|
+
`Products selection bundles (${bundles.length} of ${ids.length})`,
|
|
231
|
+
{
|
|
232
|
+
items: bundles,
|
|
233
|
+
missingIds: missingIds ?? [],
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null
|
|
239
|
+
}
|