mdi-llmkit 0.1.0 → 1.0.1

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.
Files changed (36) hide show
  1. package/README.md +116 -34
  2. package/dist/src/comparison/compareLists.d.ts +97 -0
  3. package/dist/src/comparison/compareLists.js +375 -0
  4. package/dist/src/comparison/index.d.ts +1 -0
  5. package/dist/src/comparison/index.js +1 -0
  6. package/dist/src/gptApi/functions.d.ts +21 -0
  7. package/dist/src/gptApi/functions.js +154 -0
  8. package/dist/src/gptApi/gptConversation.d.ts +43 -0
  9. package/dist/src/gptApi/gptConversation.js +146 -0
  10. package/dist/src/gptApi/index.d.ts +3 -0
  11. package/dist/src/gptApi/index.js +3 -0
  12. package/dist/src/gptApi/jsonSchemaFormat.d.ts +14 -0
  13. package/dist/src/gptApi/jsonSchemaFormat.js +198 -0
  14. package/dist/src/index.d.ts +3 -0
  15. package/dist/src/index.js +3 -0
  16. package/dist/src/jsonSurgery/jsonSurgery.d.ts +81 -0
  17. package/dist/src/jsonSurgery/jsonSurgery.js +776 -0
  18. package/dist/src/jsonSurgery/placemarkedJSON.d.ts +57 -0
  19. package/dist/src/jsonSurgery/placemarkedJSON.js +151 -0
  20. package/dist/tests/comparison/compareLists.test.d.ts +1 -0
  21. package/dist/tests/comparison/compareLists.test.js +434 -0
  22. package/dist/tests/gptApi/gptConversation.test.d.ts +1 -0
  23. package/dist/tests/gptApi/gptConversation.test.js +157 -0
  24. package/dist/tests/gptApi/gptSubmit.test.d.ts +1 -0
  25. package/dist/tests/gptApi/gptSubmit.test.js +161 -0
  26. package/dist/tests/gptApi/jsonSchemaFormat.test.d.ts +1 -0
  27. package/dist/tests/gptApi/jsonSchemaFormat.test.js +372 -0
  28. package/dist/tests/jsonSurgery/jsonSurgery.test.d.ts +1 -0
  29. package/dist/tests/jsonSurgery/jsonSurgery.test.js +729 -0
  30. package/dist/tests/jsonSurgery/placemarkedJSON.test.d.ts +1 -0
  31. package/dist/tests/jsonSurgery/placemarkedJSON.test.js +209 -0
  32. package/dist/tests/setupEnv.d.ts +1 -0
  33. package/dist/tests/setupEnv.js +4 -0
  34. package/dist/tests/subpathExports.test.d.ts +1 -0
  35. package/dist/tests/subpathExports.test.js +47 -0
  36. package/package.json +18 -5
@@ -0,0 +1 @@
1
+ export * from './compareLists.js';
@@ -0,0 +1 @@
1
+ export * from './compareLists.js';
@@ -0,0 +1,21 @@
1
+ export declare const GPT_MODEL_CHEAP = "gpt-4.1-nano";
2
+ export declare const GPT_MODEL_SMART = "gpt-4.1";
3
+ export interface OpenAIClientLike {
4
+ responses: {
5
+ create: (args: any, options?: any) => Promise<any> | any;
6
+ };
7
+ }
8
+ export interface SystemMessage {
9
+ role: "system";
10
+ content: string;
11
+ }
12
+ export interface GptSubmitOptions {
13
+ model?: string;
14
+ jsonResponse?: boolean | Record<string, unknown> | string;
15
+ systemAnnouncementMessage?: string;
16
+ retryLimit?: number;
17
+ retryBackoffTimeSeconds?: number;
18
+ warningCallback?: (message: string) => void;
19
+ }
20
+ export declare function currentDatetimeSystemMessage(): SystemMessage;
21
+ export declare function gptSubmit(messages: unknown[], openaiClient: OpenAIClientLike, options?: GptSubmitOptions): Promise<string | Record<string, unknown> | unknown[] | number | boolean | null>;
@@ -0,0 +1,154 @@
1
+ export const GPT_MODEL_CHEAP = "gpt-4.1-nano";
2
+ export const GPT_MODEL_SMART = "gpt-4.1";
3
+ const GPT_RETRY_LIMIT_DEFAULT = 5;
4
+ const GPT_RETRY_BACKOFF_TIME_SECONDS_DEFAULT = 30;
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+ /**
12
+ * OpenAI's API, even when called with a JSON schema, will often return text that is not
13
+ * valid JSON. It's often because the model will add extra text after the end of its valid
14
+ * JSON response. E.g. instead of `{"foo":"bar"}`, it will sometimes return
15
+ * `{"foo":"bar"}{"baz":"quux"}`.
16
+ *
17
+ * We intentionally do not implement a custom JSON parser here. Instead:
18
+ * 1) Try to parse the full text first (fast path, most responses).
19
+ * 2) If that fails, scan prefixes from start to end and let JSON.parse decide validity.
20
+ *
21
+ * This keeps JSON semantics delegated to the platform parser while still recovering from
22
+ * trailing junk in model output.
23
+ *
24
+ * @param input The text to parse.
25
+ * @returns The first valid JSON value found at the start of the input text.
26
+ * @throws {SyntaxError} If no valid JSON prefix exists.
27
+ */
28
+ function parseFirstJsonValue(input) {
29
+ const text = input.trimStart();
30
+ if (!text) {
31
+ throw new SyntaxError("Unexpected end of JSON input");
32
+ }
33
+ try {
34
+ return JSON.parse(text);
35
+ }
36
+ catch {
37
+ for (let end = 1; end <= text.length; end += 1) {
38
+ try {
39
+ return JSON.parse(text.slice(0, end));
40
+ }
41
+ catch {
42
+ // Keep scanning until we find a valid JSON prefix.
43
+ }
44
+ }
45
+ }
46
+ throw new SyntaxError("Unexpected token in JSON input");
47
+ }
48
+ function isRetryableOpenAIError(error) {
49
+ if (!(error instanceof Error)) {
50
+ return false;
51
+ }
52
+ const name = error.name || "";
53
+ return name.includes("OpenAI") || name.includes("APIError");
54
+ }
55
+ export function currentDatetimeSystemMessage() {
56
+ const now = new Date();
57
+ const pad = (value) => value.toString().padStart(2, "0");
58
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
59
+ `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
60
+ return {
61
+ role: "system",
62
+ content: `!DATETIME: The current date and time is ${timestamp}`,
63
+ };
64
+ }
65
+ export async function gptSubmit(messages, openaiClient, options = {}) {
66
+ const model = options.model || GPT_MODEL_SMART;
67
+ const retryLimit = options.retryLimit ?? GPT_RETRY_LIMIT_DEFAULT;
68
+ const retryBackoffTimeSeconds = options.retryBackoffTimeSeconds ?? GPT_RETRY_BACKOFF_TIME_SECONDS_DEFAULT;
69
+ let failedError = null;
70
+ let openaiTextParam;
71
+ if (options.jsonResponse) {
72
+ if (typeof options.jsonResponse === "boolean") {
73
+ openaiTextParam = { format: { type: "json_object" } };
74
+ }
75
+ else if (typeof options.jsonResponse === "string") {
76
+ openaiTextParam = JSON.parse(options.jsonResponse);
77
+ }
78
+ else if (isRecord(options.jsonResponse)) {
79
+ openaiTextParam = JSON.parse(JSON.stringify(options.jsonResponse));
80
+ const format = openaiTextParam.format;
81
+ if (isRecord(format) && typeof format.description === "string") {
82
+ format.description =
83
+ `${format.description}\n\nABSOLUTELY NO UNICODE ALLOWED. ` +
84
+ `Only use typeable keyboard characters. Do not try to circumvent this rule ` +
85
+ `with escape sequences, backslashes, or other tricks. Use double dashes (--), ` +
86
+ `straight quotes (") and single quotes (') instead of em-dashes, en-dashes, ` +
87
+ `and curly versions.`.trim();
88
+ }
89
+ }
90
+ }
91
+ const filteredMessages = messages.filter((message) => {
92
+ if (!isRecord(message)) {
93
+ return true;
94
+ }
95
+ const role = message.role;
96
+ const content = message.content;
97
+ return !(role === "system" &&
98
+ typeof content === "string" &&
99
+ content.startsWith("!DATETIME:"));
100
+ });
101
+ let preparedMessages = [currentDatetimeSystemMessage(), ...filteredMessages];
102
+ if (options.systemAnnouncementMessage && options.systemAnnouncementMessage.trim()) {
103
+ preparedMessages = [
104
+ { role: "system", content: options.systemAnnouncementMessage.trim() },
105
+ ...preparedMessages,
106
+ ];
107
+ }
108
+ for (let index = 0; index < retryLimit; index += 1) {
109
+ let llmReply = "";
110
+ try {
111
+ const payload = {
112
+ model,
113
+ input: preparedMessages,
114
+ };
115
+ if (openaiTextParam) {
116
+ payload.text = openaiTextParam;
117
+ }
118
+ const llmResponse = await openaiClient.responses.create(payload);
119
+ if (llmResponse.error && options.warningCallback) {
120
+ options.warningCallback(`ERROR: OpenAI API returned an error: ${llmResponse.error}`);
121
+ }
122
+ if (llmResponse.incomplete_details && options.warningCallback) {
123
+ options.warningCallback(`ERROR: OpenAI API returned incomplete details: ${llmResponse.incomplete_details}`);
124
+ }
125
+ llmReply = llmResponse.output_text.trim();
126
+ if (!options.jsonResponse) {
127
+ return `${llmReply}`;
128
+ }
129
+ return parseFirstJsonValue(llmReply);
130
+ }
131
+ catch (error) {
132
+ if (error instanceof SyntaxError) {
133
+ failedError = error;
134
+ if (options.warningCallback) {
135
+ options.warningCallback(`JSON decode error:\n\n${error}.\n\nRaw text of LLM Reply:\n${llmReply}\n\nRetrying (attempt ${index + 1} of ${retryLimit}) immediately...`);
136
+ }
137
+ continue;
138
+ }
139
+ if (isRetryableOpenAIError(error)) {
140
+ failedError = error;
141
+ if (options.warningCallback) {
142
+ options.warningCallback(`OpenAI API error:\n\n${error}.\n\nRetrying (attempt ${index + 1} of ${retryLimit}) in ${retryBackoffTimeSeconds} seconds...`);
143
+ }
144
+ await sleep(retryBackoffTimeSeconds * 1000);
145
+ continue;
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+ if (failedError) {
151
+ throw failedError;
152
+ }
153
+ throw new Error("Unknown error occurred in gptSubmit");
154
+ }
@@ -0,0 +1,43 @@
1
+ import { type OpenAIClientLike } from './functions.js';
2
+ export interface ConversationMessage {
3
+ role: string;
4
+ content: string;
5
+ }
6
+ export interface GptConversationOptions {
7
+ openaiClient?: OpenAIClientLike;
8
+ model?: string;
9
+ }
10
+ export interface SubmitOptions {
11
+ model?: string;
12
+ jsonResponse?: boolean | Record<string, unknown> | string;
13
+ }
14
+ export declare class GptConversation extends Array<ConversationMessage> {
15
+ #private;
16
+ static get [Symbol.species](): ArrayConstructor;
17
+ get openaiClient(): OpenAIClientLike | undefined;
18
+ set openaiClient(value: OpenAIClientLike | undefined);
19
+ get model(): string | undefined;
20
+ set model(value: string | undefined);
21
+ get lastReply(): unknown;
22
+ set lastReply(value: unknown);
23
+ constructor(messages?: ConversationMessage[], options?: GptConversationOptions);
24
+ assignMessages(messages?: ConversationMessage[]): this;
25
+ clone(): GptConversation;
26
+ submit(message?: string | Record<string, unknown>, role?: string | null, options?: SubmitOptions): Promise<unknown>;
27
+ addMessage(role: string, content: unknown): this;
28
+ addUserMessage(content: unknown): this;
29
+ addAssistantMessage(content: unknown): this;
30
+ addSystemMessage(content: unknown): this;
31
+ addDeveloperMessage(content: unknown): this;
32
+ submitMessage(role: string, content: unknown): Promise<unknown>;
33
+ submitUserMessage(content: unknown): Promise<unknown>;
34
+ submitAssistantMessage(content: unknown): Promise<unknown>;
35
+ submitSystemMessage(content: unknown): Promise<unknown>;
36
+ submitDeveloperMessage(content: unknown): Promise<unknown>;
37
+ getLastMessage(): ConversationMessage | null;
38
+ getMessagesByRole(role: string): ConversationMessage[];
39
+ getLastReplyStr(): string;
40
+ getLastReplyDict(): Record<string, unknown>;
41
+ getLastReplyDictField(fieldName: string, defaultValue?: unknown): unknown;
42
+ toDictList(): ConversationMessage[];
43
+ }
@@ -0,0 +1,146 @@
1
+ import { GPT_MODEL_SMART, gptSubmit, } from './functions.js';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ export class GptConversation extends Array {
6
+ static get [Symbol.species]() {
7
+ return Array;
8
+ }
9
+ #openaiClient;
10
+ #model;
11
+ #lastReply = null;
12
+ get openaiClient() {
13
+ return this.#openaiClient;
14
+ }
15
+ set openaiClient(value) {
16
+ this.#openaiClient = value;
17
+ }
18
+ get model() {
19
+ return this.#model;
20
+ }
21
+ set model(value) {
22
+ this.#model = value;
23
+ }
24
+ get lastReply() {
25
+ return this.#lastReply;
26
+ }
27
+ set lastReply(value) {
28
+ this.#lastReply = value;
29
+ }
30
+ constructor(messages = [], options = {}) {
31
+ super(...messages);
32
+ this.#openaiClient = options.openaiClient;
33
+ this.#model = options.model;
34
+ }
35
+ assignMessages(messages) {
36
+ this.length = 0;
37
+ if (messages?.length) {
38
+ this.push(...messages);
39
+ }
40
+ return this;
41
+ }
42
+ clone() {
43
+ return new GptConversation(JSON.parse(JSON.stringify([...this])), {
44
+ openaiClient: this.openaiClient,
45
+ model: this.model,
46
+ });
47
+ }
48
+ async submit(message, role = 'user', options = {}) {
49
+ if (!this.openaiClient) {
50
+ throw new Error('OpenAI client is not set. Please provide an OpenAI client.');
51
+ }
52
+ const model = options.model || this.model || GPT_MODEL_SMART;
53
+ let jsonResponse = options.jsonResponse;
54
+ if (message) {
55
+ if (isRecord(message)) {
56
+ if (!jsonResponse && 'format' in message) {
57
+ jsonResponse = message;
58
+ }
59
+ if (!role && typeof message.role === 'string') {
60
+ role = message.role;
61
+ }
62
+ if ('content' in message) {
63
+ message = String(message.content ?? '');
64
+ }
65
+ }
66
+ this.addMessage(role || 'user', message);
67
+ }
68
+ const llmReply = await gptSubmit(this.toDictList(), this.openaiClient, {
69
+ jsonResponse,
70
+ model,
71
+ });
72
+ this.addAssistantMessage(llmReply);
73
+ this.lastReply = llmReply;
74
+ return llmReply;
75
+ }
76
+ addMessage(role, content) {
77
+ let normalizedContent;
78
+ if (typeof content === 'string') {
79
+ normalizedContent = content;
80
+ }
81
+ else if (isRecord(content)) {
82
+ normalizedContent = JSON.stringify(content, null, 2);
83
+ }
84
+ else {
85
+ normalizedContent = String(content);
86
+ }
87
+ this.push({ role, content: normalizedContent });
88
+ return this;
89
+ }
90
+ addUserMessage(content) {
91
+ return this.addMessage('user', content);
92
+ }
93
+ addAssistantMessage(content) {
94
+ return this.addMessage('assistant', content);
95
+ }
96
+ addSystemMessage(content) {
97
+ return this.addMessage('system', content);
98
+ }
99
+ addDeveloperMessage(content) {
100
+ return this.addMessage('developer', content);
101
+ }
102
+ async submitMessage(role, content) {
103
+ this.addMessage(role, content);
104
+ return this.submit();
105
+ }
106
+ async submitUserMessage(content) {
107
+ this.addUserMessage(content);
108
+ return this.submit();
109
+ }
110
+ async submitAssistantMessage(content) {
111
+ this.addAssistantMessage(content);
112
+ return this.submit();
113
+ }
114
+ async submitSystemMessage(content) {
115
+ this.addSystemMessage(content);
116
+ return this.submit();
117
+ }
118
+ async submitDeveloperMessage(content) {
119
+ this.addDeveloperMessage(content);
120
+ return this.submit();
121
+ }
122
+ getLastMessage() {
123
+ return this.length ? this[this.length - 1] : null;
124
+ }
125
+ getMessagesByRole(role) {
126
+ return this.filter((message) => message.role === role);
127
+ }
128
+ getLastReplyStr() {
129
+ return typeof this.lastReply === 'string' ? this.lastReply : '';
130
+ }
131
+ getLastReplyDict() {
132
+ if (!isRecord(this.lastReply)) {
133
+ return {};
134
+ }
135
+ return JSON.parse(JSON.stringify(this.lastReply));
136
+ }
137
+ getLastReplyDictField(fieldName, defaultValue = null) {
138
+ if (!isRecord(this.lastReply)) {
139
+ return null;
140
+ }
141
+ return this.lastReply[fieldName] ?? defaultValue;
142
+ }
143
+ toDictList() {
144
+ return [...this];
145
+ }
146
+ }
@@ -0,0 +1,3 @@
1
+ export * from './functions.js';
2
+ export * from './gptConversation.js';
3
+ export * from './jsonSchemaFormat.js';
@@ -0,0 +1,3 @@
1
+ export * from './functions.js';
2
+ export * from './gptConversation.js';
3
+ export * from './jsonSchemaFormat.js';
@@ -0,0 +1,14 @@
1
+ export declare const JSON_INTEGER: unique symbol;
2
+ export declare const JSON_NUMBER: unique symbol;
3
+ export declare const JSON_STRING: StringConstructor;
4
+ export declare const JSON_BOOLEAN: BooleanConstructor;
5
+ export interface JSONSchemaFormatResult extends Record<string, unknown> {
6
+ format: {
7
+ type: 'json_schema';
8
+ strict: true;
9
+ name?: string;
10
+ description?: string;
11
+ schema: Record<string, unknown>;
12
+ };
13
+ }
14
+ export declare function JSONSchemaFormat(name: string, schema: unknown, description?: string): JSONSchemaFormatResult;
@@ -0,0 +1,198 @@
1
+ export const JSON_INTEGER = Symbol('JSON_INTEGER');
2
+ export const JSON_NUMBER = Symbol('JSON_NUMBER');
3
+ export const JSON_STRING = String;
4
+ export const JSON_BOOLEAN = Boolean;
5
+ const TYPEMAP = new Map([
6
+ [JSON_STRING, 'string'],
7
+ [JSON_INTEGER, 'integer'],
8
+ [JSON_NUMBER, 'number'],
9
+ [JSON_BOOLEAN, 'boolean'],
10
+ [String, 'string'],
11
+ [Boolean, 'boolean'],
12
+ [BigInt, 'integer'],
13
+ [Number, 'number'],
14
+ ]);
15
+ function isRecord(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ function isStringArray(value) {
19
+ return (Array.isArray(value) && value.every((item) => typeof item === 'string'));
20
+ }
21
+ function isNumericRangeArray(value) {
22
+ if (!Array.isArray(value) || value.length !== 2) {
23
+ return false;
24
+ }
25
+ const [min, max] = value;
26
+ const minValid = min === null || typeof min === 'number';
27
+ const maxValid = max === null || typeof max === 'number';
28
+ return (minValid && maxValid && (typeof min === 'number' || typeof max === 'number'));
29
+ }
30
+ function isTupleMetadataArray(value) {
31
+ if (!Array.isArray(value) || value.length < 2) {
32
+ return false;
33
+ }
34
+ if (isStringArray(value)) {
35
+ return false;
36
+ }
37
+ return value.some((item) => typeof item === 'string' || isNumericRangeArray(item));
38
+ }
39
+ function inferPrimitiveType(schemaValue) {
40
+ const direct = TYPEMAP.get(schemaValue);
41
+ if (direct) {
42
+ return direct;
43
+ }
44
+ if (typeof schemaValue === 'string') {
45
+ return 'string';
46
+ }
47
+ if (typeof schemaValue === 'boolean') {
48
+ return 'boolean';
49
+ }
50
+ if (typeof schemaValue === 'bigint') {
51
+ return 'integer';
52
+ }
53
+ if (typeof schemaValue === 'number') {
54
+ return Number.isInteger(schemaValue) ? 'integer' : 'number';
55
+ }
56
+ return null;
57
+ }
58
+ function convertSchemaRecursive(subschema) {
59
+ let subschemaDescription = '';
60
+ let subschemaEnum = [];
61
+ let subschemaNumrange = [null, null];
62
+ let subschemaValue = subschema;
63
+ if (isTupleMetadataArray(subschema)) {
64
+ for (const item of subschema) {
65
+ if (!item) {
66
+ subschemaValue = item;
67
+ continue;
68
+ }
69
+ if (typeof item === 'string') {
70
+ subschemaDescription = item;
71
+ continue;
72
+ }
73
+ if (isStringArray(item) && item.length >= 2) {
74
+ subschemaEnum = item;
75
+ continue;
76
+ }
77
+ if (isNumericRangeArray(item)) {
78
+ subschemaNumrange = item;
79
+ continue;
80
+ }
81
+ subschemaValue = item;
82
+ }
83
+ }
84
+ if ((Array.isArray(subschemaValue) && isTupleMetadataArray(subschemaValue)) ||
85
+ (Array.isArray(subschemaValue) && subschemaValue.length === 0)) {
86
+ if (subschemaEnum.length > 0) {
87
+ subschemaValue = JSON_STRING;
88
+ }
89
+ const [nr0, nr1] = subschemaNumrange;
90
+ if (nr0 !== null || nr1 !== null) {
91
+ if ((typeof nr0 === 'number' && !Number.isInteger(nr0)) ||
92
+ (typeof nr1 === 'number' && !Number.isInteger(nr1))) {
93
+ subschemaValue = JSON_NUMBER;
94
+ }
95
+ else {
96
+ subschemaValue = JSON_INTEGER;
97
+ }
98
+ }
99
+ }
100
+ const result = {};
101
+ if (isRecord(subschemaValue)) {
102
+ result.type = 'object';
103
+ if (subschemaDescription) {
104
+ result.description = subschemaDescription;
105
+ }
106
+ result.additionalProperties = false;
107
+ const keys = Object.keys(subschemaValue);
108
+ result.required = keys;
109
+ const properties = {};
110
+ for (const [key, value] of Object.entries(subschemaValue)) {
111
+ if (typeof value === 'string') {
112
+ properties[key] = { type: 'string', description: value };
113
+ }
114
+ else {
115
+ properties[key] = convertSchemaRecursive(value);
116
+ }
117
+ }
118
+ result.properties = properties;
119
+ }
120
+ else if (Array.isArray(subschemaValue)) {
121
+ if (subschemaValue.length >= 2 && isStringArray(subschemaValue)) {
122
+ result.type = 'string';
123
+ subschemaEnum = subschemaValue;
124
+ }
125
+ else {
126
+ result.type = 'array';
127
+ if (subschemaDescription) {
128
+ result.description = subschemaDescription;
129
+ }
130
+ if (subschemaNumrange[0] !== null) {
131
+ result.minItems = subschemaNumrange[0];
132
+ }
133
+ if (subschemaNumrange[1] !== null) {
134
+ result.maxItems = subschemaNumrange[1];
135
+ }
136
+ const arrayExemplar = subschemaValue[0];
137
+ if (typeof arrayExemplar === 'string') {
138
+ result.items = { type: 'string', description: arrayExemplar };
139
+ }
140
+ else {
141
+ result.items = convertSchemaRecursive(arrayExemplar);
142
+ }
143
+ }
144
+ }
145
+ else {
146
+ const primitiveType = inferPrimitiveType(subschemaValue);
147
+ if (!primitiveType) {
148
+ throw new Error(`Unrecognized type for schema value: ${String(subschemaValue)}`);
149
+ }
150
+ result.type = primitiveType;
151
+ if (subschemaDescription) {
152
+ result.description = subschemaDescription;
153
+ }
154
+ }
155
+ if (subschemaEnum.length) {
156
+ result.enum = subschemaEnum;
157
+ }
158
+ if (result.type === 'integer' || result.type === 'number') {
159
+ if (subschemaNumrange[0] !== null) {
160
+ result.minimum = subschemaNumrange[0];
161
+ }
162
+ if (subschemaNumrange[1] !== null) {
163
+ result.maximum = subschemaNumrange[1];
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+ export function JSONSchemaFormat(name, schema, description) {
169
+ const result = {
170
+ format: {
171
+ type: 'json_schema',
172
+ strict: true,
173
+ name,
174
+ schema: {
175
+ type: 'object',
176
+ properties: {},
177
+ required: [],
178
+ additionalProperties: false,
179
+ },
180
+ },
181
+ };
182
+ if (description) {
183
+ result.format.description = description;
184
+ }
185
+ let converted = convertSchemaRecursive(schema);
186
+ if (converted.type !== 'object') {
187
+ converted = {
188
+ type: 'object',
189
+ required: [name],
190
+ additionalProperties: false,
191
+ properties: {
192
+ [name]: converted,
193
+ },
194
+ };
195
+ }
196
+ result.format.schema = converted;
197
+ return result;
198
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./gptApi/functions.js";
2
+ export * from "./gptApi/gptConversation.js";
3
+ export * from "./gptApi/jsonSchemaFormat.js";
@@ -0,0 +1,3 @@
1
+ export * from "./gptApi/functions.js";
2
+ export * from "./gptApi/gptConversation.js";
3
+ export * from "./gptApi/jsonSchemaFormat.js";
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `jsonSurgery` performs iterative, AI-guided edits to a JSON-compatible object.
3
+ *
4
+ * The module exposes:
5
+ * - {@link jsonSurgery}: the main entry point that applies requested modifications.
6
+ * - {@link JSONSurgeryOptions}: callbacks and limits for validation, progress, and retries.
7
+ * - {@link JSONSurgeryError}: enriched error type that includes the last object state.
8
+ *
9
+ * High-level flow:
10
+ * 1. Deep-copy the input object (the original is never mutated).
11
+ * 2. Send the current object state plus user instructions to the model.
12
+ * 3. Receive one or more structured operations targeting JSON paths.
13
+ * 4. Apply operations and continue iteratively until validation succeeds.
14
+ *
15
+ * Supported operation types include assign, append, insert, delete, and rename.
16
+ * Values are transferred through constrained JSON schemas so the model can express
17
+ * primitives, objects, and arrays safely in a structured format.
18
+ *
19
+ * Callers can provide:
20
+ * - `schemaDescription` to describe expected object shape/constraints,
21
+ * - `skippedKeys` to hide sensitive or noisy fields from model context,
22
+ * - `onValidateBeforeReturn` to enforce app-specific validation,
23
+ * - `onWorkInProgress` for per-iteration monitoring/intervention,
24
+ * - `giveUpAfterSeconds` / `giveUpAfterIterations` as soft stop conditions.
25
+ */
26
+ import { OpenAI } from 'openai';
27
+ /**
28
+ * Optional configuration for {@link jsonSurgery}.
29
+ * @property schemaDescription Optional schema description for the JSON object. This can be written
30
+ * in JSON Schema format, or as a textual explanation. It's passed as a string to the AI and has
31
+ * no direct enforcement semantics.
32
+ * @property skippedKeys Optional array of key names to skip/ignore in the AI-visible JSON. This
33
+ * is useful for omitting large or sensitive fields (IDs, timestamps, binary data, etc.) that are
34
+ * often irrelevant to the requested modifications.
35
+ * @property onValidateBeforeReturn Optional callback that validates and/or corrects the object before
36
+ * final return. It receives the current object and resolves to:
37
+ * - objCorrected: a corrected object to continue with
38
+ * - errors: lingering validation errors that still need to be addressed
39
+ * @property onWorkInProgress Optional callback that's called after each iteration with the
40
+ * current state of the object. Useful for logging or monitoring progress. Can optionally return
41
+ * a promise that resolves into a modified version of the object, in case the caller wants to
42
+ * intervene or adjust the object mid-process. onWorkInProgress is a good place to implement
43
+ * custom logging, metrics, or even dynamic adjustments to the object during processing. It's also
44
+ * a place from whence you can throw an exception if needed to abort processing.
45
+ * @property giveUpAfterSeconds Optional soft-limit for total processing time, in seconds.
46
+ * @property giveUpAfterIterations Optional soft-limit for iteration count.
47
+ *
48
+ * If `objCorrected` is returned, that corrected object is used.
49
+ * If `errors` is missing or empty, the object is treated as valid.
50
+ */
51
+ export type JSONSurgeryOptions = {
52
+ schemaDescription?: string;
53
+ skippedKeys?: string[];
54
+ onValidateBeforeReturn?: (obj: any) => Promise<{
55
+ objCorrected?: any;
56
+ errors?: string[];
57
+ } | undefined>;
58
+ onWorkInProgress?: (obj: any) => Promise<any | undefined>;
59
+ giveUpAfterSeconds?: number;
60
+ giveUpAfterIterations?: number;
61
+ };
62
+ /**
63
+ * Error type reserved for failures from {@link jsonSurgery}.
64
+ * `obj` contains the object being modified, captured in whatever state it was
65
+ * left in at the moment the exception was thrown.
66
+ */
67
+ export declare class JSONSurgeryError extends Error {
68
+ obj: any;
69
+ constructor(message: string, obj: any, options?: ErrorOptions);
70
+ }
71
+ /**
72
+ * Modifies a JSON object based on modification instructions using OpenAI's API.
73
+ * Does NOT modify the original object in place; instead, works with a copy and returns
74
+ * the modified copy.
75
+ * @param openaiClient The OpenAI client to use for modifications
76
+ * @param obj The JSON object to modify
77
+ * @param modificationInstructions Instructions describing the modifications to apply
78
+ * @param options Optional configuration object. See {@link JSONSurgeryOptions}.
79
+ * @returns A copy of the original object, modified according to the instructions.
80
+ */
81
+ export declare const jsonSurgery: (openaiClient: OpenAI, obj: any, modificationInstructions: string, options?: JSONSurgeryOptions) => Promise<any>;