gaunt-sloth-assistant 0.0.4 → 0.0.8
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/.gsloth.preamble.internal.md +0 -0
- package/.gsloth.preamble.review.md +0 -0
- package/LICENSE +0 -0
- package/README.md +19 -2
- package/RELEASE-HOWTO.md +18 -0
- package/ROADMAP.md +2 -0
- package/index.js +2 -2
- package/package.json +12 -3
- package/spec/codeReview.spec.js +22 -0
- package/spec/support/jasmine.mjs +14 -0
- package/src/codeReview.js +30 -24
- package/src/config.js +16 -11
- package/src/configs/anthropic.js +1 -1
- package/src/configs/groq.js +25 -0
- package/src/configs/vertexai.js +0 -0
- package/src/consoleUtils.js +0 -0
- package/src/prompt.js +9 -1
- package/src/questionAnswering.js +0 -0
- package/src/utils.js +35 -7
- package/testMessage.txt +0 -0
package/.eslint.config.mjs
CHANGED
File without changes
|
package/.github/dependabot.yml
CHANGED
File without changes
|
File without changes
|
File without changes
|
package/LICENSE
CHANGED
File without changes
|
package/README.md
CHANGED
@@ -76,7 +76,7 @@ Make sure you edit `.gsloth.config.js` and set up your key.
|
|
76
76
|
|
77
77
|
### Further configuration
|
78
78
|
|
79
|
-
Currently
|
79
|
+
Currently vertexai, anthropic and groq can be configured with `gsloth init`.
|
80
80
|
|
81
81
|
Populate `.gsloth.preamble.review.md` with your project details and quality requirements.
|
82
82
|
Proper preamble is a paramount for good inference.
|
@@ -98,7 +98,7 @@ export async function configure(importFunction, global) {
|
|
98
98
|
const anthropic = await importFunction('@langchain/anthropic');
|
99
99
|
return {
|
100
100
|
llm: new anthropic.ChatAnthropic({
|
101
|
-
apiKey:
|
101
|
+
apiKey: process.env.ANTHROPIC_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
|
102
102
|
model: "claude-3-5-sonnet-20241022"
|
103
103
|
})
|
104
104
|
};
|
@@ -127,6 +127,23 @@ export async function configure(importFunction, global) {
|
|
127
127
|
}
|
128
128
|
```
|
129
129
|
|
130
|
+
**Example of .gsloth.config.js for Groq**
|
131
|
+
VertexAI usually needs `gcloud auth application-default login`
|
132
|
+
(or both `gcloud auth login` and `gcloud auth application-default login`) and does not need any separate API keys.
|
133
|
+
```javascript
|
134
|
+
export async function configure(importFunction, global) {
|
135
|
+
// this is going to be imported from sloth dependencies,
|
136
|
+
// but can potentially be pulled from global node modules or from this project
|
137
|
+
const groq = await importFunction('@langchain/groq');
|
138
|
+
return {
|
139
|
+
llm: new groq.ChatGroq({
|
140
|
+
model: "deepseek-r1-distill-llama-70b", // Check other models available
|
141
|
+
apiKey: process.env.GROQ_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
|
142
|
+
})
|
143
|
+
};
|
144
|
+
}
|
145
|
+
```
|
146
|
+
|
130
147
|
The configure function should simply return instance of langchain [chat model](https://v03.api.js.langchain.com/classes/_langchain_core.language_models_chat_models.BaseChatModel.html).
|
131
148
|
See [Langchain documentation](https://js.langchain.com/docs/tutorials/llm_chain/) for more details.
|
132
149
|
|
package/RELEASE-HOWTO.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Make sure `npm config set git-tag-version true`
|
2
|
+
|
3
|
+
```shell
|
4
|
+
npm version patch
|
5
|
+
git push
|
6
|
+
git push --tags
|
7
|
+
```
|
8
|
+
|
9
|
+
Note the release version and do
|
10
|
+
```shell
|
11
|
+
gh release create --generate-notes
|
12
|
+
```
|
13
|
+
|
14
|
+
Publish to NPM
|
15
|
+
```shell
|
16
|
+
npm login
|
17
|
+
npm publish
|
18
|
+
```
|
package/ROADMAP.md
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
Doing the following below and making it work stably should be sufficient to call it version 1.
|
6
6
|
|
7
7
|
### Add tests and gain reasonable coverage
|
8
|
+
### Configure eslint for code quality checks
|
9
|
+
### Automate release process
|
8
10
|
### Add project init command
|
9
11
|
Add a command to init certain model in certain project, for example `gsloth init gemini`
|
10
12
|
or `gsloth init` and select one of the provided options.
|
package/index.js
CHANGED
@@ -39,7 +39,7 @@ program.command('pr')
|
|
39
39
|
displayError('`gsloth pr` does not expect stdin, use `gsloth review` instead');
|
40
40
|
return;
|
41
41
|
}
|
42
|
-
displayInfo(
|
42
|
+
displayInfo(`Starting review of PR ${pr}`);
|
43
43
|
const diff = await getPrDiff(pr);
|
44
44
|
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
45
45
|
const content = [diff];
|
@@ -55,7 +55,7 @@ program.command('review')
|
|
55
55
|
.option('-f, --file <file>', 'Input file. Context of this file will be added BEFORE the diff')
|
56
56
|
// TODO add option consuming extra message as argument
|
57
57
|
.action(async (options) => {
|
58
|
-
if (!slothContext.stdin
|
58
|
+
if (!slothContext.stdin && !options.file) {
|
59
59
|
displayError('gsloth review expects stdin with github diff stdin or a file');
|
60
60
|
return
|
61
61
|
}
|
package/package.json
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
{
|
2
2
|
"name": "gaunt-sloth-assistant",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.8",
|
4
4
|
"description": "",
|
5
5
|
"license": "MIT",
|
6
6
|
"author": "Andrew Kondratev",
|
7
7
|
"type": "module",
|
8
8
|
"main": "index.js",
|
9
|
+
"repository": "github:andruhon/gaunt-sloth-assistant",
|
10
|
+
"engines": {
|
11
|
+
"node": ">=22.0.0",
|
12
|
+
"npm": ">=10.9.0"
|
13
|
+
},
|
9
14
|
"scripts": {
|
10
|
-
"test": "
|
11
|
-
"test-run": "node --trace-deprecation index.js ask \"status check\""
|
15
|
+
"test": "jasmine"
|
12
16
|
},
|
13
17
|
"bin": {
|
14
18
|
"gsloth": "index.js"
|
@@ -18,10 +22,15 @@
|
|
18
22
|
"@langchain/anthropic": "^0.3.17",
|
19
23
|
"@langchain/core": "^0.3.43",
|
20
24
|
"@langchain/google-vertexai": "^0.2.3",
|
25
|
+
"@langchain/groq": "^0.2.2",
|
21
26
|
"@langchain/langgraph": "^0.2.64",
|
22
27
|
"@types/node": "^22.14.1",
|
23
28
|
"chalk": "^5.4.1",
|
24
29
|
"commander": "^13.1.0",
|
25
30
|
"uuid": "^11.1.0"
|
31
|
+
},
|
32
|
+
"devDependencies": {
|
33
|
+
"jasmine": "^5.6.0",
|
34
|
+
"jest": "^29.7.0"
|
26
35
|
}
|
27
36
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { reviewInner } from '../src/codeReview.js';
|
2
|
+
import { slothContext } from '../src/config.js';
|
3
|
+
import { FakeListChatModel } from "@langchain/core/utils/testing";
|
4
|
+
|
5
|
+
describe('codeReview', () => {
|
6
|
+
|
7
|
+
it('should invoke LLM', async () => {
|
8
|
+
// Setup mock for slothContext
|
9
|
+
const testContext = {...slothContext,
|
10
|
+
config: {
|
11
|
+
llm: new FakeListChatModel({
|
12
|
+
responses: ["First LLM message", "Second LLM message"],
|
13
|
+
})
|
14
|
+
}
|
15
|
+
};
|
16
|
+
|
17
|
+
// Test the function
|
18
|
+
const output = await reviewInner(testContext, () => {}, 'test-preamble', 'test-diff');
|
19
|
+
expect(output).toBe("First LLM message");
|
20
|
+
});
|
21
|
+
|
22
|
+
});
|
package/src/codeReview.js
CHANGED
@@ -7,17 +7,36 @@ import {
|
|
7
7
|
} from "@langchain/langgraph";
|
8
8
|
import { writeFileSync } from "node:fs";
|
9
9
|
import path from "node:path";
|
10
|
-
import {initConfig, slothContext} from "./config.js";
|
10
|
+
import { initConfig, slothContext } from "./config.js";
|
11
11
|
import { display, displayError, displaySuccess } from "./consoleUtils.js";
|
12
|
-
import { fileSafeLocalDate, toFileSafeString } from "./utils.js";
|
13
|
-
|
14
|
-
await initConfig();
|
12
|
+
import { fileSafeLocalDate, toFileSafeString, ProgressIndicator } from "./utils.js";
|
15
13
|
|
16
14
|
export async function review(source, preamble, diff) {
|
15
|
+
await initConfig();
|
16
|
+
const progressIndicator = new ProgressIndicator("Reviewing.");
|
17
|
+
const outputContent = await reviewInner(slothContext, () => progressIndicator.indicate(), preamble, diff);
|
18
|
+
const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
|
19
|
+
process.stdout.write("\n");
|
20
|
+
display(`writing ${filePath}`);
|
21
|
+
process.stdout.write("\n");
|
22
|
+
// TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
|
23
|
+
display(outputContent);
|
24
|
+
try {
|
25
|
+
writeFileSync(filePath, outputContent);
|
26
|
+
displaySuccess(`This report can be found in ${filePath}`);
|
27
|
+
} catch (error) {
|
28
|
+
displayError(`Failed to write review to file: ${filePath}`);
|
29
|
+
displayError(error.message);
|
30
|
+
// Consider if you want to exit or just log the error
|
31
|
+
// process.exit(1);
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
export async function reviewInner(context, indicateProgress, preamble, diff) {
|
17
36
|
// This node receives the current state (messages) and invokes the LLM
|
18
37
|
const callModel = async (state) => {
|
19
38
|
// state.messages will contain the list including the system preamble and user diff
|
20
|
-
const response = await
|
39
|
+
const response = await context.config.llm.invoke(state.messages);
|
21
40
|
// MessagesAnnotation expects the node to return the new message(s) to be added to the state.
|
22
41
|
// Wrap the response in an array if it's a single message object.
|
23
42
|
return { messages: response };
|
@@ -48,24 +67,11 @@ export async function review(source, preamble, diff) {
|
|
48
67
|
},
|
49
68
|
];
|
50
69
|
|
51
|
-
|
52
|
-
|
53
|
-
const
|
54
|
-
const
|
55
|
-
display(`writing ${filePath}`);
|
56
|
-
// FIXME this looks ugly, there should be other way
|
57
|
-
const outputContent = output.messages[output.messages.length - 1].content;
|
70
|
+
indicateProgress();
|
71
|
+
// TODO create proper progress indicator for async tasks.
|
72
|
+
const progress = setInterval(() => indicateProgress(), 1000);
|
73
|
+
const output = await app.invoke({messages}, context.session);
|
58
74
|
clearInterval(progress);
|
59
|
-
|
60
|
-
|
61
|
-
display(outputContent);
|
62
|
-
try {
|
63
|
-
writeFileSync(filePath, outputContent);
|
64
|
-
displaySuccess(`This report can be found in ${filePath}`);
|
65
|
-
} catch (error) {
|
66
|
-
displayError(`Failed to write review to file: ${filePath}`);
|
67
|
-
displayError(error.message);
|
68
|
-
// Consider if you want to exit or just log the error
|
69
|
-
// process.exit(1);
|
70
|
-
}
|
75
|
+
// FIXME this looks ugly, there should be other way
|
76
|
+
return output.messages[output.messages.length - 1].content;
|
71
77
|
}
|
package/src/config.js
CHANGED
@@ -1,16 +1,14 @@
|
|
1
|
-
import path
|
1
|
+
import path from "node:path";
|
2
2
|
import url from "node:url";
|
3
|
-
import {v4 as uuidv4} from "uuid";
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
import {write, writeFileSync, existsSync} from "node:fs";
|
7
|
-
import {writeFileIfNotExistsWithMessages} from "./utils.js";
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
4
|
+
import { displayError, displayInfo, displayWarning } from "./consoleUtils.js";
|
5
|
+
import { writeFileIfNotExistsWithMessages } from "./utils.js";
|
8
6
|
|
9
7
|
export const USER_PROJECT_CONFIG_FILE = '.gsloth.config.js'
|
10
8
|
export const SLOTH_INTERNAL_PREAMBLE = '.gsloth.preamble.internal.md';
|
11
9
|
export const USER_PROJECT_REVIEW_PREAMBLE = '.gsloth.preamble.review.md';
|
12
10
|
|
13
|
-
export const availableDefaultConfigs = ['vertexai', 'anthropic'];
|
11
|
+
export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'];
|
14
12
|
|
15
13
|
export const slothContext = {
|
16
14
|
/**
|
@@ -29,10 +27,17 @@ export const slothContext = {
|
|
29
27
|
};
|
30
28
|
|
31
29
|
export async function initConfig() {
|
32
|
-
const configFileUrl = url.pathToFileURL(path.join(process.cwd(), USER_PROJECT_CONFIG_FILE));
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
const configFileUrl = url.pathToFileURL(path.join(process.cwd(), USER_PROJECT_CONFIG_FILE));
|
31
|
+
return import(configFileUrl)
|
32
|
+
.then((i) => i.configure((module) => import(module)))
|
33
|
+
.then((config) => {
|
34
|
+
slothContext.config = {...config};
|
35
|
+
})
|
36
|
+
.catch((e) => {
|
37
|
+
console.log(e);
|
38
|
+
displayError(`Failed to read config, make sure ${configFileUrl} contains valid JavaScript.`);
|
39
|
+
process.exit();
|
40
|
+
});
|
36
41
|
}
|
37
42
|
|
38
43
|
export async function createProjectConfig(configType) {
|
package/src/configs/anthropic.js
CHANGED
@@ -11,7 +11,7 @@ export async function configure(importFunction, global) {
|
|
11
11
|
const anthropic = await importFunction('@langchain/anthropic');
|
12
12
|
return {
|
13
13
|
llm: new anthropic.ChatAnthropic({
|
14
|
-
apiKey:
|
14
|
+
apiKey: process.env.ANTHROPIC_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
|
15
15
|
model: "claude-3-5-sonnet-20241022" // Don't forget to check new models availability.
|
16
16
|
})
|
17
17
|
};
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import {writeFileIfNotExistsWithMessages} from "../utils.js";
|
2
|
+
import path from "node:path";
|
3
|
+
import {displayInfo, displayWarning} from "../consoleUtils.js";
|
4
|
+
import {USER_PROJECT_CONFIG_FILE} from "../config.js";
|
5
|
+
|
6
|
+
const content = `/* eslint-disable */
|
7
|
+
export async function configure(importFunction, global) {
|
8
|
+
// this is going to be imported from sloth dependencies,
|
9
|
+
// but can potentially be pulled from global node modules or from this project
|
10
|
+
const groq = await importFunction('@langchain/groq');
|
11
|
+
return {
|
12
|
+
llm: new groq.ChatGroq({
|
13
|
+
model: "deepseek-r1-distill-llama-70b", // Check other models available
|
14
|
+
apiKey: process.env.GROQ_API_KEY, // Default value, but you can provide the key in many different ways, even as literal
|
15
|
+
})
|
16
|
+
};
|
17
|
+
}
|
18
|
+
`;
|
19
|
+
|
20
|
+
export function init(configFileName, context) {
|
21
|
+
path.join(context.currentDir, configFileName);
|
22
|
+
writeFileIfNotExistsWithMessages(configFileName, content);
|
23
|
+
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 ${USER_PROJECT_CONFIG_FILE} to to configure model.`);
|
25
|
+
}
|
package/src/configs/vertexai.js
CHANGED
File without changes
|
package/src/consoleUtils.js
CHANGED
File without changes
|
package/src/prompt.js
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import {resolve} from "node:path";
|
2
2
|
import {SLOTH_INTERNAL_PREAMBLE, slothContext} from "./config.js";
|
3
3
|
import {readFileSyncWithMessages, spawnCommand} from "./utils.js";
|
4
|
+
import { displayError } from "./consoleUtils.js";
|
4
5
|
|
5
6
|
export function readInternalPreamble() {
|
6
7
|
const filePath = resolve(slothContext.installDir, SLOTH_INTERNAL_PREAMBLE);
|
@@ -18,8 +19,15 @@ export function readPreamble(preambleFilename) {
|
|
18
19
|
|
19
20
|
/**
|
20
21
|
* This function expects https://cli.github.com/ to be installed and authenticated.
|
22
|
+
* It does something like `gh pr diff 42`
|
21
23
|
*/
|
22
24
|
export async function getPrDiff(pr) {
|
23
25
|
// TODO makes sense to check if gh is available and authenticated
|
24
|
-
|
26
|
+
try {
|
27
|
+
return await spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
|
28
|
+
} catch (e) {
|
29
|
+
displayError(e.toString());
|
30
|
+
displayError(`Failed to call "gh pr diff ${pr}", see message above for details.`);
|
31
|
+
process.exit();
|
32
|
+
}
|
25
33
|
}
|
package/src/questionAnswering.js
CHANGED
File without changes
|
package/src/utils.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import {display, displayError, displaySuccess, displayWarning} from "./consoleUtils.js";
|
2
2
|
import {existsSync, readFileSync, writeFileSync} from "node:fs";
|
3
|
-
import {slothContext
|
3
|
+
import {slothContext} from "./config.js";
|
4
4
|
import {resolve} from "node:path";
|
5
5
|
import {spawn} from "node:child_process";
|
6
6
|
|
@@ -63,7 +63,7 @@ export function readStdin(program) {
|
|
63
63
|
}
|
64
64
|
});
|
65
65
|
process.stdin.on('end', function() {
|
66
|
-
|
66
|
+
process.stdout.write('.\n');
|
67
67
|
program.parse(process.argv);
|
68
68
|
});
|
69
69
|
}
|
@@ -71,18 +71,27 @@ export function readStdin(program) {
|
|
71
71
|
|
72
72
|
export async function spawnCommand(command, args, progressMessage, successMessage) {
|
73
73
|
return new Promise((resolve, reject) => {
|
74
|
-
const out = {stdout: ''};
|
74
|
+
const out = {stdout: '', stderr: ''};
|
75
75
|
const spawned = spawn(command, args);
|
76
|
-
spawned.stdout.on('data', async (stdoutChunk) => {
|
76
|
+
spawned.stdout.on('data', async (stdoutChunk, dd) => {
|
77
77
|
display(progressMessage);
|
78
78
|
out.stdout += stdoutChunk.toString();
|
79
79
|
});
|
80
|
+
spawned.stderr.on('data', (err) => {
|
81
|
+
displayError(progressMessage);
|
82
|
+
out.stderr += err.toString();
|
83
|
+
})
|
80
84
|
spawned.on('error', (err) => {
|
81
|
-
reject(err);
|
85
|
+
reject(err.toString());
|
82
86
|
})
|
83
87
|
spawned.on('close', (code) => {
|
84
|
-
|
85
|
-
|
88
|
+
if (code === 0) {
|
89
|
+
display(successMessage);
|
90
|
+
resolve(out.stdout);
|
91
|
+
} else {
|
92
|
+
displayError(`Failed to spawn command with code ${code}`);
|
93
|
+
reject(out.stdout + ' ' + out.stderr);
|
94
|
+
}
|
86
95
|
});
|
87
96
|
});
|
88
97
|
}
|
@@ -92,3 +101,22 @@ export function getSlothVersion() {
|
|
92
101
|
const projectJson = readFileSync(jsonPath, { encoding: 'utf8' });
|
93
102
|
return JSON.parse(projectJson).version;
|
94
103
|
}
|
104
|
+
|
105
|
+
|
106
|
+
export class ProgressIndicator {
|
107
|
+
|
108
|
+
constructor(initialMessage) {
|
109
|
+
this.hasBeenCalled = false;
|
110
|
+
this.initialMessage = initialMessage;
|
111
|
+
}
|
112
|
+
|
113
|
+
indicate() {
|
114
|
+
if (this.hasBeenCalled) {
|
115
|
+
process.stdout.write('.');
|
116
|
+
} else {
|
117
|
+
this.hasBeenCalled = true;
|
118
|
+
process.stdout.write(this.initialMessage);
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
}
|
package/testMessage.txt
CHANGED
File without changes
|