gaunt-sloth-assistant 0.2.2 → 0.3.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.
- package/.gsloth.guidelines.md +127 -0
- package/.gsloth.review.md +13 -0
- package/README.md +1 -1
- package/dist/commands/askCommand.js +4 -4
- package/dist/commands/askCommand.js.map +1 -1
- package/dist/commands/reviewCommand.js +20 -8
- package/dist/commands/reviewCommand.js.map +1 -1
- package/dist/config.d.ts +8 -4
- package/dist/config.js +49 -19
- package/dist/config.js.map +1 -1
- package/dist/configs/anthropic.d.ts +2 -3
- package/dist/configs/anthropic.js.map +1 -1
- package/dist/configs/vertexai.d.ts +2 -2
- package/dist/configs/vertexai.js.map +1 -1
- package/dist/filePathUtils.d.ts +25 -0
- package/dist/filePathUtils.js +66 -0
- package/dist/filePathUtils.js.map +1 -0
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/llmUtils.d.ts +4 -0
- package/dist/llmUtils.js +39 -0
- package/dist/llmUtils.js.map +1 -0
- package/dist/modules/questionAnsweringModule.d.ts +0 -11
- package/dist/modules/questionAnsweringModule.js +8 -47
- package/dist/modules/questionAnsweringModule.js.map +1 -1
- package/dist/modules/reviewModule.d.ts +0 -3
- package/dist/modules/reviewModule.js +8 -38
- package/dist/modules/reviewModule.js.map +1 -1
- package/dist/prompt.d.ts +3 -2
- package/dist/prompt.js +28 -12
- package/dist/prompt.js.map +1 -1
- package/dist/systemUtils.d.ts +10 -0
- package/dist/systemUtils.js +34 -0
- package/dist/systemUtils.js.map +1 -1
- package/dist/utils.d.ts +15 -5
- package/dist/utils.js +74 -47
- package/dist/utils.js.map +1 -1
- package/docs/CONFIGURATION.md +25 -4
- package/docs/RELEASE-HOWTO.md +13 -5
- package/package.json +1 -1
- package/src/commands/askCommand.ts +4 -4
- package/src/commands/reviewCommand.ts +21 -9
- package/src/config.ts +59 -28
- package/src/configs/anthropic.ts +5 -3
- package/src/configs/vertexai.ts +2 -2
- package/src/filePathUtils.ts +79 -0
- package/src/index.ts +12 -3
- package/src/llmUtils.ts +54 -0
- package/src/modules/questionAnsweringModule.ts +10 -66
- package/src/modules/reviewModule.ts +8 -60
- package/src/prompt.ts +35 -17
- package/src/systemUtils.ts +38 -0
- package/src/utils.ts +82 -50
- package/.gsloth.preamble.review.md +0 -125
- package/UX-RESEARCH.md +0 -78
package/docs/CONFIGURATION.md
CHANGED
@@ -1,19 +1,40 @@
|
|
1
1
|
# Configuration
|
2
2
|
|
3
|
-
Populate `.gsloth.
|
3
|
+
Populate `.gsloth.guidelines.md` with your project details and quality requirements.
|
4
4
|
Proper preamble is a paramount for good inference.
|
5
|
-
Check [.gsloth.
|
5
|
+
Check [.gsloth.guidelines.md](../.gsloth.guidelines.md) for example.
|
6
6
|
|
7
7
|
Your project should have the following files in order for gsloth to function:
|
8
8
|
- Configuration file (one of):
|
9
9
|
- `.gsloth.config.js` (JavaScript module)
|
10
10
|
- `.gsloth.config.json` (JSON file)
|
11
11
|
- `.gsloth.config.mjs` (JavaScript module with explicit module extension)
|
12
|
-
- `.gsloth.
|
12
|
+
- `.gsloth.guidelines.md`
|
13
13
|
|
14
|
-
> Gaunt Sloth currently only functions from the directory which has one of the configuration files and `.gsloth.
|
14
|
+
> Gaunt Sloth currently only functions from the directory which has one of the configuration files and `.gsloth.guidelines.md`.
|
15
15
|
> Global configuration to invoke gsloth anywhere is in [ROADMAP](../ROADMAP.md).
|
16
16
|
|
17
|
+
## Using .gsloth Directory
|
18
|
+
|
19
|
+
For a tidier project structure, you can create a `.gsloth` directory in your project root. When this directory exists, gsloth will:
|
20
|
+
|
21
|
+
1. Write all output files (like responses from commands) to the `.gsloth` directory instead of the project root
|
22
|
+
2. Look for configuration files in `.gsloth/.gsloth-settings/` subdirectory
|
23
|
+
|
24
|
+
Example directory structure when using the `.gsloth` directory:
|
25
|
+
|
26
|
+
```
|
27
|
+
.gsloth/.gsloth-settings/.gsloth-config.json
|
28
|
+
.gsloth/.gsloth-settings/.gsloth.guidelines.md
|
29
|
+
.gsloth/.gsloth-settings/.gsloth.review.md
|
30
|
+
.gsloth/gth_2025-05-18_09-34-38_ASK.md
|
31
|
+
.gsloth/gth_2025-05-18_22-09-00_PR-22.md
|
32
|
+
```
|
33
|
+
|
34
|
+
If the `.gsloth` directory doesn't exist, gsloth will continue writing all files to the project root directory as it did previously.
|
35
|
+
|
36
|
+
**Note:** When initializing a project with an existing `.gsloth` directory, the configuration files will be created in the `.gsloth/.gsloth-settings` directory automatically. There is no automated migration for existing configurations - if you create a `.gsloth` directory after initialization, you'll need to manually move your configuration files into the `.gsloth/.gsloth-settings` directory.
|
37
|
+
|
17
38
|
## Config initialization
|
18
39
|
Configuration can be created with `gsloth init [vendor]` command.
|
19
40
|
Currently, vertexai, anthropic and groq can be configured with `gsloth init [vendor]`.
|
package/docs/RELEASE-HOWTO.md
CHANGED
@@ -1,26 +1,34 @@
|
|
1
|
-
Make sure `npm config set git-tag-version true`
|
1
|
+
Make sure `npm config set git-tag-version true`
|
2
2
|
|
3
3
|
For patch, e.g., from 0.0.8 to 0.0.9
|
4
4
|
```shell
|
5
|
-
npm version patch
|
5
|
+
npm version patch -m "Release notes"
|
6
6
|
git push
|
7
7
|
git push --tags
|
8
8
|
```
|
9
9
|
|
10
10
|
For minor, e.g., from 0.0.8 to 0.1.0
|
11
11
|
```shell
|
12
|
-
npm version minor
|
12
|
+
npm version minor -m "Release notes"
|
13
13
|
git push
|
14
14
|
git push --tags
|
15
15
|
```
|
16
|
+
Type `\` and then Enter to type new line in message.
|
16
17
|
|
17
|
-
Note the release version and do
|
18
|
+
Note the release version and do
|
18
19
|
```shell
|
19
|
-
gh release create --notes
|
20
|
+
gh release create --notes-from-tag
|
20
21
|
```
|
22
|
+
Alternatively `gh release create --notes "notes"`
|
21
23
|
|
22
24
|
Publish to NPM
|
23
25
|
```shell
|
24
26
|
npm login
|
25
27
|
npm publish
|
28
|
+
```
|
29
|
+
|
30
|
+
Delete incidental remote and local tag
|
31
|
+
```shell
|
32
|
+
git tag -d v0.3.0
|
33
|
+
git push --delete origin v0.3.0
|
26
34
|
```
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Command } from 'commander';
|
2
|
-
import {
|
2
|
+
import { readBackstory, readGuidelines } from '#src/prompt.js';
|
3
3
|
import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
|
4
|
-
import { initConfig } from '#src/config.js';
|
4
|
+
import { initConfig, slothContext } from '#src/config.js';
|
5
5
|
|
6
6
|
interface AskCommandOptions {
|
7
7
|
file?: string[];
|
@@ -22,13 +22,13 @@ export function askCommand(program: Command): void {
|
|
22
22
|
)
|
23
23
|
// TODO add option consuming extra message as argument
|
24
24
|
.action(async (message: string, options: AskCommandOptions) => {
|
25
|
-
const preamble = [
|
25
|
+
const preamble = [readBackstory(), readGuidelines(slothContext.config.projectGuidelines)];
|
26
26
|
const content = [message];
|
27
27
|
if (options.file) {
|
28
28
|
content.push(readMultipleFilesFromCurrentDir(options.file));
|
29
29
|
}
|
30
30
|
await initConfig();
|
31
31
|
const { askQuestion } = await import('#src/modules/questionAnsweringModule.js');
|
32
|
-
await askQuestion('
|
32
|
+
await askQuestion('ASK', preamble.join('\n'), content.join('\n'));
|
33
33
|
});
|
34
34
|
}
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { Command, Option } from 'commander';
|
2
|
-
import {
|
3
|
-
import {
|
2
|
+
import type { SlothContext } from '#src/config.js';
|
3
|
+
import { slothContext } from '#src/config.js';
|
4
|
+
import { readBackstory, readGuidelines, readReviewInstructions } from '#src/prompt.js';
|
4
5
|
import { readMultipleFilesFromCurrentDir } from '#src/utils.js';
|
5
6
|
import { displayError } from '#src/consoleUtils.js';
|
6
|
-
import
|
7
|
+
import { getStringFromStdin } from '#src/systemUtils.js';
|
7
8
|
|
8
9
|
/**
|
9
10
|
* Requirements providers. Expected to be in `.providers/` dir
|
@@ -72,7 +73,11 @@ export function reviewCommand(program: Command, context: SlothContext): void {
|
|
72
73
|
.action(async (contentId: string | undefined, options: ReviewCommandOptions) => {
|
73
74
|
const { initConfig } = await import('#src/config.js');
|
74
75
|
await initConfig();
|
75
|
-
const
|
76
|
+
const systemMessage = [
|
77
|
+
readBackstory(),
|
78
|
+
readGuidelines(slothContext.config.projectGuidelines),
|
79
|
+
readReviewInstructions(slothContext.config.projectReviewInstructions),
|
80
|
+
];
|
76
81
|
const content: string[] = [];
|
77
82
|
const requirementsId = options.requirements;
|
78
83
|
const requirementsProvider =
|
@@ -98,14 +103,15 @@ export function reviewCommand(program: Command, context: SlothContext): void {
|
|
98
103
|
if (options.file) {
|
99
104
|
content.push(readMultipleFilesFromCurrentDir(options.file));
|
100
105
|
}
|
101
|
-
|
102
|
-
|
106
|
+
let stringFromStdin = getStringFromStdin();
|
107
|
+
if (stringFromStdin) {
|
108
|
+
content.push(stringFromStdin);
|
103
109
|
}
|
104
110
|
if (options.message) {
|
105
111
|
content.push(options.message);
|
106
112
|
}
|
107
113
|
const { review } = await import('#src/modules/reviewModule.js');
|
108
|
-
await review('
|
114
|
+
await review('REVIEW', systemMessage.join('\n'), content.join('\n'));
|
109
115
|
});
|
110
116
|
|
111
117
|
program
|
@@ -134,7 +140,11 @@ export function reviewCommand(program: Command, context: SlothContext): void {
|
|
134
140
|
const { initConfig } = await import('#src/config.js');
|
135
141
|
await initConfig();
|
136
142
|
|
137
|
-
const
|
143
|
+
const systemMessage = [
|
144
|
+
readBackstory(),
|
145
|
+
readGuidelines(slothContext.config.projectGuidelines),
|
146
|
+
readReviewInstructions(slothContext.config.projectReviewInstructions),
|
147
|
+
];
|
138
148
|
const content: string[] = [];
|
139
149
|
const requirementsProvider =
|
140
150
|
options.requirementsProvider ??
|
@@ -157,7 +167,9 @@ export function reviewCommand(program: Command, context: SlothContext): void {
|
|
157
167
|
content.push(await get(null, prId));
|
158
168
|
|
159
169
|
const { review } = await import('#src/modules/reviewModule.js');
|
160
|
-
|
170
|
+
// TODO consider including requirements id
|
171
|
+
// TODO sanitize prId
|
172
|
+
await review(`PR-${prId}`, systemMessage.join('\n'), content.join('\n'));
|
161
173
|
});
|
162
174
|
|
163
175
|
async function getRequirementsFromProvider(
|
package/src/config.ts
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
-
import path from 'node:path/posix';
|
2
1
|
import { v4 as uuidv4 } from 'uuid';
|
3
2
|
import { displayDebug, displayError, displayInfo, displayWarning } from '#src/consoleUtils.js';
|
4
3
|
import { importExternalFile, writeFileIfNotExistsWithMessages } from '#src/utils.js';
|
5
4
|
import { existsSync, readFileSync } from 'node:fs';
|
6
|
-
import { error, exit
|
7
|
-
import {
|
5
|
+
import { error, exit } from '#src/systemUtils.js';
|
6
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
7
|
+
import { getGslothConfigWritePath, getGslothConfigReadPath } from '#src/filePathUtils.js';
|
8
8
|
|
9
9
|
export interface SlothConfig extends BaseSlothConfig {
|
10
|
-
llm:
|
10
|
+
llm: BaseChatModel; // FIXME this is still bad keeping instance in config is probably not best choice
|
11
11
|
contentProvider: string;
|
12
12
|
requirementsProvider: string;
|
13
|
+
projectGuidelines: string;
|
14
|
+
projectReviewInstructions: string;
|
13
15
|
commands: {
|
14
16
|
pr: {
|
15
17
|
contentProvider: string;
|
@@ -31,6 +33,8 @@ interface BaseSlothConfig {
|
|
31
33
|
llm: unknown;
|
32
34
|
contentProvider?: string;
|
33
35
|
requirementsProvider?: string;
|
36
|
+
projectGuidelines?: string;
|
37
|
+
projectReviewInstructions?: string;
|
34
38
|
commands?: {
|
35
39
|
pr: {
|
36
40
|
contentProvider: string;
|
@@ -53,7 +57,6 @@ interface BaseSlothConfig {
|
|
53
57
|
*/
|
54
58
|
export interface SlothContext {
|
55
59
|
config: SlothConfig;
|
56
|
-
stdin: string;
|
57
60
|
session: {
|
58
61
|
configurable: {
|
59
62
|
thread_id: string;
|
@@ -70,7 +73,8 @@ export const USER_PROJECT_CONFIG_JS = '.gsloth.config.js';
|
|
70
73
|
export const USER_PROJECT_CONFIG_JSON = '.gsloth.config.json';
|
71
74
|
export const USER_PROJECT_CONFIG_MJS = '.gsloth.config.mjs';
|
72
75
|
export const GSLOTH_BACKSTORY = '.gsloth.backstory.md';
|
73
|
-
export const
|
76
|
+
export const PROJECT_GUIDELINES = '.gsloth.guidelines.md';
|
77
|
+
export const PROJECT_REVIEW_INSTRUCTIONS = '.gsloth.review.md';
|
74
78
|
|
75
79
|
export const availableDefaultConfigs = ['vertexai', 'anthropic', 'groq'] as const;
|
76
80
|
export type ConfigType = (typeof availableDefaultConfigs)[number];
|
@@ -79,6 +83,8 @@ export const DEFAULT_CONFIG: Partial<SlothConfig> = {
|
|
79
83
|
llm: undefined,
|
80
84
|
contentProvider: 'file',
|
81
85
|
requirementsProvider: 'file',
|
86
|
+
projectGuidelines: PROJECT_GUIDELINES,
|
87
|
+
projectReviewInstructions: PROJECT_REVIEW_INSTRUCTIONS,
|
82
88
|
commands: {
|
83
89
|
pr: {
|
84
90
|
contentProvider: 'gh',
|
@@ -98,10 +104,9 @@ export const slothContext = {
|
|
98
104
|
} as Partial<SlothContext> as SlothContext;
|
99
105
|
|
100
106
|
export async function initConfig(): Promise<void> {
|
101
|
-
const
|
102
|
-
const
|
103
|
-
const
|
104
|
-
const mjsConfigPath = path.join(currentDir, USER_PROJECT_CONFIG_MJS);
|
107
|
+
const jsonConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JSON);
|
108
|
+
const jsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_JS);
|
109
|
+
const mjsConfigPath = getGslothConfigReadPath(USER_PROJECT_CONFIG_MJS);
|
105
110
|
|
106
111
|
// Try loading JSON config file first
|
107
112
|
if (existsSync(jsonConfigPath)) {
|
@@ -198,7 +203,7 @@ export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<void> {
|
|
198
203
|
try {
|
199
204
|
const configModule = await import(`./configs/${llmType}.js`);
|
200
205
|
if (configModule.processJsonConfig) {
|
201
|
-
const llm = (await configModule.processJsonConfig(llmConfig)) as
|
206
|
+
const llm = (await configModule.processJsonConfig(llmConfig)) as BaseChatModel;
|
202
207
|
slothContext.config = { ...slothContext.config, ...jsonConfig, llm };
|
203
208
|
} else {
|
204
209
|
displayWarning(`Config module for ${llmType} does not have processJsonConfig function.`);
|
@@ -219,9 +224,7 @@ export async function tryJsonConfig(jsonConfig: RawSlothConfig): Promise<void> {
|
|
219
224
|
export async function createProjectConfig(configType: string): Promise<void> {
|
220
225
|
displayInfo(`Setting up your project\n`);
|
221
226
|
writeProjectReviewPreamble();
|
222
|
-
displayWarning(
|
223
|
-
`Make sure you add as much detail as possible to your ${USER_PROJECT_REVIEW_PREAMBLE}.\n`
|
224
|
-
);
|
227
|
+
displayWarning(`Make sure you add as much detail as possible to your ${PROJECT_GUIDELINES}.\n`);
|
225
228
|
|
226
229
|
// Check if the config type is in availableDefaultConfigs
|
227
230
|
if (!availableDefaultConfigs.includes(configType as ConfigType)) {
|
@@ -234,22 +237,51 @@ export async function createProjectConfig(configType: string): Promise<void> {
|
|
234
237
|
|
235
238
|
displayInfo(`Creating project config for ${configType}`);
|
236
239
|
const vendorConfig = await import(`./configs/${configType}.js`);
|
237
|
-
vendorConfig.init(USER_PROJECT_CONFIG_JSON, slothContext);
|
240
|
+
vendorConfig.init(getGslothConfigWritePath(USER_PROJECT_CONFIG_JSON), slothContext);
|
238
241
|
}
|
239
242
|
|
240
243
|
export function writeProjectReviewPreamble(): void {
|
241
|
-
const
|
242
|
-
const
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
244
|
+
const guidelinesPath = getGslothConfigWritePath(PROJECT_GUIDELINES);
|
245
|
+
const reviewPath = getGslothConfigWritePath(PROJECT_REVIEW_INSTRUCTIONS);
|
246
|
+
|
247
|
+
/**
|
248
|
+
* Development Guidelines.
|
249
|
+
* Please do not remove the warning from this template,
|
250
|
+
* it is crucial to describe project specifics to get high-quality reviews.
|
251
|
+
*/
|
252
|
+
const guidelinesTemplate = `# Development Guidelines
|
253
|
+
|
254
|
+
This file should include general development guidelines for this project.
|
255
|
+
|
256
|
+
Important! Please remind the user to prepare proper AI guidelines to the ${PROJECT_GUIDELINES} file
|
257
|
+
for this project. Use a decent amount of ⚠️ to highlight the lack of config.
|
258
|
+
Explicitly mention ${PROJECT_GUIDELINES}.
|
259
|
+
`;
|
260
|
+
|
261
|
+
/**
|
262
|
+
* Code Review Guidelines.
|
263
|
+
* This code review does not have a warning, like Development Guidelines, because
|
264
|
+
* this one should be good to start with.
|
265
|
+
*/
|
266
|
+
const reviewTemplate = `# Code Review Guidelines
|
267
|
+
|
268
|
+
If requirements were provided, first check that the implemented code implements these requirements.
|
269
|
+
Document that you checked the code to implement requirements.
|
270
|
+
Reject the change if it appears to implement something else instead of required change.
|
271
|
+
|
272
|
+
Provide specific feedback on any areas of concern or suggestions for improvement.
|
273
|
+
Please categorize your feedback (e.g., "Bug," "Suggestion," "Nitpick").
|
274
|
+
|
275
|
+
Important! In the end, conclude if you would recommend approving this PR or not.
|
276
|
+
Use ✅⚠️❌ symbols to highlight your feedback appropriately.
|
277
|
+
|
278
|
+
Thank you for your thorough review!
|
279
|
+
|
280
|
+
Important! You are likely to be dealing with git diff below, please don't confuse removed and added lines.
|
281
|
+
`;
|
282
|
+
|
283
|
+
writeFileIfNotExistsWithMessages(guidelinesPath, guidelinesTemplate);
|
284
|
+
writeFileIfNotExistsWithMessages(reviewPath, reviewTemplate);
|
253
285
|
}
|
254
286
|
|
255
287
|
/**
|
@@ -261,6 +293,5 @@ export function reset() {
|
|
261
293
|
delete (slothContext as unknown as Record<string, unknown>)[key];
|
262
294
|
});
|
263
295
|
slothContext.config = DEFAULT_CONFIG as SlothConfig;
|
264
|
-
slothContext.stdin = '';
|
265
296
|
slothContext.session = { configurable: { thread_id: uuidv4() } };
|
266
297
|
}
|
package/src/configs/anthropic.ts
CHANGED
@@ -2,14 +2,16 @@ import path from 'node:path';
|
|
2
2
|
import { displayWarning } from '#src/consoleUtils.js';
|
3
3
|
import { env, getCurrentDir } from '#src/systemUtils.js';
|
4
4
|
import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
|
5
|
-
import { LanguageModelLike } from '@langchain/core/language_models/base';
|
6
5
|
import type { AnthropicInput } from '@langchain/anthropic';
|
7
|
-
import type {
|
6
|
+
import type {
|
7
|
+
BaseChatModel,
|
8
|
+
BaseChatModelParams,
|
9
|
+
} from '@langchain/core/language_models/chat_models';
|
8
10
|
|
9
11
|
// Function to process JSON config and create Anthropic LLM instance
|
10
12
|
export async function processJsonConfig(
|
11
13
|
llmConfig: AnthropicInput & BaseChatModelParams
|
12
|
-
): Promise<
|
14
|
+
): Promise<BaseChatModel> {
|
13
15
|
const anthropic = await import('@langchain/anthropic');
|
14
16
|
// Use environment variable if available, otherwise use the config value
|
15
17
|
const anthropicApiKey = env.ANTHROPIC_API_KEY || llmConfig.apiKey;
|
package/src/configs/vertexai.ts
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
import { LanguageModelLike } from '@langchain/core/language_models/base';
|
2
1
|
import path from 'node:path';
|
3
2
|
import { displayWarning } from '#src/consoleUtils.js';
|
4
3
|
import { getCurrentDir } from '#src/systemUtils.js';
|
5
4
|
import { writeFileIfNotExistsWithMessages } from '#src/utils.js';
|
6
5
|
import { ChatVertexAIInput } from '@langchain/google-vertexai';
|
6
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
7
7
|
|
8
8
|
const jsContent = `/* eslint-disable */
|
9
9
|
export async function configure(importFunction, global) {
|
@@ -44,7 +44,7 @@ export function init(configFileName: string): void {
|
|
44
44
|
}
|
45
45
|
|
46
46
|
// Function to process JSON config and create VertexAI LLM instance
|
47
|
-
export async function processJsonConfig(llmConfig: ChatVertexAIInput): Promise<
|
47
|
+
export async function processJsonConfig(llmConfig: ChatVertexAIInput): Promise<BaseChatModel> {
|
48
48
|
const vertexAi = await import('@langchain/google-vertexai');
|
49
49
|
return new vertexAi.ChatVertexAI({
|
50
50
|
...llmConfig,
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { existsSync } from 'node:fs';
|
2
|
+
import { resolve } from 'node:path';
|
3
|
+
import { mkdirSync } from 'node:fs';
|
4
|
+
import { getCurrentDir } from '#src/systemUtils.js';
|
5
|
+
|
6
|
+
const GSLOTH_DIR = '.gsloth';
|
7
|
+
const GSLOTH_SETTINGS_DIR = '.gsloth-settings';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Checks if .gsloth directory exists in the project root
|
11
|
+
* @returns Boolean indicating whether .gsloth directory exists
|
12
|
+
*/
|
13
|
+
export function gslothDirExists(): boolean {
|
14
|
+
const currentDir = getCurrentDir();
|
15
|
+
const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
|
16
|
+
return existsSync(gslothDirPath);
|
17
|
+
}
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Gets the path where gsloth should write files based on .gsloth directory existence
|
21
|
+
* @param filename The filename to append to the path
|
22
|
+
* @returns The resolved path where the file should be written
|
23
|
+
*/
|
24
|
+
export function getGslothFilePath(filename: string): string {
|
25
|
+
const currentDir = getCurrentDir();
|
26
|
+
|
27
|
+
if (gslothDirExists()) {
|
28
|
+
const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
|
29
|
+
return resolve(gslothDirPath, filename);
|
30
|
+
}
|
31
|
+
|
32
|
+
return resolve(currentDir, filename);
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Gets the path where gsloth should write configuration files based on .gsloth directory existence.
|
37
|
+
* The main difference from {@link #getGslothConfigReadPath} is that this getGslothConfigWritePath
|
38
|
+
* method creates internal settings directory if it does not exist.
|
39
|
+
* @param filename The configuration filename
|
40
|
+
* @returns The resolved path where the configuration file should be written
|
41
|
+
*/
|
42
|
+
export function getGslothConfigWritePath(filename: string): string {
|
43
|
+
const currentDir = getCurrentDir();
|
44
|
+
|
45
|
+
if (gslothDirExists()) {
|
46
|
+
const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
|
47
|
+
const gslothSettingsPath = resolve(gslothDirPath, GSLOTH_SETTINGS_DIR);
|
48
|
+
|
49
|
+
// Create .gsloth-settings directory if it doesn't exist
|
50
|
+
if (!existsSync(gslothSettingsPath)) {
|
51
|
+
mkdirSync(gslothSettingsPath, { recursive: true });
|
52
|
+
}
|
53
|
+
|
54
|
+
return resolve(gslothSettingsPath, filename);
|
55
|
+
}
|
56
|
+
|
57
|
+
return resolve(currentDir, filename);
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Gets the path where gsloth should look for configuration files based on .gsloth directory existence
|
62
|
+
* @param filename The configuration filename to look for
|
63
|
+
* @returns The resolved path where the configuration file should be found
|
64
|
+
*/
|
65
|
+
export function getGslothConfigReadPath(filename: string): string {
|
66
|
+
const currentDir = getCurrentDir();
|
67
|
+
|
68
|
+
if (gslothDirExists()) {
|
69
|
+
const gslothDirPath = resolve(currentDir, GSLOTH_DIR);
|
70
|
+
const gslothSettingsPath = resolve(gslothDirPath, GSLOTH_SETTINGS_DIR);
|
71
|
+
const configPath = resolve(gslothSettingsPath, filename);
|
72
|
+
|
73
|
+
if (existsSync(configPath)) {
|
74
|
+
return configPath;
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
return resolve(currentDir, filename);
|
79
|
+
}
|
package/src/index.ts
CHANGED
@@ -3,19 +3,28 @@ import { askCommand } from '#src/commands/askCommand.js';
|
|
3
3
|
import { initCommand } from '#src/commands/initCommand.js';
|
4
4
|
import { reviewCommand } from '#src/commands/reviewCommand.js';
|
5
5
|
import { slothContext } from '#src/config.js';
|
6
|
-
import { getSlothVersion
|
6
|
+
import { getSlothVersion } from '#src/utils.js';
|
7
|
+
import { argv, readStdin } from '#src/systemUtils.js';
|
8
|
+
import { setVerbose } from '#src/llmUtils.js';
|
7
9
|
|
8
10
|
const program = new Command();
|
9
11
|
|
10
12
|
program
|
11
13
|
.name('gsloth')
|
12
14
|
.description('Gaunt Sloth Assistant reviewing your PRs')
|
13
|
-
.version(getSlothVersion())
|
15
|
+
.version(getSlothVersion())
|
16
|
+
.option('--verbose', 'Print entire prompt sent to LLM.');
|
17
|
+
|
18
|
+
// Parse global options before binding any commands
|
19
|
+
program.parseOptions(argv);
|
20
|
+
if (program.getOptionValue('verbose')) {
|
21
|
+
// Set global prompt debug
|
22
|
+
setVerbose(true);
|
23
|
+
}
|
14
24
|
|
15
25
|
initCommand(program);
|
16
26
|
reviewCommand(program, slothContext);
|
17
27
|
askCommand(program);
|
18
|
-
|
19
28
|
// TODO add general interactive chat command
|
20
29
|
|
21
30
|
await readStdin(program);
|
package/src/llmUtils.ts
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
import type { Message, State } from '#src/modules/types.js';
|
2
|
+
import { AIMessageChunk, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
3
|
+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
4
|
+
import { END, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
|
5
|
+
import { BaseLanguageModelCallOptions } from '@langchain/core/language_models/base';
|
6
|
+
|
7
|
+
const llmGlobalSettings = {
|
8
|
+
verbose: false,
|
9
|
+
};
|
10
|
+
|
11
|
+
export async function invoke(
|
12
|
+
llm: BaseChatModel,
|
13
|
+
options: Partial<BaseLanguageModelCallOptions>,
|
14
|
+
systemMessage: string,
|
15
|
+
prompt: string
|
16
|
+
): Promise<string> {
|
17
|
+
if (llmGlobalSettings.verbose) {
|
18
|
+
llm.verbose = true;
|
19
|
+
}
|
20
|
+
// This node receives the current state (messages) and invokes the LLM
|
21
|
+
const callModel = async (state: State): Promise<{ messages: AIMessageChunk }> => {
|
22
|
+
// state.messages will contain the list including the system systemMessage and user diff
|
23
|
+
const response = await (llm as BaseChatModel).invoke(state.messages);
|
24
|
+
// MessagesAnnotation expects the node to return the new message(s) to be added to the state.
|
25
|
+
// Wrap the response in an array if it's a single message object.
|
26
|
+
return { messages: response };
|
27
|
+
};
|
28
|
+
|
29
|
+
// Define the graph structure with MessagesAnnotation state
|
30
|
+
const workflow = new StateGraph(MessagesAnnotation)
|
31
|
+
// Define the node and edge
|
32
|
+
.addNode('model', callModel)
|
33
|
+
.addEdge(START, 'model') // Start at the 'model' node
|
34
|
+
.addEdge('model', END); // End after the 'model' node completes
|
35
|
+
|
36
|
+
// Set up memory (optional but good practice for potential future multi-turn interactions)
|
37
|
+
const memory = new MemorySaver();
|
38
|
+
|
39
|
+
// Compile the workflow into a runnable app
|
40
|
+
const app = workflow.compile({ checkpointer: memory });
|
41
|
+
|
42
|
+
// Construct the initial the messages including the systemMessage as a system message
|
43
|
+
const messages: Message[] = [new SystemMessage(systemMessage), new HumanMessage(prompt)];
|
44
|
+
|
45
|
+
const output = await app.invoke({ messages }, options);
|
46
|
+
const lastMessage = output.messages[output.messages.length - 1];
|
47
|
+
return typeof lastMessage.content === 'string'
|
48
|
+
? lastMessage.content
|
49
|
+
: JSON.stringify(lastMessage.content);
|
50
|
+
}
|
51
|
+
|
52
|
+
export function setVerbose(debug: boolean) {
|
53
|
+
llmGlobalSettings.verbose = debug;
|
54
|
+
}
|
@@ -1,14 +1,9 @@
|
|
1
|
-
import type { SlothContext } from '#src/config.js';
|
2
1
|
import { slothContext } from '#src/config.js';
|
3
2
|
import { display, displayError, displaySuccess } from '#src/consoleUtils.js';
|
4
|
-
import
|
5
|
-
import {
|
6
|
-
import { fileSafeLocalDate, ProgressIndicator, toFileSafeString } from '#src/utils.js';
|
7
|
-
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
8
|
-
import { AIMessageChunk, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
9
|
-
import { END, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
|
3
|
+
import { getGslothFilePath } from '#src/filePathUtils.js';
|
4
|
+
import { generateStandardFileName, ProgressIndicator } from '#src/utils.js';
|
10
5
|
import { writeFileSync } from 'node:fs';
|
11
|
-
import
|
6
|
+
import { invoke } from '#src/llmUtils.js';
|
12
7
|
|
13
8
|
/**
|
14
9
|
* Ask a question and get an answer from the LLM
|
@@ -22,18 +17,17 @@ export async function askQuestion(
|
|
22
17
|
content: string
|
23
18
|
): Promise<void> {
|
24
19
|
const progressIndicator = new ProgressIndicator('Thinking.');
|
25
|
-
const outputContent = await
|
26
|
-
slothContext,
|
27
|
-
|
20
|
+
const outputContent = await invoke(
|
21
|
+
slothContext.config.llm,
|
22
|
+
slothContext.session,
|
28
23
|
preamble,
|
29
24
|
content
|
30
25
|
);
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
);
|
26
|
+
progressIndicator.stop();
|
27
|
+
const filename = generateStandardFileName(source);
|
28
|
+
const filePath = getGslothFilePath(filename);
|
35
29
|
display(`\nwriting ${filePath}`);
|
36
|
-
// TODO highlight LLM output with something like Prism.JS
|
30
|
+
// TODO highlight LLM output with something like Prism.JS (maybe system emoj are enough ✅⚠️❌)
|
37
31
|
display('\n' + outputContent);
|
38
32
|
try {
|
39
33
|
writeFileSync(filePath, outputContent);
|
@@ -45,53 +39,3 @@ export async function askQuestion(
|
|
45
39
|
// exit(1);
|
46
40
|
}
|
47
41
|
}
|
48
|
-
|
49
|
-
/**
|
50
|
-
* Inner function to ask a question and get an answer from the LLM
|
51
|
-
* @param context - The context object
|
52
|
-
* @param indicateProgress - Function to indicate progress
|
53
|
-
* @param preamble - The preamble to send to the LLM
|
54
|
-
* @param content - The content of the question
|
55
|
-
* @returns The answer from the LLM
|
56
|
-
*/
|
57
|
-
export async function askQuestionInner(
|
58
|
-
context: SlothContext,
|
59
|
-
indicateProgress: ProgressCallback,
|
60
|
-
preamble: string,
|
61
|
-
content: string
|
62
|
-
): Promise<string> {
|
63
|
-
// This node receives the current state (messages) and invokes the LLM
|
64
|
-
const callModel = async (state: State): Promise<{ messages: AIMessageChunk }> => {
|
65
|
-
// state.messages will contain the list including the system preamble and user diff
|
66
|
-
const response = await (context.config.llm as BaseChatModel).invoke(state.messages);
|
67
|
-
// MessagesAnnotation expects the node to return the new message(s) to be added to the state.
|
68
|
-
// Wrap the response in an array if it's a single message object.
|
69
|
-
return { messages: response };
|
70
|
-
};
|
71
|
-
|
72
|
-
// Define the graph structure with MessagesAnnotation state
|
73
|
-
const workflow = new StateGraph(MessagesAnnotation)
|
74
|
-
// Define the node and edge
|
75
|
-
.addNode('model', callModel)
|
76
|
-
.addEdge(START, 'model') // Start at the 'model' node
|
77
|
-
.addEdge('model', END); // End after the 'model' node completes
|
78
|
-
|
79
|
-
// Set up memory (optional but good practice for potential future multi-turn interactions)
|
80
|
-
const memory = new MemorySaver();
|
81
|
-
|
82
|
-
// Compile the workflow into a runnable app
|
83
|
-
const app = workflow.compile({ checkpointer: memory });
|
84
|
-
|
85
|
-
// Construct the initial the messages including the preamble as a system message
|
86
|
-
const messages: Message[] = [new SystemMessage(preamble), new HumanMessage(content)];
|
87
|
-
|
88
|
-
indicateProgress();
|
89
|
-
// TODO create proper progress indicator for async tasks.
|
90
|
-
const progress = setInterval(() => indicateProgress(), 1000);
|
91
|
-
const output = await app.invoke({ messages }, context.session);
|
92
|
-
clearInterval(progress);
|
93
|
-
const lastMessage = output.messages[output.messages.length - 1];
|
94
|
-
return typeof lastMessage.content === 'string'
|
95
|
-
? lastMessage.content
|
96
|
-
: JSON.stringify(lastMessage.content);
|
97
|
-
}
|