@open-mercato/core 0.4.8-develop-15259be22b → 0.4.8-develop-280c02b529
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/dist/generated/entities/inbox_proposal/index.js +2 -0
- package/dist/generated/entities/inbox_proposal/index.js.map +2 -2
- package/dist/modules/catalog/inbox-actions.js +49 -0
- package/dist/modules/catalog/inbox-actions.js.map +2 -2
- package/dist/modules/customers/inbox-actions.js +69 -27
- package/dist/modules/customers/inbox-actions.js.map +3 -3
- package/dist/modules/inbox_ops/ai-tools.js +346 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +7 -0
- package/dist/modules/inbox_ops/api/extract/route.js +3 -2
- package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +59 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +34 -14
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js +49 -4
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +13 -0
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +33 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +28 -3
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +103 -5
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js +24 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js.map +7 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +29 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +7 -0
- package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js +59 -0
- package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js.map +7 -0
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +3 -1
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/inbox_ops/data/entities.js +4 -0
- package/dist/modules/inbox_ops/data/entities.js.map +2 -2
- package/dist/modules/inbox_ops/data/validators.js +30 -5
- package/dist/modules/inbox_ops/data/validators.js.map +2 -2
- package/dist/modules/inbox_ops/lib/cache.js +53 -0
- package/dist/modules/inbox_ops/lib/cache.js.map +7 -0
- package/dist/modules/inbox_ops/lib/contactValidation.js +38 -3
- package/dist/modules/inbox_ops/lib/contactValidation.js.map +2 -2
- package/dist/modules/inbox_ops/lib/executionHelpers.js +28 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/extractionPrompt.js +2 -1
- package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +2 -2
- package/dist/modules/inbox_ops/lib/messageObjectPreviews.js +52 -0
- package/dist/modules/inbox_ops/lib/messageObjectPreviews.js.map +7 -0
- package/dist/modules/inbox_ops/lib/messagesIntegration.js +155 -0
- package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +7 -0
- package/dist/modules/inbox_ops/message-objects.js +36 -0
- package/dist/modules/inbox_ops/message-objects.js.map +7 -0
- package/dist/modules/inbox_ops/message-types.js +38 -0
- package/dist/modules/inbox_ops/message-types.js.map +7 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173020.js +13 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173020.js.map +7 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173215.js +15 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173215.js.map +7 -0
- package/dist/modules/inbox_ops/search.js +5 -3
- package/dist/modules/inbox_ops/search.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +65 -3
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/generated/entities/inbox_proposal/index.ts +1 -0
- package/package.json +3 -3
- package/src/modules/catalog/inbox-actions.ts +55 -0
- package/src/modules/customers/inbox-actions.ts +86 -27
- package/src/modules/inbox_ops/ai-tools.ts +451 -0
- package/src/modules/inbox_ops/api/extract/route.ts +3 -2
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +61 -0
- package/src/modules/inbox_ops/api/proposals/[id]/reject/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +36 -16
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +60 -5
- package/src/modules/inbox_ops/api/proposals/route.ts +14 -1
- package/src/modules/inbox_ops/api/settings/route.ts +36 -2
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +31 -3
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +103 -1
- package/src/modules/inbox_ops/components/messages/InboxEmailContent.tsx +45 -0
- package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +40 -0
- package/src/modules/inbox_ops/components/proposals/CategoryBadge.tsx +59 -0
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +3 -1
- package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
- package/src/modules/inbox_ops/data/entities.ts +14 -1
- package/src/modules/inbox_ops/data/validators.ts +41 -5
- package/src/modules/inbox_ops/lib/cache.ts +60 -0
- package/src/modules/inbox_ops/lib/contactValidation.ts +31 -2
- package/src/modules/inbox_ops/lib/executionHelpers.ts +40 -0
- package/src/modules/inbox_ops/lib/extractionPrompt.ts +2 -1
- package/src/modules/inbox_ops/lib/messageObjectPreviews.ts +61 -0
- package/src/modules/inbox_ops/lib/messagesIntegration.ts +231 -0
- package/src/modules/inbox_ops/message-objects.ts +34 -0
- package/src/modules/inbox_ops/message-types.ts +36 -0
- package/src/modules/inbox_ops/migrations/Migration20260303173020.ts +13 -0
- package/src/modules/inbox_ops/migrations/Migration20260303173215.ts +15 -0
- package/src/modules/inbox_ops/search.ts +5 -3
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +75 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { AwilixContainer } from 'awilix'
|
|
4
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
5
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
|
+
import {
|
|
7
|
+
resolveOpenCodeModel,
|
|
8
|
+
resolveOpenCodeProviderApiKey,
|
|
9
|
+
} from '@open-mercato/shared/lib/ai/opencode-provider'
|
|
10
|
+
import { InboxProposal, InboxProposalAction, InboxDiscrepancy } from './data/entities'
|
|
11
|
+
import { inboxProposalCategoryEnum } from './data/validators'
|
|
12
|
+
import { executeAction } from './lib/executionEngine'
|
|
13
|
+
import { resolveExtractionProviderId, createStructuredModel, withTimeout } from './lib/llmProvider'
|
|
14
|
+
import { resolveOptionalEventBus } from './lib/eventBus'
|
|
15
|
+
|
|
16
|
+
type ToolContext = {
|
|
17
|
+
tenantId: string | null
|
|
18
|
+
organizationId: string | null
|
|
19
|
+
userId: string | null
|
|
20
|
+
container: AwilixContainer
|
|
21
|
+
userFeatures: string[]
|
|
22
|
+
isSuperAdmin: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AiToolDefinition {
|
|
26
|
+
name: string
|
|
27
|
+
description: string
|
|
28
|
+
inputSchema: z.ZodType
|
|
29
|
+
requiredFeatures?: string[]
|
|
30
|
+
handler: (input: never, ctx: ToolContext) => Promise<unknown>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Helpers
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
function requireTenantContext(ctx: ToolContext): { tenantId: string; organizationId: string } {
|
|
38
|
+
if (!ctx.tenantId || !ctx.organizationId) {
|
|
39
|
+
throw new Error('Tenant context is required')
|
|
40
|
+
}
|
|
41
|
+
return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveCrossModuleEntities(container: ToolContext['container']) {
|
|
45
|
+
const entities: Record<string, unknown> = {}
|
|
46
|
+
const keys = [
|
|
47
|
+
'CustomerEntity',
|
|
48
|
+
'SalesOrder',
|
|
49
|
+
'SalesShipment',
|
|
50
|
+
'SalesChannel',
|
|
51
|
+
'Dictionary',
|
|
52
|
+
'DictionaryEntry',
|
|
53
|
+
]
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
try {
|
|
56
|
+
entities[key] = container.resolve(key)
|
|
57
|
+
} catch {
|
|
58
|
+
/* module not available */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return entities
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// inbox_ops_list_proposals — Query proposals by status, category, date range
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
const listProposalsTool = {
|
|
69
|
+
name: 'inbox_ops_list_proposals',
|
|
70
|
+
description: `List inbox proposals with optional filters by status, category, and date range.
|
|
71
|
+
|
|
72
|
+
Returns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,
|
|
73
|
+
inputSchema: z.object({
|
|
74
|
+
status: z
|
|
75
|
+
.enum(['pending', 'partial', 'accepted', 'rejected'])
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('Filter by proposal status'),
|
|
78
|
+
category: inboxProposalCategoryEnum
|
|
79
|
+
.optional()
|
|
80
|
+
.describe('Filter by email category'),
|
|
81
|
+
limit: z
|
|
82
|
+
.number()
|
|
83
|
+
.int()
|
|
84
|
+
.min(1)
|
|
85
|
+
.max(50)
|
|
86
|
+
.optional()
|
|
87
|
+
.default(10)
|
|
88
|
+
.describe('Maximum number of proposals to return (default: 10)'),
|
|
89
|
+
dateFrom: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Filter proposals created on or after this date (ISO 8601)'),
|
|
93
|
+
dateTo: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Filter proposals created on or before this date (ISO 8601)'),
|
|
97
|
+
}),
|
|
98
|
+
requiredFeatures: ['inbox_ops.proposals.view'],
|
|
99
|
+
handler: async (input: { status?: string; category?: string; limit?: number; dateFrom?: string; dateTo?: string }, ctx: ToolContext) => {
|
|
100
|
+
const scope = requireTenantContext(ctx)
|
|
101
|
+
const em = ctx.container.resolve<EntityManager>('em').fork()
|
|
102
|
+
|
|
103
|
+
const where: Record<string, unknown> = {
|
|
104
|
+
organizationId: scope.organizationId,
|
|
105
|
+
tenantId: scope.tenantId,
|
|
106
|
+
isActive: true,
|
|
107
|
+
deletedAt: null,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (input.status) {
|
|
111
|
+
where.status = input.status
|
|
112
|
+
}
|
|
113
|
+
if (input.category) {
|
|
114
|
+
where.category = input.category
|
|
115
|
+
}
|
|
116
|
+
if (input.dateFrom || input.dateTo) {
|
|
117
|
+
const createdAt: Record<string, unknown> = {}
|
|
118
|
+
if (input.dateFrom) {
|
|
119
|
+
createdAt.$gte = new Date(input.dateFrom)
|
|
120
|
+
}
|
|
121
|
+
if (input.dateTo) {
|
|
122
|
+
createdAt.$lte = new Date(input.dateTo)
|
|
123
|
+
}
|
|
124
|
+
where.createdAt = createdAt
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const proposals = await findWithDecryption(
|
|
128
|
+
em,
|
|
129
|
+
InboxProposal,
|
|
130
|
+
where,
|
|
131
|
+
{ orderBy: { createdAt: 'DESC' }, limit: input.limit },
|
|
132
|
+
scope,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// Count actions per proposal in a single query
|
|
136
|
+
const proposalIds = proposals.map((p) => p.id)
|
|
137
|
+
const actionCountMap = new Map<string, number>()
|
|
138
|
+
|
|
139
|
+
if (proposalIds.length > 0) {
|
|
140
|
+
const actions = await findWithDecryption(
|
|
141
|
+
em,
|
|
142
|
+
InboxProposalAction,
|
|
143
|
+
{
|
|
144
|
+
proposalId: { $in: proposalIds },
|
|
145
|
+
deletedAt: null,
|
|
146
|
+
},
|
|
147
|
+
undefined,
|
|
148
|
+
scope,
|
|
149
|
+
)
|
|
150
|
+
for (const action of actions) {
|
|
151
|
+
actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get total count for the filter
|
|
156
|
+
const total = await em.count(InboxProposal, where)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
total,
|
|
160
|
+
proposals: proposals.map((p) => ({
|
|
161
|
+
id: p.id,
|
|
162
|
+
summary: p.summary,
|
|
163
|
+
status: p.status,
|
|
164
|
+
category: p.category ?? null,
|
|
165
|
+
confidence: Number(p.confidence),
|
|
166
|
+
actionCount: actionCountMap.get(p.id) ?? 0,
|
|
167
|
+
createdAt: p.createdAt.toISOString(),
|
|
168
|
+
})),
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// inbox_ops_get_proposal — Fetch proposal detail with actions and discrepancies
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
const getProposalTool = {
|
|
178
|
+
name: 'inbox_ops_get_proposal',
|
|
179
|
+
description: `Get full details of an inbox proposal including its actions and discrepancies.
|
|
180
|
+
|
|
181
|
+
Returns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,
|
|
182
|
+
inputSchema: z.object({
|
|
183
|
+
proposalId: z.string().uuid().describe('The UUID of the proposal to retrieve'),
|
|
184
|
+
}),
|
|
185
|
+
requiredFeatures: ['inbox_ops.proposals.view'],
|
|
186
|
+
handler: async (input: { proposalId: string }, ctx: ToolContext) => {
|
|
187
|
+
const scope = requireTenantContext(ctx)
|
|
188
|
+
const em = ctx.container.resolve<EntityManager>('em').fork()
|
|
189
|
+
|
|
190
|
+
const proposal = await findOneWithDecryption(
|
|
191
|
+
em,
|
|
192
|
+
InboxProposal,
|
|
193
|
+
{
|
|
194
|
+
id: input.proposalId,
|
|
195
|
+
organizationId: scope.organizationId,
|
|
196
|
+
tenantId: scope.tenantId,
|
|
197
|
+
isActive: true,
|
|
198
|
+
deletedAt: null,
|
|
199
|
+
},
|
|
200
|
+
undefined,
|
|
201
|
+
scope,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if (!proposal) {
|
|
205
|
+
return { error: 'Proposal not found' }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const actions = await findWithDecryption(
|
|
209
|
+
em,
|
|
210
|
+
InboxProposalAction,
|
|
211
|
+
{
|
|
212
|
+
proposalId: proposal.id,
|
|
213
|
+
organizationId: scope.organizationId,
|
|
214
|
+
tenantId: scope.tenantId,
|
|
215
|
+
deletedAt: null,
|
|
216
|
+
},
|
|
217
|
+
{ orderBy: { sortOrder: 'ASC' } },
|
|
218
|
+
scope,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const discrepancies = await findWithDecryption(
|
|
222
|
+
em,
|
|
223
|
+
InboxDiscrepancy,
|
|
224
|
+
{
|
|
225
|
+
proposalId: proposal.id,
|
|
226
|
+
organizationId: scope.organizationId,
|
|
227
|
+
tenantId: scope.tenantId,
|
|
228
|
+
},
|
|
229
|
+
undefined,
|
|
230
|
+
scope,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
proposal: {
|
|
235
|
+
id: proposal.id,
|
|
236
|
+
summary: proposal.summary,
|
|
237
|
+
status: proposal.status,
|
|
238
|
+
category: proposal.category ?? null,
|
|
239
|
+
confidence: Number(proposal.confidence),
|
|
240
|
+
actions: actions.map((a) => ({
|
|
241
|
+
id: a.id,
|
|
242
|
+
actionType: a.actionType,
|
|
243
|
+
description: a.description,
|
|
244
|
+
status: a.status,
|
|
245
|
+
confidence: Number(a.confidence),
|
|
246
|
+
requiredFeature: a.requiredFeature ?? null,
|
|
247
|
+
sortOrder: a.sortOrder,
|
|
248
|
+
createdEntityId: a.createdEntityId ?? null,
|
|
249
|
+
createdEntityType: a.createdEntityType ?? null,
|
|
250
|
+
})),
|
|
251
|
+
discrepancies: discrepancies.map((d) => ({
|
|
252
|
+
id: d.id,
|
|
253
|
+
type: d.type,
|
|
254
|
+
severity: d.severity,
|
|
255
|
+
description: d.description,
|
|
256
|
+
expectedValue: d.expectedValue ?? null,
|
|
257
|
+
foundValue: d.foundValue ?? null,
|
|
258
|
+
resolved: d.resolved,
|
|
259
|
+
})),
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// inbox_ops_accept_action — Accept and execute a specific action
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
const acceptActionTool = {
|
|
270
|
+
name: 'inbox_ops_accept_action',
|
|
271
|
+
description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).
|
|
272
|
+
|
|
273
|
+
Returns on success: { ok: true, createdEntityId, createdEntityType }
|
|
274
|
+
Returns on error: error message with appropriate detail.`,
|
|
275
|
+
inputSchema: z.object({
|
|
276
|
+
proposalId: z.string().uuid().describe('The UUID of the proposal'),
|
|
277
|
+
actionId: z.string().uuid().describe('The UUID of the action to accept'),
|
|
278
|
+
}),
|
|
279
|
+
requiredFeatures: ['inbox_ops.proposals.manage'],
|
|
280
|
+
handler: async (input: { proposalId: string; actionId: string }, ctx: ToolContext) => {
|
|
281
|
+
const scope = requireTenantContext(ctx)
|
|
282
|
+
if (!ctx.userId) {
|
|
283
|
+
throw new Error('User context is required')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const em = ctx.container.resolve<EntityManager>('em').fork()
|
|
287
|
+
|
|
288
|
+
const action = await findOneWithDecryption(
|
|
289
|
+
em,
|
|
290
|
+
InboxProposalAction,
|
|
291
|
+
{
|
|
292
|
+
id: input.actionId,
|
|
293
|
+
proposalId: input.proposalId,
|
|
294
|
+
organizationId: scope.organizationId,
|
|
295
|
+
tenantId: scope.tenantId,
|
|
296
|
+
deletedAt: null,
|
|
297
|
+
},
|
|
298
|
+
undefined,
|
|
299
|
+
scope,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if (!action) {
|
|
303
|
+
return { error: 'Action not found' }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check if action was already processed
|
|
307
|
+
if (action.status !== 'pending' && action.status !== 'failed') {
|
|
308
|
+
return { error: 'Action already processed', status: action.status }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check target module permission
|
|
312
|
+
if (action.requiredFeature) {
|
|
313
|
+
const hasFeature =
|
|
314
|
+
ctx.isSuperAdmin || ctx.userFeatures.includes(action.requiredFeature)
|
|
315
|
+
if (!hasFeature) {
|
|
316
|
+
return {
|
|
317
|
+
error: 'Insufficient permissions',
|
|
318
|
+
requiredFeature: action.requiredFeature,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const entities = resolveCrossModuleEntities(ctx.container)
|
|
324
|
+
const eventBus = resolveOptionalEventBus(ctx.container)
|
|
325
|
+
|
|
326
|
+
const result = await executeAction(action, {
|
|
327
|
+
em,
|
|
328
|
+
userId: ctx.userId,
|
|
329
|
+
tenantId: scope.tenantId,
|
|
330
|
+
organizationId: scope.organizationId,
|
|
331
|
+
eventBus,
|
|
332
|
+
container: ctx.container,
|
|
333
|
+
entities: entities as unknown as import('./lib/executionEngine').CrossModuleEntities,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
if (!result.success) {
|
|
337
|
+
if (result.statusCode === 409) {
|
|
338
|
+
return { error: 'Action already processed', status: 'accepted' }
|
|
339
|
+
}
|
|
340
|
+
if (result.statusCode === 403) {
|
|
341
|
+
return {
|
|
342
|
+
error: 'Insufficient permissions',
|
|
343
|
+
requiredFeature: action.requiredFeature ?? 'unknown',
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { error: 'Execution failed', detail: result.error ?? 'Unknown error' }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const { resolveCache, invalidateCountsCache } = await import('./lib/cache')
|
|
351
|
+
const cache = resolveCache(ctx.container)
|
|
352
|
+
if (cache && scope.tenantId) {
|
|
353
|
+
await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId))
|
|
354
|
+
}
|
|
355
|
+
} catch { /* cache invalidation is non-critical */ }
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
ok: true,
|
|
359
|
+
createdEntityId: result.createdEntityId ?? null,
|
|
360
|
+
createdEntityType: result.createdEntityType ?? null,
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// =============================================================================
|
|
366
|
+
// inbox_ops_categorize_email — Standalone LLM-based text categorization
|
|
367
|
+
// =============================================================================
|
|
368
|
+
|
|
369
|
+
const categorizeEmailSchema = z.object({
|
|
370
|
+
category: inboxProposalCategoryEnum,
|
|
371
|
+
confidence: z.number(),
|
|
372
|
+
reasoning: z.string(),
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const categorizeEmailTool = {
|
|
376
|
+
name: 'inbox_ops_categorize_email',
|
|
377
|
+
description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.
|
|
378
|
+
|
|
379
|
+
Returns: { category, confidence (0-1), reasoning }
|
|
380
|
+
Input text is limited to 10,000 characters for cost control.`,
|
|
381
|
+
inputSchema: z.object({
|
|
382
|
+
text: z
|
|
383
|
+
.string()
|
|
384
|
+
.min(1)
|
|
385
|
+
.max(10000)
|
|
386
|
+
.describe('Email or text content to categorize (max 10K chars)'),
|
|
387
|
+
}),
|
|
388
|
+
requiredFeatures: ['inbox_ops.proposals.view'],
|
|
389
|
+
handler: async (input: { text: string }, ctx: ToolContext) => {
|
|
390
|
+
requireTenantContext(ctx)
|
|
391
|
+
|
|
392
|
+
const providerId = resolveExtractionProviderId()
|
|
393
|
+
const apiKey = resolveOpenCodeProviderApiKey(providerId)
|
|
394
|
+
if (!apiKey) {
|
|
395
|
+
throw new Error(`Missing API key for provider "${providerId}"`)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const modelConfig = resolveOpenCodeModel(providerId, {})
|
|
399
|
+
const model = await createStructuredModel(providerId, apiKey, modelConfig.modelId)
|
|
400
|
+
|
|
401
|
+
const { generateObject } = await import('ai')
|
|
402
|
+
|
|
403
|
+
const result = await withTimeout(
|
|
404
|
+
generateObject({
|
|
405
|
+
model,
|
|
406
|
+
schema: categorizeEmailSchema,
|
|
407
|
+
system: `You are an email classification agent. Classify the given text into exactly one category:
|
|
408
|
+
- rfq: Request for quotation or pricing inquiry
|
|
409
|
+
- order: New purchase order or order placement
|
|
410
|
+
- order_update: Change or update to an existing order
|
|
411
|
+
- complaint: Customer complaint, dispute, or dissatisfaction
|
|
412
|
+
- shipping_update: Shipment status, tracking, or delivery information
|
|
413
|
+
- inquiry: General question or information request
|
|
414
|
+
- payment: Payment-related (invoice, receipt, payment terms)
|
|
415
|
+
- other: Does not fit any category above
|
|
416
|
+
|
|
417
|
+
Return a JSON object with:
|
|
418
|
+
- category: one of the categories above
|
|
419
|
+
- confidence: a number between 0 and 1 indicating how confident you are
|
|
420
|
+
- reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,
|
|
421
|
+
prompt: input.text,
|
|
422
|
+
temperature: 0,
|
|
423
|
+
}),
|
|
424
|
+
15000,
|
|
425
|
+
'Email categorization timed out after 15s',
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
category: result.object.category,
|
|
430
|
+
confidence: Math.round(result.object.confidence * 100) / 100,
|
|
431
|
+
reasoning: result.object.reasoning,
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// =============================================================================
|
|
437
|
+
// Export
|
|
438
|
+
// =============================================================================
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* All AI tools exported by the inbox_ops module.
|
|
442
|
+
* Discovered by ai-assistant module's generator.
|
|
443
|
+
*/
|
|
444
|
+
export const aiTools: AiToolDefinition[] = [
|
|
445
|
+
listProposalsTool,
|
|
446
|
+
getProposalTool,
|
|
447
|
+
acceptActionTool,
|
|
448
|
+
categorizeEmailTool,
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
export default aiTools
|
|
@@ -37,8 +37,9 @@ export async function POST(req: Request) {
|
|
|
37
37
|
const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)
|
|
38
38
|
const truncatedText = text.slice(0, maxTextSize)
|
|
39
39
|
|
|
40
|
+
const submitterEmail = ctx.auth?.email || ctx.userId
|
|
40
41
|
const email = ctx.em.create(InboxEmail, {
|
|
41
|
-
forwardedByAddress:
|
|
42
|
+
forwardedByAddress: submitterEmail,
|
|
42
43
|
forwardedByName: null,
|
|
43
44
|
toAddress: 'text-extract',
|
|
44
45
|
subject: title || 'Text extraction',
|
|
@@ -64,7 +65,7 @@ export async function POST(req: Request) {
|
|
|
64
65
|
emailId: email.id,
|
|
65
66
|
tenantId: ctx.tenantId,
|
|
66
67
|
organizationId: ctx.organizationId,
|
|
67
|
-
forwardedByAddress:
|
|
68
|
+
forwardedByAddress: submitterEmail,
|
|
68
69
|
subject: title || 'Text extraction',
|
|
69
70
|
})
|
|
70
71
|
} catch (eventError) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
4
|
import { acceptAllActions } from '../../../../lib/executionEngine'
|
|
5
|
+
import { resolveCache, invalidateCountsCache } from '../../../../lib/cache'
|
|
4
6
|
import {
|
|
5
7
|
resolveRequestContext,
|
|
6
8
|
resolveProposal,
|
|
@@ -29,6 +31,9 @@ export async function POST(req: Request) {
|
|
|
29
31
|
const succeeded = results.filter((r) => r.success).length
|
|
30
32
|
const failed = results.filter((r) => !r.success).length
|
|
31
33
|
|
|
34
|
+
const cache = resolveCache(ctx.container)
|
|
35
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
36
|
+
|
|
32
37
|
return NextResponse.json({ ok: !stoppedOnFailure, succeeded, failed, stoppedOnFailure, results })
|
|
33
38
|
} catch (err) {
|
|
34
39
|
return handleRouteError(err, 'accept all actions')
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
4
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
5
|
import { InboxProposal, InboxProposalAction } from '../../../../../../data/entities'
|
|
5
6
|
import { executeAction } from '../../../../../../lib/executionEngine'
|
|
7
|
+
import { resolveCache, invalidateCountsCache } from '../../../../../../lib/cache'
|
|
6
8
|
import {
|
|
7
9
|
resolveRequestContext,
|
|
8
10
|
resolveActionAndProposal,
|
|
@@ -48,6 +50,9 @@ export async function POST(req: Request) {
|
|
|
48
50
|
ctx.scope,
|
|
49
51
|
)
|
|
50
52
|
|
|
53
|
+
const cache = resolveCache(ctx.container)
|
|
54
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
55
|
+
|
|
51
56
|
return NextResponse.json({
|
|
52
57
|
ok: true,
|
|
53
58
|
action: freshAction ? {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
5
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
6
|
import { InboxProposal, InboxProposalAction } from '../../../../../../data/entities'
|
|
6
7
|
import { recalculateProposalStatus } from '../../../../../../lib/executionEngine'
|
|
7
8
|
import { formatZodErrors } from '../../../../../../lib/validation'
|
|
9
|
+
import { resolveCache, invalidateCountsCache } from '../../../../../../lib/cache'
|
|
8
10
|
import {
|
|
9
11
|
resolveRequestContext,
|
|
10
12
|
resolveActionAndProposal,
|
|
@@ -78,6 +80,9 @@ export async function PATCH(req: Request) {
|
|
|
78
80
|
ctx.scope,
|
|
79
81
|
)
|
|
80
82
|
|
|
83
|
+
const cache = resolveCache(ctx.container)
|
|
84
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
85
|
+
|
|
81
86
|
return NextResponse.json({
|
|
82
87
|
ok: true,
|
|
83
88
|
action: {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
4
|
import { rejectAction } from '../../../../../../lib/executionEngine'
|
|
5
|
+
import { resolveCache, invalidateCountsCache } from '../../../../../../lib/cache'
|
|
4
6
|
import {
|
|
5
7
|
resolveRequestContext,
|
|
6
8
|
resolveActionAndProposal,
|
|
@@ -25,6 +27,9 @@ export async function POST(req: Request) {
|
|
|
25
27
|
|
|
26
28
|
await rejectAction(resolved.action, toExecutionContext(ctx))
|
|
27
29
|
|
|
30
|
+
const cache = resolveCache(ctx.container)
|
|
31
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
32
|
+
|
|
28
33
|
return NextResponse.json({ ok: true })
|
|
29
34
|
} catch (err) {
|
|
30
35
|
return handleRouteError(err, 'reject action')
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
4
|
import { actionEditSchema, validateActionPayloadForType } from '../../../../../data/validators'
|
|
4
5
|
import { emitInboxOpsEvent } from '../../../../../events'
|
|
6
|
+
import { resolveCache, invalidateCountsCache } from '../../../../../lib/cache'
|
|
5
7
|
import {
|
|
6
8
|
resolveRequestContext,
|
|
7
9
|
resolveActionAndProposal,
|
|
@@ -40,6 +42,9 @@ export async function PATCH(req: Request) {
|
|
|
40
42
|
action.payload = mergedPayload
|
|
41
43
|
await ctx.em.flush()
|
|
42
44
|
|
|
45
|
+
const cache = resolveCache(ctx.container)
|
|
46
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
47
|
+
|
|
43
48
|
try {
|
|
44
49
|
await emitInboxOpsEvent('inbox_ops.action.edited', {
|
|
45
50
|
actionId: action.id,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
+
import { categorizeProposalSchema } from '../../../../data/validators'
|
|
5
|
+
import { resolveCache, invalidateCountsCache } from '../../../../lib/cache'
|
|
6
|
+
import {
|
|
7
|
+
resolveRequestContext,
|
|
8
|
+
resolveProposal,
|
|
9
|
+
handleRouteError,
|
|
10
|
+
isErrorResponse,
|
|
11
|
+
} from '../../../routeHelpers'
|
|
12
|
+
|
|
13
|
+
export const metadata = {
|
|
14
|
+
POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function POST(req: Request) {
|
|
18
|
+
try {
|
|
19
|
+
const ctx = await resolveRequestContext(req)
|
|
20
|
+
const proposal = await resolveProposal(new URL(req.url), ctx)
|
|
21
|
+
if (isErrorResponse(proposal)) return proposal
|
|
22
|
+
|
|
23
|
+
const body = await req.json()
|
|
24
|
+
const parsed = categorizeProposalSchema.safeParse(body)
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')
|
|
27
|
+
return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const previousCategory = proposal.category || null
|
|
31
|
+
proposal.category = parsed.data.category
|
|
32
|
+
await ctx.em.flush()
|
|
33
|
+
|
|
34
|
+
const cache = resolveCache(ctx.container)
|
|
35
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({
|
|
38
|
+
ok: true,
|
|
39
|
+
category: parsed.data.category,
|
|
40
|
+
previousCategory,
|
|
41
|
+
})
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return handleRouteError(err, 'categorize proposal')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const openApi: OpenApiRouteDoc = {
|
|
48
|
+
tag: 'InboxOps',
|
|
49
|
+
summary: 'Categorize proposal',
|
|
50
|
+
methods: {
|
|
51
|
+
POST: {
|
|
52
|
+
summary: 'Set or change the category of a proposal',
|
|
53
|
+
description: 'Assigns a category to a proposal. Returns the new and previous category for undo support.',
|
|
54
|
+
responses: [
|
|
55
|
+
{ status: 200, description: 'Category updated' },
|
|
56
|
+
{ status: 400, description: 'Invalid category value' },
|
|
57
|
+
{ status: 404, description: 'Proposal not found' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
3
4
|
import { rejectProposal } from '../../../../lib/executionEngine'
|
|
5
|
+
import { resolveCache, invalidateCountsCache } from '../../../../lib/cache'
|
|
4
6
|
import {
|
|
5
7
|
resolveRequestContext,
|
|
6
8
|
resolveProposal,
|
|
@@ -21,6 +23,9 @@ export async function POST(req: Request) {
|
|
|
21
23
|
|
|
22
24
|
await rejectProposal(proposal.id, toExecutionContext(ctx))
|
|
23
25
|
|
|
26
|
+
const cache = resolveCache(ctx.container)
|
|
27
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
28
|
+
|
|
24
29
|
return NextResponse.json({ ok: true })
|
|
25
30
|
} catch (err) {
|
|
26
31
|
return handleRouteError(err, 'reject proposal')
|