spaider
Version:
Deterministic-first AI code assistant that crawls your codebase to implement changes using open source LLMs
222 lines (207 loc) • 9.86 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AI = void 0;
const openai_1 = __importDefault(require("openai"));
const zod_1 = require("openai/helpers/zod");
const schemas_1 = require("../lib/schemas");
const utils_1 = require("../lib/utils");
const logger_1 = require("./logger");
const DEFAULT_SYSTEM_ROLE = "You are an AI assistant specialized in software development and code generation.";
var AI;
(function (AI) {
const openai = new openai_1.default({
baseURL: process.env.OPENAI_BASE_URL || "",
apiKey: process.env.OPENAI_API_KEY || "no-key",
});
async function complete(body, options) {
if (!body.messages) {
throw new Error("No messages for complete request");
}
const { model = process.env.OPENAI_MODEL || "gpt-4.1", temperature = 0, max_completion_tokens = 8000, messages, ...rest } = body;
const response = await openai.chat.completions.create({
model,
temperature,
max_completion_tokens,
messages,
...rest,
}, {
maxRetries: 2,
timeout: 30000,
...options,
});
return response;
}
AI.complete = complete;
async function completeStructured(params) {
const messages = [
(0, utils_1.system)(`${params.role || DEFAULT_SYSTEM_ROLE}
You must respond with valid JSON that matches the provided schema.
Do not include any text outside the JSON response.
Do not wrap the JSON in markdown code blocks or use \`\`\` formatting.`),
(0, utils_1.user)(params.user),
];
const response = await complete({
messages,
response_format: (0, zod_1.zodResponseFormat)(params.schema, params.name),
});
const content = response.choices[0].message.content;
// Remove any potential markdown code block markers that might have been added
const cleanContent = content
.replace(/^```[\w]*\n?/, "")
.replace(/\n?```$/, "");
try {
const parsed = JSON.parse(cleanContent);
return params.schema.parse(parsed);
}
catch (error) {
logger_1.Logger.error("=== JSON PARSING ERROR ===");
logger_1.Logger.error("Full response content:");
logger_1.Logger.error(cleanContent);
logger_1.Logger.error("========================");
logger_1.Logger.error("Error:", error);
// If JSON parse fails, try to identify common issues
if (error instanceof SyntaxError) {
logger_1.Logger.error("JSON Syntax Error detected. Common issues:");
if (cleanContent.includes("`")) {
logger_1.Logger.error("- Contains backticks (`) which are not valid in JSON");
}
if (cleanContent.includes("\n") && !cleanContent.includes("\\n")) {
logger_1.Logger.error("- Contains unescaped newlines");
}
if (cleanContent.match(/[^\\]"/)) {
logger_1.Logger.error("- Contains unescaped quotes");
}
}
throw error;
}
}
AI.completeStructured = completeStructured;
async function analyzeIntent(userPrompt, files, _projectTree) {
const filePreview = (0, utils_1.formatFilePreviews)(files);
const semanticSummary = (0, utils_1.formatFileSemantics)(files);
const prompt = `
Analyze this code related request to understand the user intent.
User Request: ${userPrompt}
Files provided:
${filePreview}
Code Symbols:
${semanticSummary}
Based on this information, determine:
1. Should files be changed to fulfill the user's request? Set editMode to true if the user is asking for any code be created or modified. Set editMode to false if the user is only asking for information, explanation, or code review. If the request is ambiguous, prefer false unless there is a clear instruction to change files.
2. A clear description of what needs to be done (be specific about the scope and impact)
3. Whether additional context is needed to proceed - set needsMoreContext to true if additional files or information are needed
4. List of file paths relevant to this intent (filePaths)
5. List of relevant identifiers (searchTerms) EXCLUSIVELY from the "Code Symbols" above.
`;
return await completeStructured({
user: prompt,
schema: schemas_1.IntentSchema,
name: "intent",
});
}
AI.analyzeIntent = analyzeIntent;
async function generateChanges(_userPrompt, intent, files, _projectTree) {
const filePreview = (0, utils_1.formatFilePreviews)(files);
const prompt = `
${intent.description}
Files provided:
${filePreview}
Based on this information, generate a JSON response with a "changes" array containing change objects. Each change object should have:
1. operation: [new_file, delete_file, modify_file]
2. filePath: The path to the file being created, deleted or modified
3. modificationType: [replace_block, add_block, remove_block, none]. For deleted files, use type none.
4. modificationDescription: The description of the modification relative to the code blocks. Leave empty if not applicable.
5. oldCodeBlock: Applicable when modifying existing files. Leave empty if not applicable.
6. newCodeBlock: Applicable when modifying existing files or creating new ones. Leave empty if not applicable.
Focus on generating precise changes aligned with existing code structure while maintaining high code quality.
For modifications, generate multiple changes per file if necessary.
CRITICAL: Return ONLY valid JSON. Do not use backticks, template literals, or multi-line strings.
Escape all quotes and newlines properly in JSON strings.
Use \\n for newlines within strings, not actual line breaks.
Use \" for quotes within strings.
Return the response in this exact format:
{
"changes": [
{
"operation": "modify_file",
"filePath": "path/to/file.ts",
"modificationType": "add_block",
"modificationDescription": "Description of change",
"oldCodeBlock": "",
"newCodeBlock": "// New code here\\nfunction example() {\\n return true;\\n}"
}
]
}
`;
const result = await completeStructured({
role: "You are a senior software engineer generating precise code changes. Generate high-quality, production-ready code changes to implement the following request",
user: prompt,
schema: schemas_1.ChangesSchema,
name: "changes",
});
return result.changes;
}
AI.generateChanges = generateChanges;
async function applyFileChanges(changes, currentFileContent) {
const formattedChanges = (0, utils_1.formatChanges)(changes);
if (changes[0].operation === "delete_file") {
throw new Error("AI should not handle delete operations");
}
const header = changes[0].operation === "new_file"
? "Create a new file with the requested code blocks."
: "Apply ONLY the specified modifications to this existing file.";
const prompt = `
${header}
Current File Content:
\`\`\`
${currentFileContent}
\`\`\`
Modifications to Apply:
${formattedChanges}
Instructions:
1. Start with the exact current file content shown above
2. Apply ONLY the specified modifications - do not change anything else
3. For replace_block: find the exact old code block and replace it with the new code block
4. For add_block: insert the new code block at the appropriate location
5. For remove_block: remove only the specified code block
6. Maintain ALL existing formatting, imports, exports, and other code exactly as they are
7. Return the complete modified file content
8. Do not add explanations, comments, or markdown formatting
Apply the modifications precisely and return the complete file.
`;
const response = await complete({
messages: [
(0, utils_1.system)(`${DEFAULT_SYSTEM_ROLE}
Return only the complete rewritten file content without any additional formatting or explanation.`),
(0, utils_1.user)(prompt),
],
});
const rewrittenContent = response.choices[0].message.content.trim();
// Remove any potential markdown code block markers that might have been added
return rewrittenContent.replace(/^```[\w]*\n?/, "").replace(/\n?```$/, "");
}
AI.applyFileChanges = applyFileChanges;
async function generateAnswer(_userPrompt, intent, files, _projectTree) {
const filePreview = (0, utils_1.formatFilePreviews)(files);
const prompt = `
${intent.description}
Files analyzed:
${filePreview}
`;
const response = await complete({
messages: [
(0, utils_1.system)(`${DEFAULT_SYSTEM_ROLE}
For code-related prompts, prioritize code output with minimal explanation.
For refactoring requests, provide the refactored code and a very short summary of changes.
Always deliver clear, concise and efficient answers.`),
(0, utils_1.user)(prompt),
],
});
return response.choices[0].message.content.trim();
}
AI.generateAnswer = generateAnswer;
})(AI || (exports.AI = AI = {}));
//# sourceMappingURL=ai.js.map