write-language 0.1.93 → 0.1.94
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/dist/generate-response.attachments.test.d.ts +1 -0
- package/dist/generate-response.d.ts +1 -1
- package/dist/generation-types.d.ts +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/write-language.cjs.js +1 -1
- package/dist/write-language.cjs.js.map +1 -1
- package/dist/write-language.es.js +1 -1
- package/dist/write-language.es.js.map +1 -1
- package/package.json +1 -1
- package/src/generate-response.attachments.test.ts +110 -0
- package/src/generate-response.ts +53 -1
- package/src/generation-types.ts +31 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "write-language",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
4
4
|
"description": "Multi-provider language generation toolkit using Vercel AI SDK - generate responses with 10+ LLM providers including OpenAI, Anthropic, Google, and more.",
|
|
5
5
|
"author": "vtempest <grokthiscontact@gmail.com>",
|
|
6
6
|
"license": "rights.institute/PROSPER",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Verifies that file/image attachments are forwarded to the
|
|
3
|
+
* Vercel AI SDK as multimodal `messages` content parts, and that plain
|
|
4
|
+
* (attachment-free) calls keep using the simpler `prompt` form.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
// Capture the exact arguments passed to generateText so we can assert on the
|
|
9
|
+
// shape of the request the SDK receives.
|
|
10
|
+
const generateTextMock = vi.fn((_args: unknown) => Promise.resolve({ text: "ok" }));
|
|
11
|
+
|
|
12
|
+
vi.mock("ai", () => ({
|
|
13
|
+
generateText: (args: unknown) => generateTextMock(args),
|
|
14
|
+
stepCountIs: (n: number) => n,
|
|
15
|
+
tool: (t: unknown) => t,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Return a truthy fake model so writeLanguageResponse proceeds to generateText.
|
|
19
|
+
vi.mock("./provider-factory", () => ({
|
|
20
|
+
createLLMProvider: () => ({ id: "fake-model" }),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Avoid pulling the markdown/prism stack into the test.
|
|
24
|
+
vi.mock("./utils/markdown-to-html", () => ({
|
|
25
|
+
convertMarkdownToHTMLEscaped: async (s: string) => s,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { writeLanguageResponse } from "./generate-response";
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
generateTextMock.mockClear();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("writeLanguageResponse attachments", () => {
|
|
35
|
+
const base = {
|
|
36
|
+
provider: "groq",
|
|
37
|
+
apiKey: "test-key",
|
|
38
|
+
agent: "question",
|
|
39
|
+
query: "Describe the attached file",
|
|
40
|
+
html: false,
|
|
41
|
+
applyContextLimit: false,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
it("sends a plain text prompt when there are no attachments", async () => {
|
|
45
|
+
await writeLanguageResponse({ ...base });
|
|
46
|
+
|
|
47
|
+
expect(generateTextMock).toHaveBeenCalledTimes(1);
|
|
48
|
+
const arg = generateTextMock.mock.calls[0][0] as Record<string, unknown>;
|
|
49
|
+
expect(typeof arg.prompt).toBe("string");
|
|
50
|
+
expect(arg.messages).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("sends an image attachment as an image content part", async () => {
|
|
54
|
+
await writeLanguageResponse({
|
|
55
|
+
...base,
|
|
56
|
+
attachments: [
|
|
57
|
+
{ mediaType: "image/png", data: "data:image/png;base64,AAAA" },
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const arg = generateTextMock.mock.calls[0][0] as any;
|
|
62
|
+
expect(arg.prompt).toBeUndefined();
|
|
63
|
+
expect(Array.isArray(arg.messages)).toBe(true);
|
|
64
|
+
const parts = arg.messages[0].content;
|
|
65
|
+
expect(parts[0]).toMatchObject({ type: "text" });
|
|
66
|
+
expect(parts[1]).toMatchObject({
|
|
67
|
+
type: "image",
|
|
68
|
+
image: "data:image/png;base64,AAAA",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sends a document attachment as a file content part with its mediaType", async () => {
|
|
73
|
+
await writeLanguageResponse({
|
|
74
|
+
...base,
|
|
75
|
+
attachments: [
|
|
76
|
+
{
|
|
77
|
+
mediaType: "application/pdf",
|
|
78
|
+
data: "JVBERi0=",
|
|
79
|
+
filename: "report.pdf",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const arg = generateTextMock.mock.calls[0][0] as any;
|
|
85
|
+
const parts = arg.messages[0].content;
|
|
86
|
+
expect(parts[1]).toMatchObject({
|
|
87
|
+
type: "file",
|
|
88
|
+
mediaType: "application/pdf",
|
|
89
|
+
data: "JVBERi0=",
|
|
90
|
+
filename: "report.pdf",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("skips malformed attachments (missing data or mediaType)", async () => {
|
|
95
|
+
await writeLanguageResponse({
|
|
96
|
+
...base,
|
|
97
|
+
attachments: [
|
|
98
|
+
{ mediaType: "", data: "x" } as any,
|
|
99
|
+
{ mediaType: "image/png" } as any,
|
|
100
|
+
{ mediaType: "image/jpeg", data: "data:image/jpeg;base64,BBBB" },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const arg = generateTextMock.mock.calls[0][0] as any;
|
|
105
|
+
const parts = arg.messages[0].content;
|
|
106
|
+
// 1 text part + 1 valid image part only.
|
|
107
|
+
expect(parts).toHaveLength(2);
|
|
108
|
+
expect(parts[1]).toMatchObject({ type: "image" });
|
|
109
|
+
});
|
|
110
|
+
});
|
package/src/generate-response.ts
CHANGED
|
@@ -13,12 +13,14 @@ import type {
|
|
|
13
13
|
AgentTool,
|
|
14
14
|
GenerateLanguageOptions,
|
|
15
15
|
GenerateLanguageResult,
|
|
16
|
+
LanguageAttachment,
|
|
16
17
|
} from "./generation-types";
|
|
17
18
|
|
|
18
19
|
export type {
|
|
19
20
|
LLMProviderName,
|
|
20
21
|
GenerateLanguageOptions,
|
|
21
22
|
GenerateLanguageResult,
|
|
23
|
+
LanguageAttachment,
|
|
22
24
|
} from "./generation-types";
|
|
23
25
|
export { convertMarkdownToHTMLEscaped } from "./utils/markdown-to-html";
|
|
24
26
|
|
|
@@ -61,6 +63,7 @@ export async function writeLanguageResponse(
|
|
|
61
63
|
temperature = 1,
|
|
62
64
|
html = true,
|
|
63
65
|
applyContextLimit = true,
|
|
66
|
+
attachments,
|
|
64
67
|
...context
|
|
65
68
|
} = options;
|
|
66
69
|
|
|
@@ -144,10 +147,23 @@ export async function writeLanguageResponse(
|
|
|
144
147
|
: undefined;
|
|
145
148
|
|
|
146
149
|
// \u2500\u2500 8. Invoke LLM via Vercel AI SDK generateText \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
150
|
+
// When files/images are attached, issue a multimodal `messages` request so
|
|
151
|
+
// the model receives the uploaded content directly. Otherwise fall back to
|
|
152
|
+
// the simpler `prompt` form.
|
|
153
|
+
const fileParts = buildAttachmentParts(attachments);
|
|
147
154
|
const { text: rawReply } = await generateText({
|
|
148
155
|
model: llm,
|
|
149
|
-
prompt,
|
|
150
156
|
temperature,
|
|
157
|
+
...(fileParts.length > 0
|
|
158
|
+
? {
|
|
159
|
+
messages: [
|
|
160
|
+
{
|
|
161
|
+
role: "user" as const,
|
|
162
|
+
content: [{ type: "text" as const, text: prompt }, ...fileParts],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
: { prompt }),
|
|
151
167
|
...(tools && { tools, stopWhen: stepCountIs(10) }),
|
|
152
168
|
});
|
|
153
169
|
|
|
@@ -172,6 +188,42 @@ export async function writeLanguageResponse(
|
|
|
172
188
|
}
|
|
173
189
|
}
|
|
174
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Converts {@link LanguageAttachment}s into Vercel AI SDK message content
|
|
193
|
+
* parts. `image/*` attachments (or those with `kind: "image"`) become
|
|
194
|
+
* `image` parts; everything else becomes a `file` part carrying its
|
|
195
|
+
* `mediaType`. Attachments without usable data are skipped.
|
|
196
|
+
*
|
|
197
|
+
* @param attachments - File/image attachments from the caller
|
|
198
|
+
* @returns An array of AI SDK content parts (empty when none are supplied)
|
|
199
|
+
*/
|
|
200
|
+
function buildAttachmentParts(
|
|
201
|
+
attachments: LanguageAttachment[] | undefined,
|
|
202
|
+
): Array<
|
|
203
|
+
| { type: "image"; image: LanguageAttachment["data"] }
|
|
204
|
+
| { type: "file"; data: LanguageAttachment["data"]; mediaType: string; filename?: string }
|
|
205
|
+
> {
|
|
206
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
207
|
+
|
|
208
|
+
return attachments
|
|
209
|
+
.filter((a) => a && a.data != null && a.mediaType)
|
|
210
|
+
.map((a) => {
|
|
211
|
+
const isImage =
|
|
212
|
+
a.kind === "image" ||
|
|
213
|
+
(a.kind !== "file" && a.mediaType.toLowerCase().startsWith("image/"));
|
|
214
|
+
|
|
215
|
+
if (isImage) {
|
|
216
|
+
return { type: "image" as const, image: a.data };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
type: "file" as const,
|
|
220
|
+
data: a.data,
|
|
221
|
+
mediaType: a.mediaType,
|
|
222
|
+
...(a.filename ? { filename: a.filename } : {}),
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
175
227
|
/**
|
|
176
228
|
* Substitutes `{variableName}` placeholders in a template string with values
|
|
177
229
|
* from `vars`. Object/array values are pretty-printed without braces or
|
package/src/generation-types.ts
CHANGED
|
@@ -20,6 +20,31 @@ export type LLMProviderName =
|
|
|
20
20
|
| "togetherai"
|
|
21
21
|
| (string & {}); // preserve autocomplete while allowing arbitrary strings
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A file or image attachment passed to the model alongside the text prompt.
|
|
25
|
+
*
|
|
26
|
+
* The Vercel AI SDK accepts these as multimodal message content parts:
|
|
27
|
+
* images become `{ type: "image", image }` parts and every other media type
|
|
28
|
+
* becomes a `{ type: "file", data, mediaType }` part. `data` may be a base64
|
|
29
|
+
* string, a `data:` URL, an `http(s)` URL, a `URL` instance, or raw bytes.
|
|
30
|
+
*/
|
|
31
|
+
export interface LanguageAttachment {
|
|
32
|
+
/** MIME type, e.g. `"image/png"`, `"application/pdf"`. */
|
|
33
|
+
mediaType: string;
|
|
34
|
+
/**
|
|
35
|
+
* The attachment payload: a base64 string, a `data:`/`http(s)` URL, a `URL`
|
|
36
|
+
* instance, or raw bytes (`Uint8Array`/`ArrayBuffer`).
|
|
37
|
+
*/
|
|
38
|
+
data: string | Uint8Array | ArrayBuffer | URL;
|
|
39
|
+
/** Optional original filename (used by providers that surface it). */
|
|
40
|
+
filename?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Force how the part is sent. Defaults to auto-detection from `mediaType`
|
|
43
|
+
* (`image/*` → image part, otherwise a file part).
|
|
44
|
+
*/
|
|
45
|
+
kind?: "image" | "file";
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
/**
|
|
24
49
|
* Configuration options for {@link writeLanguageResponse}.
|
|
25
50
|
*/
|
|
@@ -47,6 +72,12 @@ export interface GenerateLanguageOptions {
|
|
|
47
72
|
article?: string;
|
|
48
73
|
/** Prior conversation history for context-aware agents */
|
|
49
74
|
chat_history?: string;
|
|
75
|
+
/**
|
|
76
|
+
* File and image attachments to send to the model alongside the prompt.
|
|
77
|
+
* When present, the request is issued as a multimodal `messages` call so the
|
|
78
|
+
* model receives the uploaded files (images and documents) directly.
|
|
79
|
+
*/
|
|
80
|
+
attachments?: LanguageAttachment[];
|
|
50
81
|
/** Return `HTML` (`true`) or raw Markdown (`false`). Default: `true` */
|
|
51
82
|
html?: boolean;
|
|
52
83
|
/** Truncate the prompt to the model's context window length. Default: `true` */
|