@indexnetwork/protocol 1.18.0-rc.185.1 → 1.20.0-rc.187.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.
@@ -198,14 +198,26 @@ export class OpportunityGraphFactory {
198
198
  premiseId: p.id,
199
199
  embedding: p.embedding,
200
200
  }));
201
+ const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
202
+ const rawContexts = contextToIntentEnabled
203
+ ? await this.database.getUserContexts(discoveryUserId)
204
+ : [];
205
+ const sourceContexts = rawContexts
206
+ .filter((c) => c.embedding && c.embedding.length > 0 && userNetworkIds.includes(c.networkId))
207
+ .map((c) => ({
208
+ contextId: c.id,
209
+ networkId: c.networkId,
210
+ embedding: c.embedding,
211
+ }));
201
212
  return {
202
213
  userNetworks: userNetworkIds,
203
214
  indexedIntents,
204
215
  sourceProfile,
205
216
  sourcePremises,
217
+ sourceContexts,
206
218
  trace: [{
207
219
  node: "prep",
208
- detail: `${userNetworkIds.length} network(s), ${intents.length} intent(s), ${sourcePremises.length} premise(s), ${profile ? 'profile loaded' : 'no profile'}`,
220
+ detail: `${userNetworkIds.length} network(s), ${intents.length} intent(s), ${sourcePremises.length} premise(s), ${sourceContexts.length} context(s), ${profile ? 'profile loaded' : 'no profile'}`,
209
221
  }],
210
222
  };
211
223
  }, { context: { userId: state.userId }, logOutput: true }).catch((error) => {
@@ -649,20 +661,38 @@ export class OpportunityGraphFactory {
649
661
  model: getModelName("hydeGenerator"),
650
662
  },
651
663
  });
652
- const premiseCands = await runPremiseDiscovery();
653
- const withPremises = mergePremiseCandidates(queryCandidates, premiseCands);
664
+ const [premiseCands, contextCands] = await Promise.all([
665
+ runPremiseDiscovery(),
666
+ runContextToIntentDiscovery(),
667
+ ]);
668
+ const withPremisesAndContext = mergeStrategyCandidates(queryCandidates, premiseCands, contextCands);
654
669
  if (premiseCands.length > 0) {
655
- traceEntries.push({ node: "discovery", detail: `+ Premise search → ${premiseCands.length} candidate(s), merged to ${withPremises.length}` });
670
+ traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
671
+ }
672
+ if (contextCands.length > 0) {
673
+ traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
656
674
  }
657
- return { candidates: filterByTarget(withPremises), trace: traceEntries };
675
+ return { candidates: filterByTarget(withPremisesAndContext), trace: traceEntries };
658
676
  }
659
- // No search query — premise-to-premise discovery only
660
- const premiseCands = await runPremiseDiscovery();
661
- if (premiseCands.length > 0) {
662
- return {
663
- candidates: filterByTarget(premiseCands),
664
- trace: [{ node: "discovery", detail: `No search query; premise search → ${premiseCands.length} candidate(s)` }],
665
- };
677
+ // No search query — premise-to-premise + context-to-intent discovery
678
+ const [premiseCands, contextCands] = await Promise.all([
679
+ runPremiseDiscovery(),
680
+ runContextToIntentDiscovery(),
681
+ ]);
682
+ if (premiseCands.length > 0 || contextCands.length > 0) {
683
+ const merged = mergeStrategyCandidates(premiseCands, contextCands);
684
+ const traceEntries = [];
685
+ if (premiseCands.length > 0) {
686
+ traceEntries.push({ node: "strategy", detail: `premise-to-premise → ${premiseCands.length} candidate(s)` });
687
+ }
688
+ if (contextCands.length > 0) {
689
+ traceEntries.push({ node: "strategy", detail: `context-to-intent → ${contextCands.length} candidate(s)` });
690
+ }
691
+ traceEntries.push({
692
+ node: "discovery",
693
+ 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`,
694
+ });
695
+ return { candidates: filterByTarget(merged), trace: traceEntries };
666
696
  }
667
697
  return { candidates: [] };
668
698
  }
@@ -807,20 +837,126 @@ export class OpportunityGraphFactory {
807
837
  return deduped;
808
838
  }
809
839
  /**
810
- * Merge premise candidates into an existing candidate list.
811
- * Deduplicates by userId + networkId + discoverySource + entityId.
840
+ * Context-to-intent discovery: searches intents using context HyDE embeddings.
841
+ * When HyDE documents exist for a context, uses optimised hypothetical-document
842
+ * embeddings via searchWithHydeEmbeddings. Falls back to raw context embedding
843
+ * via searchIntentsByContextEmbedding when no HyDE docs are available.
812
844
  */
813
- function mergePremiseCandidates(existing, premise) {
814
- if (premise.length === 0)
815
- return existing;
845
+ async function runContextToIntentDiscovery() {
846
+ if (!state.sourceContexts?.length)
847
+ return [];
848
+ const contextToIntentEnabled = process.env.DISCOVERY_CONTEXT_TO_INTENT !== '0';
849
+ if (!contextToIntentEnabled)
850
+ return [];
851
+ const targetNetworkIds = state.targetNetworks.map(t => t.networkId);
852
+ if (targetNetworkIds.length === 0)
853
+ return [];
854
+ logger.verbose('[Graph:Discovery] runContextToIntentDiscovery start', {
855
+ contextCount: state.sourceContexts.length,
856
+ targetNetworks: targetNetworkIds.length,
857
+ });
858
+ const contextCandidates = [];
859
+ for (const ctx of state.sourceContexts.filter(c => targetNetworkIds.includes(c.networkId))) {
860
+ // Attempt HyDE-enhanced search first
861
+ const hydeDocs = await self.database.getHydeDocumentsForSource('context', ctx.contextId);
862
+ const lensEmbeddings = hydeDocs
863
+ .filter(d => d.hydeEmbedding?.length > 0)
864
+ .map(d => ({
865
+ lens: d.strategy,
866
+ corpus: (d.targetCorpus === 'intents' ? 'intents' : d.targetCorpus === 'premises' ? 'premises' : 'intents'),
867
+ embedding: d.hydeEmbedding,
868
+ }));
869
+ if (lensEmbeddings.length > 0) {
870
+ // HyDE-enhanced search: same path as query HyDE, scoped to this context's network
871
+ const results = await self.embedder.searchWithHydeEmbeddings(lensEmbeddings, {
872
+ indexScope: [ctx.networkId],
873
+ excludeUserId: discoveryUserId,
874
+ limitPerStrategy: limitPerStrategy,
875
+ limit: 20,
876
+ minScore,
877
+ });
878
+ for (const r of results.filter(r => r.type === 'intent')) {
879
+ contextCandidates.push({
880
+ candidateUserId: r.userId,
881
+ candidateIntentId: r.id,
882
+ networkId: ctx.networkId,
883
+ similarity: r.score,
884
+ lens: r.matchedVia,
885
+ candidatePayload: '',
886
+ candidateSummary: undefined,
887
+ discoverySource: 'context-to-intent',
888
+ });
889
+ }
890
+ }
891
+ else {
892
+ // Fallback: raw context embedding search (no HyDE docs yet)
893
+ const results = await self.database.searchIntentsByContextEmbedding({
894
+ embedding: ctx.embedding,
895
+ networkIds: [ctx.networkId],
896
+ excludeUserId: discoveryUserId,
897
+ limit: 20,
898
+ minScore: minScore,
899
+ });
900
+ for (const r of results) {
901
+ contextCandidates.push({
902
+ candidateUserId: r.userId,
903
+ candidateIntentId: r.intentId,
904
+ networkId: r.networkId,
905
+ similarity: typeof r.similarity === 'number' ? r.similarity : parseFloat(String(r.similarity)),
906
+ lens: 'context_match',
907
+ candidatePayload: r.payload ?? '',
908
+ candidateSummary: r.summary ?? undefined,
909
+ discoverySource: 'context-to-intent',
910
+ });
911
+ }
912
+ }
913
+ }
914
+ const byKey = new Map();
915
+ for (const c of contextCandidates) {
916
+ const key = `${c.candidateUserId}:${c.candidateIntentId ?? 'none'}:${c.networkId}`;
917
+ if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
918
+ byKey.set(key, c);
919
+ }
920
+ }
921
+ const deduped = Array.from(byKey.values());
922
+ logger.verbose('[Graph:Discovery] runContextToIntentDiscovery complete', {
923
+ rawCount: contextCandidates.length,
924
+ dedupedCount: deduped.length,
925
+ });
926
+ return deduped;
927
+ }
928
+ /**
929
+ * Merge candidates from multiple strategies. Deduplicates by userId + networkId + entityId,
930
+ * keeps the highest similarity, tracks which strategies found each candidate,
931
+ * and applies a multi-strategy boost (+0.05 per additional strategy, boost capped at 0.15,
932
+ * final similarity capped at 1.0).
933
+ */
934
+ function mergeStrategyCandidates(...groups) {
816
935
  const merged = new Map();
817
- for (const c of [...existing, ...premise]) {
818
- const key = `${c.candidateUserId}:${c.networkId}:${c.discoverySource}:${c.candidateIntentId ?? c.candidatePremiseId ?? 'none'}`;
819
- if (!merged.has(key) || c.similarity > (merged.get(key)?.similarity ?? 0)) {
820
- merged.set(key, c);
936
+ for (const group of groups) {
937
+ for (const c of group) {
938
+ const entityId = c.candidateIntentId ?? c.candidatePremiseId ?? 'none';
939
+ const key = `${c.candidateUserId}:${c.networkId}:${entityId}`;
940
+ const existing = merged.get(key);
941
+ if (!existing) {
942
+ merged.set(key, { ...c, _strategies: new Set([c.discoverySource ?? 'unknown']) });
943
+ }
944
+ else {
945
+ existing._strategies.add(c.discoverySource ?? 'unknown');
946
+ if (c.similarity > existing.similarity) {
947
+ Object.assign(existing, c);
948
+ }
949
+ }
821
950
  }
822
951
  }
823
- return Array.from(merged.values());
952
+ return Array.from(merged.values()).map(({ _strategies, ...c }) => {
953
+ const boost = Math.min((_strategies.size - 1) * 0.05, 0.15);
954
+ return {
955
+ ...c,
956
+ similarity: Math.min(c.similarity + boost, 1.0),
957
+ matchedStrategies: Array.from(_strategies),
958
+ };
959
+ });
824
960
  }
825
961
  const resolvedIntent = state.resolvedTriggerIntentId
826
962
  ? state.indexedIntents.find((i) => i.intentId === state.resolvedTriggerIntentId)
@@ -828,11 +964,15 @@ export class OpportunityGraphFactory {
828
964
  const searchText = state.searchQuery ?? resolvedIntent?.payload ?? '';
829
965
  if (!searchText) {
830
966
  logger.warn('[Graph:Discovery] No search text available for intent path');
831
- const premiseCands = await runPremiseDiscovery();
832
- if (premiseCands.length > 0) {
967
+ const [premiseCands, contextCands] = await Promise.all([
968
+ runPremiseDiscovery(),
969
+ runContextToIntentDiscovery(),
970
+ ]);
971
+ const merged = mergeStrategyCandidates(premiseCands, contextCands);
972
+ if (merged.length > 0) {
833
973
  return {
834
- candidates: filterByTarget(premiseCands),
835
- trace: [{ node: "discovery", detail: `No search text; premise search → ${premiseCands.length} candidate(s)` }],
974
+ candidates: filterByTarget(merged),
975
+ trace: [{ node: "discovery", detail: `No search text; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
836
976
  };
837
977
  }
838
978
  return { candidates: [] };
@@ -851,12 +991,16 @@ export class OpportunityGraphFactory {
851
991
  const hydeEmbeddings = hydeResult.hydeEmbeddings;
852
992
  const lenses = hydeResult.lenses ?? [];
853
993
  if (!hydeEmbeddings || Object.keys(hydeEmbeddings).length === 0) {
854
- const premiseCands = await runPremiseDiscovery();
855
- if (premiseCands.length > 0) {
994
+ const [premiseCands, contextCands] = await Promise.all([
995
+ runPremiseDiscovery(),
996
+ runContextToIntentDiscovery(),
997
+ ]);
998
+ const merged = mergeStrategyCandidates(premiseCands, contextCands);
999
+ if (merged.length > 0) {
856
1000
  return {
857
1001
  hydeEmbeddings: {},
858
- candidates: filterByTarget(premiseCands),
859
- trace: [{ node: "discovery", detail: `No HyDE embeddings; premise search → ${premiseCands.length} candidate(s)` }],
1002
+ candidates: filterByTarget(merged),
1003
+ trace: [{ node: "discovery", detail: `No HyDE embeddings; premise → ${premiseCands.length}, context → ${contextCands.length}, merged → ${merged.length} candidate(s)` }],
860
1004
  };
861
1005
  }
862
1006
  return { hydeEmbeddings: {}, candidates: [] };
@@ -986,14 +1130,20 @@ export class OpportunityGraphFactory {
986
1130
  },
987
1131
  });
988
1132
  }
989
- const premiseCands = await runPremiseDiscovery();
990
- const withPremises = mergePremiseCandidates(candidates, premiseCands);
991
- if (premiseCands.length > 0) {
992
- traceEntries.push({ node: "discovery", detail: `+ Premise search → ${premiseCands.length} candidate(s), merged to ${withPremises.length}` });
1133
+ const [premiseCands, contextCands] = await Promise.all([
1134
+ runPremiseDiscovery(),
1135
+ runContextToIntentDiscovery(),
1136
+ ]);
1137
+ const allStrategies = mergeStrategyCandidates(candidates, premiseCands, contextCands);
1138
+ if (premiseCands.length > 0 || contextCands.length > 0) {
1139
+ traceEntries.push({
1140
+ node: "discovery",
1141
+ detail: `+ Premise → ${premiseCands.length}, Context → ${contextCands.length}, merged to ${allStrategies.length} candidate(s)`,
1142
+ });
993
1143
  }
994
1144
  return {
995
1145
  hydeEmbeddings: hydeEmbeddings,
996
- candidates: filterByTarget(withPremises),
1146
+ candidates: filterByTarget(allStrategies),
997
1147
  trace: traceEntries,
998
1148
  };
999
1149
  }