@indexnetwork/protocol 66.0.0-rc.608.1 → 66.0.0-rc.609.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 +3 -1
- package/IMPLEMENTATION.md +11 -12
- package/STABILITY.md +0 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/internal/opportunities/opportunity.cards.d.ts +78 -0
- package/dist/internal/opportunities/opportunity.cards.js +165 -0
- package/dist/internal/opportunities/opportunity.presentation.d.ts +2 -2
- package/dist/internal/opportunities/opportunity.presentation.js +3 -3
- package/dist/internal/opportunities/opportunity.utils.d.ts +0 -42
- package/dist/internal/opportunities/opportunity.utils.js +0 -102
- package/dist/platform/database/capabilities.d.ts +2 -2
- package/dist/platform/database/entities.d.ts +1 -1
- package/package.json +1 -1
- package/dist/internal/opportunities/opportunity.feed-selection.d.ts +0 -24
- package/dist/internal/opportunities/opportunity.feed-selection.js +0 -40
- package/dist/internal/opportunities/radar/radar.graph.d.ts +0 -230
- package/dist/internal/opportunities/radar/radar.graph.js +0 -506
- package/dist/internal/opportunities/radar/radar.state.d.ts +0 -86
- package/dist/internal/opportunities/radar/radar.state.js +0 -87
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Radar Graph: Build the opportunity radar view — a flat, presenter-texted
|
|
3
|
-
* list of opportunity cards for a viewer, optionally scoped to one intent.
|
|
4
|
-
*
|
|
5
|
-
* Flow:
|
|
6
|
-
* loadOpportunities → checkPresenterCache → [generateCardText if misses]
|
|
7
|
-
* → cachePresenterResults → normalizeItems
|
|
8
|
-
*
|
|
9
|
-
* Uses OpportunityPresenter for card text and caches presenter results via
|
|
10
|
-
* OpportunityCache. Responses are a flat `items` array; clients bucket by
|
|
11
|
-
* lifecycle status themselves.
|
|
12
|
-
*/
|
|
13
|
-
import { StateGraph, START, END } from '@langchain/langgraph';
|
|
14
|
-
import { RadarGraphState } from './radar.state.js';
|
|
15
|
-
import { OpportunityPresenter, gatherPresenterContext } from '../opportunity.presentation.js';
|
|
16
|
-
import { loadNegotiationContext } from '../negotiation-context.loader.js';
|
|
17
|
-
import { canUserSeeOpportunity, isActionableForViewer, selectByComposition } from '../opportunity.utils.js';
|
|
18
|
-
import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from '../opportunity.labels.js';
|
|
19
|
-
import { buildRadarCardPresentationCacheKey } from '../opportunity.presentation.js';
|
|
20
|
-
import { protocolLogger } from '../../shared/observability/protocol.logger.js';
|
|
21
|
-
import { timed } from '../../shared/observability/performance.js';
|
|
22
|
-
import { requestContext } from "../../shared/observability/request-context.js";
|
|
23
|
-
const logger = protocolLogger('RadarGraph');
|
|
24
|
-
const checkPresenterCacheLog = protocolLogger('RadarGraph:checkPresenterCache');
|
|
25
|
-
const cachePresenterResultsLog = protocolLogger('RadarGraph:cachePresenterResults');
|
|
26
|
-
const generateCardTextLog = protocolLogger('RadarGraph:generateCardText');
|
|
27
|
-
const normalizeItemsLog = protocolLogger('RadarGraph:normalizeItems');
|
|
28
|
-
/** Default radar statuses: the lifecycle stages a viewer can act on today. */
|
|
29
|
-
export const DEFAULT_RADAR_STATUSES = ['pending'];
|
|
30
|
-
// Exhaustive registry — keys must cover every OpportunityStatus union member.
|
|
31
|
-
// Adding a new status to OpportunityStatus without adding a key here is a TS error,
|
|
32
|
-
// which is the whole point: prevents ALL_OPPORTUNITY_STATUSES from silently drifting.
|
|
33
|
-
const OPPORTUNITY_STATUS_REGISTRY = {
|
|
34
|
-
negotiating: true,
|
|
35
|
-
pending: true,
|
|
36
|
-
accepted: true,
|
|
37
|
-
rejected: true,
|
|
38
|
-
expired: true,
|
|
39
|
-
};
|
|
40
|
-
/** Full status enumeration. Pass this to `RadarGraphInvokeInput.statuses` to restore pre-Issue-3 (unfiltered) behavior. */
|
|
41
|
-
export const ALL_OPPORTUNITY_STATUSES = Object.keys(OPPORTUNITY_STATUS_REGISTRY);
|
|
42
|
-
const PRESENTATION_CONCURRENCY = 50;
|
|
43
|
-
const RADAR_CACHE_TTL = 24 * 60 * 60; // 24 hours in seconds
|
|
44
|
-
/** Pure cache policy for presenter cards. */
|
|
45
|
-
export function isRadarPresentationCacheable(card, status) {
|
|
46
|
-
return Boolean(status &&
|
|
47
|
-
status !== 'negotiating' &&
|
|
48
|
-
!card.presentationPending &&
|
|
49
|
-
card.name &&
|
|
50
|
-
card.name !== 'Unknown');
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Strip leading narrator name from remark when the UI already prepends "Name: " to the chip.
|
|
54
|
-
* Avoids duplication like "Yankı Ekin Yüksel: Yankı Ekin Yüksel introduced you two..."
|
|
55
|
-
* Repeats until no leading name (handles "Name: Name rest").
|
|
56
|
-
*/
|
|
57
|
-
export function stripLeadingNarratorName(remark, narratorName) {
|
|
58
|
-
let t = remark.trim();
|
|
59
|
-
if (!t || !narratorName.trim())
|
|
60
|
-
return remark;
|
|
61
|
-
const name = narratorName.trim();
|
|
62
|
-
const nameLower = name.toLowerCase();
|
|
63
|
-
for (;;) {
|
|
64
|
-
const lower = t.toLowerCase();
|
|
65
|
-
if (!lower.startsWith(nameLower))
|
|
66
|
-
break;
|
|
67
|
-
const rest = t.slice(name.length).replace(/^\s*[:,\-–—]\s*/i, '').trim();
|
|
68
|
-
if (rest.length === 0 || rest === t)
|
|
69
|
-
break;
|
|
70
|
-
t = rest;
|
|
71
|
-
}
|
|
72
|
-
return t;
|
|
73
|
-
}
|
|
74
|
-
/** Normalize timestamp for sorting; returns numeric ms or 0 for invalid/missing. */
|
|
75
|
-
const safeParseDate = (value) => {
|
|
76
|
-
if (value == null)
|
|
77
|
-
return 0;
|
|
78
|
-
if (value instanceof Date) {
|
|
79
|
-
const t = value.getTime();
|
|
80
|
-
return Number.isFinite(t) ? t : 0;
|
|
81
|
-
}
|
|
82
|
-
if (typeof value === 'number' && Number.isFinite(value))
|
|
83
|
-
return value;
|
|
84
|
-
if (typeof value === 'string') {
|
|
85
|
-
const t = new Date(value).getTime();
|
|
86
|
-
if (!Number.isNaN(t))
|
|
87
|
-
return t;
|
|
88
|
-
}
|
|
89
|
-
return 0;
|
|
90
|
-
};
|
|
91
|
-
/** Confidence score for sorting (interpretation.confidence or opportunity.confidence). */
|
|
92
|
-
const getRawConfidence = (opp) => {
|
|
93
|
-
const fromInterp = opp.interpretation?.confidence;
|
|
94
|
-
if (typeof fromInterp === 'number' && !Number.isNaN(fromInterp))
|
|
95
|
-
return fromInterp;
|
|
96
|
-
if (typeof fromInterp === 'string') {
|
|
97
|
-
const n = parseFloat(fromInterp);
|
|
98
|
-
if (!Number.isNaN(n))
|
|
99
|
-
return n;
|
|
100
|
-
}
|
|
101
|
-
const fromRow = opp.confidence;
|
|
102
|
-
if (typeof fromRow === 'number' && !Number.isNaN(fromRow))
|
|
103
|
-
return fromRow;
|
|
104
|
-
if (typeof fromRow === 'string') {
|
|
105
|
-
const n = parseFloat(fromRow);
|
|
106
|
-
if (!Number.isNaN(n))
|
|
107
|
-
return n;
|
|
108
|
-
}
|
|
109
|
-
return 0;
|
|
110
|
-
};
|
|
111
|
-
/** Unique non-viewer userIds for an opportunity (actors can repeat). */
|
|
112
|
-
const getUniqueCounterpartUserIds = (opp, viewerId) => {
|
|
113
|
-
const ids = new Set();
|
|
114
|
-
for (const a of opp.actors) {
|
|
115
|
-
if (a.userId !== viewerId && a.userId) {
|
|
116
|
-
ids.add(a.userId);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return ids;
|
|
120
|
-
};
|
|
121
|
-
const pickDisplayCounterpartActor = (opportunity, viewerId) => {
|
|
122
|
-
const candidates = opportunity.actors.filter((actor) => actor.userId !== viewerId);
|
|
123
|
-
if (candidates.length === 0) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
// Prefer direct counterpart roles when available, then stable sort by user id.
|
|
127
|
-
const rolePriority = new Map([
|
|
128
|
-
['patient', 0],
|
|
129
|
-
['party', 1],
|
|
130
|
-
['agent', 2],
|
|
131
|
-
['peer', 3],
|
|
132
|
-
]);
|
|
133
|
-
const sorted = [...candidates].sort((a, b) => {
|
|
134
|
-
const aPriority = rolePriority.get(a.role) ?? 99;
|
|
135
|
-
const bPriority = rolePriority.get(b.role) ?? 99;
|
|
136
|
-
if (aPriority !== bPriority)
|
|
137
|
-
return aPriority - bPriority;
|
|
138
|
-
return a.userId.localeCompare(b.userId);
|
|
139
|
-
});
|
|
140
|
-
return sorted[0] ?? null;
|
|
141
|
-
};
|
|
142
|
-
export class RadarGraphFactory {
|
|
143
|
-
constructor(database, cache) {
|
|
144
|
-
this.deps = { database, cache, presenter: new OpportunityPresenter() };
|
|
145
|
-
}
|
|
146
|
-
createGraph() {
|
|
147
|
-
const deps = this.deps;
|
|
148
|
-
const graph = new StateGraph(RadarGraphState)
|
|
149
|
-
.addNode('loadOpportunities', (state) => loadOpportunitiesNode(state, deps))
|
|
150
|
-
.addNode('checkPresenterCache', (state) => checkPresenterCacheNode(state, deps))
|
|
151
|
-
.addNode('generateCardText', (state) => generateCardTextNode(state, deps))
|
|
152
|
-
.addNode('cachePresenterResults', (state) => cachePresenterResultsNode(state, deps))
|
|
153
|
-
.addNode('normalizeItems', normalizeItemsNode)
|
|
154
|
-
.addEdge(START, 'loadOpportunities')
|
|
155
|
-
.addEdge('loadOpportunities', 'checkPresenterCache')
|
|
156
|
-
.addConditionalEdges('checkPresenterCache', (state) => shouldGenerateCards(state, deps), {
|
|
157
|
-
generate: 'generateCardText',
|
|
158
|
-
skip: 'cachePresenterResults',
|
|
159
|
-
})
|
|
160
|
-
.addEdge('generateCardText', 'cachePresenterResults')
|
|
161
|
-
.addEdge('cachePresenterResults', 'normalizeItems')
|
|
162
|
-
.addEdge('normalizeItems', END);
|
|
163
|
-
return graph.compile();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
export async function loadOpportunitiesNode(state, deps) {
|
|
167
|
-
return timed("RadarGraph.loadOpportunities", async () => {
|
|
168
|
-
if (!state.userId) {
|
|
169
|
-
return { error: 'userId is required' };
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
// Minimum of 50 ensures enough candidates across all radar categories
|
|
173
|
-
// (connection, expired) for selectByComposition to fill
|
|
174
|
-
// its soft targets, even after visibility filtering and dedup.
|
|
175
|
-
const fetchLimit = Math.min(150, Math.max(50, state.limit * 3));
|
|
176
|
-
const statuses = state.statuses ?? DEFAULT_RADAR_STATUSES;
|
|
177
|
-
const options = {
|
|
178
|
-
limit: fetchLimit,
|
|
179
|
-
statuses,
|
|
180
|
-
};
|
|
181
|
-
if (state.networkId)
|
|
182
|
-
options.networkId = state.networkId;
|
|
183
|
-
if (state.scopeType === 'intent' && state.scopeId) {
|
|
184
|
-
options.scopeType = 'intent';
|
|
185
|
-
options.scopeId = state.scopeId;
|
|
186
|
-
}
|
|
187
|
-
// Do not pass conversationId: radar view excludes draft opportunities (chat-only drafts).
|
|
188
|
-
const raw = await deps.database.getOpportunitiesForUser(state.userId, options);
|
|
189
|
-
const visible = raw.filter((opp) => canUserSeeOpportunity(opp.actors, opp.status, state.userId));
|
|
190
|
-
// Actionability only gates the live statuses a viewer could act on:
|
|
191
|
-
// latent/pending cards the viewer cannot act on are noise, but
|
|
192
|
-
// terminal/internal statuses (accepted, rejected, expired, negotiating,
|
|
193
|
-
// draft) are deliberate history — when a caller explicitly
|
|
194
|
-
// requests them via `statuses`, they must pass through (they are never
|
|
195
|
-
// actionable by rule 5, so filtering them here would return nothing).
|
|
196
|
-
// The requested-status membership check is defense-in-depth: rows
|
|
197
|
-
// outside the requested set are dropped even if the adapter drifts.
|
|
198
|
-
const requestedStatuses = new Set(statuses);
|
|
199
|
-
const visibleForRadar = visible.filter((opp) => {
|
|
200
|
-
if (!requestedStatuses.has(opp.status))
|
|
201
|
-
return false;
|
|
202
|
-
if (opp.status === 'pending') {
|
|
203
|
-
return isActionableForViewer(opp.actors, opp.status, state.userId, opp.committedActorIds ?? []);
|
|
204
|
-
}
|
|
205
|
-
return true;
|
|
206
|
-
});
|
|
207
|
-
const explicitStatuses = (state.statuses?.length ?? 0) > 0;
|
|
208
|
-
if (explicitStatuses) {
|
|
209
|
-
// Lifecycle view (e.g. intent radar): newest-created first so status
|
|
210
|
-
// transitions do not reorder cards merely by touching updatedAt.
|
|
211
|
-
// Counterpart dedup still keeps each person's newest opportunity; no
|
|
212
|
-
// composition capping because the caller wants the full pipeline.
|
|
213
|
-
const newestFirst = [...visibleForRadar].sort((a, b) => safeParseDate(b.createdAt) - safeParseDate(a.createdAt));
|
|
214
|
-
const seenIds = new Set();
|
|
215
|
-
const dedupedByCounterpart = newestFirst.filter((opp) => {
|
|
216
|
-
const counterpartIds = getUniqueCounterpartUserIds(opp, state.userId);
|
|
217
|
-
const hasOverlap = [...counterpartIds].some((id) => seenIds.has(id));
|
|
218
|
-
if (hasOverlap)
|
|
219
|
-
return false;
|
|
220
|
-
for (const id of counterpartIds)
|
|
221
|
-
seenIds.add(id);
|
|
222
|
-
return true;
|
|
223
|
-
});
|
|
224
|
-
return { opportunities: dedupedByCounterpart.slice(0, state.limit) };
|
|
225
|
-
}
|
|
226
|
-
const sorted = [...visibleForRadar].sort((a, b) => {
|
|
227
|
-
const confA = getRawConfidence(a);
|
|
228
|
-
const confB = getRawConfidence(b);
|
|
229
|
-
if (confB !== confA)
|
|
230
|
-
return confB - confA;
|
|
231
|
-
const aTime = safeParseDate(a.updatedAt);
|
|
232
|
-
const bTime = safeParseDate(b.updatedAt);
|
|
233
|
-
return bTime - aTime;
|
|
234
|
-
});
|
|
235
|
-
const seenUserIds = new Set();
|
|
236
|
-
const deduped = sorted.filter((opp) => {
|
|
237
|
-
const counterpartIds = getUniqueCounterpartUserIds(opp, state.userId);
|
|
238
|
-
const hasOverlap = [...counterpartIds].some((id) => seenUserIds.has(id));
|
|
239
|
-
if (hasOverlap)
|
|
240
|
-
return false;
|
|
241
|
-
for (const id of counterpartIds)
|
|
242
|
-
seenUserIds.add(id);
|
|
243
|
-
return true;
|
|
244
|
-
});
|
|
245
|
-
const opportunities = selectByComposition(deduped, state.userId);
|
|
246
|
-
return { opportunities };
|
|
247
|
-
}
|
|
248
|
-
catch (e) {
|
|
249
|
-
logger.error('RadarGraph loadOpportunities failed', { error: e });
|
|
250
|
-
return { error: 'Failed to load opportunities', opportunities: [] };
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
export async function checkPresenterCacheNode(state, deps) {
|
|
255
|
-
return timed("RadarGraph.checkPresenterCache", async () => {
|
|
256
|
-
const { opportunities, userId, scopeId } = state;
|
|
257
|
-
if (opportunities.length === 0) {
|
|
258
|
-
return { cachedCards: new Map(), uncachedOpportunities: [] };
|
|
259
|
-
}
|
|
260
|
-
if (state.noCache) {
|
|
261
|
-
checkPresenterCacheLog.verbose('noCache=true, skipping cache');
|
|
262
|
-
return { cachedCards: new Map(), uncachedOpportunities: opportunities };
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
// Negotiating cards are templated (no LLM call) and their text
|
|
266
|
-
// depends on the live turn count, which changes between requests
|
|
267
|
-
// without changing the opportunity status. Skip cache entirely
|
|
268
|
-
// for them so each render reflects the current turn.
|
|
269
|
-
//
|
|
270
|
-
// For all other statuses, include status in the key so status
|
|
271
|
-
// transitions (e.g. negotiating → pending) don't serve stale cards.
|
|
272
|
-
const cacheable = opportunities.filter((opp) => opp.status !== 'negotiating');
|
|
273
|
-
const liveNegotiating = opportunities.filter((opp) => opp.status === 'negotiating');
|
|
274
|
-
const keys = cacheable.map((opp) => buildRadarCardPresentationCacheKey(opp.id, opp.status, userId, scopeId));
|
|
275
|
-
const results = keys.length > 0 ? await deps.cache.mget(keys) : [];
|
|
276
|
-
const cachedCards = new Map();
|
|
277
|
-
const uncachedOpportunities = [...liveNegotiating];
|
|
278
|
-
for (let i = 0; i < cacheable.length; i++) {
|
|
279
|
-
const cached = results[i];
|
|
280
|
-
if (cached) {
|
|
281
|
-
const originalIndex = opportunities.indexOf(cacheable[i]);
|
|
282
|
-
// Stamp the live status: pre-status cache entries lack the field,
|
|
283
|
-
// and the key already guarantees it matches the current status.
|
|
284
|
-
cachedCards.set(cacheable[i].id, {
|
|
285
|
-
...cached,
|
|
286
|
-
status: cacheable[i].status,
|
|
287
|
-
_cardIndex: originalIndex,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
uncachedOpportunities.push(cacheable[i]);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
checkPresenterCacheLog.verbose('', {
|
|
295
|
-
total: opportunities.length,
|
|
296
|
-
cacheHits: cachedCards.size,
|
|
297
|
-
cacheMisses: uncachedOpportunities.length,
|
|
298
|
-
});
|
|
299
|
-
return { cachedCards, uncachedOpportunities };
|
|
300
|
-
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
checkPresenterCacheLog.warn('cache unavailable, skipping', { error: e });
|
|
303
|
-
return { cachedCards: new Map(), uncachedOpportunities: opportunities };
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
export function shouldGenerateCards(state, _deps) {
|
|
308
|
-
if (state.uncachedOpportunities.length > 0) {
|
|
309
|
-
return 'generate';
|
|
310
|
-
}
|
|
311
|
-
logger.verbose('All presenter results cached, skipping generation');
|
|
312
|
-
return 'skip';
|
|
313
|
-
}
|
|
314
|
-
export async function generateCardTextNode(state, deps) {
|
|
315
|
-
return timed("RadarGraph.generateCardText", async () => {
|
|
316
|
-
const opportunities = state.uncachedOpportunities.length > 0
|
|
317
|
-
? state.uncachedOpportunities
|
|
318
|
-
: state.opportunities;
|
|
319
|
-
generateCardTextLog.verbose('entry', { opportunitiesLength: opportunities.length, userId: state.userId });
|
|
320
|
-
if (opportunities.length === 0) {
|
|
321
|
-
generateCardTextLog.verbose('exit', { totalOpportunities: 0 });
|
|
322
|
-
return { cards: [], agentTimings: [], meta: { totalOpportunities: 0 } };
|
|
323
|
-
}
|
|
324
|
-
const db = deps.database;
|
|
325
|
-
const cards = [];
|
|
326
|
-
const relevantActorIds = new Set();
|
|
327
|
-
for (const opp of opportunities) {
|
|
328
|
-
for (const a of opp.actors) {
|
|
329
|
-
if (a.userId)
|
|
330
|
-
relevantActorIds.add(a.userId);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const userEntries = await Promise.all(Array.from(relevantActorIds).map(async (userId) => {
|
|
334
|
-
try {
|
|
335
|
-
const user = await deps.database.getUser(userId);
|
|
336
|
-
return [userId, user ?? null];
|
|
337
|
-
}
|
|
338
|
-
catch {
|
|
339
|
-
return [userId, null];
|
|
340
|
-
}
|
|
341
|
-
}));
|
|
342
|
-
const userMap = new Map(userEntries);
|
|
343
|
-
const oppIndexMap = new Map(state.opportunities.map((opp, idx) => [opp.id, idx]));
|
|
344
|
-
const agentTimingsAccum = [];
|
|
345
|
-
for (let i = 0; i < opportunities.length; i += PRESENTATION_CONCURRENCY) {
|
|
346
|
-
const chunk = opportunities.slice(i, i + PRESENTATION_CONCURRENCY);
|
|
347
|
-
const chunkCards = await Promise.all(chunk.map(async (opportunity, offset) => {
|
|
348
|
-
const cardIndex = oppIndexMap.get(opportunity.id) ?? (i + offset);
|
|
349
|
-
const viewerActor = opportunity.actors.find((a) => a.userId === state.userId);
|
|
350
|
-
const viewerRole = viewerActor?.role ?? 'party';
|
|
351
|
-
const preferredActor = pickDisplayCounterpartActor(opportunity, state.userId)
|
|
352
|
-
?? opportunity.actors.find((a) => a.userId !== state.userId);
|
|
353
|
-
const actorWithProfile = opportunity.actors.find((a) => a.userId !== state.userId && !!userMap.get(a.userId));
|
|
354
|
-
const otherActor = (preferredActor && userMap.get(preferredActor.userId))
|
|
355
|
-
? preferredActor
|
|
356
|
-
: (actorWithProfile ?? preferredActor);
|
|
357
|
-
const otherUser = otherActor ? userMap.get(otherActor.userId) ?? null : null;
|
|
358
|
-
let userName = otherUser?.name ?? 'Unknown';
|
|
359
|
-
// Fallback to profile identity name when users.name is missing (e.g. profile has display name, users row does not)
|
|
360
|
-
if ((userName === 'Unknown' || !userName?.trim()) && otherActor?.userId && db.getProfile) {
|
|
361
|
-
const profile = await db.getProfile(otherActor.userId).catch((err) => {
|
|
362
|
-
logger.debug('getProfile fallback failed', { otherActorUserId: otherActor.userId, error: err });
|
|
363
|
-
return null;
|
|
364
|
-
});
|
|
365
|
-
const profileName = profile?.identity?.name?.trim();
|
|
366
|
-
if (profileName)
|
|
367
|
-
userName = profileName;
|
|
368
|
-
}
|
|
369
|
-
// Unresolvable display counterpart (deleted user: no users row, no
|
|
370
|
-
// profile fallback). Drop the card entirely instead of rendering an
|
|
371
|
-
// "Unknown" placeholder: such cards are unusable, excluded from the
|
|
372
|
-
// deps.presenter cache (see cachePresenterResults), and would otherwise
|
|
373
|
-
// trigger a fresh deps.presenter LLM call on every request — a permanent
|
|
374
|
-
// cache miss that keeps the whole radar slow (~9s per load).
|
|
375
|
-
if (userName === 'Unknown' || !userName?.trim()) {
|
|
376
|
-
logger.verbose('[RadarGraph:generateCardText] dropping card with unresolvable counterpart', {
|
|
377
|
-
opportunityId: opportunity.id,
|
|
378
|
-
otherActorUserId: otherActor?.userId,
|
|
379
|
-
});
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
const userAvatar = otherUser?.avatar ?? null;
|
|
383
|
-
// Skeleton presentation: return an identity-only card without the
|
|
384
|
-
// deps.presenter LLM or negotiation-context load. Name resolution and
|
|
385
|
-
// the unresolvable-counterpart drop above still apply, so the card
|
|
386
|
-
// set matches what the follow-up full request will return.
|
|
387
|
-
if (state.presentation === 'skeleton') {
|
|
388
|
-
return {
|
|
389
|
-
opportunityId: opportunity.id,
|
|
390
|
-
status: opportunity.status,
|
|
391
|
-
userId: otherActor?.userId ?? '',
|
|
392
|
-
name: userName,
|
|
393
|
-
avatar: userAvatar,
|
|
394
|
-
mainText: '',
|
|
395
|
-
cta: '',
|
|
396
|
-
primaryActionLabel: getPrimaryActionLabel(viewerRole),
|
|
397
|
-
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
398
|
-
mutualIntentsLabel: 'Shared interests',
|
|
399
|
-
viewerRole,
|
|
400
|
-
presentationPending: true,
|
|
401
|
-
_cardIndex: cardIndex,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
try {
|
|
405
|
-
const [ctx, negotiationContext] = await Promise.all([
|
|
406
|
-
gatherPresenterContext(db, opportunity, state.userId, otherActor?.userId, state.scopeId),
|
|
407
|
-
loadNegotiationContext(db, opportunity.id, opportunity.status, state.userId),
|
|
408
|
-
]);
|
|
409
|
-
const presenterInput = {
|
|
410
|
-
...ctx,
|
|
411
|
-
mutualIntentCount: undefined,
|
|
412
|
-
opportunityStatus: opportunity.status,
|
|
413
|
-
...(negotiationContext ? { negotiationContext } : {}),
|
|
414
|
-
};
|
|
415
|
-
const _traceEmitterPresenter = requestContext.getStore()?.traceEmitter;
|
|
416
|
-
const presenterStart = Date.now();
|
|
417
|
-
_traceEmitterPresenter?.({ type: "agent_start", name: "opportunity-presenter" });
|
|
418
|
-
const presentation = await deps.presenter.presentCard(presenterInput);
|
|
419
|
-
const _presenterDuration = Date.now() - presenterStart;
|
|
420
|
-
agentTimingsAccum.push({ name: 'opportunity.presenter', durationMs: _presenterDuration });
|
|
421
|
-
_traceEmitterPresenter?.({ type: "agent_end", name: "opportunity-presenter", durationMs: _presenterDuration, summary: `Presented: ${userName}` });
|
|
422
|
-
// Every card is system-discovered now: one narrator.
|
|
423
|
-
const narratorChip = { name: 'Index', text: presentation.narratorRemark };
|
|
424
|
-
return {
|
|
425
|
-
opportunityId: opportunity.id,
|
|
426
|
-
status: opportunity.status,
|
|
427
|
-
userId: otherActor?.userId ?? '',
|
|
428
|
-
name: userName,
|
|
429
|
-
avatar: userAvatar,
|
|
430
|
-
mainText: presentation.personalizedSummary,
|
|
431
|
-
cta: presentation.suggestedAction,
|
|
432
|
-
headline: presentation.headline,
|
|
433
|
-
primaryActionLabel: getPrimaryActionLabel(viewerRole),
|
|
434
|
-
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
435
|
-
mutualIntentsLabel: presentation.mutualIntentsLabel,
|
|
436
|
-
narratorChip,
|
|
437
|
-
viewerRole,
|
|
438
|
-
_cardIndex: cardIndex,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
catch (e) {
|
|
442
|
-
logger.warn('RadarGraph presenter failed for opportunity', { opportunityId: opportunity.id, error: e });
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
}));
|
|
446
|
-
cards.push(...chunkCards.filter((c) => c !== null));
|
|
447
|
-
}
|
|
448
|
-
generateCardTextLog.verbose('exit', { totalOpportunities: state.opportunities.length });
|
|
449
|
-
return {
|
|
450
|
-
cards,
|
|
451
|
-
agentTimings: agentTimingsAccum,
|
|
452
|
-
meta: { totalOpportunities: state.opportunities.length },
|
|
453
|
-
};
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
export async function cachePresenterResultsNode(state, deps) {
|
|
457
|
-
return timed("RadarGraph.cachePresenterResults", async () => {
|
|
458
|
-
const { cards, cachedCards, userId, opportunities, scopeId } = state;
|
|
459
|
-
// Only cache cards that weren't already from cache
|
|
460
|
-
const newCards = cards.filter((card) => !cachedCards.has(card.opportunityId));
|
|
461
|
-
const statusById = new Map(opportunities.map((opp) => [opp.id, opp.status]));
|
|
462
|
-
try {
|
|
463
|
-
await Promise.all(newCards.map((card) => {
|
|
464
|
-
const status = statusById.get(card.opportunityId);
|
|
465
|
-
// Negotiating, skeleton, and unresolved-name cards are
|
|
466
|
-
// safe for the current response but must not become 24h entries.
|
|
467
|
-
if (!status || !isRadarPresentationCacheable(card, status))
|
|
468
|
-
return Promise.resolve();
|
|
469
|
-
return deps.cache.set(buildRadarCardPresentationCacheKey(card.opportunityId, status, userId, scopeId), card, { ttl: RADAR_CACHE_TTL });
|
|
470
|
-
}));
|
|
471
|
-
}
|
|
472
|
-
catch (e) {
|
|
473
|
-
cachePresenterResultsLog.warn('cache write failed, continuing', { error: e });
|
|
474
|
-
}
|
|
475
|
-
// Merge cached cards into full card list
|
|
476
|
-
const allCards = [...cards];
|
|
477
|
-
for (const [oppId, cachedCard] of cachedCards) {
|
|
478
|
-
if (!cards.some((card) => card.opportunityId === oppId)) {
|
|
479
|
-
allCards.push(cachedCard);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
// Re-sort by _cardIndex to maintain original ordering
|
|
483
|
-
allCards.sort((a, b) => a._cardIndex - b._cardIndex);
|
|
484
|
-
cachePresenterResultsLog.verbose('', {
|
|
485
|
-
newlyCached: newCards.length,
|
|
486
|
-
totalCards: allCards.length,
|
|
487
|
-
});
|
|
488
|
-
return {
|
|
489
|
-
cards: allCards,
|
|
490
|
-
meta: { totalOpportunities: state.opportunities.length },
|
|
491
|
-
};
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
export async function normalizeItemsNode(state) {
|
|
495
|
-
return timed("RadarGraph.normalizeItems", async () => {
|
|
496
|
-
normalizeItemsLog.verbose('entry', { cardsLength: state.cards.length });
|
|
497
|
-
const createdAtById = new Map(state.opportunities.map((opportunity) => [opportunity.id, opportunity.createdAt.toISOString()]));
|
|
498
|
-
const items = state.cards.map((card) => {
|
|
499
|
-
const { _cardIndex, ...rest } = card;
|
|
500
|
-
return { ...rest, createdAt: createdAtById.get(card.opportunityId) };
|
|
501
|
-
});
|
|
502
|
-
const meta = { totalOpportunities: state.opportunities.length };
|
|
503
|
-
normalizeItemsLog.verbose('exit', { totalOpportunities: meta.totalOpportunities, totalItems: items.length });
|
|
504
|
-
return { items, meta };
|
|
505
|
-
});
|
|
506
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { Opportunity, OpportunityStatus } from '../../../platform/database.js';
|
|
2
|
-
import type { DebugMetaAgent } from "../../../protocol/core.js";
|
|
3
|
-
/**
|
|
4
|
-
* Radar card item: one opportunity with full presenter-driven display contract.
|
|
5
|
-
*/
|
|
6
|
-
export interface RadarCardItem {
|
|
7
|
-
opportunityId: string;
|
|
8
|
-
createdAt?: string;
|
|
9
|
-
/** Lifecycle status of the underlying opportunity at render time (client bucketing, e.g. intent radar). */
|
|
10
|
-
status?: OpportunityStatus;
|
|
11
|
-
userId: string;
|
|
12
|
-
name: string;
|
|
13
|
-
avatar: string | null;
|
|
14
|
-
mainText: string;
|
|
15
|
-
cta: string;
|
|
16
|
-
headline?: string;
|
|
17
|
-
/** Presenter-generated; primary button (accept) and secondary button (dismiss). */
|
|
18
|
-
primaryActionLabel: string;
|
|
19
|
-
secondaryActionLabel: string;
|
|
20
|
-
/** Presenter-generated subtitle under the other party name (e.g. "1 mutual intent"). */
|
|
21
|
-
mutualIntentsLabel: string;
|
|
22
|
-
/** Narrator chip for human-introduced opportunities; avatar set when narrator is a user */
|
|
23
|
-
narratorChip?: {
|
|
24
|
-
name: string;
|
|
25
|
-
text: string;
|
|
26
|
-
avatar?: string | null;
|
|
27
|
-
userId?: string;
|
|
28
|
-
};
|
|
29
|
-
/** Viewer's role in this opportunity (e.g. 'party', 'agent', 'patient', 'peer'). */
|
|
30
|
-
viewerRole?: string;
|
|
31
|
-
/**
|
|
32
|
-
* True when this card was produced by a skeleton-presentation run: identity
|
|
33
|
-
* fields (name/avatar/status) are real but mainText/cta are empty because the
|
|
34
|
-
* presenter LLM was skipped. Clients render a shimmer body and re-fetch the
|
|
35
|
-
* full view. Skeleton cards are never written to the presenter cache.
|
|
36
|
-
*/
|
|
37
|
-
presentationPending?: boolean;
|
|
38
|
-
/** Internal: original position in the loaded opportunity list, for stable ordering. */
|
|
39
|
-
_cardIndex: number;
|
|
40
|
-
}
|
|
41
|
-
/** Card item as returned in API responses (no internal fields). */
|
|
42
|
-
export type RadarResponseItem = Omit<RadarCardItem, '_cardIndex'>;
|
|
43
|
-
/**
|
|
44
|
-
* Radar Graph State (Annotation-based).
|
|
45
|
-
* Flow: loadOpportunities → checkPresenterCache → [generateCardText if misses]
|
|
46
|
-
* → cachePresenterResults → normalizeItems.
|
|
47
|
-
*/
|
|
48
|
-
export declare const RadarGraphState: import("@langchain/langgraph").AnnotationRoot<{
|
|
49
|
-
userId: import("@langchain/langgraph").BaseChannel<string, string | import("@langchain/langgraph").OverwriteValue<string>, unknown>;
|
|
50
|
-
networkId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
|
|
51
|
-
scopeType: import("@langchain/langgraph").BaseChannel<"intent" | undefined, "intent" | import("@langchain/langgraph").OverwriteValue<"intent" | undefined> | undefined, unknown>;
|
|
52
|
-
scopeId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
|
|
53
|
-
limit: import("@langchain/langgraph").BaseChannel<number, number | import("@langchain/langgraph").OverwriteValue<number>, unknown>;
|
|
54
|
-
/** When true, bypass the presenter Redis cache. */
|
|
55
|
-
noCache: import("@langchain/langgraph").BaseChannel<boolean, boolean | import("@langchain/langgraph").OverwriteValue<boolean>, unknown>;
|
|
56
|
-
/**
|
|
57
|
-
* Presentation depth. 'full' (default) runs the presenter LLM for cache
|
|
58
|
-
* misses. 'skeleton' skips it: uncached cards come back with resolved
|
|
59
|
-
* identity (name/avatar/status) and `presentationPending: true`, cached
|
|
60
|
-
* cards come back complete.
|
|
61
|
-
*/
|
|
62
|
-
presentation: import("@langchain/langgraph").BaseChannel<"full" | "skeleton", "full" | "skeleton" | import("@langchain/langgraph").OverwriteValue<"full" | "skeleton">, unknown>;
|
|
63
|
-
/** Optional status filter. When undefined, the graph uses `DEFAULT_RADAR_STATUSES`. */
|
|
64
|
-
statuses: import("@langchain/langgraph").BaseChannel<OpportunityStatus[] | undefined, OpportunityStatus[] | import("@langchain/langgraph").OverwriteValue<OpportunityStatus[] | undefined> | undefined, unknown>;
|
|
65
|
-
/** Raw opportunities visible to the viewer (after visibility filter). */
|
|
66
|
-
opportunities: import("@langchain/langgraph").BaseChannel<Opportunity[], Opportunity[] | import("@langchain/langgraph").OverwriteValue<Opportunity[]>, unknown>;
|
|
67
|
-
/** Cards with presenter output and narrator chip. */
|
|
68
|
-
cards: import("@langchain/langgraph").BaseChannel<RadarCardItem[], RadarCardItem[] | import("@langchain/langgraph").OverwriteValue<RadarCardItem[]>, unknown>;
|
|
69
|
-
/** Final items for response (internal fields stripped). */
|
|
70
|
-
items: import("@langchain/langgraph").BaseChannel<RadarResponseItem[], RadarResponseItem[] | import("@langchain/langgraph").OverwriteValue<RadarResponseItem[]>, unknown>;
|
|
71
|
-
/** Presenter results retrieved from cache (opportunityId → RadarCardItem). */
|
|
72
|
-
cachedCards: import("@langchain/langgraph").BaseChannel<Map<string, RadarCardItem>, Map<string, RadarCardItem> | import("@langchain/langgraph").OverwriteValue<Map<string, RadarCardItem>>, unknown>;
|
|
73
|
-
/** Opportunities that had no cache hit and need presenter generation. */
|
|
74
|
-
uncachedOpportunities: import("@langchain/langgraph").BaseChannel<Opportunity[], Opportunity[] | import("@langchain/langgraph").OverwriteValue<Opportunity[]>, unknown>;
|
|
75
|
-
error: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
|
|
76
|
-
/** Meta for response (e.g. totalOpportunities). */
|
|
77
|
-
meta: import("@langchain/langgraph").BaseChannel<{
|
|
78
|
-
totalOpportunities: number;
|
|
79
|
-
}, {
|
|
80
|
-
totalOpportunities: number;
|
|
81
|
-
} | import("@langchain/langgraph").OverwriteValue<{
|
|
82
|
-
totalOpportunities: number;
|
|
83
|
-
}>, unknown>;
|
|
84
|
-
/** Timing records for each agent invocation within this graph run. */
|
|
85
|
-
agentTimings: import("@langchain/langgraph").BaseChannel<DebugMetaAgent[], DebugMetaAgent[] | import("@langchain/langgraph").OverwriteValue<DebugMetaAgent[]>, unknown>;
|
|
86
|
-
}>;
|