@indexnetwork/protocol 3.6.3-rc.272.1 → 3.6.4-rc.273.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/dist/opportunity/opportunity.presentation.d.ts +9 -0
- package/dist/opportunity/opportunity.presentation.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presentation.js +23 -0
- package/dist/opportunity/opportunity.presentation.js.map +1 -1
- package/dist/opportunity/opportunity.presenter.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presenter.js +9 -3
- package/dist/opportunity/opportunity.presenter.js.map +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,15 @@ export interface UserInfo {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function presentOpportunity(opp: Opportunity, viewerId: string, otherPartyInfo: UserInfo, introducerInfo: UserInfo | null, format: 'card' | 'email' | 'notification'): OpportunityPresentation;
|
|
21
21
|
export declare function stripUuids(text: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Truncate user-facing text to at most `maxChars` without cutting mid-word.
|
|
24
|
+
*
|
|
25
|
+
* Prefers a sentence boundary, then a word boundary, and only falls back to a
|
|
26
|
+
* hard slice if no boundary exists within the limit. An ellipsis is appended
|
|
27
|
+
* when the text is actually shortened. Used by presenter fallbacks so a degraded
|
|
28
|
+
* card never shows a sentence chopped mid-word (e.g. "His focus on 'indiv").
|
|
29
|
+
*/
|
|
30
|
+
export declare function truncateAtBoundary(text: string, maxChars: number): string;
|
|
22
31
|
/**
|
|
23
32
|
* Strips introducer mentions from opportunity summary text.
|
|
24
33
|
* Removes patterns like:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity.presentation.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.presentation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4CAA4C,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,GAAG,IAAI,EAC/B,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,cAAc,GACxC,uBAAuB,CAsEzB;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkB/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,MAAM,CAqFR;AA0BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,QAAQ,GAAE,MAAoC,EAC9C,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAmGR;AAOD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAiDR"}
|
|
1
|
+
{"version":3,"file":"opportunity.presentation.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.presentation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4CAA4C,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,GAAG,IAAI,EAC/B,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,cAAc,GACxC,uBAAuB,CAsEzB;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkB/C;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoBzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,MAAM,CAqFR;AA0BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,QAAQ,GAAE,MAAoC,EAC9C,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAmGR;AAOD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAiDR"}
|
|
@@ -96,6 +96,29 @@ export function stripUuids(text) {
|
|
|
96
96
|
.replace(/\s{2,}/g, ' ')
|
|
97
97
|
.trim();
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Truncate user-facing text to at most `maxChars` without cutting mid-word.
|
|
101
|
+
*
|
|
102
|
+
* Prefers a sentence boundary, then a word boundary, and only falls back to a
|
|
103
|
+
* hard slice if no boundary exists within the limit. An ellipsis is appended
|
|
104
|
+
* when the text is actually shortened. Used by presenter fallbacks so a degraded
|
|
105
|
+
* card never shows a sentence chopped mid-word (e.g. "His focus on 'indiv").
|
|
106
|
+
*/
|
|
107
|
+
export function truncateAtBoundary(text, maxChars) {
|
|
108
|
+
const trimmed = text.trim();
|
|
109
|
+
if (trimmed.length <= maxChars)
|
|
110
|
+
return trimmed;
|
|
111
|
+
const slice = trimmed.slice(0, maxChars);
|
|
112
|
+
// Prefer ending on the last completed sentence within the limit.
|
|
113
|
+
const lastSentence = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf("! "), slice.lastIndexOf("? "));
|
|
114
|
+
if (lastSentence >= maxChars * 0.5) {
|
|
115
|
+
return slice.slice(0, lastSentence + 1).trim();
|
|
116
|
+
}
|
|
117
|
+
// Otherwise back off to the last whole word and add an ellipsis.
|
|
118
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
119
|
+
const body = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
|
|
120
|
+
return body.replace(/[\s,;:.!?'"-]+$/, "").trim() + "\u2026";
|
|
121
|
+
}
|
|
99
122
|
/**
|
|
100
123
|
* Strips introducer mentions from opportunity summary text.
|
|
101
124
|
* Removes patterns like:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity.presentation.js","sourceRoot":"/","sources":["opportunity/opportunity.presentation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure presentation layer for opportunities.\n * Generates title, description, and CTA based on viewer context — no DB access.\n */\n\nimport type { Opportunity } from '../shared/interfaces/database.interface.js';\nimport { MINIMAL_MAIN_TEXT_MAX_CHARS } from \"./opportunity.labels.js\";\n\nexport interface OpportunityPresentation {\n title: string;\n description: string;\n callToAction: string;\n}\n\nexport interface UserInfo {\n id: string;\n name: string;\n avatar: string | null;\n}\n\n/**\n * Generate presentation copy for an opportunity based on viewer context.\n * Pure function — no side effects, no database access.\n */\nexport function presentOpportunity(\n opp: Opportunity,\n viewerId: string,\n otherPartyInfo: UserInfo,\n introducerInfo: UserInfo | null,\n format: 'card' | 'email' | 'notification'\n): OpportunityPresentation {\n const myActor = opp.actors.find((a) => a.userId === viewerId);\n const introducer = opp.actors.find((a) => a.role === 'introducer');\n\n if (!myActor) {\n throw new Error('Viewer is not an actor in this opportunity');\n }\n\n const otherName = otherPartyInfo.name;\n let title: string;\n let description: string;\n let descriptionIsReasoning = false;\n\n switch (myActor.role) {\n case 'agent':\n title = `You can help ${otherName}`;\n description = `Based on your expertise, ${otherName} might benefit from connecting with you.`;\n break;\n case 'patient':\n title = `${otherName} might be able to help you`;\n description = `${otherName} has skills that align with what you're looking for.`;\n break;\n case 'peer':\n title = `Potential collaboration with ${otherName}`;\n description = `You and ${otherName} have complementary interests.`;\n break;\n case 'mentee':\n title = `${otherName} could mentor you`;\n description = `${otherName} has experience that could help guide your journey.`;\n break;\n case 'mentor':\n title = `${otherName} is looking for guidance`;\n description = `Your expertise could help ${otherName} on their path.`;\n break;\n case 'founder':\n title = `${otherName} might be interested in your venture`;\n description = `${otherName}'s investment focus aligns with what you're building.`;\n break;\n case 'investor':\n title = `${otherName} is building something interesting`;\n description = `${otherName}'s venture might fit your investment thesis.`;\n break;\n case 'party':\n default:\n if (introducer && introducerInfo) {\n title = `${introducerInfo.name} thinks you should meet ${otherName}`;\n description = opp.interpretation.reasoning;\n descriptionIsReasoning = true;\n } else {\n title = `Opportunity with ${otherName}`;\n description = opp.interpretation.reasoning;\n descriptionIsReasoning = true;\n }\n break;\n }\n\n if (!descriptionIsReasoning) {\n description += `\\n\\n${opp.interpretation.reasoning}`;\n }\n\n if (format === 'notification') {\n description =\n description.length > 100 ? description.slice(0, 97) + '...' : description;\n }\n\n return {\n title,\n description,\n callToAction: 'View Opportunity',\n };\n}\n\n/**\n * Strips UUID patterns from user-facing text to prevent internal ID leaks.\n */\n\nconst UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\n\nexport function stripUuids(text: string): string {\n return text\n .replace(/\\(([^)]*)\\)/g, (_match, inner: string) => {\n if (!UUID_PATTERN.test(inner)) {\n UUID_PATTERN.lastIndex = 0;\n return _match;\n }\n UUID_PATTERN.lastIndex = 0;\n const cleaned = inner\n .replace(UUID_PATTERN, '')\n .replace(/,\\s*,/g, ',')\n .replace(/\\b(?:from|and)\\b/gi, '')\n .replace(/^[\\s,]+|[\\s,]+$/g, '');\n return cleaned ? `(${cleaned})` : '';\n })\n .replace(UUID_PATTERN, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\n/**\n * Strips introducer mentions from opportunity summary text.\n * Removes patterns like:\n * - \"[Introducer] introduced you to [Counterpart]\"\n * - \"[Introducer] thinks you should meet [Counterpart]\"\n * - \"[Introducer] connected you to [Counterpart]\"\n * - \"[Introducer] suggested you meet [Counterpart]\"\n *\n * @param text - The text to clean (personalizedSummary)\n * @param introducerName - Full name of the introducer to strip\n * @returns Text with introducer mentions removed, counterpart preserved\n */\nexport function stripIntroducerMentions(\n text: string,\n introducerName: string | undefined,\n): string {\n if (!introducerName?.trim()) return text;\n\n const fullName = introducerName.trim();\n const firstName = fullName.split(/\\s+/)[0];\n const namesToCheck = [fullName];\n if (firstName && firstName.length > 1) {\n namesToCheck.push(firstName);\n }\n\n let result = text;\n\n for (const [idx, name] of namesToCheck.entries()) {\n const escapedName = escapeRegex(name);\n\n // Pattern: \"Name introduced you to \" (with or without comma, optionally with \"directly\")\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+introduced\\\\s+you\\\\s+(?:directly\\\\s+)?to\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name thinks you should meet \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+thinks\\\\s+you\\\\s+should\\\\s+meet\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name connected you to \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+connected\\\\s+you\\\\s+(?:to|with)\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name suggested you meet \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+suggested\\\\s+you\\\\s+(?:meet|connect\\\\s+(?:to|with))\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name recommended you meet \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+recommended\\\\s+you\\\\s+(?:meet|connect)\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name thinks you and Counterpart should meet\" -> remove entire phrase up to Counterpart\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+thinks\\\\s+you\\\\s+and\\\\s+`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name also thought...\" - remove sentences starting with Name + also/also thought\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+(?:also\\\\s+)?(?:thought|thinks?|believes?|felt)\\\\s*`, \"gi\"),\n \"\",\n );\n\n // General: Remove any remaining standalone mention of the introducer name at sentence start.\n // Only apply for fullName (idx === 0) to avoid stripping valid counterpart first names\n // (e.g. \"David Smith\" intro to \"David Johnson\" → we strip \"David Smith\" but not \"David\" in \"David Johnson\").\n if (idx === 0) {\n result = result.replace(\n new RegExp(`(?:^|\\\\.\\\\s*)\\\\b${escapedName}\\\\s+`, \"gi\"),\n (match, offset) => {\n if (offset === 0 || match.startsWith(\".\")) {\n return match.startsWith(\".\") ? \". \" : \"\";\n }\n return match;\n },\n );\n }\n }\n\n // Clean up: remove leading/trailing whitespace and common punctuation artifacts\n result = result\n .replace(/^[\\,\\s]+/, \"\") // Remove leading commas/spaces\n .replace(/\\s{2,}/g, \" \") // Normalize multiple spaces\n .trim();\n\n // Capitalize first letter if we removed from start\n if (result.length > 0) {\n result = result.charAt(0).toUpperCase() + result.slice(1);\n }\n\n return result;\n}\n\n// Helper function\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Viewer-centric text for opportunity cards.\n * The card is shown to the viewer (logged-in user) and should introduce the\n * counterpart, not describe the viewer to themselves.\n */\n\n/**\n * Splits text into sentences using (?<=[.!?])\\s+ (period/exclamation/question followed by whitespace).\n * Note: splits after any such punctuation, including abbreviations like \"Dr.\" or \"e.g.\".\n */\nfunction splitSentences(text: string): string[] {\n const trimmed = text.trim();\n if (!trimmed) return [];\n return trimmed\n .split(/(?<=[.!?])\\s+/)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Returns viewer-centric main text for an opportunity card.\n * Prefers the part of the reasoning that describes the counterpart (the person\n * on the card), so the viewer sees an introduction to the counterpart rather\n * than a description of themselves.\n *\n * @param reasoning - Raw interpretation.reasoning (may describe both parties).\n * @param counterpartName - Display name of the suggested connection (e.g. \"Alex Chen\").\n * @param maxChars - Max length of returned string (default MINIMAL_MAIN_TEXT_MAX_CHARS).\n * @param viewerName - Optional display name of the viewer (signed-in user). When provided, sentences or prefixes describing the viewer are skipped so the card introduces the counterpart, not the viewer.\n * @param introducerName - Optional display name of the introducer. When provided, introducer phrases (e.g., \"X introduced you to...\") are stripped from the summary to keep the body text focused on match quality.\n * @returns Viewer-centric snippet mentioning the counterpart when possible; if counterpartName is empty, returns reasoning truncated to maxChars. Never null; may be \"A suggested connection.\" when reasoning is empty.\n */\nexport function viewerCentricCardSummary(\n reasoning: string,\n counterpartName: string,\n maxChars: number = MINIMAL_MAIN_TEXT_MAX_CHARS,\n viewerName?: string,\n introducerName?: string,\n): string {\n const raw = stripUuids(reasoning);\n if (!raw) return \"A suggested connection.\";\n\n const name = counterpartName.trim();\n if (!name) {\n let out = raw.length <= maxChars ? raw : raw.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName);\n return out;\n }\n\n const sentences = splitSentences(raw);\n const nameLower = name.toLowerCase();\n const firstWordOfName = name.split(/\\s+/)[0]?.toLowerCase();\n const hasCounterpartName = (s: string) =>\n s.toLowerCase().includes(nameLower) ||\n (firstWordOfName && firstWordOfName.length > 1 && s.toLowerCase().includes(firstWordOfName));\n\n const viewer = viewerName?.trim().toLowerCase();\n const viewerFirstWord = viewerName?.trim().split(/\\s+/)[0]?.toLowerCase();\n const startsWithViewer = (s: string) => {\n if (!viewer) return false;\n const sl = s.toLowerCase();\n return sl.startsWith(viewer) ||\n (viewerFirstWord && viewerFirstWord.length > 1 && sl.startsWith(viewerFirstWord));\n };\n\n // When viewerName is provided, prefer sentences that mention the counterpart\n // but do NOT start with the viewer's name.\n if (viewer) {\n // First pass: find a sentence that mentions counterpart and doesn't start with viewer\n const cleanIdx = sentences.findIndex(\n (s) => hasCounterpartName(s) && !startsWithViewer(s),\n );\n if (cleanIdx !== -1) {\n const result = sentences.slice(cleanIdx).join(\" \").trim();\n let out = result.length <= maxChars ? result : result.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n }\n\n // Second pass: sentence mentions counterpart but starts with viewer (compound sentence).\n // Try to extract the counterpart portion after the counterpart's name.\n const compoundIdx = sentences.findIndex(\n (s) => hasCounterpartName(s) && startsWithViewer(s),\n );\n if (compoundIdx !== -1) {\n const sentence = sentences[compoundIdx];\n // Find where the counterpart name appears and extract from there\n // Use case-insensitive Unicode-aware regex so the index is correct\n // even when toLowerCase() changes string length (e.g. Turkish İ→i, German ß→ss).\n const cpMatch = sentence.match(new RegExp(escapeRegex(name), \"iu\"));\n const cpIdx = cpMatch?.index ?? -1;\n if (cpIdx > 0) {\n const extracted = sentence.slice(cpIdx).trim();\n const rest = sentences.slice(compoundIdx + 1).join(\" \").trim();\n const result = rest ? `${extracted} ${rest}` : extracted;\n let out = result.length <= maxChars ? result : result.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n }\n }\n }\n\n // Fallback: original logic without viewer awareness\n const idx = sentences.findIndex(hasCounterpartName);\n if (idx === -1) {\n let out = raw.length <= maxChars ? raw : raw.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n }\n\n const fromCounterpart = sentences.slice(idx).join(\" \").trim();\n let out =\n fromCounterpart.length <= maxChars\n ? fromCounterpart\n : fromCounterpart.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n}\n\n/** Max length for narrator chip text (matches LLM presenter schema). */\nconst NARRATOR_MAX_CHARS = 80;\n\nconst FALLBACK_REMARK = \"A potential connection worth exploring.\";\n\n/**\n * Generates a short narrator remark from opportunity reasoning for the narrator chip.\n * Used by the minimal (no-LLM) card path so each card gets a unique remark\n * instead of the same static text.\n *\n * Extracts domain keywords (e.g. \"AI\", \"design\", \"machine learning\") from the\n * reasoning and frames them in a short template like \"Shared interest in AI and design.\"\n *\n * This is a regex-based heuristic — an alternative is OpportunityPresenter.presentHomeCard()\n * which generates narratorRemark via LLM with much higher quality (already used by\n * home.graph.ts and opportunity.discover.ts). See buildMinimalOpportunityCard() in\n * opportunity.tools.ts for the trade-off discussion.\n *\n * @param reasoning - Raw interpretation.reasoning text.\n * @param counterpartName - Display name of the counterpart (stripped from output).\n * @param viewerName - Optional display name of the viewer (stripped from output).\n * @returns A short remark (max ~80 chars) suitable for the narrator chip. Never truncated with \"...\".\n */\nexport function narratorRemarkFromReasoning(\n reasoning: string,\n counterpartName: string,\n viewerName?: string,\n): string {\n const raw = stripUuids(reasoning).trim();\n if (!raw) return FALLBACK_REMARK;\n\n // Strip all person names from the text so we work only with topics.\n let cleaned = raw;\n for (const name of [counterpartName, viewerName]) {\n if (!name?.trim()) continue;\n const full = name.trim();\n cleaned = cleaned.replace(new RegExp(escapeRegex(full), \"gi\"), \"\").trim();\n const first = full.split(/\\s+/)[0];\n if (first && first.length > 1) {\n cleaned = cleaned.replace(new RegExp(`\\\\b${escapeRegex(first)}\\\\b`, \"gi\"), \"\").trim();\n }\n }\n\n // Extract domain/topic noun phrases from the cleaned text.\n // Match multi-word capitalized phrases (e.g. \"AI operations toolkit\") and\n // known domain terms.\n const domainTerms = extractDomainTerms(cleaned);\n\n if (domainTerms.length > 0) {\n // Build \"Shared interest in X and Y.\" or \"Overlap in X, Y, and Z.\"\n const prefixes = [\n \"Shared interest in\",\n \"Overlap in\",\n \"Common ground in\",\n \"Aligned on\",\n \"Mutual interest in\",\n ];\n // Pick prefix deterministically based on first term's char code\n const prefixIdx = domainTerms[0].charCodeAt(0) % prefixes.length;\n const prefix = prefixes[prefixIdx];\n const joined = joinTerms(domainTerms, NARRATOR_MAX_CHARS - prefix.length - 2); // -2 for \" \" and \".\"\n const remark = `${prefix} ${joined}.`;\n if (remark.length <= NARRATOR_MAX_CHARS) return remark;\n }\n\n // Fallback: try to extract a short relationship phrase\n const relationshipMatch = cleaned.match(\n /\\b(complementary skills|shared expertise|overlapping intents|similar interests|strong match|mutual fit|potential collaboration|looking for (?:a |an )?[\\w\\s]{3,20})\\b/i,\n );\n if (relationshipMatch) {\n const phrase = relationshipMatch[0];\n const remark = `Spotted ${phrase.toLowerCase()}.`;\n if (remark.length <= NARRATOR_MAX_CHARS) return remark;\n }\n\n return FALLBACK_REMARK;\n}\n\n/**\n * Extracts domain/topic terms from text by matching known patterns:\n * - Acronyms (AI, ML, UX, API)\n * - Multi-word domain phrases (machine learning, game development)\n * - Capitalized proper nouns that look like topics\n */\nfunction extractDomainTerms(text: string): string[] {\n const seen = new Set<string>();\n const terms: string[] = [];\n\n // Known domain phrases (order matters — longer first)\n const knownPhrases = [\n /\\b(machine learning|artificial intelligence|software development|game development|web development|data science|deep learning|natural language processing|computer vision|cloud computing|mobile development|product design|user experience|graphic design|character design|frontend development|backend development|full[- ]stack|smart contracts|visual art|creative writing|content creation|digital marketing|venture capital|angel invest(?:ing|ment)|open source|blockchain|cryptocurrency|decentralized finance|social impact|community building|music production|film(?:making| production)|photography|illustration|animation|3D modeling|startup|co-?founding|entrepreneurship|research|consulting|mentoring|freelanc(?:e|ing))\\b/gi,\n /\\b(AI|ML|UX|UI|API|NLP|SaaS|DeFi|DevOps|DeSci|NFT|DAO|React|Node|Python|TypeScript|JavaScript|Rust|Solidity|Go|Swift|Kotlin|Figma|Blender|Unity|Unreal)\\b/g,\n ];\n\n for (const pattern of knownPhrases) {\n for (const match of text.matchAll(pattern)) {\n const term = match[1] ?? match[0];\n const key = term.toLowerCase();\n if (!seen.has(key)) {\n seen.add(key);\n // Preserve case for short acronyms/proper nouns; lowercase multi-word phrases\n if (term.length <= 5 && /^[A-Z]/.test(term)) {\n terms.push(term); // Keep React, AI, ML, etc. as-is\n } else {\n terms.push(key);\n }\n }\n }\n }\n\n // If no known phrases found, look for capitalized multi-word phrases\n // that look like explicit topic references (e.g. \"Visual Art\", \"Smart Contracts\").\n // Only accept capitalized words to avoid grabbing meta-language from evaluator reasoning\n // (e.g. \"discoverer\", \"explicitly\", \"states\" which are about the matching process, not topics).\n if (terms.length === 0) {\n // Multi-word capitalized phrases first (e.g. \"Visual Art\", \"Creative Writing\")\n const multiWordPattern = /\\b([A-Z][a-z]+(?:\\s+[A-Z][a-z]+)+)\\b/g;\n for (const match of text.matchAll(multiWordPattern)) {\n const term = match[1];\n const key = term.toLowerCase();\n if (!seen.has(key)) {\n seen.add(key);\n terms.push(key);\n if (terms.length >= 3) break;\n }\n }\n\n // Single capitalized words as last resort (skip common sentence-starters and meta-words)\n if (terms.length === 0) {\n const skipCapitalized = new Set([\n // Articles / conjunctions / prepositions (capitalized at sentence start)\n \"the\", \"and\", \"but\", \"for\", \"from\", \"with\", \"without\", \"between\",\n \"into\", \"about\", \"after\", \"before\", \"over\", \"under\", \"through\",\n // Common sentence starters / pronouns / determiners\n \"both\", \"their\", \"they\", \"this\", \"that\", \"these\", \"those\",\n \"here\", \"there\", \"would\", \"could\", \"should\", \"also\", \"very\",\n \"one\", \"another\", \"other\", \"each\", \"some\", \"many\", \"most\",\n \"such\", \"clear\", \"high\", \"good\", \"well\", \"just\", \"even\",\n // Generic matching/relationship language\n \"strong\", \"match\", \"based\", \"making\", \"looking\", \"seeking\",\n \"connection\", \"relationship\", \"opportunity\", \"overlap\",\n \"complementary\", \"potential\", \"interested\", \"collaborate\",\n // Evaluator meta-language (about the matching process, not topics)\n \"intent\", \"intents\", \"profile\", \"user\", \"users\", \"person\",\n \"discoverer\", \"explicitly\", \"states\", \"expressed\", \"mentioned\",\n \"indicates\", \"suggests\", \"demonstrates\", \"describes\", \"involves\",\n \"inference\", \"preparatory\", \"sincerity\", \"evaluator\", \"classifier\",\n \"semantic\", \"pragmatic\", \"verification\", \"reconciliation\",\n \"assertive\", \"commissive\", \"directive\", \"illocutionary\",\n \"felicity\", \"utterance\", \"detected\", \"analysis\", \"confirmed\",\n \"genuine\", \"conditions\", \"determined\",\n // Discourse markers\n \"particularly\", \"specifically\", \"especially\", \"primarily\",\n \"overall\", \"furthermore\", \"however\", \"therefore\", \"moreover\",\n ]);\n const capWords = text.match(/\\b[A-Z][a-z]{2,}\\b/g) ?? [];\n for (const w of capWords) {\n const key = w.toLowerCase();\n if (!skipCapitalized.has(key) && !seen.has(key)) {\n seen.add(key);\n terms.push(key);\n if (terms.length >= 3) break;\n }\n }\n }\n }\n\n return terms.slice(0, 3); // Max 3 terms\n}\n\n/** Joins terms into \"X, Y, and Z\" form, dropping terms if too long. */\nfunction joinTerms(terms: string[], maxLen: number): string {\n if (terms.length === 1) return terms[0];\n // Try all terms first\n for (let count = terms.length; count >= 1; count--) {\n const subset = terms.slice(0, count);\n let joined: string;\n if (subset.length === 1) {\n joined = subset[0];\n } else if (subset.length === 2) {\n joined = `${subset[0]} and ${subset[1]}`;\n } else {\n joined = `${subset.slice(0, -1).join(\", \")}, and ${subset[subset.length - 1]}`;\n }\n if (joined.length <= maxLen) return joined;\n }\n return terms[0].slice(0, maxLen);\n}\n\n/**\n * Replaces viewer's name with \"you\"/\"your\" so the card addresses the viewer in second person.\n * Applied to mainText when viewerName is provided.\n * @param otherNames - Other actor names in the card; first-name replacement is\n * skipped when the viewer's first name matches any other actor's first name.\n */\nfunction replaceViewerNameWithYou(text: string, viewerName?: string, otherNames?: string[]): string {\n if (!viewerName?.trim()) return text;\n const full = viewerName.trim();\n const first = full.split(/\\s+/)[0];\n let out = text;\n // Possessive: \"Yankı's\" → \"your\", \"Yankı Ekin Yüksel's\" → \"your\"\n out = out.replace(new RegExp(`\\\\b${escapeRegex(full)}'s\\\\b`, \"gi\"), \"your\");\n\n const otherFirstNames = (otherNames ?? [])\n .map(n => n.trim().split(/\\s+/)[0]?.toLowerCase())\n .filter(Boolean);\n const firstNameCollides = first && otherFirstNames.includes(first.toLowerCase());\n\n if (first && first.length > 1 && !firstNameCollides) {\n out = out.replace(new RegExp(`\\\\b${escapeRegex(first)}'s\\\\b`, \"gi\"), \"your\");\n }\n // Standalone: full name then first name so we don't break \"Yankı Ekin Yüksel\"\n out = out.replace(new RegExp(`\\\\b${escapeRegex(full)}\\\\b`, \"gi\"), \"you\");\n if (first && first.length > 1 && !firstNameCollides) {\n out = out.replace(new RegExp(`\\\\b${escapeRegex(first)}\\\\b`, \"gi\"), \"you\");\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"opportunity.presentation.js","sourceRoot":"/","sources":["opportunity/opportunity.presentation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure presentation layer for opportunities.\n * Generates title, description, and CTA based on viewer context — no DB access.\n */\n\nimport type { Opportunity } from '../shared/interfaces/database.interface.js';\nimport { MINIMAL_MAIN_TEXT_MAX_CHARS } from \"./opportunity.labels.js\";\n\nexport interface OpportunityPresentation {\n title: string;\n description: string;\n callToAction: string;\n}\n\nexport interface UserInfo {\n id: string;\n name: string;\n avatar: string | null;\n}\n\n/**\n * Generate presentation copy for an opportunity based on viewer context.\n * Pure function — no side effects, no database access.\n */\nexport function presentOpportunity(\n opp: Opportunity,\n viewerId: string,\n otherPartyInfo: UserInfo,\n introducerInfo: UserInfo | null,\n format: 'card' | 'email' | 'notification'\n): OpportunityPresentation {\n const myActor = opp.actors.find((a) => a.userId === viewerId);\n const introducer = opp.actors.find((a) => a.role === 'introducer');\n\n if (!myActor) {\n throw new Error('Viewer is not an actor in this opportunity');\n }\n\n const otherName = otherPartyInfo.name;\n let title: string;\n let description: string;\n let descriptionIsReasoning = false;\n\n switch (myActor.role) {\n case 'agent':\n title = `You can help ${otherName}`;\n description = `Based on your expertise, ${otherName} might benefit from connecting with you.`;\n break;\n case 'patient':\n title = `${otherName} might be able to help you`;\n description = `${otherName} has skills that align with what you're looking for.`;\n break;\n case 'peer':\n title = `Potential collaboration with ${otherName}`;\n description = `You and ${otherName} have complementary interests.`;\n break;\n case 'mentee':\n title = `${otherName} could mentor you`;\n description = `${otherName} has experience that could help guide your journey.`;\n break;\n case 'mentor':\n title = `${otherName} is looking for guidance`;\n description = `Your expertise could help ${otherName} on their path.`;\n break;\n case 'founder':\n title = `${otherName} might be interested in your venture`;\n description = `${otherName}'s investment focus aligns with what you're building.`;\n break;\n case 'investor':\n title = `${otherName} is building something interesting`;\n description = `${otherName}'s venture might fit your investment thesis.`;\n break;\n case 'party':\n default:\n if (introducer && introducerInfo) {\n title = `${introducerInfo.name} thinks you should meet ${otherName}`;\n description = opp.interpretation.reasoning;\n descriptionIsReasoning = true;\n } else {\n title = `Opportunity with ${otherName}`;\n description = opp.interpretation.reasoning;\n descriptionIsReasoning = true;\n }\n break;\n }\n\n if (!descriptionIsReasoning) {\n description += `\\n\\n${opp.interpretation.reasoning}`;\n }\n\n if (format === 'notification') {\n description =\n description.length > 100 ? description.slice(0, 97) + '...' : description;\n }\n\n return {\n title,\n description,\n callToAction: 'View Opportunity',\n };\n}\n\n/**\n * Strips UUID patterns from user-facing text to prevent internal ID leaks.\n */\n\nconst UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\n\nexport function stripUuids(text: string): string {\n return text\n .replace(/\\(([^)]*)\\)/g, (_match, inner: string) => {\n if (!UUID_PATTERN.test(inner)) {\n UUID_PATTERN.lastIndex = 0;\n return _match;\n }\n UUID_PATTERN.lastIndex = 0;\n const cleaned = inner\n .replace(UUID_PATTERN, '')\n .replace(/,\\s*,/g, ',')\n .replace(/\\b(?:from|and)\\b/gi, '')\n .replace(/^[\\s,]+|[\\s,]+$/g, '');\n return cleaned ? `(${cleaned})` : '';\n })\n .replace(UUID_PATTERN, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\n/**\n * Truncate user-facing text to at most `maxChars` without cutting mid-word.\n *\n * Prefers a sentence boundary, then a word boundary, and only falls back to a\n * hard slice if no boundary exists within the limit. An ellipsis is appended\n * when the text is actually shortened. Used by presenter fallbacks so a degraded\n * card never shows a sentence chopped mid-word (e.g. \"His focus on 'indiv\").\n */\nexport function truncateAtBoundary(text: string, maxChars: number): string {\n const trimmed = text.trim();\n if (trimmed.length <= maxChars) return trimmed;\n\n const slice = trimmed.slice(0, maxChars);\n\n // Prefer ending on the last completed sentence within the limit.\n const lastSentence = Math.max(\n slice.lastIndexOf(\". \"),\n slice.lastIndexOf(\"! \"),\n slice.lastIndexOf(\"? \"),\n );\n if (lastSentence >= maxChars * 0.5) {\n return slice.slice(0, lastSentence + 1).trim();\n }\n\n // Otherwise back off to the last whole word and add an ellipsis.\n const lastSpace = slice.lastIndexOf(\" \");\n const body = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;\n return body.replace(/[\\s,;:.!?'\"-]+$/, \"\").trim() + \"\\u2026\";\n}\n\n/**\n * Strips introducer mentions from opportunity summary text.\n * Removes patterns like:\n * - \"[Introducer] introduced you to [Counterpart]\"\n * - \"[Introducer] thinks you should meet [Counterpart]\"\n * - \"[Introducer] connected you to [Counterpart]\"\n * - \"[Introducer] suggested you meet [Counterpart]\"\n *\n * @param text - The text to clean (personalizedSummary)\n * @param introducerName - Full name of the introducer to strip\n * @returns Text with introducer mentions removed, counterpart preserved\n */\nexport function stripIntroducerMentions(\n text: string,\n introducerName: string | undefined,\n): string {\n if (!introducerName?.trim()) return text;\n\n const fullName = introducerName.trim();\n const firstName = fullName.split(/\\s+/)[0];\n const namesToCheck = [fullName];\n if (firstName && firstName.length > 1) {\n namesToCheck.push(firstName);\n }\n\n let result = text;\n\n for (const [idx, name] of namesToCheck.entries()) {\n const escapedName = escapeRegex(name);\n\n // Pattern: \"Name introduced you to \" (with or without comma, optionally with \"directly\")\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+introduced\\\\s+you\\\\s+(?:directly\\\\s+)?to\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name thinks you should meet \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+thinks\\\\s+you\\\\s+should\\\\s+meet\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name connected you to \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+connected\\\\s+you\\\\s+(?:to|with)\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name suggested you meet \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+suggested\\\\s+you\\\\s+(?:meet|connect\\\\s+(?:to|with))\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name recommended you meet \"\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+recommended\\\\s+you\\\\s+(?:meet|connect)\\\\s*`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name thinks you and Counterpart should meet\" -> remove entire phrase up to Counterpart\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+thinks\\\\s+you\\\\s+and\\\\s+`, \"gi\"),\n \"\",\n );\n\n // Pattern: \"Name also thought...\" - remove sentences starting with Name + also/also thought\n result = result.replace(\n new RegExp(`\\\\b${escapedName}\\\\s+(?:also\\\\s+)?(?:thought|thinks?|believes?|felt)\\\\s*`, \"gi\"),\n \"\",\n );\n\n // General: Remove any remaining standalone mention of the introducer name at sentence start.\n // Only apply for fullName (idx === 0) to avoid stripping valid counterpart first names\n // (e.g. \"David Smith\" intro to \"David Johnson\" → we strip \"David Smith\" but not \"David\" in \"David Johnson\").\n if (idx === 0) {\n result = result.replace(\n new RegExp(`(?:^|\\\\.\\\\s*)\\\\b${escapedName}\\\\s+`, \"gi\"),\n (match, offset) => {\n if (offset === 0 || match.startsWith(\".\")) {\n return match.startsWith(\".\") ? \". \" : \"\";\n }\n return match;\n },\n );\n }\n }\n\n // Clean up: remove leading/trailing whitespace and common punctuation artifacts\n result = result\n .replace(/^[\\,\\s]+/, \"\") // Remove leading commas/spaces\n .replace(/\\s{2,}/g, \" \") // Normalize multiple spaces\n .trim();\n\n // Capitalize first letter if we removed from start\n if (result.length > 0) {\n result = result.charAt(0).toUpperCase() + result.slice(1);\n }\n\n return result;\n}\n\n// Helper function\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Viewer-centric text for opportunity cards.\n * The card is shown to the viewer (logged-in user) and should introduce the\n * counterpart, not describe the viewer to themselves.\n */\n\n/**\n * Splits text into sentences using (?<=[.!?])\\s+ (period/exclamation/question followed by whitespace).\n * Note: splits after any such punctuation, including abbreviations like \"Dr.\" or \"e.g.\".\n */\nfunction splitSentences(text: string): string[] {\n const trimmed = text.trim();\n if (!trimmed) return [];\n return trimmed\n .split(/(?<=[.!?])\\s+/)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Returns viewer-centric main text for an opportunity card.\n * Prefers the part of the reasoning that describes the counterpart (the person\n * on the card), so the viewer sees an introduction to the counterpart rather\n * than a description of themselves.\n *\n * @param reasoning - Raw interpretation.reasoning (may describe both parties).\n * @param counterpartName - Display name of the suggested connection (e.g. \"Alex Chen\").\n * @param maxChars - Max length of returned string (default MINIMAL_MAIN_TEXT_MAX_CHARS).\n * @param viewerName - Optional display name of the viewer (signed-in user). When provided, sentences or prefixes describing the viewer are skipped so the card introduces the counterpart, not the viewer.\n * @param introducerName - Optional display name of the introducer. When provided, introducer phrases (e.g., \"X introduced you to...\") are stripped from the summary to keep the body text focused on match quality.\n * @returns Viewer-centric snippet mentioning the counterpart when possible; if counterpartName is empty, returns reasoning truncated to maxChars. Never null; may be \"A suggested connection.\" when reasoning is empty.\n */\nexport function viewerCentricCardSummary(\n reasoning: string,\n counterpartName: string,\n maxChars: number = MINIMAL_MAIN_TEXT_MAX_CHARS,\n viewerName?: string,\n introducerName?: string,\n): string {\n const raw = stripUuids(reasoning);\n if (!raw) return \"A suggested connection.\";\n\n const name = counterpartName.trim();\n if (!name) {\n let out = raw.length <= maxChars ? raw : raw.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName);\n return out;\n }\n\n const sentences = splitSentences(raw);\n const nameLower = name.toLowerCase();\n const firstWordOfName = name.split(/\\s+/)[0]?.toLowerCase();\n const hasCounterpartName = (s: string) =>\n s.toLowerCase().includes(nameLower) ||\n (firstWordOfName && firstWordOfName.length > 1 && s.toLowerCase().includes(firstWordOfName));\n\n const viewer = viewerName?.trim().toLowerCase();\n const viewerFirstWord = viewerName?.trim().split(/\\s+/)[0]?.toLowerCase();\n const startsWithViewer = (s: string) => {\n if (!viewer) return false;\n const sl = s.toLowerCase();\n return sl.startsWith(viewer) ||\n (viewerFirstWord && viewerFirstWord.length > 1 && sl.startsWith(viewerFirstWord));\n };\n\n // When viewerName is provided, prefer sentences that mention the counterpart\n // but do NOT start with the viewer's name.\n if (viewer) {\n // First pass: find a sentence that mentions counterpart and doesn't start with viewer\n const cleanIdx = sentences.findIndex(\n (s) => hasCounterpartName(s) && !startsWithViewer(s),\n );\n if (cleanIdx !== -1) {\n const result = sentences.slice(cleanIdx).join(\" \").trim();\n let out = result.length <= maxChars ? result : result.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n }\n\n // Second pass: sentence mentions counterpart but starts with viewer (compound sentence).\n // Try to extract the counterpart portion after the counterpart's name.\n const compoundIdx = sentences.findIndex(\n (s) => hasCounterpartName(s) && startsWithViewer(s),\n );\n if (compoundIdx !== -1) {\n const sentence = sentences[compoundIdx];\n // Find where the counterpart name appears and extract from there\n // Use case-insensitive Unicode-aware regex so the index is correct\n // even when toLowerCase() changes string length (e.g. Turkish İ→i, German ß→ss).\n const cpMatch = sentence.match(new RegExp(escapeRegex(name), \"iu\"));\n const cpIdx = cpMatch?.index ?? -1;\n if (cpIdx > 0) {\n const extracted = sentence.slice(cpIdx).trim();\n const rest = sentences.slice(compoundIdx + 1).join(\" \").trim();\n const result = rest ? `${extracted} ${rest}` : extracted;\n let out = result.length <= maxChars ? result : result.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n }\n }\n }\n\n // Fallback: original logic without viewer awareness\n const idx = sentences.findIndex(hasCounterpartName);\n if (idx === -1) {\n let out = raw.length <= maxChars ? raw : raw.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n }\n\n const fromCounterpart = sentences.slice(idx).join(\" \").trim();\n let out =\n fromCounterpart.length <= maxChars\n ? fromCounterpart\n : fromCounterpart.slice(0, maxChars) + \"...\";\n // Strip introducer mentions BEFORE replacing viewer name to avoid \"you introduced...\" artifacts\n if (introducerName) {\n out = stripIntroducerMentions(out, introducerName);\n }\n out = replaceViewerNameWithYou(out, viewerName, [name]);\n return out;\n}\n\n/** Max length for narrator chip text (matches LLM presenter schema). */\nconst NARRATOR_MAX_CHARS = 80;\n\nconst FALLBACK_REMARK = \"A potential connection worth exploring.\";\n\n/**\n * Generates a short narrator remark from opportunity reasoning for the narrator chip.\n * Used by the minimal (no-LLM) card path so each card gets a unique remark\n * instead of the same static text.\n *\n * Extracts domain keywords (e.g. \"AI\", \"design\", \"machine learning\") from the\n * reasoning and frames them in a short template like \"Shared interest in AI and design.\"\n *\n * This is a regex-based heuristic — an alternative is OpportunityPresenter.presentHomeCard()\n * which generates narratorRemark via LLM with much higher quality (already used by\n * home.graph.ts and opportunity.discover.ts). See buildMinimalOpportunityCard() in\n * opportunity.tools.ts for the trade-off discussion.\n *\n * @param reasoning - Raw interpretation.reasoning text.\n * @param counterpartName - Display name of the counterpart (stripped from output).\n * @param viewerName - Optional display name of the viewer (stripped from output).\n * @returns A short remark (max ~80 chars) suitable for the narrator chip. Never truncated with \"...\".\n */\nexport function narratorRemarkFromReasoning(\n reasoning: string,\n counterpartName: string,\n viewerName?: string,\n): string {\n const raw = stripUuids(reasoning).trim();\n if (!raw) return FALLBACK_REMARK;\n\n // Strip all person names from the text so we work only with topics.\n let cleaned = raw;\n for (const name of [counterpartName, viewerName]) {\n if (!name?.trim()) continue;\n const full = name.trim();\n cleaned = cleaned.replace(new RegExp(escapeRegex(full), \"gi\"), \"\").trim();\n const first = full.split(/\\s+/)[0];\n if (first && first.length > 1) {\n cleaned = cleaned.replace(new RegExp(`\\\\b${escapeRegex(first)}\\\\b`, \"gi\"), \"\").trim();\n }\n }\n\n // Extract domain/topic noun phrases from the cleaned text.\n // Match multi-word capitalized phrases (e.g. \"AI operations toolkit\") and\n // known domain terms.\n const domainTerms = extractDomainTerms(cleaned);\n\n if (domainTerms.length > 0) {\n // Build \"Shared interest in X and Y.\" or \"Overlap in X, Y, and Z.\"\n const prefixes = [\n \"Shared interest in\",\n \"Overlap in\",\n \"Common ground in\",\n \"Aligned on\",\n \"Mutual interest in\",\n ];\n // Pick prefix deterministically based on first term's char code\n const prefixIdx = domainTerms[0].charCodeAt(0) % prefixes.length;\n const prefix = prefixes[prefixIdx];\n const joined = joinTerms(domainTerms, NARRATOR_MAX_CHARS - prefix.length - 2); // -2 for \" \" and \".\"\n const remark = `${prefix} ${joined}.`;\n if (remark.length <= NARRATOR_MAX_CHARS) return remark;\n }\n\n // Fallback: try to extract a short relationship phrase\n const relationshipMatch = cleaned.match(\n /\\b(complementary skills|shared expertise|overlapping intents|similar interests|strong match|mutual fit|potential collaboration|looking for (?:a |an )?[\\w\\s]{3,20})\\b/i,\n );\n if (relationshipMatch) {\n const phrase = relationshipMatch[0];\n const remark = `Spotted ${phrase.toLowerCase()}.`;\n if (remark.length <= NARRATOR_MAX_CHARS) return remark;\n }\n\n return FALLBACK_REMARK;\n}\n\n/**\n * Extracts domain/topic terms from text by matching known patterns:\n * - Acronyms (AI, ML, UX, API)\n * - Multi-word domain phrases (machine learning, game development)\n * - Capitalized proper nouns that look like topics\n */\nfunction extractDomainTerms(text: string): string[] {\n const seen = new Set<string>();\n const terms: string[] = [];\n\n // Known domain phrases (order matters — longer first)\n const knownPhrases = [\n /\\b(machine learning|artificial intelligence|software development|game development|web development|data science|deep learning|natural language processing|computer vision|cloud computing|mobile development|product design|user experience|graphic design|character design|frontend development|backend development|full[- ]stack|smart contracts|visual art|creative writing|content creation|digital marketing|venture capital|angel invest(?:ing|ment)|open source|blockchain|cryptocurrency|decentralized finance|social impact|community building|music production|film(?:making| production)|photography|illustration|animation|3D modeling|startup|co-?founding|entrepreneurship|research|consulting|mentoring|freelanc(?:e|ing))\\b/gi,\n /\\b(AI|ML|UX|UI|API|NLP|SaaS|DeFi|DevOps|DeSci|NFT|DAO|React|Node|Python|TypeScript|JavaScript|Rust|Solidity|Go|Swift|Kotlin|Figma|Blender|Unity|Unreal)\\b/g,\n ];\n\n for (const pattern of knownPhrases) {\n for (const match of text.matchAll(pattern)) {\n const term = match[1] ?? match[0];\n const key = term.toLowerCase();\n if (!seen.has(key)) {\n seen.add(key);\n // Preserve case for short acronyms/proper nouns; lowercase multi-word phrases\n if (term.length <= 5 && /^[A-Z]/.test(term)) {\n terms.push(term); // Keep React, AI, ML, etc. as-is\n } else {\n terms.push(key);\n }\n }\n }\n }\n\n // If no known phrases found, look for capitalized multi-word phrases\n // that look like explicit topic references (e.g. \"Visual Art\", \"Smart Contracts\").\n // Only accept capitalized words to avoid grabbing meta-language from evaluator reasoning\n // (e.g. \"discoverer\", \"explicitly\", \"states\" which are about the matching process, not topics).\n if (terms.length === 0) {\n // Multi-word capitalized phrases first (e.g. \"Visual Art\", \"Creative Writing\")\n const multiWordPattern = /\\b([A-Z][a-z]+(?:\\s+[A-Z][a-z]+)+)\\b/g;\n for (const match of text.matchAll(multiWordPattern)) {\n const term = match[1];\n const key = term.toLowerCase();\n if (!seen.has(key)) {\n seen.add(key);\n terms.push(key);\n if (terms.length >= 3) break;\n }\n }\n\n // Single capitalized words as last resort (skip common sentence-starters and meta-words)\n if (terms.length === 0) {\n const skipCapitalized = new Set([\n // Articles / conjunctions / prepositions (capitalized at sentence start)\n \"the\", \"and\", \"but\", \"for\", \"from\", \"with\", \"without\", \"between\",\n \"into\", \"about\", \"after\", \"before\", \"over\", \"under\", \"through\",\n // Common sentence starters / pronouns / determiners\n \"both\", \"their\", \"they\", \"this\", \"that\", \"these\", \"those\",\n \"here\", \"there\", \"would\", \"could\", \"should\", \"also\", \"very\",\n \"one\", \"another\", \"other\", \"each\", \"some\", \"many\", \"most\",\n \"such\", \"clear\", \"high\", \"good\", \"well\", \"just\", \"even\",\n // Generic matching/relationship language\n \"strong\", \"match\", \"based\", \"making\", \"looking\", \"seeking\",\n \"connection\", \"relationship\", \"opportunity\", \"overlap\",\n \"complementary\", \"potential\", \"interested\", \"collaborate\",\n // Evaluator meta-language (about the matching process, not topics)\n \"intent\", \"intents\", \"profile\", \"user\", \"users\", \"person\",\n \"discoverer\", \"explicitly\", \"states\", \"expressed\", \"mentioned\",\n \"indicates\", \"suggests\", \"demonstrates\", \"describes\", \"involves\",\n \"inference\", \"preparatory\", \"sincerity\", \"evaluator\", \"classifier\",\n \"semantic\", \"pragmatic\", \"verification\", \"reconciliation\",\n \"assertive\", \"commissive\", \"directive\", \"illocutionary\",\n \"felicity\", \"utterance\", \"detected\", \"analysis\", \"confirmed\",\n \"genuine\", \"conditions\", \"determined\",\n // Discourse markers\n \"particularly\", \"specifically\", \"especially\", \"primarily\",\n \"overall\", \"furthermore\", \"however\", \"therefore\", \"moreover\",\n ]);\n const capWords = text.match(/\\b[A-Z][a-z]{2,}\\b/g) ?? [];\n for (const w of capWords) {\n const key = w.toLowerCase();\n if (!skipCapitalized.has(key) && !seen.has(key)) {\n seen.add(key);\n terms.push(key);\n if (terms.length >= 3) break;\n }\n }\n }\n }\n\n return terms.slice(0, 3); // Max 3 terms\n}\n\n/** Joins terms into \"X, Y, and Z\" form, dropping terms if too long. */\nfunction joinTerms(terms: string[], maxLen: number): string {\n if (terms.length === 1) return terms[0];\n // Try all terms first\n for (let count = terms.length; count >= 1; count--) {\n const subset = terms.slice(0, count);\n let joined: string;\n if (subset.length === 1) {\n joined = subset[0];\n } else if (subset.length === 2) {\n joined = `${subset[0]} and ${subset[1]}`;\n } else {\n joined = `${subset.slice(0, -1).join(\", \")}, and ${subset[subset.length - 1]}`;\n }\n if (joined.length <= maxLen) return joined;\n }\n return terms[0].slice(0, maxLen);\n}\n\n/**\n * Replaces viewer's name with \"you\"/\"your\" so the card addresses the viewer in second person.\n * Applied to mainText when viewerName is provided.\n * @param otherNames - Other actor names in the card; first-name replacement is\n * skipped when the viewer's first name matches any other actor's first name.\n */\nfunction replaceViewerNameWithYou(text: string, viewerName?: string, otherNames?: string[]): string {\n if (!viewerName?.trim()) return text;\n const full = viewerName.trim();\n const first = full.split(/\\s+/)[0];\n let out = text;\n // Possessive: \"Yankı's\" → \"your\", \"Yankı Ekin Yüksel's\" → \"your\"\n out = out.replace(new RegExp(`\\\\b${escapeRegex(full)}'s\\\\b`, \"gi\"), \"your\");\n\n const otherFirstNames = (otherNames ?? [])\n .map(n => n.trim().split(/\\s+/)[0]?.toLowerCase())\n .filter(Boolean);\n const firstNameCollides = first && otherFirstNames.includes(first.toLowerCase());\n\n if (first && first.length > 1 && !firstNameCollides) {\n out = out.replace(new RegExp(`\\\\b${escapeRegex(first)}'s\\\\b`, \"gi\"), \"your\");\n }\n // Standalone: full name then first name so we don't break \"Yankı Ekin Yüksel\"\n out = out.replace(new RegExp(`\\\\b${escapeRegex(full)}\\\\b`, \"gi\"), \"you\");\n if (first && first.length > 1 && !firstNameCollides) {\n out = out.replace(new RegExp(`\\\\b${escapeRegex(first)}\\\\b`, \"gi\"), \"you\");\n }\n return out;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity.presenter.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.presenter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4CAA4C,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG1E;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,0BAA0B,EAC1B,YAAY,GAAG,kBAAkB,GAAG,YAAY,GAAG,oBAAoB,CACxE,CAAC;AAcF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;EAatB,CAAC;AAMH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE/E,oGAAoG;AACpG,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED,6GAA6G;AAC7G,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;EA+B5B,CAAC;AAEH,2GAA2G;AAC3G,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,gGAAgG;AAChG,MAAM,MAAM,0BAA0B,GAAG,iBAAiB,GAAG;IAC3D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAMF,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qGAAqG;IACrG,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAmID,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,aAAa,CAAW;;YAWlB,iBAAiB;IA4B/B;;OAEG;IAEU,OAAO,CAClB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"opportunity.presenter.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.presenter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4CAA4C,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG1E;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,0BAA0B,EAC1B,YAAY,GAAG,kBAAkB,GAAG,YAAY,GAAG,oBAAoB,CACxE,CAAC;AAcF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;EAatB,CAAC;AAMH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE/E,oGAAoG;AACpG,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED,6GAA6G;AAC7G,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;EA+B5B,CAAC;AAEH,2GAA2G;AAC3G,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,gGAAgG;AAChG,MAAM,MAAM,0BAA0B,GAAG,iBAAiB,GAAG;IAC3D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAMF,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qGAAqG;IACrG,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAmID,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,aAAa,CAAW;;YAWlB,iBAAiB;IA4B/B;;OAEG;IAEU,OAAO,CAClB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,6BAA6B,CAAC;IAsDzC;;;;;;;OAOG;IAEU,eAAe,CAC1B,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,iBAAiB,CAAC;IAwG7B;;OAEG;IAEU,YAAY,CACvB,MAAM,EAAE,cAAc,EAAE,EACxB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACjC,OAAO,CAAC,6BAA6B,EAAE,CAAC;IAa3C;;;OAGG;IAEU,oBAAoB,CAC/B,MAAM,EAAE,sBAAsB,EAAE,EAChC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACjC,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAYhC;AA0ED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,MAAM,EAChB,wBAAwB,CAAC,EAAE,MAAM,GAChC,OAAO,CAAC,cAAc,CAAC,CA0OzB"}
|
|
@@ -21,7 +21,7 @@ import { Timed } from "../shared/observability/performance.js";
|
|
|
21
21
|
import { protocolLogger } from "../shared/observability/protocol.logger.js";
|
|
22
22
|
import { createModel } from "../shared/agent/model.config.js";
|
|
23
23
|
import { viewerCentricCardSummary } from "./opportunity.presentation.js";
|
|
24
|
-
import { stripUuids, stripIntroducerMentions } from "./opportunity.presentation.js";
|
|
24
|
+
import { stripUuids, stripIntroducerMentions, truncateAtBoundary } from "./opportunity.presentation.js";
|
|
25
25
|
const logger = protocolLogger("OpportunityPresenter");
|
|
26
26
|
const LLM_TIMEOUT_MS = 20000;
|
|
27
27
|
const model = createModel("opportunityPresenter");
|
|
@@ -265,12 +265,15 @@ Produce headline, personalizedSummary (2-3 sentences in "you" language), suggest
|
|
|
265
265
|
const message = e instanceof Error ? e.message : String(e);
|
|
266
266
|
const timeoutReason = message.includes("timed out") ? message : undefined;
|
|
267
267
|
logger.warn("[OpportunityPresenter.present] LLM failed, returning fallback", {
|
|
268
|
+
event: "presenter_fallback",
|
|
269
|
+
presenter: "opportunity",
|
|
270
|
+
reason: timeoutReason ? "timeout" : "parse_error",
|
|
268
271
|
message,
|
|
269
272
|
timeoutReason,
|
|
270
273
|
});
|
|
271
274
|
return {
|
|
272
275
|
headline: "A promising connection",
|
|
273
|
-
personalizedSummary: stripUuids(input.matchReasoning
|
|
276
|
+
personalizedSummary: truncateAtBoundary(stripUuids(input.matchReasoning), 300),
|
|
274
277
|
suggestedAction: "Take a look and decide whether to reach out.",
|
|
275
278
|
greeting: "",
|
|
276
279
|
};
|
|
@@ -346,10 +349,13 @@ Produce headline, personalizedSummary, digestSummary, suggestedAction, narratorR
|
|
|
346
349
|
const message = e instanceof Error ? e.message : String(e);
|
|
347
350
|
const timeoutReason = message.includes("timed out") ? message : undefined;
|
|
348
351
|
logger.warn("[OpportunityPresenter.presentHomeCard] LLM failed, returning fallback", {
|
|
352
|
+
event: "presenter_fallback",
|
|
353
|
+
presenter: "home_card",
|
|
354
|
+
reason: timeoutReason ? "timeout" : "parse_error",
|
|
349
355
|
message,
|
|
350
356
|
timeoutReason,
|
|
351
357
|
});
|
|
352
|
-
let fallbackSummary = stripUuids(input.matchReasoning
|
|
358
|
+
let fallbackSummary = truncateAtBoundary(stripUuids(input.matchReasoning), 300);
|
|
353
359
|
if (input.isIntroduction && input.introducerName) {
|
|
354
360
|
fallbackSummary = stripIntroducerMentions(fallbackSummary, input.introducerName);
|
|
355
361
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity.presenter.js","sourceRoot":"/","sources":["opportunity/opportunity.presenter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opportunity Presenter Agent\n *\n * Generates personalized, second-person explanations of why an opportunity\n * matters to the viewing user. Uses full opportunity data (interpretation,\n * actors, profiles, intents, index) to produce headline, personalizedSummary,\n * and suggestedAction for chat tools and user-facing surfaces.\n */\n\nimport type { Runnable } from \"@langchain/core/runnables\";\nimport { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\n\nimport { Timed } from \"../shared/observability/performance.js\";\n\nimport { protocolLogger } from \"../shared/observability/protocol.logger.js\";\nimport { createModel } from \"../shared/agent/model.config.js\";\nimport { viewerCentricCardSummary } from \"./opportunity.presentation.js\";\nimport type { Opportunity } from \"../shared/interfaces/database.interface.js\";\nimport type { ChatGraphCompositeDatabase } from \"../shared/interfaces/database.interface.js\";\nimport type { NegotiationContext } from \"./negotiation-context.loader.js\";\nimport { stripUuids, stripIntroducerMentions } from \"./opportunity.presentation.js\";\n\n/**\n * Minimal database interface required by gatherPresenterContext.\n * Any database adapter that implements these three methods can be passed.\n */\nexport type PresenterDatabase = Pick<\n ChatGraphCompositeDatabase,\n \"getProfile\" | \"getActiveIntents\" | \"getNetwork\" | \"getPremisesForUser\"\n>;\n\nconst logger = protocolLogger(\"OpportunityPresenter\");\nconst LLM_TIMEOUT_MS = 20_000;\n\nconst model = createModel(\"opportunityPresenter\");\n\nconst GREETING_DESCRIPTION =\n \"A 2-4 sentence first-person message the viewer could send to the counterpart, in the viewer's voice, referencing what they have in common. Plain prose only — no markdown, no greeting prefix like 'Hey {Name},'. Example body: 'Saw we're both working on regenerative coordination tooling — your post on consent flows resonated. Would love to compare notes if you have time this week.'\";\n\n// ──────────────────────────────────────────────────────────────\n// SCHEMA & TYPES\n// ──────────────────────────────────────────────────────────────\n\nconst PresentationSchema = z.object({\n headline: z\n .string()\n .describe(\n \"Short, compelling headline for this opportunity (e.g., 'A React expert who needs your design skills')\",\n ),\n personalizedSummary: z\n .string()\n .describe(\n \"2-3 sentence explanation using 'you' language, explaining why this opportunity is specifically valuable for the viewer based on their intents and profile\",\n ),\n suggestedAction: z.string().describe(\"Brief suggested next step\"),\n greeting: z.string().max(500).describe(GREETING_DESCRIPTION),\n});\n\nconst responseFormat = z.object({\n presentation: PresentationSchema,\n});\n\nexport type OpportunityPresentationResult = z.infer<typeof PresentationSchema>;\n\n/** Input for home-card presenter call; extends PresenterInput with optional mutual intent count. */\nexport interface HomeCardPresenterInput extends PresenterInput {\n /** Number of overlapping intents (for generating mutualIntentsLabel). */\n mutualIntentCount?: number;\n /**\n * Snapshot of the opportunity's negotiation, if one exists. When status is\n * `negotiating`, the presenter returns a templated chip without invoking\n * the LLM. For `pending`/`stalled`/`accepted`/`rejected`, the full\n * transcript and outcome ground the LLM's explanation.\n */\n negotiationContext?: NegotiationContext;\n}\n\n/** LLM-generated fields for home-card presentation (buttons are hardcoded by callers, not LLM-generated). */\nexport const HomeCardLLMSchema = z.object({\n headline: z\n .string()\n .describe(\"Short, compelling headline for this opportunity\"),\n personalizedSummary: z\n .string()\n .describe(\n \"2-3 sentence explanation in 'you' language for the main card body\",\n ),\n digestSummary: z\n .string()\n .max(220)\n .describe(\n \"One concise digest-ready sentence for a morning brief. It must be addressed to the viewer and mention the counterpart by name, e.g. 'You might like meeting Paul because ...'. No markdown.\",\n ),\n suggestedAction: z\n .string()\n .describe(\"Brief suggested next step (e.g. CTA line)\"),\n narratorRemark: z\n .string()\n .max(80)\n .describe(\n \"One short sentence for the narrator chip, max ~80 chars (e.g. who is suggesting and why)\",\n ),\n mutualIntentsLabel: z\n .string()\n .max(48)\n .describe(\n \"Short line for the subtitle under the other party name (e.g. '3 mutual intents', 'Shared interests', 'Aligned goals'). NEVER output '0 mutual intents' — use a qualitative phrase like 'Shared interests' when no numeric count is available.\",\n ),\n greeting: z.string().max(500).describe(GREETING_DESCRIPTION),\n});\n\n/** LLM-generated result from presentHomeCard (callers append button labels from opportunity.constants). */\nexport type HomeCardLLMResult = z.infer<typeof HomeCardLLMSchema>;\n\n/** Full home-card display contract including hardcoded button labels (assembled by callers). */\nexport type HomeCardPresentationResult = HomeCardLLMResult & {\n primaryActionLabel: string;\n secondaryActionLabel: string;\n};\n\nconst homeCardResponseFormat = z.object({\n presentation: HomeCardLLMSchema,\n});\n\n/** Input for a single presenter call (all context pre-assembled). */\nexport interface PresenterInput {\n viewerContext: string;\n otherPartyContext: string;\n matchReasoning: string;\n category: string;\n confidence: number;\n signalsSummary: string;\n indexName: string;\n viewerRole: string;\n opportunityStatus?: string;\n /** True when this opportunity was created via an explicit introduction (not automatic discovery). */\n isIntroduction?: boolean;\n /** Name of the person who made the introduction, if applicable. */\n introducerName?: string;\n}\n\n// ──────────────────────────────────────────────────────────────\n// SYSTEM PROMPT\n// ──────────────────────────────────────────────────────────────\n\nconst systemPrompt = `\nYou are an expert at presenting connection opportunities to users in a way that feels personal and compelling.\n\nYour goal: Given raw context about the viewer (their profile, intents), the other person(s), and why the system matched them, produce a short headline, a personalized summary, and a suggested action.\n\nRules:\n1. Address the VIEWER directly using \"you\" and \"your\". This is for them.\n2. Be concise and compelling — not analytical or third-party. No \"The source user\" or \"The candidate\"; use names or \"they\" where needed.\n3. Do not leak private or confidential details. Use only the context provided.\n4. Vary user-facing nouns naturally. Do not repeatedly use the same label in one response.\n5. If possible, avoid repeating \"opportunity\" in both headline and summary. Prefer alternatives like \"connection\", \"thought partner\", \"mutual fit\", \"valuable conversation\", or \"peer\".\n6. Prefer first names in user-facing copy. Do not repeatedly use full names unless needed to disambiguate.\n\n**Introduction-originated opportunities:**\nWhen INTRODUCTION CONTEXT is provided, this opportunity was explicitly created by an introducer (a real person who saw value in this connection). This is NOT an automatic system discovery — someone made a deliberate judgment.\n- For ALL roles: acknowledge the introducer's role naturally. E.g., \"[Introducer name] thinks you should meet [other person]\" or \"[Introducer name] connected you because...\"\n- The introduction itself is a strong signal — treat it with the weight of a personal recommendation.\n- If the parties' intents don't obviously overlap, that's fine — the introducer saw something worth connecting. Focus on what the introducer likely saw.\n\n**Role-Specific Presentation:**\n\n**If viewer is \"introducer\":**\n- The viewer suggested this connection between two (or more) OTHER people. The opportunity is NOT about the viewer's own needs.\n- Headline: describe the connection between the parties (e.g., \"Connecting a React expert with a startup founder\").\n- Personalized summary: explain why the people YOU are introducing should meet. Reference THEIR profiles and intents, not yours. Frame it as \"you're connecting X and Y because...\" rather than \"this matches your intent\".\n- Suggested action: guide sharing (e.g., \"Share this with [name] to get things started\").\n- CRITICAL: Do NOT reference the introducer's own intents, skills, or needs. The introducer is the matchmaker, not a party.\n\n**If viewer is \"patient\" or \"party\":**\n- Reference their specific intents, skills, or interests that align with this opportunity.\n- If this is an introduction: mention who introduced them and frame it as a personal recommendation.\n- Headline: one short line that hooks (e.g., \"[Name] thinks you should meet [Other]\" or \"A React expert who needs your design skills\").\n- Personalized summary: 2-3 sentences. Why is this opportunity for *them*? If introduced, lead with the introduction.\n- Suggested action: encourage action (\"Send a message to start the conversation\" or \"Share this intro\").\n\n**If viewer is \"agent\":**\n- They are seeing this because someone already reached out.\n- If this is an introduction: mention who made the introduction.\n- Reference their skills/expertise that make them a match.\n- Headline: what the other person needs that they can provide.\n- Personalized summary: 2-3 sentences. Why someone reached out to them.\n- Suggested action: \"Someone is interested in connecting — check their message\" or \"Review and respond\".\n\n**If viewer is \"peer\":**\n- Mutual opportunity. Reference shared or complementary interests.\n- If this is an introduction: mention who connected them.\n- Headline: the mutual connection angle.\n- Personalized summary: 2-3 sentences. Why this is mutually valuable.\n- Suggested action: \"Send an intro to connect\" or \"Start a conversation\".\n`;\n\nconst homeCardSystemPrompt = `\nYou are an expert at presenting connection opportunities for a home feed card.\n\nGiven context about the viewer, the other person, and why they were matched, produce:\n1. headline: one short hook line.\n2. personalizedSummary: 2-3 sentences in \"you\" language (main body text).\n3. digestSummary: one polished morning-brief sentence that can be printed directly after the person's linked name. No markdown, no field labels.\n4. suggestedAction: one brief suggested next step.\n5. narratorRemark: one short sentence for the narrator chip (who is suggesting and why; max ~80 chars).\n6. greeting: a 2-4 sentence first-person message the viewer could send to the counterpart. Plain prose, no greeting prefix, no markdown.\n7. mutualIntentsLabel: short subtitle under the other party's name. Examples: \"3 mutual intents\", \"Shared interests\", \"Aligned goals\" — keep it brief. NEVER output \"0 mutual intents\" or any zero-count label; use a qualitative phrase instead.\n\nRules:\n- Address the viewer with \"you\"/\"your\". Be concise and compelling.\n- narratorRemark should feel like a single sentence from the narrator (Index or a person), not meta-commentary.\n- narratorRemark is displayed with the narrator name prepended (e.g. \"Index: …\" or \"Alice: …\"). Do NOT start narratorRemark with the narrator's name or repeat it; write only the remark (e.g. \"Based on your overlapping intents\" or \"introduced you two, sensing a valuable connection\").\n- Vary wording for the match itself. Do not repeat \"opportunity\" across headline, summary, and narratorRemark when alternatives fit.\n- Prefer first names in user-facing copy. Avoid repeated full names unless disambiguation is necessary.\n- digestSummary must be grammatically complete as a standalone sentence. It should usually start with \"You might like meeting {Name} because ...\" for direct connections, or \"You may be able to help {Name} because ...\" for connector/introducer cards.\n- digestSummary must NOT use awkward third-person fragments like \"Name is...\", \"they're ..., and is...\", \"you is...\", or \"the discoverer's query\".\n- digestSummary must be one sentence, MUST fit within 180 characters when possible, and MUST contain no markdown links; the caller will attach links.\n- If you cannot fit every detail, choose one clear reason and stop. Do not rely on downstream truncation.\n\n**Introduction-originated opportunities (ONLY when INTRODUCTION CONTEXT is provided):**\nWhen INTRODUCTION CONTEXT is provided, this opportunity was explicitly created by an introducer. It was NOT automatically discovered.\n- For parties/patients/agents/peers viewing an introduction: keep the introducer signal in narratorRemark (and narrator chip), not in personalizedSummary.\n- For these introduced parties, personalizedSummary must focus ONLY on fit/value between viewer and counterpart. Do NOT mention the introducer there.\n- narratorRemark should carry the introduction signal (e.g., \"saw strong alignment between you two\" or \"thought this connection could be valuable\"), without repeating the narrator name at the start.\n- This is a personal recommendation, not an algorithm match. Frame it accordingly.\n\n**CRITICAL: NEVER include introducer names in personalizedSummary. Examples:**\n❌ WRONG: \"Seref introduced you to Lucy, who is actively seeking a product co-founder...\"\n✅ CORRECT: \"Lucy is actively seeking a product co-founder for a niche APAC marketplace. With your expertise in UX and AI, this could be an ideal collaboration.\"\n\n❌ WRONG: \"Bob thinks you should meet Alice because your React skills align with her needs.\"\n✅ CORRECT: \"Alice is building a React-based platform and needs frontend expertise. Your experience with component architecture makes you a strong fit.\"\n\n❌ WRONG: \"Jane connected you to Mark, who is looking for a designer.\"\n✅ CORRECT: \"Mark is building a consumer app and needs design expertise. Your background in user-centered design aligns well with what he's building.\"\n\nRemember: The introducer's name goes ONLY in narratorRemark, NEVER in personalizedSummary.\n\n**When INTRODUCTION CONTEXT is NOT provided (system-discovered match):**\n- Do NOT use introducer-style wording. Do NOT say \"you suggested\", \"this is an introduction you suggested\", or \"you suggested this connection\". The system found this match; no human introducer was involved.\n- Instead, narratorRemark should describe why the match is relevant (e.g. \"Based on your overlapping intents\", \"Your skills align with what they need\").\n\n**Negotiation-grounded explanations (ONLY when NEGOTIATION CONTEXT is provided):**\nWhen NEGOTIATION CONTEXT is provided, this opportunity passed through an agent-to-agent negotiation. Use the transcript to ground your explanation in the concrete reasoning the agents exchanged.\n- Personalize the summary with *why* the negotiation produced this match — reference the roles the agents agreed on, the specific concerns raised, and how they were resolved.\n- For status \"stalled\" with reason \"turn_cap\": the agents hit the turn limit without reaching agreement. Frame the card as a hedged possibility rather than a confident match; narratorRemark should signal \"agents couldn't fully converge\" without sounding negative.\n- For status \"stalled\" with reason \"timeout\": one side went silent. Suggest the user re-engage if interested.\n- For status \"accepted\": the agents agreed; the card should confidently explain *why* they agreed.\n- For status \"rejected\": the agents declined. The card should explain the reason briefly so the user understands — not dwell on it.\n- Do NOT invent turn content. Only reference what is in the NEGOTIATION CONTEXT block.\n\n- Exception for connector/introducer: if viewer role is \"introducer\" (any status), this is a curation/connector card. Use:\n - suggestedAction: one short line about sharing the intro or confirming the match.\n - mutualIntentsLabel: a short connector label (e.g. \"Connector match\", \"You can bridge this\").\n - headline: describe the connection between the parties (e.g., \"Connecting a PhD researcher with a translator\"). Do NOT reference the introducer's own needs.\n - personalizedSummary: explain why the parties you're introducing should meet, referencing THEIR profiles and intents, not yours.\n\n**CRITICAL for latent introducer cards (opportunity status is \"latent\"):**\nWhen the viewer is the introducer and the opportunity status is \"latent\", the introducer has NOT yet approved this match. They are evaluating whether to make the introduction.\n- narratorRemark MUST use evaluation/curation language (e.g. \"Could be a strong match\", \"Worth introducing?\", \"Interesting overlap here\").\n- Do NOT say \"you suggested\", \"you introduced\", \"you connected\", or any past-tense language implying the introduction was already made.\n- suggestedAction should encourage evaluation (e.g. \"Approve if you see the fit\").\n- Exception for new-connection reveal: if viewer role is \"agent\", status is \"accepted\", and there is an introducer, this is the agent's first time seeing this opportunity. Use:\n - suggestedAction: a short line about joining the conversation.\n`;\n\n// ──────────────────────────────────────────────────────────────\n// CLASS\n// ──────────────────────────────────────────────────────────────\n\nexport class OpportunityPresenter {\n private model: Runnable;\n private homeCardModel: Runnable;\n\n constructor() {\n this.model = model.withStructuredOutput(responseFormat, {\n name: \"opportunity_presenter\",\n });\n this.homeCardModel = model.withStructuredOutput(homeCardResponseFormat, {\n name: \"opportunity_presenter_home_card\",\n });\n }\n\n private async invokeWithTimeout(\n targetModel: Runnable,\n messages: (SystemMessage | HumanMessage)[],\n ): Promise<unknown> {\n const timeoutReason = `LLM invoke timed out after ${LLM_TIMEOUT_MS}ms`;\n const controller = new AbortController();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const invokePromise = targetModel.invoke(messages, {\n signal: controller.signal,\n });\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n controller.abort(timeoutReason);\n reject(new Error(timeoutReason));\n }, LLM_TIMEOUT_MS);\n });\n\n try {\n return await Promise.race([invokePromise, timeoutPromise]);\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n\n /**\n * Generate personalized presentation for a single opportunity.\n */\n @Timed()\n public async present(\n input: PresenterInput,\n ): Promise<OpportunityPresentationResult> {\n const introContext = input.isIntroduction\n ? `\\nINTRODUCTION CONTEXT: This opportunity was created by an explicit introduction from ${input.introducerName ?? \"someone in the community\"}. It was NOT discovered automatically — a real person made this connection.\\n`\n : \"\";\n const humanContent = `\nVIEWER (the person seeing this opportunity):\n${input.viewerContext}\n\nOTHER PARTY:\n${input.otherPartyContext}\n\nMATCH CONTEXT:\n- Category: ${input.category}\n- Confidence: ${input.confidence}\n- Why we matched: ${input.matchReasoning}\n- Signals: ${input.signalsSummary}\n${introContext}\nCOMMUNITY: ${input.indexName}\nViewer's role in this opportunity: ${input.viewerRole}\n\nProduce headline, personalizedSummary (2-3 sentences in \"you\" language), suggestedAction, and greeting.\n`;\n\n try {\n const messages = [\n new SystemMessage(systemPrompt),\n new HumanMessage(humanContent),\n ];\n const result = await this.invokeWithTimeout(this.model, messages);\n const parsed = responseFormat.parse(result);\n parsed.presentation.personalizedSummary = stripUuids(parsed.presentation.personalizedSummary);\n return parsed.presentation;\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n const timeoutReason = message.includes(\"timed out\") ? message : undefined;\n logger.warn(\n \"[OpportunityPresenter.present] LLM failed, returning fallback\",\n {\n message,\n timeoutReason,\n },\n );\n return {\n headline: \"A promising connection\",\n personalizedSummary: stripUuids(input.matchReasoning.slice(0, 300)),\n suggestedAction: \"Take a look and decide whether to reach out.\",\n greeting: \"\",\n };\n }\n }\n\n /**\n * Generate LLM-powered home-card content (headline, body, narrator remark, mutual-intent label).\n * Callers append button labels from opportunity.constants.\n *\n * When `negotiationContext.status === 'negotiating'`, returns a templated\n * chip synchronously without invoking the LLM — the card just reflects\n * \"negotiation in progress\" at that point.\n */\n @Timed()\n public async presentHomeCard(\n input: HomeCardPresenterInput,\n ): Promise<HomeCardLLMResult> {\n if (input.negotiationContext?.status === 'negotiating') {\n return buildNegotiatingChip(input);\n }\n\n const mutualHint =\n input.mutualIntentCount != null && input.mutualIntentCount > 0\n ? `There are ${input.mutualIntentCount} overlapping intent(s) between viewer and other party.`\n : \"Match is based on profile and intent alignment. Do not cite a numeric intent count.\";\n const introContext = input.isIntroduction\n ? `\\nINTRODUCTION CONTEXT: This opportunity was created by an explicit introduction from ${input.introducerName ?? \"someone in the community\"}. It was NOT discovered automatically — a real person made this connection.\\n`\n : \"\";\n const negotiationBlock = buildNegotiationPromptBlock(input.negotiationContext);\n // When negotiation context exists, lead with it — these cards exist\n // *because* the negotiation happened. Trailing the block lets weaker\n // models lean on surface signals and ignore the transcript entirely.\n const negotiationDirective = negotiationBlock\n ? `\\nIMPORTANT: This opportunity surfaced because the agents negotiated and converged. Your personalizedSummary MUST reference at least one specific signal from the NEGOTIATION CONTEXT block below — what concern was raised, what was confirmed, what the agents agreed on. Do not produce a generic skill-complementarity summary; that's what every card looked like before this negotiation happened. Use the transcript to explain *why this specific match* surfaced now.\\n`\n : \"\";\n const humanContent = `\n${negotiationBlock}${negotiationDirective}\nVIEWER (the person seeing this opportunity):\n${input.viewerContext}\n\nOTHER PARTY:\n${input.otherPartyContext}\n\nMATCH CONTEXT:\n- Category: ${input.category}\n- Confidence: ${input.confidence}\n- Why we matched: ${input.matchReasoning}\n- Signals: ${input.signalsSummary}\n- ${mutualHint}\n${introContext}\nCOMMUNITY: ${input.indexName}\nViewer's role in this opportunity: ${input.viewerRole}\nOpportunity status: ${input.opportunityStatus ?? \"pending\"}\n\nProduce headline, personalizedSummary, digestSummary, suggestedAction, narratorRemark, greeting, and mutualIntentsLabel.\n`;\n\n const isIntroducer = input.viewerRole === \"introducer\";\n\n try {\n const messages = [\n new SystemMessage(homeCardSystemPrompt),\n new HumanMessage(humanContent),\n ];\n const result = await this.invokeWithTimeout(this.homeCardModel, messages);\n const parsed = homeCardResponseFormat.parse(result);\n parsed.presentation.personalizedSummary = stripUuids(parsed.presentation.personalizedSummary);\n parsed.presentation.digestSummary = stripUuids(parsed.presentation.digestSummary);\n parsed.presentation.narratorRemark = stripUuids(parsed.presentation.narratorRemark);\n if (/^0\\s+(mutual|overlapping)\\s+intent/i.test(parsed.presentation.mutualIntentsLabel)) {\n parsed.presentation.mutualIntentsLabel = \"Shared interests\";\n }\n if (input.isIntroduction && input.introducerName) {\n parsed.presentation.personalizedSummary = stripIntroducerMentions(\n parsed.presentation.personalizedSummary,\n input.introducerName,\n );\n parsed.presentation.digestSummary = stripIntroducerMentions(\n parsed.presentation.digestSummary,\n input.introducerName,\n );\n }\n return parsed.presentation;\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n const timeoutReason = message.includes(\"timed out\") ? message : undefined;\n logger.warn(\n \"[OpportunityPresenter.presentHomeCard] LLM failed, returning fallback\",\n {\n message,\n timeoutReason,\n },\n );\n let fallbackSummary = stripUuids(input.matchReasoning.slice(0, 300));\n if (input.isIntroduction && input.introducerName) {\n fallbackSummary = stripIntroducerMentions(fallbackSummary, input.introducerName);\n }\n return {\n headline: \"A promising connection\",\n personalizedSummary: fallbackSummary,\n digestSummary: isIntroducer\n ? \"You may be able to help make a useful introduction here.\"\n : \"You might like meeting them based on your current interests.\",\n suggestedAction: isIntroducer\n ? \"Share this introduction to get things started.\"\n : \"Take a look and decide whether to reach out.\",\n narratorRemark: \"Worth a look.\",\n mutualIntentsLabel: isIntroducer\n ? \"Connector match\"\n : input.mutualIntentCount != null && input.mutualIntentCount > 0\n ? `${input.mutualIntentCount} mutual intent${input.mutualIntentCount !== 1 ? \"s\" : \"\"}`\n : \"Shared interests\",\n greeting: \"\",\n };\n }\n }\n\n /**\n * Process multiple opportunities in parallel with bounded concurrency.\n */\n @Timed()\n public async presentBatch(\n inputs: PresenterInput[],\n options?: { concurrency?: number },\n ): Promise<OpportunityPresentationResult[]> {\n const concurrency = options?.concurrency ?? 5;\n const results: OpportunityPresentationResult[] = [];\n for (let i = 0; i < inputs.length; i += concurrency) {\n const chunk = inputs.slice(i, i + concurrency);\n const chunkResults = await Promise.all(\n chunk.map((inp) => this.present(inp)),\n );\n results.push(...chunkResults);\n }\n return results;\n }\n\n /**\n * Process multiple opportunities as home cards in parallel with bounded concurrency.\n * Returns full home-card display contracts (headline, body, narrator remark, action labels, mutual-intent label).\n */\n @Timed()\n public async presentHomeCardBatch(\n inputs: HomeCardPresenterInput[],\n options?: { concurrency?: number },\n ): Promise<HomeCardLLMResult[]> {\n const concurrency = options?.concurrency ?? 5;\n const results: HomeCardLLMResult[] = [];\n for (let i = 0; i < inputs.length; i += concurrency) {\n const chunk = inputs.slice(i, i + concurrency);\n const chunkResults = await Promise.all(\n chunk.map((inp) => this.presentHomeCard(inp)),\n );\n results.push(...chunkResults);\n }\n return results;\n }\n}\n\n// ──────────────────────────────────────────────────────────────\n// NEGOTIATION CONTEXT HELPERS\n// ──────────────────────────────────────────────────────────────\n\n/**\n * Builds a \"NEGOTIATION CONTEXT:\" block for the home-card prompt. Returns an\n * empty string when the opportunity has no meaningful negotiation context\n * (draft/latent) or when the opportunity is still negotiating (handled via\n * the templated chip, not the LLM).\n */\nfunction buildNegotiationPromptBlock(context: NegotiationContext | undefined): string {\n if (!context || context.status === 'negotiating') return \"\";\n\n const turnCapLabel = context.turnCap > 0 ? `${context.turnCap}` : \"unlimited\";\n const reason = context.outcome?.reason;\n const reasonLabel = reason === 'turn_cap'\n ? \"agents hit the turn cap without converging\"\n : reason === 'timeout'\n ? \"counterpart went silent before responding\"\n : undefined;\n\n const turnLines = (context.turns ?? []).map((turn, index) => {\n const action = turn.action;\n const reasoning = turn.assessment?.reasoning ?? \"(no reasoning)\";\n const message = turn.message ? ` — said: \"${turn.message}\"` : \"\";\n return `Turn ${index + 1} (${action}): ${reasoning}${message}`;\n });\n\n const outcomeSummary = context.outcome\n ? `Final outcome: ${context.outcome.hasOpportunity ? \"agreed\" : \"declined\"} — ${context.outcome.reasoning}`\n : \"Final outcome: not recorded.\";\n\n return `\nNEGOTIATION CONTEXT:\n- Negotiation status: ${context.status}${reasonLabel ? ` (${reasonLabel})` : \"\"}\n- Turns exchanged: ${context.turnCount} of ${turnCapLabel}\n- Transcript:\n${turnLines.length > 0 ? turnLines.map((l) => ` ${l}`).join(\"\\n\") : \" (no turns recorded)\"}\n- ${outcomeSummary}\n`;\n}\n\n/**\n * Builds a templated home-card result for an opportunity whose negotiation\n * is still in progress. Bypasses the LLM so users see a stable \"currently\n * negotiating\" chip while turns are still being exchanged.\n */\nfunction buildNegotiatingChip(input: HomeCardPresenterInput): HomeCardLLMResult {\n const ctx = input.negotiationContext;\n const turnCount = ctx?.turnCount ?? 0;\n const turnCap = ctx?.turnCap && ctx.turnCap > 0 ? ctx.turnCap : undefined;\n const narratorRemark = turnCap\n ? `Currently negotiating · turn ${turnCount} of ${turnCap}`\n : `Currently negotiating · turn ${turnCount}`;\n\n return {\n headline: \"Negotiation in progress\",\n personalizedSummary: \"Your agent is still talking with theirs to see if this connection makes sense. We'll surface the full match as soon as they converge.\",\n digestSummary: \"Your agent is still checking whether this connection makes sense.\",\n suggestedAction: \"Check back shortly — no action needed yet.\",\n narratorRemark,\n mutualIntentsLabel: input.mutualIntentCount && input.mutualIntentCount > 0\n ? `${input.mutualIntentCount} mutual intent${input.mutualIntentCount !== 1 ? \"s\" : \"\"}`\n : \"Shared interests\",\n greeting: \"\",\n };\n}\n\n// ──────────────────────────────────────────────────────────────\n// CONTEXT GATHERER (used by tools)\n// ──────────────────────────────────────────────────────────────\n\n/**\n * Gather all context needed for the presenter from the database.\n * Fetches viewer profile, viewer intents, other party profile(s), and index in parallel.\n *\n * @param displayCounterpartUserId - When set (e.g. for home card), only this counterpart is included in otherPartyContext so the presenter writes about the person on the card. Omitted for introducer view (card shows both parties).\n */\nexport async function gatherPresenterContext(\n database: PresenterDatabase,\n opportunity: Opportunity,\n viewerId: string,\n displayCounterpartUserId?: string,\n): Promise<PresenterInput> {\n const myActor = opportunity.actors.find((a) => a.userId === viewerId);\n if (!myActor) {\n throw new Error(\"Viewer is not an actor in this opportunity\");\n }\n\n const isIntroducer = myActor.role === \"introducer\";\n const otherActors = opportunity.actors.filter((a) => a.userId !== viewerId);\n let otherPartyIds = [...new Set(otherActors.map((a) => a.userId))];\n if (\n displayCounterpartUserId &&\n !isIntroducer &&\n otherPartyIds.includes(displayCounterpartUserId)\n ) {\n otherPartyIds = [displayCounterpartUserId];\n }\n\n const contextIndexId = opportunity.context?.networkId;\n\n // For introducers: fetch profiles + intents for both parties; skip introducer's own intents.\n // For other roles: fetch viewer's profile + intents and other party profiles.\n const [viewerProfile, indexRecord, ...otherProfiles] = await Promise.all([\n database.getProfile(viewerId),\n contextIndexId ? database.getNetwork(contextIndexId) : Promise.resolve(null),\n ...otherPartyIds.map((uid) => database.getProfile(uid)),\n ]);\n\n // Fetch intents: for introducer, fetch each party's intents; otherwise fetch viewer's intents.\n let viewerIntents:\n | Awaited<ReturnType<typeof database.getActiveIntents>>\n | undefined;\n let partyIntentsMap:\n | Map<string, Awaited<ReturnType<typeof database.getActiveIntents>>>\n | undefined;\n\n if (isIntroducer) {\n const partyIntentResults = await Promise.all(\n otherPartyIds.map(async (uid) => ({\n uid,\n intents: await database.getActiveIntents(uid),\n })),\n );\n partyIntentsMap = new Map(\n partyIntentResults.map((r) => [r.uid, r.intents]),\n );\n } else {\n viewerIntents = await database.getActiveIntents(viewerId);\n }\n\n // Fetch premises when any actor is premise-grounded\n const premiseGroundedActors = opportunity.actors.filter((a) => a.premise);\n let viewerPremiseContext = '';\n let otherPremiseContext = '';\n\n if (premiseGroundedActors.length > 0) {\n // Only fetch premises for actors that are actually premise-grounded, not all parties\n const groundedOtherIds = premiseGroundedActors\n .filter((a) => a.userId !== viewerId)\n .map((a) => a.userId);\n const viewerIsGrounded = premiseGroundedActors.some((a) => a.userId === viewerId);\n\n const results = await Promise.all([\n ...(viewerIsGrounded ? [database.getPremisesForUser(viewerId, 'ACTIVE')] : []),\n ...groundedOtherIds.map((uid) => database.getPremisesForUser(uid, 'ACTIVE')),\n ]);\n\n let idx = 0;\n if (viewerIsGrounded) {\n const viewerPremises = results[idx++];\n if (viewerPremises?.length) {\n viewerPremiseContext =\n '\\nPremises (self-descriptions):\\n' +\n viewerPremises\n .slice(0, 5)\n .map((p) => `- ${p.assertion.text}`)\n .join('\\n');\n }\n }\n\n const otherPremiseLines: string[] = [];\n for (let i = 0; i < groundedOtherIds.length; i++) {\n const premises = results[idx++];\n if (premises?.length) {\n for (const p of premises.slice(0, 3)) {\n otherPremiseLines.push(`- ${p.assertion.text}`);\n }\n }\n }\n if (otherPremiseLines.length > 0) {\n otherPremiseContext = '\\nPremises (self-descriptions):\\n' + otherPremiseLines.join('\\n');\n }\n }\n\n let viewerContext: string;\n let otherPartyContext: string;\n\n if (isIntroducer) {\n // Introducer view: minimal viewer context (just name + role), rich other-party context with intents\n const introducerApproved = opportunity.actors.find(a => a.role === 'introducer')?.approved === true;\n const hasApproved = introducerApproved || opportunity.status !== 'latent';\n viewerContext = [\n \"Profile:\",\n `Name: ${viewerProfile?.identity?.name ?? \"Unknown\"}`,\n hasApproved\n ? \"Role: You are the introducer who suggested this connection.\"\n : \"Role: You are being asked whether these two people would benefit from meeting. You have NOT yet approved this introduction.\",\n ].join(\"\\n\");\n\n const otherParts = otherPartyIds.map((uid, idx) => {\n const profile = otherProfiles[idx] as Awaited<\n ReturnType<typeof database.getProfile>\n >;\n const name = profile?.identity?.name ?? \"Unknown\";\n const bio = profile?.identity?.bio ?? \"\";\n const location = profile?.identity?.location ?? \"\";\n const skills = profile?.attributes?.skills?.join(\", \") ?? \"\";\n const interests = profile?.attributes?.interests?.join(\", \") ?? \"\";\n const context = profile?.narrative?.context ?? \"\";\n const intents = partyIntentsMap?.get(uid);\n const intentLines = intents?.length\n ? intents\n .slice(0, 5)\n .map((i) => ` - ${i.payload}${i.summary ? ` (${i.summary})` : \"\"}`)\n : [\" (no active intents)\"];\n return [\n `${name}:`,\n ` Bio: ${bio}`,\n location ? ` Location: ${location}` : null,\n skills ? ` Skills: ${skills}` : null,\n interests ? ` Interests: ${interests}` : null,\n context ? ` Context: ${context}` : null,\n ` Active intents:`,\n ...intentLines,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n });\n otherPartyContext =\n otherParts.join(\"\\n\\n\") || \"Parties (details not available).\";\n } else {\n // Non-introducer view: full viewer profile + intents, other party profiles\n const viewerContextLines = [\n \"Profile:\",\n `Name: ${viewerProfile?.identity?.name ?? \"Unknown\"}`,\n `Bio: ${viewerProfile?.identity?.bio ?? \"\"}`,\n `Location: ${viewerProfile?.identity?.location ?? \"\"}`,\n `Skills: ${viewerProfile?.attributes?.skills?.join(\", \") ?? \"\"}`,\n `Interests: ${viewerProfile?.attributes?.interests?.join(\", \") ?? \"\"}`,\n `Context: ${viewerProfile?.narrative?.context ?? \"\"}`,\n \"Active intents:\",\n ...(viewerIntents?.length\n ? viewerIntents.map(\n (i) => `- ${i.payload}${i.summary ? ` (${i.summary})` : \"\"}`,\n )\n : [\"(none listed)\"]),\n ];\n viewerContext = viewerContextLines.join(\"\\n\");\n\n const otherParts = otherPartyIds.map((uid, idx) => {\n const profile = otherProfiles[idx] as Awaited<\n ReturnType<typeof database.getProfile>\n >;\n const name = profile?.identity?.name ?? \"Unknown\";\n const bio = profile?.identity?.bio ?? \"\";\n const skills = profile?.attributes?.skills?.join(\", \") ?? \"\";\n const interests = profile?.attributes?.interests?.join(\", \") ?? \"\";\n return `${name}: ${bio}. Skills: ${skills}. Interests: ${interests}`;\n });\n otherPartyContext =\n otherParts.join(\"\\n\\n\") || \"Other party (details not available).\";\n }\n\n const interp = opportunity.interpretation;\n const signalsSummary =\n interp.signals?.map((s) => `${s.type}: ${s.detail ?? s.type}`).join(\"; \") ??\n \"Match based on profile and intent alignment.\";\n\n // Detect introduction-originated opportunities: only when there is an explicit introducer actor.\n // Do NOT use detection.source === \"manual\" alone — system-discovered opportunities can have manual source without an introducer.\n const introducerActor = opportunity.actors.find(\n (a) => a.role === \"introducer\",\n );\n const isIntroduction = !!introducerActor;\n let introducerName: string | undefined;\n if (introducerActor) {\n introducerName = opportunity.detection.createdByName;\n if (!introducerName) {\n const introducerProfile = await database.getProfile(\n introducerActor.userId,\n );\n introducerName = introducerProfile?.identity?.name ?? undefined;\n }\n }\n\n const counterpartName =\n otherPartyIds.length === 1 && otherProfiles[0]\n ? (otherProfiles[0] as { identity?: { name?: string } })?.identity?.name?.trim()\n : undefined;\n const viewerNameForFilter = viewerProfile?.identity?.name?.trim();\n const matchReasoning =\n counterpartName && interp.reasoning\n ? viewerCentricCardSummary(\n interp.reasoning,\n counterpartName,\n 400,\n viewerNameForFilter,\n introducerName,\n )\n : stripUuids(interp.reasoning);\n\n if (viewerPremiseContext) {\n viewerContext += viewerPremiseContext;\n }\n if (otherPremiseContext) {\n otherPartyContext += otherPremiseContext;\n }\n\n const result: PresenterInput = {\n viewerContext,\n otherPartyContext,\n matchReasoning,\n category: interp.category ?? \"connection\",\n confidence:\n typeof interp.confidence === \"number\"\n ? interp.confidence\n : parseFloat(String(interp.confidence ?? 0)) || 0,\n signalsSummary,\n indexName: indexRecord?.title ?? contextIndexId ?? \"\",\n viewerRole: myActor.role ?? \"party\",\n isIntroduction,\n introducerName,\n };\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"opportunity.presenter.js","sourceRoot":"/","sources":["opportunity/opportunity.presenter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opportunity Presenter Agent\n *\n * Generates personalized, second-person explanations of why an opportunity\n * matters to the viewing user. Uses full opportunity data (interpretation,\n * actors, profiles, intents, index) to produce headline, personalizedSummary,\n * and suggestedAction for chat tools and user-facing surfaces.\n */\n\nimport type { Runnable } from \"@langchain/core/runnables\";\nimport { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\n\nimport { Timed } from \"../shared/observability/performance.js\";\n\nimport { protocolLogger } from \"../shared/observability/protocol.logger.js\";\nimport { createModel } from \"../shared/agent/model.config.js\";\nimport { viewerCentricCardSummary } from \"./opportunity.presentation.js\";\nimport type { Opportunity } from \"../shared/interfaces/database.interface.js\";\nimport type { ChatGraphCompositeDatabase } from \"../shared/interfaces/database.interface.js\";\nimport type { NegotiationContext } from \"./negotiation-context.loader.js\";\nimport { stripUuids, stripIntroducerMentions, truncateAtBoundary } from \"./opportunity.presentation.js\";\n\n/**\n * Minimal database interface required by gatherPresenterContext.\n * Any database adapter that implements these three methods can be passed.\n */\nexport type PresenterDatabase = Pick<\n ChatGraphCompositeDatabase,\n \"getProfile\" | \"getActiveIntents\" | \"getNetwork\" | \"getPremisesForUser\"\n>;\n\nconst logger = protocolLogger(\"OpportunityPresenter\");\nconst LLM_TIMEOUT_MS = 20_000;\n\nconst model = createModel(\"opportunityPresenter\");\n\nconst GREETING_DESCRIPTION =\n \"A 2-4 sentence first-person message the viewer could send to the counterpart, in the viewer's voice, referencing what they have in common. Plain prose only — no markdown, no greeting prefix like 'Hey {Name},'. Example body: 'Saw we're both working on regenerative coordination tooling — your post on consent flows resonated. Would love to compare notes if you have time this week.'\";\n\n// ──────────────────────────────────────────────────────────────\n// SCHEMA & TYPES\n// ──────────────────────────────────────────────────────────────\n\nconst PresentationSchema = z.object({\n headline: z\n .string()\n .describe(\n \"Short, compelling headline for this opportunity (e.g., 'A React expert who needs your design skills')\",\n ),\n personalizedSummary: z\n .string()\n .describe(\n \"2-3 sentence explanation using 'you' language, explaining why this opportunity is specifically valuable for the viewer based on their intents and profile\",\n ),\n suggestedAction: z.string().describe(\"Brief suggested next step\"),\n greeting: z.string().max(500).describe(GREETING_DESCRIPTION),\n});\n\nconst responseFormat = z.object({\n presentation: PresentationSchema,\n});\n\nexport type OpportunityPresentationResult = z.infer<typeof PresentationSchema>;\n\n/** Input for home-card presenter call; extends PresenterInput with optional mutual intent count. */\nexport interface HomeCardPresenterInput extends PresenterInput {\n /** Number of overlapping intents (for generating mutualIntentsLabel). */\n mutualIntentCount?: number;\n /**\n * Snapshot of the opportunity's negotiation, if one exists. When status is\n * `negotiating`, the presenter returns a templated chip without invoking\n * the LLM. For `pending`/`stalled`/`accepted`/`rejected`, the full\n * transcript and outcome ground the LLM's explanation.\n */\n negotiationContext?: NegotiationContext;\n}\n\n/** LLM-generated fields for home-card presentation (buttons are hardcoded by callers, not LLM-generated). */\nexport const HomeCardLLMSchema = z.object({\n headline: z\n .string()\n .describe(\"Short, compelling headline for this opportunity\"),\n personalizedSummary: z\n .string()\n .describe(\n \"2-3 sentence explanation in 'you' language for the main card body\",\n ),\n digestSummary: z\n .string()\n .max(220)\n .describe(\n \"One concise digest-ready sentence for a morning brief. It must be addressed to the viewer and mention the counterpart by name, e.g. 'You might like meeting Paul because ...'. No markdown.\",\n ),\n suggestedAction: z\n .string()\n .describe(\"Brief suggested next step (e.g. CTA line)\"),\n narratorRemark: z\n .string()\n .max(80)\n .describe(\n \"One short sentence for the narrator chip, max ~80 chars (e.g. who is suggesting and why)\",\n ),\n mutualIntentsLabel: z\n .string()\n .max(48)\n .describe(\n \"Short line for the subtitle under the other party name (e.g. '3 mutual intents', 'Shared interests', 'Aligned goals'). NEVER output '0 mutual intents' — use a qualitative phrase like 'Shared interests' when no numeric count is available.\",\n ),\n greeting: z.string().max(500).describe(GREETING_DESCRIPTION),\n});\n\n/** LLM-generated result from presentHomeCard (callers append button labels from opportunity.constants). */\nexport type HomeCardLLMResult = z.infer<typeof HomeCardLLMSchema>;\n\n/** Full home-card display contract including hardcoded button labels (assembled by callers). */\nexport type HomeCardPresentationResult = HomeCardLLMResult & {\n primaryActionLabel: string;\n secondaryActionLabel: string;\n};\n\nconst homeCardResponseFormat = z.object({\n presentation: HomeCardLLMSchema,\n});\n\n/** Input for a single presenter call (all context pre-assembled). */\nexport interface PresenterInput {\n viewerContext: string;\n otherPartyContext: string;\n matchReasoning: string;\n category: string;\n confidence: number;\n signalsSummary: string;\n indexName: string;\n viewerRole: string;\n opportunityStatus?: string;\n /** True when this opportunity was created via an explicit introduction (not automatic discovery). */\n isIntroduction?: boolean;\n /** Name of the person who made the introduction, if applicable. */\n introducerName?: string;\n}\n\n// ──────────────────────────────────────────────────────────────\n// SYSTEM PROMPT\n// ──────────────────────────────────────────────────────────────\n\nconst systemPrompt = `\nYou are an expert at presenting connection opportunities to users in a way that feels personal and compelling.\n\nYour goal: Given raw context about the viewer (their profile, intents), the other person(s), and why the system matched them, produce a short headline, a personalized summary, and a suggested action.\n\nRules:\n1. Address the VIEWER directly using \"you\" and \"your\". This is for them.\n2. Be concise and compelling — not analytical or third-party. No \"The source user\" or \"The candidate\"; use names or \"they\" where needed.\n3. Do not leak private or confidential details. Use only the context provided.\n4. Vary user-facing nouns naturally. Do not repeatedly use the same label in one response.\n5. If possible, avoid repeating \"opportunity\" in both headline and summary. Prefer alternatives like \"connection\", \"thought partner\", \"mutual fit\", \"valuable conversation\", or \"peer\".\n6. Prefer first names in user-facing copy. Do not repeatedly use full names unless needed to disambiguate.\n\n**Introduction-originated opportunities:**\nWhen INTRODUCTION CONTEXT is provided, this opportunity was explicitly created by an introducer (a real person who saw value in this connection). This is NOT an automatic system discovery — someone made a deliberate judgment.\n- For ALL roles: acknowledge the introducer's role naturally. E.g., \"[Introducer name] thinks you should meet [other person]\" or \"[Introducer name] connected you because...\"\n- The introduction itself is a strong signal — treat it with the weight of a personal recommendation.\n- If the parties' intents don't obviously overlap, that's fine — the introducer saw something worth connecting. Focus on what the introducer likely saw.\n\n**Role-Specific Presentation:**\n\n**If viewer is \"introducer\":**\n- The viewer suggested this connection between two (or more) OTHER people. The opportunity is NOT about the viewer's own needs.\n- Headline: describe the connection between the parties (e.g., \"Connecting a React expert with a startup founder\").\n- Personalized summary: explain why the people YOU are introducing should meet. Reference THEIR profiles and intents, not yours. Frame it as \"you're connecting X and Y because...\" rather than \"this matches your intent\".\n- Suggested action: guide sharing (e.g., \"Share this with [name] to get things started\").\n- CRITICAL: Do NOT reference the introducer's own intents, skills, or needs. The introducer is the matchmaker, not a party.\n\n**If viewer is \"patient\" or \"party\":**\n- Reference their specific intents, skills, or interests that align with this opportunity.\n- If this is an introduction: mention who introduced them and frame it as a personal recommendation.\n- Headline: one short line that hooks (e.g., \"[Name] thinks you should meet [Other]\" or \"A React expert who needs your design skills\").\n- Personalized summary: 2-3 sentences. Why is this opportunity for *them*? If introduced, lead with the introduction.\n- Suggested action: encourage action (\"Send a message to start the conversation\" or \"Share this intro\").\n\n**If viewer is \"agent\":**\n- They are seeing this because someone already reached out.\n- If this is an introduction: mention who made the introduction.\n- Reference their skills/expertise that make them a match.\n- Headline: what the other person needs that they can provide.\n- Personalized summary: 2-3 sentences. Why someone reached out to them.\n- Suggested action: \"Someone is interested in connecting — check their message\" or \"Review and respond\".\n\n**If viewer is \"peer\":**\n- Mutual opportunity. Reference shared or complementary interests.\n- If this is an introduction: mention who connected them.\n- Headline: the mutual connection angle.\n- Personalized summary: 2-3 sentences. Why this is mutually valuable.\n- Suggested action: \"Send an intro to connect\" or \"Start a conversation\".\n`;\n\nconst homeCardSystemPrompt = `\nYou are an expert at presenting connection opportunities for a home feed card.\n\nGiven context about the viewer, the other person, and why they were matched, produce:\n1. headline: one short hook line.\n2. personalizedSummary: 2-3 sentences in \"you\" language (main body text).\n3. digestSummary: one polished morning-brief sentence that can be printed directly after the person's linked name. No markdown, no field labels.\n4. suggestedAction: one brief suggested next step.\n5. narratorRemark: one short sentence for the narrator chip (who is suggesting and why; max ~80 chars).\n6. greeting: a 2-4 sentence first-person message the viewer could send to the counterpart. Plain prose, no greeting prefix, no markdown.\n7. mutualIntentsLabel: short subtitle under the other party's name. Examples: \"3 mutual intents\", \"Shared interests\", \"Aligned goals\" — keep it brief. NEVER output \"0 mutual intents\" or any zero-count label; use a qualitative phrase instead.\n\nRules:\n- Address the viewer with \"you\"/\"your\". Be concise and compelling.\n- narratorRemark should feel like a single sentence from the narrator (Index or a person), not meta-commentary.\n- narratorRemark is displayed with the narrator name prepended (e.g. \"Index: …\" or \"Alice: …\"). Do NOT start narratorRemark with the narrator's name or repeat it; write only the remark (e.g. \"Based on your overlapping intents\" or \"introduced you two, sensing a valuable connection\").\n- Vary wording for the match itself. Do not repeat \"opportunity\" across headline, summary, and narratorRemark when alternatives fit.\n- Prefer first names in user-facing copy. Avoid repeated full names unless disambiguation is necessary.\n- digestSummary must be grammatically complete as a standalone sentence. It should usually start with \"You might like meeting {Name} because ...\" for direct connections, or \"You may be able to help {Name} because ...\" for connector/introducer cards.\n- digestSummary must NOT use awkward third-person fragments like \"Name is...\", \"they're ..., and is...\", \"you is...\", or \"the discoverer's query\".\n- digestSummary must be one sentence, MUST fit within 180 characters when possible, and MUST contain no markdown links; the caller will attach links.\n- If you cannot fit every detail, choose one clear reason and stop. Do not rely on downstream truncation.\n\n**Introduction-originated opportunities (ONLY when INTRODUCTION CONTEXT is provided):**\nWhen INTRODUCTION CONTEXT is provided, this opportunity was explicitly created by an introducer. It was NOT automatically discovered.\n- For parties/patients/agents/peers viewing an introduction: keep the introducer signal in narratorRemark (and narrator chip), not in personalizedSummary.\n- For these introduced parties, personalizedSummary must focus ONLY on fit/value between viewer and counterpart. Do NOT mention the introducer there.\n- narratorRemark should carry the introduction signal (e.g., \"saw strong alignment between you two\" or \"thought this connection could be valuable\"), without repeating the narrator name at the start.\n- This is a personal recommendation, not an algorithm match. Frame it accordingly.\n\n**CRITICAL: NEVER include introducer names in personalizedSummary. Examples:**\n❌ WRONG: \"Seref introduced you to Lucy, who is actively seeking a product co-founder...\"\n✅ CORRECT: \"Lucy is actively seeking a product co-founder for a niche APAC marketplace. With your expertise in UX and AI, this could be an ideal collaboration.\"\n\n❌ WRONG: \"Bob thinks you should meet Alice because your React skills align with her needs.\"\n✅ CORRECT: \"Alice is building a React-based platform and needs frontend expertise. Your experience with component architecture makes you a strong fit.\"\n\n❌ WRONG: \"Jane connected you to Mark, who is looking for a designer.\"\n✅ CORRECT: \"Mark is building a consumer app and needs design expertise. Your background in user-centered design aligns well with what he's building.\"\n\nRemember: The introducer's name goes ONLY in narratorRemark, NEVER in personalizedSummary.\n\n**When INTRODUCTION CONTEXT is NOT provided (system-discovered match):**\n- Do NOT use introducer-style wording. Do NOT say \"you suggested\", \"this is an introduction you suggested\", or \"you suggested this connection\". The system found this match; no human introducer was involved.\n- Instead, narratorRemark should describe why the match is relevant (e.g. \"Based on your overlapping intents\", \"Your skills align with what they need\").\n\n**Negotiation-grounded explanations (ONLY when NEGOTIATION CONTEXT is provided):**\nWhen NEGOTIATION CONTEXT is provided, this opportunity passed through an agent-to-agent negotiation. Use the transcript to ground your explanation in the concrete reasoning the agents exchanged.\n- Personalize the summary with *why* the negotiation produced this match — reference the roles the agents agreed on, the specific concerns raised, and how they were resolved.\n- For status \"stalled\" with reason \"turn_cap\": the agents hit the turn limit without reaching agreement. Frame the card as a hedged possibility rather than a confident match; narratorRemark should signal \"agents couldn't fully converge\" without sounding negative.\n- For status \"stalled\" with reason \"timeout\": one side went silent. Suggest the user re-engage if interested.\n- For status \"accepted\": the agents agreed; the card should confidently explain *why* they agreed.\n- For status \"rejected\": the agents declined. The card should explain the reason briefly so the user understands — not dwell on it.\n- Do NOT invent turn content. Only reference what is in the NEGOTIATION CONTEXT block.\n\n- Exception for connector/introducer: if viewer role is \"introducer\" (any status), this is a curation/connector card. Use:\n - suggestedAction: one short line about sharing the intro or confirming the match.\n - mutualIntentsLabel: a short connector label (e.g. \"Connector match\", \"You can bridge this\").\n - headline: describe the connection between the parties (e.g., \"Connecting a PhD researcher with a translator\"). Do NOT reference the introducer's own needs.\n - personalizedSummary: explain why the parties you're introducing should meet, referencing THEIR profiles and intents, not yours.\n\n**CRITICAL for latent introducer cards (opportunity status is \"latent\"):**\nWhen the viewer is the introducer and the opportunity status is \"latent\", the introducer has NOT yet approved this match. They are evaluating whether to make the introduction.\n- narratorRemark MUST use evaluation/curation language (e.g. \"Could be a strong match\", \"Worth introducing?\", \"Interesting overlap here\").\n- Do NOT say \"you suggested\", \"you introduced\", \"you connected\", or any past-tense language implying the introduction was already made.\n- suggestedAction should encourage evaluation (e.g. \"Approve if you see the fit\").\n- Exception for new-connection reveal: if viewer role is \"agent\", status is \"accepted\", and there is an introducer, this is the agent's first time seeing this opportunity. Use:\n - suggestedAction: a short line about joining the conversation.\n`;\n\n// ──────────────────────────────────────────────────────────────\n// CLASS\n// ──────────────────────────────────────────────────────────────\n\nexport class OpportunityPresenter {\n private model: Runnable;\n private homeCardModel: Runnable;\n\n constructor() {\n this.model = model.withStructuredOutput(responseFormat, {\n name: \"opportunity_presenter\",\n });\n this.homeCardModel = model.withStructuredOutput(homeCardResponseFormat, {\n name: \"opportunity_presenter_home_card\",\n });\n }\n\n private async invokeWithTimeout(\n targetModel: Runnable,\n messages: (SystemMessage | HumanMessage)[],\n ): Promise<unknown> {\n const timeoutReason = `LLM invoke timed out after ${LLM_TIMEOUT_MS}ms`;\n const controller = new AbortController();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const invokePromise = targetModel.invoke(messages, {\n signal: controller.signal,\n });\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n controller.abort(timeoutReason);\n reject(new Error(timeoutReason));\n }, LLM_TIMEOUT_MS);\n });\n\n try {\n return await Promise.race([invokePromise, timeoutPromise]);\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n\n /**\n * Generate personalized presentation for a single opportunity.\n */\n @Timed()\n public async present(\n input: PresenterInput,\n ): Promise<OpportunityPresentationResult> {\n const introContext = input.isIntroduction\n ? `\\nINTRODUCTION CONTEXT: This opportunity was created by an explicit introduction from ${input.introducerName ?? \"someone in the community\"}. It was NOT discovered automatically — a real person made this connection.\\n`\n : \"\";\n const humanContent = `\nVIEWER (the person seeing this opportunity):\n${input.viewerContext}\n\nOTHER PARTY:\n${input.otherPartyContext}\n\nMATCH CONTEXT:\n- Category: ${input.category}\n- Confidence: ${input.confidence}\n- Why we matched: ${input.matchReasoning}\n- Signals: ${input.signalsSummary}\n${introContext}\nCOMMUNITY: ${input.indexName}\nViewer's role in this opportunity: ${input.viewerRole}\n\nProduce headline, personalizedSummary (2-3 sentences in \"you\" language), suggestedAction, and greeting.\n`;\n\n try {\n const messages = [\n new SystemMessage(systemPrompt),\n new HumanMessage(humanContent),\n ];\n const result = await this.invokeWithTimeout(this.model, messages);\n const parsed = responseFormat.parse(result);\n parsed.presentation.personalizedSummary = stripUuids(parsed.presentation.personalizedSummary);\n return parsed.presentation;\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n const timeoutReason = message.includes(\"timed out\") ? message : undefined;\n logger.warn(\n \"[OpportunityPresenter.present] LLM failed, returning fallback\",\n {\n event: \"presenter_fallback\",\n presenter: \"opportunity\",\n reason: timeoutReason ? \"timeout\" : \"parse_error\",\n message,\n timeoutReason,\n },\n );\n return {\n headline: \"A promising connection\",\n personalizedSummary: truncateAtBoundary(stripUuids(input.matchReasoning), 300),\n suggestedAction: \"Take a look and decide whether to reach out.\",\n greeting: \"\",\n };\n }\n }\n\n /**\n * Generate LLM-powered home-card content (headline, body, narrator remark, mutual-intent label).\n * Callers append button labels from opportunity.constants.\n *\n * When `negotiationContext.status === 'negotiating'`, returns a templated\n * chip synchronously without invoking the LLM — the card just reflects\n * \"negotiation in progress\" at that point.\n */\n @Timed()\n public async presentHomeCard(\n input: HomeCardPresenterInput,\n ): Promise<HomeCardLLMResult> {\n if (input.negotiationContext?.status === 'negotiating') {\n return buildNegotiatingChip(input);\n }\n\n const mutualHint =\n input.mutualIntentCount != null && input.mutualIntentCount > 0\n ? `There are ${input.mutualIntentCount} overlapping intent(s) between viewer and other party.`\n : \"Match is based on profile and intent alignment. Do not cite a numeric intent count.\";\n const introContext = input.isIntroduction\n ? `\\nINTRODUCTION CONTEXT: This opportunity was created by an explicit introduction from ${input.introducerName ?? \"someone in the community\"}. It was NOT discovered automatically — a real person made this connection.\\n`\n : \"\";\n const negotiationBlock = buildNegotiationPromptBlock(input.negotiationContext);\n // When negotiation context exists, lead with it — these cards exist\n // *because* the negotiation happened. Trailing the block lets weaker\n // models lean on surface signals and ignore the transcript entirely.\n const negotiationDirective = negotiationBlock\n ? `\\nIMPORTANT: This opportunity surfaced because the agents negotiated and converged. Your personalizedSummary MUST reference at least one specific signal from the NEGOTIATION CONTEXT block below — what concern was raised, what was confirmed, what the agents agreed on. Do not produce a generic skill-complementarity summary; that's what every card looked like before this negotiation happened. Use the transcript to explain *why this specific match* surfaced now.\\n`\n : \"\";\n const humanContent = `\n${negotiationBlock}${negotiationDirective}\nVIEWER (the person seeing this opportunity):\n${input.viewerContext}\n\nOTHER PARTY:\n${input.otherPartyContext}\n\nMATCH CONTEXT:\n- Category: ${input.category}\n- Confidence: ${input.confidence}\n- Why we matched: ${input.matchReasoning}\n- Signals: ${input.signalsSummary}\n- ${mutualHint}\n${introContext}\nCOMMUNITY: ${input.indexName}\nViewer's role in this opportunity: ${input.viewerRole}\nOpportunity status: ${input.opportunityStatus ?? \"pending\"}\n\nProduce headline, personalizedSummary, digestSummary, suggestedAction, narratorRemark, greeting, and mutualIntentsLabel.\n`;\n\n const isIntroducer = input.viewerRole === \"introducer\";\n\n try {\n const messages = [\n new SystemMessage(homeCardSystemPrompt),\n new HumanMessage(humanContent),\n ];\n const result = await this.invokeWithTimeout(this.homeCardModel, messages);\n const parsed = homeCardResponseFormat.parse(result);\n parsed.presentation.personalizedSummary = stripUuids(parsed.presentation.personalizedSummary);\n parsed.presentation.digestSummary = stripUuids(parsed.presentation.digestSummary);\n parsed.presentation.narratorRemark = stripUuids(parsed.presentation.narratorRemark);\n if (/^0\\s+(mutual|overlapping)\\s+intent/i.test(parsed.presentation.mutualIntentsLabel)) {\n parsed.presentation.mutualIntentsLabel = \"Shared interests\";\n }\n if (input.isIntroduction && input.introducerName) {\n parsed.presentation.personalizedSummary = stripIntroducerMentions(\n parsed.presentation.personalizedSummary,\n input.introducerName,\n );\n parsed.presentation.digestSummary = stripIntroducerMentions(\n parsed.presentation.digestSummary,\n input.introducerName,\n );\n }\n return parsed.presentation;\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n const timeoutReason = message.includes(\"timed out\") ? message : undefined;\n logger.warn(\n \"[OpportunityPresenter.presentHomeCard] LLM failed, returning fallback\",\n {\n event: \"presenter_fallback\",\n presenter: \"home_card\",\n reason: timeoutReason ? \"timeout\" : \"parse_error\",\n message,\n timeoutReason,\n },\n );\n let fallbackSummary = truncateAtBoundary(stripUuids(input.matchReasoning), 300);\n if (input.isIntroduction && input.introducerName) {\n fallbackSummary = stripIntroducerMentions(fallbackSummary, input.introducerName);\n }\n return {\n headline: \"A promising connection\",\n personalizedSummary: fallbackSummary,\n digestSummary: isIntroducer\n ? \"You may be able to help make a useful introduction here.\"\n : \"You might like meeting them based on your current interests.\",\n suggestedAction: isIntroducer\n ? \"Share this introduction to get things started.\"\n : \"Take a look and decide whether to reach out.\",\n narratorRemark: \"Worth a look.\",\n mutualIntentsLabel: isIntroducer\n ? \"Connector match\"\n : input.mutualIntentCount != null && input.mutualIntentCount > 0\n ? `${input.mutualIntentCount} mutual intent${input.mutualIntentCount !== 1 ? \"s\" : \"\"}`\n : \"Shared interests\",\n greeting: \"\",\n };\n }\n }\n\n /**\n * Process multiple opportunities in parallel with bounded concurrency.\n */\n @Timed()\n public async presentBatch(\n inputs: PresenterInput[],\n options?: { concurrency?: number },\n ): Promise<OpportunityPresentationResult[]> {\n const concurrency = options?.concurrency ?? 5;\n const results: OpportunityPresentationResult[] = [];\n for (let i = 0; i < inputs.length; i += concurrency) {\n const chunk = inputs.slice(i, i + concurrency);\n const chunkResults = await Promise.all(\n chunk.map((inp) => this.present(inp)),\n );\n results.push(...chunkResults);\n }\n return results;\n }\n\n /**\n * Process multiple opportunities as home cards in parallel with bounded concurrency.\n * Returns full home-card display contracts (headline, body, narrator remark, action labels, mutual-intent label).\n */\n @Timed()\n public async presentHomeCardBatch(\n inputs: HomeCardPresenterInput[],\n options?: { concurrency?: number },\n ): Promise<HomeCardLLMResult[]> {\n const concurrency = options?.concurrency ?? 5;\n const results: HomeCardLLMResult[] = [];\n for (let i = 0; i < inputs.length; i += concurrency) {\n const chunk = inputs.slice(i, i + concurrency);\n const chunkResults = await Promise.all(\n chunk.map((inp) => this.presentHomeCard(inp)),\n );\n results.push(...chunkResults);\n }\n return results;\n }\n}\n\n// ──────────────────────────────────────────────────────────────\n// NEGOTIATION CONTEXT HELPERS\n// ──────────────────────────────────────────────────────────────\n\n/**\n * Builds a \"NEGOTIATION CONTEXT:\" block for the home-card prompt. Returns an\n * empty string when the opportunity has no meaningful negotiation context\n * (draft/latent) or when the opportunity is still negotiating (handled via\n * the templated chip, not the LLM).\n */\nfunction buildNegotiationPromptBlock(context: NegotiationContext | undefined): string {\n if (!context || context.status === 'negotiating') return \"\";\n\n const turnCapLabel = context.turnCap > 0 ? `${context.turnCap}` : \"unlimited\";\n const reason = context.outcome?.reason;\n const reasonLabel = reason === 'turn_cap'\n ? \"agents hit the turn cap without converging\"\n : reason === 'timeout'\n ? \"counterpart went silent before responding\"\n : undefined;\n\n const turnLines = (context.turns ?? []).map((turn, index) => {\n const action = turn.action;\n const reasoning = turn.assessment?.reasoning ?? \"(no reasoning)\";\n const message = turn.message ? ` — said: \"${turn.message}\"` : \"\";\n return `Turn ${index + 1} (${action}): ${reasoning}${message}`;\n });\n\n const outcomeSummary = context.outcome\n ? `Final outcome: ${context.outcome.hasOpportunity ? \"agreed\" : \"declined\"} — ${context.outcome.reasoning}`\n : \"Final outcome: not recorded.\";\n\n return `\nNEGOTIATION CONTEXT:\n- Negotiation status: ${context.status}${reasonLabel ? ` (${reasonLabel})` : \"\"}\n- Turns exchanged: ${context.turnCount} of ${turnCapLabel}\n- Transcript:\n${turnLines.length > 0 ? turnLines.map((l) => ` ${l}`).join(\"\\n\") : \" (no turns recorded)\"}\n- ${outcomeSummary}\n`;\n}\n\n/**\n * Builds a templated home-card result for an opportunity whose negotiation\n * is still in progress. Bypasses the LLM so users see a stable \"currently\n * negotiating\" chip while turns are still being exchanged.\n */\nfunction buildNegotiatingChip(input: HomeCardPresenterInput): HomeCardLLMResult {\n const ctx = input.negotiationContext;\n const turnCount = ctx?.turnCount ?? 0;\n const turnCap = ctx?.turnCap && ctx.turnCap > 0 ? ctx.turnCap : undefined;\n const narratorRemark = turnCap\n ? `Currently negotiating · turn ${turnCount} of ${turnCap}`\n : `Currently negotiating · turn ${turnCount}`;\n\n return {\n headline: \"Negotiation in progress\",\n personalizedSummary: \"Your agent is still talking with theirs to see if this connection makes sense. We'll surface the full match as soon as they converge.\",\n digestSummary: \"Your agent is still checking whether this connection makes sense.\",\n suggestedAction: \"Check back shortly — no action needed yet.\",\n narratorRemark,\n mutualIntentsLabel: input.mutualIntentCount && input.mutualIntentCount > 0\n ? `${input.mutualIntentCount} mutual intent${input.mutualIntentCount !== 1 ? \"s\" : \"\"}`\n : \"Shared interests\",\n greeting: \"\",\n };\n}\n\n// ──────────────────────────────────────────────────────────────\n// CONTEXT GATHERER (used by tools)\n// ──────────────────────────────────────────────────────────────\n\n/**\n * Gather all context needed for the presenter from the database.\n * Fetches viewer profile, viewer intents, other party profile(s), and index in parallel.\n *\n * @param displayCounterpartUserId - When set (e.g. for home card), only this counterpart is included in otherPartyContext so the presenter writes about the person on the card. Omitted for introducer view (card shows both parties).\n */\nexport async function gatherPresenterContext(\n database: PresenterDatabase,\n opportunity: Opportunity,\n viewerId: string,\n displayCounterpartUserId?: string,\n): Promise<PresenterInput> {\n const myActor = opportunity.actors.find((a) => a.userId === viewerId);\n if (!myActor) {\n throw new Error(\"Viewer is not an actor in this opportunity\");\n }\n\n const isIntroducer = myActor.role === \"introducer\";\n const otherActors = opportunity.actors.filter((a) => a.userId !== viewerId);\n let otherPartyIds = [...new Set(otherActors.map((a) => a.userId))];\n if (\n displayCounterpartUserId &&\n !isIntroducer &&\n otherPartyIds.includes(displayCounterpartUserId)\n ) {\n otherPartyIds = [displayCounterpartUserId];\n }\n\n const contextIndexId = opportunity.context?.networkId;\n\n // For introducers: fetch profiles + intents for both parties; skip introducer's own intents.\n // For other roles: fetch viewer's profile + intents and other party profiles.\n const [viewerProfile, indexRecord, ...otherProfiles] = await Promise.all([\n database.getProfile(viewerId),\n contextIndexId ? database.getNetwork(contextIndexId) : Promise.resolve(null),\n ...otherPartyIds.map((uid) => database.getProfile(uid)),\n ]);\n\n // Fetch intents: for introducer, fetch each party's intents; otherwise fetch viewer's intents.\n let viewerIntents:\n | Awaited<ReturnType<typeof database.getActiveIntents>>\n | undefined;\n let partyIntentsMap:\n | Map<string, Awaited<ReturnType<typeof database.getActiveIntents>>>\n | undefined;\n\n if (isIntroducer) {\n const partyIntentResults = await Promise.all(\n otherPartyIds.map(async (uid) => ({\n uid,\n intents: await database.getActiveIntents(uid),\n })),\n );\n partyIntentsMap = new Map(\n partyIntentResults.map((r) => [r.uid, r.intents]),\n );\n } else {\n viewerIntents = await database.getActiveIntents(viewerId);\n }\n\n // Fetch premises when any actor is premise-grounded\n const premiseGroundedActors = opportunity.actors.filter((a) => a.premise);\n let viewerPremiseContext = '';\n let otherPremiseContext = '';\n\n if (premiseGroundedActors.length > 0) {\n // Only fetch premises for actors that are actually premise-grounded, not all parties\n const groundedOtherIds = premiseGroundedActors\n .filter((a) => a.userId !== viewerId)\n .map((a) => a.userId);\n const viewerIsGrounded = premiseGroundedActors.some((a) => a.userId === viewerId);\n\n const results = await Promise.all([\n ...(viewerIsGrounded ? [database.getPremisesForUser(viewerId, 'ACTIVE')] : []),\n ...groundedOtherIds.map((uid) => database.getPremisesForUser(uid, 'ACTIVE')),\n ]);\n\n let idx = 0;\n if (viewerIsGrounded) {\n const viewerPremises = results[idx++];\n if (viewerPremises?.length) {\n viewerPremiseContext =\n '\\nPremises (self-descriptions):\\n' +\n viewerPremises\n .slice(0, 5)\n .map((p) => `- ${p.assertion.text}`)\n .join('\\n');\n }\n }\n\n const otherPremiseLines: string[] = [];\n for (let i = 0; i < groundedOtherIds.length; i++) {\n const premises = results[idx++];\n if (premises?.length) {\n for (const p of premises.slice(0, 3)) {\n otherPremiseLines.push(`- ${p.assertion.text}`);\n }\n }\n }\n if (otherPremiseLines.length > 0) {\n otherPremiseContext = '\\nPremises (self-descriptions):\\n' + otherPremiseLines.join('\\n');\n }\n }\n\n let viewerContext: string;\n let otherPartyContext: string;\n\n if (isIntroducer) {\n // Introducer view: minimal viewer context (just name + role), rich other-party context with intents\n const introducerApproved = opportunity.actors.find(a => a.role === 'introducer')?.approved === true;\n const hasApproved = introducerApproved || opportunity.status !== 'latent';\n viewerContext = [\n \"Profile:\",\n `Name: ${viewerProfile?.identity?.name ?? \"Unknown\"}`,\n hasApproved\n ? \"Role: You are the introducer who suggested this connection.\"\n : \"Role: You are being asked whether these two people would benefit from meeting. You have NOT yet approved this introduction.\",\n ].join(\"\\n\");\n\n const otherParts = otherPartyIds.map((uid, idx) => {\n const profile = otherProfiles[idx] as Awaited<\n ReturnType<typeof database.getProfile>\n >;\n const name = profile?.identity?.name ?? \"Unknown\";\n const bio = profile?.identity?.bio ?? \"\";\n const location = profile?.identity?.location ?? \"\";\n const skills = profile?.attributes?.skills?.join(\", \") ?? \"\";\n const interests = profile?.attributes?.interests?.join(\", \") ?? \"\";\n const context = profile?.narrative?.context ?? \"\";\n const intents = partyIntentsMap?.get(uid);\n const intentLines = intents?.length\n ? intents\n .slice(0, 5)\n .map((i) => ` - ${i.payload}${i.summary ? ` (${i.summary})` : \"\"}`)\n : [\" (no active intents)\"];\n return [\n `${name}:`,\n ` Bio: ${bio}`,\n location ? ` Location: ${location}` : null,\n skills ? ` Skills: ${skills}` : null,\n interests ? ` Interests: ${interests}` : null,\n context ? ` Context: ${context}` : null,\n ` Active intents:`,\n ...intentLines,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n });\n otherPartyContext =\n otherParts.join(\"\\n\\n\") || \"Parties (details not available).\";\n } else {\n // Non-introducer view: full viewer profile + intents, other party profiles\n const viewerContextLines = [\n \"Profile:\",\n `Name: ${viewerProfile?.identity?.name ?? \"Unknown\"}`,\n `Bio: ${viewerProfile?.identity?.bio ?? \"\"}`,\n `Location: ${viewerProfile?.identity?.location ?? \"\"}`,\n `Skills: ${viewerProfile?.attributes?.skills?.join(\", \") ?? \"\"}`,\n `Interests: ${viewerProfile?.attributes?.interests?.join(\", \") ?? \"\"}`,\n `Context: ${viewerProfile?.narrative?.context ?? \"\"}`,\n \"Active intents:\",\n ...(viewerIntents?.length\n ? viewerIntents.map(\n (i) => `- ${i.payload}${i.summary ? ` (${i.summary})` : \"\"}`,\n )\n : [\"(none listed)\"]),\n ];\n viewerContext = viewerContextLines.join(\"\\n\");\n\n const otherParts = otherPartyIds.map((uid, idx) => {\n const profile = otherProfiles[idx] as Awaited<\n ReturnType<typeof database.getProfile>\n >;\n const name = profile?.identity?.name ?? \"Unknown\";\n const bio = profile?.identity?.bio ?? \"\";\n const skills = profile?.attributes?.skills?.join(\", \") ?? \"\";\n const interests = profile?.attributes?.interests?.join(\", \") ?? \"\";\n return `${name}: ${bio}. Skills: ${skills}. Interests: ${interests}`;\n });\n otherPartyContext =\n otherParts.join(\"\\n\\n\") || \"Other party (details not available).\";\n }\n\n const interp = opportunity.interpretation;\n const signalsSummary =\n interp.signals?.map((s) => `${s.type}: ${s.detail ?? s.type}`).join(\"; \") ??\n \"Match based on profile and intent alignment.\";\n\n // Detect introduction-originated opportunities: only when there is an explicit introducer actor.\n // Do NOT use detection.source === \"manual\" alone — system-discovered opportunities can have manual source without an introducer.\n const introducerActor = opportunity.actors.find(\n (a) => a.role === \"introducer\",\n );\n const isIntroduction = !!introducerActor;\n let introducerName: string | undefined;\n if (introducerActor) {\n introducerName = opportunity.detection.createdByName;\n if (!introducerName) {\n const introducerProfile = await database.getProfile(\n introducerActor.userId,\n );\n introducerName = introducerProfile?.identity?.name ?? undefined;\n }\n }\n\n const counterpartName =\n otherPartyIds.length === 1 && otherProfiles[0]\n ? (otherProfiles[0] as { identity?: { name?: string } })?.identity?.name?.trim()\n : undefined;\n const viewerNameForFilter = viewerProfile?.identity?.name?.trim();\n const matchReasoning =\n counterpartName && interp.reasoning\n ? viewerCentricCardSummary(\n interp.reasoning,\n counterpartName,\n 400,\n viewerNameForFilter,\n introducerName,\n )\n : stripUuids(interp.reasoning);\n\n if (viewerPremiseContext) {\n viewerContext += viewerPremiseContext;\n }\n if (otherPremiseContext) {\n otherPartyContext += otherPremiseContext;\n }\n\n const result: PresenterInput = {\n viewerContext,\n otherPartyContext,\n matchReasoning,\n category: interp.category ?? \"connection\",\n confidence:\n typeof interp.confidence === \"number\"\n ? interp.confidence\n : parseFloat(String(interp.confidence ?? 0)) || 0,\n signalsSummary,\n indexName: indexRecord?.title ?? contextIndexId ?? \"\",\n viewerRole: myActor.role ?? \"party\",\n isIntroduction,\n introducerName,\n };\n\n return result;\n}\n"]}
|