gaunt-sloth-assistant 0.1.2 → 0.1.3
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/.eslint.config.mjs +0 -0
- package/.github/dependabot.yml +0 -0
- package/.github/workflows/ci.yml +33 -0
- package/.gsloth.preamble.internal.md +0 -0
- package/.gsloth.preamble.review.md +0 -0
- package/LICENSE +0 -0
- package/README.md +59 -33
- package/ROADMAP.md +20 -20
- package/UX-RESEARCH.md +1 -1
- package/docs/CONFIGURATION.md +96 -6
- package/docs/DEVELOPMENT.md +12 -0
- package/docs/RELEASE-HOWTO.md +0 -0
- package/eslint.config.js +38 -0
- package/index.js +4 -2
- package/package.json +7 -3
- package/spec/.gsloth.config.js +1 -1
- package/spec/.gsloth.config.json +25 -0
- package/spec/askCommand.spec.js +39 -5
- package/spec/config.spec.js +421 -0
- package/spec/initCommand.spec.js +3 -2
- package/spec/predefinedConfigs.spec.js +100 -0
- package/spec/questionAnsweringModule.spec.js +14 -14
- package/spec/reviewCommand.spec.js +46 -8
- package/spec/reviewModule.spec.js +7 -1
- package/spec/support/jasmine.mjs +0 -0
- package/src/commands/askCommand.js +5 -4
- package/src/commands/initCommand.js +2 -1
- package/src/commands/reviewCommand.js +19 -12
- package/src/config.js +139 -25
- package/src/configs/anthropic.js +28 -5
- package/src/configs/fake.js +15 -0
- package/src/configs/groq.js +27 -4
- package/src/configs/vertexai.js +23 -2
- package/src/consoleUtils.js +15 -5
- package/src/modules/questionAnsweringModule.js +9 -14
- package/src/modules/reviewModule.js +11 -16
- package/src/prompt.js +4 -3
- package/src/providers/file.js +19 -0
- package/src/providers/ghPrDiffProvider.js +2 -2
- package/src/providers/jiraIssueLegacyAccessTokenProvider.js +22 -28
- package/src/providers/text.js +1 -1
- package/src/systemUtils.js +32 -0
- package/src/utils.js +49 -13
- package/testMessage.txt +0 -0
@@ -4,14 +4,37 @@ import * as td from 'testdouble';
|
|
4
4
|
describe('reviewCommand', function (){
|
5
5
|
|
6
6
|
beforeEach(async function() {
|
7
|
+
td.reset();
|
7
8
|
this.review = td.function();
|
8
9
|
this.prompt = await td.replaceEsm("../src/prompt.js");
|
9
10
|
td.when(this.prompt.readInternalPreamble()).thenReturn("INTERNAL PREAMBLE");
|
10
11
|
td.when(this.prompt.readPreamble(".gsloth.preamble.review.md")).thenReturn("PROJECT PREAMBLE");
|
11
12
|
this.codeReviewMock = await td.replaceEsm("../src/modules/reviewModule.js");
|
12
|
-
await td.replaceEsm("../src/config.js"
|
13
|
-
|
14
|
-
|
13
|
+
await td.replaceEsm("../src/config.js", {
|
14
|
+
SLOTH_INTERNAL_PREAMBLE: '.gsloth.preamble.internal.md',
|
15
|
+
USER_PROJECT_REVIEW_PREAMBLE: '.gsloth.preamble.review.md',
|
16
|
+
slothContext: {
|
17
|
+
config: {},
|
18
|
+
currentDir: '/mock/current/dir'
|
19
|
+
},
|
20
|
+
initConfig: td.function()
|
21
|
+
});
|
22
|
+
const readFileFromCurrentDir = td.function();
|
23
|
+
const readMultipleFilesFromCurrentDir = td.function();
|
24
|
+
const extractLastMessageContent = td.function();
|
25
|
+
const toFileSafeString = td.function();
|
26
|
+
const fileSafeLocalDate = td.function();
|
27
|
+
this.utilsMock = {
|
28
|
+
readFileFromCurrentDir,
|
29
|
+
readMultipleFilesFromCurrentDir,
|
30
|
+
ProgressIndicator: td.constructor(),
|
31
|
+
extractLastMessageContent,
|
32
|
+
toFileSafeString,
|
33
|
+
fileSafeLocalDate
|
34
|
+
};
|
35
|
+
await td.replaceEsm("../src/utils.js", this.utilsMock);
|
36
|
+
td.when(this.utilsMock.readFileFromCurrentDir("test.file")).thenReturn("FILE TO REVIEW");
|
37
|
+
td.when(this.utilsMock.readMultipleFilesFromCurrentDir(["test.file"])).thenReturn("test.file:\n```\nFILE TO REVIEW\n```");
|
15
38
|
td.when(this.codeReviewMock.review(
|
16
39
|
'sloth-DIFF-review',
|
17
40
|
td.matchers.anything(),
|
@@ -21,7 +44,7 @@ describe('reviewCommand', function (){
|
|
21
44
|
|
22
45
|
it('Should call review with file contents', async function() {
|
23
46
|
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
24
|
-
const program = new Command()
|
47
|
+
const program = new Command();
|
25
48
|
await reviewCommand(program, {});
|
26
49
|
await program.parseAsync(['na', 'na', 'review', '-f', 'test.file']);
|
27
50
|
td.verify(this.review(
|
@@ -31,6 +54,20 @@ describe('reviewCommand', function (){
|
|
31
54
|
);
|
32
55
|
});
|
33
56
|
|
57
|
+
it('Should call review with multiple file contents', async function() {
|
58
|
+
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
59
|
+
const program = new Command();
|
60
|
+
await reviewCommand(program, {});
|
61
|
+
td.when(this.utilsMock.readMultipleFilesFromCurrentDir(["test.file", "test2.file"]))
|
62
|
+
.thenReturn("test.file:\n```\nFILE TO REVIEW\n```\n\ntest2.file:\n```\nFILE2 TO REVIEW\n```");
|
63
|
+
await program.parseAsync(['na', 'na', 'review', '-f', 'test.file', 'test2.file']);
|
64
|
+
td.verify(this.review(
|
65
|
+
'sloth-DIFF-review',
|
66
|
+
"INTERNAL PREAMBLE\nPROJECT PREAMBLE",
|
67
|
+
"test.file:\n```\nFILE TO REVIEW\n```\n\ntest2.file:\n```\nFILE2 TO REVIEW\n```")
|
68
|
+
);
|
69
|
+
});
|
70
|
+
|
34
71
|
it('Should display predefined providers in help', async function() {
|
35
72
|
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
36
73
|
const program = new Command();
|
@@ -43,17 +80,18 @@ describe('reviewCommand', function (){
|
|
43
80
|
|
44
81
|
await reviewCommand(program, {});
|
45
82
|
|
46
|
-
const commandUnderTest = program.commands.find(c => c.name()
|
83
|
+
const commandUnderTest = program.commands.find(c => c.name() === 'review');
|
84
|
+
|
47
85
|
expect(commandUnderTest).toBeDefined();
|
48
86
|
commandUnderTest.outputHelp();
|
49
87
|
|
50
88
|
// Verify content providers are displayed
|
51
89
|
expect(testOutput.text).toContain('--content-provider <contentProvider>');
|
52
|
-
expect(testOutput.text).toContain('(choices: "gh", "text")');
|
90
|
+
expect(testOutput.text).toContain('(choices: "gh", "text", "file")');
|
53
91
|
|
54
92
|
// Verify requirements providers are displayed
|
55
93
|
expect(testOutput.text).toContain('--requirements-provider <requirementsProvider>');
|
56
|
-
expect(testOutput.text).toContain('(choices: "jira-legacy", "text")');
|
94
|
+
expect(testOutput.text).toContain('(choices: "jira-legacy", "text", "file")');
|
57
95
|
});
|
58
96
|
|
59
97
|
it('Should call review with predefined requirements provider', async function() {
|
@@ -129,7 +167,7 @@ describe('reviewCommand', function (){
|
|
129
167
|
|
130
168
|
// Mock the gh provider
|
131
169
|
const ghProvider = td.func();
|
132
|
-
td.when(ghProvider('123')).thenResolve('PR Diff Content');
|
170
|
+
td.when(ghProvider(td.matchers.anything(), '123')).thenResolve('PR Diff Content');
|
133
171
|
|
134
172
|
// Replace the dynamic import with our mock
|
135
173
|
await td.replaceEsm('../src/providers/ghPrDiffProvider.js', {
|
@@ -1,9 +1,14 @@
|
|
1
1
|
import { reviewInner } from '../src/modules/reviewModule.js';
|
2
2
|
import { slothContext } from '../src/config.js';
|
3
3
|
import { FakeListChatModel } from "@langchain/core/utils/testing";
|
4
|
+
import * as td from "testdouble";
|
4
5
|
|
5
6
|
describe('reviewModule', () => {
|
6
7
|
|
8
|
+
beforeEach(async function() {
|
9
|
+
td.reset();
|
10
|
+
});
|
11
|
+
|
7
12
|
it('should invoke LLM', async () => {
|
8
13
|
// Setup mock for slothContext
|
9
14
|
const testContext = {...slothContext,
|
@@ -16,7 +21,8 @@ describe('reviewModule', () => {
|
|
16
21
|
|
17
22
|
// Test the function
|
18
23
|
const output = await reviewInner(testContext, () => {}, 'test-preamble', 'test-diff');
|
24
|
+
|
19
25
|
expect(output).toBe("First LLM message");
|
20
26
|
});
|
21
27
|
|
22
|
-
});
|
28
|
+
});
|
package/spec/support/jasmine.mjs
CHANGED
File without changes
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { readInternalPreamble } from "../prompt.js";
|
2
|
-
import {
|
2
|
+
import { readMultipleFilesFromCurrentDir } from "../utils.js";
|
3
3
|
import { initConfig } from "../config.js";
|
4
4
|
|
5
5
|
/**
|
@@ -7,20 +7,21 @@ import { initConfig } from "../config.js";
|
|
7
7
|
* @param {Object} program - The commander program
|
8
8
|
* @param {Object} context - The context object
|
9
9
|
*/
|
10
|
+
// eslint-disable-next-line no-unused-vars
|
10
11
|
export function askCommand(program, context) {
|
11
12
|
program.command('ask')
|
12
13
|
.description('Ask a question')
|
13
14
|
.argument('<message>', 'A message')
|
14
|
-
.option('-f, --file
|
15
|
+
.option('-f, --file [files...]', 'Input files. Content of these files will be added BEFORE the message')
|
15
16
|
// TODO add option consuming extra message as argument
|
16
17
|
.action(async (message, options) => {
|
17
18
|
const preamble = [readInternalPreamble()];
|
18
19
|
const content = [message];
|
19
20
|
if (options.file) {
|
20
|
-
content.push(
|
21
|
+
content.push(readMultipleFilesFromCurrentDir(options.file));
|
21
22
|
}
|
22
23
|
await initConfig();
|
23
24
|
const { askQuestion } = await import('../modules/questionAnsweringModule.js');
|
24
25
|
await askQuestion('sloth-ASK', preamble.join("\n"), content.join("\n"));
|
25
26
|
});
|
26
|
-
}
|
27
|
+
}
|
@@ -6,6 +6,7 @@ import { availableDefaultConfigs, createProjectConfig } from "../config.js";
|
|
6
6
|
* @param {Object} program - The commander program
|
7
7
|
* @param {Object} context - The context object
|
8
8
|
*/
|
9
|
+
// eslint-disable-next-line no-unused-vars
|
9
10
|
export function initCommand(program, context) {
|
10
11
|
program.command('init')
|
11
12
|
.description('Initialize the Gaunt Sloth Assistant in your project. This will write necessary config files.')
|
@@ -13,4 +14,4 @@ export function initCommand(program, context) {
|
|
13
14
|
.action(async (config) => {
|
14
15
|
await createProjectConfig(config);
|
15
16
|
});
|
16
|
-
}
|
17
|
+
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import {Option} from 'commander';
|
2
2
|
import {USER_PROJECT_REVIEW_PREAMBLE} from "../config.js";
|
3
3
|
import {readInternalPreamble, readPreamble} from "../prompt.js";
|
4
|
-
import {
|
4
|
+
import {readMultipleFilesFromCurrentDir} from "../utils.js";
|
5
5
|
import {displayError} from "../consoleUtils.js";
|
6
6
|
|
7
7
|
/**
|
@@ -9,7 +9,8 @@ import {displayError} from "../consoleUtils.js";
|
|
9
9
|
*/
|
10
10
|
const REQUIREMENTS_PROVIDERS = {
|
11
11
|
'jira-legacy': 'jiraIssueLegacyAccessTokenProvider.js',
|
12
|
-
'text': 'text.js'
|
12
|
+
'text': 'text.js',
|
13
|
+
'file': 'file.js'
|
13
14
|
};
|
14
15
|
|
15
16
|
/**
|
@@ -17,7 +18,8 @@ const REQUIREMENTS_PROVIDERS = {
|
|
17
18
|
*/
|
18
19
|
const CONTENT_PROVIDERS = {
|
19
20
|
'gh': 'ghPrDiffProvider.js',
|
20
|
-
'text': 'text.js'
|
21
|
+
'text': 'text.js',
|
22
|
+
'file': 'file.js'
|
21
23
|
};
|
22
24
|
|
23
25
|
export function reviewCommand(program, context) {
|
@@ -27,8 +29,7 @@ export function reviewCommand(program, context) {
|
|
27
29
|
.argument('[contentId]', 'Optional content ID argument to retrieve content with content provider')
|
28
30
|
.alias('r')
|
29
31
|
// TODO add provider to get results of git --no-pager diff
|
30
|
-
|
31
|
-
.option('-f, --file <file>', 'Input file. Content of this file will be added BEFORE the diff, but after requirements')
|
32
|
+
.option('-f, --file [files...]', 'Input files. Content of these files will be added BEFORE the diff, but after requirements')
|
32
33
|
// TODO figure out what to do with this (we probably want to merge it with requirementsId)?
|
33
34
|
.option('-r, --requirements <requirements>', 'Requirements for this review.')
|
34
35
|
.addOption(
|
@@ -46,8 +47,12 @@ export function reviewCommand(program, context) {
|
|
46
47
|
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
47
48
|
const content = [];
|
48
49
|
const requirementsId = options.requirements;
|
49
|
-
const requirementsProvider = options.requirementsProvider
|
50
|
-
|
50
|
+
const requirementsProvider = options.requirementsProvider
|
51
|
+
?? context.config?.review?.requirementsProvider
|
52
|
+
?? context.config?.requirementsProvider;
|
53
|
+
const contentProvider = options.contentProvider
|
54
|
+
?? context.config?.review?.contentProvider
|
55
|
+
?? context.config?.contentProvider;
|
51
56
|
|
52
57
|
// TODO consider calling these in parallel
|
53
58
|
const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
|
@@ -61,7 +66,7 @@ export function reviewCommand(program, context) {
|
|
61
66
|
}
|
62
67
|
|
63
68
|
if (options.file) {
|
64
|
-
content.push(
|
69
|
+
content.push(readMultipleFilesFromCurrentDir(options.file));
|
65
70
|
}
|
66
71
|
if (context.stdin) {
|
67
72
|
content.push(context.stdin);
|
@@ -83,14 +88,16 @@ export function reviewCommand(program, context) {
|
|
83
88
|
new Option('-p, --requirements-provider <requirementsProvider>', 'Requirements provider for this review.')
|
84
89
|
.choices(Object.keys(REQUIREMENTS_PROVIDERS))
|
85
90
|
)
|
86
|
-
.option('-f, --file
|
91
|
+
.option('-f, --file [files...]', 'Input files. Content of these files will be added BEFORE the diff, but after requirements')
|
87
92
|
.action(async (prId, requirementsId, options) => {
|
88
93
|
const {initConfig} = await import("../config.js");
|
89
94
|
await initConfig();
|
90
95
|
|
91
96
|
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
92
97
|
const content = [];
|
93
|
-
const requirementsProvider = options.requirementsProvider
|
98
|
+
const requirementsProvider = options.requirementsProvider
|
99
|
+
?? context.config?.pr?.requirementsProvider
|
100
|
+
?? context.config?.requirementsProvider;
|
94
101
|
|
95
102
|
// Handle requirements
|
96
103
|
const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
|
@@ -99,7 +106,7 @@ export function reviewCommand(program, context) {
|
|
99
106
|
}
|
100
107
|
|
101
108
|
if (options.file) {
|
102
|
-
content.push(
|
109
|
+
content.push(readMultipleFilesFromCurrentDir(options.file));
|
103
110
|
}
|
104
111
|
|
105
112
|
// Get PR diff using the 'gh' provider
|
@@ -126,7 +133,7 @@ export function reviewCommand(program, context) {
|
|
126
133
|
contentId,
|
127
134
|
(context.config?.contentProviderConfig ?? {})[contentProvider],
|
128
135
|
CONTENT_PROVIDERS
|
129
|
-
)
|
136
|
+
);
|
130
137
|
}
|
131
138
|
|
132
139
|
async function getFromProvider(provider, id, config, legitPredefinedProviders) {
|
package/src/config.js
CHANGED
@@ -1,15 +1,29 @@
|
|
1
|
-
import path from "node:path";
|
2
|
-
import
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
1
|
+
import path from "node:path/posix";
|
2
|
+
import {v4 as uuidv4} from "uuid";
|
3
|
+
import {displayDebug, displayError, displayInfo, displayWarning} from "./consoleUtils.js";
|
4
|
+
import {importExternalFile, writeFileIfNotExistsWithMessages} from "./utils.js";
|
5
|
+
import {existsSync, readFileSync} from "node:fs";
|
6
|
+
import {exit, getCurrentDir} from "./systemUtils.js";
|
6
7
|
|
7
|
-
export const
|
8
|
+
export const USER_PROJECT_CONFIG_JS = '.gsloth.config.js';
|
9
|
+
export const USER_PROJECT_CONFIG_JSON = '.gsloth.config.json';
|
10
|
+
export const USER_PROJECT_CONFIG_MJS = '.gsloth.config.mjs';
|
8
11
|
export const SLOTH_INTERNAL_PREAMBLE = '.gsloth.preamble.internal.md';
|
9
12
|
export const USER_PROJECT_REVIEW_PREAMBLE = '.gsloth.preamble.review.md';
|
10
13
|
|
11
14
|
export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'];
|
12
15
|
|
16
|
+
export const DEFAULT_CONFIG = {
|
17
|
+
llm: undefined,
|
18
|
+
contentProvider: "file",
|
19
|
+
requirementsProvider: "file",
|
20
|
+
commands: {
|
21
|
+
pr: {
|
22
|
+
contentProvider: "gh"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
13
27
|
export const slothContext = {
|
14
28
|
/**
|
15
29
|
* Directory where the sloth is installed.
|
@@ -21,43 +35,143 @@ export const slothContext = {
|
|
21
35
|
* index.js should set up this.
|
22
36
|
*/
|
23
37
|
currentDir: null,
|
24
|
-
config:
|
38
|
+
config: DEFAULT_CONFIG,
|
25
39
|
stdin: '',
|
26
40
|
session: {configurable: {thread_id: uuidv4()}}
|
27
41
|
};
|
28
42
|
|
29
43
|
export async function initConfig() {
|
30
|
-
const
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
const currentDir = getCurrentDir();
|
45
|
+
const jsonConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_JSON);
|
46
|
+
const jsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_JS);
|
47
|
+
const mjsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_MJS);
|
48
|
+
|
49
|
+
// Try loading JSON config file first
|
50
|
+
if (existsSync(jsonConfigPath)) {
|
51
|
+
try {
|
52
|
+
const jsonConfig = JSON.parse(readFileSync(jsonConfigPath, 'utf8'));
|
53
|
+
|
54
|
+
// If the config has an LLM with a type, create the appropriate LLM instance
|
55
|
+
if (jsonConfig.llm && jsonConfig.llm.type) {
|
56
|
+
await tryJsonConfig(jsonConfig);
|
57
|
+
} else {
|
58
|
+
slothContext.config = {...slothContext.config, ...jsonConfig};
|
59
|
+
}
|
60
|
+
} catch (e) {
|
61
|
+
displayDebug(e);
|
62
|
+
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JSON}, will try other formats.`);
|
63
|
+
// Continue to try other formats
|
64
|
+
return tryJsConfig();
|
65
|
+
}
|
66
|
+
} else {
|
67
|
+
// JSON config not found, try JS
|
68
|
+
return tryJsConfig();
|
69
|
+
}
|
70
|
+
|
71
|
+
// Helper function to try loading JS config
|
72
|
+
async function tryJsConfig() {
|
73
|
+
if (existsSync(jsConfigPath)) {
|
74
|
+
return importExternalFile(jsConfigPath)
|
75
|
+
.then((i) => i.configure((module) => import(module)))
|
76
|
+
.then((config) => {
|
77
|
+
slothContext.config = {...slothContext.config, ...config};
|
78
|
+
})
|
79
|
+
.catch((e) => {
|
80
|
+
displayDebug(e);
|
81
|
+
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_JS}, will try other formats.`);
|
82
|
+
// Continue to try other formats
|
83
|
+
return tryMjsConfig();
|
84
|
+
});
|
85
|
+
} else {
|
86
|
+
// JS config not found, try MJS
|
87
|
+
return tryMjsConfig();
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
// Helper function to try loading MJS config
|
92
|
+
async function tryMjsConfig() {
|
93
|
+
if (existsSync(mjsConfigPath)) {
|
94
|
+
return importExternalFile(mjsConfigPath)
|
95
|
+
.then((i) => i.configure((module) => import(module)))
|
96
|
+
.then((config) => {
|
97
|
+
slothContext.config = {...slothContext.config, ...config};
|
98
|
+
})
|
99
|
+
.catch((e) => {
|
100
|
+
displayDebug(e);
|
101
|
+
displayError(`Failed to read config from ${USER_PROJECT_CONFIG_MJS}.`);
|
102
|
+
displayError(`No valid configuration found. Please create a valid configuration file.`);
|
103
|
+
exit();
|
104
|
+
});
|
105
|
+
} else {
|
106
|
+
// No config files found
|
107
|
+
displayError(
|
108
|
+
'No configuration file found. Please create one of: '
|
109
|
+
+ `${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} `
|
110
|
+
+ 'in your project directory.'
|
111
|
+
);
|
112
|
+
exit();
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
// Process JSON LLM config by creating the appropriate LLM instance
|
118
|
+
export async function tryJsonConfig(jsonConfig) {
|
119
|
+
const llmConfig = jsonConfig.llm;
|
120
|
+
const llmType = llmConfig.type.toLowerCase();
|
121
|
+
|
122
|
+
// Check if the LLM type is in availableDefaultConfigs
|
123
|
+
if (!availableDefaultConfigs.includes(llmType)) {
|
124
|
+
displayError(`Unsupported LLM type: ${llmType}. Available types are: ${availableDefaultConfigs.join(', ')}`);
|
125
|
+
slothContext.config = {...slothContext.config, ...jsonConfig};
|
126
|
+
return;
|
127
|
+
}
|
128
|
+
|
129
|
+
try {
|
130
|
+
// Import the appropriate config module based on the LLM type
|
131
|
+
try {
|
132
|
+
const configModule = await import(`./configs/${llmType}.js`);
|
133
|
+
if (configModule.processJsonConfig) {
|
134
|
+
jsonConfig.llm = await configModule.processJsonConfig(llmConfig);
|
135
|
+
} else {
|
136
|
+
displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
|
137
|
+
}
|
138
|
+
} catch (importError) {
|
139
|
+
displayDebug(importError);
|
140
|
+
displayWarning(`Could not import config module for ${llmType}.`);
|
141
|
+
}
|
142
|
+
} catch (error) {
|
143
|
+
displayDebug(error);
|
144
|
+
displayError(`Error creating LLM instance for type ${llmType}.`);
|
145
|
+
}
|
146
|
+
|
147
|
+
slothContext.config = {...slothContext.config, ...jsonConfig};
|
41
148
|
}
|
42
149
|
|
43
150
|
export async function createProjectConfig(configType) {
|
44
151
|
displayInfo(`Setting up your project\n`);
|
45
152
|
writeProjectReviewPreamble();
|
46
|
-
displayWarning(`Make sure you add as much detail as possible to your ${USER_PROJECT_REVIEW_PREAMBLE}.\n`)
|
153
|
+
displayWarning(`Make sure you add as much detail as possible to your ${USER_PROJECT_REVIEW_PREAMBLE}.\n`);
|
154
|
+
|
155
|
+
// Check if the config type is in availableDefaultConfigs
|
156
|
+
if (!availableDefaultConfigs.includes(configType)) {
|
157
|
+
displayError(`Unsupported config type: ${configType}. Available types are: ${availableDefaultConfigs.join(', ')}`);
|
158
|
+
exit(1);
|
159
|
+
return;
|
160
|
+
}
|
47
161
|
|
48
162
|
displayInfo(`Creating project config for ${configType}`);
|
49
163
|
const vendorConfig = await import(`./configs/${configType}.js`);
|
50
|
-
vendorConfig.init(
|
164
|
+
vendorConfig.init(USER_PROJECT_CONFIG_JSON, slothContext);
|
51
165
|
}
|
52
166
|
|
53
|
-
function writeProjectReviewPreamble() {
|
167
|
+
export function writeProjectReviewPreamble() {
|
54
168
|
let reviewPreamblePath = path.join(slothContext.currentDir, USER_PROJECT_REVIEW_PREAMBLE);
|
55
169
|
writeFileIfNotExistsWithMessages(
|
56
170
|
reviewPreamblePath,
|
57
171
|
'You are doing generic code review.\n'
|
58
|
-
+ 'Important! Please remind user to prepare proper AI preamble in
|
172
|
+
+ ' Important! Please remind user to prepare proper AI preamble in'
|
59
173
|
+ USER_PROJECT_REVIEW_PREAMBLE
|
60
|
-
+ 'for this project. Use decent amount of ⚠️ to highlight lack of config.
|
61
|
-
+ 'Explicitly mention `'+ USER_PROJECT_REVIEW_PREAMBLE + '`.'
|
174
|
+
+ ' for this project. Use decent amount of ⚠️ to highlight lack of config.'
|
175
|
+
+ ' Explicitly mention `' + USER_PROJECT_REVIEW_PREAMBLE + '`.'
|
62
176
|
);
|
63
|
-
}
|
177
|
+
}
|
package/src/configs/anthropic.js
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
import {writeFileIfNotExistsWithMessages} from "../utils.js";
|
2
2
|
import path from "node:path";
|
3
3
|
import {displayWarning} from "../consoleUtils.js";
|
4
|
-
import {
|
4
|
+
import { env } from "../systemUtils.js";
|
5
5
|
|
6
|
-
|
6
|
+
// Function to process JSON config and create Anthropic LLM instance
|
7
|
+
export async function processJsonConfig(llmConfig) {
|
8
|
+
const anthropic = await import('@langchain/anthropic');
|
9
|
+
// Use environment variable if available, otherwise use the config value
|
10
|
+
const anthropicApiKey = env.ANTHROPIC_API_KEY || llmConfig.apiKey;
|
11
|
+
return new anthropic.ChatAnthropic({
|
12
|
+
apiKey: anthropicApiKey,
|
13
|
+
model: llmConfig.model || "claude-3-7-sonnet-20250219"
|
14
|
+
});
|
15
|
+
}
|
16
|
+
|
17
|
+
const jsContent = `/* eslint-disable */
|
7
18
|
export async function configure(importFunction, global) {
|
8
19
|
// this is going to be imported from sloth dependencies,
|
9
20
|
// but can potentially be pulled from global node modules or from this project
|
@@ -12,14 +23,26 @@ export async function configure(importFunction, global) {
|
|
12
23
|
return {
|
13
24
|
llm: new anthropic.ChatAnthropic({
|
14
25
|
apiKey: process.env.ANTHROPIC_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
|
15
|
-
model: "claude-3-
|
26
|
+
model: "claude-3-7-sonnet-20250219" // Don't forget to check new models availability.
|
16
27
|
})
|
17
28
|
};
|
18
29
|
}
|
19
30
|
`;
|
20
31
|
|
32
|
+
const jsonContent = `{
|
33
|
+
"llm": {
|
34
|
+
"type": "anthropic",
|
35
|
+
"apiKey": "your-api-key-here",
|
36
|
+
"model": "claude-3-7-sonnet-20250219"
|
37
|
+
}
|
38
|
+
}`;
|
39
|
+
|
21
40
|
export function init(configFileName, context) {
|
22
41
|
path.join(context.currentDir, configFileName);
|
42
|
+
|
43
|
+
// Determine which content to use based on file extension
|
44
|
+
const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
|
45
|
+
|
23
46
|
writeFileIfNotExistsWithMessages(configFileName, content);
|
24
|
-
displayWarning(`You need to update your ${
|
25
|
-
}
|
47
|
+
displayWarning(`You need to update your ${configFileName} to add your Anthropic API key.`);
|
48
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import {displayWarning} from "../consoleUtils.js";
|
2
|
+
|
3
|
+
// Function to process JSON config and create Fake LLM instance for testing
|
4
|
+
export async function processJsonConfig(llmConfig) {
|
5
|
+
if (llmConfig.responses) {
|
6
|
+
const test = await import('@langchain/core/utils/testing');
|
7
|
+
return new test.FakeListChatModel({
|
8
|
+
responses: llmConfig.responses
|
9
|
+
});
|
10
|
+
}
|
11
|
+
displayWarning("Fake LLM requires 'responses' array in config");
|
12
|
+
return null;
|
13
|
+
}
|
14
|
+
|
15
|
+
// No init function needed for fake LLM as it's only used for testing
|
package/src/configs/groq.js
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
import {writeFileIfNotExistsWithMessages} from "../utils.js";
|
2
2
|
import path from "node:path";
|
3
3
|
import {displayInfo, displayWarning} from "../consoleUtils.js";
|
4
|
-
import {
|
4
|
+
import {env} from "../systemUtils.js";
|
5
5
|
|
6
|
-
|
6
|
+
// Function to process JSON config and create Groq LLM instance
|
7
|
+
export async function processJsonConfig(llmConfig) {
|
8
|
+
const groq = await import('@langchain/groq');
|
9
|
+
// Use environment variable if available, otherwise use the config value
|
10
|
+
const groqApiKey = env.GROQ_API_KEY || llmConfig.apiKey;
|
11
|
+
return new groq.ChatGroq({
|
12
|
+
apiKey: groqApiKey,
|
13
|
+
model: llmConfig.model || "deepseek-r1-distill-llama-70b"
|
14
|
+
});
|
15
|
+
}
|
16
|
+
|
17
|
+
const jsContent = `/* eslint-disable */
|
7
18
|
export async function configure(importFunction, global) {
|
8
19
|
// this is going to be imported from sloth dependencies,
|
9
20
|
// but can potentially be pulled from global node modules or from this project
|
@@ -17,9 +28,21 @@ export async function configure(importFunction, global) {
|
|
17
28
|
}
|
18
29
|
`;
|
19
30
|
|
31
|
+
const jsonContent = `{
|
32
|
+
"llm": {
|
33
|
+
"type": "groq",
|
34
|
+
"model": "deepseek-r1-distill-llama-70b",
|
35
|
+
"apiKey": "your-api-key-here"
|
36
|
+
}
|
37
|
+
}`;
|
38
|
+
|
20
39
|
export function init(configFileName, context) {
|
21
40
|
path.join(context.currentDir, configFileName);
|
41
|
+
|
42
|
+
// Determine which content to use based on file extension
|
43
|
+
const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
|
44
|
+
|
22
45
|
writeFileIfNotExistsWithMessages(configFileName, content);
|
23
46
|
displayInfo(`You can define GROQ_API_KEY environment variable with your Groq API key and it will work with default model.`);
|
24
|
-
displayWarning(`You need to edit your ${
|
25
|
-
}
|
47
|
+
displayWarning(`You need to edit your ${configFileName} to configure model.`);
|
48
|
+
}
|
package/src/configs/vertexai.js
CHANGED
@@ -2,7 +2,7 @@ import {writeFileIfNotExistsWithMessages} from "../utils.js";
|
|
2
2
|
import path from "node:path";
|
3
3
|
import {displayWarning} from "../consoleUtils.js";
|
4
4
|
|
5
|
-
const
|
5
|
+
const jsContent = `/* eslint-disable */
|
6
6
|
export async function configure(importFunction, global) {
|
7
7
|
// this is going to be imported from sloth dependencies,
|
8
8
|
// but can potentially be pulled from global node modules or from this project
|
@@ -19,8 +19,29 @@ export async function configure(importFunction, global) {
|
|
19
19
|
}
|
20
20
|
`;
|
21
21
|
|
22
|
+
const jsonContent = `{
|
23
|
+
"llm": {
|
24
|
+
"type": "vertexai",
|
25
|
+
"model": "gemini-2.5-pro-exp-03-25",
|
26
|
+
"temperature": 0
|
27
|
+
}
|
28
|
+
}`;
|
29
|
+
|
22
30
|
export function init(configFileName, context) {
|
23
31
|
path.join(context.currentDir, configFileName);
|
32
|
+
|
33
|
+
// Determine which content to use based on file extension
|
34
|
+
const content = configFileName.endsWith('.json') ? jsonContent : jsContent;
|
35
|
+
|
24
36
|
writeFileIfNotExistsWithMessages(configFileName, content);
|
25
37
|
displayWarning("For Google VertexAI you likely to need to do `gcloud auth login` and `gcloud auth application-default login`.");
|
26
|
-
}
|
38
|
+
}
|
39
|
+
|
40
|
+
// Function to process JSON config and create VertexAI LLM instance
|
41
|
+
export async function processJsonConfig(llmConfig) {
|
42
|
+
const vertexAi = await import('@langchain/google-vertexai');
|
43
|
+
return new vertexAi.ChatVertexAI({
|
44
|
+
...llmConfig,
|
45
|
+
model: llmConfig.model || "gemini-pro"
|
46
|
+
});
|
47
|
+
}
|