PAIPAI

Files

Upload attachments, send them on a message, show them in the UI.

Files are user-supplied inputs. PAI stores an immutable attachment on the transcript and asks a file provider for the bytes when the model needs them.

Upload first, then send. That is what makes progress, thumbnails, and "which file failed" possible.

const client = paiClient.agent("main");
const thread = client.thread(threadId);

const uploaded = await client.files.upload({
  body: file,
  mediaType: file.type,
  filename: file.name,
});

await thread.send({
  text: "Describe this image.",
  attachments: [uploaded],
});

Only fileId crosses the wire. Filename, type, and size are snapshotted from the provider when the part is stored.

Configure a provider

server/pai.ts
import { createPai } from "@pai/core";
import { createInMemoryFileProvider } from "@pai/files-inmemory";
import { createMongoFileProvider } from "@pai/files-mongo";
import { assistantAgent } from "./assistant-agent";

const files = process.env.MONGO_URL
  ? createMongoFileProvider({ url: process.env.MONGO_URL })
  : createInMemoryFileProvider();
if ("initialize" in files) await files.initialize();

export const pai = createPai({
  agents: { main: assistantAgent },
  scopeKey: (identity) => identity.workspaceId,
  userKey: (identity) => identity.userId,
  files,
});

Without a provider, uploads fail. In-memory is for tests and the first app. Mongo GridFS is the production default next to thread storage.

Render an attachment

Attachment parts sit in message.parts like text and tools:

if (part.type === "attachment") {
  return <AttachmentCard key={part.id} part={part} />;
}

URLs are not stored on the message. Ask for one when you need a preview:

const preview = await client.files.url({
  fileId: part.fileId,
  intent: "view",
});
const href = preview?.url;

In a tool card or transcript, prefer useFileUrl():

const preview = Assistant.useFileUrl(part.fileId, { intent: "view" });
const href = preview?.url;

Or stream bytes with client.files.read({ fileId }).

Composer

function Composer() {
  const composer = Assistant.useComposer();

  return (
    <input
      type="file"
      onChange={(event) => {
        if (event.target.files) void composer.attachFiles(event.target.files);
      }}
    />
  );
}

attachFiles uploads through client.files.upload and stages the result. Image files also get a local previewUrl for a thumbnail.

Next

On this page