@qualve/ai 0.0.1 → 0.1.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/package.json +13 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -17
- package/core/README.md +0 -74
- package/core/package.json +0 -28
- package/core/src/index.js +0 -1
- package/core/src/prompts.js +0 -83
- package/core/src/types/llm.js +0 -404
- package/core/src/util.js +0 -111
- package/providers/anthropic/README.md +0 -52
- package/providers/anthropic/package.json +0 -29
- package/providers/anthropic/src/index.js +0 -164
- package/providers/googleai/README.md +0 -54
- package/providers/googleai/package.json +0 -29
- package/providers/googleai/src/index.js +0 -210
- package/providers/openai/README.md +0 -53
- package/providers/openai/package.json +0 -29
- package/providers/openai/src/index.js +0 -182
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qualve/ai",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "LLM task plugin for Qualve, with adapters for Gemini, OpenAI, and Claude.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -20,12 +20,21 @@
|
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/qualve/ai.git"
|
|
23
|
+
"url": "git+https://github.com/qualve/ai.git"
|
|
24
24
|
},
|
|
25
25
|
"contributors": [
|
|
26
26
|
"Dmitry Sharabin",
|
|
27
27
|
"Lea Verou"
|
|
28
28
|
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"release": "npm login && release-it"
|
|
31
|
+
},
|
|
32
|
+
"release-it": {
|
|
33
|
+
"git": {
|
|
34
|
+
"tagName": "${npm.name}@${version}",
|
|
35
|
+
"commitMessage": "Release ${npm.name}@${version}"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
29
38
|
"peerDependencies": {
|
|
30
39
|
"qualve": "*"
|
|
31
40
|
},
|
|
@@ -33,7 +42,8 @@
|
|
|
33
42
|
"prettier": "^3.8.1",
|
|
34
43
|
"prettier-plugin-brace-style": "^0.10.0",
|
|
35
44
|
"prettier-plugin-merge": "^0.10.0",
|
|
36
|
-
"prettier-plugin-space-before-function-paren": "^0.1.0"
|
|
45
|
+
"prettier-plugin-space-before-function-paren": "^0.1.0",
|
|
46
|
+
"release-it": "^20.0.0"
|
|
37
47
|
},
|
|
38
48
|
"dependencies": {
|
|
39
49
|
"@qualve/llm": "*",
|
package/.prettierignore
DELETED
package/.prettierrc
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"plugins": [
|
|
3
|
-
"prettier-plugin-brace-style",
|
|
4
|
-
"prettier-plugin-space-before-function-paren",
|
|
5
|
-
"prettier-plugin-merge"
|
|
6
|
-
],
|
|
7
|
-
"braceStyle": "stroustrup",
|
|
8
|
-
"arrowParens": "avoid",
|
|
9
|
-
"bracketSpacing": true,
|
|
10
|
-
"endOfLine": "auto",
|
|
11
|
-
"semi": true,
|
|
12
|
-
"singleQuote": false,
|
|
13
|
-
"tabWidth": 4,
|
|
14
|
-
"useTabs": true,
|
|
15
|
-
"trailingComma": "all",
|
|
16
|
-
"printWidth": 100
|
|
17
|
-
}
|
package/core/README.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# @qualve/llm
|
|
2
|
-
|
|
3
|
-
Core LLM task framework for [Qualve](https://npmjs.com/package/qualve).
|
|
4
|
-
Provides the `LLMTask` base class that all LLM provider adapters extend.
|
|
5
|
-
|
|
6
|
-
If you want all providers out of the box, install [@qualve/llms](https://npmjs.com/package/@qualve/llms) instead.
|
|
7
|
-
|
|
8
|
-
## Setup
|
|
9
|
-
|
|
10
|
-
Requires **Node.js v23+**.
|
|
11
|
-
|
|
12
|
-
```sh
|
|
13
|
-
npm install @qualve/llm
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
Import alongside a provider to register the `llm` task type:
|
|
19
|
-
|
|
20
|
-
```js
|
|
21
|
-
import "@qualve/anthropic"; // or @qualve/openai, @qualve/googleai
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Writing a custom provider
|
|
25
|
-
|
|
26
|
-
```js
|
|
27
|
-
import { LLMTask } from "@qualve/llm";
|
|
28
|
-
|
|
29
|
-
export default class MyProvider extends LLMTask {
|
|
30
|
-
static id = "my-provider";
|
|
31
|
-
static name = "My Provider";
|
|
32
|
-
static models = ["my-model-v1"];
|
|
33
|
-
static capabilities = {};
|
|
34
|
-
|
|
35
|
-
// Required: implement these abstract methods
|
|
36
|
-
async uploadFile (filepath, { mimeType, contents }) { /* ... */ }
|
|
37
|
-
async getFile (filepath) { /* ... */ }
|
|
38
|
-
async deleteFile (filepath) { /* ... */ }
|
|
39
|
-
async listFiles () { /* ... */ }
|
|
40
|
-
async createStream () { /* ... */ }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
LLMTask.register(MyProvider);
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## API
|
|
47
|
-
|
|
48
|
-
### `LLMTask`
|
|
49
|
-
|
|
50
|
-
Extends the base Qualve `Task` class with LLM-specific functionality:
|
|
51
|
-
|
|
52
|
-
- **Provider dispatch** — `LLMTask.create()` routes to the registered provider based on `task.llm`
|
|
53
|
-
- **File management** — Upload, retrieve, and manage files on the provider
|
|
54
|
-
- **Streaming** — `handleStream()` writes streamed responses to disk with backpressure handling
|
|
55
|
-
- **Prompt helpers** — `this.inputFile()`, `this.inputFiles()`, `this.outputFile()` generate prompt text describing task I/O
|
|
56
|
-
- **Thinking levels** — Normalized across providers via `thinkingLevels` and per-provider `levelMap`
|
|
57
|
-
- **Stop reasons** — Normalized stop reasons (`COMPLETE`, `MAX_TOKENS`, `ABORTED`, `UNKNOWN`)
|
|
58
|
-
|
|
59
|
-
### Abstract methods (providers must implement)
|
|
60
|
-
|
|
61
|
-
| Method | Description |
|
|
62
|
-
| --- | --- |
|
|
63
|
-
| `uploadFile(filepath, { mimeType, contents })` | Upload data to the provider |
|
|
64
|
-
| `getFile(filepath)` | Retrieve a previously uploaded file, or `null` |
|
|
65
|
-
| `deleteFile(filepath)` | Delete a previously uploaded file |
|
|
66
|
-
| `listFiles()` | List all uploaded files |
|
|
67
|
-
| `createStream()` | Create the streaming API call; returns `{ stream, transformChunk, onChunk?, onFinish? }` |
|
|
68
|
-
|
|
69
|
-
### Optional overrides
|
|
70
|
-
|
|
71
|
-
| Method | Description |
|
|
72
|
-
| --- | --- |
|
|
73
|
-
| `getStatus(chunk)` | Extract a human-readable status from a streaming chunk |
|
|
74
|
-
| `countTokens()` | Count input tokens for a dry run |
|
package/core/package.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@qualve/llm",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"description": "Core LLM task framework for Qualve.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=23"
|
|
8
|
-
},
|
|
9
|
-
"main": "src/index.js",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": "./src/index.js"
|
|
12
|
-
},
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "https://github.com/qualve/ai.git",
|
|
16
|
-
"directory": "core"
|
|
17
|
-
},
|
|
18
|
-
"author": "Lea Verou",
|
|
19
|
-
"contributors": [
|
|
20
|
-
"Dmitry Sharabin"
|
|
21
|
-
],
|
|
22
|
-
"peerDependencies": {
|
|
23
|
-
"qualve": "*"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"dedent": "^1.7.2"
|
|
27
|
-
}
|
|
28
|
-
}
|
package/core/src/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as LLMTask } from "./types/llm.js";
|
package/core/src/prompts.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Describe a single input file for inclusion in the prompt.
|
|
5
|
-
* Called with `this` bound to the LLMTask instance.
|
|
6
|
-
* @param {object} file
|
|
7
|
-
* @returns {string}
|
|
8
|
-
*/
|
|
9
|
-
export function inputFile (file) {
|
|
10
|
-
let ret = [];
|
|
11
|
-
|
|
12
|
-
if (file.description && !this.capabilities.inputDescriptions) {
|
|
13
|
-
ret.push(`containing ${file.description}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (file.schema && !this.capabilities.inputSchema) {
|
|
17
|
-
ret.push(`follows the JSON schema: ${JSON.stringify(file.schema, null, "\t")}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
ret = ret.join(" and ");
|
|
21
|
-
|
|
22
|
-
if (!ret.startsWith("containing")) {
|
|
23
|
-
ret = "which " + ret;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
ret = `\`${path.basename(file.filePath)}\` ${ret}.`;
|
|
27
|
-
|
|
28
|
-
if (file.schema) {
|
|
29
|
-
ret += "\nRead the field descriptions in the JSON schema for details on each field.";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return ret;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Describe all input files for inclusion in the prompt.
|
|
37
|
-
* Called with `this` bound to the LLMTask instance.
|
|
38
|
-
* @param {Array} files
|
|
39
|
-
* @returns {string}
|
|
40
|
-
*/
|
|
41
|
-
export function inputFiles (files) {
|
|
42
|
-
if (files.length === 0) {
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return `I provide the contents of the following files:
|
|
47
|
-
${files.map(file => inputFile.call(this, file)).join("\n")}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Describe the expected output file for inclusion in the prompt.
|
|
52
|
-
* Called with `this` bound to the LLMTask instance.
|
|
53
|
-
* @param {object} file
|
|
54
|
-
* @returns {string}
|
|
55
|
-
*/
|
|
56
|
-
export function outputFile (file) {
|
|
57
|
-
let ret = [`Produce a JSON file that I’m going to save as \`${path.basename(file.filePath)}\``];
|
|
58
|
-
|
|
59
|
-
if (file.description && !this.capabilities.outputDescriptions) {
|
|
60
|
-
ret.push(`containing ${file.description}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (file.schema && !this.capabilities.outputSchema) {
|
|
64
|
-
ret.push(`following the JSON schema: ${JSON.stringify(file.schema, null, "\t")}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (ret.length <= 1) {
|
|
68
|
-
ret = ret[0];
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
if (ret.length === 3) {
|
|
72
|
-
ret[2] = "and " + ret[2];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
ret = ret.join(", ");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (file.schema) {
|
|
79
|
-
ret += ".\nRead the field descriptions in the JSON schema for details on each field.";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return ret;
|
|
83
|
-
}
|
package/core/src/types/llm.js
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { loadEnvFile } from "node:process";
|
|
5
|
-
import Task from "qualve/task";
|
|
6
|
-
import { ProgressIndicator, addFilenameSuffix, readJSONSync } from "qualve/util";
|
|
7
|
-
import { handleStream, dedent } from "../util.js";
|
|
8
|
-
import * as prompts from "../prompts.js";
|
|
9
|
-
import options from "qualve/options";
|
|
10
|
-
|
|
11
|
-
Object.assign(options, {
|
|
12
|
-
// Options specific to LLM tasks
|
|
13
|
-
llm: {},
|
|
14
|
-
model: {},
|
|
15
|
-
thinking: {},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export default class LLMTask extends Task {
|
|
19
|
-
// Subclass must define these
|
|
20
|
-
client = null;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Normalized stop reason vocabulary, consistent across all providers.
|
|
24
|
-
* Each provider subclass maps its own provider-specific stop states to these values.
|
|
25
|
-
*
|
|
26
|
-
* - `COMPLETE` — The model finished generating its full response.
|
|
27
|
-
* - `MAX_TOKENS` — The response was truncated because it hit the output token limit.
|
|
28
|
-
* - `ABORTED` — The provider refused to produce output (safety, policy, PII, etc.).
|
|
29
|
-
* - `UNKNOWN` — The provider returned a stop state we don't have a mapping for.
|
|
30
|
-
* @enum {string}
|
|
31
|
-
*/
|
|
32
|
-
static stopReasons = {
|
|
33
|
-
COMPLETE: "complete",
|
|
34
|
-
MAX_TOKENS: "max_tokens",
|
|
35
|
-
ABORTED: "aborted",
|
|
36
|
-
UNKNOWN: "unknown",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Canonical ordered list of all thinking levels across all providers.
|
|
41
|
-
* Subclasses use {@link levelMap} to remap levels they don't natively support.
|
|
42
|
-
*/
|
|
43
|
-
static thinkingLevels = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
44
|
-
|
|
45
|
-
static type = "llm";
|
|
46
|
-
static capabilities = {};
|
|
47
|
-
|
|
48
|
-
static #registry = new Map();
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Register an LLM provider so LLMTask.create() can dispatch to it by `task.llm`.
|
|
52
|
-
* Reads `SubClass.id` as the registry key.
|
|
53
|
-
* Each provider calls this after its own definition to self-register.
|
|
54
|
-
* @param {typeof LLMTask} SubClass
|
|
55
|
-
*/
|
|
56
|
-
static register (SubClass) {
|
|
57
|
-
LLMTask.#registry.set(SubClass.id, SubClass);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Select and instantiate the right provider subclass based on `task.llm`.
|
|
62
|
-
* Overrides the base Task.create factory to add provider dispatch.
|
|
63
|
-
*/
|
|
64
|
-
static create (task, ...args) {
|
|
65
|
-
let id = task.llm ?? "gemini";
|
|
66
|
-
let Provider = LLMTask.#registry.get(id);
|
|
67
|
-
if (!Provider) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Unknown LLM provider: "${id}". Available: ${[...LLMTask.#registry.keys()].join(", ")}`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
return new Provider(task, ...args);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
get capabilities () {
|
|
76
|
-
return this.constructor.capabilities;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Provider display name (e.g., "Gemini"). */
|
|
80
|
-
get name () {
|
|
81
|
-
return this.constructor.name;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Provider ID string (e.g., "gemini", "claude").
|
|
86
|
-
* This getter also prevents the Task constructor from overwriting `this.llm`
|
|
87
|
-
* with the raw string from task config data.
|
|
88
|
-
*/
|
|
89
|
-
get llm () {
|
|
90
|
-
return this.constructor.id;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* LLM tasks default to sequential for question expansion and bounded for batch,
|
|
95
|
-
* to avoid overwhelming the provider API.
|
|
96
|
-
* Task definitions can override this by setting `concurrency` explicitly.
|
|
97
|
-
*/
|
|
98
|
-
get concurrency () {
|
|
99
|
-
return this.task.concurrency ?? (this.batched ? 5 : 1);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
constructor (task, args) {
|
|
103
|
-
super(task, args);
|
|
104
|
-
|
|
105
|
-
if (existsSync(".env")) {
|
|
106
|
-
loadEnvFile(".env");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Default to the first model in the provider's list, also falling back
|
|
110
|
-
// if the task's hardcoded model isn't supported by this provider (e.g., when switching providers via --llm).
|
|
111
|
-
if (!this.constructor.models.includes(this.model)) {
|
|
112
|
-
this.model = this.constructor.models[0];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Normalize thinking level for this provider, or unset it if not supported.
|
|
116
|
-
if (
|
|
117
|
-
this.constructor.capabilities.thinkingLevel &&
|
|
118
|
-
LLMTask.thinkingLevels.includes(this.thinking)
|
|
119
|
-
) {
|
|
120
|
-
this.thinking = this.constructor.levelMap?.[this.thinking] ?? this.thinking;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
this.thinking = undefined;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
Object.assign(this.debug, {
|
|
127
|
-
llm: this.name,
|
|
128
|
-
model: this.model,
|
|
129
|
-
thinking: this.thinking,
|
|
130
|
-
itemsPerPage: this.itemsPerPage,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Read a file if no contents are provided and prepare it for upload.
|
|
136
|
-
* Mainly intended to be used internally.
|
|
137
|
-
* @protected
|
|
138
|
-
* @param {string} filepath
|
|
139
|
-
* @param {object} [options]
|
|
140
|
-
* @param {string} [options.mimeType="application/json"] - The MIME type of the file.
|
|
141
|
-
* @param {string|object|Array} [options.contents] - The file contents to upload. Reads filepath if not provided.
|
|
142
|
-
*/
|
|
143
|
-
readFile (filepath, options = {}) {
|
|
144
|
-
options.mimeType ??= "application/json";
|
|
145
|
-
let isJSON = options.mimeType === "application/json";
|
|
146
|
-
options.contents ??= isJSON ? readJSONSync(filepath) : readFileSync(filepath);
|
|
147
|
-
|
|
148
|
-
if (isJSON && typeof options.contents !== "string") {
|
|
149
|
-
options.contents = JSON.stringify(options.contents, (k, v) => v ?? undefined);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return options;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Read a file, prepare its contents, and upload it to the provider.
|
|
157
|
-
* For JSON files, this minifies the data and strips nulls to reduce token usage.
|
|
158
|
-
* @param {string} filepath
|
|
159
|
-
* @param {object} [options]
|
|
160
|
-
* @param {string} [options.mimeType="application/json"] - The MIME type of the file.
|
|
161
|
-
* @param {string|object|Array} [options.contents] - The file contents to upload. Reads filepath if not provided.
|
|
162
|
-
*/
|
|
163
|
-
sendData (filepath, options = {}) {
|
|
164
|
-
options = this.readFile(filepath, options);
|
|
165
|
-
return this.uploadFile(filepath, options);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
/**
|
|
170
|
-
* Resolve a local filepath to a stable remote filename, namespaced by entity.
|
|
171
|
-
* @param {string} filepath
|
|
172
|
-
* @returns {{ name: string, dirName: string }}
|
|
173
|
-
*/
|
|
174
|
-
getFileInfo (filepath) {
|
|
175
|
-
// FIXME there is an implicit assumption here that dirName is equal to an id, which is not always the case
|
|
176
|
-
let dirName = path.basename(path.dirname(filepath));
|
|
177
|
-
let prefix = this.entityModel?.truncatedIds?.[dirName];
|
|
178
|
-
let name = path.basename(filepath);
|
|
179
|
-
|
|
180
|
-
// Make sure the filename is unique per entity by prefixing it with the truncated parent directory name.
|
|
181
|
-
// For other files (e.g., shared data), no prefix is needed since they are already unique.
|
|
182
|
-
name = (prefix ? prefix + "-" : "") + name;
|
|
183
|
-
return { name, dirName };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Ensure a file is available on the provider, uploading it if necessary.
|
|
188
|
-
* Freshness is determined by the file-level `fresh` option, falling back to the task-level `this.fresh`.
|
|
189
|
-
* @param {string} filepath
|
|
190
|
-
* @param {object} [options]
|
|
191
|
-
* @param {string|object|Array} [options.contents] - In-memory file contents. Skips disk read when provided.
|
|
192
|
-
* @param {boolean} [options.fresh] - Force re-upload for this specific file.
|
|
193
|
-
*/
|
|
194
|
-
async getRemoteFile (filepath, options = {}) {
|
|
195
|
-
let fresh = options.fresh ?? this.fresh;
|
|
196
|
-
|
|
197
|
-
if (fresh) {
|
|
198
|
-
this.info(`Removing previously uploaded file ${filepath} ...`);
|
|
199
|
-
await this.deleteFile(filepath);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
let ret = !fresh ? await this.getFile(filepath) : null;
|
|
203
|
-
if (!ret) {
|
|
204
|
-
this.info(`Uploading ${filepath} ...`);
|
|
205
|
-
ret = await this.sendData(filepath, options);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this.info(`Source file ${filepath} ready`);
|
|
209
|
-
return ret;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Ensure all input files are available on the provider, populating `entry.remoteFile`.
|
|
214
|
-
* @param {Array} input
|
|
215
|
-
*/
|
|
216
|
-
async getRemoteFiles (input) {
|
|
217
|
-
await Promise.all(
|
|
218
|
-
input.map(async entry => {
|
|
219
|
-
entry.remoteFile ??= await this.getRemoteFile(entry.filePath, {
|
|
220
|
-
contents: entry.contents,
|
|
221
|
-
fresh: entry.fresh,
|
|
222
|
-
});
|
|
223
|
-
}),
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Abstract — subclasses must override
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Low-level: upload data to the provider.
|
|
231
|
-
* Use {@link sendData} for most things instead.
|
|
232
|
-
* @protected
|
|
233
|
-
* @param {string} filepath
|
|
234
|
-
* @param {object} options
|
|
235
|
-
* @param {string} options.contents - The file contents to upload.
|
|
236
|
-
* @param {string} options.mimeType - The MIME type of the file.
|
|
237
|
-
*/
|
|
238
|
-
uploadFile (filepath, options) {
|
|
239
|
-
throw this.notImplemented();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** Retrieve a previously uploaded file from the provider, or null if not found. */
|
|
243
|
-
getFile (filepath) {
|
|
244
|
-
throw this.notImplemented();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Delete a previously uploaded file from the provider. */
|
|
248
|
-
deleteFile (filepath) {
|
|
249
|
-
throw this.notImplemented();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** List all files currently uploaded to the provider. */
|
|
253
|
-
listFiles () {
|
|
254
|
-
throw this.notImplemented();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Create the streaming API call for this task. Returns stream + handler callbacks. */
|
|
258
|
-
createStream () {
|
|
259
|
-
throw this.notImplemented();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// To be overridden
|
|
263
|
-
|
|
264
|
-
/** Extract a human-readable status message from a streaming chunk. */
|
|
265
|
-
getStatus (chunk) {}
|
|
266
|
-
|
|
267
|
-
/** Count the total input tokens for this task. Returns undefined if unsupported. */
|
|
268
|
-
async countTokens () {}
|
|
269
|
-
|
|
270
|
-
/** Describe a single input file for inclusion in the prompt. */
|
|
271
|
-
inputFile (file) {
|
|
272
|
-
return prompts.inputFile.call(this, file);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Describe all input files for inclusion in the prompt. */
|
|
276
|
-
inputFiles (files) {
|
|
277
|
-
return prompts.inputFiles.call(this, files);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Describe the expected output file for inclusion in the prompt. */
|
|
281
|
-
outputFile (file) {
|
|
282
|
-
return prompts.outputFile.call(this, file);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Normalize a prompts value to a flat array of strings.
|
|
287
|
-
* Accepts a string, array, or function (called with the current entity).
|
|
288
|
-
*/
|
|
289
|
-
normalizePrompts (prompts) {
|
|
290
|
-
if (!prompts) {
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (typeof prompts === "function") {
|
|
295
|
-
prompts = prompts.call(this, this.entity);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Do not convert to else if, function may return a string or an array
|
|
299
|
-
if (typeof prompts === "string") {
|
|
300
|
-
prompts = dedent(prompts);
|
|
301
|
-
}
|
|
302
|
-
else if (Array.isArray(prompts)) {
|
|
303
|
-
return prompts.flatMap(prompt => this.normalizePrompts(prompt));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return [prompts];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async postInit () {
|
|
310
|
-
await super.postInit();
|
|
311
|
-
|
|
312
|
-
this.system = this.normalizePrompts(this.system);
|
|
313
|
-
this.prompt = this.normalizePrompts(this.prompt);
|
|
314
|
-
|
|
315
|
-
const capabilities = this.capabilities;
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
this.input?.length > 0 &&
|
|
319
|
-
!capabilities.inputSchema &&
|
|
320
|
-
!capabilities.inputDescriptions
|
|
321
|
-
) {
|
|
322
|
-
// Incorporate file descriptions and schemas into the prompt
|
|
323
|
-
this.prompt.push(this.inputFiles(this.input));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (this.output) {
|
|
327
|
-
this.prompt.push(this.outputFile(this.output));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
Object.assign(this.debug, { system: this.system, prompt: this.prompt });
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async runTask () {
|
|
334
|
-
if (this.dryRun) {
|
|
335
|
-
if (!this.batched) {
|
|
336
|
-
this.debug.tokens = await this.countTokens();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (this.input) {
|
|
343
|
-
await this.getRemoteFiles(this.input);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// If no progress indicator exists (standalone task, not part of a multiple run),
|
|
347
|
-
// install one so chunk status gets logUpdate-based in-place display.
|
|
348
|
-
// Subtasks in a multiple run already have an indicator (child of the coordinator's).
|
|
349
|
-
let ownedIndicator = !this.progressIndicator;
|
|
350
|
-
if (ownedIndicator) {
|
|
351
|
-
this.progressIndicator = new ProgressIndicator({
|
|
352
|
-
status: `${this.title} with ${this.name}...`,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const streamParams = await this.createStream();
|
|
357
|
-
let chunksReceived = 0;
|
|
358
|
-
|
|
359
|
-
let text;
|
|
360
|
-
try {
|
|
361
|
-
text = await handleStream({
|
|
362
|
-
...streamParams,
|
|
363
|
-
outputPath: this.output?.filePath,
|
|
364
|
-
onChunk: chunk => {
|
|
365
|
-
chunksReceived++;
|
|
366
|
-
let status = this.getStatus(chunk);
|
|
367
|
-
status = status
|
|
368
|
-
? `Chunk ${chunksReceived}: ${status}`
|
|
369
|
-
: `${chunksReceived} chunks received...`;
|
|
370
|
-
this.info(status);
|
|
371
|
-
|
|
372
|
-
// The explicit onChunk above shadows the one from streamParams, so call it manually.
|
|
373
|
-
streamParams.onChunk?.(chunk);
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
catch (e) {
|
|
378
|
-
// var hoists `error` out of the catch block so it's accessible after the finally.
|
|
379
|
-
var error = e;
|
|
380
|
-
}
|
|
381
|
-
finally {
|
|
382
|
-
if (ownedIndicator) {
|
|
383
|
-
this.progressIndicator?.stop();
|
|
384
|
-
this.progressIndicator = null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
let outputPath = this.output?.filePath;
|
|
389
|
-
|
|
390
|
-
if (outputPath && error) {
|
|
391
|
-
outputPath = addFilenameSuffix(outputPath, ".tmp");
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
outputPath,
|
|
396
|
-
size: chunksReceived,
|
|
397
|
-
sizeUnit: "chunk",
|
|
398
|
-
error,
|
|
399
|
-
result: text,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
Task.register(LLMTask);
|