UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

155 lines • 8.24 kB
import type { ChunkOptions } from '../rag/chunk.js'; import type { VectorStore } from '../rag/vector-store.js'; import type { EmbeddingProvider } from '../spi/embedding-provider.js'; import { type UploadCompletePayload } from './media-events.js'; import { type TextExtractor } from './text-extractor.js'; /** The minimal, structural slice of an `@adonis-agora/media` disk this bridge reads bytes from. */ export interface DiskBytesReader { /** Return the object's raw bytes (a media disk's `getBytes(key)`). */ getBytes(key: string): Promise<Uint8Array>; } /** * The minimal, structural slice of an `@adonis-agora/media` `MediaManager` this bridge needs — just * `disk(name)` yielding a {@link DiskBytesReader}. Structural typing keeps `@adonis-agora/media` an * OPTIONAL peer: nothing is imported from it, an app passes its live `MediaManager` (or any handle that * satisfies this shape) at wiring time. */ export interface MediaManagerHandle { disk(name?: string): DiskBytesReader; } /** * A media file to ingest. `id` becomes the RAG document id (chunk ids `${id}#<n>`); `disk`/`key` locate * the bytes through the {@link MediaManagerHandle}; `contentType` drives extractor dispatch. The owner / * tenant fields are optional and, when present, tag every chunk's metadata so retrieval can filter by * tenant/owner/collection. */ export interface MediaRef { id: string; disk: string; key: string; contentType: string; ownerType?: string; ownerId?: string; collection?: string; tenantRef?: string; /** Byte size, when known up front — used only by the `maxBytes` gate; the bytes are the source of truth. */ size?: number; } export type MediaIngestSkipReason = 'unsupported-type' | 'too-large' | 'empty-text'; export type MediaIngestResult = { status: 'ingested'; chunks: number; } | { status: 'skipped'; reason: MediaIngestSkipReason; }; /** Turn an `upload.complete` diagnostics payload into an ingestable {@link MediaRef}, or `null` to skip. */ export type ResolveMediaRef = (payload: UploadCompletePayload) => MediaRef | null | Promise<MediaRef | null>; /** * Everything the media→RAG bridge needs. Only `media`, `embedder`, and `store` are required; the * extractor defaults to {@link defaultTextExtractor} (text/*, JSON, HTML) so binary formats are skipped * rather than indexed as garbage. Register a PDF/DOCX parser on a {@link import('./text-extractor.js').MimeTextExtractor} * and pass it as `extractor` — the PDF library stays entirely on the host side (never a dependency here). */ export interface MediaRagIngestionConfig { /** The media handle bytes are read through (an `@adonis-agora/media` `MediaManager`, structurally typed). */ media: MediaManagerHandle; /** Embeds each chunk. Pass the same {@link EmbeddingProvider} the agent's retriever uses. */ embedder: EmbeddingProvider; /** The RAG vector store chunks are upserted into (the same store the retriever searches). */ store: VectorStore; /** Bytes → text. Default {@link defaultTextExtractor}. Register a PDF/DOCX hook to widen it. */ extractor?: TextExtractor; /** * Restrict ingestion to these content types (checked before the extractor). Omit/empty = every type * the extractor supports. A content type not in this allow-list is skipped as `unsupported-type`. */ contentTypes?: string[]; /** Chunking options forwarded to `chunkDocuments`. */ chunk?: ChunkOptions; /** Skip (don't read) files larger than this many bytes (checked against `ref.size` when present). */ maxBytes?: number; /** * Resolve an `upload.complete` diagnostics payload (`{ id, disk, key }`) into a full {@link MediaRef} * — the seam the auto-subscriber needs, since the event carries no content-type or owner metadata. * Required to call {@link MediaRagIngestion.subscribe}; return `null` to skip an event. */ resolve?: ResolveMediaRef; } /** Thrown by {@link MediaRagIngestion.subscribe} when no `resolve` seam was configured. */ export declare class MediaRagResolveRequiredError extends Error { constructor(); } /** * Bridges `@adonis-agora/media` uploads into the agent's RAG stack: fetch the bytes → extract text by * content type → chunk → embed → upsert into the configured {@link VectorStore}, tagging every chunk * with `{ mediaId, ownerType, ownerId, collection, tenantRef }` so retrieval can be scoped per * tenant/owner/collection. Two triggers, both opt-in and no-op until wired: * * - {@link ingestMedia} — call it explicitly with a {@link MediaRef} (e.g. from your own upload flow). * - {@link subscribe} — subscribe to the media library's `agora:media:upload.complete` diagnostics * channel and auto-ingest each finished upload (needs a `resolve` seam). * * Everything is structural: no import of `@adonis-agora/media`, and `node:diagnostics_channel` is a * Node builtin, so there is no hard dependency on `@adonis-agora/diagnostics` for the subscribe path. */ export declare class MediaRagIngestion { private readonly config; private readonly extractor; private readonly contentTypes; private readonly inFlight; private teardown; constructor(config: MediaRagIngestionConfig); /** * Ingest one media file: content-type filter → size gate → read bytes → extract → remove-then-chunk * → embed → upsert. Unsupported/oversized/empty files are skipped (not errors). Chunk metadata carries * `{ mediaId, ownerType, ownerId, collection, tenantRef }` (only the fields present on `ref`). */ ingestMedia(ref: MediaRef): Promise<MediaIngestResult>; /** Drop a media document's chunks from the vector store — the delete half of keeping RAG in sync. */ removeMedia(mediaId: string): Promise<void>; /** * Guard + resolve + ingest one `upload.complete` diagnostics payload. Public so a caller can await it * directly (deterministic tests) instead of going through the subscription. A no-op when `resolve` * isn't configured, the payload isn't a valid `upload.complete`, or `resolve` returns `null`. */ handleUploadComplete(payload: unknown): Promise<MediaIngestResult | undefined>; /** * Subscribe to the media library's `agora:media:upload.complete` channel and auto-ingest each finished * upload. Idempotent (a second call is a no-op while already subscribed). Returns — and stores — an * unsubscribe function; ingestion errors are caught and published on `agora:rag:media.failed` so a bad * file never breaks the channel. Requires a `resolve` seam ({@link MediaRagResolveRequiredError}). */ subscribe(): () => void; /** Unsubscribe from the media channel (if subscribed) and await every in-flight auto-ingestion. */ unsubscribe(): Promise<void>; /** Await every in-flight auto-ingestion — for graceful shutdown and deterministic tests. */ settle(): Promise<void>; /** Whether the auto-subscriber is currently attached to the media channel. */ get subscribed(): boolean; /** Handle one channel event with the same error boundary the subscriber uses. */ private ingestFromEvent; private track; } /** * Build a media→RAG ingestion bridge (idiomatic factory). Wire it in a provider's `boot()`: * * ```ts * import { mediaRagIngestion } from '@adonis-agora/agent/rag-media' * * const ingestion = mediaRagIngestion({ * media: await app.container.make('media.manager'), * embedder: myEmbedder, * store: myVectorStore, * contentTypes: ['text/plain', 'text/markdown', 'application/pdf'], * extractor: defaultTextExtractor().register('application/pdf', myPdfExtractor), * resolve: async ({ id, disk, key }) => lookupMediaRecord(id), // → MediaRef * }) * const off = ingestion.subscribe() // auto-ingest on upload.complete * ``` * * Or trigger it explicitly: `await ingestion.ingestMedia(ref)`. Everything is opt-in and no-op until * wired — nothing runs, subscribes, or imports the media/pdf peers unless you call it. */ export declare function mediaRagIngestion(config: MediaRagIngestionConfig): MediaRagIngestion; //# sourceMappingURL=media-rag-ingestion.d.ts.map