@maestrogtm/maestro-gtm 0.10.16
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/ai-RNHSWSNV.js +158 -0
- package/dist/ai-RNHSWSNV.js.map +1 -0
- package/dist/app-PSZH2J56.js +54 -0
- package/dist/app-PSZH2J56.js.map +1 -0
- package/dist/batch-ZCHN54YJ.js +28 -0
- package/dist/batch-ZCHN54YJ.js.map +1 -0
- package/dist/campaign-XDXQA7KX.js +119 -0
- package/dist/campaign-XDXQA7KX.js.map +1 -0
- package/dist/chunk-365Q36GF.js +54 -0
- package/dist/chunk-365Q36GF.js.map +1 -0
- package/dist/chunk-4IV6QS4U.js +122 -0
- package/dist/chunk-4IV6QS4U.js.map +1 -0
- package/dist/chunk-6GLLK5KO.js +64 -0
- package/dist/chunk-6GLLK5KO.js.map +1 -0
- package/dist/chunk-6UNBW5SN.js +686 -0
- package/dist/chunk-6UNBW5SN.js.map +1 -0
- package/dist/chunk-A7JD6EYV.js +92 -0
- package/dist/chunk-A7JD6EYV.js.map +1 -0
- package/dist/chunk-ARNVJPFM.js +139 -0
- package/dist/chunk-ARNVJPFM.js.map +1 -0
- package/dist/chunk-AX6BOEF2.js +345 -0
- package/dist/chunk-AX6BOEF2.js.map +1 -0
- package/dist/chunk-C3T7QPSO.js +507 -0
- package/dist/chunk-C3T7QPSO.js.map +1 -0
- package/dist/chunk-FG43GILY.js +46 -0
- package/dist/chunk-FG43GILY.js.map +1 -0
- package/dist/chunk-FS6DCNCA.js +139 -0
- package/dist/chunk-FS6DCNCA.js.map +1 -0
- package/dist/chunk-I6GRD4X7.js +1144 -0
- package/dist/chunk-I6GRD4X7.js.map +1 -0
- package/dist/chunk-IP34URKR.js +621 -0
- package/dist/chunk-IP34URKR.js.map +1 -0
- package/dist/chunk-JFSKOY7Z.js +252 -0
- package/dist/chunk-JFSKOY7Z.js.map +1 -0
- package/dist/chunk-M25KLO7T.js +3272 -0
- package/dist/chunk-M25KLO7T.js.map +1 -0
- package/dist/chunk-M3G2WREL.js +57 -0
- package/dist/chunk-M3G2WREL.js.map +1 -0
- package/dist/chunk-MFFACSBE.js +4266 -0
- package/dist/chunk-MFFACSBE.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QZH3XFOQ.js +2636 -0
- package/dist/chunk-QZH3XFOQ.js.map +1 -0
- package/dist/chunk-SPWDMOEU.js +1940 -0
- package/dist/chunk-SPWDMOEU.js.map +1 -0
- package/dist/chunk-TP3BZDVV.js +28 -0
- package/dist/chunk-TP3BZDVV.js.map +1 -0
- package/dist/chunk-UBJUBYSQ.js +18 -0
- package/dist/chunk-UBJUBYSQ.js.map +1 -0
- package/dist/chunk-VNKXGHWY.js +20 -0
- package/dist/chunk-VNKXGHWY.js.map +1 -0
- package/dist/chunk-WKLCPIFB.js +9862 -0
- package/dist/chunk-WKLCPIFB.js.map +1 -0
- package/dist/chunk-YV5XOXRQ.js +7 -0
- package/dist/chunk-YV5XOXRQ.js.map +1 -0
- package/dist/cli-Z3BNNJYQ.js +852 -0
- package/dist/cli-Z3BNNJYQ.js.map +1 -0
- package/dist/client-Y34LNEWN.js +8 -0
- package/dist/client-Y34LNEWN.js.map +1 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/configure-XSENK4X5.js +64 -0
- package/dist/configure-XSENK4X5.js.map +1 -0
- package/dist/context.js +10 -0
- package/dist/context.js.map +1 -0
- package/dist/crm-QBNHVBYV.js +86 -0
- package/dist/crm-QBNHVBYV.js.map +1 -0
- package/dist/dfy-X3OXIYRA.js +356 -0
- package/dist/dfy-X3OXIYRA.js.map +1 -0
- package/dist/dist-LGCJKGBS.js +121 -0
- package/dist/dist-LGCJKGBS.js.map +1 -0
- package/dist/engagement-C4U7LPJH.js +463 -0
- package/dist/engagement-C4U7LPJH.js.map +1 -0
- package/dist/enrich-F5GPVZFE.js +226 -0
- package/dist/enrich-F5GPVZFE.js.map +1 -0
- package/dist/extract-DS5N6SSJ.js +155 -0
- package/dist/extract-DS5N6SSJ.js.map +1 -0
- package/dist/feedback-AIXKXNM5.js +51 -0
- package/dist/feedback-AIXKXNM5.js.map +1 -0
- package/dist/fetch-QJDSPI63.js +87 -0
- package/dist/fetch-QJDSPI63.js.map +1 -0
- package/dist/handlers.js +13 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/list-HL7NQQJX.js +236 -0
- package/dist/list-HL7NQQJX.js.map +1 -0
- package/dist/maestro-N7Q2JX22.js +903 -0
- package/dist/maestro-N7Q2JX22.js.map +1 -0
- package/dist/prospect-RUOT43H6.js +532 -0
- package/dist/prospect-RUOT43H6.js.map +1 -0
- package/dist/providers/factory.js +10 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/registry.js +8 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/provision-FT5NWN77.js +394 -0
- package/dist/provision-FT5NWN77.js.map +1 -0
- package/dist/recipe-JU3SXMZF.js +137 -0
- package/dist/recipe-JU3SXMZF.js.map +1 -0
- package/dist/review-5SB6DYDZ.js +70 -0
- package/dist/review-5SB6DYDZ.js.map +1 -0
- package/dist/sdk-LVBHNQ6T.js +3852 -0
- package/dist/sdk-LVBHNQ6T.js.map +1 -0
- package/dist/server-REKYQZ2E.js +22 -0
- package/dist/server-REKYQZ2E.js.map +1 -0
- package/dist/status-V3EEFS7S.js +114 -0
- package/dist/status-V3EEFS7S.js.map +1 -0
- package/dist/tam-J6NDBP5W.js +682 -0
- package/dist/tam-J6NDBP5W.js.map +1 -0
- package/dist/tools.js +80 -0
- package/dist/tools.js.map +1 -0
- package/dist/validation.js +12 -0
- package/dist/validation.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyPersonaBoost,
|
|
3
|
+
parseCountry,
|
|
4
|
+
parseTitle,
|
|
5
|
+
resolvePersona
|
|
6
|
+
} from "./chunk-MFFACSBE.js";
|
|
7
|
+
import {
|
|
8
|
+
getSupabase,
|
|
9
|
+
resolveOwner
|
|
10
|
+
} from "./chunk-FG43GILY.js";
|
|
11
|
+
import {
|
|
12
|
+
assembleContext,
|
|
13
|
+
buildProposal,
|
|
14
|
+
changelog_service_exports,
|
|
15
|
+
clusterEntriesByTopic,
|
|
16
|
+
competitor_service_exports,
|
|
17
|
+
edge_service_exports,
|
|
18
|
+
extractOntology,
|
|
19
|
+
loadExistingEntityNames,
|
|
20
|
+
loadExistingGraphSummary,
|
|
21
|
+
outcome_service_exports,
|
|
22
|
+
persona_repo_exports,
|
|
23
|
+
persona_service_exports,
|
|
24
|
+
product_service_exports,
|
|
25
|
+
proof_point_service_exports,
|
|
26
|
+
segment_service_exports,
|
|
27
|
+
source_document_service_exports
|
|
28
|
+
} from "./chunk-SPWDMOEU.js";
|
|
29
|
+
import {
|
|
30
|
+
createGtmError
|
|
31
|
+
} from "./chunk-UBJUBYSQ.js";
|
|
32
|
+
import {
|
|
33
|
+
HarvestClient
|
|
34
|
+
} from "./chunk-WKLCPIFB.js";
|
|
35
|
+
import {
|
|
36
|
+
logError
|
|
37
|
+
} from "./chunk-6GLLK5KO.js";
|
|
38
|
+
import "./chunk-PZ5AY32C.js";
|
|
39
|
+
|
|
40
|
+
// src/handlers/maestro.ts
|
|
41
|
+
async function handleMaestro(name, args) {
|
|
42
|
+
const supabase = getSupabase();
|
|
43
|
+
const owner = resolveOwner(args);
|
|
44
|
+
const action = name === "intel" ? args.action : name.startsWith("intel_") ? name.slice(6) : name;
|
|
45
|
+
switch (action) {
|
|
46
|
+
case "context":
|
|
47
|
+
return handleGetContext(supabase, owner, args);
|
|
48
|
+
case "product":
|
|
49
|
+
return handleUpsertProduct(supabase, owner, args);
|
|
50
|
+
case "persona":
|
|
51
|
+
return handleUpsertPersona(supabase, owner, args);
|
|
52
|
+
case "get_persona":
|
|
53
|
+
return handleGetPersona(supabase, owner, args);
|
|
54
|
+
case "segment":
|
|
55
|
+
return handleUpsertSegment(supabase, owner, args);
|
|
56
|
+
case "proof":
|
|
57
|
+
return handleAddProofPoint(supabase, owner, args);
|
|
58
|
+
case "competitor":
|
|
59
|
+
return handleAddCompetitor(supabase, owner, args);
|
|
60
|
+
case "outcome":
|
|
61
|
+
return handleRecordOutcome(supabase, owner, args);
|
|
62
|
+
case "gaps":
|
|
63
|
+
return handleGetGaps(supabase, owner, args);
|
|
64
|
+
case "propose":
|
|
65
|
+
return handleProposeGraph(supabase, owner, args);
|
|
66
|
+
case "ingest":
|
|
67
|
+
return handleIngestDocument(supabase, owner, args);
|
|
68
|
+
case "ingest_linkedin":
|
|
69
|
+
return handleIngestLinkedIn(supabase, owner, args);
|
|
70
|
+
case "refresh_competitor":
|
|
71
|
+
return handleRefreshCompetitor(supabase, owner, args);
|
|
72
|
+
case "changelog":
|
|
73
|
+
return handleGetChangelog(supabase, owner, args);
|
|
74
|
+
case "score":
|
|
75
|
+
return handleScoreContact(supabase, owner, args);
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Unknown intel action: ${action}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function handleGetContext(supabase, owner, args) {
|
|
81
|
+
const strategy = args.strategy;
|
|
82
|
+
const query = args.query;
|
|
83
|
+
const persona_id = args.persona_id;
|
|
84
|
+
return assembleContext(supabase, {
|
|
85
|
+
owner_id: owner.owner_id,
|
|
86
|
+
owner_type: owner.owner_type,
|
|
87
|
+
strategy,
|
|
88
|
+
query,
|
|
89
|
+
persona_id
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function handleUpsertProduct(supabase, owner, args) {
|
|
93
|
+
const input = {
|
|
94
|
+
owner_id: owner.owner_id,
|
|
95
|
+
owner_type: owner.owner_type,
|
|
96
|
+
name: args.name,
|
|
97
|
+
description: args.description,
|
|
98
|
+
category: args.category,
|
|
99
|
+
value_proposition: args.value_proposition,
|
|
100
|
+
key_features: args.key_features,
|
|
101
|
+
pricing_summary: args.pricing_summary
|
|
102
|
+
};
|
|
103
|
+
return product_service_exports.upsertProduct(supabase, input);
|
|
104
|
+
}
|
|
105
|
+
async function handleUpsertPersona(supabase, owner, args) {
|
|
106
|
+
const input = {
|
|
107
|
+
owner_id: owner.owner_id,
|
|
108
|
+
owner_type: owner.owner_type,
|
|
109
|
+
name: args.name,
|
|
110
|
+
title_patterns: args.title_patterns,
|
|
111
|
+
industries: args.industries,
|
|
112
|
+
company_size_min: args.company_size_min,
|
|
113
|
+
company_size_max: args.company_size_max,
|
|
114
|
+
pain_points: args.pain_points,
|
|
115
|
+
goals: args.goals,
|
|
116
|
+
objections: args.objections,
|
|
117
|
+
buying_signals: args.buying_signals,
|
|
118
|
+
geographic_focus: args.geographic_focus,
|
|
119
|
+
budget_range: args.budget_range,
|
|
120
|
+
is_primary: args.is_primary
|
|
121
|
+
};
|
|
122
|
+
return persona_service_exports.upsertPersona(supabase, input);
|
|
123
|
+
}
|
|
124
|
+
async function handleGetPersona(supabase, owner, args) {
|
|
125
|
+
const personaId = args.persona_id;
|
|
126
|
+
if (personaId) {
|
|
127
|
+
return persona_service_exports.getPersona(supabase, owner.owner_id, owner.owner_type, personaId);
|
|
128
|
+
}
|
|
129
|
+
return persona_service_exports.getPrimaryPersona(supabase, owner.owner_id, owner.owner_type);
|
|
130
|
+
}
|
|
131
|
+
async function handleUpsertSegment(supabase, owner, args) {
|
|
132
|
+
const input = {
|
|
133
|
+
owner_id: owner.owner_id,
|
|
134
|
+
owner_type: owner.owner_type,
|
|
135
|
+
name: args.name,
|
|
136
|
+
description: args.description,
|
|
137
|
+
qualification_criteria: args.qualification_criteria,
|
|
138
|
+
market_size_estimate: args.market_size_estimate
|
|
139
|
+
};
|
|
140
|
+
return segment_service_exports.upsertSegment(supabase, input);
|
|
141
|
+
}
|
|
142
|
+
async function handleAddProofPoint(supabase, owner, args) {
|
|
143
|
+
const input = {
|
|
144
|
+
owner_id: owner.owner_id,
|
|
145
|
+
owner_type: owner.owner_type,
|
|
146
|
+
type: args.type,
|
|
147
|
+
title: args.title,
|
|
148
|
+
content: args.content,
|
|
149
|
+
metric_value: args.metric_value,
|
|
150
|
+
source_type: args.source_type,
|
|
151
|
+
source_id: args.source_id
|
|
152
|
+
};
|
|
153
|
+
const result = await proof_point_service_exports.addProofPoint(supabase, input);
|
|
154
|
+
if (result.success && result.data) {
|
|
155
|
+
const proofPointId = result.data.id;
|
|
156
|
+
if (args.persona_id) {
|
|
157
|
+
try {
|
|
158
|
+
await edge_service_exports.createEdge(supabase, {
|
|
159
|
+
owner_id: owner.owner_id,
|
|
160
|
+
owner_type: owner.owner_type,
|
|
161
|
+
source_type: "proof_point",
|
|
162
|
+
source_id: proofPointId,
|
|
163
|
+
target_type: "persona",
|
|
164
|
+
target_id: args.persona_id,
|
|
165
|
+
relationship: "supports"
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logError(
|
|
169
|
+
"maestro.handleAddProofPoint.edgePersona",
|
|
170
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
171
|
+
{ proofPointId, personaId: args.persona_id }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (args.product_id) {
|
|
176
|
+
try {
|
|
177
|
+
await edge_service_exports.createEdge(supabase, {
|
|
178
|
+
owner_id: owner.owner_id,
|
|
179
|
+
owner_type: owner.owner_type,
|
|
180
|
+
source_type: "proof_point",
|
|
181
|
+
source_id: proofPointId,
|
|
182
|
+
target_type: "product",
|
|
183
|
+
target_id: args.product_id,
|
|
184
|
+
relationship: "demonstrates"
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logError(
|
|
188
|
+
"maestro.handleAddProofPoint.edgeProduct",
|
|
189
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
190
|
+
{ proofPointId, productId: args.product_id }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
async function handleAddCompetitor(supabase, owner, args) {
|
|
198
|
+
const input = {
|
|
199
|
+
owner_id: owner.owner_id,
|
|
200
|
+
owner_type: owner.owner_type,
|
|
201
|
+
name: args.name,
|
|
202
|
+
website: args.website,
|
|
203
|
+
positioning: args.positioning,
|
|
204
|
+
strengths: args.strengths,
|
|
205
|
+
weaknesses: args.weaknesses,
|
|
206
|
+
differentiation_notes: args.differentiation_notes
|
|
207
|
+
};
|
|
208
|
+
return competitor_service_exports.upsertCompetitor(supabase, input);
|
|
209
|
+
}
|
|
210
|
+
async function handleRecordOutcome(supabase, owner, args) {
|
|
211
|
+
const input = {
|
|
212
|
+
owner_id: owner.owner_id,
|
|
213
|
+
owner_type: owner.owner_type,
|
|
214
|
+
outcome_type: args.outcome_type,
|
|
215
|
+
entity_type: args.entity_type,
|
|
216
|
+
entity_id: args.entity_id,
|
|
217
|
+
detail: args.detail,
|
|
218
|
+
metrics: args.metrics,
|
|
219
|
+
occurred_at: args.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
220
|
+
};
|
|
221
|
+
return outcome_service_exports.recordOutcome(supabase, input);
|
|
222
|
+
}
|
|
223
|
+
async function handleGetGaps(supabase, owner, args) {
|
|
224
|
+
const personaId = args.persona_id;
|
|
225
|
+
const context = await assembleContext(supabase, {
|
|
226
|
+
owner_id: owner.owner_id,
|
|
227
|
+
owner_type: owner.owner_type,
|
|
228
|
+
strategy: "campaign_planning",
|
|
229
|
+
persona_id: personaId
|
|
230
|
+
});
|
|
231
|
+
const gaps = [];
|
|
232
|
+
for (const persona of context.personas) {
|
|
233
|
+
const linkedEdges = context.edges.filter(
|
|
234
|
+
(e) => e.target_type === "persona" && e.target_id === persona.id
|
|
235
|
+
);
|
|
236
|
+
const linkedProofPoints = linkedEdges.filter((e) => e.source_type === "proof_point");
|
|
237
|
+
if (linkedProofPoints.length < 2) {
|
|
238
|
+
gaps.push({
|
|
239
|
+
entity_type: "persona",
|
|
240
|
+
entity_name: persona.name,
|
|
241
|
+
gap: `Only ${linkedProofPoints.length} proof points linked. Need at least 2.`,
|
|
242
|
+
severity: linkedProofPoints.length === 0 ? "high" : "medium"
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (persona.objections.length === 0) {
|
|
246
|
+
gaps.push({
|
|
247
|
+
entity_type: "persona",
|
|
248
|
+
entity_name: persona.name,
|
|
249
|
+
gap: "No objections documented. Record objections from calls and replies.",
|
|
250
|
+
severity: "medium"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const product of context.products) {
|
|
255
|
+
const linkedEdges = context.edges.filter(
|
|
256
|
+
(e) => e.target_type === "product" && e.target_id === product.id
|
|
257
|
+
);
|
|
258
|
+
const linkedProofPoints = linkedEdges.filter((e) => e.source_type === "proof_point");
|
|
259
|
+
if (linkedProofPoints.length < 3) {
|
|
260
|
+
gaps.push({
|
|
261
|
+
entity_type: "product",
|
|
262
|
+
entity_name: product.name,
|
|
263
|
+
gap: `Only ${linkedProofPoints.length} proof points. Need at least 3 for credibility.`,
|
|
264
|
+
severity: linkedProofPoints.length === 0 ? "high" : "medium"
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (context.competitors.length === 0) {
|
|
269
|
+
gaps.push({
|
|
270
|
+
entity_type: "graph",
|
|
271
|
+
entity_name: "Competitors",
|
|
272
|
+
gap: "No competitors documented. Add competitive intelligence for differentiation.",
|
|
273
|
+
severity: "medium"
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
persona_count: context.personas.length,
|
|
278
|
+
product_count: context.products.length,
|
|
279
|
+
proof_point_count: context.proof_points.length,
|
|
280
|
+
competitor_count: context.competitors.length,
|
|
281
|
+
segment_count: context.segments.length,
|
|
282
|
+
gaps,
|
|
283
|
+
gap_count: gaps.length,
|
|
284
|
+
high_severity_count: gaps.filter((g) => g.severity === "high").length
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function getAnthropicKey() {
|
|
288
|
+
return process.env.GTM_ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
289
|
+
}
|
|
290
|
+
var VALID_PROPOSAL_TYPES = [
|
|
291
|
+
"product",
|
|
292
|
+
"persona",
|
|
293
|
+
"segment",
|
|
294
|
+
"proof_point",
|
|
295
|
+
"competitor"
|
|
296
|
+
];
|
|
297
|
+
async function handleProposeGraph(supabase, owner, args) {
|
|
298
|
+
const minEntries = args.min_entries_per_topic ?? 3;
|
|
299
|
+
const maxClusters = args.max_clusters ?? 20;
|
|
300
|
+
const summary = await loadExistingGraphSummary(supabase, owner.owner_id, owner.owner_type);
|
|
301
|
+
const clusters = await clusterEntriesByTopic(supabase, owner.owner_id, owner.owner_type, {
|
|
302
|
+
minEntries,
|
|
303
|
+
maxClusters
|
|
304
|
+
});
|
|
305
|
+
if (clusters.length === 0) {
|
|
306
|
+
return {
|
|
307
|
+
proposals: [],
|
|
308
|
+
clusters_analyzed: 0,
|
|
309
|
+
candidates_extracted: 0,
|
|
310
|
+
candidates_deduplicated: 0,
|
|
311
|
+
existing_graph_summary: {
|
|
312
|
+
products: summary.products.count,
|
|
313
|
+
personas: summary.personas.count,
|
|
314
|
+
segments: summary.segments.count,
|
|
315
|
+
proof_points: summary.proof_points.count,
|
|
316
|
+
competitors: summary.competitors.count
|
|
317
|
+
},
|
|
318
|
+
message: `No topic clusters found with ${minEntries}+ entries. Process more transcripts or lower min_entries_per_topic.`
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const apiKey = getAnthropicKey();
|
|
322
|
+
const allCandidates = [];
|
|
323
|
+
const existingNames = {
|
|
324
|
+
products: summary.products.names,
|
|
325
|
+
personas: summary.personas.names,
|
|
326
|
+
segments: summary.segments.names,
|
|
327
|
+
competitors: summary.competitors.names
|
|
328
|
+
};
|
|
329
|
+
if (apiKey) {
|
|
330
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
331
|
+
const candidates = await extractCandidatesFromCluster(clusters[i], existingNames, apiKey);
|
|
332
|
+
allCandidates.push(...candidates);
|
|
333
|
+
if (i < clusters.length - 1) {
|
|
334
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
logError("maestro.handleProposeGraph", new Error("No Anthropic API key configured"), {
|
|
339
|
+
hint: "Set GTM_ANTHROPIC_API_KEY or ANTHROPIC_API_KEY"
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return buildProposal({
|
|
343
|
+
candidates: allCandidates,
|
|
344
|
+
existing_entities: existingNames,
|
|
345
|
+
proof_points_count: summary.proof_points.count
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function buildExtractionPrompt(cluster, existingNames) {
|
|
349
|
+
const entriesText = cluster.entries.map(
|
|
350
|
+
(e, i) => `[${i + 1}] (${e.category}${e.knowledge_type ? "/" + e.knowledge_type : ""}) ${e.content}`
|
|
351
|
+
).join("\n\n");
|
|
352
|
+
const existingSection = [
|
|
353
|
+
existingNames.products.length > 0 ? `Products: ${existingNames.products.join(", ")}` : null,
|
|
354
|
+
existingNames.personas.length > 0 ? `Personas: ${existingNames.personas.join(", ")}` : null,
|
|
355
|
+
existingNames.segments.length > 0 ? `Segments: ${existingNames.segments.join(", ")}` : null,
|
|
356
|
+
existingNames.competitors.length > 0 ? `Competitors: ${existingNames.competitors.join(", ")}` : null
|
|
357
|
+
].filter(Boolean).join("\n");
|
|
358
|
+
return `You are analyzing knowledge entries about the topic "${cluster.topic_name}" to identify business entities.
|
|
359
|
+
|
|
360
|
+
${existingSection ? `EXISTING GRAPH ENTITIES (do NOT re-propose these):
|
|
361
|
+
${existingSection}
|
|
362
|
+
|
|
363
|
+
` : "No graph entities exist yet.\n\n"}KNOWLEDGE ENTRIES:
|
|
364
|
+
${entriesText}
|
|
365
|
+
|
|
366
|
+
Identify any NEW entities mentioned in these entries:
|
|
367
|
+
- Products/services being discussed or offered
|
|
368
|
+
- Types of buyers/personas being described (job titles, industries, pain points)
|
|
369
|
+
- Market segments being referenced
|
|
370
|
+
- Proof points: results, metrics, case studies, testimonials cited
|
|
371
|
+
- Competitors being mentioned
|
|
372
|
+
|
|
373
|
+
For each entity found, return:
|
|
374
|
+
- entity_type: "product" | "persona" | "segment" | "proof_point" | "competitor"
|
|
375
|
+
- name: concise entity name
|
|
376
|
+
- description: 1-2 sentence description based on the entries
|
|
377
|
+
- confidence: 0.0-1.0 (higher = more clearly and frequently referenced)
|
|
378
|
+
- evidence: array of short quoted phrases from entries that support this entity
|
|
379
|
+
- source_entries: array of entry numbers (e.g. [1, 3, 7]) that reference this entity
|
|
380
|
+
|
|
381
|
+
Return a JSON array. Empty array [] if no new entities found.
|
|
382
|
+
Example: [{"entity_type":"product","name":"GTM Coaching","description":"A coaching program for agency owners","confidence":0.85,"evidence":["our coaching program helps"],"source_entries":[1,3]}]`;
|
|
383
|
+
}
|
|
384
|
+
async function extractCandidatesFromCluster(cluster, existingNames, apiKey) {
|
|
385
|
+
try {
|
|
386
|
+
const prompt = buildExtractionPrompt(cluster, existingNames);
|
|
387
|
+
const controller = new AbortController();
|
|
388
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
389
|
+
let result;
|
|
390
|
+
try {
|
|
391
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: {
|
|
394
|
+
"Content-Type": "application/json",
|
|
395
|
+
"x-api-key": apiKey,
|
|
396
|
+
"anthropic-version": "2023-06-01"
|
|
397
|
+
},
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
model: "claude-sonnet-4-5-20250514",
|
|
400
|
+
max_tokens: 4096,
|
|
401
|
+
messages: [{ role: "user", content: prompt }]
|
|
402
|
+
}),
|
|
403
|
+
signal: controller.signal
|
|
404
|
+
});
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const body = await response.json().catch(() => ({}));
|
|
407
|
+
logError("maestro.extractCandidates.api", new Error(`Anthropic ${response.status}`), {
|
|
408
|
+
topic: cluster.topic_slug,
|
|
409
|
+
body
|
|
410
|
+
});
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
const text = data.content[0]?.text ?? "";
|
|
415
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
416
|
+
if (!jsonMatch) return [];
|
|
417
|
+
result = JSON.parse(jsonMatch[0]);
|
|
418
|
+
} finally {
|
|
419
|
+
clearTimeout(timeout);
|
|
420
|
+
}
|
|
421
|
+
if (!Array.isArray(result)) return [];
|
|
422
|
+
const entryIds = cluster.entries.map((e) => e.id);
|
|
423
|
+
return result.filter(isValidRawCandidate).map((raw) => ({
|
|
424
|
+
entity_type: raw.entity_type,
|
|
425
|
+
name: String(raw.name).trim(),
|
|
426
|
+
description: String(raw.description ?? "").trim(),
|
|
427
|
+
confidence: Math.max(0, Math.min(1, Number(raw.confidence) || 0.5)),
|
|
428
|
+
evidence: Array.isArray(raw.evidence) ? raw.evidence.filter((e) => typeof e === "string").map(String) : [],
|
|
429
|
+
source_topic: cluster.topic_slug,
|
|
430
|
+
source_entry_ids: Array.isArray(raw.source_entries) ? raw.source_entries.filter((n) => typeof n === "number").map((n) => entryIds[n - 1]).filter(Boolean) : []
|
|
431
|
+
}));
|
|
432
|
+
} catch (error) {
|
|
433
|
+
logError(
|
|
434
|
+
"maestro.extractCandidatesFromCluster",
|
|
435
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
436
|
+
{ topic: cluster.topic_slug }
|
|
437
|
+
);
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function isValidRawCandidate(raw) {
|
|
442
|
+
return typeof raw.entity_type === "string" && VALID_PROPOSAL_TYPES.includes(raw.entity_type) && typeof raw.name === "string" && raw.name.trim().length > 0;
|
|
443
|
+
}
|
|
444
|
+
var VALID_MENTION_ENTITY_TYPES = ["product", "persona", "competitor"];
|
|
445
|
+
function buildClassificationPrompt(content, entityNames) {
|
|
446
|
+
const sections = [];
|
|
447
|
+
if (entityNames.products.length > 0)
|
|
448
|
+
sections.push(`Products: ${entityNames.products.join(", ")}`);
|
|
449
|
+
if (entityNames.personas.length > 0)
|
|
450
|
+
sections.push(`Personas: ${entityNames.personas.join(", ")}`);
|
|
451
|
+
if (entityNames.competitors.length > 0)
|
|
452
|
+
sections.push(`Competitors: ${entityNames.competitors.join(", ")}`);
|
|
453
|
+
const entityList = sections.join("\n");
|
|
454
|
+
return `You are an entity mention classifier. Given text content and a list of known business entities, identify which entities are referenced in the text.
|
|
455
|
+
|
|
456
|
+
KNOWN ENTITIES:
|
|
457
|
+
${entityList}
|
|
458
|
+
|
|
459
|
+
TEXT CONTENT:
|
|
460
|
+
${content.slice(0, 8e3)}
|
|
461
|
+
|
|
462
|
+
RULES:
|
|
463
|
+
- ONLY match against the known entities listed above
|
|
464
|
+
- Do NOT invent new entities
|
|
465
|
+
- Match by meaning, not exact string \u2014 "our coaching program" matches a product named "GTM Coaching"
|
|
466
|
+
- For personas, match when the text discusses pain points, goals, or characteristics of that persona type
|
|
467
|
+
- For competitors, match when the text references or compares to that competitor
|
|
468
|
+
- confidence: 0.0-1.0 based on how clearly the entity is referenced
|
|
469
|
+
- evidence: quote the specific phrase(s) that indicate the mention
|
|
470
|
+
|
|
471
|
+
Respond with ONLY a JSON array of matches (empty array if none):
|
|
472
|
+
[{"entity_type": "product"|"persona"|"competitor", "name": "exact name from the list above", "relationship": "mentions"|"addresses_pain_point"|"references_competitor"|"demonstrates_value"|"overcomes_objection", "confidence": 0.8, "evidence": "quoted phrase"}]`;
|
|
473
|
+
}
|
|
474
|
+
function isValidMention(m) {
|
|
475
|
+
if (!m || typeof m !== "object") return false;
|
|
476
|
+
const obj = m;
|
|
477
|
+
return typeof obj.entity_type === "string" && VALID_MENTION_ENTITY_TYPES.includes(obj.entity_type) && typeof obj.name === "string" && typeof obj.relationship === "string" && typeof obj.confidence === "number" && typeof obj.evidence === "string" && obj.confidence >= 0 && obj.confidence <= 1;
|
|
478
|
+
}
|
|
479
|
+
async function classifyEntityMentionsForGraph(content, entityNames) {
|
|
480
|
+
const totalEntities = entityNames.products.length + entityNames.personas.length + entityNames.competitors.length;
|
|
481
|
+
if (totalEntities === 0) return [];
|
|
482
|
+
const apiKey = getAnthropicKey();
|
|
483
|
+
if (!apiKey) {
|
|
484
|
+
logError("maestro.classifyEntityMentions", new Error("No Anthropic API key configured"), {
|
|
485
|
+
hint: "Set GTM_ANTHROPIC_API_KEY or ANTHROPIC_API_KEY"
|
|
486
|
+
});
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const prompt = buildClassificationPrompt(content, entityNames);
|
|
491
|
+
const controller = new AbortController();
|
|
492
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
493
|
+
try {
|
|
494
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
495
|
+
method: "POST",
|
|
496
|
+
headers: {
|
|
497
|
+
"Content-Type": "application/json",
|
|
498
|
+
"x-api-key": apiKey,
|
|
499
|
+
"anthropic-version": "2023-06-01"
|
|
500
|
+
},
|
|
501
|
+
body: JSON.stringify({
|
|
502
|
+
model: "claude-haiku-4-5-20251001",
|
|
503
|
+
max_tokens: 1024,
|
|
504
|
+
messages: [{ role: "user", content: prompt }]
|
|
505
|
+
}),
|
|
506
|
+
signal: controller.signal
|
|
507
|
+
});
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
const body = await response.json().catch(() => ({}));
|
|
510
|
+
logError("maestro.classifyEntityMentions.api", new Error(`Anthropic ${response.status}`), {
|
|
511
|
+
body
|
|
512
|
+
});
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
const data = await response.json();
|
|
516
|
+
const text = data.content[0]?.text ?? "";
|
|
517
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
518
|
+
if (!jsonMatch) return [];
|
|
519
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
520
|
+
if (!Array.isArray(parsed)) return [];
|
|
521
|
+
return parsed.filter(isValidMention);
|
|
522
|
+
} finally {
|
|
523
|
+
clearTimeout(timeout);
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
logError(
|
|
527
|
+
"maestro.classifyEntityMentionsForGraph",
|
|
528
|
+
err instanceof Error ? err : new Error(String(err))
|
|
529
|
+
);
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function handleIngestDocument(supabase, owner, args) {
|
|
534
|
+
const filename = args.filename;
|
|
535
|
+
const documentType = args.document_type ?? "other";
|
|
536
|
+
const content = args.content;
|
|
537
|
+
const docResult = await source_document_service_exports.createDocument(supabase, {
|
|
538
|
+
owner_id: owner.owner_id,
|
|
539
|
+
owner_type: owner.owner_type,
|
|
540
|
+
filename,
|
|
541
|
+
document_type: documentType
|
|
542
|
+
});
|
|
543
|
+
if (!docResult.success) {
|
|
544
|
+
throw createGtmError(
|
|
545
|
+
"PROVIDER_REQUEST_FAILED",
|
|
546
|
+
`Failed to record document: ${docResult.error}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const doc = docResult.data;
|
|
550
|
+
if (!content) {
|
|
551
|
+
return {
|
|
552
|
+
success: true,
|
|
553
|
+
document: doc,
|
|
554
|
+
edges_created: 0,
|
|
555
|
+
entities_matched: 0,
|
|
556
|
+
message: "Document recorded. No content provided \u2014 use content param for automatic extraction."
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
await source_document_service_exports.markProcessing(supabase, doc.id);
|
|
560
|
+
try {
|
|
561
|
+
const entityNames = await loadExistingEntityNames(supabase, owner.owner_id, owner.owner_type);
|
|
562
|
+
const mentions = await classifyEntityMentionsForGraph(content, entityNames);
|
|
563
|
+
let edgesCreated = 0;
|
|
564
|
+
if (mentions.length > 0) {
|
|
565
|
+
const result = await extractOntology(supabase, {
|
|
566
|
+
owner_id: owner.owner_id,
|
|
567
|
+
owner_type: owner.owner_type,
|
|
568
|
+
knowledge_entry_id: doc.id,
|
|
569
|
+
source_type: "source_document",
|
|
570
|
+
source_id: doc.id,
|
|
571
|
+
mentions
|
|
572
|
+
});
|
|
573
|
+
if (result.success) {
|
|
574
|
+
edgesCreated = result.data.edges_created;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
await source_document_service_exports.markCompleted(supabase, doc.id, edgesCreated);
|
|
578
|
+
return {
|
|
579
|
+
success: true,
|
|
580
|
+
document: doc,
|
|
581
|
+
edges_created: edgesCreated,
|
|
582
|
+
entities_matched: mentions.length,
|
|
583
|
+
message: `Document processed. ${mentions.length} entity mentions found, ${edgesCreated} edges created.`
|
|
584
|
+
};
|
|
585
|
+
} catch (err) {
|
|
586
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
587
|
+
await source_document_service_exports.markFailed(supabase, doc.id, errorMsg);
|
|
588
|
+
logError("maestro.handleIngestDocument", err instanceof Error ? err : new Error(String(err)), {
|
|
589
|
+
filename,
|
|
590
|
+
docId: doc.id
|
|
591
|
+
});
|
|
592
|
+
throw createGtmError("PROVIDER_REQUEST_FAILED", `Document processing failed: ${errorMsg}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function getHarvestApiKey() {
|
|
596
|
+
return process.env.GTM_HARVEST_API_KEY ?? null;
|
|
597
|
+
}
|
|
598
|
+
async function handleIngestLinkedIn(supabase, owner, args) {
|
|
599
|
+
const linkedinUrl = args.linkedin_url;
|
|
600
|
+
const includePosts = args.include_posts ?? false;
|
|
601
|
+
const harvestKey = getHarvestApiKey();
|
|
602
|
+
if (!harvestKey) {
|
|
603
|
+
throw createGtmError(
|
|
604
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
605
|
+
"HarvestAPI key not configured. Set GTM_HARVEST_API_KEY."
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const harvest = new HarvestClient({ apiKey: harvestKey });
|
|
609
|
+
const profile = await harvest.getProfile(linkedinUrl);
|
|
610
|
+
const textParts = [];
|
|
611
|
+
if (profile.headline) textParts.push(profile.headline);
|
|
612
|
+
if (profile.about) textParts.push(profile.about);
|
|
613
|
+
if (profile.experience && Array.isArray(profile.experience)) {
|
|
614
|
+
for (const exp of profile.experience) {
|
|
615
|
+
const parts = [exp.position, exp.companyName, exp.description].filter(Boolean);
|
|
616
|
+
if (parts.length > 0) textParts.push(parts.join(" at "));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (includePosts) {
|
|
620
|
+
try {
|
|
621
|
+
const postsResponse = await harvest.getProfilePosts({ profile: linkedinUrl });
|
|
622
|
+
if (postsResponse.elements && Array.isArray(postsResponse.elements)) {
|
|
623
|
+
for (const post of postsResponse.elements.slice(0, 10)) {
|
|
624
|
+
if (post.content) textParts.push(post.content);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
logError(
|
|
629
|
+
"maestro.handleIngestLinkedIn.posts",
|
|
630
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
631
|
+
{ linkedinUrl }
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const fullText = textParts.join("\n\n");
|
|
636
|
+
if (!fullText.trim()) {
|
|
637
|
+
return {
|
|
638
|
+
success: true,
|
|
639
|
+
profile_name: `${profile.firstName ?? ""} ${profile.lastName ?? ""}`.trim(),
|
|
640
|
+
edges_created: 0,
|
|
641
|
+
entities_matched: 0,
|
|
642
|
+
message: "Profile scraped but no classifiable text found."
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const docResult = await source_document_service_exports.createDocument(
|
|
646
|
+
supabase,
|
|
647
|
+
{
|
|
648
|
+
owner_id: owner.owner_id,
|
|
649
|
+
owner_type: owner.owner_type,
|
|
650
|
+
filename: linkedinUrl,
|
|
651
|
+
document_type: "other"
|
|
652
|
+
},
|
|
653
|
+
"linkedin_ingester"
|
|
654
|
+
);
|
|
655
|
+
if (!docResult.success) {
|
|
656
|
+
throw createGtmError(
|
|
657
|
+
"PROVIDER_REQUEST_FAILED",
|
|
658
|
+
`Failed to record LinkedIn source: ${docResult.error}`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const doc = docResult.data;
|
|
662
|
+
await source_document_service_exports.markProcessing(supabase, doc.id);
|
|
663
|
+
try {
|
|
664
|
+
const entityNames = await loadExistingEntityNames(supabase, owner.owner_id, owner.owner_type);
|
|
665
|
+
const mentions = await classifyEntityMentionsForGraph(fullText, entityNames);
|
|
666
|
+
let edgesCreated = 0;
|
|
667
|
+
if (mentions.length > 0) {
|
|
668
|
+
const result = await extractOntology(supabase, {
|
|
669
|
+
owner_id: owner.owner_id,
|
|
670
|
+
owner_type: owner.owner_type,
|
|
671
|
+
knowledge_entry_id: doc.id,
|
|
672
|
+
source_type: "source_document",
|
|
673
|
+
source_id: doc.id,
|
|
674
|
+
mentions
|
|
675
|
+
});
|
|
676
|
+
if (result.success) {
|
|
677
|
+
edgesCreated = result.data.edges_created;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
await source_document_service_exports.markCompleted(supabase, doc.id, edgesCreated);
|
|
681
|
+
return {
|
|
682
|
+
success: true,
|
|
683
|
+
profile_name: `${profile.firstName ?? ""} ${profile.lastName ?? ""}`.trim(),
|
|
684
|
+
edges_created: edgesCreated,
|
|
685
|
+
entities_matched: mentions.length,
|
|
686
|
+
source_document_id: doc.id,
|
|
687
|
+
message: `LinkedIn profile processed. ${mentions.length} entity mentions, ${edgesCreated} edges.` + (includePosts ? " Posts included." : "")
|
|
688
|
+
};
|
|
689
|
+
} catch (err) {
|
|
690
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
691
|
+
await source_document_service_exports.markFailed(supabase, doc.id, errorMsg);
|
|
692
|
+
logError("maestro.handleIngestLinkedIn", err instanceof Error ? err : new Error(String(err)), {
|
|
693
|
+
linkedinUrl,
|
|
694
|
+
docId: doc.id
|
|
695
|
+
});
|
|
696
|
+
throw createGtmError("PROVIDER_REQUEST_FAILED", `LinkedIn ingestion failed: ${errorMsg}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function getExaApiKey() {
|
|
700
|
+
return process.env.GTM_EXA_API_KEY ?? null;
|
|
701
|
+
}
|
|
702
|
+
async function handleRefreshCompetitor(supabase, owner, args) {
|
|
703
|
+
const competitorName = args.competitor_name;
|
|
704
|
+
const customQuery = args.search_query;
|
|
705
|
+
if (!competitorName && !customQuery) {
|
|
706
|
+
throw createGtmError("QUERY_INVALID", "Provide competitor_name and/or search_query.");
|
|
707
|
+
}
|
|
708
|
+
const exaKey = getExaApiKey();
|
|
709
|
+
const anthropicKey = getAnthropicKey();
|
|
710
|
+
if (!exaKey) {
|
|
711
|
+
throw createGtmError(
|
|
712
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
713
|
+
"Exa API key not configured. Set GTM_EXA_API_KEY."
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (!anthropicKey) {
|
|
717
|
+
throw createGtmError(
|
|
718
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
719
|
+
"Anthropic API key not configured. Set GTM_ANTHROPIC_API_KEY."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const searchQuery = customQuery ?? `${competitorName} company positioning pricing features services`;
|
|
723
|
+
const exaResponse = await fetch("https://api.exa.ai/search", {
|
|
724
|
+
method: "POST",
|
|
725
|
+
headers: {
|
|
726
|
+
"Content-Type": "application/json",
|
|
727
|
+
"x-api-key": exaKey
|
|
728
|
+
},
|
|
729
|
+
body: JSON.stringify({
|
|
730
|
+
query: searchQuery,
|
|
731
|
+
type: "neural",
|
|
732
|
+
numResults: 5,
|
|
733
|
+
contents: {
|
|
734
|
+
text: { maxCharacters: 2e3 },
|
|
735
|
+
summary: {
|
|
736
|
+
query: `What is ${competitorName ?? "this company"}'s positioning, strengths, and weaknesses?`
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}),
|
|
740
|
+
signal: AbortSignal.timeout(15e3)
|
|
741
|
+
});
|
|
742
|
+
if (!exaResponse.ok) {
|
|
743
|
+
const errText = await exaResponse.text().catch(() => "");
|
|
744
|
+
throw createGtmError(
|
|
745
|
+
"PROVIDER_REQUEST_FAILED",
|
|
746
|
+
`Exa search failed (${exaResponse.status}): ${errText}`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const exaData = await exaResponse.json();
|
|
750
|
+
const results = exaData.results ?? [];
|
|
751
|
+
if (results.length === 0) {
|
|
752
|
+
return {
|
|
753
|
+
success: true,
|
|
754
|
+
competitor_name: competitorName,
|
|
755
|
+
fields_updated: 0,
|
|
756
|
+
sources_checked: 0,
|
|
757
|
+
message: "No search results found."
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const webContent = results.map((r, i) => `[Source ${i + 1}] ${r.title ?? r.url}
|
|
761
|
+
${r.summary ?? ""}
|
|
762
|
+
${r.text ?? ""}`).join("\n\n---\n\n");
|
|
763
|
+
const extractionPrompt = `Analyze the following web search results about "${competitorName ?? "the company"}".
|
|
764
|
+
Extract structured competitive intelligence.
|
|
765
|
+
|
|
766
|
+
WEB RESULTS:
|
|
767
|
+
${webContent.slice(0, 6e3)}
|
|
768
|
+
|
|
769
|
+
Return ONLY a JSON object with these fields (empty string/array if not found):
|
|
770
|
+
{
|
|
771
|
+
"positioning": "How they position themselves in the market (1-2 sentences)",
|
|
772
|
+
"strengths": ["strength1", "strength2"],
|
|
773
|
+
"weaknesses": ["weakness1", "weakness2"],
|
|
774
|
+
"differentiation_notes": "Key differentiators or unique selling points",
|
|
775
|
+
"website": "their main website domain if found"
|
|
776
|
+
}`;
|
|
777
|
+
const aiResponse = await fetch("https://api.anthropic.com/v1/messages", {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: {
|
|
780
|
+
"Content-Type": "application/json",
|
|
781
|
+
"x-api-key": anthropicKey,
|
|
782
|
+
"anthropic-version": "2023-06-01"
|
|
783
|
+
},
|
|
784
|
+
body: JSON.stringify({
|
|
785
|
+
model: "claude-haiku-4-5-20251001",
|
|
786
|
+
max_tokens: 1024,
|
|
787
|
+
messages: [{ role: "user", content: extractionPrompt }]
|
|
788
|
+
}),
|
|
789
|
+
signal: AbortSignal.timeout(3e4)
|
|
790
|
+
});
|
|
791
|
+
if (!aiResponse.ok) {
|
|
792
|
+
throw createGtmError("PROVIDER_REQUEST_FAILED", `AI extraction failed: ${aiResponse.status}`);
|
|
793
|
+
}
|
|
794
|
+
const aiData = await aiResponse.json();
|
|
795
|
+
const aiText = aiData.content[0]?.text ?? "";
|
|
796
|
+
const jsonMatch = aiText.match(/\{[\s\S]*\}/);
|
|
797
|
+
if (!jsonMatch) {
|
|
798
|
+
return {
|
|
799
|
+
success: true,
|
|
800
|
+
competitor_name: competitorName,
|
|
801
|
+
fields_updated: 0,
|
|
802
|
+
sources_checked: results.length,
|
|
803
|
+
message: "Could not extract structured intel from search results."
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
let intel;
|
|
807
|
+
try {
|
|
808
|
+
intel = JSON.parse(jsonMatch[0]);
|
|
809
|
+
} catch (parseErr) {
|
|
810
|
+
logError(
|
|
811
|
+
"maestro.refreshCompetitor.parse",
|
|
812
|
+
parseErr instanceof Error ? parseErr : new Error(String(parseErr)),
|
|
813
|
+
{ competitor_name: competitorName, raw: jsonMatch[0]?.slice(0, 200) }
|
|
814
|
+
);
|
|
815
|
+
return {
|
|
816
|
+
success: true,
|
|
817
|
+
competitor_name: competitorName,
|
|
818
|
+
fields_updated: 0,
|
|
819
|
+
sources_checked: results.length,
|
|
820
|
+
message: "Failed to parse AI extraction result."
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const upsertInput = {
|
|
824
|
+
owner_id: owner.owner_id,
|
|
825
|
+
owner_type: owner.owner_type,
|
|
826
|
+
name: competitorName ?? (typeof intel.website === "string" ? intel.website : "Unknown")
|
|
827
|
+
};
|
|
828
|
+
let fieldsUpdated = 0;
|
|
829
|
+
if (typeof intel.positioning === "string" && intel.positioning) {
|
|
830
|
+
upsertInput.positioning = intel.positioning;
|
|
831
|
+
fieldsUpdated++;
|
|
832
|
+
}
|
|
833
|
+
if (Array.isArray(intel.strengths) && intel.strengths.length > 0) {
|
|
834
|
+
upsertInput.strengths = intel.strengths.filter((s) => typeof s === "string");
|
|
835
|
+
fieldsUpdated++;
|
|
836
|
+
}
|
|
837
|
+
if (Array.isArray(intel.weaknesses) && intel.weaknesses.length > 0) {
|
|
838
|
+
upsertInput.weaknesses = intel.weaknesses.filter((s) => typeof s === "string");
|
|
839
|
+
fieldsUpdated++;
|
|
840
|
+
}
|
|
841
|
+
if (typeof intel.differentiation_notes === "string" && intel.differentiation_notes) {
|
|
842
|
+
upsertInput.differentiation_notes = intel.differentiation_notes;
|
|
843
|
+
fieldsUpdated++;
|
|
844
|
+
}
|
|
845
|
+
if (typeof intel.website === "string" && intel.website) {
|
|
846
|
+
upsertInput.website = intel.website;
|
|
847
|
+
fieldsUpdated++;
|
|
848
|
+
}
|
|
849
|
+
await competitor_service_exports.upsertCompetitor(supabase, upsertInput, "web_scraper");
|
|
850
|
+
return {
|
|
851
|
+
success: true,
|
|
852
|
+
competitor_name: upsertInput.name,
|
|
853
|
+
fields_updated: fieldsUpdated,
|
|
854
|
+
sources_checked: results.length,
|
|
855
|
+
extracted_intel: intel,
|
|
856
|
+
message: `Competitor "${upsertInput.name}" updated with ${fieldsUpdated} fields from ${results.length} web sources.`
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function handleGetChangelog(supabase, owner, args) {
|
|
860
|
+
const entityId = args.entity_id;
|
|
861
|
+
const entityType = args.entity_type;
|
|
862
|
+
if (entityId && entityType) {
|
|
863
|
+
return changelog_service_exports.getEntityChangelog(
|
|
864
|
+
supabase,
|
|
865
|
+
owner.owner_id,
|
|
866
|
+
owner.owner_type,
|
|
867
|
+
entityType,
|
|
868
|
+
entityId
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
return changelog_service_exports.getHistory(supabase, owner.owner_id, owner.owner_type, {
|
|
872
|
+
limit: args.limit ?? 50,
|
|
873
|
+
entityType,
|
|
874
|
+
source: args.source
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
async function handleScoreContact(supabase, owner, args) {
|
|
878
|
+
const headline = args.headline;
|
|
879
|
+
const location = args.location;
|
|
880
|
+
const contactSnapshot = {
|
|
881
|
+
parsed_title: headline ? parseTitle(headline) : null,
|
|
882
|
+
headline: headline ?? null,
|
|
883
|
+
location: location ?? null,
|
|
884
|
+
parsed_country: location ? parseCountry(location) : null,
|
|
885
|
+
company_name: args.company_name ?? null,
|
|
886
|
+
company_size: args.company_size ?? null,
|
|
887
|
+
industry: args.industry ?? null
|
|
888
|
+
};
|
|
889
|
+
const personas = await persona_repo_exports.listPersonas(supabase, owner.owner_id, owner.owner_type);
|
|
890
|
+
const personaMatch = resolvePersona(contactSnapshot, personas);
|
|
891
|
+
const baseScore = args.base_score ?? 0;
|
|
892
|
+
const boostResult = applyPersonaBoost(baseScore, personaMatch);
|
|
893
|
+
return {
|
|
894
|
+
persona_match: personaMatch,
|
|
895
|
+
boost: boostResult,
|
|
896
|
+
personas_checked: personas.length,
|
|
897
|
+
contact_snapshot: contactSnapshot
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
export {
|
|
901
|
+
handleMaestro
|
|
902
|
+
};
|
|
903
|
+
//# sourceMappingURL=maestro-N7Q2JX22.js.map
|