186 lines · 7.5 KB · TypeScript
RawDownload
import { projectQuestionToolInput } from "@t3tools/shared/toolActivity";import {  type OrchestrationThreadActivity,  UserInputAttachmentAnswerPayload,} from "@t3tools/contracts";import * as Schema from "effect/Schema"; function record(value: unknown): Record<string, unknown> | undefined {  return value !== null && typeof value === "object" && !Array.isArray(value)    ? (value as Record<string, unknown>)    : undefined;} const isQuestionAnswer = Schema.is(UserInputAttachmentAnswerPayload); function displayOptionAnswer(value: unknown, labels: ReadonlyMap<string, string>): unknown {  if (typeof value === "string") return labels.get(value) ?? value;  if (Array.isArray(value)) return value.map((answer) => displayOptionAnswer(answer, labels));  const nested = record(value);  return nested && "answers" in nested    ? { ...nested, answers: displayOptionAnswer(nested.answers, labels) }    : value;} function questionFingerprint(  turnId: string,  questions: ReadonlyArray<unknown>,): string | undefined {  const texts = questions.map((question) => (typeof question === "string" ? question.trim() : ""));  return texts.length > 0 && texts.every(Boolean)    ? JSON.stringify([turnId, texts.toSorted()])    : undefined;} function withoutDuplicateQuestionTools(  activities: ReadonlyArray<OrchestrationThreadActivity>,): ReadonlyArray<OrchestrationThreadActivity> {  const questions = new Set<string>();  for (const activity of activities) {    if (activity.kind !== "user-input.answer-submitted" || !activity.turnId) continue;    const payload = record(activity.payload);    const texts = Object.values(record(payload?.questionTextById) ?? {});    const fingerprint = questionFingerprint(activity.turnId, texts);    if (fingerprint) questions.add(fingerprint);  }  if (questions.size === 0) return activities;  const duplicateToolIds = new Set<string>();  for (const activity of activities) {    if (!activity.kind.startsWith("tool.") || !activity.turnId) continue;    const payload = record(activity.payload);    if (typeof payload?.toolCallId !== "string") continue;    const input = projectQuestionToolInput(record(payload.data) ?? {}, payload.title).input;    if (!input) continue;    const fingerprint = questionFingerprint(      activity.turnId,      input.questions.map((question) => record(question)?.question),    );    if (fingerprint && questions.has(fingerprint)) {      duplicateToolIds.add(JSON.stringify([activity.turnId, payload.toolCallId]));    }  }  return activities.filter((activity) => {    const payload = record(activity.payload);    const toolCallId = payload?.toolCallId;    return (      activity.tone === "error" ||      /^(failed|declined|stopped|cancelled)$/.test(String(payload?.status)) ||      !activity.kind.startsWith("tool.") ||      typeof toolCallId !== "string" ||      !duplicateToolIds.has(JSON.stringify([activity.turnId, toolCallId]))    );  });} /** Keep a question and its answer at the original tool position in the work log. */export function foldUserInputActivities(  activities: ReadonlyArray<OrchestrationThreadActivity>,): ReadonlyArray<OrchestrationThreadActivity> {  const requests = new Map<string, OrchestrationThreadActivity[]>();  for (const activity of activities) {    if (      activity.kind !== "user-input.requested" &&      activity.kind !== "user-input.resolved" &&      activity.kind !== "user-input.answer-submitted"    )      continue;    const requestId = record(activity.payload)?.requestId;    if (typeof requestId !== "string" || !requestId) continue;    const group = requests.get(requestId) ?? [];    group.push(activity);    requests.set(requestId, group);  }  const replacements = new Map<OrchestrationThreadActivity, OrchestrationThreadActivity | null>();  for (const [requestId, group] of requests) {    const payloads = group.map((activity) => record(activity.payload)!);    const questions = new Map<string, Record<string, unknown>>();    const texts = new Map<string, unknown>();    for (const payload of payloads) {      for (const [id, text] of Object.entries(record(payload.questionTextById) ?? {}))        texts.set(id, text);      for (const value of Array.isArray(payload.questions) ? payload.questions : []) {        const question = record(value);        if (typeof question?.id !== "string") continue;        questions.set(question.id, question);        if (typeof question.question === "string") texts.set(question.id, question.question);      }    }    const questionTextById = Object.fromEntries(texts);    const submitted = group.findLast(      (activity) =>        activity.kind === "user-input.answer-submitted" &&        record(record(activity.payload)?.answers),    );    const rawAnswers =      record(record(submitted?.payload)?.answers) ??      payloads.map((payload) => record(payload.answers)).findLast(Boolean) ??      {};    const answers = Object.fromEntries(      Object.entries(rawAnswers).map(([id, value]) => {        const options = questions.get(id)?.options;        const labels = new Map<string, string>();        for (const candidate of Array.isArray(options) ? options : []) {          const option = record(candidate);          if (typeof option?.value === "string" && typeof option.label === "string")            labels.set(option.value, option.label);        }        return [id, displayOptionAnswer(value, labels)];      }),    );    const attachmentsByQuestionId = Object.fromEntries(      payloads.flatMap((payload) => Object.entries(record(payload.attachmentsByQuestionId) ?? {})),    );    const answer = { requestId, questionTextById, answers, attachmentsByQuestionId };    if (!isQuestionAnswer(answer)) continue;    const submittedAnswer =      Object.keys(answers).length > 0 || Object.keys(attachmentsByQuestionId).length > 0;    for (const activity of group) replacements.set(activity, null);    replacements.set(group[0]!, {      ...group[0]!,      kind: "user-input.answer-submitted",      tone: "tool",      summary: submittedAnswer        ? "User input submitted"        : group.some((activity) => activity.kind === "user-input.resolved")          ? "User input dismissed"          : "User input requested",      payload: answer,    });  }  return withoutDuplicateQuestionTools(    activities.flatMap((activity) => {      const replacement = replacements.get(activity);      return replacement === null ? [] : [replacement ?? activity];    }),  );} export function getQuestionAnswerText(value: unknown): string {  if (typeof value === "string") return value;  if (Array.isArray(value)) return value.map(getQuestionAnswerText).filter(Boolean).join(", ");  const nested = record(value);  return nested ? getQuestionAnswerText(nested.answers) : "";} export function getQuestionAnswerPreview(answer: UserInputAttachmentAnswerPayload): string {  const answers = Object.values(answer.answers).map(getQuestionAnswerText).filter(Boolean);  const attachments = Object.values(answer.attachmentsByQuestionId)    .flat()    .map((attachment) => attachment.name);  return (    answers.length > 0      ? answers.join(" · ")      : attachments.length > 0        ? attachments.join(", ")        : Object.values(answer.questionTextById ?? {}).join(" · ")  )    .replace(/\s+/g, " ")    .trim();} export function hasQuestionAnswer(answer: UserInputAttachmentAnswerPayload): boolean {  return (    Object.values(answer.answers).some(getQuestionAnswerText) ||    Object.values(answer.attachmentsByQuestionId).some((attachments) => attachments.length > 0)  );}