@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3043.1a796c3920
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +13 -1
- package/dist/helpers/integration/api.js +29 -16
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/auth.js +11 -6
- package/dist/helpers/integration/auth.js.map +3 -3
- package/dist/modules/auth/commands/roles.js +9 -12
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/catalog/ai-agents-context.js +147 -0
- package/dist/modules/catalog/ai-agents-context.js.map +7 -0
- package/dist/modules/catalog/ai-agents.js +383 -0
- package/dist/modules/catalog/ai-agents.js.map +7 -0
- package/dist/modules/catalog/ai-tools/_shared.js +318 -0
- package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
- package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/types.js +10 -0
- package/dist/modules/catalog/ai-tools/types.js.map +7 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools.js +28 -0
- package/dist/modules/catalog/ai-tools.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
- package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
- package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
- package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
- package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/events.js +7 -4
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/catalog/widgets/injection-table.js +13 -1
- package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/ai-agents-context.js +96 -0
- package/dist/modules/customers/ai-agents-context.js.map +7 -0
- package/dist/modules/customers/ai-agents.js +244 -0
- package/dist/modules/customers/ai-agents.js.map +7 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
- package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
- package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/people-pack.js +261 -0
- package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
- package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/types.js +10 -0
- package/dist/modules/customers/ai-tools/types.js.map +7 -0
- package/dist/modules/customers/ai-tools.js +20 -0
- package/dist/modules/customers/ai-tools.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection-table.js +26 -0
- package/dist/modules/customers/widgets/injection-table.js.map +7 -0
- package/dist/modules/inbox_ops/ai-tools.js +4 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
- package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
- package/dist/modules/notifications/setup.js +13 -0
- package/dist/modules/notifications/setup.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.setup.ts +18 -0
- package/package.json +5 -3
- package/src/helpers/integration/api.ts +38 -16
- package/src/helpers/integration/auth.ts +13 -6
- package/src/modules/auth/commands/roles.ts +10 -12
- package/src/modules/catalog/AGENTS.md +11 -0
- package/src/modules/catalog/ai-agents-context.ts +239 -0
- package/src/modules/catalog/ai-agents.ts +525 -0
- package/src/modules/catalog/ai-tools/_shared.ts +487 -0
- package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
- package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
- package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
- package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
- package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
- package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
- package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
- package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
- package/src/modules/catalog/ai-tools/types.ts +81 -0
- package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
- package/src/modules/catalog/ai-tools.ts +78 -0
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
- package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
- package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
- package/src/modules/catalog/events.ts +7 -4
- package/src/modules/catalog/i18n/de.json +17 -0
- package/src/modules/catalog/i18n/en.json +17 -0
- package/src/modules/catalog/i18n/es.json +17 -0
- package/src/modules/catalog/i18n/pl.json +17 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
- package/src/modules/catalog/widgets/injection-table.ts +12 -0
- package/src/modules/customer_accounts/i18n/de.json +5 -0
- package/src/modules/customer_accounts/i18n/en.json +5 -0
- package/src/modules/customer_accounts/i18n/es.json +5 -0
- package/src/modules/customer_accounts/i18n/pl.json +5 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
- package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
- package/src/modules/customers/AGENTS.md +13 -0
- package/src/modules/customers/ai-agents-context.ts +150 -0
- package/src/modules/customers/ai-agents.ts +355 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
- package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
- package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
- package/src/modules/customers/ai-tools/people-pack.ts +369 -0
- package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
- package/src/modules/customers/ai-tools/types.ts +76 -0
- package/src/modules/customers/ai-tools.ts +34 -0
- package/src/modules/customers/i18n/de.json +25 -0
- package/src/modules/customers/i18n/en.json +25 -0
- package/src/modules/customers/i18n/es.json +25 -0
- package/src/modules/customers/i18n/pl.json +25 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
- package/src/modules/customers/widgets/injection-table.ts +41 -0
- package/src/modules/inbox_ops/ai-tools.ts +4 -0
- package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
- package/src/modules/notifications/setup.ts +11 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Step 4.10 — Portal AiChat injection widget (client).
|
|
5
|
+
*
|
|
6
|
+
* Renders an "Ask AI" trigger button + sheet inside the portal profile
|
|
7
|
+
* page's `portal:profile:after` injection spot. Clicking the trigger
|
|
8
|
+
* opens a right-side sheet embedding `<AiChat>` wired to
|
|
9
|
+
* `customers.account_assistant`.
|
|
10
|
+
*
|
|
11
|
+
* Feature-gating:
|
|
12
|
+
* - Declared in `widget.ts` metadata (`portal.account.manage`).
|
|
13
|
+
* - The widget ALSO self-checks `context.resolvedFeatures` as a
|
|
14
|
+
* defense-in-depth measure so the button never renders for a
|
|
15
|
+
* customer who lacks the feature (portal pages are themselves
|
|
16
|
+
* feature-gated, but the injection registry does not currently
|
|
17
|
+
* enforce metadata.features at render time).
|
|
18
|
+
*
|
|
19
|
+
* `pageContext` shape:
|
|
20
|
+
* { view: 'portal.profile', recordType: 'customer', recordId: <userId|null>, extra: {} }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as React from 'react'
|
|
24
|
+
import { Sparkles } from 'lucide-react'
|
|
25
|
+
import { AiChat } from '@open-mercato/ui/ai/AiChat'
|
|
26
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
27
|
+
import {
|
|
28
|
+
Dialog,
|
|
29
|
+
DialogContent,
|
|
30
|
+
DialogDescription,
|
|
31
|
+
DialogHeader,
|
|
32
|
+
DialogTitle,
|
|
33
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
34
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
35
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
36
|
+
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
37
|
+
|
|
38
|
+
export const PORTAL_AI_INJECT_AGENT_ID = 'customers.account_assistant'
|
|
39
|
+
export const PORTAL_AI_INJECT_REQUIRED_FEATURE = 'portal.account.manage'
|
|
40
|
+
|
|
41
|
+
export interface PortalAiInjectPageContext {
|
|
42
|
+
view: 'portal.profile'
|
|
43
|
+
recordType: 'customer'
|
|
44
|
+
recordId: string | null
|
|
45
|
+
extra: Record<string, never>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface PortalInjectionContext {
|
|
49
|
+
orgSlug?: string
|
|
50
|
+
user?: { id?: string | null } | null
|
|
51
|
+
resolvedFeatures?: string[]
|
|
52
|
+
isPortalAdmin?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface PortalAiAssistantTriggerProps {
|
|
56
|
+
context?: PortalInjectionContext
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readUserId(user: PortalInjectionContext['user']): string | null {
|
|
60
|
+
if (!user) return null
|
|
61
|
+
const id = user.id
|
|
62
|
+
return typeof id === 'string' && id.length > 0 ? id : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default function PortalAiAssistantTriggerWidget({ context }: PortalAiAssistantTriggerProps) {
|
|
66
|
+
const t = useT()
|
|
67
|
+
const [open, setOpen] = React.useState(false)
|
|
68
|
+
|
|
69
|
+
const resolvedFeatures = Array.isArray(context?.resolvedFeatures)
|
|
70
|
+
? (context?.resolvedFeatures as string[])
|
|
71
|
+
: []
|
|
72
|
+
const featureAllowed =
|
|
73
|
+
context?.isPortalAdmin === true ||
|
|
74
|
+
hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE)
|
|
75
|
+
|
|
76
|
+
const pageContext = React.useMemo<PortalAiInjectPageContext>(() => ({
|
|
77
|
+
view: 'portal.profile',
|
|
78
|
+
recordType: 'customer',
|
|
79
|
+
recordId: readUserId(context?.user ?? null),
|
|
80
|
+
extra: {},
|
|
81
|
+
}), [context?.user])
|
|
82
|
+
|
|
83
|
+
if (!featureAllowed) return null
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="mt-6" data-ai-portal-inject-wrapper="">
|
|
87
|
+
<Button
|
|
88
|
+
type="button"
|
|
89
|
+
variant="outline"
|
|
90
|
+
size="sm"
|
|
91
|
+
onClick={() => setOpen(true)}
|
|
92
|
+
data-ai-portal-inject-trigger=""
|
|
93
|
+
aria-label={t(
|
|
94
|
+
'customer_accounts.portal_ai_assistant.trigger.ariaLabel',
|
|
95
|
+
'Open portal AI assistant',
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Sparkles className="size-4" aria-hidden />
|
|
99
|
+
<span>{t('customer_accounts.portal_ai_assistant.trigger.label', 'Ask AI')}</span>
|
|
100
|
+
</Button>
|
|
101
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
102
|
+
<DialogContent
|
|
103
|
+
className={cn(
|
|
104
|
+
'sm:max-w-xl sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',
|
|
105
|
+
'sm:h-screen sm:max-h-screen sm:rounded-none sm:rounded-l-2xl',
|
|
106
|
+
'flex flex-col gap-3 p-4',
|
|
107
|
+
)}
|
|
108
|
+
data-ai-portal-inject-sheet=""
|
|
109
|
+
>
|
|
110
|
+
<DialogHeader>
|
|
111
|
+
<DialogTitle>
|
|
112
|
+
{t('customer_accounts.portal_ai_assistant.sheet.title', 'Portal AI assistant')}
|
|
113
|
+
</DialogTitle>
|
|
114
|
+
<DialogDescription>
|
|
115
|
+
{t(
|
|
116
|
+
'customer_accounts.portal_ai_assistant.sheet.description',
|
|
117
|
+
'Read-only assistant for portal customers. Ask about your account and recent activity.',
|
|
118
|
+
)}
|
|
119
|
+
</DialogDescription>
|
|
120
|
+
</DialogHeader>
|
|
121
|
+
<div className="min-h-0 flex-1" data-ai-portal-inject-chat-container="">
|
|
122
|
+
<AiChat
|
|
123
|
+
agent={PORTAL_AI_INJECT_AGENT_ID}
|
|
124
|
+
pageContext={pageContext as unknown as Record<string, unknown>}
|
|
125
|
+
className="h-full"
|
|
126
|
+
placeholder={t(
|
|
127
|
+
'customer_accounts.portal_ai_assistant.sheet.composerPlaceholder',
|
|
128
|
+
'Ask about your account...',
|
|
129
|
+
)}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</DialogContent>
|
|
133
|
+
</Dialog>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import PortalAiAssistantTriggerWidget from './widget.client'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Step 4.10 — Portal AiChat injection example.
|
|
6
|
+
*
|
|
7
|
+
* Demonstrates how a third-party module can drop `<AiChat>` onto a
|
|
8
|
+
* portal page it does NOT own (the customer-portal profile page) via
|
|
9
|
+
* the existing widget-injection system. Targets spot
|
|
10
|
+
* `portal:profile:after` (see `PortalInjectionSpots.pageAfter('profile')`
|
|
11
|
+
* in `packages/ui/src/backend/injection/spotIds.ts`).
|
|
12
|
+
*
|
|
13
|
+
* The trigger opens a right-side sheet embedding
|
|
14
|
+
* `<AiChat agent="customers.account_assistant" pageContext={...} />`.
|
|
15
|
+
* `pageContext` follows the spec §10.1 shape:
|
|
16
|
+
*
|
|
17
|
+
* { view: 'portal.profile',
|
|
18
|
+
* recordType: 'customer',
|
|
19
|
+
* recordId: <customer-user-id | null>,
|
|
20
|
+
* extra: {} }
|
|
21
|
+
*
|
|
22
|
+
* Gated behind customer feature `portal.account.manage`, which is the
|
|
23
|
+
* closest existing customer-facing feature (no dedicated
|
|
24
|
+
* `portal.ai_assistant.view` feature exists yet — tracked as a
|
|
25
|
+
* follow-up gap; see Step 4.10 checks).
|
|
26
|
+
*
|
|
27
|
+
* Phase 2 ships `customers.account_assistant` as the agent (read-only)
|
|
28
|
+
* because no dedicated customer-portal agent has been introduced yet.
|
|
29
|
+
*/
|
|
30
|
+
const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
|
|
31
|
+
metadata: {
|
|
32
|
+
id: 'customer_accounts.injection.portal-ai-assistant-trigger',
|
|
33
|
+
title: 'Portal AI Assistant Trigger',
|
|
34
|
+
description:
|
|
35
|
+
'Renders an "Ask AI" button on the portal profile page that opens a sheet embedding the customers account assistant.',
|
|
36
|
+
features: ['portal.account.manage'],
|
|
37
|
+
priority: 100,
|
|
38
|
+
enabled: true,
|
|
39
|
+
},
|
|
40
|
+
Widget: PortalAiAssistantTriggerWidget,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default widget
|
|
@@ -19,6 +19,15 @@ export const injectionTable: ModuleInjectionTable = {
|
|
|
19
19
|
priority: 200,
|
|
20
20
|
},
|
|
21
21
|
],
|
|
22
|
+
// Step 4.10 — Portal AiChat injection example.
|
|
23
|
+
// Mapped to the portal profile page's `pageAfter('profile')` spot;
|
|
24
|
+
// third-party modules targeting other portal pages can copy this entry.
|
|
25
|
+
'portal:profile:after': [
|
|
26
|
+
{
|
|
27
|
+
widgetId: 'customer_accounts.injection.portal-ai-assistant-trigger',
|
|
28
|
+
priority: 100,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export default injectionTable
|
|
@@ -93,3 +93,16 @@ Use `collectCustomFieldValues()` from `@open-mercato/ui/backend/utils/customFiel
|
|
|
93
93
|
## Module Files Checklist — All MUST Be Present
|
|
94
94
|
|
|
95
95
|
`acl.ts`, `ce.ts`, `di.ts`, `events.ts`, `index.ts`, `notifications.ts`, `search.ts`, `setup.ts`, `analytics.ts`, `vector.ts`
|
|
96
|
+
|
|
97
|
+
## AI Agents in This Module
|
|
98
|
+
|
|
99
|
+
This module is the reference implementation for the AI framework. Copy `ai-agents.ts` + `ai-tools.ts` when adding AI agents to other modules. See `/framework/ai-assistant/agents` for the full guide.
|
|
100
|
+
|
|
101
|
+
| Agent ID | Mode | Policy | Purpose |
|
|
102
|
+
|----------|------|--------|---------|
|
|
103
|
+
| `customers.account_assistant` | chat | read-only (mutation-capable via per-tenant override that unlocks `customers.update_deal_stage`; see `packages/core/src/modules/customers/ai-agents.ts` for the whitelist and prompt) | Operator-facing assistant that explores people, companies, deals, activities, tasks, addresses, tags, and settings through the customers tool pack. |
|
|
104
|
+
| `customers.update_deal_stage` | tool (mutation) | `destructive-confirm-required` — goes through `prepareMutation` + the approval card | Moves a deal between stages / flips status between open, won, lost. Declared via `defineAiTool` in `ai-tools.ts` and exposed only when the tenant mutation-policy override promotes the agent above read-only. |
|
|
105
|
+
|
|
106
|
+
`<AiChat agent="customers.account_assistant" />` is injected in two places (both live in `widgets/injection/`):
|
|
107
|
+
- People list: `data-table:customers.people.list:header` via the `ai-assistant-trigger` widget.
|
|
108
|
+
- Deal detail: `detail:customers.deal:header` via the `ai-deal-detail-trigger` widget.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-context hydration helpers for `customers.account_assistant`
|
|
3
|
+
* (Phase 3 WS-A, Step 5.2).
|
|
4
|
+
*
|
|
5
|
+
* The `resolvePageContext` callback on the module-root `ai-agents.ts`
|
|
6
|
+
* delegates to this file when the incoming request carries an `entityType`
|
|
7
|
+
* + `recordId` combination the runtime recognises. Each helper reuses the
|
|
8
|
+
* same tool-pack handler the Step 3.9 pack ships (`customers.get_person`,
|
|
9
|
+
* `customers.get_company`, `customers.get_deal`) so there is exactly one
|
|
10
|
+
* loader per record type — not a second parallel query path that could
|
|
11
|
+
* drift from what the agent is actually allowed to call.
|
|
12
|
+
*
|
|
13
|
+
* Every helper:
|
|
14
|
+
* - Runs only when `tenantId` is present; cross-tenant ids return `null`
|
|
15
|
+
* (the tool handlers already guard tenant scope via
|
|
16
|
+
* `findOneWithDecryption`, but we double-check in the output).
|
|
17
|
+
* - Caps `includeRelated` payloads to what the tool's own cap enforces.
|
|
18
|
+
* - Swallows errors and returns `null` so a hydration fault NEVER breaks
|
|
19
|
+
* the chat request — the runtime will proceed without extra context.
|
|
20
|
+
*/
|
|
21
|
+
import type { AwilixContainer } from 'awilix'
|
|
22
|
+
import customersAiTools from './ai-tools'
|
|
23
|
+
import type {
|
|
24
|
+
CustomersAiToolDefinition,
|
|
25
|
+
CustomersToolContext,
|
|
26
|
+
} from './ai-tools/types'
|
|
27
|
+
|
|
28
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
29
|
+
|
|
30
|
+
function isUuid(value: unknown): value is string {
|
|
31
|
+
return typeof value === 'string' && UUID_REGEX.test(value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findTool(name: string): CustomersAiToolDefinition | null {
|
|
35
|
+
return (
|
|
36
|
+
(customersAiTools as CustomersAiToolDefinition[]).find((tool) => tool.name === name) ?? null
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildToolContext(
|
|
41
|
+
container: AwilixContainer,
|
|
42
|
+
tenantId: string,
|
|
43
|
+
organizationId: string | null,
|
|
44
|
+
): CustomersToolContext {
|
|
45
|
+
return {
|
|
46
|
+
tenantId,
|
|
47
|
+
organizationId,
|
|
48
|
+
userId: null,
|
|
49
|
+
container,
|
|
50
|
+
userFeatures: [],
|
|
51
|
+
isSuperAdmin: true,
|
|
52
|
+
apiKeySecret: undefined,
|
|
53
|
+
sessionId: undefined,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderContextBlock(label: string, payload: unknown): string {
|
|
58
|
+
return `## Page context — ${label}\n${JSON.stringify(payload, null, 2)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface HydrateCustomersContextInput {
|
|
62
|
+
entityType: string
|
|
63
|
+
recordId: string
|
|
64
|
+
container: AwilixContainer
|
|
65
|
+
tenantId: string | null
|
|
66
|
+
organizationId: string | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const PERSON_ENTITY_TYPES = new Set([
|
|
70
|
+
'person',
|
|
71
|
+
'customers.person',
|
|
72
|
+
'customers:customer_entity',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
const COMPANY_ENTITY_TYPES = new Set([
|
|
76
|
+
'company',
|
|
77
|
+
'customers.company',
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
const DEAL_ENTITY_TYPES = new Set([
|
|
81
|
+
'deal',
|
|
82
|
+
'customers.deal',
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
async function hydrateWithTool(
|
|
86
|
+
toolName: string,
|
|
87
|
+
inputArgs: Record<string, unknown>,
|
|
88
|
+
toolContext: CustomersToolContext,
|
|
89
|
+
): Promise<unknown | null> {
|
|
90
|
+
const tool = findTool(toolName)
|
|
91
|
+
if (!tool) {
|
|
92
|
+
console.warn(`[customers.account_assistant] resolvePageContext: tool "${toolName}" not registered`)
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const result = await tool.handler(inputArgs as never, toolContext)
|
|
97
|
+
if (!result || typeof result !== 'object') return null
|
|
98
|
+
if ((result as { found?: boolean }).found === false) return null
|
|
99
|
+
return result
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[customers.account_assistant] resolvePageContext: tool "${toolName}" failed (reason="hydration_error"); skipping`,
|
|
103
|
+
error instanceof Error ? error.message : error,
|
|
104
|
+
)
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function hydrateCustomersAccountContext(
|
|
110
|
+
input: HydrateCustomersContextInput,
|
|
111
|
+
): Promise<string | null> {
|
|
112
|
+
const tenantId = input.tenantId
|
|
113
|
+
if (!tenantId) return null
|
|
114
|
+
if (!isUuid(input.recordId)) return null
|
|
115
|
+
const entityType = input.entityType.trim().toLowerCase()
|
|
116
|
+
if (!entityType) return null
|
|
117
|
+
const toolContext = buildToolContext(input.container, tenantId, input.organizationId)
|
|
118
|
+
|
|
119
|
+
if (PERSON_ENTITY_TYPES.has(entityType)) {
|
|
120
|
+
const result = await hydrateWithTool(
|
|
121
|
+
'customers.get_person',
|
|
122
|
+
{ personId: input.recordId, includeRelated: true },
|
|
123
|
+
toolContext,
|
|
124
|
+
)
|
|
125
|
+
if (!result) return null
|
|
126
|
+
return renderContextBlock(`Person ${input.recordId}`, result)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (COMPANY_ENTITY_TYPES.has(entityType)) {
|
|
130
|
+
const result = await hydrateWithTool(
|
|
131
|
+
'customers.get_company',
|
|
132
|
+
{ companyId: input.recordId, includeRelated: true },
|
|
133
|
+
toolContext,
|
|
134
|
+
)
|
|
135
|
+
if (!result) return null
|
|
136
|
+
return renderContextBlock(`Company ${input.recordId}`, result)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (DEAL_ENTITY_TYPES.has(entityType)) {
|
|
140
|
+
const result = await hydrateWithTool(
|
|
141
|
+
'customers.get_deal',
|
|
142
|
+
{ dealId: input.recordId, includeRelated: true },
|
|
143
|
+
toolContext,
|
|
144
|
+
)
|
|
145
|
+
if (!result) return null
|
|
146
|
+
return renderContextBlock(`Deal ${input.recordId}`, result)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null
|
|
150
|
+
}
|