@indexnetwork/protocol 0.1.0 → 0.2.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/README.md +113 -0
- package/dist/agents/chat.agent.js +2 -2
- package/dist/agents/chat.agent.js.map +1 -1
- package/dist/agents/chat.prompt.d.ts.map +1 -1
- package/dist/agents/chat.prompt.js +44 -32
- package/dist/agents/chat.prompt.js.map +1 -1
- package/dist/agents/chat.prompt.modules.js +16 -16
- package/dist/agents/chat.prompt.modules.js.map +1 -1
- package/dist/agents/negotiation.proposer.d.ts +1 -1
- package/dist/agents/negotiation.proposer.d.ts.map +1 -1
- package/dist/agents/negotiation.responder.d.ts +1 -1
- package/dist/agents/negotiation.responder.d.ts.map +1 -1
- package/dist/agents/opportunity.evaluator.d.ts +1 -1
- package/dist/agents/opportunity.evaluator.d.ts.map +1 -1
- package/dist/agents/opportunity.evaluator.js +2 -2
- package/dist/agents/opportunity.evaluator.js.map +1 -1
- package/dist/agents/opportunity.presenter.js +1 -1
- package/dist/agents/opportunity.presenter.js.map +1 -1
- package/dist/graphs/chat.graph.d.ts +9 -9
- package/dist/graphs/chat.graph.d.ts.map +1 -1
- package/dist/graphs/chat.graph.js +2 -2
- package/dist/graphs/chat.graph.js.map +1 -1
- package/dist/graphs/home.graph.d.ts +5 -5
- package/dist/graphs/home.graph.d.ts.map +1 -1
- package/dist/graphs/home.graph.js +2 -2
- package/dist/graphs/home.graph.js.map +1 -1
- package/dist/graphs/intent.graph.d.ts +21 -21
- package/dist/graphs/intent.graph.d.ts.map +1 -1
- package/dist/graphs/intent.graph.js +8 -8
- package/dist/graphs/intent.graph.js.map +1 -1
- package/dist/graphs/intent_network.graph.d.ts +398 -0
- package/dist/graphs/intent_network.graph.d.ts.map +1 -0
- package/dist/graphs/intent_network.graph.js +345 -0
- package/dist/graphs/intent_network.graph.js.map +1 -0
- package/dist/graphs/negotiation.graph.d.ts +27 -27
- package/dist/graphs/negotiation.graph.d.ts.map +1 -1
- package/dist/graphs/negotiation.graph.js +2 -2
- package/dist/graphs/negotiation.graph.js.map +1 -1
- package/dist/graphs/network.graph.d.ts +620 -0
- package/dist/graphs/network.graph.d.ts.map +1 -0
- package/dist/graphs/network.graph.js +226 -0
- package/dist/graphs/network.graph.js.map +1 -0
- package/dist/graphs/network_membership.graph.d.ts +250 -0
- package/dist/graphs/network_membership.graph.d.ts.map +1 -0
- package/dist/graphs/network_membership.graph.js +204 -0
- package/dist/graphs/network_membership.graph.js.map +1 -0
- package/dist/graphs/opportunity.graph.d.ts +33 -33
- package/dist/graphs/opportunity.graph.d.ts.map +1 -1
- package/dist/graphs/opportunity.graph.js +161 -144
- package/dist/graphs/opportunity.graph.js.map +1 -1
- package/dist/graphs/profile.graph.js +4 -4
- package/dist/graphs/profile.graph.js.map +1 -1
- package/dist/graphs/tests/chat.graph.mocks.d.ts +14 -14
- package/dist/graphs/tests/chat.graph.mocks.d.ts.map +1 -1
- package/dist/graphs/tests/chat.graph.mocks.js +23 -23
- package/dist/graphs/tests/chat.graph.mocks.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/interfaces/database.interface.d.ts +111 -111
- package/dist/interfaces/database.interface.d.ts.map +1 -1
- package/dist/interfaces/embedder.interface.d.ts +1 -1
- package/dist/interfaces/embedder.interface.d.ts.map +1 -1
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/mcp.server.js.map +1 -1
- package/dist/states/chat.state.d.ts +2 -2
- package/dist/states/chat.state.js +2 -2
- package/dist/states/chat.state.js.map +1 -1
- package/dist/states/home.state.d.ts +1 -1
- package/dist/states/home.state.js +1 -1
- package/dist/states/home.state.js.map +1 -1
- package/dist/states/intent.state.d.ts +4 -4
- package/dist/states/intent.state.d.ts.map +1 -1
- package/dist/states/intent.state.js +1 -1
- package/dist/states/intent.state.js.map +1 -1
- package/dist/states/intent_network.state.d.ts +148 -0
- package/dist/states/intent_network.state.d.ts.map +1 -0
- package/dist/states/intent_network.state.js +100 -0
- package/dist/states/intent_network.state.js.map +1 -0
- package/dist/states/negotiation.state.d.ts +4 -4
- package/dist/states/negotiation.state.d.ts.map +1 -1
- package/dist/states/negotiation.state.js +1 -1
- package/dist/states/negotiation.state.js.map +1 -1
- package/dist/states/network.state.d.ts +179 -0
- package/dist/states/network.state.d.ts.map +1 -0
- package/dist/states/network.state.js +56 -0
- package/dist/states/network.state.js.map +1 -0
- package/dist/states/network_membership.state.d.ts +77 -0
- package/dist/states/network_membership.state.d.ts.map +1 -0
- package/dist/states/network_membership.state.js +43 -0
- package/dist/states/network_membership.state.js.map +1 -0
- package/dist/states/opportunity.state.d.ts +15 -15
- package/dist/states/opportunity.state.d.ts.map +1 -1
- package/dist/states/opportunity.state.js +6 -6
- package/dist/states/opportunity.state.js.map +1 -1
- package/dist/streamers/chat.streamer.d.ts +2 -2
- package/dist/streamers/chat.streamer.d.ts.map +1 -1
- package/dist/streamers/chat.streamer.js +6 -6
- package/dist/streamers/chat.streamer.js.map +1 -1
- package/dist/support/opportunity.discover.js +2 -2
- package/dist/support/opportunity.discover.js.map +1 -1
- package/dist/support/opportunity.enricher.js +2 -2
- package/dist/support/opportunity.enricher.js.map +1 -1
- package/dist/support/protocol.logger.d.ts +1 -1
- package/dist/support/protocol.logger.d.ts.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +21 -17
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/intent.tools.js +67 -67
- package/dist/tools/intent.tools.js.map +1 -1
- package/dist/tools/network.tools.d.ts +3 -0
- package/dist/tools/network.tools.d.ts.map +1 -0
- package/dist/tools/network.tools.js +423 -0
- package/dist/tools/network.tools.js.map +1 -0
- package/dist/tools/opportunity.tools.d.ts.map +1 -1
- package/dist/tools/opportunity.tools.js +51 -44
- package/dist/tools/opportunity.tools.js.map +1 -1
- package/dist/tools/profile.tools.js +21 -21
- package/dist/tools/profile.tools.js.map +1 -1
- package/dist/tools/tool.helpers.d.ts +10 -10
- package/dist/tools/tool.helpers.d.ts.map +1 -1
- package/dist/tools/tool.helpers.js +16 -16
- package/dist/tools/tool.helpers.js.map +1 -1
- package/dist/tools/tool.registry.js +3 -3
- package/dist/tools/tool.registry.js.map +1 -1
- package/dist/tools/utility.tools.js +3 -3
- package/package.json +4 -2
|
@@ -113,18 +113,18 @@ export class OpportunityGraphFactory {
|
|
|
113
113
|
const prepNode = withNodeTrace("opportunity-prep", async (state) => timed("OpportunityGraph.prep", async () => withCallLogging(logger, '[Graph:Prep] prepNode', {
|
|
114
114
|
userId: state.userId,
|
|
115
115
|
hasSearchQuery: !!state.searchQuery,
|
|
116
|
-
requestedIndexId: state.
|
|
116
|
+
requestedIndexId: state.networkId ?? undefined,
|
|
117
117
|
}, async () => {
|
|
118
|
-
// Use
|
|
118
|
+
// Use getNetworkMemberships (all memberships) for search scope — NOT getUserIndexIds
|
|
119
119
|
// (which filters by autoAssign=true and is intended only for intent assignment).
|
|
120
|
-
const memberships = await this.database.
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
logger.verbose('[Graph:Prep] User has no
|
|
120
|
+
const memberships = await this.database.getNetworkMemberships(state.userId);
|
|
121
|
+
const userNetworkIds = memberships.map(m => m.networkId);
|
|
122
|
+
if (userNetworkIds.length === 0) {
|
|
123
|
+
logger.verbose('[Graph:Prep] User has no network memberships - cannot find opportunities');
|
|
124
124
|
return {
|
|
125
|
-
|
|
125
|
+
userNetworks: [],
|
|
126
126
|
sourceProfile: null,
|
|
127
|
-
error: 'You need to join at least one
|
|
127
|
+
error: 'You need to join at least one network to find opportunities.',
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
const discoveryUserId = state.onBehalfOfUserId ?? state.userId;
|
|
@@ -147,12 +147,12 @@ export class OpportunityGraphFactory {
|
|
|
147
147
|
}
|
|
148
148
|
: null;
|
|
149
149
|
return {
|
|
150
|
-
|
|
150
|
+
userNetworks: userNetworkIds,
|
|
151
151
|
indexedIntents,
|
|
152
152
|
sourceProfile,
|
|
153
153
|
trace: [{
|
|
154
154
|
node: "prep",
|
|
155
|
-
detail: `${
|
|
155
|
+
detail: `${userNetworkIds.length} network(s), ${intents.length} intent(s), ${profile ? 'profile loaded' : 'no profile'}`,
|
|
156
156
|
}],
|
|
157
157
|
};
|
|
158
158
|
}, { context: { userId: state.userId }, logOutput: true }).catch((error) => {
|
|
@@ -170,47 +170,49 @@ export class OpportunityGraphFactory {
|
|
|
170
170
|
const r = result;
|
|
171
171
|
if (r?.error)
|
|
172
172
|
return `error: ${r.error}`;
|
|
173
|
-
const indexes = r?.
|
|
173
|
+
const indexes = r?.userNetworks;
|
|
174
174
|
const intents = r?.indexedIntents;
|
|
175
175
|
return indexes && intents ? `${indexes.length} index(es), ${intents.length} intent(s)` : undefined;
|
|
176
176
|
});
|
|
177
177
|
/**
|
|
178
178
|
* Node 1: Scope
|
|
179
179
|
* Determines which indexes to search within.
|
|
180
|
-
* If
|
|
180
|
+
* If networkId provided: searches only that index.
|
|
181
181
|
* Otherwise: searches all user's indexes.
|
|
182
182
|
*/
|
|
183
183
|
const scopeNode = withNodeTrace("opportunity-scope", async (state) => {
|
|
184
184
|
return timed("OpportunityGraph.scope", async () => {
|
|
185
185
|
logger.verbose('[Graph:Scope] Determining search scope', {
|
|
186
|
-
requestedIndexId: state.
|
|
187
|
-
|
|
186
|
+
requestedIndexId: state.networkId,
|
|
187
|
+
userNetworksCount: state.userNetworks.length,
|
|
188
188
|
});
|
|
189
189
|
try {
|
|
190
190
|
let targetIndexIds;
|
|
191
|
-
if (state.
|
|
192
|
-
// Validate user is member of requested
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
if (state.networkId) {
|
|
192
|
+
// Validate user is member or owner of requested network
|
|
193
|
+
const isInScope = state.userNetworks.includes(state.networkId);
|
|
194
|
+
const isOwner = !isInScope && await this.database.isIndexOwner(state.networkId, state.userId);
|
|
195
|
+
if (!isInScope && !isOwner) {
|
|
196
|
+
logger.warn('[Graph:Scope] User not member of requested network', {
|
|
197
|
+
networkId: state.networkId,
|
|
196
198
|
});
|
|
197
199
|
return {
|
|
198
200
|
targetIndexes: [],
|
|
199
|
-
error: 'You are not a member of that
|
|
201
|
+
error: 'You are not a member of that network.',
|
|
200
202
|
};
|
|
201
203
|
}
|
|
202
|
-
targetIndexIds = [state.
|
|
204
|
+
targetIndexIds = [state.networkId];
|
|
203
205
|
}
|
|
204
206
|
else {
|
|
205
207
|
// Search all user's indexes
|
|
206
|
-
targetIndexIds = state.
|
|
208
|
+
targetIndexIds = state.userNetworks;
|
|
207
209
|
}
|
|
208
210
|
// Fetch index details
|
|
209
|
-
const targetIndexes = await Promise.all(targetIndexIds.map(async (
|
|
210
|
-
const index = await this.database.getIndex(
|
|
211
|
-
const memberCount = await this.database.getIndexMemberCount(
|
|
211
|
+
const targetIndexes = await Promise.all(targetIndexIds.map(async (networkId) => {
|
|
212
|
+
const index = await this.database.getIndex(networkId);
|
|
213
|
+
const memberCount = await this.database.getIndexMemberCount(networkId);
|
|
212
214
|
return {
|
|
213
|
-
|
|
215
|
+
networkId,
|
|
214
216
|
title: index?.title ?? 'Unknown',
|
|
215
217
|
memberCount,
|
|
216
218
|
};
|
|
@@ -225,9 +227,9 @@ export class OpportunityGraphFactory {
|
|
|
225
227
|
// Background path: look up persisted scores from intent_indexes
|
|
226
228
|
try {
|
|
227
229
|
const scores = await this.database.getIntentIndexScores(state.triggerIntentId);
|
|
228
|
-
for (const {
|
|
230
|
+
for (const { networkId, relevancyScore } of scores) {
|
|
229
231
|
if (relevancyScore != null) {
|
|
230
|
-
indexRelevancyScores[
|
|
232
|
+
indexRelevancyScores[networkId] = relevancyScore;
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
}
|
|
@@ -242,32 +244,35 @@ export class OpportunityGraphFactory {
|
|
|
242
244
|
const scopeAgentTimings = [];
|
|
243
245
|
const scorableIndexes = targetIndexes.filter(ti => ti.title !== 'Unknown');
|
|
244
246
|
const scoringPromises = scorableIndexes.map(async (ti) => {
|
|
247
|
+
const ctx = await this.database.getIndexMemberContext(ti.networkId, state.userId);
|
|
248
|
+
if (!ctx?.indexPrompt?.trim() && !ctx?.memberPrompt?.trim()) {
|
|
249
|
+
return { networkId: ti.networkId, score: 1.0 };
|
|
250
|
+
}
|
|
251
|
+
const _indexerStart = Date.now();
|
|
252
|
+
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
253
|
+
traceEmitter?.({ type: "agent_start", name: "intent-indexer" });
|
|
254
|
+
let result = null;
|
|
245
255
|
try {
|
|
246
|
-
|
|
247
|
-
if (!ctx?.indexPrompt?.trim() && !ctx?.memberPrompt?.trim()) {
|
|
248
|
-
return { indexId: ti.indexId, score: 1.0 };
|
|
249
|
-
}
|
|
250
|
-
const _indexerStart = Date.now();
|
|
251
|
-
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
252
|
-
traceEmitter?.({ type: "agent_start", name: "intent-indexer" });
|
|
253
|
-
const result = await indexer.invoke(state.searchQuery, ctx?.indexPrompt ?? null, ctx?.memberPrompt ?? null);
|
|
254
|
-
const _indexerDuration = Date.now() - _indexerStart;
|
|
255
|
-
traceEmitter?.({ type: "agent_end", name: "intent-indexer", durationMs: _indexerDuration, summary: `Scored index ${ti.indexId}` });
|
|
256
|
-
scopeAgentTimings.push({ name: 'intent.indexer', durationMs: _indexerDuration });
|
|
257
|
-
if (!result)
|
|
258
|
-
return { indexId: ti.indexId, score: 1.0 };
|
|
259
|
-
const score = ctx?.indexPrompt && ctx?.memberPrompt
|
|
260
|
-
? result.indexScore * 0.6 + result.memberScore * 0.4
|
|
261
|
-
: ctx?.indexPrompt ? result.indexScore : result.memberScore;
|
|
262
|
-
return { indexId: ti.indexId, score };
|
|
256
|
+
result = await indexer.invoke(state.searchQuery, ctx?.indexPrompt ?? null, ctx?.memberPrompt ?? null);
|
|
263
257
|
}
|
|
264
258
|
catch {
|
|
265
|
-
return {
|
|
259
|
+
return { networkId: ti.networkId, score: 1.0 };
|
|
266
260
|
}
|
|
261
|
+
finally {
|
|
262
|
+
const _indexerDuration = Date.now() - _indexerStart;
|
|
263
|
+
traceEmitter?.({ type: "agent_end", name: "intent-indexer", durationMs: _indexerDuration, summary: `Scored index ${ti.networkId}` });
|
|
264
|
+
scopeAgentTimings.push({ name: 'intent.indexer', durationMs: _indexerDuration });
|
|
265
|
+
}
|
|
266
|
+
if (!result)
|
|
267
|
+
return { networkId: ti.networkId, score: 1.0 };
|
|
268
|
+
const score = ctx?.indexPrompt && ctx?.memberPrompt
|
|
269
|
+
? result.indexScore * 0.6 + result.memberScore * 0.4
|
|
270
|
+
: ctx?.indexPrompt ? result.indexScore : result.memberScore;
|
|
271
|
+
return { networkId: ti.networkId, score };
|
|
267
272
|
});
|
|
268
273
|
const results = await Promise.all(scoringPromises);
|
|
269
|
-
for (const {
|
|
270
|
-
indexRelevancyScores[
|
|
274
|
+
for (const { networkId, score } of results) {
|
|
275
|
+
indexRelevancyScores[networkId] = score;
|
|
271
276
|
}
|
|
272
277
|
// Accumulate indexer timings into graph state
|
|
273
278
|
if (scopeAgentTimings.length > 0) {
|
|
@@ -331,12 +336,12 @@ export class OpportunityGraphFactory {
|
|
|
331
336
|
hasSearchQuery: !!state.searchQuery,
|
|
332
337
|
indexedIntentsCount: state.indexedIntents.length,
|
|
333
338
|
});
|
|
334
|
-
const targetIndexIds = state.targetIndexes.map((t) => t.
|
|
339
|
+
const targetIndexIds = state.targetIndexes.map((t) => t.networkId);
|
|
335
340
|
try {
|
|
336
341
|
let resolvedIntentId;
|
|
337
342
|
if (state.triggerIntentId) {
|
|
338
|
-
const
|
|
339
|
-
const inTarget =
|
|
343
|
+
const inNetwork = await this.database.getNetworkIdsForIntent(state.triggerIntentId);
|
|
344
|
+
const inTarget = inNetwork.some((id) => targetIndexIds.includes(id));
|
|
340
345
|
resolvedIntentId = state.triggerIntentId;
|
|
341
346
|
const resolvedIntentInIndex = inTarget;
|
|
342
347
|
const discoverySource = resolvedIntentInIndex ? 'intent' : 'profile';
|
|
@@ -351,8 +356,8 @@ export class OpportunityGraphFactory {
|
|
|
351
356
|
const matched = state.indexedIntents.find((i) => i.payload?.toLowerCase().includes(q));
|
|
352
357
|
if (matched) {
|
|
353
358
|
resolvedIntentId = matched.intentId;
|
|
354
|
-
const
|
|
355
|
-
const resolvedIntentInIndex =
|
|
359
|
+
const inNetwork = await this.database.getNetworkIdsForIntent(matched.intentId);
|
|
360
|
+
const resolvedIntentInIndex = inNetwork.some((id) => targetIndexIds.includes(id));
|
|
356
361
|
const discoverySource = resolvedIntentInIndex ? 'intent' : 'profile';
|
|
357
362
|
return {
|
|
358
363
|
resolvedTriggerIntentId: resolvedIntentId,
|
|
@@ -452,15 +457,15 @@ export class OpportunityGraphFactory {
|
|
|
452
457
|
logger.verbose('[Graph:Discovery] Direct-connection mode — bypassing vector search', {
|
|
453
458
|
targetUserId: state.targetUserId,
|
|
454
459
|
});
|
|
455
|
-
const targetMemberships = await this.database.
|
|
456
|
-
const targetUserIndexIds = targetMemberships.map(m => m.
|
|
460
|
+
const targetMemberships = await this.database.getNetworkMemberships(state.targetUserId);
|
|
461
|
+
const targetUserIndexIds = targetMemberships.map(m => m.networkId);
|
|
457
462
|
const sharedIndexIds = state.targetIndexes
|
|
458
|
-
.filter(ti => targetUserIndexIds.includes(ti.
|
|
459
|
-
.map(ti => ti.
|
|
463
|
+
.filter(ti => targetUserIndexIds.includes(ti.networkId))
|
|
464
|
+
.map(ti => ti.networkId);
|
|
460
465
|
if (sharedIndexIds.length === 0) {
|
|
461
466
|
logger.warn('[Graph:Discovery] Target user shares no indexes with discoverer', {
|
|
462
467
|
targetUserId: state.targetUserId,
|
|
463
|
-
discovererIndexes: state.targetIndexes.map(ti => ti.
|
|
468
|
+
discovererIndexes: state.targetIndexes.map(ti => ti.networkId),
|
|
464
469
|
});
|
|
465
470
|
return {
|
|
466
471
|
candidates: [],
|
|
@@ -477,13 +482,13 @@ export class OpportunityGraphFactory {
|
|
|
477
482
|
if (targetIntents.length > 0) {
|
|
478
483
|
// Build one candidate per intent per shared index it belongs to
|
|
479
484
|
for (const intent of targetIntents) {
|
|
480
|
-
const
|
|
481
|
-
const overlapping = sharedIndexIds.filter(id =>
|
|
482
|
-
for (const
|
|
485
|
+
const intentNetworkIds = await this.database.getNetworkIdsForIntent(intent.id);
|
|
486
|
+
const overlapping = sharedIndexIds.filter(id => intentNetworkIds.includes(id));
|
|
487
|
+
for (const networkId of overlapping) {
|
|
483
488
|
directCandidates.push({
|
|
484
489
|
candidateUserId: state.targetUserId,
|
|
485
490
|
candidateIntentId: intent.id,
|
|
486
|
-
|
|
491
|
+
networkId,
|
|
487
492
|
similarity: 1.0,
|
|
488
493
|
lens: 'explicit_mention',
|
|
489
494
|
candidatePayload: intent.payload,
|
|
@@ -498,7 +503,7 @@ export class OpportunityGraphFactory {
|
|
|
498
503
|
directCandidates.push({
|
|
499
504
|
candidateUserId: state.targetUserId,
|
|
500
505
|
candidateIntentId: undefined,
|
|
501
|
-
|
|
506
|
+
networkId: sharedIndexIds[0],
|
|
502
507
|
similarity: 1.0,
|
|
503
508
|
lens: 'explicit_mention',
|
|
504
509
|
candidatePayload: '',
|
|
@@ -604,7 +609,7 @@ export class OpportunityGraphFactory {
|
|
|
604
609
|
const profileCandidates = [];
|
|
605
610
|
for (const targetIndex of state.targetIndexes) {
|
|
606
611
|
const results = await this.embedder.searchWithProfileEmbedding(vector, {
|
|
607
|
-
indexScope: [targetIndex.
|
|
612
|
+
indexScope: [targetIndex.networkId],
|
|
608
613
|
excludeUserId: discoveryUserId,
|
|
609
614
|
limitPerStrategy: Math.floor(limitPerStrategy / 2),
|
|
610
615
|
limit: Math.floor(perIndexLimit / 2),
|
|
@@ -614,7 +619,7 @@ export class OpportunityGraphFactory {
|
|
|
614
619
|
profileCandidates.push({
|
|
615
620
|
candidateUserId: result.userId,
|
|
616
621
|
candidateIntentId: result.type === 'intent' ? result.id : undefined,
|
|
617
|
-
|
|
622
|
+
networkId: targetIndex.networkId,
|
|
618
623
|
similarity: result.score,
|
|
619
624
|
lens: result.matchedVia,
|
|
620
625
|
candidatePayload: '',
|
|
@@ -626,7 +631,7 @@ export class OpportunityGraphFactory {
|
|
|
626
631
|
// Merge and dedupe - keep both intent and profile candidates per user
|
|
627
632
|
const byKey = new Map();
|
|
628
633
|
for (const c of [...queryCandidates, ...profileCandidates]) {
|
|
629
|
-
const key = `${c.candidateUserId}:${c.
|
|
634
|
+
const key = `${c.candidateUserId}:${c.networkId}:${c.candidateIntentId ?? 'profile'}:${c.discoverySource ?? 'unknown'}`;
|
|
630
635
|
if (!byKey.has(key) || c.similarity > (byKey.get(key)?.similarity ?? 0)) {
|
|
631
636
|
byKey.set(key, c);
|
|
632
637
|
}
|
|
@@ -656,7 +661,7 @@ export class OpportunityGraphFactory {
|
|
|
656
661
|
const allCandidates = [];
|
|
657
662
|
for (const targetIndex of state.targetIndexes) {
|
|
658
663
|
const results = await this.embedder.searchWithProfileEmbedding(vector, {
|
|
659
|
-
indexScope: [targetIndex.
|
|
664
|
+
indexScope: [targetIndex.networkId],
|
|
660
665
|
excludeUserId: discoveryUserId,
|
|
661
666
|
limitPerStrategy,
|
|
662
667
|
limit: perIndexLimit,
|
|
@@ -667,7 +672,7 @@ export class OpportunityGraphFactory {
|
|
|
667
672
|
allCandidates.push({
|
|
668
673
|
candidateUserId: result.userId,
|
|
669
674
|
candidateIntentId: result.id,
|
|
670
|
-
|
|
675
|
+
networkId: targetIndex.networkId,
|
|
671
676
|
similarity: result.score,
|
|
672
677
|
lens: result.matchedVia,
|
|
673
678
|
candidatePayload: '',
|
|
@@ -678,7 +683,7 @@ export class OpportunityGraphFactory {
|
|
|
678
683
|
else {
|
|
679
684
|
allCandidates.push({
|
|
680
685
|
candidateUserId: result.userId,
|
|
681
|
-
|
|
686
|
+
networkId: targetIndex.networkId,
|
|
682
687
|
similarity: result.score,
|
|
683
688
|
lens: result.matchedVia,
|
|
684
689
|
candidatePayload: '',
|
|
@@ -690,7 +695,7 @@ export class OpportunityGraphFactory {
|
|
|
690
695
|
}
|
|
691
696
|
const byUserAndIndex = new Map();
|
|
692
697
|
for (const c of allCandidates) {
|
|
693
|
-
const key = `${c.candidateUserId}:${c.
|
|
698
|
+
const key = `${c.candidateUserId}:${c.networkId}:${c.candidateIntentId ?? 'profile'}`;
|
|
694
699
|
if (!byUserAndIndex.has(key) || c.similarity > (byUserAndIndex.get(key)?.similarity ?? 0)) {
|
|
695
700
|
byUserAndIndex.set(key, c);
|
|
696
701
|
}
|
|
@@ -793,7 +798,7 @@ export class OpportunityGraphFactory {
|
|
|
793
798
|
const all = [];
|
|
794
799
|
await Promise.all(state.targetIndexes.map(async (targetIndex) => {
|
|
795
800
|
const results = await self.embedder.searchWithHydeEmbeddings(lensEmbeddings, {
|
|
796
|
-
indexScope: [targetIndex.
|
|
801
|
+
indexScope: [targetIndex.networkId],
|
|
797
802
|
excludeUserId: discoveryUserId,
|
|
798
803
|
limitPerStrategy,
|
|
799
804
|
limit: perIndexLimit,
|
|
@@ -803,7 +808,7 @@ export class OpportunityGraphFactory {
|
|
|
803
808
|
all.push({
|
|
804
809
|
candidateUserId: r.userId,
|
|
805
810
|
candidateIntentId: r.id,
|
|
806
|
-
|
|
811
|
+
networkId: targetIndex.networkId,
|
|
807
812
|
similarity: r.score,
|
|
808
813
|
lens: r.matchedVia,
|
|
809
814
|
candidatePayload: '',
|
|
@@ -814,7 +819,7 @@ export class OpportunityGraphFactory {
|
|
|
814
819
|
for (const r of results.filter((x) => x.type === 'profile')) {
|
|
815
820
|
all.push({
|
|
816
821
|
candidateUserId: r.userId,
|
|
817
|
-
|
|
822
|
+
networkId: targetIndex.networkId,
|
|
818
823
|
similarity: r.score,
|
|
819
824
|
lens: r.matchedVia,
|
|
820
825
|
candidatePayload: '',
|
|
@@ -876,7 +881,7 @@ export class OpportunityGraphFactory {
|
|
|
876
881
|
const allCandidates = [];
|
|
877
882
|
await Promise.all(state.targetIndexes.map(async (targetIndex) => {
|
|
878
883
|
const results = await this.embedder.searchWithHydeEmbeddings(lensEmbeddings, {
|
|
879
|
-
indexScope: [targetIndex.
|
|
884
|
+
indexScope: [targetIndex.networkId],
|
|
880
885
|
excludeUserId: discoveryUserId,
|
|
881
886
|
limitPerStrategy,
|
|
882
887
|
limit: perIndexLimit,
|
|
@@ -886,7 +891,7 @@ export class OpportunityGraphFactory {
|
|
|
886
891
|
allCandidates.push({
|
|
887
892
|
candidateUserId: result.userId,
|
|
888
893
|
candidateIntentId: result.id,
|
|
889
|
-
|
|
894
|
+
networkId: targetIndex.networkId,
|
|
890
895
|
similarity: result.score,
|
|
891
896
|
lens: result.matchedVia,
|
|
892
897
|
candidatePayload: '',
|
|
@@ -897,7 +902,7 @@ export class OpportunityGraphFactory {
|
|
|
897
902
|
for (const result of results.filter((r) => r.type === 'profile')) {
|
|
898
903
|
allCandidates.push({
|
|
899
904
|
candidateUserId: result.userId,
|
|
900
|
-
|
|
905
|
+
networkId: targetIndex.networkId,
|
|
901
906
|
similarity: result.score,
|
|
902
907
|
lens: result.matchedVia,
|
|
903
908
|
candidatePayload: '',
|
|
@@ -908,7 +913,7 @@ export class OpportunityGraphFactory {
|
|
|
908
913
|
}));
|
|
909
914
|
const byUserAndIndex = new Map();
|
|
910
915
|
for (const c of allCandidates) {
|
|
911
|
-
const key = `${c.candidateUserId}:${c.
|
|
916
|
+
const key = `${c.candidateUserId}:${c.networkId}:${c.candidateIntentId ?? 'profile'}`;
|
|
912
917
|
if (!byUserAndIndex.has(key) || c.similarity > (byUserAndIndex.get(key)?.similarity ?? 0)) {
|
|
913
918
|
byUserAndIndex.set(key, c);
|
|
914
919
|
}
|
|
@@ -1017,7 +1022,7 @@ export class OpportunityGraphFactory {
|
|
|
1017
1022
|
});
|
|
1018
1023
|
/**
|
|
1019
1024
|
* Node 3: Evaluation (Entity bundle)
|
|
1020
|
-
* Builds entity bundle from source + candidates, invokes entity-bundle evaluator, maps to EvaluatedOpportunity with
|
|
1025
|
+
* Builds entity bundle from source + candidates, invokes entity-bundle evaluator, maps to EvaluatedOpportunity with networkId from entities.
|
|
1021
1026
|
*/
|
|
1022
1027
|
const evaluationNode = async (state) => {
|
|
1023
1028
|
return timed("OpportunityGraph.evaluation", async () => {
|
|
@@ -1045,8 +1050,8 @@ export class OpportunityGraphFactory {
|
|
|
1045
1050
|
}
|
|
1046
1051
|
else if (c.similarity === existing.similarity) {
|
|
1047
1052
|
// Tie-break: prefer index with higher relevancy score
|
|
1048
|
-
const cScore = state.indexRelevancyScores[c.
|
|
1049
|
-
const existingScore = state.indexRelevancyScores[existing.
|
|
1053
|
+
const cScore = state.indexRelevancyScores[c.networkId] ?? 0;
|
|
1054
|
+
const existingScore = state.indexRelevancyScores[existing.networkId] ?? 0;
|
|
1050
1055
|
if (cScore > existingScore) {
|
|
1051
1056
|
bestByUser.set(c.candidateUserId, c);
|
|
1052
1057
|
}
|
|
@@ -1100,7 +1105,7 @@ export class OpportunityGraphFactory {
|
|
|
1100
1105
|
payload: i.payload,
|
|
1101
1106
|
summary: i.summary,
|
|
1102
1107
|
})),
|
|
1103
|
-
|
|
1108
|
+
networkId: '', // Placeholder — overwritten per-pairing below
|
|
1104
1109
|
ragScore: undefined,
|
|
1105
1110
|
matchedVia: undefined,
|
|
1106
1111
|
};
|
|
@@ -1128,7 +1133,7 @@ export class OpportunityGraphFactory {
|
|
|
1128
1133
|
intents: c.candidateIntentId != null
|
|
1129
1134
|
? [{ intentId: c.candidateIntentId, payload: intentPayload ?? '', summary: intentSummary }]
|
|
1130
1135
|
: undefined,
|
|
1131
|
-
|
|
1136
|
+
networkId: c.networkId,
|
|
1132
1137
|
ragScore: c.similarity * 100,
|
|
1133
1138
|
matchedVia: c.lens,
|
|
1134
1139
|
};
|
|
@@ -1136,7 +1141,7 @@ export class OpportunityGraphFactory {
|
|
|
1136
1141
|
const userIdToIndexId = new Map();
|
|
1137
1142
|
for (const e of candidateEntities) {
|
|
1138
1143
|
if (!userIdToIndexId.has(e.userId))
|
|
1139
|
-
userIdToIndexId.set(e.userId, e.
|
|
1144
|
+
userIdToIndexId.set(e.userId, e.networkId);
|
|
1140
1145
|
}
|
|
1141
1146
|
// Lower default threshold to 50 for better recall
|
|
1142
1147
|
const minScore = state.options.minScore ?? 50;
|
|
@@ -1297,23 +1302,23 @@ export class OpportunityGraphFactory {
|
|
|
1297
1302
|
actors: op.actors.map((a) => {
|
|
1298
1303
|
const isSource = a.userId === discoveryUserId;
|
|
1299
1304
|
if (isSource) {
|
|
1300
|
-
// Source actor inherits the counterpart's
|
|
1305
|
+
// Source actor inherits the counterpart's networkId (shared match context)
|
|
1301
1306
|
const counterpart = op.actors.find((other) => other.userId !== a.userId);
|
|
1302
1307
|
const counterpartIndexId = counterpart
|
|
1303
|
-
? userIdToIndexId.get(counterpart.userId) ?? candidateEntities.find((e) => e.userId === counterpart.userId)?.
|
|
1308
|
+
? userIdToIndexId.get(counterpart.userId) ?? candidateEntities.find((e) => e.userId === counterpart.userId)?.networkId
|
|
1304
1309
|
: undefined;
|
|
1305
1310
|
return {
|
|
1306
1311
|
userId: a.userId,
|
|
1307
1312
|
role: a.role,
|
|
1308
1313
|
intentId: a.intentId,
|
|
1309
|
-
|
|
1314
|
+
networkId: counterpartIndexId ?? userIdToIndexId.get(a.userId) ?? '',
|
|
1310
1315
|
};
|
|
1311
1316
|
}
|
|
1312
1317
|
return {
|
|
1313
1318
|
userId: a.userId,
|
|
1314
1319
|
role: a.role,
|
|
1315
1320
|
intentId: a.intentId,
|
|
1316
|
-
|
|
1321
|
+
networkId: userIdToIndexId.get(a.userId) ?? candidateEntities.find((e) => e.userId === a.userId)?.networkId,
|
|
1317
1322
|
};
|
|
1318
1323
|
}),
|
|
1319
1324
|
}));
|
|
@@ -1457,7 +1462,7 @@ export class OpportunityGraphFactory {
|
|
|
1457
1462
|
},
|
|
1458
1463
|
};
|
|
1459
1464
|
// Build candidates with enriched context from database.
|
|
1460
|
-
// Each actor carries its own
|
|
1465
|
+
// Each actor carries its own networkId — use it for per-candidate index context.
|
|
1461
1466
|
const candidateEntries = state.evaluatedOpportunities
|
|
1462
1467
|
.map(opp => {
|
|
1463
1468
|
const candidateActor = opp.actors.find(a => a.userId !== discoveryUserId);
|
|
@@ -1497,7 +1502,7 @@ export class OpportunityGraphFactory {
|
|
|
1497
1502
|
score: opp.score,
|
|
1498
1503
|
reasoning: opp.reasoning,
|
|
1499
1504
|
valencyRole: candidateActor.role ?? 'peer',
|
|
1500
|
-
|
|
1505
|
+
networkId: candidateActor.networkId,
|
|
1501
1506
|
candidateUser: {
|
|
1502
1507
|
id: userId,
|
|
1503
1508
|
intents: candidateIntents,
|
|
@@ -1513,19 +1518,19 @@ export class OpportunityGraphFactory {
|
|
|
1513
1518
|
}));
|
|
1514
1519
|
const isChatPath = !!state.options?.conversationId;
|
|
1515
1520
|
const maxTurns = isChatPath ? 4 : 6;
|
|
1516
|
-
// Fetch per-candidate index context (group by
|
|
1517
|
-
const uniqueIndexIds = [...new Set(candidates.map(c => c.
|
|
1521
|
+
// Fetch per-candidate index context (group by networkId to avoid duplicate lookups)
|
|
1522
|
+
const uniqueIndexIds = [...new Set(candidates.map(c => c.networkId).filter((id) => !!id))];
|
|
1518
1523
|
const indexContextMap = new Map();
|
|
1519
|
-
await Promise.all(uniqueIndexIds.map(async (
|
|
1520
|
-
const ctx = await this.database.getIndexMemberContext(
|
|
1524
|
+
await Promise.all(uniqueIndexIds.map(async (networkId) => {
|
|
1525
|
+
const ctx = await this.database.getIndexMemberContext(networkId, discoveryUserId).catch(() => null);
|
|
1521
1526
|
const prompt = [ctx?.indexPrompt, ctx?.memberPrompt]
|
|
1522
1527
|
.filter((v) => !!v?.trim())
|
|
1523
1528
|
.join('\n\n');
|
|
1524
1529
|
if (prompt)
|
|
1525
|
-
indexContextMap.set(
|
|
1530
|
+
indexContextMap.set(networkId, prompt);
|
|
1526
1531
|
}));
|
|
1527
1532
|
// Run negotiations per candidate with their actual index context
|
|
1528
|
-
const acceptedResults = await negotiateCandidates(this.negotiationGraph, sourceUser, candidates, {
|
|
1533
|
+
const acceptedResults = await negotiateCandidates(this.negotiationGraph, sourceUser, candidates, { networkId: '', prompt: '' }, // base context, overridden per-candidate below
|
|
1529
1534
|
{ maxTurns, traceEmitter: traceEmitter ?? undefined,
|
|
1530
1535
|
indexContextOverrides: indexContextMap });
|
|
1531
1536
|
// Filter opportunities to only those with an opportunity outcome, update scores
|
|
@@ -1563,7 +1568,7 @@ export class OpportunityGraphFactory {
|
|
|
1563
1568
|
const limit = state.options.limit ?? 20;
|
|
1564
1569
|
const ranked = sorted.slice(0, limit);
|
|
1565
1570
|
const actorSetKey = (opp) => opp.actors
|
|
1566
|
-
.map((a) => `${a.userId}:${a.
|
|
1571
|
+
.map((a) => `${a.userId}:${a.networkId}`)
|
|
1567
1572
|
.sort()
|
|
1568
1573
|
.join('|');
|
|
1569
1574
|
const seen = new Set();
|
|
@@ -1610,37 +1615,46 @@ export class OpportunityGraphFactory {
|
|
|
1610
1615
|
return timed("OpportunityGraph.introValidation", async () => {
|
|
1611
1616
|
logger.verbose('[Graph:IntroValidation] Starting', {
|
|
1612
1617
|
userId: state.userId,
|
|
1613
|
-
|
|
1618
|
+
networkId: state.networkId,
|
|
1614
1619
|
entitiesCount: state.introductionEntities?.length ?? 0,
|
|
1615
1620
|
});
|
|
1616
1621
|
try {
|
|
1617
1622
|
const entities = state.introductionEntities ?? [];
|
|
1618
|
-
const
|
|
1623
|
+
const primaryNetworkId = (state.networkId ?? entities[0]?.networkId);
|
|
1619
1624
|
const partyUserIds = [...new Set(entities.map((e) => e.userId).filter((id) => id !== state.userId))];
|
|
1620
|
-
if (!
|
|
1625
|
+
if (!primaryNetworkId || partyUserIds.length < 1) {
|
|
1621
1626
|
return {
|
|
1622
|
-
error: 'Introduction requires
|
|
1627
|
+
error: 'Introduction requires networkId and at least two entities (introducer + one counterpart).',
|
|
1623
1628
|
};
|
|
1624
1629
|
}
|
|
1625
|
-
if (state.
|
|
1630
|
+
if (state.requiredNetworkId && primaryNetworkId !== state.requiredNetworkId) {
|
|
1626
1631
|
return {
|
|
1627
1632
|
error: 'This chat is scoped to a different community. You can only introduce members of the current community.',
|
|
1628
1633
|
};
|
|
1629
1634
|
}
|
|
1630
|
-
const introducerIsMember = await
|
|
1631
|
-
|
|
1635
|
+
const [introducerIsMember, introducerIsOwner] = await Promise.all([
|
|
1636
|
+
this.database.isNetworkMember(primaryNetworkId, state.userId),
|
|
1637
|
+
this.database.isIndexOwner(primaryNetworkId, state.userId),
|
|
1638
|
+
]);
|
|
1639
|
+
if (!introducerIsMember && !introducerIsOwner) {
|
|
1632
1640
|
return {
|
|
1633
|
-
error: 'One or more users are not members of the specified community. You can only introduce members who share
|
|
1641
|
+
error: 'One or more users are not members of the specified community. You can only introduce members who share a network.',
|
|
1634
1642
|
};
|
|
1635
1643
|
}
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1644
|
+
const partyInScope = await Promise.all(partyUserIds.map(async (userId) => {
|
|
1645
|
+
const [isMember, isOwner] = await Promise.all([
|
|
1646
|
+
this.database.isNetworkMember(primaryNetworkId, userId),
|
|
1647
|
+
this.database.isIndexOwner(primaryNetworkId, userId),
|
|
1648
|
+
]);
|
|
1649
|
+
return isMember || isOwner;
|
|
1650
|
+
}));
|
|
1651
|
+
const allPartyMembers = partyInScope.every(Boolean);
|
|
1638
1652
|
if (!allPartyMembers) {
|
|
1639
1653
|
return {
|
|
1640
|
-
error: 'One or more users are not members of the specified community. You can only introduce members who share
|
|
1654
|
+
error: 'One or more users are not members of the specified community. You can only introduce members who share a network.',
|
|
1641
1655
|
};
|
|
1642
1656
|
}
|
|
1643
|
-
const exists = await this.database.opportunityExistsBetweenActors(partyUserIds,
|
|
1657
|
+
const exists = await this.database.opportunityExistsBetweenActors(partyUserIds, primaryNetworkId);
|
|
1644
1658
|
if (exists) {
|
|
1645
1659
|
return { error: 'An opportunity already exists between these people.' };
|
|
1646
1660
|
}
|
|
@@ -1651,7 +1665,7 @@ export class OpportunityGraphFactory {
|
|
|
1651
1665
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1652
1666
|
logger.error('[Graph:IntroValidation] Failed', {
|
|
1653
1667
|
userId: state.userId,
|
|
1654
|
-
|
|
1668
|
+
networkId: state.networkId,
|
|
1655
1669
|
error: err,
|
|
1656
1670
|
});
|
|
1657
1671
|
return {
|
|
@@ -1668,7 +1682,7 @@ export class OpportunityGraphFactory {
|
|
|
1668
1682
|
/**
|
|
1669
1683
|
* Build fallback reasoning and actors when evaluator returns empty or throws.
|
|
1670
1684
|
*/
|
|
1671
|
-
function buildIntroFallback(entities, state,
|
|
1685
|
+
function buildIntroFallback(entities, state, primaryNetworkId, introducerName) {
|
|
1672
1686
|
const reasoning = `${introducerName ?? 'A member'} believes these people should connect.` +
|
|
1673
1687
|
(state.introductionHint ? ` Context: ${state.introductionHint}` : '');
|
|
1674
1688
|
const score = 70;
|
|
@@ -1676,7 +1690,7 @@ export class OpportunityGraphFactory {
|
|
|
1676
1690
|
const actors = partyUserIds.map((uid) => ({
|
|
1677
1691
|
userId: uid,
|
|
1678
1692
|
role: 'peer',
|
|
1679
|
-
|
|
1693
|
+
networkId: primaryNetworkId,
|
|
1680
1694
|
}));
|
|
1681
1695
|
return { reasoning, score, actors };
|
|
1682
1696
|
}
|
|
@@ -1691,9 +1705,9 @@ export class OpportunityGraphFactory {
|
|
|
1691
1705
|
return { evaluatedOpportunities: [], agentTimings: [] };
|
|
1692
1706
|
}
|
|
1693
1707
|
const entities = state.introductionEntities ?? [];
|
|
1694
|
-
const
|
|
1695
|
-
if (!
|
|
1696
|
-
return { evaluatedOpportunities: [], error: 'Missing entities or
|
|
1708
|
+
const primaryNetworkId = (state.networkId ?? entities[0]?.networkId);
|
|
1709
|
+
if (!primaryNetworkId || entities.length < 2) {
|
|
1710
|
+
return { evaluatedOpportunities: [], error: 'Missing entities or network for introduction.', agentTimings: [] };
|
|
1697
1711
|
}
|
|
1698
1712
|
const agentTimingsAccum = [];
|
|
1699
1713
|
let introducerName;
|
|
@@ -1728,11 +1742,11 @@ export class OpportunityGraphFactory {
|
|
|
1728
1742
|
userId: a.userId,
|
|
1729
1743
|
role: a.role,
|
|
1730
1744
|
intentId: a.intentId ?? undefined,
|
|
1731
|
-
|
|
1745
|
+
networkId: primaryNetworkId,
|
|
1732
1746
|
}));
|
|
1733
1747
|
}
|
|
1734
1748
|
else {
|
|
1735
|
-
const fallback = buildIntroFallback(entities, state,
|
|
1749
|
+
const fallback = buildIntroFallback(entities, state, primaryNetworkId, introducerName);
|
|
1736
1750
|
reasoning = fallback.reasoning;
|
|
1737
1751
|
score = fallback.score;
|
|
1738
1752
|
actors = fallback.actors;
|
|
@@ -1747,7 +1761,7 @@ export class OpportunityGraphFactory {
|
|
|
1747
1761
|
agentTimingsAccum.push({ name: 'opportunity.evaluator', durationMs: _introErrDuration });
|
|
1748
1762
|
}
|
|
1749
1763
|
logger.warn('[Graph:IntroEvaluation] Evaluator or getUser failed, using fallback', { error: evalErr });
|
|
1750
|
-
const fallback = buildIntroFallback(entities, state,
|
|
1764
|
+
const fallback = buildIntroFallback(entities, state, primaryNetworkId, introducerName);
|
|
1751
1765
|
reasoning = fallback.reasoning;
|
|
1752
1766
|
score = fallback.score;
|
|
1753
1767
|
actors = fallback.actors;
|
|
@@ -1778,7 +1792,7 @@ export class OpportunityGraphFactory {
|
|
|
1778
1792
|
};
|
|
1779
1793
|
/**
|
|
1780
1794
|
* Node 5: Persist
|
|
1781
|
-
* Creates opportunities from evaluator-proposed actors (
|
|
1795
|
+
* Creates opportunities from evaluator-proposed actors (networkId, userId, role, optional intent).
|
|
1782
1796
|
*/
|
|
1783
1797
|
const persistNode = withNodeTrace("opportunity-persist", async (state) => {
|
|
1784
1798
|
return timed("OpportunityGraph.persist", async () => {
|
|
@@ -1806,18 +1820,18 @@ export class OpportunityGraphFactory {
|
|
|
1806
1820
|
? await this.database.getUser(state.userId)
|
|
1807
1821
|
: null;
|
|
1808
1822
|
for (const evaluated of state.evaluatedOpportunities) {
|
|
1809
|
-
const indexIdForActors = state.
|
|
1823
|
+
const indexIdForActors = state.networkId ?? evaluated.actors[0]?.networkId;
|
|
1810
1824
|
let actors;
|
|
1811
1825
|
let data;
|
|
1812
1826
|
logger.verbose('[Graph:Persist:PathSelect]', {
|
|
1813
1827
|
isIntroduction: !!state.introductionContext,
|
|
1814
1828
|
stateUserId: state.userId,
|
|
1815
|
-
stateIndexId: state.
|
|
1829
|
+
stateIndexId: state.networkId,
|
|
1816
1830
|
evaluatedActorUserIds: evaluated.actors.map(a => a.userId),
|
|
1817
1831
|
});
|
|
1818
1832
|
if (state.introductionContext) {
|
|
1819
1833
|
if (indexIdForActors === undefined) {
|
|
1820
|
-
logger.warn('[Graph:Persist] Introduction path missing
|
|
1834
|
+
logger.warn('[Graph:Persist] Introduction path missing networkId; skipping opportunity', {
|
|
1821
1835
|
userId: state.userId,
|
|
1822
1836
|
actorsCount: evaluated.actors.length,
|
|
1823
1837
|
});
|
|
@@ -1825,7 +1839,7 @@ export class OpportunityGraphFactory {
|
|
|
1825
1839
|
}
|
|
1826
1840
|
// Introduction path: manual detection, introducer actor, curator_judgment signal.
|
|
1827
1841
|
const evaluatorActors = evaluated.actors.map((a) => ({
|
|
1828
|
-
|
|
1842
|
+
networkId: a.networkId ?? indexIdForActors,
|
|
1829
1843
|
userId: a.userId,
|
|
1830
1844
|
role: a.role,
|
|
1831
1845
|
...(a.intentId ? { intent: a.intentId } : {}),
|
|
@@ -1835,7 +1849,7 @@ export class OpportunityGraphFactory {
|
|
|
1835
1849
|
? evaluatorActors
|
|
1836
1850
|
: [
|
|
1837
1851
|
...evaluatorActors,
|
|
1838
|
-
{
|
|
1852
|
+
{ networkId: indexIdForActors, userId: state.userId, role: 'introducer' },
|
|
1839
1853
|
];
|
|
1840
1854
|
data = {
|
|
1841
1855
|
detection: {
|
|
@@ -1858,7 +1872,7 @@ export class OpportunityGraphFactory {
|
|
|
1858
1872
|
],
|
|
1859
1873
|
},
|
|
1860
1874
|
context: {
|
|
1861
|
-
|
|
1875
|
+
networkId: state.networkId ?? indexIdForActors,
|
|
1862
1876
|
...(state.options.conversationId ? { conversationId: state.options.conversationId } : {}),
|
|
1863
1877
|
},
|
|
1864
1878
|
confidence: String(evaluated.score / 100),
|
|
@@ -1867,7 +1881,7 @@ export class OpportunityGraphFactory {
|
|
|
1867
1881
|
}
|
|
1868
1882
|
else if (state.onBehalfOfUserId) {
|
|
1869
1883
|
if (indexIdForActors === undefined) {
|
|
1870
|
-
logger.warn('[Graph:Persist] Introducer discovery path missing
|
|
1884
|
+
logger.warn('[Graph:Persist] Introducer discovery path missing networkId; skipping opportunity', {
|
|
1871
1885
|
userId: state.userId,
|
|
1872
1886
|
actorsCount: evaluated.actors.length,
|
|
1873
1887
|
});
|
|
@@ -1875,7 +1889,7 @@ export class OpportunityGraphFactory {
|
|
|
1875
1889
|
}
|
|
1876
1890
|
// Introducer discovery path: manual detection, introducer is state.userId, target is onBehalfOfUserId.
|
|
1877
1891
|
const evaluatorActors = evaluated.actors.map((a) => ({
|
|
1878
|
-
|
|
1892
|
+
networkId: a.networkId ?? indexIdForActors,
|
|
1879
1893
|
userId: a.userId,
|
|
1880
1894
|
role: a.role,
|
|
1881
1895
|
...(a.intentId ? { intent: a.intentId } : {}),
|
|
@@ -1885,7 +1899,7 @@ export class OpportunityGraphFactory {
|
|
|
1885
1899
|
? evaluatorActors
|
|
1886
1900
|
: [
|
|
1887
1901
|
...evaluatorActors,
|
|
1888
|
-
{
|
|
1902
|
+
{ networkId: indexIdForActors, userId: state.userId, role: 'introducer' },
|
|
1889
1903
|
];
|
|
1890
1904
|
const candidateUserId = evaluated.actors.find((a) => a.userId !== state.onBehalfOfUserId)?.userId;
|
|
1891
1905
|
const overlapping = candidateUserId
|
|
@@ -1915,7 +1929,7 @@ export class OpportunityGraphFactory {
|
|
|
1915
1929
|
if (existing.status !== 'expired' && candidateUserId) {
|
|
1916
1930
|
existingBetweenActors.push({
|
|
1917
1931
|
candidateUserId: candidateUserId,
|
|
1918
|
-
|
|
1932
|
+
networkId: (state.networkId ?? indexIdForActors ?? ''),
|
|
1919
1933
|
existingOpportunityId: existing.id,
|
|
1920
1934
|
existingStatus: existing.status,
|
|
1921
1935
|
});
|
|
@@ -1941,7 +1955,7 @@ export class OpportunityGraphFactory {
|
|
|
1941
1955
|
}],
|
|
1942
1956
|
},
|
|
1943
1957
|
context: {
|
|
1944
|
-
|
|
1958
|
+
networkId: state.networkId ?? indexIdForActors,
|
|
1945
1959
|
...(state.options.conversationId ? { conversationId: state.options.conversationId } : {}),
|
|
1946
1960
|
},
|
|
1947
1961
|
confidence: String(evaluated.score / 100),
|
|
@@ -1951,7 +1965,7 @@ export class OpportunityGraphFactory {
|
|
|
1951
1965
|
else {
|
|
1952
1966
|
// Discovery path: opportunity_graph source, no introducer, lifecycle guard for agent/patient.
|
|
1953
1967
|
const evaluatorActors = evaluated.actors.map((a) => ({
|
|
1954
|
-
|
|
1968
|
+
networkId: a.networkId ?? indexIdForActors,
|
|
1955
1969
|
userId: a.userId,
|
|
1956
1970
|
role: a.role,
|
|
1957
1971
|
...(a.intentId ? { intent: a.intentId } : {}),
|
|
@@ -1972,7 +1986,7 @@ export class OpportunityGraphFactory {
|
|
|
1972
1986
|
}
|
|
1973
1987
|
}
|
|
1974
1988
|
// Index-agnostic dedup: find ANY existing opportunity between these users,
|
|
1975
|
-
// regardless of which index it was created in or whether context.
|
|
1989
|
+
// regardless of which index it was created in or whether context.networkId is set.
|
|
1976
1990
|
const candidateUserId = evaluated.actors.find((a) => a.userId !== state.userId)?.userId;
|
|
1977
1991
|
logger.verbose('[Graph:Persist:Dedup] Checking overlapping opportunities', {
|
|
1978
1992
|
stateUserId: state.userId,
|
|
@@ -1988,7 +2002,7 @@ export class OpportunityGraphFactory {
|
|
|
1988
2002
|
});
|
|
1989
2003
|
if (overlapping.length > 0) {
|
|
1990
2004
|
const existing = overlapping[0];
|
|
1991
|
-
const existingIndexId = (existing.context?.
|
|
2005
|
+
const existingIndexId = (existing.context?.networkId ?? state.networkId ?? state.userNetworks?.[0] ?? '');
|
|
1992
2006
|
if (existing.status === 'expired') {
|
|
1993
2007
|
const reactivated = await this.database.updateOpportunityStatus(existing.id, initialStatus);
|
|
1994
2008
|
if (reactivated) {
|
|
@@ -2016,7 +2030,7 @@ export class OpportunityGraphFactory {
|
|
|
2016
2030
|
else if (candidateUserId) {
|
|
2017
2031
|
existingBetweenActors.push({
|
|
2018
2032
|
candidateUserId: candidateUserId,
|
|
2019
|
-
|
|
2033
|
+
networkId: existingIndexId,
|
|
2020
2034
|
existingOpportunityId: existing.id,
|
|
2021
2035
|
existingStatus: existing.status,
|
|
2022
2036
|
});
|
|
@@ -2051,7 +2065,7 @@ export class OpportunityGraphFactory {
|
|
|
2051
2065
|
],
|
|
2052
2066
|
},
|
|
2053
2067
|
context: {
|
|
2054
|
-
...(state.
|
|
2068
|
+
...(state.networkId ? { networkId: state.networkId } : {}),
|
|
2055
2069
|
...(state.options.conversationId ? { conversationId: state.options.conversationId } : {}),
|
|
2056
2070
|
},
|
|
2057
2071
|
confidence: String(evaluated.score / 100),
|
|
@@ -2124,29 +2138,32 @@ export class OpportunityGraphFactory {
|
|
|
2124
2138
|
// CRUD NODES (read, update, delete, send)
|
|
2125
2139
|
// ═══════════════════════════════════════════════════════════════
|
|
2126
2140
|
/**
|
|
2127
|
-
* Read Node: List opportunities for the user, optionally filtered by
|
|
2141
|
+
* Read Node: List opportunities for the user, optionally filtered by networkId.
|
|
2128
2142
|
* Fast path — no LLM calls.
|
|
2129
2143
|
*/
|
|
2130
2144
|
const readNode = async (state) => {
|
|
2131
2145
|
return timed("OpportunityGraph.read", async () => {
|
|
2132
2146
|
logger.verbose('[Graph:Read] Listing opportunities', {
|
|
2133
2147
|
userId: state.userId,
|
|
2134
|
-
|
|
2148
|
+
networkId: state.networkId,
|
|
2135
2149
|
});
|
|
2136
2150
|
try {
|
|
2137
2151
|
let indexIdFilter;
|
|
2138
|
-
if (state.
|
|
2139
|
-
const isMember = await
|
|
2140
|
-
|
|
2152
|
+
if (state.networkId) {
|
|
2153
|
+
const [isMember, isOwner] = await Promise.all([
|
|
2154
|
+
this.database.isNetworkMember(state.networkId, state.userId),
|
|
2155
|
+
this.database.isIndexOwner(state.networkId, state.userId),
|
|
2156
|
+
]);
|
|
2157
|
+
if (!isMember && !isOwner) {
|
|
2141
2158
|
return {
|
|
2142
|
-
readResult: { count: 0, opportunities: [], message: '
|
|
2159
|
+
readResult: { count: 0, opportunities: [], message: 'Network not found or you are not a member.' },
|
|
2143
2160
|
};
|
|
2144
2161
|
}
|
|
2145
|
-
indexIdFilter = state.
|
|
2162
|
+
indexIdFilter = state.networkId;
|
|
2146
2163
|
}
|
|
2147
2164
|
const rawList = await this.database.getOpportunitiesForUser(state.userId, {
|
|
2148
2165
|
limit: 30,
|
|
2149
|
-
...(indexIdFilter ? {
|
|
2166
|
+
...(indexIdFilter ? { networkId: indexIdFilter } : {}),
|
|
2150
2167
|
});
|
|
2151
2168
|
const list = rawList.filter((opp) => opp.status !== 'expired');
|
|
2152
2169
|
if (list.length === 0) {
|
|
@@ -2193,10 +2210,10 @@ export class OpportunityGraphFactory {
|
|
|
2193
2210
|
const introducer = opp.actors.find((a) => a.role === 'introducer');
|
|
2194
2211
|
const partyIds = otherParties.map((a) => a.userId);
|
|
2195
2212
|
const idsToResolve = introducer ? [...partyIds, introducer.userId] : partyIds;
|
|
2196
|
-
// Use the counterpart's (non-viewer)
|
|
2213
|
+
// Use the counterpart's (non-viewer) networkId — it reflects where the match was found.
|
|
2197
2214
|
// actors[0] is typically the viewer with an arbitrary first-target-index value.
|
|
2198
2215
|
const counterpartActor = opp.actors.find((a) => a.userId !== state.userId);
|
|
2199
|
-
const actorIndexId = counterpartActor?.
|
|
2216
|
+
const actorIndexId = counterpartActor?.networkId ?? opp.actors[0]?.networkId;
|
|
2200
2217
|
const [indexRecord, ...profileAndUserPairs] = await Promise.all([
|
|
2201
2218
|
actorIndexId ? this.database.getIndex(actorIndexId) : Promise.resolve(null),
|
|
2202
2219
|
...idsToResolve.map(async (uid) => {
|