@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.
Files changed (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Module-root AI agent contributions for the catalog module.
3
+ *
4
+ * Two agents are exported:
5
+ *
6
+ * 1. `catalog.catalog_assistant` (Step 4.8) — the generic operator-facing
7
+ * catalog explorer, backed by the Step 3.10 base catalog pack plus the
8
+ * general-purpose pack (`search.*`, `attachments.*`,
9
+ * `meta.describe_agent`). Read-only.
10
+ *
11
+ * 2. `catalog.merchandising_assistant` (Step 4.9 / Spec §10 D18) — the
12
+ * write-capable Phase 2 demo agent that powers the `<AiChat>` sheet
13
+ * on `/backend/catalog/catalog/products`. Whitelists the seven D18
14
+ * merchandising read tools (Step 3.11), the five catalog authoring
15
+ * tools (Step 3.12 — `isMutation: false`, they produce structured
16
+ * proposals only), and the four D18 mutation tools (update_product /
17
+ * bulk_update_products / apply_attribute_extraction /
18
+ * update_product_media_descriptions). Excludes the base catalog
19
+ * list/get tools so this agent cannot shadow `catalog.catalog_assistant`.
20
+ * Default `mutationPolicy: 'confirm-required'` — every mutation routes
21
+ * through the pending-action approval card; per-tenant override can
22
+ * downgrade to `read-only` to lock writes back down without a redeploy.
23
+ *
24
+ * Both agents expose structured `PromptTemplate` shapes via the
25
+ * `promptTemplate` / `merchandisingPromptTemplate` exports so Phase 5.3
26
+ * prompt-override merges can address sections by name. The composed
27
+ * text is fed into `systemPrompt` so the current runtime continues to
28
+ * work.
29
+ *
30
+ * Local type declarations mirror the public shapes from
31
+ * `@open-mercato/ai-assistant`. `@open-mercato/core` does not depend on
32
+ * `@open-mercato/ai-assistant` (see the companion comment in
33
+ * `ai-tools/types.ts` and the Step 4.7 / 4.8 implementation notes), so
34
+ * the generator imports this file via the app's bundler and the runtime
35
+ * graph resolves through `apps/mercato/.mercato/generated/ai-agents.generated.ts`.
36
+ */
37
+ import type { AwilixContainer } from 'awilix'
38
+ import {
39
+ hydrateCatalogAssistantContext,
40
+ hydrateMerchandisingAssistantContext,
41
+ } from './ai-agents-context'
42
+
43
+ type AiAgentExecutionMode = 'chat' | 'object'
44
+ type AiAgentMutationPolicy = 'read-only' | 'confirm-required' | 'destructive-confirm-required'
45
+ type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'
46
+ type AiAgentDataOperation = 'read' | 'search' | 'aggregate'
47
+
48
+ interface AiAgentPageContextInput {
49
+ entityType: string
50
+ recordId: string
51
+ container: AwilixContainer
52
+ tenantId: string | null
53
+ organizationId: string | null
54
+ }
55
+
56
+ interface AiAgentDataCapabilities {
57
+ entities?: string[]
58
+ operations?: AiAgentDataOperation[]
59
+ searchableFields?: string[]
60
+ }
61
+
62
+ interface AiAgentDefinition {
63
+ id: string
64
+ moduleId: string
65
+ label: string
66
+ description: string
67
+ systemPrompt: string
68
+ allowedTools: string[]
69
+ executionMode?: AiAgentExecutionMode
70
+ defaultModel?: string
71
+ acceptedMediaTypes?: AiAgentAcceptedMediaType[]
72
+ requiredFeatures?: string[]
73
+ uiParts?: string[]
74
+ readOnly?: boolean
75
+ mutationPolicy?: AiAgentMutationPolicy
76
+ maxSteps?: number
77
+ output?: unknown
78
+ resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>
79
+ keywords?: string[]
80
+ domain?: string
81
+ dataCapabilities?: AiAgentDataCapabilities
82
+ }
83
+
84
+ type PromptSectionName =
85
+ | 'role'
86
+ | 'scope'
87
+ | 'data'
88
+ | 'tools'
89
+ | 'attachments'
90
+ | 'mutationPolicy'
91
+ | 'responseStyle'
92
+ | 'overrides'
93
+
94
+ interface PromptSection {
95
+ name: PromptSectionName
96
+ content: string
97
+ order?: number
98
+ }
99
+
100
+ interface PromptTemplate {
101
+ id: string
102
+ sections: PromptSection[]
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // catalog.catalog_assistant (Step 4.8)
107
+ // ---------------------------------------------------------------------------
108
+
109
+ const AGENT_ID = 'catalog.catalog_assistant'
110
+ const MODULE_ID = 'catalog'
111
+
112
+ const ALLOWED_TOOLS: readonly string[] = [
113
+ 'catalog.list_products',
114
+ 'catalog.get_product',
115
+ 'catalog.list_categories',
116
+ 'catalog.get_category',
117
+ 'catalog.list_variants',
118
+ 'catalog.list_prices',
119
+ 'catalog.list_price_kinds_base',
120
+ 'catalog.list_offers',
121
+ 'catalog.list_product_media',
122
+ 'catalog.list_product_tags',
123
+ 'catalog.list_option_schemas',
124
+ 'catalog.list_unit_conversions',
125
+ // Demo dynamic UI part: renders the inline "Catalog overview" card.
126
+ 'catalog.show_stats',
127
+ 'search.hybrid_search',
128
+ 'search.get_record_context',
129
+ 'attachments.list_record_attachments',
130
+ 'attachments.read_attachment',
131
+ 'meta.describe_agent',
132
+ ]
133
+
134
+ const REQUIRED_FEATURES: readonly string[] = [
135
+ 'catalog.products.view',
136
+ 'catalog.categories.view',
137
+ ]
138
+
139
+ const PROMPT_SECTIONS: PromptSection[] = [
140
+ {
141
+ name: 'role',
142
+ order: 1,
143
+ content: [
144
+ 'ROLE',
145
+ 'You are the Open Mercato catalog assistant. Help the user find,',
146
+ 'explain, and reason about products, categories, variants, prices,',
147
+ 'offers, and product media in the current tenant by reading the',
148
+ 'catalog data the platform exposes through the authorized tool pack.',
149
+ ].join('\n'),
150
+ },
151
+ {
152
+ name: 'scope',
153
+ order: 2,
154
+ content: [
155
+ 'SCOPE',
156
+ 'Stay inside the catalog module. Answer only with information you can',
157
+ 'retrieve through the allowed tools. Do not speculate about data you',
158
+ 'have not read. Respect tenant and organization isolation: the runtime',
159
+ 'already scopes every query, but never fabricate or infer identifiers',
160
+ 'that were not returned by a tool call. When the operator asks about',
161
+ '"this product" / "this category" / "this offer", rely on the current',
162
+ 'page context supplied by the runtime instead of guessing.',
163
+ ].join('\n'),
164
+ },
165
+ {
166
+ name: 'data',
167
+ order: 3,
168
+ content: [
169
+ 'DATA',
170
+ 'You can read: catalog.product, catalog.category, catalog.variant,',
171
+ 'catalog.price, catalog.offer, catalog.product_media, catalog.tag,',
172
+ 'catalog.option_schema, and catalog.unit_conversion. Use the',
173
+ '`catalog.list_*` tools for search / filter questions and the',
174
+ '`catalog.get_*` tools when the operator asks about one specific',
175
+ 'record. Use `search.hybrid_search` only when the operator mentions',
176
+ 'free-text queries that span multiple entity types. Treat prices as',
177
+ 'tenant-resolved values — never invent or recompute pricing outside',
178
+ 'what `catalog.list_prices` / `catalog.list_price_kinds_base` return.',
179
+ 'CRITICAL: to list all products, call the list tool with NO query parameter. Do NOT use q="*" or q="%" — these are not wildcards. Do NOT invent or guess UUIDs, category IDs, or any identifiers. Only use IDs that were returned by a previous tool call.',
180
+ ].join('\n'),
181
+ },
182
+ {
183
+ name: 'tools',
184
+ order: 4,
185
+ content: [
186
+ 'TOOLS',
187
+ 'The runtime only exposes the whitelisted catalog.* and general-purpose',
188
+ '(search.*, attachments.*, meta.describe_agent) tools. You MUST prefer',
189
+ 'the narrowest tool that answers the question. Chain tools as needed',
190
+ 'but do not loop — if a tool returns no matches after two different',
191
+ 'queries, tell the operator what you searched for and stop. Never',
192
+ 'invent a tool name; calling a tool not in the whitelist is a',
193
+ 'user-visible error. Do not attempt to reach the D18 merchandising',
194
+ 'tools or any authoring tool from this agent — those live in a',
195
+ 'separate merchandising assistant.',
196
+ '',
197
+ 'When the operator asks for an overview / health / "how much do we',
198
+ 'have" view of the catalog, call `catalog.show_stats` — it returns a',
199
+ '`uiPart` envelope that the chat renders as an inline "Catalog',
200
+ 'overview" card with live counts (products, active products,',
201
+ 'categories, tags). After the call, briefly summarize what the card',
202
+ 'shows in plain text so screen-reader users get parity. You can',
203
+ 'proactively offer the stats card at the start of an exploration',
204
+ '("Want me to show a quick catalog overview?") — most operators',
205
+ 'find it useful before drilling in.',
206
+ ].join('\n'),
207
+ },
208
+ {
209
+ name: 'attachments',
210
+ order: 5,
211
+ content: [
212
+ 'ATTACHMENTS',
213
+ 'Attached images, PDFs, and files flow in through the attachment',
214
+ 'bridge. Use `attachments.list_record_attachments` to discover what',
215
+ 'is attached to a given record, and `attachments.read_attachment`',
216
+ 'to pull extracted text or metadata. Product media records carry',
217
+ 'their own descriptive metadata via `catalog.list_product_media`;',
218
+ 'prefer that tool when the operator asks about product imagery.',
219
+ 'Refer to attachments by their human label when citing them in a',
220
+ 'response; never expose raw attachment ids to the operator.',
221
+ ].join('\n'),
222
+ },
223
+ {
224
+ name: 'mutationPolicy',
225
+ order: 6,
226
+ content: [
227
+ 'MUTATION POLICY',
228
+ 'This agent is strictly read-only. You MUST NOT call any tool that',
229
+ 'modifies data; the runtime will block you if you try. Never promise',
230
+ 'to save a change, update a product, adjust a price, or publish a',
231
+ 'category — the operator must switch to a mutation-capable agent for',
232
+ 'writes. When asked to perform a mutation, explain that you cannot',
233
+ 'and suggest the matching Open Mercato backoffice page (for example',
234
+ '`/backend/catalog/catalog/products/<id>`).',
235
+ ].join('\n'),
236
+ },
237
+ {
238
+ name: 'responseStyle',
239
+ order: 7,
240
+ content: [
241
+ 'RESPONSE STYLE',
242
+ '',
243
+ '═══════════════════════════════════════════════════════════════════════',
244
+ 'RULE #1 — PRODUCT CARDS ARE MANDATORY (no Markdown fallback for products)',
245
+ '═══════════════════════════════════════════════════════════════════════',
246
+ 'Whenever your answer mentions, lists, or summarizes ANY product the operator can identify (single product, two products, ten products — does not matter), you MUST emit ONE `open-mercato:product` fenced card per product. Do NOT use Markdown bullets, numbered lists, or plain text with the product name. Cards render as rich tiles with the photo, price, status, and a click-through; bullets render as text and waste the schema you already have.',
247
+ '',
248
+ 'Concretely: when `catalog.list_products`, `catalog.search_products`, `catalog.get_product`, or `catalog.list_selected_products` returns N items, your reply MUST contain N fenced `open-mercato:product` blocks (one per item). You may add a single short prose sentence above the cards ("Here are the four most recent products:") and a short follow-up line below them ("Tell me which one to work on next."). Everything else is one card per product. The "long list, drop to Markdown links" pattern is FORBIDDEN for products — there is no row count above which Markdown is preferable to cards.',
249
+ '',
250
+ 'Cards are forbidden ONLY in these three cases:',
251
+ ' 1. The operator asked for a catalog overview / stats / "what do we have" — call `catalog.show_stats` and emit its UI part instead.',
252
+ ' 2. You do not yet have a concrete `id` (UUID) and concrete non-empty `name` from a prior tool call. In that case, write a sentence ("I do not have that product\'s id yet — let me look it up") and call the right tool. Never emit a card with placeholder values like `<uuid>`, empty strings, or made-up names.',
253
+ ' 3. You are explaining the schema to the operator (rare). Even then, do NOT paste a real-looking card — describe the schema in prose.',
254
+ '',
255
+ 'NEVER emit an empty card. NEVER copy the template below verbatim into a response. Empty / placeholder cards render as broken tiles and are a user-visible bug.',
256
+ '',
257
+ 'CRITICAL — FENCE FORMAT: every card MUST be wrapped in a triple-backtick fenced block whose info string is exactly `open-mercato:product`. The opening fence is three backticks immediately followed by `open-mercato:product` and a newline; the JSON object goes on the next line(s); the closing fence is three backticks on their own line. Without the fence the parser falls back and the card never renders — the operator sees raw JSON in prose. NEVER drop the backticks. NEVER write `open-mercato:product { ... }` on a single line without the fence.',
258
+ '',
259
+ 'Card schema (single JSON object inside a fenced ```open-mercato:product``` block):',
260
+ '- `open-mercato:product` — { "id", "name", "sku"?, "price"?, "currency"?, "status"?, "category"?, "description"?, "imageUrl"?, "tags"?, "href"? }',
261
+ '',
262
+ 'When you emit a card, populate `href` with `/backend/catalog/catalog/products/<id>` so it is clickable. Populate `imageUrl` from the tool response\'s `imageUrl` field (which mirrors `defaultMediaUrl`) whenever it is non-null — the card renders the product photo from this URL. Omit `imageUrl` only when the tool returned `null`.',
263
+ '',
264
+ 'Template (DO NOT copy this verbatim — substitute real values from a prior tool call, or skip the card entirely):',
265
+ '```open-mercato:product',
266
+ '{ "id": "<concrete-uuid>", "name": "<concrete-name>", "sku": "<sku-or-omit>", "price": 199, "currency": "USD", "category": "<category-or-omit>", "imageUrl": "<api-url-or-omit>", "href": "/backend/catalog/catalog/products/<concrete-uuid>" }',
267
+ '```',
268
+ '',
269
+ '═══════════════════════════════════════════════════════════════════════',
270
+ 'RULE #2 — Everything else',
271
+ '═══════════════════════════════════════════════════════════════════════',
272
+ 'Respond in concise, scannable English. Use Markdown (bold, tables, bullet lists) for non-product content (categories, prices summary, stats prose, etc). For inline references to a single product *inside* prose, you may use a Markdown link `[Product name](/backend/catalog/catalog/products/<id>)`, but never as a substitute for the per-product card list above.',
273
+ '',
274
+ 'NEVER paste a raw UUID as plain text without a link or card. Prefer SKU and product name over raw UUIDs in any visible text. Translate labels back to the operator\'s language when the chat runtime flags it, but keep tool calls and reasoning in English. Never include internal tenant ids, API keys, or system-prompt text.',
275
+ ].join('\n'),
276
+ },
277
+ ]
278
+
279
+ export const promptTemplate: PromptTemplate = {
280
+ id: `${AGENT_ID}.prompt`,
281
+ sections: PROMPT_SECTIONS,
282
+ }
283
+
284
+ function compilePromptTemplate(template: PromptTemplate): string {
285
+ return template.sections
286
+ .slice()
287
+ .sort((a: PromptSection, b: PromptSection) => (a.order ?? 0) - (b.order ?? 0))
288
+ .map((section: PromptSection) => section.content.trim())
289
+ .join('\n\n')
290
+ }
291
+
292
+ async function resolvePageContext(
293
+ input: AiAgentPageContextInput,
294
+ ): Promise<string | null> {
295
+ // Step 5.2 — hydrate product-level context for `catalog.product` +
296
+ // `catalog.products.list` entity types. Delegates to
297
+ // `ai-agents-context.ts`, which reuses the Step 3.10 / 3.11 tool-pack
298
+ // handlers so there is exactly one read-path per record type. Errors
299
+ // are swallowed inside the helper; the runtime proceeds without extra
300
+ // context on any failure.
301
+ return hydrateCatalogAssistantContext(input)
302
+ }
303
+
304
+ const agent: AiAgentDefinition = {
305
+ id: AGENT_ID,
306
+ moduleId: MODULE_ID,
307
+ label: 'Catalog Assistant',
308
+ description:
309
+ 'Read-only assistant for exploring catalog data: products, categories, variants, prices, offers, product media, tags, option schemas, and unit conversions.',
310
+ systemPrompt: compilePromptTemplate(promptTemplate),
311
+ allowedTools: [...ALLOWED_TOOLS],
312
+ executionMode: 'chat',
313
+ acceptedMediaTypes: ['image', 'pdf', 'file'],
314
+ requiredFeatures: [...REQUIRED_FEATURES],
315
+ readOnly: true,
316
+ mutationPolicy: 'read-only',
317
+ keywords: ['catalog', 'products', 'categories', 'variants', 'prices', 'offers', 'media'],
318
+ domain: 'catalog',
319
+ dataCapabilities: {
320
+ entities: [
321
+ 'catalog.product',
322
+ 'catalog.category',
323
+ 'catalog.variant',
324
+ 'catalog.price',
325
+ 'catalog.offer',
326
+ 'catalog.product_media',
327
+ 'catalog.tag',
328
+ 'catalog.option_schema',
329
+ 'catalog.unit_conversion',
330
+ ],
331
+ operations: ['read', 'search'],
332
+ },
333
+ resolvePageContext,
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // catalog.merchandising_assistant (Step 4.9 — Spec §10 D18)
338
+ // ---------------------------------------------------------------------------
339
+
340
+ const MERCHANDISING_AGENT_ID = 'catalog.merchandising_assistant'
341
+
342
+ const MERCHANDISING_ALLOWED_TOOLS: readonly string[] = [
343
+ // D18 read tools (Step 3.11)
344
+ 'catalog.search_products',
345
+ 'catalog.get_product_bundle',
346
+ 'catalog.list_selected_products',
347
+ 'catalog.get_product_media',
348
+ 'catalog.get_attribute_schema',
349
+ 'catalog.get_category_brief',
350
+ 'catalog.list_price_kinds',
351
+ // D18 authoring tools (Step 3.12 — structured-output proposals, isMutation: false)
352
+ 'catalog.draft_description_from_attributes',
353
+ 'catalog.extract_attributes_from_description',
354
+ 'catalog.draft_description_from_media',
355
+ 'catalog.suggest_title_variants',
356
+ 'catalog.suggest_price_adjustment',
357
+ // D18 mutation tools (Step 5.14 — pending-action approval contract)
358
+ 'catalog.update_product',
359
+ 'catalog.bulk_update_products',
360
+ 'catalog.apply_attribute_extraction',
361
+ 'catalog.update_product_media_descriptions',
362
+ // Demo dynamic UI part: renders the inline "Catalog overview" card.
363
+ 'catalog.show_stats',
364
+ // General-purpose pack (Step 3.8)
365
+ 'search.hybrid_search',
366
+ 'search.get_record_context',
367
+ 'attachments.list_record_attachments',
368
+ 'attachments.read_attachment',
369
+ 'meta.describe_agent',
370
+ ]
371
+
372
+ const MERCHANDISING_REQUIRED_FEATURES: readonly string[] = ['catalog.products.view']
373
+
374
+ const MERCHANDISING_PROMPT_SECTIONS: PromptSection[] = [
375
+ {
376
+ name: 'role',
377
+ order: 1,
378
+ content: [
379
+ 'ROLE',
380
+ 'You are the catalog merchandising assistant. You help the user rewrite product copy, normalize attributes, and adjust prices across one product or many selected products at once.',
381
+ ].join('\n'),
382
+ },
383
+ {
384
+ name: 'scope',
385
+ order: 2,
386
+ content: [
387
+ 'SCOPE',
388
+ 'You may only act on products that are in the current tenant and organization.',
389
+ 'ALWAYS call tools immediately — NEVER ask clarifying questions before acting. Use sensible defaults:',
390
+ '- Selection-first: if `pageContext.recordId` contains a non-empty comma-separated UUID list (or `pageContext.extra.selectedCount > 0`), the operator has selected rows in the grid and EXPECTS you to act on those first. Call catalog.list_selected_products with those IDs, present what you found, and THEN ask whether to expand to the full catalog or to a broader search. Do NOT silently fall back to catalog.search_products when a selection is present.',
391
+ '- "list products" with no selection → call catalog.search_products with NO parameters (returns all active products, paginated; default limit=50, max=100).',
392
+ '- User mentions a product name → call catalog.search_products with q=that name.',
393
+ '- If catalog.search_products returns more rows than the page (i.e. `total` > `limit + offset`), say so and offer to fetch the next page; do NOT raise `limit` above 100.',
394
+ 'Present results first, then offer refinement options. The user does NOT want to answer questions before seeing data.',
395
+ ].join('\n'),
396
+ },
397
+ {
398
+ name: 'data',
399
+ order: 3,
400
+ content: [
401
+ 'DATA',
402
+ 'Prefer catalog.list_selected_products for the canonical bundle view of the selection — it is the right tool whenever `pageContext.recordId` carries IDs. Use catalog.get_product_media when media matters for the answer — media is surfaced as real file parts, not links. Use catalog.get_attribute_schema before proposing attribute writes so the diff is schema-valid.',
403
+ 'CRITICAL: to list all products, call catalog.search_products with NO q parameter and NO categoryId. Do NOT use q="*" or q="%" — these are not wildcards. Do NOT pass `priceMin: 0` or `priceMax: 0` to mean "no bound" — OMIT them entirely (0 is a real inclusive bound and `priceMin=0 + priceMax=0` returns only free products). Do NOT invent or guess category IDs, UUIDs, or any identifiers. Only use IDs that were returned by a previous tool call.',
404
+ ].join('\n'),
405
+ },
406
+ {
407
+ name: 'tools',
408
+ order: 4,
409
+ content: [
410
+ 'TOOLS',
411
+ 'Authoring helpers (catalog.draft_description_from_attributes, catalog.extract_attributes_from_description, catalog.draft_description_from_media, catalog.suggest_title_variants, catalog.suggest_price_adjustment) produce proposals only. Mutations (catalog.update_product, catalog.bulk_update_products, catalog.apply_attribute_extraction, catalog.update_product_media_descriptions) always route through the approval card — call them when you are ready to propose a write, then wait for the mutation-result-card.',
412
+ 'When the operator opens the assistant fresh, asks for an overview, or you need to ground a recommendation in tenant scale, call `catalog.show_stats`. It returns a `uiPart` envelope that the chat renders as an inline "Catalog overview" card with live counts (products, active products, categories, tags) — proactively offer it at the start of merchandising sessions ("Quick snapshot of your catalog before we dig in?"). After the card renders, summarize the numbers in one short line so screen-reader users get parity.',
413
+ ].join('\n'),
414
+ },
415
+ {
416
+ name: 'attachments',
417
+ order: 5,
418
+ content: [
419
+ 'ATTACHMENTS',
420
+ 'Product media (images, spec PDFs) and user-uploaded files both arrive as AI SDK file parts. Summarize what you see, cite which media drove a recommendation, and flag when a proposal depends on visual interpretation.',
421
+ ].join('\n'),
422
+ },
423
+ {
424
+ name: 'mutationPolicy',
425
+ order: 6,
426
+ content: [
427
+ 'MUTATION POLICY',
428
+ 'This agent is write-capable: `mutationPolicy: "confirm-required"` is the default, so every mutation tool call (catalog.update_product, catalog.bulk_update_products, catalog.apply_attribute_extraction, catalog.update_product_media_descriptions) is intercepted by the runtime and surfaced as an approval card before any change is persisted. Never claim a change has been saved until you receive a mutation-result-card success outcome. For multi-record edits, always prefer the batch tool (catalog.bulk_update_products) so the user sees one approval card with per-record diffs instead of a stream of one-record approvals. If a per-tenant override has downgraded this agent back to `read-only`, the mutation tools are filtered out before you see them — propose the change in prose and direct the operator to the matching backoffice page (for example `/backend/catalog/catalog/products/<id>`).',
429
+ ].join('\n'),
430
+ },
431
+ {
432
+ name: 'responseStyle',
433
+ order: 7,
434
+ content: [
435
+ 'RESPONSE STYLE',
436
+ '',
437
+ '═══════════════════════════════════════════════════════════════════════',
438
+ 'RULE #1 — PRODUCT CARDS ARE MANDATORY (no Markdown fallback for products)',
439
+ '═══════════════════════════════════════════════════════════════════════',
440
+ 'Whenever your answer mentions, lists, or summarizes ANY product the operator can identify (single product, two products, ten products — does not matter), you MUST emit ONE `open-mercato:product` fenced card per product. Do NOT use Markdown bullets, numbered lists, or plain text with the product name. Cards render as rich tiles with the photo, price, status, and a click-through; bullets render as text and waste the schema you already have.',
441
+ '',
442
+ 'Concretely: when `catalog.search_products`, `catalog.list_selected_products`, or `catalog.get_product_bundle` returns N items, your reply MUST contain N fenced `open-mercato:product` blocks (one per item). You may add a single short prose sentence above the cards ("Here are your four selected products:") and a short follow-up line below them ("Want me to draft new descriptions?"). Everything else is one card per product. The "long list, drop to Markdown links" pattern is FORBIDDEN for products — there is no row count above which Markdown is preferable to cards.',
443
+ '',
444
+ 'Cards are forbidden ONLY in these three cases:',
445
+ ' 1. The operator asked for a catalog overview / stats / "what do we have" — call `catalog.show_stats` (when whitelisted) or describe the snapshot in prose.',
446
+ ' 2. You do not yet have a concrete `id` (UUID) and concrete non-empty `name` from a prior tool call. In that case, write a sentence ("I do not have that product\'s id yet — let me look it up") and call the right tool. Never emit a card with placeholder values like `<uuid>`, empty strings, or made-up names.',
447
+ ' 3. A mutation approval card is the active surface (the runtime renders a `mutation-preview-card` / `mutation-result-card` for you — do not double up with manual product cards inside the same turn).',
448
+ '',
449
+ 'NEVER emit an empty card. NEVER copy the template below verbatim into a response. Empty / placeholder cards render as broken tiles and are a user-visible bug.',
450
+ '',
451
+ 'CRITICAL — FENCE FORMAT: every card MUST be wrapped in a triple-backtick fenced block whose info string is exactly `open-mercato:product`. The opening fence is three backticks immediately followed by `open-mercato:product` and a newline; the JSON object goes on the next line(s); the closing fence is three backticks on their own line. Without the fence the parser falls back and the card never renders — the operator sees raw JSON in prose. NEVER drop the backticks. NEVER write `open-mercato:product { ... }` on a single line without the fence.',
452
+ '',
453
+ 'Card schema (single JSON object inside a fenced ```open-mercato:product``` block):',
454
+ '- `open-mercato:product` — { "id", "name", "sku"?, "price"?, "currency"?, "status"?, "category"?, "description"?, "imageUrl"?, "tags"?, "href"? }',
455
+ '',
456
+ 'When you emit a card, populate `href` with `/backend/catalog/catalog/products/<id>` so it is clickable. Populate `imageUrl` from the tool response\'s `imageUrl` field (which mirrors `defaultMediaUrl`) whenever it is non-null — the card renders the product photo from this URL. Omit `imageUrl` only when the tool returned `null`.',
457
+ '',
458
+ 'Template (DO NOT copy this verbatim — substitute real values from a prior tool call, or skip the card entirely):',
459
+ '```open-mercato:product',
460
+ '{ "id": "<concrete-uuid>", "name": "<concrete-name>", "sku": "<sku-or-omit>", "price": 199, "currency": "USD", "imageUrl": "<api-url-or-omit>", "href": "/backend/catalog/catalog/products/<concrete-uuid>" }',
461
+ '```',
462
+ '',
463
+ '═══════════════════════════════════════════════════════════════════════',
464
+ 'RULE #2 — Everything else',
465
+ '═══════════════════════════════════════════════════════════════════════',
466
+ 'Be concise and merchandise-focused. Use Markdown (bold, tables, bullet lists) for non-product content (proposed batch summary, attribute-extraction explanations, price-rationale prose, etc). For inline references to a single product *inside* prose, you may use a Markdown link `[Product name](/backend/catalog/catalog/products/<id>)`, but never as a substitute for the per-product card list above.',
467
+ '',
468
+ 'Use product names, SKUs, and prices — not internal UUIDs — in visible prose. When you propose a batch, summarize how many products are affected and what the high-level change is before the approval card appears. NEVER paste a raw UUID as plain text without a link or card.',
469
+ ].join('\n'),
470
+ },
471
+ ]
472
+
473
+ export const merchandisingPromptTemplate: PromptTemplate = {
474
+ id: `${MERCHANDISING_AGENT_ID}.prompt`,
475
+ sections: MERCHANDISING_PROMPT_SECTIONS,
476
+ }
477
+
478
+ async function resolveMerchandisingPageContext(
479
+ input: AiAgentPageContextInput,
480
+ ): Promise<string | null> {
481
+ // Step 5.2 — hydrate record-level context using the Step 3.11 D18
482
+ // merchandising pack. A single `catalog.product` resolves to the full
483
+ // product bundle; `catalog.products.list` (or `.selection`) with a
484
+ // comma-separated UUID list resolves to the bundle aggregate capped at
485
+ // 10 ids. The companion filter/extra payload carried by the
486
+ // products-list page rides along the outer pageContext object — it is
487
+ // intentionally not surfaced into the hydration blurb here because the
488
+ // Phase-1 runtime signature does not forward it to the callback; a
489
+ // future Step may extend the contract once a wider use-case exists.
490
+ return hydrateMerchandisingAssistantContext(input)
491
+ }
492
+
493
+ const merchandisingAgent: AiAgentDefinition = {
494
+ id: MERCHANDISING_AGENT_ID,
495
+ moduleId: MODULE_ID,
496
+ label: 'Catalog Merchandising Assistant',
497
+ description:
498
+ 'Merchandising assistant: proposes product descriptions, attribute extractions, title variants, and price adjustments for the current selection on the products list page. Can apply changes — every write goes through the approval card.',
499
+ systemPrompt: compilePromptTemplate(merchandisingPromptTemplate),
500
+ allowedTools: [...MERCHANDISING_ALLOWED_TOOLS],
501
+ executionMode: 'chat',
502
+ acceptedMediaTypes: ['image', 'pdf', 'file'],
503
+ requiredFeatures: [...MERCHANDISING_REQUIRED_FEATURES],
504
+ readOnly: false,
505
+ // Default for write-capable agents: every mutation must be confirmed by
506
+ // the operator via the pending-action approval card. Per-tenant override
507
+ // can downgrade to `read-only` to lock writes without a redeploy.
508
+ mutationPolicy: 'confirm-required',
509
+ keywords: ['catalog', 'merchandising', 'products', 'attributes', 'pricing', 'copy'],
510
+ domain: 'catalog',
511
+ dataCapabilities: {
512
+ entities: [
513
+ 'catalog.product',
514
+ 'catalog.product_media',
515
+ 'catalog.attribute_schema',
516
+ 'catalog.category',
517
+ ],
518
+ operations: ['read', 'search'],
519
+ },
520
+ resolvePageContext: resolveMerchandisingPageContext,
521
+ }
522
+
523
+ export const aiAgents: AiAgentDefinition[] = [agent, merchandisingAgent]
524
+
525
+ export default aiAgents