File size: 11,343 Bytes
58843c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
const INDENT_STEP = "    "; // 4 spaces for indentation

function stringifyJsJsonRecursive(value: unknown, currentIndent: string): string {
	if (typeof value === "string") return `"${value}"`;
	if (value === null) return "null";
	if (typeof value === "boolean" || typeof value === "number") return String(value);

	if (typeof value === "object" && value !== null) {
		const nextIndent = currentIndent + INDENT_STEP;
		if (Array.isArray(value)) {
			if (value.length === 0) return "[]";
			const items = value.map(v => stringifyJsJsonRecursive(v, nextIndent));
			return "[\n" + items.map(item => nextIndent + item).join(",\n") + "\n" + currentIndent + "]";
		}

		const entries = Object.entries(value);
		if (entries.length === 0) return "{}";
		const properties = entries.map(([k, v]) => `${nextIndent}"${k}": ${stringifyJsJsonRecursive(v, nextIndent)}`);

		return "{\n" + properties.join(",\n") + "\n" + currentIndent + "}";
	}
	return String(value); // Fallback for other types
}

function formatJsJsonValue(value: unknown, baseIndent: string): string {
	return stringifyJsJsonRecursive(value, baseIndent);
}

function stringifyPythonRecursive(value: unknown, currentIndent: string): string {
	if (typeof value === "string") return `"${value}"`;
	if (typeof value === "boolean") return value ? "True" : "False";
	if (value === null) return "None";
	if (typeof value === "number") return String(value);

	if (typeof value === "object" && value !== null) {
		const nextIndent = currentIndent + INDENT_STEP;
		if (Array.isArray(value)) {
			if (value.length === 0) return "[]";
			const items = value.map(v => stringifyPythonRecursive(v, nextIndent));
			return "[\n" + items.map(item => nextIndent + item).join(",\n") + "\n" + currentIndent + "]";
		}

		const entries = Object.entries(value);
		if (entries.length === 0) return "{}";
		// In Python, dictionary keys are typically strings.
		const properties = entries.map(([k, v]) => `${nextIndent}"${k}": ${stringifyPythonRecursive(v, nextIndent)}`);

		return "{\n" + properties.join(",\n") + "\n" + currentIndent + "}";
	}
	return String(value); // Fallback
}

function formatPythonValue(value: unknown, baseIndent: string): string {
	return stringifyPythonRecursive(value, baseIndent);
}

/**
 * Inserts new properties into a code snippet block (like a JS object or Python dict).
 */
function insertPropertiesInternal(
	snippet: string,
	newProperties: Record<string, unknown>,
	blockStartMarker: RegExp, // Regex to find the character *opening* the block (e.g., '{' or '(')
	openChar: string, // The opening character, e.g., '{' or '('
	closeChar: string, // The closing character, e.g., '}' or ')'
	propFormatter: (key: string, formattedValue: string, indent: string) => string,
	valueFormatter: (value: unknown, baseIndent: string) => string
): string {
	if (Object.keys(newProperties).length === 0) {
		return snippet;
	}

	const match = snippet.match(blockStartMarker);
	// match.index is the start of the whole marker, e.g. "client.chatCompletionStream("
	// We need the index of the openChar itself.
	if (!match || typeof match.index !== "number") {
		return snippet;
	}

	const openCharIndex = snippet.indexOf(openChar, match.index + match[0].length - 1);
	if (openCharIndex === -1) {
		return snippet;
	}

	let balance = 1;
	let closeCharIndex = -1;
	for (let i = openCharIndex + 1; i < snippet.length; i++) {
		if (snippet[i] === openChar) {
			balance++;
		} else if (snippet[i] === closeChar) {
			balance--;
		}
		if (balance === 0) {
			closeCharIndex = i;
			break;
		}
	}

	if (closeCharIndex === -1) {
		return snippet; // Malformed or not found
	}

	const contentBeforeBlock = snippet.substring(0, openCharIndex + 1);
	const currentContent = snippet.substring(openCharIndex + 1, closeCharIndex);
	const contentAfterBlock = snippet.substring(closeCharIndex);

	// Determine indentation
	let indent = "";
	const lines = currentContent.split("\n");
	if (lines.length > 1) {
		for (const line of lines) {
			const lineIndentMatch = line.match(/^(\s+)\S/);
			if (lineIndentMatch) {
				indent = lineIndentMatch[1] ?? "";
				break;
			}
		}
	}
	if (!indent) {
		// If no indent found, or content is empty/single line, derive from openChar line
		const lineOfOpenCharStart = snippet.lastIndexOf("\n", openCharIndex) + 1;
		const lineOfOpenChar = snippet.substring(lineOfOpenCharStart, openCharIndex);
		const openCharLineIndentMatch = lineOfOpenChar.match(/^(\s*)/);
		indent = (openCharLineIndentMatch ? openCharLineIndentMatch[1] : "") + "    "; // Default to 4 spaces more
	}

	let newPropsStr = "";
	Object.entries(newProperties).forEach(([key, value]) => {
		newPropsStr += propFormatter(key, valueFormatter(value, indent), indent);
	});

	const trimmedOriginalContent = currentContent.trim();
	let combinedContent;

	if (trimmedOriginalContent) {
		// There was actual non-whitespace content.
		// Preserve original currentContent structure as much as possible.
		// Find the end of the textual part of currentContent (before any pure trailing whitespace).
		let endOfTextualPart = currentContent.length;
		while (endOfTextualPart > 0 && /\s/.test(currentContent.charAt(endOfTextualPart - 1))) {
			endOfTextualPart--;
		}
		const textualPartOfCurrentContent = currentContent.substring(0, endOfTextualPart);
		const trailingWhitespaceOfCurrentContent = currentContent.substring(endOfTextualPart);

		let processedTextualPart = textualPartOfCurrentContent;
		if (processedTextualPart && !processedTextualPart.endsWith(",")) {
			processedTextualPart += ",";
		}

		// Add a newline separator if the original trailing whitespace doesn't end with one.
		const separator =
			trailingWhitespaceOfCurrentContent.endsWith("\n") || trailingWhitespaceOfCurrentContent.endsWith("\r")
				? ""
				: "\n";
		combinedContent = processedTextualPart + trailingWhitespaceOfCurrentContent + separator + newPropsStr;
	} else {
		// currentContent was empty or contained only whitespace.
		// Check if the original block opening (e.g., '{' or '(') was immediately followed by a newline.
		const openCharFollowedByNewline =
			snippet[openCharIndex + 1] === "\n" ||
			(snippet[openCharIndex + 1] === "\r" && snippet[openCharIndex + 2] === "\n");
		if (openCharFollowedByNewline) {
			combinedContent = newPropsStr; // newPropsStr already starts with indent
		} else {
			combinedContent = "\n" + newPropsStr; // Add a newline first, then newPropsStr
		}
	}

	// Remove the trailing comma (and its trailing whitespace/newline) from the last property added.
	combinedContent = combinedContent.replace(/,\s*$/, "");

	// Ensure the block content ends with a newline, and the closing character is on its own line, indented.
	if (combinedContent.trim()) {
		// If there's any actual content in the block
		if (!combinedContent.endsWith("\n")) {
			combinedContent += "\n";
		}
		// Determine the base indent for the closing character's line
		const lineOfOpenCharStart = snippet.lastIndexOf("\n", openCharIndex) + 1;
		const openCharLine = snippet.substring(lineOfOpenCharStart, openCharIndex);
		const baseIndentMatch = openCharLine.match(/^(\s*)/);
		const baseIndent = baseIndentMatch ? baseIndentMatch[1] : "";
		combinedContent += baseIndent;
	} else {
		// Block is effectively empty (e.g., was {} and no properties added, or newPropsStr was empty - though current logic prevents this if newProperties is not empty).
		// Format as an empty block with the closing char on a new, indented line.
		const lineOfOpenCharStart = snippet.lastIndexOf("\n", openCharIndex) + 1;
		const openCharLine = snippet.substring(lineOfOpenCharStart, openCharIndex);
		const baseIndentMatch = openCharLine.match(/^(\s*)/);
		const baseIndent = baseIndentMatch ? baseIndentMatch[1] : "";

		const openCharFollowedByNewline =
			snippet[openCharIndex + 1] === "\n" ||
			(snippet[openCharIndex + 1] === "\r" && snippet[openCharIndex + 2] === "\n");
		if (openCharFollowedByNewline) {
			// Original was like {\n}
			combinedContent = baseIndent; // Just the indent for the closing char
		} else {
			// Original was like {}
			combinedContent = "\n" + baseIndent; // Newline, then indent for closing char
		}
	}

	return contentBeforeBlock + combinedContent + contentAfterBlock;
}

export function modifySnippet(snippet: string, newProperties: Record<string, unknown>): string {
	// JS: HuggingFace InferenceClient (streaming)
	if (snippet.includes("client.chatCompletionStream")) {
		return insertPropertiesInternal(
			snippet,
			newProperties,
			/client\.chatCompletionStream\s*\(\s*/, // Finds "client.chatCompletionStream("
			"{", // The parameters are in an object literal
			"}",
			(key, value, indent) => `${indent}${key}: ${value},\n`, // JS object literal style
			formatJsJsonValue
		);
	}
	// JS: HuggingFace InferenceClient (non-streaming)
	else if (snippet.includes("client.chatCompletion") && snippet.includes("InferenceClient")) {
		// Ensure it's not the OpenAI client by also checking for InferenceClient
		return insertPropertiesInternal(
			snippet,
			newProperties,
			/client\.chatCompletion\s*\(\s*/, // Finds "client.chatCompletion("
			"{", // The parameters are in an object literal
			"}",
			(key, value, indent) => `${indent}${key}: ${value},\n`, // JS object literal style
			formatJsJsonValue
		);
	}
	// JS: OpenAI Client
	// Check for client.chat.completions.create and a common JS import pattern
	else if (
		snippet.includes("client.chat.completions.create") &&
		(snippet.includes('import { OpenAI } from "openai"') || snippet.includes("new OpenAI("))
	) {
		return insertPropertiesInternal(
			snippet,
			newProperties,
			/client\.chat\.completions\.create\s*\(\s*/, // Finds "client.chat.completions.create("
			"{", // The parameters are in an object literal
			"}",
			(key, value, indent) => `${indent}${key}: ${value},\n`,
			formatJsJsonValue
		);
	}
	// Python: OpenAI or HuggingFace Client using client.chat.completions.create
	else if (snippet.includes("client.chat.completions.create")) {
		return insertPropertiesInternal(
			snippet,
			newProperties,
			/client\.chat\.completions\.create\s*\(/, // Finds "client.chat.completions.create("
			"(", // The parameters are directly in the function call tuple
			")",
			(key, value, indent) => {
				const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
				return `${indent}${snakeKey}=${value},\n`;
			},
			formatPythonValue
		);
	}
	// Python: requests example with query({...})
	else if (snippet.includes("def query(payload):") && snippet.includes("query({")) {
		return insertPropertiesInternal(
			snippet,
			newProperties,
			/query\s*\(\s*/, // Finds "query(" and expects a dictionary literal next
			"{", // The parameters are in a dictionary literal
			"}",
			// Python dict keys are strings, values formatted for Python
			(key, formattedValue, indent) => `${indent}"${key}": ${formattedValue},\n`,
			formatPythonValue // Use formatPythonValue for the values themselves
		);
	}
	// Shell/curl (JSON content)
	else if (snippet.includes("curl") && snippet.includes("-d")) {
		return insertPropertiesInternal(
			snippet,
			newProperties,
			/-d\s*'(?:\\n)?\s*/,
			"{",
			"}",
			(key, value, indent) => {
				const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
				return `${indent}"${snakeKey}": ${value},\n`;
			},
			formatJsJsonValue
		);
	}
	return snippet;
}