@dudousxd/nestjs-agent 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -155,6 +155,111 @@ minted per turn by `resolve`), bounded to `ATTACHMENT_PAGE_SIZE`, and rides the
155
155
  `attachments: { upload: true }` flag. It takes no `threadId` filter: a thread's attachments already
156
156
  ride on its messages in `GET /agent/threads/:id`.
157
157
 
158
+ ### Attachments on nestjs-media (`@dudousxd/nestjs-agent/media`)
159
+
160
+ A ready `AGENT_ATTACHMENT_STAGING` backed by [`@dudousxd/nestjs-media`](https://davidecarvalho.github.io/aviary/docs/media),
161
+ with resumable (tus) uploads. `@dudousxd/nestjs-media` is an **optional** peer: only this subpath
162
+ imports it, and nothing on the root entry needs it.
163
+
164
+ Minimal — next to a `MediaModule` that has `store`, `uploadSessions` and `tus`:
165
+
166
+ ```ts
167
+ @Module({
168
+ imports: [
169
+ MediaModule.forRoot({ /* disks, store, uploadSessions, tus */ }),
170
+ AgentModule.forRoot({ /* … */ }),
171
+ AgentMediaAttachmentsModule.forRoot(), // ← the whole integration
172
+ ],
173
+ })
174
+ // main.ts — nestjs-media's own requirement for tus PATCH bodies:
175
+ app.useBodyParser('raw', { type: 'application/offset+octet-stream' });
176
+ ```
177
+
178
+ Everything is overridable when you need it:
179
+
180
+ ```ts
181
+ AgentMediaAttachmentsModule.forRoot({
182
+ maxBytes: 50 * 1024 * 1024,
183
+ allowedContentTypes: ['image/png', 'image/jpeg', 'application/pdf'],
184
+ collection: 'chat-files',
185
+ canAccess: ({ allowed, actor }) => allowed || actor.roles?.includes('support') === true,
186
+ resolveUrl: (record) => signMyProxyUrl(record.id), // instead of presign / public / inline
187
+ indexForRag: true, // see below
188
+ guards: [SessionGuard],
189
+ });
190
+ ```
191
+
192
+ Each attachment is a media record (`ownerType: 'agent-actor'`, `ownerId: actor.id`, collection
193
+ `agent-attachments`). The upload never goes through the agent:
194
+
195
+ | Step | Route | Who serves it |
196
+ | --- | --- | --- |
197
+ | 1. open | `POST /agent/attachments/uploads` `{ filename, contentType, size }` → `{ mediaId, uploadId, location }` | agent — validates type/size and opens a tus session the actor owns |
198
+ | 2. bytes | `PATCH <location>` (tus, chunked, resumable) | **nestjs-media** |
199
+ | 3. complete | `POST /agent/attachments/uploads/:mediaId/complete` → `MessageAttachment` | agent — checks the bytes landed at the declared size |
200
+ | drop | `DELETE /agent/attachments/uploads/:mediaId` | agent — aborts the session, deletes bytes + record |
201
+
202
+ `@dudousxd/nestjs-agent-react/media` drives all three for `useAttachments`. `POST /agent/attachments`
203
+ (the buffered multipart route, `attachments: { upload: true }`) keeps working on the same store — its
204
+ `stage()` writes a media record too — but it is not needed with this module; `GET
205
+ /agent/attachments` lists either kind.
206
+
207
+ `resolve` hands the model provider a presigned url when the disk can mint one (`urlExpiresInSeconds`,
208
+ default 7 days), the disk's public `url()` with `visibility: 'public'`, or — on a disk that can do
209
+ neither, such as local disk in development — the bytes inline as a `data:` url (logged once; that
210
+ url is persisted with the message). `resolveUrl(record, { disk })` takes over entirely. The actor may
211
+ use media they own, and media a message in one of their own threads already carries; anything else
212
+ resolves `null` (`403` on the chat route).
213
+
214
+ Cleanup: an upload abandoned half-way is already a record, so it shows up in `list` and ages into
215
+ `collectableAttachments`. Delete through the store so the tus session is aborted too:
216
+
217
+ ```ts
218
+ const media = app.get(MediaAttachmentStaging);
219
+ for (const entry of await agents.collectableAttachments(actor, { olderThan })) {
220
+ await media.remove(entry.mediaId);
221
+ }
222
+ ```
223
+
224
+ `indexForRag: true` announces every ready attachment on `aviary:media:attach` (and its removal on
225
+ `aviary:media:delete`) so `@dudousxd/nestjs-agent-rag-media` indexes it. Off by default — only turn
226
+ it on where retrieval is filtered by owner (`FilteredRetriever` on `ownerType`/`ownerId`), and
227
+ restrict rag-media's `collections` accordingly.
228
+
229
+ `forRootAsync({ imports, inject, useFactory, path, guards })` takes the same options; `path` must
230
+ match `AgentModule`'s `path` (default `agent`), and `guards` gates the upload routes.
231
+
232
+ ### Bring your own storage
233
+
234
+ Nothing requires nestjs-media. Bind your own `AttachmentStagingStore` and upload however you like:
235
+
236
+ ```ts
237
+ @Injectable()
238
+ export class S3Staging implements AttachmentStagingStore {
239
+ async stage({ data, filename, contentType, actor }: StageAttachmentInput) {
240
+ const mediaId = randomUUID();
241
+ await this.s3.put(`chat/${actor.id}/${mediaId}`, data, { contentType });
242
+ await this.db.insert(files).values({ mediaId, owner: actor.id, filename, contentType });
243
+ return { mediaId, url: '', contentType, name: filename };
244
+ }
245
+ async resolve({ mediaId, actor }: ResolveAttachmentInput) {
246
+ const row = await this.db.query.files.findFirst({ where: eq(files.mediaId, mediaId) });
247
+ if (!row || row.owner !== actor.id) return null; // unknown and foreign look the same
248
+ const url = await this.s3.presign(`chat/${actor.id}/${mediaId}`, 3600);
249
+ return { mediaId, url, contentType: row.contentType, name: row.filename };
250
+ }
251
+ }
252
+
253
+ // alongside AgentModule, in a @Global() module (or any module AgentModule can see):
254
+ { provide: AGENT_ATTACHMENT_STAGING, useClass: S3Staging }
255
+ ```
256
+
257
+ Then either set `attachments: { upload: true }` for the built-in `POST /agent/attachments` (which
258
+ calls your `stage`), or upload through your own route and give the React side your own `upload`
259
+ (`useAttachments({ upload })` / a backend's `uploadAttachment`) that resolves to a `{ mediaId, … }`
260
+ your `resolve` recognises. `list` is optional and only needed for `GET /agent/attachments` and
261
+ sweeping.
262
+
158
263
  ### Who may read a run
159
264
 
160
265
  `GET /agent/chat/:runId/stream` and `POST /agent/chat/:runId/cancel` both resolve the acting actor