@modelprofile.com/flexharness 1.0.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/.smartconfig.json +34 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.flexharness.d.ts +61 -0
- package/dist_ts/classes.flexharness.js +1312 -0
- package/dist_ts/classes.stores.d.ts +26 -0
- package/dist_ts/classes.stores.js +156 -0
- package/dist_ts/errors.d.ts +44 -0
- package/dist_ts/errors.js +105 -0
- package/dist_ts/index.d.ts +5 -0
- package/dist_ts/index.js +6 -0
- package/dist_ts/interfaces.d.ts +288 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/plugins.d.ts +7 -0
- package/dist_ts/plugins.js +9 -0
- package/dist_ts/utils.json.d.ts +8 -0
- package/dist_ts/utils.json.js +540 -0
- package/dist_ts/utils.prompt.d.ts +8 -0
- package/dist_ts/utils.prompt.js +170 -0
- package/license.md +21 -0
- package/package.json +36 -0
- package/readme.hints.md +29 -0
- package/readme.md +285 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.flexharness.ts +1648 -0
- package/ts/classes.stores.ts +195 -0
- package/ts/errors.ts +125 -0
- package/ts/index.ts +5 -0
- package/ts/interfaces.ts +369 -0
- package/ts/plugins.ts +15 -0
- package/ts/utils.json.ts +651 -0
- package/ts/utils.prompt.ts +193 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexFilePromptPart,
|
|
4
|
+
IFlexImagePromptPart,
|
|
5
|
+
TFlexAgentModelMessage,
|
|
6
|
+
TFlexAgentPrompt,
|
|
7
|
+
TFlexPrompt,
|
|
8
|
+
TFlexPromptPart,
|
|
9
|
+
} from './interfaces.js';
|
|
10
|
+
import { cloneSerializable } from './utils.json.js';
|
|
11
|
+
import { assertJsonSerializable } from './utils.json.js';
|
|
12
|
+
|
|
13
|
+
export interface INormalizedFlexPrompt {
|
|
14
|
+
agentPrompt: TFlexAgentPrompt;
|
|
15
|
+
modelMessage: TFlexAgentModelMessage;
|
|
16
|
+
parts: TFlexPromptPart[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const prototype = Object.getPrototypeOf(value);
|
|
24
|
+
return prototype === Object.prototype || prototype === null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requireNonEmptyString(value: unknown, path: string): string {
|
|
28
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
29
|
+
throw new FlexHarnessValidationError(`${path} must be a non-empty string.`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateOptionalString(value: unknown, path: string): string | undefined {
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return requireNonEmptyString(value, path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toAgentData(data: string): string | URL {
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(data);
|
|
44
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Plain base64 and data URLs stay strings at the SmartAgent boundary.
|
|
49
|
+
}
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validatePart(value: unknown, index: number): TFlexPromptPart {
|
|
54
|
+
const path = `prompt[${index}]`;
|
|
55
|
+
if (!isPlainObject(value)) {
|
|
56
|
+
throw new FlexHarnessValidationError(`${path} must be a plain object.`);
|
|
57
|
+
}
|
|
58
|
+
if (value.type === 'text') {
|
|
59
|
+
rejectUnknownKeys(value, ['type', 'text'], path);
|
|
60
|
+
return {
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: requireNonEmptyString(value.text, `${path}.text`),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (value.type === 'image') {
|
|
66
|
+
rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name'], path);
|
|
67
|
+
const part: IFlexImagePromptPart = {
|
|
68
|
+
type: 'image',
|
|
69
|
+
data: requireNonEmptyString(value.data, `${path}.data`),
|
|
70
|
+
};
|
|
71
|
+
const mediaType = validateOptionalString(value.mediaType, `${path}.mediaType`);
|
|
72
|
+
const name = validateOptionalString(value.name, `${path}.name`);
|
|
73
|
+
if (mediaType) part.mediaType = mediaType;
|
|
74
|
+
if (name) part.name = name;
|
|
75
|
+
return part;
|
|
76
|
+
}
|
|
77
|
+
if (value.type === 'file') {
|
|
78
|
+
rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name'], path);
|
|
79
|
+
const part: IFlexFilePromptPart = {
|
|
80
|
+
type: 'file',
|
|
81
|
+
data: requireNonEmptyString(value.data, `${path}.data`),
|
|
82
|
+
mediaType: requireNonEmptyString(value.mediaType, `${path}.mediaType`),
|
|
83
|
+
};
|
|
84
|
+
const name = validateOptionalString(value.name, `${path}.name`);
|
|
85
|
+
if (name) part.name = name;
|
|
86
|
+
return part;
|
|
87
|
+
}
|
|
88
|
+
throw new FlexHarnessValidationError(`${path}.type must be text, image, or file.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rejectUnknownKeys(
|
|
92
|
+
value: Record<string, unknown>,
|
|
93
|
+
allowedKeys: string[],
|
|
94
|
+
path: string,
|
|
95
|
+
): void {
|
|
96
|
+
const unknownKey = Object.keys(value).find((key) => !allowedKeys.includes(key));
|
|
97
|
+
if (unknownKey) {
|
|
98
|
+
throw new FlexHarnessValidationError(`${path}.${unknownKey} is not a supported prompt field.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toAgentParts(parts: TFlexPromptPart[]): TFlexAgentPrompt {
|
|
103
|
+
return parts.map((part) => {
|
|
104
|
+
if (part.type === 'text') {
|
|
105
|
+
return { type: 'text' as const, text: part.text };
|
|
106
|
+
}
|
|
107
|
+
if (part.type === 'image') {
|
|
108
|
+
return {
|
|
109
|
+
type: 'image' as const,
|
|
110
|
+
image: toAgentData(part.data),
|
|
111
|
+
...(part.mediaType ? { mediaType: part.mediaType } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
type: 'file' as const,
|
|
116
|
+
data: toAgentData(part.data),
|
|
117
|
+
mediaType: part.mediaType,
|
|
118
|
+
...(part.name ? { filename: part.name } : {}),
|
|
119
|
+
};
|
|
120
|
+
}) as TFlexAgentPrompt;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toStoredModelParts(parts: TFlexPromptPart[]): TFlexAgentPrompt {
|
|
124
|
+
return parts.map((part) => {
|
|
125
|
+
if (part.type === 'text') {
|
|
126
|
+
return { type: 'text' as const, text: part.text };
|
|
127
|
+
}
|
|
128
|
+
if (part.type === 'image') {
|
|
129
|
+
return {
|
|
130
|
+
type: 'image' as const,
|
|
131
|
+
image: part.data,
|
|
132
|
+
...(part.mediaType ? { mediaType: part.mediaType } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
type: 'file' as const,
|
|
137
|
+
data: part.data,
|
|
138
|
+
mediaType: part.mediaType,
|
|
139
|
+
...(part.name ? { filename: part.name } : {}),
|
|
140
|
+
};
|
|
141
|
+
}) as TFlexAgentPrompt;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function normalizeFlexPrompt(prompt: TFlexPrompt): INormalizedFlexPrompt {
|
|
145
|
+
try {
|
|
146
|
+
assertJsonSerializable(prompt, '$prompt');
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new FlexHarnessValidationError('prompt must contain only JSON-safe values.', {
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (typeof prompt === 'string') {
|
|
153
|
+
if (prompt.length === 0) {
|
|
154
|
+
throw new FlexHarnessValidationError('prompt must not be empty.');
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
agentPrompt: prompt,
|
|
158
|
+
modelMessage: { role: 'user', content: prompt },
|
|
159
|
+
parts: [{ type: 'text', text: prompt }],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(prompt) || prompt.length === 0) {
|
|
163
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
164
|
+
}
|
|
165
|
+
const parts = prompt.map(validatePart);
|
|
166
|
+
return {
|
|
167
|
+
agentPrompt: toAgentParts(parts),
|
|
168
|
+
modelMessage: {
|
|
169
|
+
role: 'user',
|
|
170
|
+
content: toStoredModelParts(parts),
|
|
171
|
+
},
|
|
172
|
+
parts: cloneSerializable(parts),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function hydrateAgentMessages(
|
|
177
|
+
messages: TFlexAgentModelMessage[],
|
|
178
|
+
): TFlexAgentModelMessage[] {
|
|
179
|
+
const hydrated = cloneSerializable(messages);
|
|
180
|
+
for (const message of hydrated) {
|
|
181
|
+
if (!Array.isArray(message.content)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const part of message.content) {
|
|
185
|
+
if (message.role === 'user' && part.type === 'image' && typeof part.image === 'string') {
|
|
186
|
+
part.image = toAgentData(part.image);
|
|
187
|
+
} else if (part.type === 'file' && typeof part.data === 'string') {
|
|
188
|
+
part.data = toAgentData(part.data);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return hydrated;
|
|
193
|
+
}
|