chat-agent-toolkit 1.2.115 → 1.2.116

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.
@@ -7,20 +7,39 @@ export interface R2CredentialsInput {
7
7
  }
8
8
  export declare function buildFallbackDocs(query: string): Document[];
9
9
  export declare function normalizeSourcesOutput(output: unknown, query: string): Document[];
10
- /**
11
- * Resolves an uploaded fileId to its extracted `{ title, content }` payload.
12
- * Hosts register a loader (e.g. reading the native R2 binding) so the search
13
- * pipeline does not depend on filesystem or credential-based access.
14
- */
15
- export type UploadFileLoader = (fileId: string) => Promise<{
10
+ /** The payload resolved for an uploaded fileId. */
11
+ export interface LoadedUpload {
16
12
  title: string;
17
13
  content: string;
18
- } | null>;
14
+ /** MIME type for image uploads (e.g. `"image/png"`). */
15
+ mediaType?: string;
16
+ /** `data:`/`http(s)` URL of an image upload, passed to the LLM as image content. */
17
+ image?: string;
18
+ }
19
+ /** An image attachment resolved from an uploaded fileId. */
20
+ export interface UploadImageAttachment {
21
+ mediaType: string;
22
+ /** `data:`/`http(s)` URL of the image, ready for an AI SDK image content part. */
23
+ image: string;
24
+ }
25
+ /**
26
+ * Resolves an uploaded fileId to its extracted payload. Hosts register a loader
27
+ * (e.g. reading the native R2 binding) so the search pipeline does not depend
28
+ * on filesystem or credential-based access. Image uploads additionally carry
29
+ * `mediaType` and `image` so they can be passed to the LLM directly.
30
+ */
31
+ export type UploadFileLoader = (fileId: string) => Promise<LoadedUpload | null>;
19
32
  /**
20
33
  * Registers the loader used by {@link rerankDocs} to resolve uploaded
21
34
  * fileIds to extracted content. The registered loader takes precedence over
22
35
  * the S3-credentials fallback.
23
36
  */
24
37
  export declare function registerUploadFileLoader(loader: UploadFileLoader): void;
38
+ /**
39
+ * Resolves image attachments for the given uploaded fileIds so they can be
40
+ * passed to the LLM as image content parts. Non-image uploads (documents) and
41
+ * images stored without inline data are skipped.
42
+ */
43
+ export declare function loadUploadImages(fileIds: string[], r2Credentials?: R2CredentialsInput): Promise<UploadImageAttachment[]>;
25
44
  export declare function rerankDocs(query: string, docs: Document[], fileIds: string[], optimizationMode: "speed" | "balanced" | "quality", r2Credentials?: R2CredentialsInput): Promise<Document[]>;
26
45
  export declare function processDocs(docs: Document[]): string;
@@ -9,5 +9,5 @@ export { default as generateSuggestions } from './suggestionGeneratorAgent';
9
9
  export { groupAndSummarizeDocs } from './link-summarizer';
10
10
  export type { Document } from './document';
11
11
  export { splitTextIntoChunks } from './document';
12
- export { buildFallbackDocs, rerankDocs, processDocs, normalizeSourcesOutput, registerUploadFileLoader } from './doc-utils';
13
- export type { UploadFileLoader } from './doc-utils';
12
+ export { buildFallbackDocs, rerankDocs, processDocs, normalizeSourcesOutput, registerUploadFileLoader, loadUploadImages } from './doc-utils';
13
+ export type { UploadFileLoader, LoadedUpload, UploadImageAttachment } from './doc-utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-agent-toolkit",
3
- "version": "1.2.115",
3
+ "version": "1.2.116",
4
4
  "description": "Multi-provider AI agent toolkit: generate language responses, search the web, extract content, and manage memory across 10+ LLM providers.",
5
5
  "author": "vtempest <grokthiscontact@gmail.com>",
6
6
  "license": "rights.institute/PROSPER",
@@ -36,14 +36,30 @@ export function normalizeSourcesOutput(output: unknown, query: string): Document
36
36
  return buildFallbackDocs(query);
37
37
  }
38
38
 
39
+ /** The payload resolved for an uploaded fileId. */
40
+ export interface LoadedUpload {
41
+ title: string;
42
+ content: string;
43
+ /** MIME type for image uploads (e.g. `"image/png"`). */
44
+ mediaType?: string;
45
+ /** `data:`/`http(s)` URL of an image upload, passed to the LLM as image content. */
46
+ image?: string;
47
+ }
48
+
49
+ /** An image attachment resolved from an uploaded fileId. */
50
+ export interface UploadImageAttachment {
51
+ mediaType: string;
52
+ /** `data:`/`http(s)` URL of the image, ready for an AI SDK image content part. */
53
+ image: string;
54
+ }
55
+
39
56
  /**
40
- * Resolves an uploaded fileId to its extracted `{ title, content }` payload.
41
- * Hosts register a loader (e.g. reading the native R2 binding) so the search
42
- * pipeline does not depend on filesystem or credential-based access.
57
+ * Resolves an uploaded fileId to its extracted payload. Hosts register a loader
58
+ * (e.g. reading the native R2 binding) so the search pipeline does not depend
59
+ * on filesystem or credential-based access. Image uploads additionally carry
60
+ * `mediaType` and `image` so they can be passed to the LLM directly.
43
61
  */
44
- export type UploadFileLoader = (
45
- fileId: string,
46
- ) => Promise<{ title: string; content: string } | null>;
62
+ export type UploadFileLoader = (fileId: string) => Promise<LoadedUpload | null>;
47
63
 
48
64
  let uploadFileLoader: UploadFileLoader | null = null;
49
65
 
@@ -56,7 +72,7 @@ export function registerUploadFileLoader(loader: UploadFileLoader): void {
56
72
  uploadFileLoader = loader;
57
73
  }
58
74
 
59
- async function downloadExtractedContent(fileId: string, r2Credentials: R2CredentialsInput): Promise<{ title: string; content: string } | null> {
75
+ async function downloadExtractedContent(fileId: string, r2Credentials: R2CredentialsInput): Promise<LoadedUpload | null> {
60
76
  try {
61
77
  const { manageStorage } = await import("manage-storage");
62
78
  const config = {
@@ -69,13 +85,69 @@ async function downloadExtractedContent(fileId: string, r2Credentials: R2Credent
69
85
  const extractedKey = `${fileId}-extracted.json`;
70
86
  const data = await manageStorage("download", { ...config, key: extractedKey });
71
87
  const parsed = JSON.parse(data);
72
- return { title: parsed.title || "Uploaded Document", content: parsed.content || "" };
88
+ return {
89
+ title: parsed.title || "Uploaded Document",
90
+ content: parsed.content || "",
91
+ ...(parsed.mediaType ? { mediaType: parsed.mediaType } : {}),
92
+ ...(parsed.image ? { image: parsed.image } : {}),
93
+ };
73
94
  } catch (error) {
74
95
  console.error(`[rerankDocs] Failed to download extracted content for fileId ${fileId}:`, error);
75
96
  return null;
76
97
  }
77
98
  }
78
99
 
100
+ /**
101
+ * Resolves a single uploaded fileId via the registered loader (preferred) or
102
+ * the R2 credentials fallback. Returns `null` when neither can resolve it.
103
+ */
104
+ async function resolveUpload(
105
+ fileId: string,
106
+ r2Credentials?: R2CredentialsInput,
107
+ ): Promise<LoadedUpload | null> {
108
+ if (uploadFileLoader) {
109
+ try {
110
+ const loaded = await uploadFileLoader(fileId);
111
+ if (loaded) return loaded;
112
+ } catch (error) {
113
+ console.error(
114
+ `[resolveUpload] Registered upload loader failed for fileId ${fileId}:`,
115
+ error,
116
+ );
117
+ }
118
+ }
119
+ if (r2Credentials) {
120
+ return downloadExtractedContent(fileId, r2Credentials);
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Resolves image attachments for the given uploaded fileIds so they can be
127
+ * passed to the LLM as image content parts. Non-image uploads (documents) and
128
+ * images stored without inline data are skipped.
129
+ */
130
+ export async function loadUploadImages(
131
+ fileIds: string[],
132
+ r2Credentials?: R2CredentialsInput,
133
+ ): Promise<UploadImageAttachment[]> {
134
+ if (!fileIds || fileIds.length === 0) return [];
135
+
136
+ const resolved = await Promise.all(
137
+ fileIds.map((fileId) => resolveUpload(fileId, r2Credentials)),
138
+ );
139
+
140
+ return resolved
141
+ .filter(
142
+ (r): r is LoadedUpload =>
143
+ r !== null && typeof r.image === "string" && r.image.length > 0,
144
+ )
145
+ .map((r) => ({
146
+ mediaType: r.mediaType || "image/png",
147
+ image: r.image as string,
148
+ }));
149
+ }
150
+
79
151
  export async function rerankDocs(
80
152
  query: string,
81
153
  docs: Document[],
@@ -87,37 +159,25 @@ export async function rerankDocs(
87
159
  return docs;
88
160
  }
89
161
 
90
- let filesData: { title: string; content: string }[] = [];
162
+ let filesData: LoadedUpload[] = [];
91
163
 
92
164
  if (fileIds.length > 0) {
93
165
  const results = await Promise.all(
94
- fileIds.map(async (fileId) => {
95
- if (uploadFileLoader) {
96
- try {
97
- const loaded = await uploadFileLoader(fileId);
98
- if (loaded) return loaded;
99
- } catch (error) {
100
- console.error(
101
- `[rerankDocs] Registered upload loader failed for fileId ${fileId}:`,
102
- error,
103
- );
104
- }
105
- }
106
- if (r2Credentials) {
107
- return downloadExtractedContent(fileId, r2Credentials);
108
- }
109
- return null;
110
- }),
166
+ fileIds.map((fileId) => resolveUpload(fileId, r2Credentials)),
111
167
  );
112
- filesData = results.filter((r) => r !== null);
168
+ filesData = results.filter((r): r is LoadedUpload => r !== null);
113
169
  }
114
170
 
115
- // Uploaded files must always reach the LLM. Build their docs up front so
116
- // every return path below keeps them in the answer context.
117
- const fileDocs: Document[] = filesData.map((fileData) => ({
118
- pageContent: fileData.content,
119
- metadata: { title: fileData.title, url: "File" },
120
- }));
171
+ // Uploaded documents must always reach the LLM. Build their docs up front so
172
+ // every return path below keeps them in the answer context. Image uploads
173
+ // carry no text content — they are passed separately as image parts (see
174
+ // loadUploadImages) — so they are excluded here to avoid empty docs.
175
+ const fileDocs: Document[] = filesData
176
+ .filter((fileData) => fileData.content && fileData.content.length > 0)
177
+ .map((fileData) => ({
178
+ pageContent: fileData.content,
179
+ metadata: { title: fileData.title, url: "File" },
180
+ }));
121
181
 
122
182
  if (query.toLocaleLowerCase() === "summarize") {
123
183
  // Skip web-result reranking for an explicit "summarize" request, but keep
@@ -10,5 +10,5 @@ export { default as generateSuggestions } from "./suggestionGeneratorAgent";
10
10
  export { groupAndSummarizeDocs } from "./link-summarizer";
11
11
  export type { Document } from "./document";
12
12
  export { splitTextIntoChunks } from "./document";
13
- export { buildFallbackDocs, rerankDocs, processDocs, normalizeSourcesOutput, registerUploadFileLoader } from "./doc-utils";
14
- export type { UploadFileLoader } from "./doc-utils";
13
+ export { buildFallbackDocs, rerankDocs, processDocs, normalizeSourcesOutput, registerUploadFileLoader, loadUploadImages } from "./doc-utils";
14
+ export type { UploadFileLoader, LoadedUpload, UploadImageAttachment } from "./doc-utils";
@@ -35,6 +35,7 @@ import {
35
35
  rerankDocs,
36
36
  processDocs,
37
37
  normalizeSourcesOutput,
38
+ loadUploadImages,
38
39
  type R2CredentialsInput,
39
40
  } from "./doc-utils";
40
41
  import { groupAndSummarizeDocs } from "./link-summarizer";
@@ -378,11 +379,26 @@ class MetaSearchAgent implements MetaSearchAgentType {
378
379
  date: new Date().toISOString(),
379
380
  });
380
381
 
382
+ // Resolve any uploaded images so they are passed to the LLM directly as
383
+ // image content parts (alongside the text query). Documents already
384
+ // reach the model as text context via processDocs above.
385
+ const imageAttachments = await loadUploadImages(fileIds, r2Credentials);
386
+ const userContent =
387
+ imageAttachments.length > 0
388
+ ? [
389
+ { type: "text" as const, text: message },
390
+ ...imageAttachments.map((img) => ({
391
+ type: "image" as const,
392
+ image: img.image,
393
+ })),
394
+ ]
395
+ : message;
396
+
381
397
  const result = streamText({
382
398
  model: llm,
383
399
  temperature: 0.7,
384
400
  system: systemPrompt,
385
- messages: [...history, { role: "user", content: message }],
401
+ messages: [...history, { role: "user", content: userContent }],
386
402
  });
387
403
 
388
404
  let responseChunkCount = 0;