Ctrl K
ComunidadGuías…

server/app-events.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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;
}