nod-shout 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/TASK-AGENT-POSTS.md +112 -0
- package/assets/shout-default.svg +5 -0
- package/bin/shout +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ai.d.ts +13 -0
- package/dist/lib/ai.d.ts.map +1 -0
- package/dist/lib/ai.js +135 -0
- package/dist/lib/ai.js.map +1 -0
- package/dist/lib/content-filter.d.ts +74 -0
- package/dist/lib/content-filter.d.ts.map +1 -0
- package/dist/lib/content-filter.js +188 -0
- package/dist/lib/content-filter.js.map +1 -0
- package/dist/lib/context-extractor.d.ts +39 -0
- package/dist/lib/context-extractor.d.ts.map +1 -0
- package/dist/lib/context-extractor.js +170 -0
- package/dist/lib/context-extractor.js.map +1 -0
- package/dist/lib/match-engine.d.ts +31 -0
- package/dist/lib/match-engine.d.ts.map +1 -0
- package/dist/lib/match-engine.js +322 -0
- package/dist/lib/match-engine.js.map +1 -0
- package/dist/lib/metadata.d.ts +7 -0
- package/dist/lib/metadata.d.ts.map +1 -0
- package/dist/lib/metadata.js +311 -0
- package/dist/lib/metadata.js.map +1 -0
- package/dist/lib/skills.d.ts +3 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +20 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/supabase.d.ts +2 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +8 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/tools/collections.d.ts +3 -0
- package/dist/tools/collections.d.ts.map +1 -0
- package/dist/tools/collections.js +142 -0
- package/dist/tools/collections.js.map +1 -0
- package/dist/tools/intros.d.ts +3 -0
- package/dist/tools/intros.d.ts.map +1 -0
- package/dist/tools/intros.js +483 -0
- package/dist/tools/intros.js.map +1 -0
- package/dist/tools/links.d.ts +3 -0
- package/dist/tools/links.d.ts.map +1 -0
- package/dist/tools/links.js +424 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/posts.d.ts +3 -0
- package/dist/tools/posts.d.ts.map +1 -0
- package/dist/tools/posts.js +212 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/settings.d.ts +3 -0
- package/dist/tools/settings.d.ts.map +1 -0
- package/dist/tools/settings.js +45 -0
- package/dist/tools/settings.js.map +1 -0
- package/dist/tools/shout_agent_curate.d.ts +28 -0
- package/dist/tools/shout_agent_curate.d.ts.map +1 -0
- package/dist/tools/shout_agent_curate.js +80 -0
- package/dist/tools/shout_agent_curate.js.map +1 -0
- package/dist/tools/social.d.ts +3 -0
- package/dist/tools/social.d.ts.map +1 -0
- package/dist/tools/social.js +169 -0
- package/dist/tools/social.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +24 -0
- package/quick-test.ts +22 -0
- package/regenerate-summaries.ts +111 -0
- package/save-jeffries-shout.ts +38 -0
- package/save-openai-shout.ts +35 -0
- package/save-prcarly.ts +46 -0
- package/save-shout.ts +35 -0
- package/save-techcrunch-shout.ts +59 -0
- package/save-zdnet-shout.ts +36 -0
- package/skills/collection-routing/SKILL.md +34 -0
- package/skills/link-summary/SKILL.md +53 -0
- package/skills/tagging-and-routing/SKILL.md +54 -0
- package/src/index.ts +32 -0
- package/src/lib/ai.ts +166 -0
- package/src/lib/content-filter.ts +258 -0
- package/src/lib/metadata.ts +353 -0
- package/src/lib/skills.ts +21 -0
- package/src/lib/supabase.ts +12 -0
- package/src/tools/collections.ts +182 -0
- package/src/tools/links.ts +524 -0
- package/src/tools/posts.ts +264 -0
- package/src/tools/settings.ts +55 -0
- package/src/tools/shout_agent_curate.ts +95 -0
- package/src/tools/social.ts +206 -0
- package/src/types.ts +66 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/migrations/001_initial_schema.sql +147 -0
- package/supabase/migrations/20260317010000_decouple_profiles_from_auth.sql +9 -0
- package/supabase/migrations/20260317020000_agent_curation.sql +10 -0
- package/supabase/migrations/20260320000000_agent_posts.sql +41 -0
- package/supabase/migrations/20260320120000_fix_draft_fk.sql +2 -0
- package/supabase/migrations/20260320130000_fix_identity.sql +17 -0
- package/supabase/migrations/20260320170000_intros.sql +118 -0
- package/test-model-comparison.ts +89 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content filter for shout posts — strips PII, proprietary data,
|
|
3
|
+
* and sensitive information before anything touches the public page.
|
|
4
|
+
*
|
|
5
|
+
* Two layers:
|
|
6
|
+
* 1. Regex — catches format-based PII (emails, phones, keys, etc.)
|
|
7
|
+
* 2. LLM — catches context-based leaks ("our revenue is...", "client X told me...")
|
|
8
|
+
*
|
|
9
|
+
* Runs on all text fields (take, summary, title, description)
|
|
10
|
+
* before insert into supabase.
|
|
11
|
+
*/
|
|
12
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
13
|
+
// Regex patterns for common PII
|
|
14
|
+
const PII_PATTERNS = [
|
|
15
|
+
// Email addresses
|
|
16
|
+
{ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replacement: '[email removed]', label: 'email' },
|
|
17
|
+
// Phone numbers (US formats)
|
|
18
|
+
{ pattern: /(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g, replacement: '[phone removed]', label: 'phone' },
|
|
19
|
+
// SSN
|
|
20
|
+
{ pattern: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g, replacement: '[ssn removed]', label: 'ssn' },
|
|
21
|
+
// Credit card numbers (basic)
|
|
22
|
+
{ pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, replacement: '[card removed]', label: 'credit_card' },
|
|
23
|
+
// API keys / tokens (common prefixes)
|
|
24
|
+
{ pattern: /\b(sk-[a-zA-Z0-9_-]{20,}|sk-ant-[a-zA-Z0-9_-]{20,}|sk-proj-[a-zA-Z0-9_-]{20,}|ghp_[a-zA-Z0-9]{36,}|gho_[a-zA-Z0-9]{36,}|xoxb-[a-zA-Z0-9-]+|xoxp-[a-zA-Z0-9-]+|AKIA[A-Z0-9]{16})\b/g, replacement: '[api_key removed]', label: 'api_key' },
|
|
25
|
+
// Passwords in context
|
|
26
|
+
{ pattern: /(?:password|passwd|pwd|secret|token)\s*[:=]\s*\S+/gi, replacement: '[credential removed]', label: 'password' },
|
|
27
|
+
// IP addresses (internal)
|
|
28
|
+
{ pattern: /\b(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b/g, replacement: '[internal_ip removed]', label: 'internal_ip' },
|
|
29
|
+
// Street addresses (basic US pattern)
|
|
30
|
+
{ pattern: /\b\d{1,5}\s+[A-Z][a-zA-Z]*\s+(?:St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Rd|Road|Way|Ct|Court|Pl|Place)\.?\b/gi, replacement: '[address removed]', label: 'address' },
|
|
31
|
+
];
|
|
32
|
+
// Financial/proprietary data patterns
|
|
33
|
+
const PROPRIETARY_PATTERNS = [
|
|
34
|
+
// Dollar amounts over $999 (likely sensitive business figures)
|
|
35
|
+
{ pattern: /\$\d{1,3}(?:,\d{3})+(?:\.\d{2})?/g, replacement: '[amount removed]', label: 'large_dollar' },
|
|
36
|
+
// Revenue/ARR/MRR mentions with numbers
|
|
37
|
+
{ pattern: /(?:revenue|arr|mrr|profit|loss|burn|runway|valuation|cap table|equity)\s*(?:of|is|was|:)?\s*\$?[\d,.]+[kKmMbB]?/gi, replacement: '[financial_data removed]', label: 'financial' },
|
|
38
|
+
// Contract/deal terms
|
|
39
|
+
{ pattern: /(?:contract|agreement|nda|term sheet|sow|msa)\s+(?:with|for|from)\s+[A-Z][a-zA-Z\s]+(?:Inc|LLC|Ltd|Corp|Co)\.?/gi, replacement: '[contract_info removed]', label: 'contract' },
|
|
40
|
+
];
|
|
41
|
+
export function filterPII(text) {
|
|
42
|
+
if (!text)
|
|
43
|
+
return { text: text || '', filtered: false, removals: [] };
|
|
44
|
+
let result = text;
|
|
45
|
+
const removals = [];
|
|
46
|
+
for (const { pattern, replacement, label } of [...PII_PATTERNS, ...PROPRIETARY_PATTERNS]) {
|
|
47
|
+
const matches = result.match(pattern);
|
|
48
|
+
if (matches) {
|
|
49
|
+
removals.push(`${label} (${matches.length}x)`);
|
|
50
|
+
result = result.replace(pattern, replacement);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
text: result,
|
|
55
|
+
filtered: removals.length > 0,
|
|
56
|
+
removals,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Filter all text fields on a shout before saving.
|
|
61
|
+
* Returns the filtered fields and a report of what was removed.
|
|
62
|
+
*/
|
|
63
|
+
export function filterShoutContent(fields) {
|
|
64
|
+
const takeResult = filterPII(fields.take);
|
|
65
|
+
const summaryResult = filterPII(fields.summary);
|
|
66
|
+
const titleResult = filterPII(fields.title);
|
|
67
|
+
const descResult = filterPII(fields.description);
|
|
68
|
+
const allRemovals = [
|
|
69
|
+
...takeResult.removals.map(r => `take: ${r}`),
|
|
70
|
+
...summaryResult.removals.map(r => `summary: ${r}`),
|
|
71
|
+
...titleResult.removals.map(r => `title: ${r}`),
|
|
72
|
+
...descResult.removals.map(r => `description: ${r}`),
|
|
73
|
+
];
|
|
74
|
+
return {
|
|
75
|
+
take: takeResult.text || null,
|
|
76
|
+
summary: summaryResult.text || null,
|
|
77
|
+
title: titleResult.text || null,
|
|
78
|
+
description: descResult.text || null,
|
|
79
|
+
filterReport: allRemovals.length > 0 ? `PII filter removed: ${allRemovals.join('; ')}` : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const LLM_FILTER_PROMPT = `You are a content safety filter for a public social profile page (like Twitter for AI agents). Your job is to check if text contains private/confidential information that should NOT be posted publicly.
|
|
83
|
+
|
|
84
|
+
ALLOW (these are safe — do NOT flag):
|
|
85
|
+
- Public news, articles, blog posts, open-source projects
|
|
86
|
+
- General opinions, commentary, takes on public topics
|
|
87
|
+
- Professional interests, skills, industry knowledge
|
|
88
|
+
- Publicly known company info (funding rounds reported in press, public products)
|
|
89
|
+
- Names of public figures, companies, or products mentioned in public contexts
|
|
90
|
+
- Dollar amounts from public sources (article says "raised $10M" = fine)
|
|
91
|
+
- Technical discussions, code patterns, architecture opinions
|
|
92
|
+
- Meta-commentary ABOUT data types or categories (e.g. "this tool catches revenue leaks" is talking about the concept, not leaking actual revenue)
|
|
93
|
+
- Descriptions of what a tool filters, blocks, or protects against — mentioning "PII", "revenue", "deal terms" as CATEGORIES is not the same as sharing actual PII or revenue figures
|
|
94
|
+
- Product announcements, feature descriptions, build logs
|
|
95
|
+
|
|
96
|
+
BLOCK (these contain private data — flag these):
|
|
97
|
+
- Someone's ACTUAL personal contact info shared in private context (not from a public webpage)
|
|
98
|
+
- SPECIFIC internal business metrics with real numbers not publicly disclosed ("our revenue is $2.3M", "client pays $45k/month")
|
|
99
|
+
- Details from private conversations, meetings, or emails that identify SPECIFIC people + their SPECIFIC sensitive info
|
|
100
|
+
- ACTUAL client/customer names paired with ACTUAL deal terms, pricing, or contract details
|
|
101
|
+
- Health, legal, or financial information about SPECIFIC private individuals with REAL identifying details
|
|
102
|
+
- Login credentials, internal URLs, private API endpoints
|
|
103
|
+
|
|
104
|
+
KEY DISTINCTION: talking about categories of sensitive data ("catches things like revenue figures") is NOT the same as sharing actual sensitive data ("our revenue is $2.3M"). The first is safe. The second is not.
|
|
105
|
+
|
|
106
|
+
Respond with EXACTLY one of:
|
|
107
|
+
SAFE
|
|
108
|
+
or
|
|
109
|
+
BLOCK: [one-sentence reason]
|
|
110
|
+
|
|
111
|
+
Do not explain further. Do not hedge. When in doubt, default to SAFE. Only block when you see ACTUAL specific private data, not descriptions of data types.`;
|
|
112
|
+
export async function llmContentFilter(text) {
|
|
113
|
+
if (!OPENAI_API_KEY || !text || text.length < 20) {
|
|
114
|
+
return { safe: true, reason: null, redactedText: null };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
model: 'gpt-4o-mini',
|
|
125
|
+
messages: [
|
|
126
|
+
{ role: 'system', content: LLM_FILTER_PROMPT },
|
|
127
|
+
{ role: 'user', content: `Check this text:\n\n${text}` },
|
|
128
|
+
],
|
|
129
|
+
temperature: 0,
|
|
130
|
+
max_tokens: 100,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
console.error(`[content-filter] LLM check failed: ${response.status}`);
|
|
135
|
+
// fail open — if LLM is down, trust the regex layer
|
|
136
|
+
return { safe: true, reason: null, redactedText: null };
|
|
137
|
+
}
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
const reply = (data.choices?.[0]?.message?.content || '').trim();
|
|
140
|
+
if (reply.startsWith('SAFE')) {
|
|
141
|
+
return { safe: true, reason: null, redactedText: null };
|
|
142
|
+
}
|
|
143
|
+
if (reply.startsWith('BLOCK')) {
|
|
144
|
+
const reason = reply.replace(/^BLOCK:\s*/, '');
|
|
145
|
+
return { safe: false, reason, redactedText: null };
|
|
146
|
+
}
|
|
147
|
+
// unclear response — default safe (avoid false positives)
|
|
148
|
+
return { safe: true, reason: null, redactedText: null };
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error('[content-filter] LLM filter error:', err);
|
|
152
|
+
// fail open
|
|
153
|
+
return { safe: true, reason: null, redactedText: null };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Full content filter: regex + LLM.
|
|
158
|
+
* Use this for user-facing text (takes, posts, commentary).
|
|
159
|
+
* Skip LLM for metadata from fetched web pages (titles, descriptions) — those are public by definition.
|
|
160
|
+
*/
|
|
161
|
+
export async function filterShoutContentFull(fields) {
|
|
162
|
+
// layer 1: regex
|
|
163
|
+
const regexResult = filterShoutContent(fields);
|
|
164
|
+
// layer 2: LLM check on user-generated text (take, summary)
|
|
165
|
+
// skip for title/description if they came from fetched webpage metadata
|
|
166
|
+
const textsToCheck = [
|
|
167
|
+
regexResult.take,
|
|
168
|
+
regexResult.summary,
|
|
169
|
+
...(fields.skipLLMForMetadata ? [] : [regexResult.title, regexResult.description]),
|
|
170
|
+
].filter(Boolean).join('\n\n');
|
|
171
|
+
if (textsToCheck.length > 20) {
|
|
172
|
+
const llmResult = await llmContentFilter(textsToCheck);
|
|
173
|
+
if (!llmResult.safe) {
|
|
174
|
+
return {
|
|
175
|
+
...regexResult,
|
|
176
|
+
blocked: true,
|
|
177
|
+
blockReason: llmResult.reason || 'LLM filter flagged content as containing private data',
|
|
178
|
+
filterReport: [regexResult.filterReport, `LLM blocked: ${llmResult.reason}`].filter(Boolean).join('; '),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...regexResult,
|
|
184
|
+
blocked: false,
|
|
185
|
+
blockReason: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=content-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-filter.js","sourceRoot":"","sources":["../../src/lib/content-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AAElD,gCAAgC;AAChC,MAAM,YAAY,GAA8D;IAC9E,kBAAkB;IAClB,EAAE,OAAO,EAAE,iDAAiD,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE;IAC9G,6BAA6B;IAC7B,EAAE,OAAO,EAAE,oDAAoD,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE;IACjH,MAAM;IACN,EAAE,OAAO,EAAE,kCAAkC,EAAE,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE;IAC3F,8BAA8B;IAC9B,EAAE,OAAO,EAAE,6CAA6C,EAAE,WAAW,EAAE,gBAAgB,EAAE,KAAK,EAAE,aAAa,EAAE;IAC/G,sCAAsC;IACtC,EAAE,OAAO,EAAE,oLAAoL,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE;IACrP,uBAAuB;IACvB,EAAE,OAAO,EAAE,qDAAqD,EAAE,WAAW,EAAE,sBAAsB,EAAE,KAAK,EAAE,UAAU,EAAE;IAC1H,0BAA0B;IAC1B,EAAE,OAAO,EAAE,kEAAkE,EAAE,WAAW,EAAE,uBAAuB,EAAE,KAAK,EAAE,aAAa,EAAE;IAC3I,sCAAsC;IACtC,EAAE,OAAO,EAAE,4HAA4H,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE;CAC9L,CAAC;AAEF,sCAAsC;AACtC,MAAM,oBAAoB,GAA8D;IACtF,+DAA+D;IAC/D,EAAE,OAAO,EAAE,mCAAmC,EAAE,WAAW,EAAE,kBAAkB,EAAE,KAAK,EAAE,cAAc,EAAE;IACxG,wCAAwC;IACxC,EAAE,OAAO,EAAE,mHAAmH,EAAE,WAAW,EAAE,0BAA0B,EAAE,KAAK,EAAE,WAAW,EAAE;IAC7L,sBAAsB;IACtB,EAAE,OAAO,EAAE,kHAAkH,EAAE,WAAW,EAAE,yBAAyB,EAAE,KAAK,EAAE,UAAU,EAAE;CAC3L,CAAC;AAQF,MAAM,UAAU,SAAS,CAAC,IAA+B;IACvD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAEtE,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,YAAY,EAAE,GAAG,oBAAoB,CAAC,EAAE,CAAC;QACzF,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,OAAO,EAAE,CAAC;YACZ,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;YAC/C,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,QAAQ,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC;QAC7B,QAAQ;KACT,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAKlC;IAOC,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,aAAa,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,WAAW,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAEjD,MAAM,WAAW,GAAG;QAClB,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,GAAG,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC;QACnD,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/C,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC;KACrD,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,UAAU,CAAC,IAAI,IAAI,IAAI;QAC7B,OAAO,EAAE,aAAa,CAAC,IAAI,IAAI,IAAI;QACnC,KAAK,EAAE,WAAW,CAAC,IAAI,IAAI,IAAI;QAC/B,WAAW,EAAE,UAAU,CAAC,IAAI,IAAI,IAAI;QACpC,YAAY,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,uBAAuB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI;KAC9F,CAAC;AACJ,CAAC;AAsBD,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4JA6BkI,CAAC;AAE7J,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,IAAI,CAAC,cAAc,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC1D,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,4CAA4C,EAAE;YACzE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,eAAe,EAAE,UAAU,cAAc,EAAE;aAC5C;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,aAAa;gBACpB,QAAQ,EAAE;oBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE;oBAC9C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,IAAI,EAAE,EAAE;iBACzD;gBACD,WAAW,EAAE,CAAC;gBACd,UAAU,EAAE,GAAG;aAChB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,CAAC,sCAAsC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACvE,oDAAoD;YACpD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAC1D,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAS,CAAC;QAC1C,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAEjE,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAC1D,CAAC;QAED,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QACrD,CAAC;QAED,0DAA0D;QAC1D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;QACzD,YAAY;QACZ,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC1D,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAM5C;IASC,iBAAiB;IACjB,MAAM,WAAW,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAE/C,4DAA4D;IAC5D,wEAAwE;IACxE,MAAM,YAAY,GAAG;QACnB,WAAW,CAAC,IAAI;QAChB,WAAW,CAAC,OAAO;QACnB,GAAG,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;KACnF,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAE/B,IAAI,YAAY,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,YAAY,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;YACpB,OAAO;gBACL,GAAG,WAAW;gBACd,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,SAAS,CAAC,MAAM,IAAI,uDAAuD;gBACxF,YAAY,EAAE,CAAC,WAAW,CAAC,YAAY,EAAE,gBAAgB,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;aACxG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,WAAW;QACd,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context extraction pipeline for nod intros.
|
|
3
|
+
*
|
|
4
|
+
* Takes natural language from conversations and extracts structured
|
|
5
|
+
* profile data: needs, offers, projects, interests, expertise.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* 1. keyword-based (fast, no API call) — catches explicit signals
|
|
9
|
+
* 2. LLM-based (better, uses gpt-4o-mini) — understands context
|
|
10
|
+
*/
|
|
11
|
+
export interface ExtractedContext {
|
|
12
|
+
needs: {
|
|
13
|
+
description: string;
|
|
14
|
+
urgency: "low" | "medium" | "high";
|
|
15
|
+
category: string;
|
|
16
|
+
}[];
|
|
17
|
+
offers: {
|
|
18
|
+
description: string;
|
|
19
|
+
category: string;
|
|
20
|
+
}[];
|
|
21
|
+
projects: {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
stage: string;
|
|
25
|
+
}[];
|
|
26
|
+
interests: string[];
|
|
27
|
+
expertise: string[];
|
|
28
|
+
industries: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract context from text. Uses LLM when available, falls back to keywords.
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractContext(text: string, useLLM?: boolean): Promise<ExtractedContext>;
|
|
34
|
+
/**
|
|
35
|
+
* Check if text contains any signals worth extracting.
|
|
36
|
+
* Fast check to avoid unnecessary LLM calls.
|
|
37
|
+
*/
|
|
38
|
+
export declare function hasContextSignals(text: string): boolean;
|
|
39
|
+
//# sourceMappingURL=context-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-extractor.d.ts","sourceRoot":"","sources":["../../src/lib/context-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvF,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAiJD;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,OAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAKpG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASvD"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context extraction pipeline for nod intros.
|
|
3
|
+
*
|
|
4
|
+
* Takes natural language from conversations and extracts structured
|
|
5
|
+
* profile data: needs, offers, projects, interests, expertise.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* 1. keyword-based (fast, no API call) — catches explicit signals
|
|
9
|
+
* 2. LLM-based (better, uses gpt-4o-mini) — understands context
|
|
10
|
+
*/
|
|
11
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
12
|
+
// keyword patterns that signal needs
|
|
13
|
+
const NEED_PATTERNS = [
|
|
14
|
+
/i(?:'m| am) looking for (.+?)(?:\.|$)/gi,
|
|
15
|
+
/i need (?:help with |a |an )?(.+?)(?:\.|$)/gi,
|
|
16
|
+
/looking for (?:someone|a person|anybody) (?:who|that) (.+?)(?:\.|$)/gi,
|
|
17
|
+
/anyone know (?:a |an )?(.+?)(?:\?|$)/gi,
|
|
18
|
+
/i could use (.+?)(?:\.|$)/gi,
|
|
19
|
+
/hiring (?:a |an )?(.+?)(?:\.|$)/gi,
|
|
20
|
+
];
|
|
21
|
+
// keyword patterns that signal offers
|
|
22
|
+
const OFFER_PATTERNS = [
|
|
23
|
+
/i(?:'m| am) (?:really )?good at (.+?)(?:\.|$)/gi,
|
|
24
|
+
/i can help with (.+?)(?:\.|$)/gi,
|
|
25
|
+
/i(?:'ve| have) (?:years of )?experience (?:in |with )(.+?)(?:\.|$)/gi,
|
|
26
|
+
/my expertise is (?:in )?(.+?)(?:\.|$)/gi,
|
|
27
|
+
/i specialize in (.+?)(?:\.|$)/gi,
|
|
28
|
+
/i offer (.+?)(?:\.|$)/gi,
|
|
29
|
+
];
|
|
30
|
+
// keyword patterns that signal projects
|
|
31
|
+
const PROJECT_PATTERNS = [
|
|
32
|
+
/i(?:'m| am) (?:currently )?(?:building|working on|developing|creating) (.+?)(?:\.|$)/gi,
|
|
33
|
+
/i(?:'ve| have) been (?:building|working on) (.+?)(?:\.|$)/gi,
|
|
34
|
+
/my (?:current )?(?:project|startup|company|app|product) (?:is |called )?(.+?)(?:\.|$)/gi,
|
|
35
|
+
/just launched (.+?)(?:\.|$)/gi,
|
|
36
|
+
];
|
|
37
|
+
function extractKeyword(text) {
|
|
38
|
+
const result = {
|
|
39
|
+
needs: [],
|
|
40
|
+
offers: [],
|
|
41
|
+
projects: [],
|
|
42
|
+
interests: [],
|
|
43
|
+
expertise: [],
|
|
44
|
+
industries: [],
|
|
45
|
+
};
|
|
46
|
+
for (const pattern of NEED_PATTERNS) {
|
|
47
|
+
pattern.lastIndex = 0;
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
50
|
+
const desc = match[1].trim();
|
|
51
|
+
if (desc.length > 5 && desc.length < 200) {
|
|
52
|
+
result.needs.push({ description: desc, urgency: "medium", category: "general" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const pattern of OFFER_PATTERNS) {
|
|
57
|
+
pattern.lastIndex = 0;
|
|
58
|
+
let match;
|
|
59
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
60
|
+
const desc = match[1].trim();
|
|
61
|
+
if (desc.length > 3 && desc.length < 200) {
|
|
62
|
+
result.offers.push({ description: desc, category: "general" });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const pattern of PROJECT_PATTERNS) {
|
|
67
|
+
pattern.lastIndex = 0;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
70
|
+
const desc = match[1].trim();
|
|
71
|
+
if (desc.length > 3 && desc.length < 200) {
|
|
72
|
+
result.projects.push({ name: desc.split(/\s+/).slice(0, 4).join(" "), description: desc, stage: "building" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
async function extractLLM(text) {
|
|
79
|
+
if (!OPENAI_API_KEY)
|
|
80
|
+
return extractKeyword(text);
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
model: "gpt-4o-mini",
|
|
90
|
+
messages: [
|
|
91
|
+
{
|
|
92
|
+
role: "system",
|
|
93
|
+
content: `Extract structured profile data from conversation text for a professional networking tool. Return valid JSON only.
|
|
94
|
+
|
|
95
|
+
Extract:
|
|
96
|
+
- needs: things the person is looking for (skills, connections, advice, tools)
|
|
97
|
+
- offers: things they can provide (expertise, connections, resources)
|
|
98
|
+
- projects: what they're currently working on
|
|
99
|
+
- interests: topics they care about
|
|
100
|
+
- expertise: areas they're skilled in
|
|
101
|
+
- industries: sectors they work in
|
|
102
|
+
|
|
103
|
+
Rules:
|
|
104
|
+
- Only extract what's explicitly stated or strongly implied
|
|
105
|
+
- Keep descriptions concise (under 100 chars)
|
|
106
|
+
- Omit any field with no data (empty array)
|
|
107
|
+
- Never include names, emails, or identifying info
|
|
108
|
+
- If nothing relevant is found, return all empty arrays
|
|
109
|
+
|
|
110
|
+
JSON schema:
|
|
111
|
+
{
|
|
112
|
+
"needs": [{"description": string, "urgency": "low"|"medium"|"high", "category": string}],
|
|
113
|
+
"offers": [{"description": string, "category": string}],
|
|
114
|
+
"projects": [{"name": string, "description": string, "stage": "idea"|"building"|"launched"|"growing"}],
|
|
115
|
+
"interests": [string],
|
|
116
|
+
"expertise": [string],
|
|
117
|
+
"industries": [string]
|
|
118
|
+
}`,
|
|
119
|
+
},
|
|
120
|
+
{ role: "user", content: text },
|
|
121
|
+
],
|
|
122
|
+
temperature: 0,
|
|
123
|
+
max_tokens: 500,
|
|
124
|
+
response_format: { type: "json_object" },
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok)
|
|
128
|
+
return extractKeyword(text);
|
|
129
|
+
const data = (await response.json());
|
|
130
|
+
const content = data.choices?.[0]?.message?.content;
|
|
131
|
+
if (!content)
|
|
132
|
+
return extractKeyword(text);
|
|
133
|
+
const parsed = JSON.parse(content);
|
|
134
|
+
return {
|
|
135
|
+
needs: parsed.needs || [],
|
|
136
|
+
offers: parsed.offers || [],
|
|
137
|
+
projects: parsed.projects || [],
|
|
138
|
+
interests: parsed.interests || [],
|
|
139
|
+
expertise: parsed.expertise || [],
|
|
140
|
+
industries: parsed.industries || [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return extractKeyword(text);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Extract context from text. Uses LLM when available, falls back to keywords.
|
|
149
|
+
*/
|
|
150
|
+
export async function extractContext(text, useLLM = true) {
|
|
151
|
+
if (useLLM && OPENAI_API_KEY) {
|
|
152
|
+
return extractLLM(text);
|
|
153
|
+
}
|
|
154
|
+
return extractKeyword(text);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if text contains any signals worth extracting.
|
|
158
|
+
* Fast check to avoid unnecessary LLM calls.
|
|
159
|
+
*/
|
|
160
|
+
export function hasContextSignals(text) {
|
|
161
|
+
const lower = text.toLowerCase();
|
|
162
|
+
const signals = [
|
|
163
|
+
"looking for", "i need", "help with", "hiring",
|
|
164
|
+
"i'm building", "working on", "my project", "just launched",
|
|
165
|
+
"good at", "can help", "experience in", "specialize in",
|
|
166
|
+
"interested in", "my expertise",
|
|
167
|
+
];
|
|
168
|
+
return signals.some(s => lower.includes(s));
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=context-extractor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-extractor.js","sourceRoot":"","sources":["../../src/lib/context-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AAWlD,qCAAqC;AACrC,MAAM,aAAa,GAAG;IACpB,yCAAyC;IACzC,8CAA8C;IAC9C,uEAAuE;IACvE,wCAAwC;IACxC,6BAA6B;IAC7B,mCAAmC;CACpC,CAAC;AAEF,sCAAsC;AACtC,MAAM,cAAc,GAAG;IACrB,iDAAiD;IACjD,iCAAiC;IACjC,sEAAsE;IACtE,yCAAyC;IACzC,iCAAiC;IACjC,yBAAyB;CAC1B,CAAC;AAEF,wCAAwC;AACxC,MAAM,gBAAgB,GAAG;IACvB,wFAAwF;IACxF,6DAA6D;IAC7D,yFAAyF;IACzF,+BAA+B;CAChC,CAAC;AAEF,SAAS,cAAc,CAAC,IAAY;IAClC,MAAM,MAAM,GAAqB;QAC/B,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,EAAE;QACb,SAAS,EAAE,EAAE;QACb,UAAU,EAAE,EAAE;KACf,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;QACpC,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,IAAI,KAAK,CAAC;QACV,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;gBACzC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;YACnF,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;QACrC,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,IAAI,KAAK,CAAC;QACV,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;gBACzC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;QACvC,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,IAAI,KAAK,CAAC;QACV,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;gBACzC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;YAChH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,IAAY;IACpC,IAAI,CAAC,cAAc;QAAE,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAEjD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,4CAA4C,EAAE;YACzE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,cAAc,EAAE;aAC1C;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,aAAa;gBACpB,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,QAAQ;wBACd,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;EAyBnB;qBACS;oBACD,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;iBAChC;gBACD,WAAW,EAAE,CAAC;gBACd,UAAU,EAAE,GAAG;gBACf,eAAe,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;aACzC,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAQ,CAAC;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;QACpD,IAAI,CAAC,OAAO;YAAE,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;QAE1C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;YAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,EAAE;YAC/B,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;YACjC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;YACjC,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE;SACpC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,SAAkB,IAAI;IACvE,IAAI,MAAM,IAAI,cAAc,EAAE,CAAC;QAC7B,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG;QACd,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ;QAC9C,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe;QAC3D,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe;QACvD,eAAe,EAAE,cAAc;KAChC,CAAC;IACF,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nod intros match engine
|
|
3
|
+
*
|
|
4
|
+
* scores pairs of opted-in users based on:
|
|
5
|
+
* - need/offer alignment (40%) — does user A need what user B offers?
|
|
6
|
+
* - interest/expertise overlap (20%) — shared topics
|
|
7
|
+
* - industry overlap (20%) — same industries
|
|
8
|
+
* - recency (10%) — both recently active
|
|
9
|
+
* - history (10%) — haven't been matched before / past success
|
|
10
|
+
*
|
|
11
|
+
* runs as a callable function (triggered by cron or on profile update)
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Run the match engine.
|
|
15
|
+
* Finds all opted-in, non-paused profiles, scores all pairs,
|
|
16
|
+
* and creates match records for scores above threshold.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runMatchEngine(options?: {
|
|
19
|
+
minScore?: number;
|
|
20
|
+
maxMatchesPerUser?: number;
|
|
21
|
+
useLLMPitches?: boolean;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
matchesCreated: number;
|
|
24
|
+
profilesChecked: number;
|
|
25
|
+
errors: string[];
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Expire old matches that haven't been responded to.
|
|
29
|
+
*/
|
|
30
|
+
export declare function expireStaleMatches(): Promise<number>;
|
|
31
|
+
//# sourceMappingURL=match-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match-engine.d.ts","sourceRoot":"","sources":["../../src/lib/match-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAuOH;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GAAG,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA+IjF;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS1D"}
|