@indexnetwork/protocol 8.1.0-rc.421.1 → 8.2.0-rc.423.1
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/CHANGELOG.md +47 -7
- package/dist/capabilities/opportunities.facade.d.ts +2 -0
- package/dist/capabilities/opportunities.facade.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/opportunity/application/opportunity.evaluator.d.ts +20 -6
- package/dist/opportunity/application/opportunity.evaluator.js +102 -45
- package/dist/opportunity/application/opportunity.graph.d.ts +8 -0
- package/dist/opportunity/application/opportunity.graph.js +233 -81
- package/dist/opportunity/discovery.env.d.ts +14 -0
- package/dist/opportunity/discovery.env.js +79 -0
- package/dist/opportunity/domain/opportunity.evidence.d.ts +2 -1
- package/dist/opportunity/domain/opportunity.evidence.js +11 -2
- package/dist/opportunity/domain/opportunity.state.d.ts +4 -2
- package/dist/opportunity/public/index.d.ts +2 -0
- package/dist/opportunity/public/index.js +2 -0
- package/dist/shared/interfaces/database.interface.d.ts +21 -2
- package/dist/shared/interfaces/embedder.interface.d.ts +16 -2
- package/dist/shared/schemas/network-assignment.schema.d.ts +10 -7
- package/dist/shared/schemas/network-assignment.schema.js +4 -1
- package/package.json +2 -1
|
@@ -29,7 +29,9 @@ import { protocolLogger, withCallLogging } from '../../shared/observability/prot
|
|
|
29
29
|
import { timed } from '../../shared/observability/performance.js';
|
|
30
30
|
import { renderNetworkContext } from '../../shared/network/metadata.renderer.js';
|
|
31
31
|
import { requestContext } from "../../shared/observability/request-context.js";
|
|
32
|
+
import { getAbortSignalConfig } from "../../shared/agent/model-signal.js";
|
|
32
33
|
import { mergeOpportunityEvidence, withCandidateEvidence, withMatchedStrategies } from '../domain/opportunity.evidence.js';
|
|
34
|
+
import { discoveryIntentMatchingEnabled, discoveryProfileMatchingEnabled, discoveryProfileSource } from '../discovery.env.js';
|
|
33
35
|
import { normalizeOpportunityActorIntent, resolveOpportunityActorIntent } from '../domain/opportunity.actor.js';
|
|
34
36
|
import { approveOpportunityIntroduction, deleteOpportunityLifecycle, sendOpportunityLifecycle, updateOpportunityLifecycle } from './opportunity.lifecycle.js';
|
|
35
37
|
import { stampEligibleNewbornOpportunities } from './opportunity.newborn-stamping.js';
|
|
@@ -55,6 +57,14 @@ const deleteLog = protocolLogger('OpportunityGraph:Delete');
|
|
|
55
57
|
const sendLog = protocolLogger('OpportunityGraph:Send');
|
|
56
58
|
const negotiateExistingLog = protocolLogger('OpportunityGraph:NegotiateExisting');
|
|
57
59
|
const routingLog = protocolLogger('OpportunityGraph:Routing');
|
|
60
|
+
/**
|
|
61
|
+
* Error text can include provider response bodies, URLs, and credentials. Keep
|
|
62
|
+
* observability useful by retaining only a conservative error class at this
|
|
63
|
+
* boundary; detailed errors are intentionally not emitted from graph traces.
|
|
64
|
+
*/
|
|
65
|
+
export function safeOpportunityGraphError(_error) {
|
|
66
|
+
return 'OpportunityEvaluationError: [redacted]';
|
|
67
|
+
}
|
|
58
68
|
/** Time window for persist-node dedup. Suppresses a second opportunity with the same person while a recent one (within 30 days) is still in flight, so a person is not re-surfaced multiple times within a month (EDG-23). */
|
|
59
69
|
const DEDUP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
60
70
|
const ACTIVE_NEGOTIATION_TASK_STATES = new Set([
|
|
@@ -120,7 +130,7 @@ function buildEvaluatorEvidenceKey(candidate) {
|
|
|
120
130
|
return [
|
|
121
131
|
candidate.candidateUserId,
|
|
122
132
|
candidate.networkId,
|
|
123
|
-
candidate.candidateIntentId ?? candidate.candidatePremiseId ?? candidate.sourceContextId ?? 'profile',
|
|
133
|
+
candidate.candidateIntentId ?? candidate.candidatePremiseId ?? candidate.candidateContextId ?? candidate.sourceContextId ?? 'profile',
|
|
124
134
|
].join(':');
|
|
125
135
|
}
|
|
126
136
|
/**
|
|
@@ -237,7 +247,7 @@ export class OpportunityGraphFactory {
|
|
|
237
247
|
}
|
|
238
248
|
catch (err) {
|
|
239
249
|
const durationMs = Date.now() - nodeStart;
|
|
240
|
-
const errMsg =
|
|
250
|
+
const errMsg = safeOpportunityGraphError(err);
|
|
241
251
|
traceEmitter?.({ type: "agent_end", name: traceName, durationMs, summary: `error: ${errMsg}` });
|
|
242
252
|
throw err;
|
|
243
253
|
}
|
|
@@ -287,8 +297,14 @@ export class OpportunityGraphFactory {
|
|
|
287
297
|
// DISCOVERY_SOURCE_PREMISE_LIMIT. Loading all premises here caused
|
|
288
298
|
// BACKEND-5: thousands of parallel vector searches for premise-rich users.
|
|
289
299
|
const sourcePremises = [];
|
|
290
|
-
const
|
|
291
|
-
|
|
300
|
+
const profileEnabled = discoveryProfileMatchingEnabled();
|
|
301
|
+
// Context-backed discovery is exclusively the user_context profile-source
|
|
302
|
+
// strategy. Premise mode must not load contexts or emit context-to-intent
|
|
303
|
+
// evidence merely because intent matching is also enabled.
|
|
304
|
+
const userContextProfileEnabled = profileEnabled && discoveryProfileSource() === 'user_context';
|
|
305
|
+
const contextToIntentEnabled = userContextProfileEnabled && process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
|
|
306
|
+
const contextToContextEnabled = userContextProfileEnabled;
|
|
307
|
+
const rawContexts = (contextToIntentEnabled || contextToContextEnabled) && typeof this.database.getUserContexts === 'function'
|
|
292
308
|
? await this.database.getUserContexts(discoveryUserId)
|
|
293
309
|
: [];
|
|
294
310
|
const sourceContexts = rawContexts
|
|
@@ -731,6 +747,17 @@ export class OpportunityGraphFactory {
|
|
|
731
747
|
const perIndexLimit = 80;
|
|
732
748
|
// Similarity threshold for recall (0.30 = 30% similarity)
|
|
733
749
|
const minScore = 0.3;
|
|
750
|
+
// Discovery match-type gating (DISCOVERY_ALLOWED_TYPES / DISCOVERY_PROFILE_SOURCE).
|
|
751
|
+
// Result filtering below is defense-in-depth: corpusGating already scopes the
|
|
752
|
+
// embedder's corpus searches, but adapters may still return other types.
|
|
753
|
+
const intentResultsEnabled = discoveryIntentMatchingEnabled();
|
|
754
|
+
const premiseResultsEnabled = discoveryProfileMatchingEnabled() && discoveryProfileSource() === 'premise';
|
|
755
|
+
const contextResultsEnabled = discoveryProfileMatchingEnabled() && discoveryProfileSource() === 'user_context';
|
|
756
|
+
const corpusGating = {
|
|
757
|
+
intents: discoveryIntentMatchingEnabled(),
|
|
758
|
+
profile: discoveryProfileMatchingEnabled(),
|
|
759
|
+
profileCorpus: discoveryProfileSource(),
|
|
760
|
+
};
|
|
734
761
|
if (state.discoverySource === 'context') {
|
|
735
762
|
// Context discovery: HyDE (when search query exists) + premise-to-premise.
|
|
736
763
|
if (state.searchQuery?.trim()) {
|
|
@@ -791,26 +818,31 @@ export class OpportunityGraphFactory {
|
|
|
791
818
|
model: getModelName("hydeGenerator"),
|
|
792
819
|
},
|
|
793
820
|
});
|
|
794
|
-
const [premiseCands, contextCands] = await Promise.all([
|
|
821
|
+
const [premiseCands, contextCands, contextSimCands] = await Promise.all([
|
|
795
822
|
runPremiseDiscovery(),
|
|
796
823
|
runContextToIntentDiscovery(),
|
|
824
|
+
runContextToContextDiscovery(),
|
|
797
825
|
]);
|
|
798
|
-
const withPremisesAndContext = mergeStrategyCandidates(queryCandidates, premiseCands, contextCands);
|
|
826
|
+
const withPremisesAndContext = mergeStrategyCandidates(queryCandidates, premiseCands, contextCands, contextSimCands);
|
|
799
827
|
if (premiseCands.length > 0) {
|
|
800
828
|
traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
|
|
801
829
|
}
|
|
802
830
|
if (contextCands.length > 0) {
|
|
803
831
|
traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
|
|
804
832
|
}
|
|
833
|
+
if (contextSimCands.length > 0) {
|
|
834
|
+
traceEntries.push({ node: "strategy", detail: `context-to-context → ${contextSimCands.length} candidate(s)` });
|
|
835
|
+
}
|
|
805
836
|
return { candidates: filterByTarget(withPremisesAndContext), trace: traceEntries };
|
|
806
837
|
}
|
|
807
838
|
// No search query — premise-to-premise + context-to-intent discovery
|
|
808
|
-
const [premiseCands, contextCands] = await Promise.all([
|
|
839
|
+
const [premiseCands, contextCands, contextSimCands] = await Promise.all([
|
|
809
840
|
runPremiseDiscovery(),
|
|
810
841
|
runContextToIntentDiscovery(),
|
|
842
|
+
runContextToContextDiscovery(),
|
|
811
843
|
]);
|
|
812
|
-
if (premiseCands.length > 0 || contextCands.length > 0) {
|
|
813
|
-
const merged = mergeStrategyCandidates(premiseCands, contextCands);
|
|
844
|
+
if (premiseCands.length > 0 || contextCands.length > 0 || contextSimCands.length > 0) {
|
|
845
|
+
const merged = mergeStrategyCandidates(premiseCands, contextCands, contextSimCands);
|
|
814
846
|
const traceEntries = [];
|
|
815
847
|
if (premiseCands.length > 0) {
|
|
816
848
|
traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
|
|
@@ -818,9 +850,12 @@ export class OpportunityGraphFactory {
|
|
|
818
850
|
if (contextCands.length > 0) {
|
|
819
851
|
traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
|
|
820
852
|
}
|
|
853
|
+
if (contextSimCands.length > 0) {
|
|
854
|
+
traceEntries.push({ node: "strategy", detail: `context-to-context → ${contextSimCands.length} candidate(s)` });
|
|
855
|
+
}
|
|
821
856
|
traceEntries.push({
|
|
822
857
|
node: "discovery",
|
|
823
|
-
detail: `${[premiseCands.length > 0 && 'premise-to-premise', contextCands.length > 0 && 'context-to-intent'].filter(Boolean).length} strategies → ${premiseCands.length + contextCands.length} raw, ${merged.length} after dedup`,
|
|
858
|
+
detail: `${[premiseCands.length > 0 && 'premise-to-premise', contextCands.length > 0 && 'context-to-intent', contextSimCands.length > 0 && 'context-to-context'].filter(Boolean).length} strategies → ${premiseCands.length + contextCands.length + contextSimCands.length} raw, ${merged.length} after dedup`,
|
|
824
859
|
});
|
|
825
860
|
return { candidates: filterByTarget(merged), trace: traceEntries };
|
|
826
861
|
}
|
|
@@ -871,31 +906,47 @@ export class OpportunityGraphFactory {
|
|
|
871
906
|
limitPerStrategy,
|
|
872
907
|
limit: perIndexLimit,
|
|
873
908
|
minScore,
|
|
909
|
+
corpusGating,
|
|
874
910
|
});
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
911
|
+
if (intentResultsEnabled)
|
|
912
|
+
for (const r of results.filter((x) => x.type === 'intent')) {
|
|
913
|
+
all.push(withCandidateEvidence({
|
|
914
|
+
candidateUserId: r.userId,
|
|
915
|
+
candidateIntentId: r.id,
|
|
916
|
+
networkId: targetIndex.networkId,
|
|
917
|
+
similarity: r.score,
|
|
918
|
+
lens: r.matchedVia,
|
|
919
|
+
candidatePayload: '',
|
|
920
|
+
candidateSummary: undefined,
|
|
921
|
+
discoverySource: 'query',
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
924
|
+
if (premiseResultsEnabled)
|
|
925
|
+
for (const r of results.filter((x) => x.type === 'premise')) {
|
|
926
|
+
all.push(withCandidateEvidence({
|
|
927
|
+
candidateUserId: r.userId,
|
|
928
|
+
candidatePremiseId: r.id,
|
|
929
|
+
networkId: targetIndex.networkId,
|
|
930
|
+
similarity: r.score,
|
|
931
|
+
lens: r.matchedVia,
|
|
932
|
+
candidatePayload: '',
|
|
933
|
+
candidateSummary: undefined,
|
|
934
|
+
discoverySource: 'query',
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
if (contextResultsEnabled)
|
|
938
|
+
for (const r of results.filter((x) => x.type === 'user_context')) {
|
|
939
|
+
all.push(withCandidateEvidence({
|
|
940
|
+
candidateUserId: r.userId,
|
|
941
|
+
candidateContextId: r.id,
|
|
942
|
+
networkId: targetIndex.networkId,
|
|
943
|
+
similarity: r.score,
|
|
944
|
+
lens: r.matchedVia,
|
|
945
|
+
candidatePayload: r.text ?? '',
|
|
946
|
+
candidateSummary: undefined,
|
|
947
|
+
discoverySource: 'query',
|
|
948
|
+
}));
|
|
949
|
+
}
|
|
899
950
|
}));
|
|
900
951
|
const intentCount = all.filter((c) => c.candidateIntentId).length;
|
|
901
952
|
const premiseCount = all.filter((c) => c.candidatePremiseId).length;
|
|
@@ -903,12 +954,17 @@ export class OpportunityGraphFactory {
|
|
|
903
954
|
total: all.length,
|
|
904
955
|
fromIntent: intentCount,
|
|
905
956
|
fromPremise: premiseCount,
|
|
957
|
+
fromContext: all.filter((c) => c.candidateContextId).length,
|
|
906
958
|
});
|
|
907
959
|
const byKey = new Map();
|
|
908
960
|
for (const c of all) {
|
|
909
961
|
// Dedup by candidateUserId + entity (intent or premise), NOT by indexId.
|
|
910
962
|
// Including indexId caused the same user to appear once per index they belong to.
|
|
911
|
-
const entityKey = c.candidateIntentId
|
|
963
|
+
const entityKey = c.candidateIntentId
|
|
964
|
+
? `intent:${c.candidateIntentId}`
|
|
965
|
+
: c.candidatePremiseId
|
|
966
|
+
? `premise:${c.candidatePremiseId}`
|
|
967
|
+
: `context:${c.candidateContextId}`;
|
|
912
968
|
const key = `${c.candidateUserId}:${entityKey}`;
|
|
913
969
|
if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
|
|
914
970
|
byKey.set(key, c);
|
|
@@ -925,6 +981,10 @@ export class OpportunityGraphFactory {
|
|
|
925
981
|
const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
|
|
926
982
|
if (targetNetworkIds.length === 0)
|
|
927
983
|
return [];
|
|
984
|
+
if (!discoveryProfileMatchingEnabled() || discoveryProfileSource() === 'user_context') {
|
|
985
|
+
discoveryLog.verbose('runPremiseDiscovery disabled by DISCOVERY_ALLOWED_TYPES/DISCOVERY_PROFILE_SOURCE');
|
|
986
|
+
return [];
|
|
987
|
+
}
|
|
928
988
|
const sourceLimit = getSourcePremiseDiscoveryLimit();
|
|
929
989
|
if (sourceLimit === 0) {
|
|
930
990
|
discoveryLog.verbose('runPremiseDiscovery disabled by DISCOVERY_SOURCE_PREMISE_LIMIT=0');
|
|
@@ -1004,6 +1064,8 @@ export class OpportunityGraphFactory {
|
|
|
1004
1064
|
const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
|
|
1005
1065
|
if (!contextToIntentEnabled)
|
|
1006
1066
|
return [];
|
|
1067
|
+
if (!discoveryProfileMatchingEnabled() || discoveryProfileSource() !== 'user_context' || !discoveryIntentMatchingEnabled())
|
|
1068
|
+
return [];
|
|
1007
1069
|
const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
|
|
1008
1070
|
if (targetNetworkIds.length === 0)
|
|
1009
1071
|
return [];
|
|
@@ -1031,6 +1093,7 @@ export class OpportunityGraphFactory {
|
|
|
1031
1093
|
limitPerStrategy: limitPerStrategy,
|
|
1032
1094
|
limit: 20,
|
|
1033
1095
|
minScore,
|
|
1096
|
+
corpusGating,
|
|
1034
1097
|
});
|
|
1035
1098
|
for (const r of results.filter(r => r.type === 'intent')) {
|
|
1036
1099
|
contextCandidates.push(withCandidateEvidence({
|
|
@@ -1084,6 +1147,60 @@ export class OpportunityGraphFactory {
|
|
|
1084
1147
|
});
|
|
1085
1148
|
return deduped;
|
|
1086
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Context-to-context discovery (lightweight profile mode only).
|
|
1152
|
+
* Raw network-scoped context embeddings → candidate user_contexts.
|
|
1153
|
+
* No HyDE: both sides are the same document type (synthesized
|
|
1154
|
+
* 3–6 sentence paragraphs), so direct paragraph similarity is correct
|
|
1155
|
+
* (same rationale as raw premise-to-premise).
|
|
1156
|
+
*/
|
|
1157
|
+
async function runContextToContextDiscovery() {
|
|
1158
|
+
if (!contextResultsEnabled)
|
|
1159
|
+
return [];
|
|
1160
|
+
if (!state.sourceContexts?.length)
|
|
1161
|
+
return [];
|
|
1162
|
+
if (typeof self.database.searchUserContextsBySimilarity !== 'function')
|
|
1163
|
+
return [];
|
|
1164
|
+
const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
|
|
1165
|
+
if (targetNetworkIds.length === 0)
|
|
1166
|
+
return [];
|
|
1167
|
+
const contextCandidates = [];
|
|
1168
|
+
for (const ctx of state.sourceContexts.filter(c => targetNetworkIds.includes(c.networkId))) {
|
|
1169
|
+
const results = await self.database.searchUserContextsBySimilarity({
|
|
1170
|
+
embedding: ctx.embedding,
|
|
1171
|
+
networkIds: [ctx.networkId],
|
|
1172
|
+
excludeUserId: discoveryUserId,
|
|
1173
|
+
limit: 20,
|
|
1174
|
+
minScore,
|
|
1175
|
+
});
|
|
1176
|
+
for (const r of results) {
|
|
1177
|
+
contextCandidates.push(withCandidateEvidence({
|
|
1178
|
+
candidateUserId: r.userId,
|
|
1179
|
+
candidateContextId: r.contextId,
|
|
1180
|
+
sourceContextId: ctx.contextId,
|
|
1181
|
+
networkId: r.networkId,
|
|
1182
|
+
similarity: typeof r.similarity === 'number' ? r.similarity : parseFloat(String(r.similarity)),
|
|
1183
|
+
lens: 'context_match',
|
|
1184
|
+
candidatePayload: r.text ?? '',
|
|
1185
|
+
discoverySource: 'context-similarity',
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// Dedup by userId + contextId + networkId (mirror premise dedup)
|
|
1190
|
+
const byKey = new Map();
|
|
1191
|
+
for (const c of contextCandidates) {
|
|
1192
|
+
const key = `${c.candidateUserId}:${c.candidateContextId ?? 'none'}:${c.networkId}`;
|
|
1193
|
+
if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
|
|
1194
|
+
byKey.set(key, c);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const deduped = Array.from(byKey.values());
|
|
1198
|
+
discoveryLog.verbose('runContextToContextDiscovery complete', {
|
|
1199
|
+
rawCount: contextCandidates.length,
|
|
1200
|
+
dedupedCount: deduped.length,
|
|
1201
|
+
});
|
|
1202
|
+
return deduped;
|
|
1203
|
+
}
|
|
1087
1204
|
/**
|
|
1088
1205
|
* Merge candidates from multiple strategies. Deduplicates by userId + networkId + entityId,
|
|
1089
1206
|
* keeps the highest similarity, tracks which strategies found each candidate,
|
|
@@ -1094,7 +1211,7 @@ export class OpportunityGraphFactory {
|
|
|
1094
1211
|
const merged = new Map();
|
|
1095
1212
|
for (const group of groups) {
|
|
1096
1213
|
for (const c of group) {
|
|
1097
|
-
const entityId = c.candidateIntentId ?? c.candidatePremiseId ?? 'none';
|
|
1214
|
+
const entityId = c.candidateIntentId ?? c.candidatePremiseId ?? c.candidateContextId ?? 'none';
|
|
1098
1215
|
const key = `${c.candidateUserId}:${c.networkId}:${entityId}`;
|
|
1099
1216
|
const existing = merged.get(key);
|
|
1100
1217
|
if (!existing) {
|
|
@@ -1129,15 +1246,16 @@ export class OpportunityGraphFactory {
|
|
|
1129
1246
|
const searchText = state.searchQuery ?? resolvedIntent?.payload ?? '';
|
|
1130
1247
|
if (!searchText) {
|
|
1131
1248
|
discoveryLog.warn('No search text available for intent path');
|
|
1132
|
-
const [premiseCands, contextCands] = await Promise.all([
|
|
1249
|
+
const [premiseCands, contextCands, contextSimCands] = await Promise.all([
|
|
1133
1250
|
runPremiseDiscovery(),
|
|
1134
1251
|
runContextToIntentDiscovery(),
|
|
1252
|
+
runContextToContextDiscovery(),
|
|
1135
1253
|
]);
|
|
1136
|
-
const merged = mergeStrategyCandidates(premiseCands, contextCands);
|
|
1254
|
+
const merged = mergeStrategyCandidates(premiseCands, contextCands, contextSimCands);
|
|
1137
1255
|
if (merged.length > 0) {
|
|
1138
1256
|
return {
|
|
1139
1257
|
candidates: filterByTarget(merged),
|
|
1140
|
-
trace: [{ node: "discovery", detail: `No search text; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
|
|
1258
|
+
trace: [{ node: "discovery", detail: `No search text; premise → ${premiseCands.length}, context → ${contextCands.length}, context-sim → ${contextSimCands.length}, merged → ${merged.length} candidate(s)` }],
|
|
1141
1259
|
};
|
|
1142
1260
|
}
|
|
1143
1261
|
return { candidates: [] };
|
|
@@ -1156,16 +1274,17 @@ export class OpportunityGraphFactory {
|
|
|
1156
1274
|
const hydeEmbeddings = hydeResult.hydeEmbeddings;
|
|
1157
1275
|
const lenses = hydeResult.lenses ?? [];
|
|
1158
1276
|
if (!hydeEmbeddings || Object.keys(hydeEmbeddings).length === 0) {
|
|
1159
|
-
const [premiseCands, contextCands] = await Promise.all([
|
|
1277
|
+
const [premiseCands, contextCands, contextSimCands] = await Promise.all([
|
|
1160
1278
|
runPremiseDiscovery(),
|
|
1161
1279
|
runContextToIntentDiscovery(),
|
|
1280
|
+
runContextToContextDiscovery(),
|
|
1162
1281
|
]);
|
|
1163
|
-
const merged = mergeStrategyCandidates(premiseCands, contextCands);
|
|
1282
|
+
const merged = mergeStrategyCandidates(premiseCands, contextCands, contextSimCands);
|
|
1164
1283
|
if (merged.length > 0) {
|
|
1165
1284
|
return {
|
|
1166
1285
|
hydeEmbeddings: {},
|
|
1167
1286
|
candidates: filterByTarget(merged),
|
|
1168
|
-
trace: [{ node: "discovery", detail: `No HyDE embeddings; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
|
|
1287
|
+
trace: [{ node: "discovery", detail: `No HyDE embeddings; premise → ${premiseCands.length}, context → ${contextCands.length}, context-sim → ${contextSimCands.length}, merged → ${merged.length} candidate(s)` }],
|
|
1169
1288
|
};
|
|
1170
1289
|
}
|
|
1171
1290
|
return { hydeEmbeddings: {}, candidates: [] };
|
|
@@ -1186,35 +1305,55 @@ export class OpportunityGraphFactory {
|
|
|
1186
1305
|
limitPerStrategy,
|
|
1187
1306
|
limit: perIndexLimit,
|
|
1188
1307
|
minScore,
|
|
1308
|
+
corpusGating,
|
|
1189
1309
|
});
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1310
|
+
if (intentResultsEnabled)
|
|
1311
|
+
for (const result of results.filter((r) => r.type === 'intent')) {
|
|
1312
|
+
allCandidates.push(withCandidateEvidence({
|
|
1313
|
+
candidateUserId: result.userId,
|
|
1314
|
+
candidateIntentId: result.id,
|
|
1315
|
+
networkId: targetIndex.networkId,
|
|
1316
|
+
similarity: result.score,
|
|
1317
|
+
lens: result.matchedVia,
|
|
1318
|
+
candidatePayload: '',
|
|
1319
|
+
candidateSummary: undefined,
|
|
1320
|
+
discoverySource: 'query',
|
|
1321
|
+
}));
|
|
1322
|
+
}
|
|
1323
|
+
if (premiseResultsEnabled)
|
|
1324
|
+
for (const result of results.filter((r) => r.type === 'premise')) {
|
|
1325
|
+
allCandidates.push(withCandidateEvidence({
|
|
1326
|
+
candidateUserId: result.userId,
|
|
1327
|
+
candidatePremiseId: result.id,
|
|
1328
|
+
networkId: targetIndex.networkId,
|
|
1329
|
+
similarity: result.score,
|
|
1330
|
+
lens: result.matchedVia,
|
|
1331
|
+
candidatePayload: '',
|
|
1332
|
+
candidateSummary: undefined,
|
|
1333
|
+
discoverySource: 'query',
|
|
1334
|
+
}));
|
|
1335
|
+
}
|
|
1336
|
+
if (contextResultsEnabled)
|
|
1337
|
+
for (const result of results.filter((r) => r.type === 'user_context')) {
|
|
1338
|
+
allCandidates.push(withCandidateEvidence({
|
|
1339
|
+
candidateUserId: result.userId,
|
|
1340
|
+
candidateContextId: result.id,
|
|
1341
|
+
networkId: targetIndex.networkId,
|
|
1342
|
+
similarity: result.score,
|
|
1343
|
+
lens: result.matchedVia,
|
|
1344
|
+
candidatePayload: result.text ?? '',
|
|
1345
|
+
candidateSummary: undefined,
|
|
1346
|
+
discoverySource: 'query',
|
|
1347
|
+
}));
|
|
1348
|
+
}
|
|
1214
1349
|
}));
|
|
1215
1350
|
const byUserAndIndex = new Map();
|
|
1216
1351
|
for (const c of allCandidates) {
|
|
1217
|
-
const entityKey = c.candidateIntentId
|
|
1352
|
+
const entityKey = c.candidateIntentId
|
|
1353
|
+
? `intent:${c.candidateIntentId}`
|
|
1354
|
+
: c.candidatePremiseId
|
|
1355
|
+
? `premise:${c.candidatePremiseId}`
|
|
1356
|
+
: `context:${c.candidateContextId}`;
|
|
1218
1357
|
const key = `${c.candidateUserId}:${c.networkId}:${entityKey}`;
|
|
1219
1358
|
if (!byUserAndIndex.has(key) || c.similarity > (byUserAndIndex.get(key)?.similarity ?? 0)) {
|
|
1220
1359
|
byUserAndIndex.set(key, c);
|
|
@@ -1295,15 +1434,16 @@ export class OpportunityGraphFactory {
|
|
|
1295
1434
|
},
|
|
1296
1435
|
});
|
|
1297
1436
|
}
|
|
1298
|
-
const [premiseCands, contextCands] = await Promise.all([
|
|
1437
|
+
const [premiseCands, contextCands, contextSimCands] = await Promise.all([
|
|
1299
1438
|
runPremiseDiscovery(),
|
|
1300
1439
|
runContextToIntentDiscovery(),
|
|
1440
|
+
runContextToContextDiscovery(),
|
|
1301
1441
|
]);
|
|
1302
|
-
const allStrategies = mergeStrategyCandidates(candidates, premiseCands, contextCands);
|
|
1303
|
-
if (premiseCands.length > 0 || contextCands.length > 0) {
|
|
1442
|
+
const allStrategies = mergeStrategyCandidates(candidates, premiseCands, contextCands, contextSimCands);
|
|
1443
|
+
if (premiseCands.length > 0 || contextCands.length > 0 || contextSimCands.length > 0) {
|
|
1304
1444
|
traceEntries.push({
|
|
1305
1445
|
node: "discovery",
|
|
1306
|
-
detail: `+ Premise → ${premiseCands.length}, Context → ${contextCands.length}, merged to ${allStrategies.length} candidate(s)`,
|
|
1446
|
+
detail: `+ Premise → ${premiseCands.length}, Context → ${contextCands.length}, Context-sim → ${contextSimCands.length}, merged to ${allStrategies.length} candidate(s)`,
|
|
1307
1447
|
});
|
|
1308
1448
|
}
|
|
1309
1449
|
return {
|
|
@@ -1437,7 +1577,7 @@ export class OpportunityGraphFactory {
|
|
|
1437
1577
|
}
|
|
1438
1578
|
catch (err) {
|
|
1439
1579
|
evaluationLog.warn('IND-567 rejection cool-down: lookup failed, skipping penalty', {
|
|
1440
|
-
error:
|
|
1580
|
+
error: safeOpportunityGraphError(err),
|
|
1441
1581
|
});
|
|
1442
1582
|
}
|
|
1443
1583
|
}
|
|
@@ -1493,6 +1633,7 @@ export class OpportunityGraphFactory {
|
|
|
1493
1633
|
const profile = await this.database.getProfile(c.candidateUserId);
|
|
1494
1634
|
let intentPayload = c.candidatePayload;
|
|
1495
1635
|
let intentSummary = c.candidateSummary;
|
|
1636
|
+
let evidence = c.evidence;
|
|
1496
1637
|
if (c.candidateIntentId != null && (!intentPayload || intentPayload === '')) {
|
|
1497
1638
|
const intent = await this.database.getIntent(c.candidateIntentId);
|
|
1498
1639
|
if (intent) {
|
|
@@ -1500,6 +1641,16 @@ export class OpportunityGraphFactory {
|
|
|
1500
1641
|
intentSummary = intent.summary ?? undefined;
|
|
1501
1642
|
}
|
|
1502
1643
|
}
|
|
1644
|
+
if (c.candidatePremiseId != null && (!intentPayload || intentPayload === '')) {
|
|
1645
|
+
const premise = await this.database.getPremise(c.candidatePremiseId);
|
|
1646
|
+
if (premise) {
|
|
1647
|
+
intentPayload = premise.assertion.text;
|
|
1648
|
+
intentSummary = premise.assertion.summary;
|
|
1649
|
+
evidence = (c.evidence ?? []).map((item) => item.candidatePremiseId === c.candidatePremiseId
|
|
1650
|
+
? { ...item, payload: premise.assertion.text, summary: premise.assertion.summary, assertionText: premise.assertion.text }
|
|
1651
|
+
: item);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1503
1654
|
const evidenceKey = buildEvaluatorEvidenceKey(c);
|
|
1504
1655
|
return {
|
|
1505
1656
|
userId: c.candidateUserId,
|
|
@@ -1516,7 +1667,7 @@ export class OpportunityGraphFactory {
|
|
|
1516
1667
|
evidenceKey,
|
|
1517
1668
|
ragScore: c.similarity * 100,
|
|
1518
1669
|
matchedVia: c.lens,
|
|
1519
|
-
evidence
|
|
1670
|
+
evidence,
|
|
1520
1671
|
};
|
|
1521
1672
|
}));
|
|
1522
1673
|
const userIdToIndexId = new Map();
|
|
@@ -1545,6 +1696,7 @@ export class OpportunityGraphFactory {
|
|
|
1545
1696
|
}
|
|
1546
1697
|
// Lower default threshold to 50 for better recall
|
|
1547
1698
|
const minScore = state.options.minScore ?? 50;
|
|
1699
|
+
const evaluatorSignalConfig = getAbortSignalConfig();
|
|
1548
1700
|
const evaluator = typeof evaluatorAgent.invokeEntityBundle === 'function'
|
|
1549
1701
|
? evaluatorAgent
|
|
1550
1702
|
: new OpportunityEvaluator();
|
|
@@ -1569,7 +1721,7 @@ export class OpportunityGraphFactory {
|
|
|
1569
1721
|
const _traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
1570
1722
|
_traceEmitter?.({ type: "agent_start", name: "opportunity-evaluator" });
|
|
1571
1723
|
const _candidateName = candidateEntity.profile?.name ?? "Unknown";
|
|
1572
|
-
return evaluator.invokeEntityBundle(input, { minScore, returnAll: true })
|
|
1724
|
+
return evaluator.invokeEntityBundle(input, { minScore, returnAll: true, ...evaluatorSignalConfig })
|
|
1573
1725
|
.then((res) => {
|
|
1574
1726
|
const _evalDuration = Date.now() - _evalStart;
|
|
1575
1727
|
agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
|
|
@@ -1580,12 +1732,12 @@ export class OpportunityGraphFactory {
|
|
|
1580
1732
|
})
|
|
1581
1733
|
.catch((err) => {
|
|
1582
1734
|
const _evalDuration = Date.now() - _evalStart;
|
|
1583
|
-
const _errMsg =
|
|
1735
|
+
const _errMsg = safeOpportunityGraphError(err);
|
|
1584
1736
|
agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
|
|
1585
1737
|
_traceEmitter?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `${_candidateName}: error — ${_errMsg}` });
|
|
1586
1738
|
evaluationLog.warn('Parallel eval failed for candidate', {
|
|
1587
1739
|
candidateUserId: candidateEntity.userId,
|
|
1588
|
-
error:
|
|
1740
|
+
error: _errMsg,
|
|
1589
1741
|
});
|
|
1590
1742
|
parallelErrors.push({
|
|
1591
1743
|
candidateUserId: candidateEntity.userId,
|
|
@@ -1632,14 +1784,14 @@ export class OpportunityGraphFactory {
|
|
|
1632
1784
|
_traceEmitterSerial?.({ type: "agent_start", name: "opportunity-evaluator" });
|
|
1633
1785
|
let opportunitiesWithActors;
|
|
1634
1786
|
try {
|
|
1635
|
-
opportunitiesWithActors = await evaluator.invokeEntityBundle(input, { minScore, returnAll: true });
|
|
1787
|
+
opportunitiesWithActors = await evaluator.invokeEntityBundle(input, { minScore, returnAll: true, ...evaluatorSignalConfig });
|
|
1636
1788
|
const _evalDuration = Date.now() - _evalStart;
|
|
1637
1789
|
agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
|
|
1638
1790
|
_traceEmitterSerial?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `Evaluated ${candidateEntities.length} candidate(s)` });
|
|
1639
1791
|
}
|
|
1640
1792
|
catch (serialErr) {
|
|
1641
1793
|
const _evalDuration = Date.now() - _evalStart;
|
|
1642
|
-
const _errMsg =
|
|
1794
|
+
const _errMsg = safeOpportunityGraphError(serialErr);
|
|
1643
1795
|
agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _evalDuration });
|
|
1644
1796
|
_traceEmitterSerial?.({ type: "agent_end", name: "opportunity-evaluator", durationMs: _evalDuration, summary: `error — ${_errMsg}` });
|
|
1645
1797
|
throw serialErr; // Re-throw for the outer catch to handle
|
|
@@ -1816,8 +1968,8 @@ export class OpportunityGraphFactory {
|
|
|
1816
1968
|
};
|
|
1817
1969
|
}
|
|
1818
1970
|
catch (error) {
|
|
1819
|
-
const errMsg =
|
|
1820
|
-
evaluationLog.error('Failed', { error });
|
|
1971
|
+
const errMsg = safeOpportunityGraphError(error);
|
|
1972
|
+
evaluationLog.error('Failed', { error: errMsg });
|
|
1821
1973
|
return {
|
|
1822
1974
|
evaluatedOpportunities: [],
|
|
1823
1975
|
error: 'Failed to evaluate candidates.',
|
|
@@ -2391,7 +2543,7 @@ export class OpportunityGraphFactory {
|
|
|
2391
2543
|
_evalStart = Date.now();
|
|
2392
2544
|
_traceEmitterIntro?.({ type: "agent_start", name: "intro-evaluator" });
|
|
2393
2545
|
_introEvalStarted = true;
|
|
2394
|
-
const evaluated = await evaluatorAgent.invokeEntityBundle(input, { minScore: 0 });
|
|
2546
|
+
const evaluated = await evaluatorAgent.invokeEntityBundle(input, { minScore: 0, ...getAbortSignalConfig() });
|
|
2395
2547
|
const _introDuration = Date.now() - _evalStart;
|
|
2396
2548
|
agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _introDuration });
|
|
2397
2549
|
_traceEmitterIntro?.({ type: "agent_end", name: "intro-evaluator", durationMs: _introDuration, summary: "Evaluated introduction" });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Data types allowed to participate in opportunity matching. */
|
|
2
|
+
export type DiscoveryMatchType = 'intent' | 'profile';
|
|
3
|
+
/** Test hook: clear warn-once state so each test observes warnings. */
|
|
4
|
+
export declare function resetDiscoveryEnvWarningsForTests(): void;
|
|
5
|
+
/** Current DISCOVERY_ALLOWED_TYPES set (default: intent + profile). */
|
|
6
|
+
export declare function discoveryAllowedTypes(): ReadonlySet<DiscoveryMatchType>;
|
|
7
|
+
/** True when intents may participate in matching (source or candidate). */
|
|
8
|
+
export declare function discoveryIntentMatchingEnabled(): boolean;
|
|
9
|
+
/** True when profile data (premises or user_contexts) may participate. */
|
|
10
|
+
export declare function discoveryProfileMatchingEnabled(): boolean;
|
|
11
|
+
/** What "profile" means in matching: heavyweight premises or lightweight user_contexts. */
|
|
12
|
+
export type DiscoveryProfileSource = 'premise' | 'user_context';
|
|
13
|
+
/** Current DISCOVERY_PROFILE_SOURCE (default: premise). */
|
|
14
|
+
export declare function discoveryProfileSource(): DiscoveryProfileSource;
|