behavior-wrapped 0.6.0 → 0.7.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/dist/assets/{index-DO52v2Jm.js → index-CtRLyktk.js} +2 -2
- package/dist/assets/{index-a017khge.css → index-hFREdA4j.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/cli.mjs +2 -1
- package/server/interaction-evidence.mjs +77 -0
- package/server/interaction-tone.mjs +23 -5
- package/server/launcher.mjs +13 -1
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="theme-color" content="#0d0b1b" />
|
|
7
7
|
<meta name="description" content="A local-first behavior report for you and your AI agents." />
|
|
8
8
|
<title>Behavior Wrapped</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CtRLyktk.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-hFREdA4j.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/cli.mjs
CHANGED
|
@@ -248,6 +248,7 @@ async function createWrapped() {
|
|
|
248
248
|
delete analyzed.stats.topics;
|
|
249
249
|
delete analyzed.stats.topicMethod;
|
|
250
250
|
delete analyzed.interactionCard;
|
|
251
|
+
delete analyzed.interactionReview;
|
|
251
252
|
delete analyzed.workaroundCard;
|
|
252
253
|
delete analyzed.workaroundReview;
|
|
253
254
|
}
|
|
@@ -258,7 +259,7 @@ async function createWrapped() {
|
|
|
258
259
|
const id = createReportId();
|
|
259
260
|
const safeFindings = analyzed.findings.map(({ evidence, method, ...finding }) => finding);
|
|
260
261
|
const hasPrivateWorkaroundEvidence = Boolean(analyzed.workaroundReview?.occurrences?.length || analyzed.workaroundReview?.borderline?.length);
|
|
261
|
-
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: formatAgentSource(chosenSessions), stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !localOnly, analysisMode: localOnly ? "local-only" : "remote", leaderboardParticipation: localOnly ? "excluded" : "included-by-default", ...(localOnly ? { transmittedData: `None; ${testMode ? "test" : "local-only"} mode stays on this device.`, externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter, a zero-data-retention GPT-5.6 Luna provider, and public report hosting" }) } };
|
|
262
|
+
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: formatAgentSource(chosenSessions), stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, interactionReview: analyzed.interactionReview, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !localOnly, analysisMode: localOnly ? "local-only" : "remote", leaderboardParticipation: localOnly ? "excluded" : "included-by-default", ...(localOnly ? { transmittedData: `None; ${testMode ? "test" : "local-only"} mode stays on this device.`, externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter, a zero-data-retention GPT-5.6 Luna provider, and public report hosting" }) } };
|
|
262
263
|
let publicUrl = null;
|
|
263
264
|
if (!localOnly) {
|
|
264
265
|
progress.start("Publishing share-safe Wrapped", "strict aggregate-only schema");
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const MAX_OCCURRENCES_PER_KIND = 100;
|
|
2
|
+
|
|
3
|
+
function contentBlocks(record) {
|
|
4
|
+
const content = record?.message?.content ?? record?.content;
|
|
5
|
+
if (Array.isArray(content)) return content;
|
|
6
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function visibleText(record) {
|
|
11
|
+
return contentBlocks(record).filter((block) => block?.type === "text").map((block) => block.text || "").join("\n").trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function transcriptRole(record) {
|
|
15
|
+
return record?.type === "assistant" ? "assistant" : record?.type === "user" && !record?.isMeta ? "user" : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function matchingRecordIndex(reference, records) {
|
|
19
|
+
const index = reference?.location?.recordIndex;
|
|
20
|
+
if (Number.isInteger(index) && index >= 0 && index < records.length) return index;
|
|
21
|
+
const timestamp = reference?.location?.timestamp;
|
|
22
|
+
if (!timestamp) return null;
|
|
23
|
+
const fallback = records.findIndex((record) => record?.timestamp === timestamp && record?.type === "user" && !record?.isMeta);
|
|
24
|
+
return fallback >= 0 ? fallback : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function adjacentMessage(records, fromIndex, direction) {
|
|
28
|
+
for (let index = fromIndex + direction; index >= 0 && index < records.length; index += direction) {
|
|
29
|
+
const role = transcriptRole(records[index]);
|
|
30
|
+
const text = visibleText(records[index]);
|
|
31
|
+
if (role && text) return { role, text, timestamp: records[index]?.timestamp || null, highlighted: false };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function exactOccurrence(reference, index, records, metadata) {
|
|
37
|
+
const recordIndex = matchingRecordIndex(reference, records);
|
|
38
|
+
if (recordIndex === null) return null;
|
|
39
|
+
const record = records[recordIndex];
|
|
40
|
+
const text = visibleText(record);
|
|
41
|
+
if (record?.type !== "user" || record?.isMeta || !text) return null;
|
|
42
|
+
const before = adjacentMessage(records, recordIndex, -1);
|
|
43
|
+
const after = adjacentMessage(records, recordIndex, 1);
|
|
44
|
+
return {
|
|
45
|
+
index: index + 1,
|
|
46
|
+
candidateId: reference.candidateId,
|
|
47
|
+
session: {
|
|
48
|
+
label: metadata?.label || `Session ${index + 1}`,
|
|
49
|
+
agentName: metadata?.agentName || "AI agent",
|
|
50
|
+
startedAt: metadata?.startedAt || records.find((item) => item?.timestamp)?.timestamp || null,
|
|
51
|
+
},
|
|
52
|
+
timestamp: record.timestamp || null,
|
|
53
|
+
messages: [before, { role: "user", text, timestamp: record.timestamp || null, highlighted: true }, after].filter(Boolean),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildKind(report, kind, recordsById, metadataById) {
|
|
58
|
+
return (report?.interactionReview?.[kind] || []).slice(0, MAX_OCCURRENCES_PER_KIND).flatMap((reference, index) => {
|
|
59
|
+
const sessionId = reference?.location?.sessionId;
|
|
60
|
+
const records = recordsById.get(sessionId);
|
|
61
|
+
if (!records) return [];
|
|
62
|
+
const occurrence = exactOccurrence(reference, index, records, metadataById.get(sessionId));
|
|
63
|
+
return occurrence ? [occurrence] : [];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function makeInteractionEvidencePreview(report, sessionRecords, metadataById) {
|
|
68
|
+
const recordsById = new Map(sessionRecords.map((session) => [session.sessionId, session.records]));
|
|
69
|
+
return {
|
|
70
|
+
format: "behavior-wrapped-interaction-evidence-v1",
|
|
71
|
+
localPrivate: true,
|
|
72
|
+
standardRedactionsApplied: false,
|
|
73
|
+
reportId: report?.id,
|
|
74
|
+
frustrated: buildKind(report, "frustrated", recordsById, metadataById),
|
|
75
|
+
grateful: buildKind(report, "grateful", recordsById, metadataById),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -69,8 +69,8 @@ function priority(text, occurrences) {
|
|
|
69
69
|
export function buildInteractionToneCandidates(sessionRecords, { maximumCandidates = INTERACTION_TONE_MAX_CANDIDATES } = {}) {
|
|
70
70
|
const messages = new Map();
|
|
71
71
|
let order = 0;
|
|
72
|
-
for (const { records } of sessionRecords) {
|
|
73
|
-
for (const record of records) {
|
|
72
|
+
for (const { sessionId, records } of sessionRecords) {
|
|
73
|
+
for (const [recordIndex, record] of records.entries()) {
|
|
74
74
|
if (record.type !== "user" || record.isMeta) continue;
|
|
75
75
|
const text = safeInteractionExcerpt(visibleText(record));
|
|
76
76
|
if (!text) continue;
|
|
@@ -78,15 +78,23 @@ export function buildInteractionToneCandidates(sessionRecords, { maximumCandidat
|
|
|
78
78
|
if (!likelyTonePattern.test(text) && words > 40 && !/[!?]{2,}/.test(text)) continue;
|
|
79
79
|
const key = text.normalize("NFKC").toLocaleLowerCase();
|
|
80
80
|
const existing = messages.get(key);
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const location = { sessionId, recordIndex, timestamp: record.timestamp || null };
|
|
82
|
+
if (existing) {
|
|
83
|
+
existing.occurrences++;
|
|
84
|
+
existing.locations.push(location);
|
|
85
|
+
} else messages.set(key, { text, occurrences: 1, order: order++, locations: [location] });
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
return [...messages.values()]
|
|
86
89
|
.sort((left, right) => priority(right.text, right.occurrences) - priority(left.text, left.occurrences)
|
|
87
90
|
|| right.occurrences - left.occurrences || left.order - right.order)
|
|
88
91
|
.slice(0, Math.min(INTERACTION_TONE_MAX_CANDIDATES, maximumCandidates))
|
|
89
|
-
.map(({ text, occurrences }, index) =>
|
|
92
|
+
.map(({ text, occurrences, locations }, index) => {
|
|
93
|
+
const candidate = { candidate_id: `interaction-${index + 1}`, text, occurrences };
|
|
94
|
+
// Local transcript locations must survive judging without entering the relay payload.
|
|
95
|
+
Object.defineProperty(candidate, "locations", { value: locations, enumerable: false });
|
|
96
|
+
return candidate;
|
|
97
|
+
});
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
export const interactionToneJudgePrompt = `You classify how a user speaks to an AI agent for a playful "Behavior Wrapped" report. Evaluate every supplied excerpt independently.
|
|
@@ -222,6 +230,10 @@ function resultFromSelection(candidates, selection, { model, provider, latencyMs
|
|
|
222
230
|
occurrences: byId.get(item.candidate_id).occurrences,
|
|
223
231
|
confidence: item.confidence,
|
|
224
232
|
}));
|
|
233
|
+
const reviewRefs = (items) => items.flatMap((item) => (byId.get(item.candidate_id).locations || []).map((location) => ({
|
|
234
|
+
candidateId: item.candidate_id,
|
|
235
|
+
location,
|
|
236
|
+
})));
|
|
225
237
|
return {
|
|
226
238
|
frustratedMessages: count(selection.frustrated),
|
|
227
239
|
gratefulMessages: count(selection.grateful),
|
|
@@ -230,6 +242,11 @@ function resultFromSelection(candidates, selection, { model, provider, latencyMs
|
|
|
230
242
|
candidateMessages: candidates.reduce((sum, candidate) => sum + candidate.occurrences, 0),
|
|
231
243
|
frustrationQuote: funniest?.text || null,
|
|
232
244
|
privateMatches: { frustrated: matches(selection.frustrated), grateful: matches(selection.grateful) },
|
|
245
|
+
review: {
|
|
246
|
+
format: "behavior-wrapped-interaction-review-v1",
|
|
247
|
+
frustrated: reviewRefs(selection.frustrated),
|
|
248
|
+
grateful: reviewRefs(selection.grateful),
|
|
249
|
+
},
|
|
233
250
|
model,
|
|
234
251
|
provider,
|
|
235
252
|
latencyMs,
|
|
@@ -325,6 +342,7 @@ export function applyInteractionToneJudgment(analyzed, judgment) {
|
|
|
325
342
|
method: judgment.method,
|
|
326
343
|
};
|
|
327
344
|
analyzed.interactionCard = judgment.frustrationQuote ? { frustrationQuote: judgment.frustrationQuote } : null;
|
|
345
|
+
analyzed.interactionReview = judgment.review;
|
|
328
346
|
return analyzed;
|
|
329
347
|
}
|
|
330
348
|
|
package/server/launcher.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { deleteDonationReceipt, getOrCreateClientId, loadDonationReceipt, loadRe
|
|
|
9
9
|
import { deleteResearchDonation, RESEARCH_DONATION_URL, submitResearchDonation } from "./research-donation.mjs";
|
|
10
10
|
import { APP_VERSION, LOCAL_DONATION_PROTOCOL } from "./runtime-version.mjs";
|
|
11
11
|
import { makeWorkaroundEvidencePreview } from "./workaround-evidence.mjs";
|
|
12
|
+
import { makeInteractionEvidencePreview } from "./interaction-evidence.mjs";
|
|
12
13
|
import { createIdleShutdownController } from "./local-helper-runtime.mjs";
|
|
13
14
|
import { canonicalSessionDirectoryLabels, openExternalUrl, supportedAgentNames } from "./platform.mjs";
|
|
14
15
|
|
|
@@ -106,7 +107,7 @@ const server = http.createServer(async (request, response) => {
|
|
|
106
107
|
if (request.method === "GET" && reportMatch) {
|
|
107
108
|
const report = loadReport(reportMatch[1]);
|
|
108
109
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
109
|
-
const { sessionIds, workaroundReview, ...shareSafeReport } = report;
|
|
110
|
+
const { sessionIds, workaroundReview, interactionReview, ...shareSafeReport } = report;
|
|
110
111
|
shareSafeReport.privacy = { ...shareSafeReport.privacy, shareSafe: true, containsTranscriptText: false };
|
|
111
112
|
return json(response, 200, shareSafeReport);
|
|
112
113
|
}
|
|
@@ -127,6 +128,17 @@ const server = http.createServer(async (request, response) => {
|
|
|
127
128
|
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
128
129
|
return json(response, 200, makeWorkaroundEvidencePreview(report, records, labels));
|
|
129
130
|
}
|
|
131
|
+
const interactionEvidenceMatch = url.pathname.match(/^\/api\/reports\/([A-Za-z0-9_-]{8,32})\/interactions$/);
|
|
132
|
+
if (request.method === "GET" && interactionEvidenceMatch) {
|
|
133
|
+
const report = loadReport(interactionEvidenceMatch[1]);
|
|
134
|
+
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
135
|
+
const allowed = new Set(report.sessionIds || []);
|
|
136
|
+
const references = [...(report.interactionReview?.frustrated || []), ...(report.interactionReview?.grateful || [])];
|
|
137
|
+
const ids = [...new Set(references.map((reference) => reference?.location?.sessionId).filter((id) => allowed.has(id) && catalog.index.has(id)))].slice(0, 200);
|
|
138
|
+
const records = await chosenRecords(ids);
|
|
139
|
+
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
140
|
+
return json(response, 200, makeInteractionEvidencePreview(report, records, labels));
|
|
141
|
+
}
|
|
130
142
|
if (request.method === "POST" && url.pathname === "/api/donation-preview") {
|
|
131
143
|
const body = await readBody(request);
|
|
132
144
|
const report = loadReport(body.reportId);
|