@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +13 -1
- package/dist/helpers/integration/api.js +29 -16
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/auth.js +11 -6
- package/dist/helpers/integration/auth.js.map +3 -3
- package/dist/modules/auth/commands/roles.js +9 -12
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/catalog/ai-agents-context.js +147 -0
- package/dist/modules/catalog/ai-agents-context.js.map +7 -0
- package/dist/modules/catalog/ai-agents.js +383 -0
- package/dist/modules/catalog/ai-agents.js.map +7 -0
- package/dist/modules/catalog/ai-tools/_shared.js +318 -0
- package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
- package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/types.js +10 -0
- package/dist/modules/catalog/ai-tools/types.js.map +7 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools.js +28 -0
- package/dist/modules/catalog/ai-tools.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
- package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
- package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
- package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
- package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/events.js +7 -4
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/catalog/widgets/injection-table.js +13 -1
- package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/ai-agents-context.js +96 -0
- package/dist/modules/customers/ai-agents-context.js.map +7 -0
- package/dist/modules/customers/ai-agents.js +244 -0
- package/dist/modules/customers/ai-agents.js.map +7 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
- package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
- package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/people-pack.js +261 -0
- package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
- package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/types.js +10 -0
- package/dist/modules/customers/ai-tools/types.js.map +7 -0
- package/dist/modules/customers/ai-tools.js +20 -0
- package/dist/modules/customers/ai-tools.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection-table.js +26 -0
- package/dist/modules/customers/widgets/injection-table.js.map +7 -0
- package/dist/modules/inbox_ops/ai-tools.js +4 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
- package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
- package/dist/modules/notifications/setup.js +13 -0
- package/dist/modules/notifications/setup.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.setup.ts +18 -0
- package/package.json +5 -3
- package/src/helpers/integration/api.ts +38 -16
- package/src/helpers/integration/auth.ts +13 -6
- package/src/modules/auth/commands/roles.ts +10 -12
- package/src/modules/catalog/AGENTS.md +11 -0
- package/src/modules/catalog/ai-agents-context.ts +239 -0
- package/src/modules/catalog/ai-agents.ts +525 -0
- package/src/modules/catalog/ai-tools/_shared.ts +487 -0
- package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
- package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
- package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
- package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
- package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
- package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
- package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
- package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
- package/src/modules/catalog/ai-tools/types.ts +81 -0
- package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
- package/src/modules/catalog/ai-tools.ts +78 -0
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
- package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
- package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
- package/src/modules/catalog/events.ts +7 -4
- package/src/modules/catalog/i18n/de.json +17 -0
- package/src/modules/catalog/i18n/en.json +17 -0
- package/src/modules/catalog/i18n/es.json +17 -0
- package/src/modules/catalog/i18n/pl.json +17 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
- package/src/modules/catalog/widgets/injection-table.ts +12 -0
- package/src/modules/customer_accounts/i18n/de.json +5 -0
- package/src/modules/customer_accounts/i18n/en.json +5 -0
- package/src/modules/customer_accounts/i18n/es.json +5 -0
- package/src/modules/customer_accounts/i18n/pl.json +5 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
- package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
- package/src/modules/customers/AGENTS.md +13 -0
- package/src/modules/customers/ai-agents-context.ts +150 -0
- package/src/modules/customers/ai-agents.ts +355 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
- package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
- package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
- package/src/modules/customers/ai-tools/people-pack.ts +369 -0
- package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
- package/src/modules/customers/ai-tools/types.ts +76 -0
- package/src/modules/customers/ai-tools.ts +34 -0
- package/src/modules/customers/i18n/de.json +25 -0
- package/src/modules/customers/i18n/en.json +25 -0
- package/src/modules/customers/i18n/es.json +25 -0
- package/src/modules/customers/i18n/pl.json +25 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
- package/src/modules/customers/widgets/injection-table.ts +41 -0
- package/src/modules/inbox_ops/ai-tools.ts +4 -0
- package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
- package/src/modules/notifications/setup.ts +11 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Sparkles } from "lucide-react";
|
|
5
|
+
import { AiChat } from "@open-mercato/ui/ai/AiChat";
|
|
6
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle
|
|
13
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
14
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
15
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
16
|
+
const CUSTOMERS_AI_DEAL_DETAIL_AGENT_ID = "customers.account_assistant";
|
|
17
|
+
function readString(value) {
|
|
18
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
19
|
+
}
|
|
20
|
+
function buildPageContext(context, data) {
|
|
21
|
+
const dealRecord = data?.deal ?? context?.data?.deal;
|
|
22
|
+
const dealId = readString(context?.dealId) ?? readString(context?.recordId) ?? readString(dealRecord?.id) ?? null;
|
|
23
|
+
if (!dealId) return null;
|
|
24
|
+
const stage = readString(context?.stage) ?? readString(dealRecord?.status) ?? readString(dealRecord?.pipelineStage) ?? null;
|
|
25
|
+
const pipelineStageId = readString(context?.pipelineStageId) ?? readString(dealRecord?.pipelineStageId) ?? null;
|
|
26
|
+
return {
|
|
27
|
+
view: "customers.deal.detail",
|
|
28
|
+
recordType: "deal",
|
|
29
|
+
recordId: dealId,
|
|
30
|
+
extra: { stage, pipelineStageId }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function computeCustomersAiDealDetailPageContext(context, data) {
|
|
34
|
+
return buildPageContext(context, data);
|
|
35
|
+
}
|
|
36
|
+
function AiDealDetailTriggerWidget({ context, data }) {
|
|
37
|
+
const t = useT();
|
|
38
|
+
const [open, setOpen] = React.useState(false);
|
|
39
|
+
const pageContext = React.useMemo(() => buildPageContext(context, data), [context, data]);
|
|
40
|
+
const handleClick = React.useCallback(() => {
|
|
41
|
+
setOpen(true);
|
|
42
|
+
}, []);
|
|
43
|
+
if (!pageContext) return null;
|
|
44
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
45
|
+
/* @__PURE__ */ jsxs(
|
|
46
|
+
Button,
|
|
47
|
+
{
|
|
48
|
+
type: "button",
|
|
49
|
+
variant: "outline",
|
|
50
|
+
size: "sm",
|
|
51
|
+
onClick: handleClick,
|
|
52
|
+
"data-ai-customers-deal-trigger": "",
|
|
53
|
+
"data-ai-customers-deal-id": pageContext.recordId,
|
|
54
|
+
"aria-label": t(
|
|
55
|
+
"customers.ai_assistant.dealDetail.trigger.ariaLabel",
|
|
56
|
+
"Open AI assistant for this deal"
|
|
57
|
+
),
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "size-4", "aria-hidden": true }),
|
|
60
|
+
/* @__PURE__ */ jsx("span", { children: t("customers.ai_assistant.dealDetail.trigger.label", "Ask AI") })
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(
|
|
65
|
+
DialogContent,
|
|
66
|
+
{
|
|
67
|
+
className: cn(
|
|
68
|
+
"sm:max-w-xl sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0",
|
|
69
|
+
"sm:h-screen sm:max-h-screen sm:rounded-none sm:rounded-l-2xl",
|
|
70
|
+
"flex flex-col gap-3 p-4 z-[70]"
|
|
71
|
+
),
|
|
72
|
+
"data-ai-customers-deal-sheet": "",
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
75
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
76
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: t(
|
|
77
|
+
"customers.ai_assistant.dealDetail.sheet.title",
|
|
78
|
+
"Customers AI assistant \u2014 deal"
|
|
79
|
+
) }),
|
|
80
|
+
pageContext.extra.stage ? /* @__PURE__ */ jsx(
|
|
81
|
+
"span",
|
|
82
|
+
{
|
|
83
|
+
className: "inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground",
|
|
84
|
+
"data-ai-customers-deal-stage-pill": "",
|
|
85
|
+
"data-ai-customers-deal-stage": pageContext.extra.stage,
|
|
86
|
+
children: pageContext.extra.stage
|
|
87
|
+
}
|
|
88
|
+
) : null
|
|
89
|
+
] }),
|
|
90
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: t(
|
|
91
|
+
"customers.ai_assistant.dealDetail.sheet.description",
|
|
92
|
+
"Ask about this deal. With the per-tenant mutation-policy override enabled, the assistant can also propose a stage change that you confirm before anything is saved."
|
|
93
|
+
) })
|
|
94
|
+
] }),
|
|
95
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", "data-ai-customers-deal-chat-container": "", children: /* @__PURE__ */ jsx(
|
|
96
|
+
AiChat,
|
|
97
|
+
{
|
|
98
|
+
agent: CUSTOMERS_AI_DEAL_DETAIL_AGENT_ID,
|
|
99
|
+
pageContext,
|
|
100
|
+
className: "h-full",
|
|
101
|
+
placeholder: t(
|
|
102
|
+
"customers.ai_assistant.dealDetail.sheet.composerPlaceholder",
|
|
103
|
+
"Ask about this deal, the stage, pipeline..."
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
) })
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
) })
|
|
110
|
+
] });
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
CUSTOMERS_AI_DEAL_DETAIL_AGENT_ID,
|
|
114
|
+
computeCustomersAiDealDetailPageContext,
|
|
115
|
+
AiDealDetailTriggerWidget as default
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=widget.client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Step 5.15 \u2014 Backend AiChat injection widget for the deal detail page.\n *\n * Mirrors the Step 4.10 People-list trigger, but targets a record-level\n * surface: the deal detail page's header injection spot\n * (`detail:customers.deal:header`). The page is NOT modified beyond\n * adding the shared `<InjectionSpot>` mount point.\n *\n * `pageContext` shape (spec \u00A710.1):\n *\n * { view: 'customers.deal.detail',\n * recordType: 'deal',\n * recordId: <dealId>,\n * extra: { stage: string | null, pipelineStageId: string | null } }\n *\n * The agent is `customers.account_assistant`, feature-gated at the widget\n * metadata layer behind `customers.deals.view` + `ai_assistant.view`.\n * The read-only agent serves information requests today; when the tenant\n * opts into Step 5.4's mutation-policy override, the agent unlocks the\n * Step 5.13 `customers.update_deal_stage` tool behind the pending-action\n * contract.\n */\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { AiChat } from '@open-mercato/ui/ai/AiChat'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport const CUSTOMERS_AI_DEAL_DETAIL_AGENT_ID = 'customers.account_assistant'\n\nexport interface CustomersAiDealDetailPageContext {\n view: 'customers.deal.detail'\n recordType: 'deal'\n recordId: string\n extra: {\n stage: string | null\n pipelineStageId: string | null\n }\n}\n\ninterface HostInjectionContext {\n dealId?: string\n recordId?: string\n stage?: string | null\n pipelineStageId?: string | null\n data?: {\n deal?: {\n id?: string\n status?: string | null\n pipelineStage?: string | null\n pipelineStageId?: string | null\n }\n }\n}\n\ninterface AiDealDetailTriggerProps {\n context?: HostInjectionContext\n data?: HostInjectionContext['data']\n}\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction buildPageContext(\n context: HostInjectionContext | undefined,\n data: HostInjectionContext['data'] | undefined,\n): CustomersAiDealDetailPageContext | null {\n const dealRecord = data?.deal ?? context?.data?.deal\n const dealId =\n readString(context?.dealId) ??\n readString(context?.recordId) ??\n readString(dealRecord?.id) ??\n null\n if (!dealId) return null\n const stage =\n readString(context?.stage) ??\n readString(dealRecord?.status) ??\n readString(dealRecord?.pipelineStage) ??\n null\n const pipelineStageId =\n readString(context?.pipelineStageId) ??\n readString(dealRecord?.pipelineStageId) ??\n null\n return {\n view: 'customers.deal.detail',\n recordType: 'deal',\n recordId: dealId,\n extra: { stage, pipelineStageId },\n }\n}\n\n/**\n * Exposed for unit tests so the page-context derivation is exercisable\n * without mounting the widget.\n */\nexport function computeCustomersAiDealDetailPageContext(\n context: HostInjectionContext | undefined,\n data?: HostInjectionContext['data'],\n): CustomersAiDealDetailPageContext | null {\n return buildPageContext(context, data)\n}\n\nexport default function AiDealDetailTriggerWidget({ context, data }: AiDealDetailTriggerProps) {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const pageContext = React.useMemo(() => buildPageContext(context, data), [context, data])\n const handleClick = React.useCallback(() => {\n setOpen(true)\n }, [])\n\n if (!pageContext) return null\n\n return (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleClick}\n data-ai-customers-deal-trigger=\"\"\n data-ai-customers-deal-id={pageContext.recordId}\n aria-label={t(\n 'customers.ai_assistant.dealDetail.trigger.ariaLabel',\n 'Open AI assistant for this deal',\n )}\n >\n <Sparkles className=\"size-4\" aria-hidden />\n <span>{t('customers.ai_assistant.dealDetail.trigger.label', 'Ask AI')}</span>\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent\n className={cn(\n 'sm:max-w-xl sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:h-screen sm:max-h-screen sm:rounded-none sm:rounded-l-2xl',\n 'flex flex-col gap-3 p-4 z-[70]',\n )}\n data-ai-customers-deal-sheet=\"\"\n >\n <DialogHeader>\n <div className=\"flex items-center justify-between gap-3\">\n <DialogTitle>\n {t(\n 'customers.ai_assistant.dealDetail.sheet.title',\n 'Customers AI assistant \u2014 deal',\n )}\n </DialogTitle>\n {pageContext.extra.stage ? (\n <span\n className=\"inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground\"\n data-ai-customers-deal-stage-pill=\"\"\n data-ai-customers-deal-stage={pageContext.extra.stage}\n >\n {pageContext.extra.stage}\n </span>\n ) : null}\n </div>\n <DialogDescription>\n {t(\n 'customers.ai_assistant.dealDetail.sheet.description',\n 'Ask about this deal. With the per-tenant mutation-policy override enabled, the assistant can also propose a stage change that you confirm before anything is saved.',\n )}\n </DialogDescription>\n </DialogHeader>\n <div className=\"min-h-0 flex-1\" data-ai-customers-deal-chat-container=\"\">\n <AiChat\n agent={CUSTOMERS_AI_DEAL_DETAIL_AGENT_ID}\n pageContext={pageContext as unknown as Record<string, unknown>}\n className=\"h-full\"\n placeholder={t(\n 'customers.ai_assistant.dealDetail.sheet.composerPlaceholder',\n 'Ask about this deal, the stage, pipeline...',\n )}\n />\n </div>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA6HI,mBAaI,KAZF,YADF;AApGJ,YAAY,WAAW;AACvB,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AACrB,SAAS,UAAU;AAEZ,MAAM,oCAAoC;AAgCjD,SAAS,WAAW,OAA+B;AACjD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,iBACP,SACA,MACyC;AACzC,QAAM,aAAa,MAAM,QAAQ,SAAS,MAAM;AAChD,QAAM,SACJ,WAAW,SAAS,MAAM,KAC1B,WAAW,SAAS,QAAQ,KAC5B,WAAW,YAAY,EAAE,KACzB;AACF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QACJ,WAAW,SAAS,KAAK,KACzB,WAAW,YAAY,MAAM,KAC7B,WAAW,YAAY,aAAa,KACpC;AACF,QAAM,kBACJ,WAAW,SAAS,eAAe,KACnC,WAAW,YAAY,eAAe,KACtC;AACF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,OAAO,EAAE,OAAO,gBAAgB;AAAA,EAClC;AACF;AAMO,SAAS,wCACd,SACA,MACyC;AACzC,SAAO,iBAAiB,SAAS,IAAI;AACvC;AAEe,SAAR,0BAA2C,EAAE,SAAS,KAAK,GAA6B;AAC7F,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,cAAc,MAAM,QAAQ,MAAM,iBAAiB,SAAS,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC;AACxF,QAAM,cAAc,MAAM,YAAY,MAAM;AAC1C,YAAQ,IAAI;AAAA,EACd,GAAG,CAAC,CAAC;AAEL,MAAI,CAAC,YAAa,QAAO;AAEzB,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS;AAAA,QACT,kCAA+B;AAAA,QAC/B,6BAA2B,YAAY;AAAA,QACvC,cAAY;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,YAAS,WAAU,UAAS,eAAW,MAAC;AAAA,UACzC,oBAAC,UAAM,YAAE,mDAAmD,QAAQ,GAAE;AAAA;AAAA;AAAA,IACxE;AAAA,IACA,oBAAC,UAAO,MAAY,cAAc,SAChC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,gCAA6B;AAAA,QAE7B;AAAA,+BAAC,gBACC;AAAA,iCAAC,SAAI,WAAU,2CACb;AAAA,kCAAC,eACE;AAAA,gBACC;AAAA,gBACA;AAAA,cACF,GACF;AAAA,cACC,YAAY,MAAM,QACjB;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,qCAAkC;AAAA,kBAClC,gCAA8B,YAAY,MAAM;AAAA,kBAE/C,sBAAY,MAAM;AAAA;AAAA,cACrB,IACE;AAAA,eACN;AAAA,YACA,oBAAC,qBACE;AAAA,cACC;AAAA,cACA;AAAA,YACF,GACF;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAU,kBAAiB,yCAAsC,IACpE;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA,WAAU;AAAA,cACV,aAAa;AAAA,gBACX;AAAA,gBACA;AAAA,cACF;AAAA;AAAA,UACF,GACF;AAAA;AAAA;AAAA,IACF,GACF;AAAA,KACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import AiDealDetailTriggerWidget from "./widget.client.js";
|
|
2
|
+
const widget = {
|
|
3
|
+
metadata: {
|
|
4
|
+
id: "customers.injection.ai-deal-detail-trigger",
|
|
5
|
+
title: "Customers AI Deal Detail Trigger",
|
|
6
|
+
description: 'Renders an "Ask AI" button in the deal detail header that opens a sheet embedding the customers account assistant with deal-scoped page context.',
|
|
7
|
+
features: ["customers.deals.view", "ai_assistant.view"],
|
|
8
|
+
priority: 100,
|
|
9
|
+
enabled: true
|
|
10
|
+
},
|
|
11
|
+
Widget: AiDealDetailTriggerWidget
|
|
12
|
+
};
|
|
13
|
+
var widget_default = widget;
|
|
14
|
+
export {
|
|
15
|
+
widget_default as default
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts"],
|
|
4
|
+
"sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport AiDealDetailTriggerWidget from './widget.client'\n\n/**\n * Step 5.15 (Phase 3 WS-D) \u2014 Customers Deal detail AiChat injection.\n *\n * Extends the Step 4.10 pattern to a record-level surface. Drops an\n * \"Ask AI\" trigger on the deal detail page (`detail:customers.deal:header`)\n * and opens a sheet embedding\n * `<AiChat agent=\"customers.account_assistant\" pageContext={\u2026} />` with a\n * deal-scoped `pageContext` shape:\n *\n * { view: 'customers.deal.detail',\n * recordType: 'deal',\n * recordId: <dealId>,\n * extra: { stage, pipelineStageId } }\n *\n * Wires the stable conversation id so the Step 5.13\n * `customers.update_deal_stage` mutation tool's idempotency hash stays\n * constant across repeated confirms / retries within the same chat.\n *\n * Feature-gated behind `customers.deals.view` + `ai_assistant.view`.\n */\nconst widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {\n metadata: {\n id: 'customers.injection.ai-deal-detail-trigger',\n title: 'Customers AI Deal Detail Trigger',\n description:\n 'Renders an \"Ask AI\" button in the deal detail header that opens a sheet embedding the customers account assistant with deal-scoped page context.',\n features: ['customers.deals.view', 'ai_assistant.view'],\n priority: 100,\n enabled: true,\n },\n Widget: AiDealDetailTriggerWidget,\n}\n\nexport default widget\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,+BAA+B;AAsBtC,MAAM,SAAkF;AAAA,EACtF,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU,CAAC,wBAAwB,mBAAmB;AAAA,IACtD,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA,QAAQ;AACV;AAEA,IAAO,iBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const injectionTable = {
|
|
2
|
+
"data-table:customers.people.list:search-trailing": [
|
|
3
|
+
{
|
|
4
|
+
widgetId: "customers.injection.ai-assistant-trigger",
|
|
5
|
+
priority: 100
|
|
6
|
+
}
|
|
7
|
+
],
|
|
8
|
+
"data-table:customers.companies.list:search-trailing": [
|
|
9
|
+
{
|
|
10
|
+
widgetId: "customers.injection.ai-assistant-trigger",
|
|
11
|
+
priority: 100
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"detail:customers.deal:header": [
|
|
15
|
+
{
|
|
16
|
+
widgetId: "customers.injection.ai-deal-detail-trigger",
|
|
17
|
+
priority: 100
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
var injection_table_default = injectionTable;
|
|
22
|
+
export {
|
|
23
|
+
injection_table_default as default,
|
|
24
|
+
injectionTable
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=injection-table.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customers/widgets/injection-table.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\n\n/**\n * Step 4.10 / Step 5.15 \u2014 customers module injection table.\n *\n * - Step 4.10 drops the `ai-assistant-trigger` widget on the People-list\n * `DataTable` `:search-trailing` slot, which renders adjacent to the\n * list search input. The previous mount point was the `:header` slot\n * (separate row); the round icon-only trigger now lives next to the\n * search box for a tighter, single-row toolbar.\n * - Step 5.15 (Phase 3 WS-D) adds the `ai-deal-detail-trigger` widget on\n * the Deal detail page header spot (`detail:customers.deal:header`).\n *\n * Both widgets embed `<AiChat agent=\"customers.account_assistant\" \u2026>`\n * with a selection- or record-aware `pageContext`. The page files\n * themselves only register the shared `<InjectionSpot>` mount point \u2014\n * the trigger, sheet, and chat surface live entirely in the injection\n * widgets so third-party modules can copy the pattern unchanged.\n */\nexport const injectionTable: ModuleInjectionTable = {\n 'data-table:customers.people.list:search-trailing': [\n {\n widgetId: 'customers.injection.ai-assistant-trigger',\n priority: 100,\n },\n ],\n 'data-table:customers.companies.list:search-trailing': [\n {\n widgetId: 'customers.injection.ai-assistant-trigger',\n priority: 100,\n },\n ],\n 'detail:customers.deal:header': [\n {\n widgetId: 'customers.injection.ai-deal-detail-trigger',\n priority: 100,\n },\n ],\n}\n\nexport default injectionTable\n"],
|
|
5
|
+
"mappings": "AAmBO,MAAM,iBAAuC;AAAA,EAClD,oDAAoD;AAAA,IAClD;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EACA,uDAAuD;AAAA,IACrD;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EACA,gCAAgC;AAAA,IAC9B;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,IAAO,0BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -205,6 +205,10 @@ Returns on error: error message with appropriate detail.`,
|
|
|
205
205
|
actionId: z.string().uuid().describe("The UUID of the action to accept")
|
|
206
206
|
}),
|
|
207
207
|
requiredFeatures: ["inbox_ops.proposals.manage"],
|
|
208
|
+
// Accepting an inbox action creates downstream entities (orders, contacts,
|
|
209
|
+
// etc.) in target modules — must surface as a write so any agent that
|
|
210
|
+
// whitelists it routes through the approval card.
|
|
211
|
+
isMutation: true,
|
|
208
212
|
handler: async (input, ctx) => {
|
|
209
213
|
const scope = requireTenantContext(ctx);
|
|
210
214
|
if (!ctx.userId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/inbox_ops/ai-tools.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from './data/entities'\nimport { inboxProposalCategoryEnum } from './data/validators'\nimport { executeAction } from './lib/executionEngine'\nimport { resolveExtractionProviderId, createStructuredModel, withTimeout } from './lib/llmProvider'\nimport { resolveOptionalEventBus } from './lib/eventBus'\n\ntype ToolContext = {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n}\n\ninterface AiToolDefinition {\n name: string\n description: string\n inputSchema: z.ZodType\n requiredFeatures?: string[]\n handler: (input: never, ctx: ToolContext) => Promise<unknown>\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction requireTenantContext(ctx: ToolContext): { tenantId: string; organizationId: string } {\n if (!ctx.tenantId || !ctx.organizationId) {\n throw new Error('Tenant context is required')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n\nfunction resolveCrossModuleEntities(container: ToolContext['container']) {\n const entities: Record<string, unknown> = {}\n const keys = [\n 'CustomerEntity',\n 'SalesOrder',\n 'SalesShipment',\n 'SalesChannel',\n 'Dictionary',\n 'DictionaryEntry',\n ]\n for (const key of keys) {\n try {\n entities[key] = container.resolve(key)\n } catch {\n /* module not available */\n }\n }\n return entities\n}\n\n// =============================================================================\n// inbox_ops_list_proposals \u2014 Query proposals by status, category, date range\n// =============================================================================\n\nconst listProposalsTool = {\n name: 'inbox_ops_list_proposals',\n description: `List inbox proposals with optional filters by status, category, and date range.\n\nReturns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,\n inputSchema: z.object({\n status: z\n .enum(['pending', 'partial', 'accepted', 'rejected'])\n .optional()\n .describe('Filter by proposal status'),\n category: inboxProposalCategoryEnum\n .optional()\n .describe('Filter by email category'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .optional()\n .default(10)\n .describe('Maximum number of proposals to return (default: 10)'),\n dateFrom: z\n .string()\n .optional()\n .describe('Filter proposals created on or after this date (ISO 8601)'),\n dateTo: z\n .string()\n .optional()\n .describe('Filter proposals created on or before this date (ISO 8601)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { status?: string; category?: string; limit?: number; dateFrom?: string; dateTo?: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const where: Record<string, unknown> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n }\n\n if (input.status) {\n where.status = input.status\n }\n if (input.category) {\n where.category = input.category\n }\n if (input.dateFrom || input.dateTo) {\n const createdAt: Record<string, unknown> = {}\n if (input.dateFrom) {\n createdAt.$gte = new Date(input.dateFrom)\n }\n if (input.dateTo) {\n createdAt.$lte = new Date(input.dateTo)\n }\n where.createdAt = createdAt\n }\n\n const proposals = await findWithDecryption(\n em,\n InboxProposal,\n where,\n { orderBy: { createdAt: 'DESC' }, limit: input.limit },\n scope,\n )\n\n // Count actions per proposal in a single query\n const proposalIds = proposals.map((p) => p.id)\n const actionCountMap = new Map<string, number>()\n\n if (proposalIds.length > 0) {\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: { $in: proposalIds },\n deletedAt: null,\n },\n undefined,\n scope,\n )\n for (const action of actions) {\n actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1)\n }\n }\n\n // Get total count for the filter\n const total = await em.count(InboxProposal, where)\n\n return {\n total,\n proposals: proposals.map((p) => ({\n id: p.id,\n summary: p.summary,\n status: p.status,\n category: p.category ?? null,\n confidence: Number(p.confidence),\n actionCount: actionCountMap.get(p.id) ?? 0,\n createdAt: p.createdAt.toISOString(),\n })),\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_get_proposal \u2014 Fetch proposal detail with actions and discrepancies\n// =============================================================================\n\nconst getProposalTool = {\n name: 'inbox_ops_get_proposal',\n description: `Get full details of an inbox proposal including its actions and discrepancies.\n\nReturns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal to retrieve'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { proposalId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n {\n id: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!proposal) {\n return { error: 'Proposal not found' }\n }\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n scope,\n )\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n return {\n proposal: {\n id: proposal.id,\n summary: proposal.summary,\n status: proposal.status,\n category: proposal.category ?? null,\n confidence: Number(proposal.confidence),\n actions: actions.map((a) => ({\n id: a.id,\n actionType: a.actionType,\n description: a.description,\n status: a.status,\n confidence: Number(a.confidence),\n requiredFeature: a.requiredFeature ?? null,\n sortOrder: a.sortOrder,\n createdEntityId: a.createdEntityId ?? null,\n createdEntityType: a.createdEntityType ?? null,\n })),\n discrepancies: discrepancies.map((d) => ({\n id: d.id,\n type: d.type,\n severity: d.severity,\n description: d.description,\n expectedValue: d.expectedValue ?? null,\n foundValue: d.foundValue ?? null,\n resolved: d.resolved,\n })),\n },\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_accept_action \u2014 Accept and execute a specific action\n// =============================================================================\n\nconst acceptActionTool = {\n name: 'inbox_ops_accept_action',\n description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).\n\nReturns on success: { ok: true, createdEntityId, createdEntityType }\nReturns on error: error message with appropriate detail.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal'),\n actionId: z.string().uuid().describe('The UUID of the action to accept'),\n }),\n requiredFeatures: ['inbox_ops.proposals.manage'],\n handler: async (input: { proposalId: string; actionId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n if (!ctx.userId) {\n throw new Error('User context is required')\n }\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n {\n id: input.actionId,\n proposalId: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!action) {\n return { error: 'Action not found' }\n }\n\n // Check if action was already processed\n if (action.status !== 'pending' && action.status !== 'failed') {\n return { error: 'Action already processed', status: action.status }\n }\n\n // Check target module permission\n if (action.requiredFeature) {\n const featureGranted =\n ctx.isSuperAdmin || hasFeature(ctx.userFeatures, action.requiredFeature)\n if (!featureGranted) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature,\n }\n }\n }\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const eventBus = resolveOptionalEventBus(ctx.container)\n\n const result = await executeAction(action, {\n em,\n userId: ctx.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n eventBus,\n container: ctx.container,\n entities: entities as unknown as import('./lib/executionEngine').CrossModuleEntities,\n })\n\n if (!result.success) {\n if (result.statusCode === 409) {\n return { error: 'Action already processed', status: 'accepted' }\n }\n if (result.statusCode === 403) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature ?? 'unknown',\n }\n }\n return { error: 'Execution failed', detail: result.error ?? 'Unknown error' }\n }\n\n try {\n const { resolveCache, invalidateCountsCache } = await import('./lib/cache')\n const cache = resolveCache(ctx.container)\n if (cache && scope.tenantId) {\n await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId))\n }\n } catch { /* cache invalidation is non-critical */ }\n\n return {\n ok: true,\n createdEntityId: result.createdEntityId ?? null,\n createdEntityType: result.createdEntityType ?? null,\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_categorize_email \u2014 Standalone LLM-based text categorization\n// =============================================================================\n\nconst categorizeEmailSchema = z.object({\n category: inboxProposalCategoryEnum,\n confidence: z.number(),\n reasoning: z.string(),\n})\n\nconst categorizeEmailTool = {\n name: 'inbox_ops_categorize_email',\n description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.\n\nReturns: { category, confidence (0-1), reasoning }\nInput text is limited to 10,000 characters for cost control.`,\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(10000)\n .describe('Email or text content to categorize (max 10K chars)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { text: string }, ctx: ToolContext) => {\n requireTenantContext(ctx)\n\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n\n const modelConfig = resolveOpenCodeModel(providerId, {})\n const model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n\n const { generateObject } = await import('ai')\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: categorizeEmailSchema,\n system: `You are an email classification agent. Classify the given text into exactly one category:\n- rfq: Request for quotation or pricing inquiry\n- order: New purchase order or order placement\n- order_update: Change or update to an existing order\n- complaint: Customer complaint, dispute, or dissatisfaction\n- shipping_update: Shipment status, tracking, or delivery information\n- inquiry: General question or information request\n- payment: Payment-related (invoice, receipt, payment terms)\n- other: Does not fit any category above\n\nReturn a JSON object with:\n- category: one of the categories above\n- confidence: a number between 0 and 1 indicating how confident you are\n- reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,\n prompt: input.text,\n temperature: 0,\n }),\n 15000,\n 'Email categorization timed out after 15s',\n )\n\n return {\n category: result.object.category,\n confidence: Math.round(result.object.confidence * 100) / 100,\n reasoning: result.object.reasoning,\n }\n },\n}\n\n// =============================================================================\n// Export\n// =============================================================================\n\n/**\n * All AI tools exported by the inbox_ops module.\n * Discovered by ai-assistant module's generator.\n */\nexport const aiTools: AiToolDefinition[] = [\n listProposalsTool,\n getProposalTool,\n acceptActionTool,\n categorizeEmailTool,\n]\n\nexport default aiTools\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport {\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from './data/entities'\nimport { inboxProposalCategoryEnum } from './data/validators'\nimport { executeAction } from './lib/executionEngine'\nimport { resolveExtractionProviderId, createStructuredModel, withTimeout } from './lib/llmProvider'\nimport { resolveOptionalEventBus } from './lib/eventBus'\n\ntype ToolContext = {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n}\n\ninterface AiToolDefinition {\n name: string\n description: string\n inputSchema: z.ZodType\n requiredFeatures?: string[]\n handler: (input: never, ctx: ToolContext) => Promise<unknown>\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction requireTenantContext(ctx: ToolContext): { tenantId: string; organizationId: string } {\n if (!ctx.tenantId || !ctx.organizationId) {\n throw new Error('Tenant context is required')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n\nfunction resolveCrossModuleEntities(container: ToolContext['container']) {\n const entities: Record<string, unknown> = {}\n const keys = [\n 'CustomerEntity',\n 'SalesOrder',\n 'SalesShipment',\n 'SalesChannel',\n 'Dictionary',\n 'DictionaryEntry',\n ]\n for (const key of keys) {\n try {\n entities[key] = container.resolve(key)\n } catch {\n /* module not available */\n }\n }\n return entities\n}\n\n// =============================================================================\n// inbox_ops_list_proposals \u2014 Query proposals by status, category, date range\n// =============================================================================\n\nconst listProposalsTool = {\n name: 'inbox_ops_list_proposals',\n description: `List inbox proposals with optional filters by status, category, and date range.\n\nReturns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,\n inputSchema: z.object({\n status: z\n .enum(['pending', 'partial', 'accepted', 'rejected'])\n .optional()\n .describe('Filter by proposal status'),\n category: inboxProposalCategoryEnum\n .optional()\n .describe('Filter by email category'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .optional()\n .default(10)\n .describe('Maximum number of proposals to return (default: 10)'),\n dateFrom: z\n .string()\n .optional()\n .describe('Filter proposals created on or after this date (ISO 8601)'),\n dateTo: z\n .string()\n .optional()\n .describe('Filter proposals created on or before this date (ISO 8601)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { status?: string; category?: string; limit?: number; dateFrom?: string; dateTo?: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const where: Record<string, unknown> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n }\n\n if (input.status) {\n where.status = input.status\n }\n if (input.category) {\n where.category = input.category\n }\n if (input.dateFrom || input.dateTo) {\n const createdAt: Record<string, unknown> = {}\n if (input.dateFrom) {\n createdAt.$gte = new Date(input.dateFrom)\n }\n if (input.dateTo) {\n createdAt.$lte = new Date(input.dateTo)\n }\n where.createdAt = createdAt\n }\n\n const proposals = await findWithDecryption(\n em,\n InboxProposal,\n where,\n { orderBy: { createdAt: 'DESC' }, limit: input.limit },\n scope,\n )\n\n // Count actions per proposal in a single query\n const proposalIds = proposals.map((p) => p.id)\n const actionCountMap = new Map<string, number>()\n\n if (proposalIds.length > 0) {\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: { $in: proposalIds },\n deletedAt: null,\n },\n undefined,\n scope,\n )\n for (const action of actions) {\n actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1)\n }\n }\n\n // Get total count for the filter\n const total = await em.count(InboxProposal, where)\n\n return {\n total,\n proposals: proposals.map((p) => ({\n id: p.id,\n summary: p.summary,\n status: p.status,\n category: p.category ?? null,\n confidence: Number(p.confidence),\n actionCount: actionCountMap.get(p.id) ?? 0,\n createdAt: p.createdAt.toISOString(),\n })),\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_get_proposal \u2014 Fetch proposal detail with actions and discrepancies\n// =============================================================================\n\nconst getProposalTool = {\n name: 'inbox_ops_get_proposal',\n description: `Get full details of an inbox proposal including its actions and discrepancies.\n\nReturns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal to retrieve'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { proposalId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n {\n id: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!proposal) {\n return { error: 'Proposal not found' }\n }\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n scope,\n )\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n return {\n proposal: {\n id: proposal.id,\n summary: proposal.summary,\n status: proposal.status,\n category: proposal.category ?? null,\n confidence: Number(proposal.confidence),\n actions: actions.map((a) => ({\n id: a.id,\n actionType: a.actionType,\n description: a.description,\n status: a.status,\n confidence: Number(a.confidence),\n requiredFeature: a.requiredFeature ?? null,\n sortOrder: a.sortOrder,\n createdEntityId: a.createdEntityId ?? null,\n createdEntityType: a.createdEntityType ?? null,\n })),\n discrepancies: discrepancies.map((d) => ({\n id: d.id,\n type: d.type,\n severity: d.severity,\n description: d.description,\n expectedValue: d.expectedValue ?? null,\n foundValue: d.foundValue ?? null,\n resolved: d.resolved,\n })),\n },\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_accept_action \u2014 Accept and execute a specific action\n// =============================================================================\n\nconst acceptActionTool = {\n name: 'inbox_ops_accept_action',\n description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).\n\nReturns on success: { ok: true, createdEntityId, createdEntityType }\nReturns on error: error message with appropriate detail.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal'),\n actionId: z.string().uuid().describe('The UUID of the action to accept'),\n }),\n requiredFeatures: ['inbox_ops.proposals.manage'],\n // Accepting an inbox action creates downstream entities (orders, contacts,\n // etc.) in target modules \u2014 must surface as a write so any agent that\n // whitelists it routes through the approval card.\n isMutation: true,\n handler: async (input: { proposalId: string; actionId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n if (!ctx.userId) {\n throw new Error('User context is required')\n }\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n {\n id: input.actionId,\n proposalId: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!action) {\n return { error: 'Action not found' }\n }\n\n // Check if action was already processed\n if (action.status !== 'pending' && action.status !== 'failed') {\n return { error: 'Action already processed', status: action.status }\n }\n\n // Check target module permission\n if (action.requiredFeature) {\n const featureGranted =\n ctx.isSuperAdmin || hasFeature(ctx.userFeatures, action.requiredFeature)\n if (!featureGranted) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature,\n }\n }\n }\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const eventBus = resolveOptionalEventBus(ctx.container)\n\n const result = await executeAction(action, {\n em,\n userId: ctx.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n eventBus,\n container: ctx.container,\n entities: entities as unknown as import('./lib/executionEngine').CrossModuleEntities,\n })\n\n if (!result.success) {\n if (result.statusCode === 409) {\n return { error: 'Action already processed', status: 'accepted' }\n }\n if (result.statusCode === 403) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature ?? 'unknown',\n }\n }\n return { error: 'Execution failed', detail: result.error ?? 'Unknown error' }\n }\n\n try {\n const { resolveCache, invalidateCountsCache } = await import('./lib/cache')\n const cache = resolveCache(ctx.container)\n if (cache && scope.tenantId) {\n await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId))\n }\n } catch { /* cache invalidation is non-critical */ }\n\n return {\n ok: true,\n createdEntityId: result.createdEntityId ?? null,\n createdEntityType: result.createdEntityType ?? null,\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_categorize_email \u2014 Standalone LLM-based text categorization\n// =============================================================================\n\nconst categorizeEmailSchema = z.object({\n category: inboxProposalCategoryEnum,\n confidence: z.number(),\n reasoning: z.string(),\n})\n\nconst categorizeEmailTool = {\n name: 'inbox_ops_categorize_email',\n description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.\n\nReturns: { category, confidence (0-1), reasoning }\nInput text is limited to 10,000 characters for cost control.`,\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(10000)\n .describe('Email or text content to categorize (max 10K chars)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { text: string }, ctx: ToolContext) => {\n requireTenantContext(ctx)\n\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n\n const modelConfig = resolveOpenCodeModel(providerId, {})\n const model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n\n const { generateObject } = await import('ai')\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: categorizeEmailSchema,\n system: `You are an email classification agent. Classify the given text into exactly one category:\n- rfq: Request for quotation or pricing inquiry\n- order: New purchase order or order placement\n- order_update: Change or update to an existing order\n- complaint: Customer complaint, dispute, or dissatisfaction\n- shipping_update: Shipment status, tracking, or delivery information\n- inquiry: General question or information request\n- payment: Payment-related (invoice, receipt, payment terms)\n- other: Does not fit any category above\n\nReturn a JSON object with:\n- category: one of the categories above\n- confidence: a number between 0 and 1 indicating how confident you are\n- reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,\n prompt: input.text,\n temperature: 0,\n }),\n 15000,\n 'Email categorization timed out after 15s',\n )\n\n return {\n category: result.object.category,\n confidence: Math.round(result.object.confidence * 100) / 100,\n reasoning: result.object.reasoning,\n }\n },\n}\n\n// =============================================================================\n// Export\n// =============================================================================\n\n/**\n * All AI tools exported by the inbox_ops module.\n * Discovered by ai-assistant module's generator.\n */\nexport const aiTools: AiToolDefinition[] = [\n listProposalsTool,\n getProposalTool,\n acceptActionTool,\n categorizeEmailTool,\n]\n\nexport default aiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { generateObject } from "ai";
|
|
2
|
+
import { createContainer } from "awilix";
|
|
2
3
|
import {
|
|
3
4
|
resolveFirstConfiguredOpenCodeProvider,
|
|
4
5
|
resolveOpenCodeModel,
|
|
5
6
|
requireOpenCodeProviderApiKey,
|
|
6
7
|
resolveOpenCodeProviderId
|
|
7
8
|
} from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
9
|
+
import {
|
|
10
|
+
AiModelFactoryError,
|
|
11
|
+
createModelFactory
|
|
12
|
+
} from "@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory";
|
|
8
13
|
import { extractionOutputSchema } from "../data/validators.js";
|
|
9
14
|
function asAiModel(model) {
|
|
10
15
|
return model;
|
|
@@ -51,13 +56,52 @@ async function withTimeout(operation, timeoutMs, timeoutMessage) {
|
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
}
|
|
59
|
+
const __inboxOpsLlmProviderInternal = {
|
|
60
|
+
createModelFactory,
|
|
61
|
+
createContainer
|
|
62
|
+
};
|
|
63
|
+
function tryFactoryResolution(input) {
|
|
64
|
+
let factory;
|
|
65
|
+
try {
|
|
66
|
+
const container = __inboxOpsLlmProviderInternal.createContainer();
|
|
67
|
+
factory = __inboxOpsLlmProviderInternal.createModelFactory(container);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const resolution = factory.resolveModel({
|
|
73
|
+
moduleId: "inbox_ops",
|
|
74
|
+
callerOverride: input.modelOverride ?? void 0
|
|
75
|
+
});
|
|
76
|
+
const providerId = resolveOpenCodeProviderId(resolution.providerId);
|
|
77
|
+
return {
|
|
78
|
+
modelId: resolution.modelId,
|
|
79
|
+
providerId,
|
|
80
|
+
model: asAiModel(resolution.model)
|
|
81
|
+
};
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof AiModelFactoryError) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
54
89
|
async function runExtractionWithConfiguredProvider(input) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
const factoryResolution = tryFactoryResolution({ modelOverride: input.modelOverride });
|
|
91
|
+
let model;
|
|
92
|
+
let modelWithProvider;
|
|
93
|
+
if (factoryResolution) {
|
|
94
|
+
model = factoryResolution.model;
|
|
95
|
+
modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`;
|
|
96
|
+
} else {
|
|
97
|
+
const providerId = resolveExtractionProviderId();
|
|
98
|
+
const apiKey = requireOpenCodeProviderApiKey(providerId);
|
|
99
|
+
const modelConfig = resolveOpenCodeModel(providerId, {
|
|
100
|
+
overrideModel: input.modelOverride
|
|
101
|
+
});
|
|
102
|
+
model = await createStructuredModel(providerId, apiKey, modelConfig.modelId);
|
|
103
|
+
modelWithProvider = modelConfig.modelWithProvider;
|
|
104
|
+
}
|
|
61
105
|
const result = await withTimeout(
|
|
62
106
|
generateObject({
|
|
63
107
|
model,
|
|
@@ -72,10 +116,11 @@ async function runExtractionWithConfiguredProvider(input) {
|
|
|
72
116
|
return {
|
|
73
117
|
object: result.object,
|
|
74
118
|
totalTokens: Number(result.usage?.totalTokens ?? 0) || 0,
|
|
75
|
-
modelWithProvider
|
|
119
|
+
modelWithProvider
|
|
76
120
|
};
|
|
77
121
|
}
|
|
78
122
|
export {
|
|
123
|
+
__inboxOpsLlmProviderInternal,
|
|
79
124
|
createStructuredModel,
|
|
80
125
|
resolveExtractionProviderId,
|
|
81
126
|
runExtractionWithConfiguredProvider,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/inbox_ops/lib/llmProvider.ts"],
|
|
4
|
-
"sourcesContent": ["import { generateObject } from 'ai'\nimport {\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n resolveOpenCodeProviderId,\n type OpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { extractionOutputSchema } from '../data/validators'\n\n// Vercel AI SDK provider factories return LanguageModelV1 but generateObject()\n// expects a narrower LanguageModel union. The types are structurally compatible\n// at runtime; the cast is required until the AI SDK unifies its model types.\ntype AiModel = Parameters<typeof generateObject>[0]['model']\nfunction asAiModel(model: unknown): AiModel {\n return model as AiModel\n}\n\nexport function resolveExtractionProviderId(): OpenCodeProviderId {\n const configuredProvider = process.env.OPENCODE_PROVIDER\n if (configuredProvider && configuredProvider.trim().length > 0) {\n return resolveOpenCodeProviderId(configuredProvider)\n }\n\n const firstConfiguredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (firstConfiguredProvider) {\n return firstConfiguredProvider\n }\n\n return resolveOpenCodeProviderId(undefined)\n}\n\nexport async function createStructuredModel(\n providerId: OpenCodeProviderId,\n apiKey: string,\n modelId: string,\n): Promise<AiModel> {\n switch (providerId) {\n case 'anthropic': {\n const { createAnthropic } = await import('@ai-sdk/anthropic')\n return asAiModel(createAnthropic({ apiKey })(modelId))\n }\n case 'openai': {\n const { createOpenAI } = await import('@ai-sdk/openai')\n return asAiModel(createOpenAI({ apiKey })(modelId))\n }\n case 'google': {\n const { createGoogleGenerativeAI } = await import('@ai-sdk/google')\n return asAiModel(createGoogleGenerativeAI({ apiKey })(modelId))\n }\n default:\n throw new Error(`Unsupported provider: ${providerId}`)\n }\n}\n\nexport async function withTimeout<T>(\n operation: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n): Promise<T> {\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n })\n\n try {\n return await Promise.race([operation, timeoutPromise])\n } finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n }\n}\n\nexport async function runExtractionWithConfiguredProvider(input: {\n systemPrompt: string\n userPrompt: string\n modelOverride?: string | null\n timeoutMs: number\n}): Promise<{\n object: ReturnType<typeof extractionOutputSchema.parse>\n totalTokens: number\n modelWithProvider: string\n}> {\n const providerId = resolveExtractionProviderId()\n
|
|
5
|
-
"mappings": "AAAA,SAAS,sBAAsB;
|
|
4
|
+
"sourcesContent": ["import { generateObject } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport { createContainer } from 'awilix'\nimport {\n resolveFirstConfiguredOpenCodeProvider,\n resolveOpenCodeModel,\n requireOpenCodeProviderApiKey,\n resolveOpenCodeProviderId,\n type OpenCodeProviderId,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n AiModelFactoryError,\n createModelFactory,\n type AiModelFactory,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory'\nimport { extractionOutputSchema } from '../data/validators'\n\n// Vercel AI SDK provider factories return LanguageModelV1 but generateObject()\n// expects a narrower LanguageModel union. The types are structurally compatible\n// at runtime; the cast is required until the AI SDK unifies its model types.\ntype AiModel = Parameters<typeof generateObject>[0]['model']\nfunction asAiModel(model: unknown): AiModel {\n return model as AiModel\n}\n\n/**\n * Step 5.1 \u2014 thin backward-compatibility shim. The public surface of this\n * module (`resolveExtractionProviderId`, `createStructuredModel`,\n * `withTimeout`, `runExtractionWithConfiguredProvider`) is unchanged so\n * `ai-tools.ts`, `translationProvider.ts`, and `extractionWorker.ts` continue\n * to compile and pass their existing tests.\n *\n * The model-instantiation path inside {@link runExtractionWithConfiguredProvider}\n * now delegates to the shared {@link createModelFactory} so every\n * AI-runtime caller shares one resolution order. The legacy\n * `OPENCODE_MODEL` / `OPENCODE_PROVIDER` envs remain honored via\n * {@link resolveExtractionProviderId} and {@link resolveOpenCodeModel} so\n * inbox_ops deployments do not see a behavior change \u2014 the factory is\n * consulted first (honoring `INBOX_OPS_AI_MODEL` + `input.modelOverride`),\n * with the legacy path as the fallback when no registry provider is\n * configured (preserving the historical error messages).\n */\n\nexport function resolveExtractionProviderId(): OpenCodeProviderId {\n const configuredProvider = process.env.OPENCODE_PROVIDER\n if (configuredProvider && configuredProvider.trim().length > 0) {\n return resolveOpenCodeProviderId(configuredProvider)\n }\n\n const firstConfiguredProvider = resolveFirstConfiguredOpenCodeProvider()\n if (firstConfiguredProvider) {\n return firstConfiguredProvider\n }\n\n return resolveOpenCodeProviderId(undefined)\n}\n\nexport async function createStructuredModel(\n providerId: OpenCodeProviderId,\n apiKey: string,\n modelId: string,\n): Promise<AiModel> {\n switch (providerId) {\n case 'anthropic': {\n const { createAnthropic } = await import('@ai-sdk/anthropic')\n return asAiModel(createAnthropic({ apiKey })(modelId))\n }\n case 'openai': {\n const { createOpenAI } = await import('@ai-sdk/openai')\n return asAiModel(createOpenAI({ apiKey })(modelId))\n }\n case 'google': {\n const { createGoogleGenerativeAI } = await import('@ai-sdk/google')\n return asAiModel(createGoogleGenerativeAI({ apiKey })(modelId))\n }\n default:\n throw new Error(`Unsupported provider: ${providerId}`)\n }\n}\n\nexport async function withTimeout<T>(\n operation: Promise<T>,\n timeoutMs: number,\n timeoutMessage: string,\n): Promise<T> {\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n })\n\n try {\n return await Promise.race([operation, timeoutPromise])\n } finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n }\n}\n\n/**\n * Test-only seam for the factory-delegation regression suite. Production\n * callers MUST use {@link runExtractionWithConfiguredProvider} directly; the\n * suite overrides this binding via `jest.spyOn` to assert the shim actually\n * reaches `createModelFactory` without stubbing `@open-mercato/ai-assistant`.\n */\nexport const __inboxOpsLlmProviderInternal = {\n createModelFactory,\n createContainer,\n}\n\nfunction tryFactoryResolution(input: {\n modelOverride?: string | null\n}): { modelId: string; providerId: OpenCodeProviderId; model: AiModel } | null {\n let factory: AiModelFactory\n try {\n const container = __inboxOpsLlmProviderInternal.createContainer()\n factory = __inboxOpsLlmProviderInternal.createModelFactory(container as AwilixContainer)\n } catch {\n return null\n }\n try {\n const resolution = factory.resolveModel({\n moduleId: 'inbox_ops',\n callerOverride: input.modelOverride ?? undefined,\n })\n const providerId = resolveOpenCodeProviderId(resolution.providerId)\n return {\n modelId: resolution.modelId,\n providerId,\n model: asAiModel(resolution.model),\n }\n } catch (err) {\n if (err instanceof AiModelFactoryError) {\n // Fall back to the legacy path so the shim keeps throwing the original\n // OPENCODE_*-era error messages existing tests/consumers rely on.\n return null\n }\n throw err\n }\n}\n\nexport async function runExtractionWithConfiguredProvider(input: {\n systemPrompt: string\n userPrompt: string\n modelOverride?: string | null\n timeoutMs: number\n}): Promise<{\n object: ReturnType<typeof extractionOutputSchema.parse>\n totalTokens: number\n modelWithProvider: string\n}> {\n const factoryResolution = tryFactoryResolution({ modelOverride: input.modelOverride })\n\n let model: AiModel\n let modelWithProvider: string\n if (factoryResolution) {\n model = factoryResolution.model\n modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`\n } else {\n const providerId = resolveExtractionProviderId()\n const apiKey = requireOpenCodeProviderApiKey(providerId)\n const modelConfig = resolveOpenCodeModel(providerId, {\n overrideModel: input.modelOverride,\n })\n model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)\n modelWithProvider = modelConfig.modelWithProvider\n }\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: extractionOutputSchema,\n system: input.systemPrompt,\n prompt: input.userPrompt,\n temperature: 0,\n }),\n input.timeoutMs,\n `LLM extraction timed out after ${input.timeoutMs}ms`,\n )\n\n return {\n object: result.object,\n totalTokens: Number(result.usage?.totalTokens ?? 0) || 0,\n modelWithProvider,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const setup = {
|
|
2
|
+
defaultRoleFeatures: {
|
|
3
|
+
superadmin: ["notifications.*"],
|
|
4
|
+
admin: ["notifications.*"],
|
|
5
|
+
employee: ["notifications.view"]
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var setup_default = setup;
|
|
9
|
+
export {
|
|
10
|
+
setup_default as default,
|
|
11
|
+
setup
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/notifications/setup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['notifications.*'],\n admin: ['notifications.*'],\n employee: ['notifications.view'],\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,iBAAiB;AAAA,IAC9B,OAAO,CAAC,iBAAiB;AAAA,IACzB,UAAU,CAAC,oBAAoB;AAAA,EACjC;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/jest.config.cjs
CHANGED
|
@@ -10,6 +10,7 @@ module.exports = {
|
|
|
10
10
|
'^@open-mercato/core/(.*)$': '<rootDir>/src/$1',
|
|
11
11
|
'^@open-mercato/shared/(.*)$': '<rootDir>/../shared/src/$1',
|
|
12
12
|
'^@open-mercato/ui/(.*)$': '<rootDir>/../ui/src/$1',
|
|
13
|
+
'^@open-mercato/ai-assistant/(.*)$': '<rootDir>/../ai-assistant/src/$1',
|
|
13
14
|
'^@/\\.mercato/generated/inbox-actions\\.generated$': '<rootDir>/jest.mocks/inbox-actions.generated.js',
|
|
14
15
|
'^react-markdown$': '<rootDir>/jest.mocks/react-markdown.js',
|
|
15
16
|
'^remark-gfm$': '<rootDir>/jest.mocks/remark-gfm.js',
|
package/jest.setup.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import '@testing-library/jest-dom'
|
|
2
2
|
|
|
3
|
+
// Web Streams polyfill for jsdom — `eventsource-parser/stream` (loaded
|
|
4
|
+
// transitively through `ai` -> `@ai-sdk/*` whenever a test file imports
|
|
5
|
+
// `@open-mercato/ui/ai/useAiChat` or any UI component that touches it)
|
|
6
|
+
// references TransformStream/ReadableStream/WritableStream at module
|
|
7
|
+
// load time. jsdom doesn't ship those globals, so the import throws
|
|
8
|
+
// before the test body runs. Pull them from `node:stream/web` (Node 18+).
|
|
9
|
+
import { ReadableStream, WritableStream, TransformStream } from 'node:stream/web'
|
|
10
|
+
|
|
11
|
+
if (typeof globalThis.TransformStream === 'undefined') {
|
|
12
|
+
;(globalThis as any).TransformStream = TransformStream
|
|
13
|
+
}
|
|
14
|
+
if (typeof globalThis.ReadableStream === 'undefined') {
|
|
15
|
+
;(globalThis as any).ReadableStream = ReadableStream
|
|
16
|
+
}
|
|
17
|
+
if (typeof globalThis.WritableStream === 'undefined') {
|
|
18
|
+
;(globalThis as any).WritableStream = WritableStream
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
jest.mock('react-markdown', () => ({
|
|
4
22
|
__esModule: true,
|
|
5
23
|
default: ({ children }: { children?: unknown }) => children ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.3045.b4b3320cc2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -237,10 +237,12 @@
|
|
|
237
237
|
"ts-pattern": "^5.0.0"
|
|
238
238
|
},
|
|
239
239
|
"peerDependencies": {
|
|
240
|
-
"@open-mercato/
|
|
240
|
+
"@open-mercato/ai-assistant": "0.5.1-develop.3045.b4b3320cc2",
|
|
241
|
+
"@open-mercato/shared": "0.5.1-develop.3045.b4b3320cc2"
|
|
241
242
|
},
|
|
242
243
|
"devDependencies": {
|
|
243
|
-
"@open-mercato/
|
|
244
|
+
"@open-mercato/ai-assistant": "0.5.1-develop.3045.b4b3320cc2",
|
|
245
|
+
"@open-mercato/shared": "0.5.1-develop.3045.b4b3320cc2",
|
|
244
246
|
"@testing-library/dom": "^10.4.1",
|
|
245
247
|
"@testing-library/jest-dom": "^6.9.1",
|
|
246
248
|
"@testing-library/react": "^16.3.1",
|
|
@@ -7,6 +7,12 @@ function resolveUrl(path: string): string {
|
|
|
7
7
|
return BASE_URL ? `${BASE_URL}${path}` : path;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// Cached tokens per credential to dodge the login rate limit
|
|
11
|
+
// (5 attempts/60s per email). Tokens are reused across tests in the same
|
|
12
|
+
// Playwright worker; each worker still mints its own.
|
|
13
|
+
const tokenCache = new Map<string, { token: string; mintedAt: number }>();
|
|
14
|
+
const TOKEN_TTL_MS = 45 * 60 * 1000; // 45 min; well under the default 2h session TTL.
|
|
15
|
+
|
|
10
16
|
export async function getAuthToken(
|
|
11
17
|
request: APIRequestContext,
|
|
12
18
|
roleOrEmail: Role | string = 'admin',
|
|
@@ -25,6 +31,14 @@ export async function getAuthToken(
|
|
|
25
31
|
credentialAttempts.push({ email: roleOrEmail, password: password ?? 'secret' });
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
const cacheKey = credentialAttempts
|
|
35
|
+
.map((entry) => `${entry.email}:${entry.password}`)
|
|
36
|
+
.join('|');
|
|
37
|
+
const cached = tokenCache.get(cacheKey);
|
|
38
|
+
if (cached && Date.now() - cached.mintedAt < TOKEN_TTL_MS) {
|
|
39
|
+
return cached.token;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
let lastStatus = 0;
|
|
29
43
|
|
|
30
44
|
for (const attempt of credentialAttempts) {
|
|
@@ -32,24 +46,32 @@ export async function getAuthToken(
|
|
|
32
46
|
form.set('email', attempt.email);
|
|
33
47
|
form.set('password', attempt.password);
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
// Retry on 429 (auth rate limit kicks in after ~25-30 rapid attempts from
|
|
50
|
+
// the same test run). Capped exponential backoff: 1s, 2s, 4s; 3 retries.
|
|
51
|
+
for (let retry = 0; retry < 4; retry += 1) {
|
|
52
|
+
const response = await request.post(resolveUrl('/api/auth/login'), {
|
|
53
|
+
headers: {
|
|
54
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
55
|
+
},
|
|
56
|
+
data: form.toString(),
|
|
57
|
+
});
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
const raw = await response.text();
|
|
60
|
+
let body: Record<string, unknown> | null = null;
|
|
61
|
+
try {
|
|
62
|
+
body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
|
|
63
|
+
} catch {
|
|
64
|
+
body = null;
|
|
65
|
+
}
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
lastStatus = response.status();
|
|
68
|
+
if (response.ok() && body && typeof body.token === 'string' && body.token) {
|
|
69
|
+
tokenCache.set(cacheKey, { token: body.token, mintedAt: Date.now() });
|
|
70
|
+
return body.token;
|
|
71
|
+
}
|
|
72
|
+
if (response.status() !== 429) break;
|
|
73
|
+
const backoffMs = 1000 * 2 ** retry;
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
53
75
|
}
|
|
54
76
|
}
|
|
55
77
|
|