server/app-events.ts
Rama main · réplica de lectura del repositorio original.
// What a connected-app event becomes on its way to a run: the plain filter check, the one-line preview, and the run
// prompt, where events that arrive together share one run.
import type { AppTrigger, AppTriggerFilter } from "../shared/app-triggers.ts";
import { filtersAtSource } from "../shared/app-triggers.ts";
import { AUTHORITY, appEventWho, fromNote } from "./provenance-label.ts";
import { defuse as defused } from "./records-block.ts";
/** Event data in one run, as for a webhook delivery; and how many events one run takes before another starts. */
export const MAX_EVENT_CHARS = 48_000;
export const MAX_EVENTS_PER_RUN = 20;
export const BLOCK_OPEN = "[UNTRUSTED APP EVENT DATA]";
const BLOCK_CLOSE = "[/UNTRUSTED APP EVENT DATA]";
const FIELDS: Record<keyof AppTriggerFilter, string[]> = {
from: ["sender", "from", "from_email", "author", "user", "user_name", "username", "email"],
subject: ["subject", "title", "name", "summary"],
label: ["label_ids", "labels", "label", "channel", "channel_name", "tags"],
text: [],
};
function contains(value: unknown, wanted: string): boolean {
if (typeof value === "string" || typeof value === "number") return String(value).toLowerCase().includes(wanted);
if (Array.isArray(value)) return value.some((item) => contains(item, wanted));
return false;
}
/** All set fields must match, case-insensitively. A field the event does not have is not a match. Gmail never needs
* this: its filter went into the search Composio runs. */
export function matchesFilter(app: string, filter: AppTriggerFilter | undefined, data: Record<string, unknown>): boolean {
if (!filter || filtersAtSource(app)) return true;
for (const [key, raw] of Object.entries(filter) as Array<[keyof AppTriggerFilter, string | undefined]>) {
const wanted = raw?.trim().toLowerCase();
if (!wanted) continue;
if (key === "text") {
if (!JSON.stringify(data).toLowerCase().includes(wanted)) return false;
continue;
}
if (!FIELDS[key].some((field) => contains(data[field], wanted))) return false;
}
return true;
}
/** "From Ada <ada@example.com> · Where is my order?", from whichever of those fields the event has. */
export function eventPreview(data: Record<string, unknown>): string | undefined {
const first = (fields: string[]) => fields.map((field) => data[field]).find((value): value is string => typeof value === "string" && value.trim() !== "");
const from = first(FIELDS.from);
const about = first(FIELDS.subject) ?? first(["text", "message_text", "message", "snippet", "body"]);
const line = [from && `From ${from}`, about].filter(Boolean).join(" · ").replace(/\s+/g, " ").trim();
return line ? line.slice(0, 160) : undefined;
}
export function eventBlock(trigger: Pick<AppTrigger, "appName" | "eventName" | "accountLabel">, event: { id: string; receivedAt: number; test: boolean; data: Record<string, unknown> }): string {
let text: string;
try { text = JSON.stringify(event.data, null, 2) ?? "{}"; } catch { text = String(event.data); }
const room = MAX_EVENT_CHARS - 400;
if (text.length > room) text = `${text.slice(0, room)}\n\n[Event truncated by OpenMausBot]`;
const header = [
`App: ${trigger.appName}${trigger.accountLabel ? ` (${trigger.accountLabel})` : ""}`,
`Event: ${trigger.eventName}`,
`Received: ${new Date(event.receivedAt).toISOString()}`,
`Event ID: ${event.id.slice(0, 200)}`,
].join(" ");
const test = event.test ? "\nTEST EVENT, started from OpenMausBot: nothing really happened. Do not reply to, send or change anything; say what you would do." : "";
return [fromNote(appEventWho(trigger.appName, trigger.eventName), "event", AUTHORITY.info), BLOCK_OPEN, `${defused(header)}${test}`, "", defused(text), BLOCK_CLOSE].join("\n");
}
export function eventPrompt(instructions: string, block: string): string {
const configured = instructions.trim() || "Review the event and summarize what happened. Take no outside action unless the event clearly needs it and your permissions allow it.";
return [
"[USER-CONFIGURED TRIGGER INSTRUCTIONS]",
configured,
"[/USER-CONFIGURED TRIGGER INSTRUCTIONS]",
"The email, message or record these instructions mean is in the UNTRUSTED APP EVENT DATA below. Each block is one event; handle each one on its own.",
"",
block,
].join("\n");
}
/** The run's prompt with one more event, or null once the run holds its share of events. */
export function mergeEventBlock(prompt: string, block: string): string | null {
const start = prompt.indexOf(BLOCK_OPEN);
if (start === -1) return null;
const events = prompt.split(BLOCK_OPEN).length - 1;
if (events >= MAX_EVENTS_PER_RUN || prompt.length - start + block.length + 2 > MAX_EVENT_CHARS) return null;
return `${prompt}\n\n${block}`;
}
export function eventCount(prompt: string | undefined): number {
return prompt ? prompt.split(BLOCK_OPEN).length - 1 : 0;
}