mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Phase 1 (data collection & standardization) — drive the memory layer with
|
|
// emails fetched from Composio's GMAIL_FETCH_EMAILS action.
|
|
//
|
|
// Inputs:
|
|
// - A JSON file with the slim post-processed shape produced by
|
|
// `src/openhuman/composio/providers/gmail/post_process.rs`. Each entry
|
|
// under `messages[]` looks like:
|
|
// { id, threadId, subject, from, to, date, labels, markdown, attachments }
|
|
// Default fixture: tests/fixtures/memory/composio_gmail_inbox.json
|
|
//
|
|
// Behaviour:
|
|
// - Groups messages by `threadId` so a single ingest call covers a whole
|
|
// email thread (this is what the canonicaliser expects — one
|
|
// EmailThread per source_id).
|
|
// - For each thread calls `openhuman.memory_tree_ingest` with
|
|
// source_kind=email + an EmailThread payload (see
|
|
// src/openhuman/memory/tree/canonicalize/email.rs).
|
|
// - Verifies via `openhuman.memory_tree_list_chunks` that chunks landed.
|
|
//
|
|
// Pre-reqs: the core server must already be serving JSON-RPC on $RPC_URL
|
|
// (default http://127.0.0.1:7810/rpc). Start it with:
|
|
//
|
|
// cargo run --bin openhuman -- serve
|
|
//
|
|
// Usage:
|
|
// node scripts/test-memory-email-ingest.mjs [path/to/inbox.json]
|
|
//
|
|
// Env:
|
|
// RPC_URL override the JSON-RPC endpoint
|
|
// OWNER owner string stamped on every chunk (default: stevent95@gmail.com)
|
|
// PROVIDER provider tag emitted in EmailThread.provider (default: gmail)
|
|
|
|
import { readFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
import process from "node:process";
|
|
|
|
const RPC_URL = process.env.RPC_URL || "http://127.0.0.1:7810/rpc";
|
|
const OWNER = process.env.OWNER || "stevent95@gmail.com";
|
|
const PROVIDER = process.env.PROVIDER || "gmail";
|
|
const FIXTURE =
|
|
process.argv[2] ||
|
|
resolve("tests/fixtures/memory/composio_gmail_inbox.json");
|
|
|
|
let rpcId = 0;
|
|
async function rpc(method, params) {
|
|
rpcId += 1;
|
|
const res = await fetch(RPC_URL, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: rpcId, method, params }),
|
|
});
|
|
if (!res.ok) throw new Error(`${method}: HTTP ${res.status} ${await res.text()}`);
|
|
const body = await res.json();
|
|
if (body.error) {
|
|
throw new Error(`${method}: ${body.error.message || JSON.stringify(body.error)}`);
|
|
}
|
|
return body.result;
|
|
}
|
|
|
|
function parseEmailDate(raw) {
|
|
if (!raw) return Date.now();
|
|
if (typeof raw === "number") return raw < 1e12 ? raw * 1000 : raw;
|
|
const ms = Date.parse(raw);
|
|
return Number.isFinite(ms) ? ms : Date.now();
|
|
}
|
|
|
|
function splitAddresses(value) {
|
|
if (!value) return [];
|
|
if (Array.isArray(value)) return value.filter(Boolean);
|
|
return String(value)
|
|
.split(/[;,]/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function toEmailMessage(slim) {
|
|
return {
|
|
from: slim.from || "unknown@unknown",
|
|
to: splitAddresses(slim.to),
|
|
cc: splitAddresses(slim.cc),
|
|
subject: slim.subject || "(no subject)",
|
|
sent_at: parseEmailDate(slim.date),
|
|
body: slim.markdown || "",
|
|
source_ref: slim.id ? `gmail://message/${slim.id}` : null,
|
|
};
|
|
}
|
|
|
|
function groupByThread(messages) {
|
|
const threads = new Map();
|
|
for (const m of messages) {
|
|
const tid = m.threadId || m.id || "unknown-thread";
|
|
if (!threads.has(tid)) threads.set(tid, []);
|
|
threads.get(tid).push(m);
|
|
}
|
|
return [...threads.entries()].map(([threadId, msgs]) => {
|
|
msgs.sort((a, b) => parseEmailDate(a.date) - parseEmailDate(b.date));
|
|
return {
|
|
threadId,
|
|
subject: msgs[0]?.subject || "(no subject)",
|
|
messages: msgs.map(toEmailMessage),
|
|
};
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
console.log(`[memory-email-ingest] fixture=${FIXTURE}`);
|
|
console.log(`[memory-email-ingest] rpc_url=${RPC_URL}`);
|
|
|
|
// Sanity-check that the core is up.
|
|
await rpc("openhuman.health_snapshot", {}).catch((err) => {
|
|
throw new Error(
|
|
`core not reachable at ${RPC_URL} — start it with \`cargo run --bin openhuman -- serve\`. (${err.message})`,
|
|
);
|
|
});
|
|
|
|
const raw = await readFile(FIXTURE, "utf8");
|
|
const inbox = JSON.parse(raw);
|
|
const messages = Array.isArray(inbox.messages) ? inbox.messages : [];
|
|
if (messages.length === 0) {
|
|
console.error("[memory-email-ingest] no messages in fixture, nothing to do");
|
|
process.exit(1);
|
|
}
|
|
console.log(`[memory-email-ingest] loaded ${messages.length} email(s)`);
|
|
|
|
const threads = groupByThread(messages);
|
|
console.log(`[memory-email-ingest] grouped into ${threads.length} thread(s)`);
|
|
|
|
let chunksWritten = 0;
|
|
let chunksDropped = 0;
|
|
for (const t of threads) {
|
|
const sourceId = `gmail:${t.threadId}`;
|
|
const params = {
|
|
source_kind: "email",
|
|
source_id: sourceId,
|
|
owner: OWNER,
|
|
tags: ["gmail", "ingested", "phase1"],
|
|
payload: {
|
|
provider: PROVIDER,
|
|
thread_subject: t.subject,
|
|
messages: t.messages,
|
|
},
|
|
};
|
|
process.stdout.write(
|
|
` · ${sourceId} (${t.messages.length} msg, subject="${t.subject.slice(0, 60)}") … `,
|
|
);
|
|
try {
|
|
const result = await rpc("openhuman.memory_tree_ingest", params);
|
|
const r = result?.result || result || {};
|
|
chunksWritten += r.chunks_written || 0;
|
|
chunksDropped += r.chunks_dropped || 0;
|
|
console.log(
|
|
`ok written=${r.chunks_written ?? 0} dropped=${r.chunks_dropped ?? 0}`,
|
|
);
|
|
} catch (err) {
|
|
console.log(`FAIL ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`[memory-email-ingest] summary: threads=${threads.length} chunks_written=${chunksWritten} chunks_dropped=${chunksDropped}`,
|
|
);
|
|
|
|
// Quick verification — pull email chunks back out and print a count.
|
|
const list = await rpc("openhuman.memory_tree_list_chunks", {
|
|
source_kind: "email",
|
|
owner: OWNER,
|
|
limit: 100,
|
|
});
|
|
const chunks = list?.result?.chunks || list?.chunks || [];
|
|
console.log(
|
|
`[memory-email-ingest] verify: list_chunks(source_kind=email, owner=${OWNER}) returned ${chunks.length} chunk(s)`,
|
|
);
|
|
for (const c of chunks.slice(0, 5)) {
|
|
const ts = c.metadata?.timestamp
|
|
? new Date(c.metadata.timestamp).toISOString()
|
|
: "?";
|
|
const preview = (c.content || "").replace(/\s+/g, " ").slice(0, 80);
|
|
console.log(` - ${c.id} ${c.metadata?.source_id} ${ts} ${preview}`);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`[memory-email-ingest] fatal: ${err.message}`);
|
|
process.exit(1);
|
|
});
|