@gonzih/meet-the-one-ai 1.0.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/.env.example +41 -0
- package/.node-version +1 -0
- package/basis/BERNAYS.md +233 -0
- package/basis/FOUNDING_TRANSCRIPT.md +218 -0
- package/basis/TECH_SPEC.md +303 -0
- package/basis/VALS.md +255 -0
- package/basis/layers/L1_IDENTITY_AUTH.md +78 -0
- package/basis/layers/L2_CONVERSATION.md +159 -0
- package/basis/layers/L3_RECORDING_STORE.md +104 -0
- package/basis/layers/L4_ANALYSIS_PIPELINE.md +257 -0
- package/basis/layers/L5_MATCHING_ENGINE.md +164 -0
- package/basis/layers/L6_CONSENT_INTRODUCTION.md +143 -0
- package/basis/layers/L7_PORTABLE_IDENTITY.md +139 -0
- package/basis/layers/STACK.md +64 -0
- package/basis/schema.sql +203 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +114 -0
- package/dist/agent.js.map +1 -0
- package/dist/api/routes/auth.d.ts +2 -0
- package/dist/api/routes/auth.d.ts.map +1 -0
- package/dist/api/routes/auth.js +79 -0
- package/dist/api/routes/auth.js.map +1 -0
- package/dist/api/routes/identity.d.ts +2 -0
- package/dist/api/routes/identity.d.ts.map +1 -0
- package/dist/api/routes/identity.js +92 -0
- package/dist/api/routes/identity.js.map +1 -0
- package/dist/api/routes/text-submission.d.ts +2 -0
- package/dist/api/routes/text-submission.d.ts.map +1 -0
- package/dist/api/routes/text-submission.js +56 -0
- package/dist/api/routes/text-submission.js.map +1 -0
- package/dist/api/webhooks/twilio.d.ts +2 -0
- package/dist/api/webhooks/twilio.d.ts.map +1 -0
- package/dist/api/webhooks/twilio.js +144 -0
- package/dist/api/webhooks/twilio.js.map +1 -0
- package/dist/api/webhooks/vapi.d.ts +2 -0
- package/dist/api/webhooks/vapi.d.ts.map +1 -0
- package/dist/api/webhooks/vapi.js +177 -0
- package/dist/api/webhooks/vapi.js.map +1 -0
- package/dist/bot.d.ts +3 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +39 -0
- package/dist/bot.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/compact-identity.d.ts +2 -0
- package/dist/jobs/compact-identity.d.ts.map +1 -0
- package/dist/jobs/compact-identity.js +159 -0
- package/dist/jobs/compact-identity.js.map +1 -0
- package/dist/jobs/consent-call.d.ts +2 -0
- package/dist/jobs/consent-call.d.ts.map +1 -0
- package/dist/jobs/consent-call.js +70 -0
- package/dist/jobs/consent-call.js.map +1 -0
- package/dist/jobs/export-identity.d.ts +2 -0
- package/dist/jobs/export-identity.d.ts.map +1 -0
- package/dist/jobs/export-identity.js +129 -0
- package/dist/jobs/export-identity.js.map +1 -0
- package/dist/jobs/introduction-call.d.ts +2 -0
- package/dist/jobs/introduction-call.d.ts.map +1 -0
- package/dist/jobs/introduction-call.js +86 -0
- package/dist/jobs/introduction-call.js.map +1 -0
- package/dist/jobs/reanalyze-identity.d.ts +2 -0
- package/dist/jobs/reanalyze-identity.d.ts.map +1 -0
- package/dist/jobs/reanalyze-identity.js +56 -0
- package/dist/jobs/reanalyze-identity.js.map +1 -0
- package/dist/jobs/run-matching.d.ts +2 -0
- package/dist/jobs/run-matching.d.ts.map +1 -0
- package/dist/jobs/run-matching.js +200 -0
- package/dist/jobs/run-matching.js.map +1 -0
- package/dist/jobs/scheduled-matching.d.ts +2 -0
- package/dist/jobs/scheduled-matching.d.ts.map +1 -0
- package/dist/jobs/scheduled-matching.js +44 -0
- package/dist/jobs/scheduled-matching.js.map +1 -0
- package/dist/jobs/transcribe-session.d.ts +2 -0
- package/dist/jobs/transcribe-session.d.ts.map +1 -0
- package/dist/jobs/transcribe-session.js +66 -0
- package/dist/jobs/transcribe-session.js.map +1 -0
- package/dist/lib/anthropic.d.ts +4 -0
- package/dist/lib/anthropic.d.ts.map +1 -0
- package/dist/lib/anthropic.js +32 -0
- package/dist/lib/anthropic.js.map +1 -0
- package/dist/lib/config.d.ts +57 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +73 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/deepgram.d.ts +15 -0
- package/dist/lib/deepgram.d.ts.map +1 -0
- package/dist/lib/deepgram.js +37 -0
- package/dist/lib/deepgram.js.map +1 -0
- package/dist/lib/inngest.d.ts +42 -0
- package/dist/lib/inngest.d.ts.map +1 -0
- package/dist/lib/inngest.js +7 -0
- package/dist/lib/inngest.js.map +1 -0
- package/dist/lib/openai.d.ts +3 -0
- package/dist/lib/openai.d.ts.map +1 -0
- package/dist/lib/openai.js +13 -0
- package/dist/lib/openai.js.map +1 -0
- package/dist/lib/prompts.d.ts +8 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +258 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/r2.d.ts +7 -0
- package/dist/lib/r2.d.ts.map +1 -0
- package/dist/lib/r2.js +49 -0
- package/dist/lib/r2.js.map +1 -0
- package/dist/lib/session-helpers.d.ts +8 -0
- package/dist/lib/session-helpers.d.ts.map +1 -0
- package/dist/lib/session-helpers.js +31 -0
- package/dist/lib/session-helpers.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 +11 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/lib/twilio.d.ts +7 -0
- package/dist/lib/twilio.d.ts.map +1 -0
- package/dist/lib/twilio.js +34 -0
- package/dist/lib/twilio.js.map +1 -0
- package/dist/lib/vapi.d.ts +4 -0
- package/dist/lib/vapi.d.ts.map +1 -0
- package/dist/lib/vapi.js +59 -0
- package/dist/lib/vapi.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +177 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/types/index.d.ts +104 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +28 -0
- package/railway.json +14 -0
- package/src/agent.ts +123 -0
- package/src/api/routes/auth.ts +95 -0
- package/src/api/routes/identity.ts +112 -0
- package/src/api/routes/text-submission.ts +64 -0
- package/src/api/webhooks/twilio.ts +181 -0
- package/src/api/webhooks/vapi.ts +219 -0
- package/src/bot.ts +44 -0
- package/src/index.ts +11 -0
- package/src/jobs/compact-identity.ts +211 -0
- package/src/jobs/consent-call.ts +87 -0
- package/src/jobs/export-identity.ts +166 -0
- package/src/jobs/introduction-call.ts +101 -0
- package/src/jobs/reanalyze-identity.ts +65 -0
- package/src/jobs/run-matching.ts +243 -0
- package/src/jobs/scheduled-matching.ts +59 -0
- package/src/jobs/transcribe-session.ts +77 -0
- package/src/lib/anthropic.ts +37 -0
- package/src/lib/config.ts +81 -0
- package/src/lib/deepgram.ts +57 -0
- package/src/lib/inngest.ts +33 -0
- package/src/lib/openai.ts +14 -0
- package/src/lib/prompts.ts +266 -0
- package/src/lib/r2.ts +79 -0
- package/src/lib/session-helpers.ts +37 -0
- package/src/lib/supabase.ts +15 -0
- package/src/lib/twilio.ts +49 -0
- package/src/lib/vapi.ts +80 -0
- package/src/mcp-server.ts +195 -0
- package/src/types/index.ts +146 -0
- package/supabase/.branches/_current_branch +1 -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/config.toml +384 -0
- package/supabase/migrations/20260303000000_initial_schema.sql +203 -0
- package/supabase/migrations/20260304000000_brand_consents.sql +13 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { inngest } from "../lib/inngest.js";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
import { complete, extract_json } from "../lib/anthropic.js";
|
|
4
|
+
import { embed } from "../lib/openai.js";
|
|
5
|
+
import { download_json, upload_json } from "../lib/r2.js";
|
|
6
|
+
import {
|
|
7
|
+
SYSTEM_SIGNAL_EXTRACTION,
|
|
8
|
+
SYSTEM_IDENTITY_MERGE,
|
|
9
|
+
SYSTEM_MODALITY_RENDER,
|
|
10
|
+
} from "../lib/prompts.js";
|
|
11
|
+
import type { BaseIdentityProfile, ModalityWeights, Modality } from "../types/index.js";
|
|
12
|
+
|
|
13
|
+
// Triggered after transcription completes (or after text submission)
|
|
14
|
+
// Pass 1: Extract signals from session
|
|
15
|
+
// Pass 2: Merge signals into compacted identity
|
|
16
|
+
// Pass 3: Render modality-specific portraits → embed per modality
|
|
17
|
+
export const compact_identity = inngest.createFunction(
|
|
18
|
+
{ id: "compact-identity", retries: 2 },
|
|
19
|
+
{ event: "session/transcribed" },
|
|
20
|
+
async ({ event, step }) => {
|
|
21
|
+
const { user_id, session_id, transcript_r2_key } = event.data;
|
|
22
|
+
|
|
23
|
+
await step.run("mark-processing", async () => {
|
|
24
|
+
await supabase
|
|
25
|
+
.from("sessions")
|
|
26
|
+
.update({ analysis_status: "processing" })
|
|
27
|
+
.eq("id", session_id);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Load transcript or raw text
|
|
31
|
+
const user_transcript = await step.run("load-transcript", async () => {
|
|
32
|
+
if (transcript_r2_key) {
|
|
33
|
+
const data = await download_json<{ text: string }>(transcript_r2_key);
|
|
34
|
+
return data.text;
|
|
35
|
+
}
|
|
36
|
+
const { data, error } = await supabase
|
|
37
|
+
.from("sessions")
|
|
38
|
+
.select("raw_text")
|
|
39
|
+
.eq("id", session_id)
|
|
40
|
+
.single();
|
|
41
|
+
if (error || !data?.raw_text) throw new Error("No transcript or raw_text found");
|
|
42
|
+
return data.raw_text;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const session_meta = await step.run("load-session-meta", async () => {
|
|
46
|
+
const { data, error } = await supabase
|
|
47
|
+
.from("sessions")
|
|
48
|
+
.select("time_of_day, duration_seconds, source")
|
|
49
|
+
.eq("id", session_id)
|
|
50
|
+
.single();
|
|
51
|
+
if (error || !data) throw new Error("Session not found");
|
|
52
|
+
return data;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { count: completed_count } = await supabase
|
|
56
|
+
.from("sessions")
|
|
57
|
+
.select("*", { count: "exact", head: true })
|
|
58
|
+
.eq("user_id", user_id)
|
|
59
|
+
.eq("analysis_status", "complete");
|
|
60
|
+
|
|
61
|
+
const session_number = (completed_count ?? 0) + 1;
|
|
62
|
+
|
|
63
|
+
// ── Pass 1: Signal extraction ─────────────────────────────────────────────
|
|
64
|
+
const session_signals_raw = await step.run("extract-signals", async () => {
|
|
65
|
+
const is_late_night = session_meta.time_of_day === "late_night";
|
|
66
|
+
return complete(
|
|
67
|
+
"analysis",
|
|
68
|
+
SYSTEM_SIGNAL_EXTRACTION,
|
|
69
|
+
[
|
|
70
|
+
`Session #${session_number} (source: ${session_meta.source})`,
|
|
71
|
+
`Time: ${session_meta.time_of_day}${is_late_night ? " [HIGH SIGNAL — 4AM PRINCIPLE]" : ""}`,
|
|
72
|
+
`Duration: ${session_meta.duration_seconds ?? "unknown"}s`,
|
|
73
|
+
`\nTranscript:\n${user_transcript}`,
|
|
74
|
+
].join("\n")
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const existing_identity = await step.run("load-identity", async () => {
|
|
79
|
+
const { data } = await supabase
|
|
80
|
+
.from("identities")
|
|
81
|
+
.select("base_profile, modality_weights, session_count")
|
|
82
|
+
.eq("user_id", user_id)
|
|
83
|
+
.single();
|
|
84
|
+
return data;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── Pass 2: Identity merge ────────────────────────────────────────────────
|
|
88
|
+
const merged_raw = await step.run("merge-identity", async () => {
|
|
89
|
+
const existing_str = existing_identity
|
|
90
|
+
? JSON.stringify(existing_identity.base_profile, null, 2)
|
|
91
|
+
: "null (first session)";
|
|
92
|
+
|
|
93
|
+
return complete(
|
|
94
|
+
"analysis",
|
|
95
|
+
SYSTEM_IDENTITY_MERGE,
|
|
96
|
+
[
|
|
97
|
+
`Existing identity:\n${existing_str}`,
|
|
98
|
+
`\nNew session signals:\n${session_signals_raw}`,
|
|
99
|
+
`\nSession metadata: ${JSON.stringify(session_meta)}`,
|
|
100
|
+
`\nTotal sessions completed: ${session_number}`,
|
|
101
|
+
].join("\n\n")
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const { base_profile, modality_weights } = await step.run(
|
|
106
|
+
"parse-output",
|
|
107
|
+
async () => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(extract_json(merged_raw)) as {
|
|
110
|
+
base_profile: BaseIdentityProfile;
|
|
111
|
+
modality_weights: ModalityWeights;
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error(`LLM returned invalid JSON for identity merge: ${merged_raw.slice(0, 200)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const completeness = compute_completeness(base_profile);
|
|
120
|
+
const ready =
|
|
121
|
+
completeness >= 0.5 &&
|
|
122
|
+
base_profile.worldview.world_danger_adventure !== undefined;
|
|
123
|
+
|
|
124
|
+
const new_session_count = (existing_identity?.session_count ?? 0) + 1;
|
|
125
|
+
|
|
126
|
+
// ── Pass 3: Modality-specific portraits + embeddings ──────────────────────
|
|
127
|
+
const active_modalities = Object.entries(modality_weights)
|
|
128
|
+
.filter(([, w]) => (w as number) > 0.1)
|
|
129
|
+
.map(([m]) => m as Modality);
|
|
130
|
+
|
|
131
|
+
await step.run("upsert-modality-embeddings", async () => {
|
|
132
|
+
for (const modality of active_modalities) {
|
|
133
|
+
// Render a focused portrait through this modality's lens
|
|
134
|
+
const portrait = await complete(
|
|
135
|
+
"analysis",
|
|
136
|
+
SYSTEM_MODALITY_RENDER,
|
|
137
|
+
[
|
|
138
|
+
`Target modality: ${modality}`,
|
|
139
|
+
`\nFull identity profile:\n${JSON.stringify(base_profile, null, 2)}`,
|
|
140
|
+
`\nModality weights: ${JSON.stringify(modality_weights)}`,
|
|
141
|
+
].join("\n\n"),
|
|
142
|
+
512 // short output — dense paragraph only
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const vec = await embed(portrait);
|
|
146
|
+
|
|
147
|
+
await supabase.rpc("upsert_modality_embedding", {
|
|
148
|
+
p_user_id: user_id,
|
|
149
|
+
p_modality: modality,
|
|
150
|
+
p_embedding: vec,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── Save identity ─────────────────────────────────────────────────────────
|
|
156
|
+
await step.run("save-identity", async () => {
|
|
157
|
+
await supabase.from("identities").upsert(
|
|
158
|
+
{
|
|
159
|
+
user_id,
|
|
160
|
+
base_profile,
|
|
161
|
+
modality_weights,
|
|
162
|
+
signal_completeness_score: completeness,
|
|
163
|
+
session_count: new_session_count,
|
|
164
|
+
ready_for_matching: ready,
|
|
165
|
+
last_updated: new Date().toISOString(),
|
|
166
|
+
},
|
|
167
|
+
{ onConflict: "user_id" }
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Audit trail to R2
|
|
172
|
+
await step.run("archive-signals", async () => {
|
|
173
|
+
await upload_json(`signals/${user_id}/${session_id}.json`, {
|
|
174
|
+
session_id,
|
|
175
|
+
signals_raw: session_signals_raw,
|
|
176
|
+
base_profile,
|
|
177
|
+
modality_weights,
|
|
178
|
+
completeness,
|
|
179
|
+
merged_at: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await step.run("mark-complete", async () => {
|
|
184
|
+
await supabase
|
|
185
|
+
.from("sessions")
|
|
186
|
+
.update({ analysis_status: "complete" })
|
|
187
|
+
.eq("id", session_id);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await step.sendEvent("trigger-matching", {
|
|
191
|
+
name: "identity/updated",
|
|
192
|
+
data: { user_id },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return { user_id, completeness, ready_for_matching: ready, session_count: new_session_count };
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
function compute_completeness(profile: BaseIdentityProfile): number {
|
|
200
|
+
const domain_scores = [
|
|
201
|
+
profile.relationships.confidence,
|
|
202
|
+
profile.desire.confidence,
|
|
203
|
+
profile.money.confidence,
|
|
204
|
+
profile.health.confidence,
|
|
205
|
+
];
|
|
206
|
+
const domain_avg =
|
|
207
|
+
domain_scores.reduce((a, b) => a + b, 0) / domain_scores.length;
|
|
208
|
+
const has_worldview =
|
|
209
|
+
profile.worldview.world_danger_adventure !== undefined ? 1 : 0;
|
|
210
|
+
return domain_avg * 0.7 + has_worldview * 0.3;
|
|
211
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { inngest } from "../lib/inngest.js";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
import { trigger_consent_call } from "../lib/vapi.js";
|
|
4
|
+
import { send_sms } from "../lib/twilio.js";
|
|
5
|
+
|
|
6
|
+
// Triggered when a match is confirmed by the matching engine
|
|
7
|
+
// Calls both users via Vapi consent assistant — sequentially with a gap
|
|
8
|
+
// If Vapi call fails, falls back to SMS consent
|
|
9
|
+
export const consent_call = inngest.createFunction(
|
|
10
|
+
{ id: "consent-call", retries: 2 },
|
|
11
|
+
{ event: "match/confirmed" },
|
|
12
|
+
async ({ event, step }) => {
|
|
13
|
+
const { match_id } = event.data;
|
|
14
|
+
|
|
15
|
+
// Load match + both users' phones
|
|
16
|
+
const match = await step.run("load-match", async () => {
|
|
17
|
+
const { data, error } = await supabase
|
|
18
|
+
.from("matches")
|
|
19
|
+
.select(`
|
|
20
|
+
id,
|
|
21
|
+
user_a_id,
|
|
22
|
+
user_b_id,
|
|
23
|
+
primary_modality,
|
|
24
|
+
confidence_score,
|
|
25
|
+
consent_call_framing,
|
|
26
|
+
status
|
|
27
|
+
`)
|
|
28
|
+
.eq("id", match_id)
|
|
29
|
+
.single();
|
|
30
|
+
if (error || !data) throw new Error(`Match not found: ${match_id}`);
|
|
31
|
+
return data;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Only proceed if still pending consent
|
|
35
|
+
if (match.status !== "pending_consent") {
|
|
36
|
+
return { skipped: true, reason: `match status is ${match.status}` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const users = await step.run("load-users", async () => {
|
|
40
|
+
const { data, error } = await supabase
|
|
41
|
+
.from("users")
|
|
42
|
+
.select("id, phone, brand")
|
|
43
|
+
.in("id", [match.user_a_id, match.user_b_id]);
|
|
44
|
+
if (error || !data || data.length !== 2) {
|
|
45
|
+
throw new Error("Failed to load both users for consent call");
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const user_a = users.find((u) => u.id === match.user_a_id)!;
|
|
51
|
+
const user_b = users.find((u) => u.id === match.user_b_id)!;
|
|
52
|
+
const framing = match.consent_call_framing ?? "";
|
|
53
|
+
|
|
54
|
+
// ── Call user A ───────────────────────────────────────────────────────────
|
|
55
|
+
await step.run("call-user-a", async () => {
|
|
56
|
+
try {
|
|
57
|
+
await trigger_consent_call(user_a.phone, match_id, framing);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("Vapi consent call failed for user_a, falling back to SMS", err);
|
|
60
|
+
await send_sms(
|
|
61
|
+
user_a.phone,
|
|
62
|
+
`We found someone interesting for you. Reply ACCEPT to connect or DECLINE to pass. [match:${match_id}]`,
|
|
63
|
+
user_a.brand as Parameters<typeof send_sms>[2]
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Small delay between calls so they don't land simultaneously
|
|
69
|
+
await step.sleep("gap-between-calls", "2m");
|
|
70
|
+
|
|
71
|
+
// ── Call user B ───────────────────────────────────────────────────────────
|
|
72
|
+
await step.run("call-user-b", async () => {
|
|
73
|
+
try {
|
|
74
|
+
await trigger_consent_call(user_b.phone, match_id, framing);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Vapi consent call failed for user_b, falling back to SMS", err);
|
|
77
|
+
await send_sms(
|
|
78
|
+
user_b.phone,
|
|
79
|
+
`We found someone interesting for you. Reply ACCEPT to connect or DECLINE to pass. [match:${match_id}]`,
|
|
80
|
+
user_b.brand as Parameters<typeof send_sms>[2]
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { match_id, called: [user_a.id, user_b.id] };
|
|
86
|
+
}
|
|
87
|
+
);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import archiver from "archiver";
|
|
2
|
+
import { PassThrough } from "stream";
|
|
3
|
+
import { inngest } from "../lib/inngest.js";
|
|
4
|
+
import { supabase } from "../lib/supabase.js";
|
|
5
|
+
import { complete } from "../lib/anthropic.js";
|
|
6
|
+
import { upload_buffer, generate_presigned_get_url } from "../lib/r2.js";
|
|
7
|
+
import { send_sms } from "../lib/twilio.js";
|
|
8
|
+
import { SYSTEM_IDENTITY_NARRATIVE } from "../lib/prompts.js";
|
|
9
|
+
import type { BaseIdentityProfile, ModalityWeights, Brand } from "../types/index.js";
|
|
10
|
+
|
|
11
|
+
// Triggered by identity/export event (SMS "EXPORT" or web portal)
|
|
12
|
+
// Renders 7 markdown files → zips → uploads to R2 → sends SMS download link
|
|
13
|
+
export const export_identity = inngest.createFunction(
|
|
14
|
+
{ id: "export-identity", retries: 1 },
|
|
15
|
+
{ event: "identity/export" },
|
|
16
|
+
async ({ event, step }) => {
|
|
17
|
+
const { user_id } = event.data;
|
|
18
|
+
|
|
19
|
+
// Load identity + user
|
|
20
|
+
const { identity, user } = await step.run("load-data", async () => {
|
|
21
|
+
const [id_res, user_res] = await Promise.all([
|
|
22
|
+
supabase
|
|
23
|
+
.from("identities")
|
|
24
|
+
.select("base_profile, modality_weights, signal_completeness_score, session_count")
|
|
25
|
+
.eq("user_id", user_id)
|
|
26
|
+
.single(),
|
|
27
|
+
supabase
|
|
28
|
+
.from("users")
|
|
29
|
+
.select("id, phone, brand")
|
|
30
|
+
.eq("id", user_id)
|
|
31
|
+
.single(),
|
|
32
|
+
]);
|
|
33
|
+
if (id_res.error || !id_res.data) throw new Error("Identity not found");
|
|
34
|
+
if (user_res.error || !user_res.data) throw new Error("User not found");
|
|
35
|
+
return { identity: id_res.data, user: user_res.data };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const profile = identity.base_profile as BaseIdentityProfile;
|
|
39
|
+
const weights = identity.modality_weights as ModalityWeights;
|
|
40
|
+
|
|
41
|
+
// ── Render all narrative files in parallel ────────────────────────────────
|
|
42
|
+
const files = await step.run("render-narratives", async () => {
|
|
43
|
+
const domains = [
|
|
44
|
+
{ name: "relationships", data: profile.relationships },
|
|
45
|
+
{ name: "desire", data: profile.desire },
|
|
46
|
+
{ name: "money", data: profile.money },
|
|
47
|
+
{ name: "health", data: profile.health },
|
|
48
|
+
{ name: "worldview", data: profile.worldview },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const rendered = await Promise.all(
|
|
52
|
+
domains.map(async ({ name, data }) => {
|
|
53
|
+
const content = await complete(
|
|
54
|
+
"analysis",
|
|
55
|
+
SYSTEM_IDENTITY_NARRATIVE,
|
|
56
|
+
[
|
|
57
|
+
`Domain: ${name}`,
|
|
58
|
+
`\nProfile data:\n${JSON.stringify(data, null, 2)}`,
|
|
59
|
+
`\nFull context (other domains):\n${JSON.stringify(profile, null, 2)}`,
|
|
60
|
+
].join("\n\n"),
|
|
61
|
+
1024
|
|
62
|
+
);
|
|
63
|
+
return { filename: `${name}.md`, content: `# My ${capitalize(name)} Identity\n\n${content}` };
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Modality weights file
|
|
68
|
+
const modality_content = await complete(
|
|
69
|
+
"analysis",
|
|
70
|
+
SYSTEM_IDENTITY_NARRATIVE,
|
|
71
|
+
[
|
|
72
|
+
`Domain: modality_weights`,
|
|
73
|
+
`\nModality weights:\n${JSON.stringify(weights, null, 2)}`,
|
|
74
|
+
`\nFull profile context:\n${JSON.stringify(profile, null, 2)}`,
|
|
75
|
+
].join("\n\n"),
|
|
76
|
+
512
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
rendered.push({
|
|
80
|
+
filename: "modality_weights.md",
|
|
81
|
+
content: `# How I Approach Connection\n\n${modality_content}`,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// README
|
|
85
|
+
const completeness_pct = Math.round((identity.signal_completeness_score ?? 0) * 100);
|
|
86
|
+
rendered.push({
|
|
87
|
+
filename: "README.md",
|
|
88
|
+
content: [
|
|
89
|
+
`# My Identity — meet-the-one.ai`,
|
|
90
|
+
``,
|
|
91
|
+
`Generated: ${new Date().toISOString().slice(0, 10)}`,
|
|
92
|
+
`Sessions: ${identity.session_count ?? 0}`,
|
|
93
|
+
`Completeness: ${completeness_pct}%`,
|
|
94
|
+
``,
|
|
95
|
+
`## What This Is`,
|
|
96
|
+
``,
|
|
97
|
+
`This is your portable identity — built from your own words across ${identity.session_count ?? 0} conversation sessions.`,
|
|
98
|
+
`It belongs to you. You can read it, keep it, share it, or import it into compatible platforms.`,
|
|
99
|
+
``,
|
|
100
|
+
`## Files`,
|
|
101
|
+
``,
|
|
102
|
+
`- \`relationships.md\` — how you attach, trust, and connect`,
|
|
103
|
+
`- \`desire.md\` — what you want and how you want it`,
|
|
104
|
+
`- \`money.md\` — your relationship with resources and risk`,
|
|
105
|
+
`- \`health.md\` — how you inhabit your body and energy`,
|
|
106
|
+
`- \`worldview.md\` — how you see the world and people in it`,
|
|
107
|
+
`- \`modality_weights.md\` — how you orient toward different kinds of connection`,
|
|
108
|
+
``,
|
|
109
|
+
`## Privacy`,
|
|
110
|
+
``,
|
|
111
|
+
`To delete all your data: text DELETE MY DATA to the number you enrolled with.`,
|
|
112
|
+
``,
|
|
113
|
+
`---`,
|
|
114
|
+
`meet-the-one.ai`,
|
|
115
|
+
].join("\n"),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return rendered;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── Zip all files + upload to R2 (single step — Buffer can't cross step boundary) ──
|
|
122
|
+
const export_key = `exports/${user_id}/${Date.now()}.zip`;
|
|
123
|
+
await step.run("zip-and-upload", async () => {
|
|
124
|
+
const zip_buf = await new Promise<Buffer>((resolve, reject) => {
|
|
125
|
+
const archive = archiver("zip", { zlib: { level: 6 } });
|
|
126
|
+
const pass = new PassThrough();
|
|
127
|
+
const chunks: Buffer[] = [];
|
|
128
|
+
|
|
129
|
+
pass.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
130
|
+
pass.on("end", () => resolve(Buffer.concat(chunks)));
|
|
131
|
+
pass.on("error", reject);
|
|
132
|
+
archive.on("error", reject);
|
|
133
|
+
|
|
134
|
+
archive.pipe(pass);
|
|
135
|
+
|
|
136
|
+
for (const { filename, content } of files) {
|
|
137
|
+
archive.append(content, { name: `my-identity/${filename}` });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
archive.finalize();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await upload_buffer(export_key, zip_buf, "application/zip");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── Generate presigned download URL (24h) ─────────────────────────────────
|
|
147
|
+
const download_url = await step.run("presign-url", async () => {
|
|
148
|
+
return generate_presigned_get_url(export_key, 86400);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── Send SMS with download link ───────────────────────────────────────────
|
|
152
|
+
await step.run("send-sms", async () => {
|
|
153
|
+
await send_sms(
|
|
154
|
+
user.phone,
|
|
155
|
+
`Your identity export is ready. Download it here (link expires in 24h):\n${download_url}`,
|
|
156
|
+
user.brand as Brand
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return { user_id, export_key, files: files.length };
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
function capitalize(s: string): string {
|
|
165
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
166
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { inngest } from "../lib/inngest.js";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
import { twilio } from "../lib/twilio.js";
|
|
4
|
+
import { send_sms } from "../lib/twilio.js";
|
|
5
|
+
import { config } from "../lib/config.js";
|
|
6
|
+
|
|
7
|
+
// Triggered when both users have accepted the match
|
|
8
|
+
// Creates a Twilio conference and calls both in
|
|
9
|
+
export const introduction_call = inngest.createFunction(
|
|
10
|
+
{ id: "introduction-call", retries: 2 },
|
|
11
|
+
{ event: "match/both-accepted" },
|
|
12
|
+
async ({ event, step }) => {
|
|
13
|
+
const { match_id } = event.data;
|
|
14
|
+
|
|
15
|
+
const match = await step.run("load-match", async () => {
|
|
16
|
+
const { data, error } = await supabase
|
|
17
|
+
.from("matches")
|
|
18
|
+
.select("id, user_a_id, user_b_id, status")
|
|
19
|
+
.eq("id", match_id)
|
|
20
|
+
.single();
|
|
21
|
+
if (error || !data) throw new Error(`Match not found: ${match_id}`);
|
|
22
|
+
return data;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (match.status !== "both_accepted") {
|
|
26
|
+
return { skipped: true, reason: `match status is ${match.status}` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const users = await step.run("load-users", async () => {
|
|
30
|
+
const { data, error } = await supabase
|
|
31
|
+
.from("users")
|
|
32
|
+
.select("id, phone, brand")
|
|
33
|
+
.in("id", [match.user_a_id, match.user_b_id]);
|
|
34
|
+
if (error || !data || data.length !== 2) {
|
|
35
|
+
throw new Error("Failed to load users for introduction call");
|
|
36
|
+
}
|
|
37
|
+
return data;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const user_a = users.find((u) => u.id === match.user_a_id)!;
|
|
41
|
+
const user_b = users.find((u) => u.id === match.user_b_id)!;
|
|
42
|
+
|
|
43
|
+
// Conference room name is deterministic from match_id
|
|
44
|
+
const conference_name = `intro-${match_id}`;
|
|
45
|
+
const from_number = config.twilio.numbers["meet-the-one"];
|
|
46
|
+
|
|
47
|
+
// TwiML to join a named conference
|
|
48
|
+
const conference_twiml = (participant_name: string) =>
|
|
49
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
50
|
+
<Response>
|
|
51
|
+
<Say voice="Polly.Joanna">Please hold while we connect you with your match.</Say>
|
|
52
|
+
<Dial>
|
|
53
|
+
<Conference
|
|
54
|
+
startConferenceOnEnter="false"
|
|
55
|
+
endConferenceOnExit="true"
|
|
56
|
+
waitUrl="https://twimlets.com/holdmusic?Bucket=com.twilio.music.soft-rock"
|
|
57
|
+
statusCallback="${config.app.url}/api/webhooks/twilio/conference"
|
|
58
|
+
statusCallbackEvent="end"
|
|
59
|
+
friendlyName="${participant_name}"
|
|
60
|
+
>${conference_name}</Conference>
|
|
61
|
+
</Dial>
|
|
62
|
+
</Response>`;
|
|
63
|
+
|
|
64
|
+
// Call both users into the conference
|
|
65
|
+
await step.run("dial-both", async () => {
|
|
66
|
+
await Promise.all([
|
|
67
|
+
twilio.calls.create({
|
|
68
|
+
to: user_a.phone,
|
|
69
|
+
from: from_number,
|
|
70
|
+
twiml: conference_twiml("You"),
|
|
71
|
+
}),
|
|
72
|
+
twilio.calls.create({
|
|
73
|
+
to: user_b.phone,
|
|
74
|
+
from: from_number,
|
|
75
|
+
twiml: conference_twiml("You"),
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Update match status
|
|
81
|
+
await step.run("mark-introduced", async () => {
|
|
82
|
+
await supabase
|
|
83
|
+
.from("matches")
|
|
84
|
+
.update({ status: "introduced", updated_at: new Date().toISOString() })
|
|
85
|
+
.eq("id", match_id);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Send follow-up SMS to both after the call
|
|
89
|
+
await step.sleep("post-call-delay", "30m");
|
|
90
|
+
|
|
91
|
+
await step.run("send-followup-sms", async () => {
|
|
92
|
+
const msg = "Hope that went well. Let us know how it went — your feedback helps us improve.";
|
|
93
|
+
await Promise.all([
|
|
94
|
+
send_sms(user_a.phone, msg, user_a.brand as Parameters<typeof send_sms>[2]),
|
|
95
|
+
send_sms(user_b.phone, msg, user_b.brand as Parameters<typeof send_sms>[2]),
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { match_id, introduced: [user_a.id, user_b.id] };
|
|
100
|
+
}
|
|
101
|
+
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { inngest } from "../lib/inngest.js";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
|
|
4
|
+
// Triggered manually or after prompt changes
|
|
5
|
+
// Replays all completed sessions through the current prompt version
|
|
6
|
+
// by re-firing session/transcribed for each session in order
|
|
7
|
+
export const reanalyze_identity = inngest.createFunction(
|
|
8
|
+
{ id: "reanalyze-identity", retries: 1 },
|
|
9
|
+
{ event: "identity/reanalyze" },
|
|
10
|
+
async ({ event, step }) => {
|
|
11
|
+
const { user_id } = event.data;
|
|
12
|
+
|
|
13
|
+
// Load all completed sessions in chronological order
|
|
14
|
+
const sessions = await step.run("load-sessions", async () => {
|
|
15
|
+
const { data, error } = await supabase
|
|
16
|
+
.from("sessions")
|
|
17
|
+
.select("id, transcript_r2_key, raw_text, analysis_status")
|
|
18
|
+
.eq("user_id", user_id)
|
|
19
|
+
.in("analysis_status", ["complete", "error"])
|
|
20
|
+
.order("created_at", { ascending: true });
|
|
21
|
+
if (error) throw new Error(`Failed to load sessions: ${error.message}`);
|
|
22
|
+
return data ?? [];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (sessions.length === 0) {
|
|
26
|
+
return { skipped: true, reason: "no completed sessions" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Reset identity
|
|
30
|
+
await step.run("reset-identity", async () => {
|
|
31
|
+
await supabase
|
|
32
|
+
.from("identities")
|
|
33
|
+
.update({
|
|
34
|
+
base_profile: {},
|
|
35
|
+
modality_weights: {},
|
|
36
|
+
signal_completeness_score: 0,
|
|
37
|
+
session_count: 0,
|
|
38
|
+
ready_for_matching: false,
|
|
39
|
+
last_updated: new Date().toISOString(),
|
|
40
|
+
})
|
|
41
|
+
.eq("user_id", user_id);
|
|
42
|
+
|
|
43
|
+
// Clear all modality embeddings
|
|
44
|
+
await supabase
|
|
45
|
+
.from("identity_modality_embeddings")
|
|
46
|
+
.delete()
|
|
47
|
+
.eq("user_id", user_id);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Re-fire each session through compact-identity sequentially
|
|
51
|
+
// Inngest will process them in order via event fan-out
|
|
52
|
+
for (const session of sessions) {
|
|
53
|
+
await step.sendEvent(`replay-${session.id}`, {
|
|
54
|
+
name: "session/transcribed",
|
|
55
|
+
data: {
|
|
56
|
+
user_id,
|
|
57
|
+
session_id: session.id,
|
|
58
|
+
transcript_r2_key: session.transcript_r2_key ?? "",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { user_id, replayed: sessions.length };
|
|
64
|
+
}
|
|
65
|
+
);
|