@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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `customers.list_addresses` + `customers.list_tags` (Phase 1 WS-C, Step 3.9).
|
|
3
|
+
*/
|
|
4
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
|
+
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
8
|
+
import { CustomerAddress, CustomerTag } from '../data/entities'
|
|
9
|
+
import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
|
|
10
|
+
|
|
11
|
+
function resolveEm(ctx: CustomersToolContext): EntityManager {
|
|
12
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildScope(ctx: CustomersToolContext, tenantId: string) {
|
|
16
|
+
return { tenantId, organizationId: ctx.organizationId }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const listAddressesInput = z.object({
|
|
20
|
+
entityType: z.enum(['person', 'company']).describe('Parent entity kind.'),
|
|
21
|
+
entityId: z.string().uuid().describe('Parent person/company entity id.'),
|
|
22
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 100).'),
|
|
23
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const listAddressesTool: CustomersAiToolDefinition = {
|
|
27
|
+
name: 'customers.list_addresses',
|
|
28
|
+
displayName: 'List addresses',
|
|
29
|
+
description:
|
|
30
|
+
'List addresses attached to a person or company (tenant + organization scoped). `entityType` is informational; the actual filter is by `entityId`.',
|
|
31
|
+
inputSchema: listAddressesInput,
|
|
32
|
+
// Addresses share the same route-level guard as activities in the existing
|
|
33
|
+
// route handler (`customers.activities.view`).
|
|
34
|
+
requiredFeatures: ['customers.activities.view'],
|
|
35
|
+
tags: ['read', 'customers'],
|
|
36
|
+
handler: async (rawInput, ctx) => {
|
|
37
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
38
|
+
const input = listAddressesInput.parse(rawInput)
|
|
39
|
+
const em = resolveEm(ctx)
|
|
40
|
+
const limit = input.limit ?? 100
|
|
41
|
+
const offset = input.offset ?? 0
|
|
42
|
+
const where: Record<string, unknown> = { tenantId, entity: input.entityId }
|
|
43
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
44
|
+
const [rows, total] = await Promise.all([
|
|
45
|
+
findWithDecryption<CustomerAddress>(
|
|
46
|
+
em,
|
|
47
|
+
CustomerAddress,
|
|
48
|
+
where as any,
|
|
49
|
+
{ limit, offset, orderBy: { isPrimary: 'desc', createdAt: 'desc' } as any } as any,
|
|
50
|
+
buildScope(ctx, tenantId),
|
|
51
|
+
),
|
|
52
|
+
em.count(CustomerAddress, where as any),
|
|
53
|
+
])
|
|
54
|
+
const filtered = rows.filter((row) => row.tenantId === tenantId)
|
|
55
|
+
return {
|
|
56
|
+
entityType: input.entityType,
|
|
57
|
+
entityId: input.entityId,
|
|
58
|
+
items: filtered.map((row) => ({
|
|
59
|
+
id: row.id,
|
|
60
|
+
name: row.name ?? null,
|
|
61
|
+
purpose: row.purpose ?? null,
|
|
62
|
+
companyName: row.companyName ?? null,
|
|
63
|
+
addressLine1: row.addressLine1,
|
|
64
|
+
addressLine2: row.addressLine2 ?? null,
|
|
65
|
+
buildingNumber: row.buildingNumber ?? null,
|
|
66
|
+
flatNumber: row.flatNumber ?? null,
|
|
67
|
+
city: row.city ?? null,
|
|
68
|
+
region: row.region ?? null,
|
|
69
|
+
postalCode: row.postalCode ?? null,
|
|
70
|
+
country: row.country ?? null,
|
|
71
|
+
latitude: row.latitude ?? null,
|
|
72
|
+
longitude: row.longitude ?? null,
|
|
73
|
+
isPrimary: !!row.isPrimary,
|
|
74
|
+
organizationId: row.organizationId ?? null,
|
|
75
|
+
tenantId: row.tenantId ?? null,
|
|
76
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
77
|
+
})),
|
|
78
|
+
total,
|
|
79
|
+
limit,
|
|
80
|
+
offset,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const listTagsInput = z
|
|
86
|
+
.object({
|
|
87
|
+
q: z.string().trim().optional().describe('Fuzzy search against tag label / slug. Omit or leave empty to list all.'),
|
|
88
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 100).'),
|
|
89
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
|
|
90
|
+
})
|
|
91
|
+
.passthrough()
|
|
92
|
+
|
|
93
|
+
const listTagsTool: CustomersAiToolDefinition = {
|
|
94
|
+
name: 'customers.list_tags',
|
|
95
|
+
displayName: 'List tags',
|
|
96
|
+
description:
|
|
97
|
+
'List available customer tags (slug, label, color, description) scoped to tenant + organization.',
|
|
98
|
+
inputSchema: listTagsInput,
|
|
99
|
+
// Tag administration routes require `customers.activities.*` in the
|
|
100
|
+
// existing codebase; keep the same least-privilege view feature here.
|
|
101
|
+
requiredFeatures: ['customers.activities.view'],
|
|
102
|
+
tags: ['read', 'customers'],
|
|
103
|
+
handler: async (rawInput, ctx) => {
|
|
104
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
105
|
+
const input = listTagsInput.parse(rawInput)
|
|
106
|
+
const em = resolveEm(ctx)
|
|
107
|
+
const limit = input.limit ?? 100
|
|
108
|
+
const offset = input.offset ?? 0
|
|
109
|
+
const where: Record<string, unknown> = { tenantId }
|
|
110
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
111
|
+
if (input.q) {
|
|
112
|
+
const pattern = `%${escapeLikePattern(input.q)}%`
|
|
113
|
+
where.$or = [{ label: { $ilike: pattern } }, { slug: { $ilike: pattern } }]
|
|
114
|
+
}
|
|
115
|
+
const [rows, total] = await Promise.all([
|
|
116
|
+
findWithDecryption<CustomerTag>(
|
|
117
|
+
em,
|
|
118
|
+
CustomerTag,
|
|
119
|
+
where as any,
|
|
120
|
+
{ limit, offset, orderBy: { label: 'asc' } as any } as any,
|
|
121
|
+
buildScope(ctx, tenantId),
|
|
122
|
+
),
|
|
123
|
+
em.count(CustomerTag, where as any),
|
|
124
|
+
])
|
|
125
|
+
const filtered = rows.filter((row) => row.tenantId === tenantId)
|
|
126
|
+
return {
|
|
127
|
+
items: filtered.map((row) => ({
|
|
128
|
+
id: row.id,
|
|
129
|
+
slug: row.slug,
|
|
130
|
+
label: row.label,
|
|
131
|
+
color: row.color ?? null,
|
|
132
|
+
description: row.description ?? null,
|
|
133
|
+
organizationId: row.organizationId ?? null,
|
|
134
|
+
tenantId: row.tenantId ?? null,
|
|
135
|
+
})),
|
|
136
|
+
total,
|
|
137
|
+
limit,
|
|
138
|
+
offset,
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const addressesTagsAiTools: CustomersAiToolDefinition[] = [listAddressesTool, listTagsTool]
|
|
144
|
+
|
|
145
|
+
export default addressesTagsAiTools
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).
|
|
3
|
+
*
|
|
4
|
+
* Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
|
|
5
|
+
* `customers.list_companies` is now an API-backed wrapper over
|
|
6
|
+
* `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and
|
|
7
|
+
* output shape are unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Phase 3c of the same spec migrates `customers.get_company` to a single
|
|
10
|
+
* in-process call to `GET /api/customers/companies/<id>?include=...` over the
|
|
11
|
+
* documented aggregate detail route. Tool name, schema, requiredFeatures, and
|
|
12
|
+
* output shape are unchanged.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
|
|
16
|
+
import {
|
|
17
|
+
createAiApiOperationRunner,
|
|
18
|
+
type AiApiOperationRequest,
|
|
19
|
+
type AiToolExecutionContext,
|
|
20
|
+
} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
|
|
21
|
+
import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
|
|
22
|
+
|
|
23
|
+
const listCompaniesInput = z
|
|
24
|
+
.object({
|
|
25
|
+
q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),
|
|
26
|
+
limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),
|
|
27
|
+
offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),
|
|
28
|
+
tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),
|
|
29
|
+
})
|
|
30
|
+
.passthrough()
|
|
31
|
+
|
|
32
|
+
type ListCompaniesInput = z.infer<typeof listCompaniesInput>
|
|
33
|
+
|
|
34
|
+
type ListCompaniesApiItem = {
|
|
35
|
+
id?: string
|
|
36
|
+
display_name?: string | null
|
|
37
|
+
displayName?: string | null
|
|
38
|
+
primary_email?: string | null
|
|
39
|
+
primaryEmail?: string | null
|
|
40
|
+
primary_phone?: string | null
|
|
41
|
+
primaryPhone?: string | null
|
|
42
|
+
status?: string | null
|
|
43
|
+
lifecycle_stage?: string | null
|
|
44
|
+
lifecycleStage?: string | null
|
|
45
|
+
source?: string | null
|
|
46
|
+
owner_user_id?: string | null
|
|
47
|
+
ownerUserId?: string | null
|
|
48
|
+
organization_id?: string | null
|
|
49
|
+
organizationId?: string | null
|
|
50
|
+
tenant_id?: string | null
|
|
51
|
+
tenantId?: string | null
|
|
52
|
+
domain?: string | null
|
|
53
|
+
website_url?: string | null
|
|
54
|
+
websiteUrl?: string | null
|
|
55
|
+
industry?: string | null
|
|
56
|
+
size_bucket?: string | null
|
|
57
|
+
sizeBucket?: string | null
|
|
58
|
+
created_at?: string | null
|
|
59
|
+
createdAt?: string | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ListCompaniesApiResponse = {
|
|
63
|
+
items?: ListCompaniesApiItem[]
|
|
64
|
+
total?: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type ListCompaniesOutput = {
|
|
68
|
+
items: Array<Record<string, unknown>>
|
|
69
|
+
total: number
|
|
70
|
+
limit: number
|
|
71
|
+
offset: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const listCompaniesTool = defineApiBackedAiTool<
|
|
75
|
+
ListCompaniesInput,
|
|
76
|
+
ListCompaniesApiResponse,
|
|
77
|
+
ListCompaniesOutput
|
|
78
|
+
>({
|
|
79
|
+
name: 'customers.list_companies',
|
|
80
|
+
displayName: 'List companies',
|
|
81
|
+
description:
|
|
82
|
+
'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',
|
|
83
|
+
inputSchema: listCompaniesInput,
|
|
84
|
+
requiredFeatures: ['customers.companies.view'],
|
|
85
|
+
toOperation: (input, ctx) => {
|
|
86
|
+
assertTenantScope(ctx as unknown as CustomersToolContext)
|
|
87
|
+
const limit = input.limit ?? 50
|
|
88
|
+
const offset = input.offset ?? 0
|
|
89
|
+
const page = Math.floor(offset / limit) + 1
|
|
90
|
+
|
|
91
|
+
const query: Record<string, string | number | boolean | null | undefined> = {
|
|
92
|
+
page,
|
|
93
|
+
pageSize: limit,
|
|
94
|
+
}
|
|
95
|
+
if (input.q?.trim()) query.search = input.q.trim()
|
|
96
|
+
if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')
|
|
97
|
+
|
|
98
|
+
const operation: AiApiOperationRequest = {
|
|
99
|
+
method: 'GET',
|
|
100
|
+
path: '/customers/companies',
|
|
101
|
+
query,
|
|
102
|
+
}
|
|
103
|
+
return operation
|
|
104
|
+
},
|
|
105
|
+
mapResponse: (response, input) => {
|
|
106
|
+
const limit = input.limit ?? 50
|
|
107
|
+
const offset = input.offset ?? 0
|
|
108
|
+
const data = (response.data ?? {}) as ListCompaniesApiResponse
|
|
109
|
+
const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []
|
|
110
|
+
return {
|
|
111
|
+
items: rawItems.map((row) => {
|
|
112
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null
|
|
113
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
|
|
114
|
+
return {
|
|
115
|
+
id: row.id,
|
|
116
|
+
displayName: row.display_name ?? row.displayName ?? null,
|
|
117
|
+
primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
|
|
118
|
+
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
119
|
+
status: row.status ?? null,
|
|
120
|
+
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
121
|
+
source: row.source ?? null,
|
|
122
|
+
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
123
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
124
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
125
|
+
domain: row.domain ?? null,
|
|
126
|
+
websiteUrl: row.website_url ?? row.websiteUrl ?? null,
|
|
127
|
+
industry: row.industry ?? null,
|
|
128
|
+
sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,
|
|
129
|
+
createdAt,
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
total: typeof data.total === 'number' ? data.total : 0,
|
|
133
|
+
limit,
|
|
134
|
+
offset,
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
}) as unknown as CustomersAiToolDefinition
|
|
138
|
+
|
|
139
|
+
const getCompanyInput = z.object({
|
|
140
|
+
companyId: z.string().uuid().describe('Company entity id (UUID).'),
|
|
141
|
+
includeRelated: z
|
|
142
|
+
.boolean()
|
|
143
|
+
.optional()
|
|
144
|
+
.describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
type GetCompanyInput = z.infer<typeof getCompanyInput>
|
|
148
|
+
|
|
149
|
+
function toIsoCompany(value: unknown): string | null {
|
|
150
|
+
if (!value) return null
|
|
151
|
+
const dt = value instanceof Date ? value : new Date(String(value))
|
|
152
|
+
if (Number.isNaN(dt.getTime())) return null
|
|
153
|
+
return dt.toISOString()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const getCompanyTool: CustomersAiToolDefinition = {
|
|
157
|
+
name: 'customers.get_company',
|
|
158
|
+
displayName: 'Get company',
|
|
159
|
+
description:
|
|
160
|
+
'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',
|
|
161
|
+
inputSchema: getCompanyInput,
|
|
162
|
+
requiredFeatures: ['customers.companies.view'],
|
|
163
|
+
tags: ['read', 'customers'],
|
|
164
|
+
handler: async (rawInput, ctx) => {
|
|
165
|
+
const { tenantId: _tenantId } = assertTenantScope(ctx)
|
|
166
|
+
void _tenantId
|
|
167
|
+
const input: GetCompanyInput = getCompanyInput.parse(rawInput)
|
|
168
|
+
const includeRelated = !!input.includeRelated
|
|
169
|
+
|
|
170
|
+
const operation: AiApiOperationRequest = {
|
|
171
|
+
method: 'GET',
|
|
172
|
+
path: `/customers/companies/${input.companyId}`,
|
|
173
|
+
}
|
|
174
|
+
if (includeRelated) {
|
|
175
|
+
operation.query = {
|
|
176
|
+
include: 'addresses,comments,activities,interactions,deals,todos,people',
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)
|
|
181
|
+
const response = await runner.run<Record<string, unknown>>(operation)
|
|
182
|
+
if (!response.success) {
|
|
183
|
+
if (response.statusCode === 404 || response.statusCode === 403) {
|
|
184
|
+
return { found: false as const, companyId: input.companyId }
|
|
185
|
+
}
|
|
186
|
+
throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)
|
|
187
|
+
}
|
|
188
|
+
const data = (response.data ?? {}) as Record<string, unknown>
|
|
189
|
+
const companyRow = (data.company ?? null) as Record<string, unknown> | null
|
|
190
|
+
if (!companyRow) {
|
|
191
|
+
return { found: false as const, companyId: input.companyId }
|
|
192
|
+
}
|
|
193
|
+
const profileRow = (data.profile ?? null) as Record<string, unknown> | null
|
|
194
|
+
const customFields = (data.customFields ?? {}) as Record<string, unknown>
|
|
195
|
+
|
|
196
|
+
let related: Record<string, unknown> | null = null
|
|
197
|
+
if (includeRelated) {
|
|
198
|
+
const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []
|
|
199
|
+
const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []
|
|
200
|
+
const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []
|
|
201
|
+
const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []
|
|
202
|
+
const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []
|
|
203
|
+
const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []
|
|
204
|
+
const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []
|
|
205
|
+
const peopleRows = Array.isArray(data.people) ? (data.people as Array<Record<string, unknown>>) : []
|
|
206
|
+
related = {
|
|
207
|
+
addresses: addresses.map((address) => ({
|
|
208
|
+
id: address.id,
|
|
209
|
+
name: address.name ?? null,
|
|
210
|
+
purpose: address.purpose ?? null,
|
|
211
|
+
addressLine1: address.addressLine1 ?? null,
|
|
212
|
+
addressLine2: address.addressLine2 ?? null,
|
|
213
|
+
city: address.city ?? null,
|
|
214
|
+
region: address.region ?? null,
|
|
215
|
+
postalCode: address.postalCode ?? null,
|
|
216
|
+
country: address.country ?? null,
|
|
217
|
+
isPrimary: !!address.isPrimary,
|
|
218
|
+
})),
|
|
219
|
+
activities: activities.map((activity) => ({
|
|
220
|
+
id: activity.id,
|
|
221
|
+
activityType: activity.activityType,
|
|
222
|
+
subject: activity.subject ?? null,
|
|
223
|
+
body: activity.body ?? null,
|
|
224
|
+
occurredAt: toIsoCompany(activity.occurredAt),
|
|
225
|
+
createdAt: toIsoCompany(activity.createdAt),
|
|
226
|
+
})),
|
|
227
|
+
notes: notes.map((comment) => ({
|
|
228
|
+
id: comment.id,
|
|
229
|
+
body: comment.body,
|
|
230
|
+
authorUserId: comment.authorUserId ?? null,
|
|
231
|
+
createdAt: toIsoCompany(comment.createdAt),
|
|
232
|
+
})),
|
|
233
|
+
tasks: todos.map((task) => ({
|
|
234
|
+
id: task.id,
|
|
235
|
+
todoId: task.todoId ?? task.id,
|
|
236
|
+
todoSource: task.todoSource ?? null,
|
|
237
|
+
createdAt: toIsoCompany(task.createdAt),
|
|
238
|
+
})),
|
|
239
|
+
interactions: interactions.map((interaction) => ({
|
|
240
|
+
id: interaction.id,
|
|
241
|
+
interactionType: interaction.interactionType,
|
|
242
|
+
title: interaction.title ?? null,
|
|
243
|
+
status: interaction.status,
|
|
244
|
+
scheduledAt: toIsoCompany(interaction.scheduledAt),
|
|
245
|
+
occurredAt: toIsoCompany(interaction.occurredAt),
|
|
246
|
+
})),
|
|
247
|
+
tags: tagsRows
|
|
248
|
+
.map((tag) => {
|
|
249
|
+
if (!tag || typeof tag !== 'object') return null
|
|
250
|
+
const id = typeof tag.id === 'string' ? tag.id : null
|
|
251
|
+
const label = typeof tag.label === 'string' ? tag.label : null
|
|
252
|
+
if (!id || !label) return null
|
|
253
|
+
const slug = typeof tag.slug === 'string' ? tag.slug : label
|
|
254
|
+
const color = typeof tag.color === 'string' ? tag.color : null
|
|
255
|
+
return { id, slug, label, color }
|
|
256
|
+
})
|
|
257
|
+
.filter(
|
|
258
|
+
(entry): entry is { id: string; slug: string; label: string; color: string | null } =>
|
|
259
|
+
entry !== null,
|
|
260
|
+
),
|
|
261
|
+
deals: dealsRows
|
|
262
|
+
.map((deal) => {
|
|
263
|
+
if (!deal || typeof deal !== 'object') return null
|
|
264
|
+
const id = typeof deal.id === 'string' ? deal.id : null
|
|
265
|
+
if (!id) return null
|
|
266
|
+
return {
|
|
267
|
+
id,
|
|
268
|
+
title: typeof deal.title === 'string' ? deal.title : '',
|
|
269
|
+
status: typeof deal.status === 'string' ? deal.status : null,
|
|
270
|
+
pipelineStageId:
|
|
271
|
+
typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
|
|
272
|
+
valueAmount:
|
|
273
|
+
typeof deal.valueAmount === 'string'
|
|
274
|
+
? deal.valueAmount
|
|
275
|
+
: deal.valueAmount === null || deal.valueAmount === undefined
|
|
276
|
+
? null
|
|
277
|
+
: String(deal.valueAmount),
|
|
278
|
+
valueCurrency:
|
|
279
|
+
typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
.filter(
|
|
283
|
+
(
|
|
284
|
+
value,
|
|
285
|
+
): value is {
|
|
286
|
+
id: string
|
|
287
|
+
title: string
|
|
288
|
+
status: string | null
|
|
289
|
+
pipelineStageId: string | null
|
|
290
|
+
valueAmount: string | null
|
|
291
|
+
valueCurrency: string | null
|
|
292
|
+
} => value !== null,
|
|
293
|
+
),
|
|
294
|
+
people: peopleRows
|
|
295
|
+
.map((person) => {
|
|
296
|
+
if (!person || typeof person !== 'object') return null
|
|
297
|
+
const id = typeof person.id === 'string' ? person.id : null
|
|
298
|
+
const displayName = typeof person.displayName === 'string' ? person.displayName : null
|
|
299
|
+
if (!id || !displayName) return null
|
|
300
|
+
return {
|
|
301
|
+
id,
|
|
302
|
+
displayName,
|
|
303
|
+
primaryEmail:
|
|
304
|
+
typeof person.primaryEmail === 'string' ? person.primaryEmail : null,
|
|
305
|
+
primaryPhone:
|
|
306
|
+
typeof person.primaryPhone === 'string' ? person.primaryPhone : null,
|
|
307
|
+
jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,
|
|
308
|
+
department: typeof person.department === 'string' ? person.department : null,
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
.filter(
|
|
312
|
+
(
|
|
313
|
+
value,
|
|
314
|
+
): value is {
|
|
315
|
+
id: string
|
|
316
|
+
displayName: string
|
|
317
|
+
primaryEmail: string | null
|
|
318
|
+
primaryPhone: string | null
|
|
319
|
+
jobTitle: string | null
|
|
320
|
+
department: string | null
|
|
321
|
+
} => value !== null,
|
|
322
|
+
),
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
found: true as const,
|
|
327
|
+
company: {
|
|
328
|
+
id: companyRow.id,
|
|
329
|
+
displayName: companyRow.displayName ?? null,
|
|
330
|
+
description: companyRow.description ?? null,
|
|
331
|
+
primaryEmail: companyRow.primaryEmail ?? null,
|
|
332
|
+
primaryPhone: companyRow.primaryPhone ?? null,
|
|
333
|
+
status: companyRow.status ?? null,
|
|
334
|
+
lifecycleStage: companyRow.lifecycleStage ?? null,
|
|
335
|
+
source: companyRow.source ?? null,
|
|
336
|
+
ownerUserId: companyRow.ownerUserId ?? null,
|
|
337
|
+
organizationId: companyRow.organizationId ?? null,
|
|
338
|
+
tenantId: companyRow.tenantId ?? null,
|
|
339
|
+
createdAt: toIsoCompany(companyRow.createdAt),
|
|
340
|
+
updatedAt: toIsoCompany(companyRow.updatedAt),
|
|
341
|
+
},
|
|
342
|
+
profile: profileRow
|
|
343
|
+
? {
|
|
344
|
+
id: profileRow.id,
|
|
345
|
+
legalName: profileRow.legalName ?? null,
|
|
346
|
+
brandName: profileRow.brandName ?? null,
|
|
347
|
+
domain: profileRow.domain ?? null,
|
|
348
|
+
websiteUrl: profileRow.websiteUrl ?? null,
|
|
349
|
+
industry: profileRow.industry ?? null,
|
|
350
|
+
sizeBucket: profileRow.sizeBucket ?? null,
|
|
351
|
+
annualRevenue: profileRow.annualRevenue ?? null,
|
|
352
|
+
}
|
|
353
|
+
: null,
|
|
354
|
+
customFields,
|
|
355
|
+
related,
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]
|
|
361
|
+
|
|
362
|
+
export default companiesAiTools
|