gaunt-sloth-assistant 0.0.8 → 0.1.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/.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/DEVELOPMENT.md +9 -0
- package/LICENSE +0 -0
- package/README.md +57 -14
- package/RELEASE-HOWTO.md +8 -0
- package/ROADMAP.md +2 -2
- package/UX-RESEARCH.md +78 -0
- package/index.js +12 -74
- package/package.json +9 -8
- package/spec/.gsloth.config.js +22 -0
- package/spec/askCommand.spec.js +58 -0
- package/spec/initCommand.spec.js +54 -0
- package/spec/questionAnsweringModule.spec.js +137 -0
- package/spec/reviewCommand.spec.js +144 -0
- package/spec/{codeReview.spec.js → reviewModule.spec.js} +2 -2
- package/spec/support/jasmine.mjs +0 -0
- package/src/commands/askCommand.js +26 -0
- package/src/commands/initCommand.js +16 -0
- package/src/commands/reviewCommand.js +147 -0
- package/src/config.js +1 -1
- package/src/configs/anthropic.js +0 -0
- package/src/configs/groq.js +0 -0
- package/src/configs/vertexai.js +0 -0
- package/src/consoleUtils.js +2 -0
- package/src/{questionAnswering.js → modules/questionAnsweringModule.js} +87 -68
- package/src/{codeReview.js → modules/reviewModule.js} +4 -6
- package/src/prompt.js +0 -0
- package/src/providers/ghPrDiffProvider.js +11 -0
- package/src/providers/jiraIssueLegacyAccessTokenProvider.js +81 -0
- package/src/providers/text.js +6 -0
- package/src/utils.js +35 -20
- package/testMessage.txt +0 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
import {Command} from 'commander';
|
2
|
+
import * as td from 'testdouble';
|
3
|
+
|
4
|
+
describe('reviewCommand', function (){
|
5
|
+
|
6
|
+
beforeEach(async function() {
|
7
|
+
this.review = td.function();
|
8
|
+
this.prompt = await td.replaceEsm("../src/prompt.js");
|
9
|
+
td.when(this.prompt.readInternalPreamble()).thenReturn("INTERNAL PREAMBLE");
|
10
|
+
td.when(this.prompt.readPreamble(".gsloth.preamble.review.md")).thenReturn("PROJECT PREAMBLE");
|
11
|
+
this.codeReviewMock = await td.replaceEsm("../src/modules/reviewModule.js");
|
12
|
+
await td.replaceEsm("../src/config.js");
|
13
|
+
this.utils = await td.replaceEsm("../src/utils.js");
|
14
|
+
td.when(this.utils.readFileFromCurrentDir("test.file")).thenReturn("FILE TO REVIEW");
|
15
|
+
td.when(this.codeReviewMock.review(
|
16
|
+
'sloth-DIFF-review',
|
17
|
+
td.matchers.anything(),
|
18
|
+
td.matchers.anything())
|
19
|
+
).thenDo(this.review);
|
20
|
+
});
|
21
|
+
|
22
|
+
it('Should call review with file contents', async function() {
|
23
|
+
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
24
|
+
const program = new Command()
|
25
|
+
await reviewCommand(program, {});
|
26
|
+
await program.parseAsync(['na', 'na', 'review', '-f', 'test.file']);
|
27
|
+
td.verify(this.review(
|
28
|
+
'sloth-DIFF-review',
|
29
|
+
"INTERNAL PREAMBLE\nPROJECT PREAMBLE",
|
30
|
+
"test.file:\n```\nFILE TO REVIEW\n```")
|
31
|
+
);
|
32
|
+
});
|
33
|
+
|
34
|
+
it('Should display predefined providers in help', async function() {
|
35
|
+
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
36
|
+
const program = new Command();
|
37
|
+
const testOutput = { text: '' };
|
38
|
+
|
39
|
+
program.configureOutput({
|
40
|
+
writeOut: (str) => testOutput.text += str,
|
41
|
+
writeErr: (str) => testOutput.text += str
|
42
|
+
});
|
43
|
+
|
44
|
+
await reviewCommand(program, {});
|
45
|
+
|
46
|
+
const commandUnderTest = program.commands.find(c => c.name() == 'review');
|
47
|
+
expect(commandUnderTest).toBeDefined();
|
48
|
+
commandUnderTest.outputHelp();
|
49
|
+
|
50
|
+
// Verify content providers are displayed
|
51
|
+
expect(testOutput.text).toContain('--content-provider <contentProvider>');
|
52
|
+
expect(testOutput.text).toContain('(choices: "gh", "text")');
|
53
|
+
|
54
|
+
// Verify requirements providers are displayed
|
55
|
+
expect(testOutput.text).toContain('--requirements-provider <requirementsProvider>');
|
56
|
+
expect(testOutput.text).toContain('(choices: "jira-legacy", "text")');
|
57
|
+
});
|
58
|
+
|
59
|
+
it('Should call review with predefined requirements provider', async function() {
|
60
|
+
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
61
|
+
const program = new Command();
|
62
|
+
const context = {
|
63
|
+
config: {
|
64
|
+
requirementsProvider: 'jira-legacy',
|
65
|
+
requirementsProviderConfig: {
|
66
|
+
'jira-legacy': {
|
67
|
+
username: 'test-user',
|
68
|
+
token: 'test-token',
|
69
|
+
baseUrl: 'https://test-jira.atlassian.net/rest/api/2/issue/'
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
};
|
74
|
+
|
75
|
+
// Mock the jira provider
|
76
|
+
const jiraProvider = td.func();
|
77
|
+
td.when(jiraProvider(td.matchers.anything(), 'JIRA-123')).thenResolve('JIRA Requirements');
|
78
|
+
|
79
|
+
// Replace the dynamic import with our mock
|
80
|
+
await td.replaceEsm('../src/providers/jiraIssueLegacyAccessTokenProvider.js', {
|
81
|
+
get: jiraProvider
|
82
|
+
});
|
83
|
+
|
84
|
+
await reviewCommand(program, context);
|
85
|
+
await program.parseAsync(['na', 'na', 'review', 'content-id', '-r', 'JIRA-123']);
|
86
|
+
|
87
|
+
td.verify(this.review('sloth-DIFF-review', "INTERNAL PREAMBLE\nPROJECT PREAMBLE", "JIRA Requirements"));
|
88
|
+
});
|
89
|
+
|
90
|
+
it('Should call review with predefined content provider', async function() {
|
91
|
+
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
92
|
+
const program = new Command();
|
93
|
+
const context = {
|
94
|
+
config: {
|
95
|
+
contentProvider: 'gh'
|
96
|
+
}
|
97
|
+
};
|
98
|
+
|
99
|
+
// Mock the gh provider
|
100
|
+
const ghProvider = td.func();
|
101
|
+
td.when(ghProvider(td.matchers.anything(), '123')).thenResolve('PR Diff Content');
|
102
|
+
|
103
|
+
// Replace the dynamic import with our mock
|
104
|
+
await td.replaceEsm('../src/providers/ghPrDiffProvider.js', {
|
105
|
+
get: ghProvider
|
106
|
+
});
|
107
|
+
|
108
|
+
await reviewCommand(program, context);
|
109
|
+
await program.parseAsync(['na', 'na', 'review', '123']);
|
110
|
+
|
111
|
+
td.verify(this.review('sloth-DIFF-review', "INTERNAL PREAMBLE\nPROJECT PREAMBLE", "PR Diff Content"));
|
112
|
+
});
|
113
|
+
|
114
|
+
it('Should call pr command', async function() {
|
115
|
+
// Create a spy for the review function
|
116
|
+
const reviewSpy = td.func();
|
117
|
+
|
118
|
+
// Replace the review function in the codeReviewMock
|
119
|
+
this.codeReviewMock.review = reviewSpy;
|
120
|
+
|
121
|
+
// Mock the modules/reviewModule.js import in the reviewCommand.js file
|
122
|
+
await td.replaceEsm('../src/modules/reviewModule.js', {
|
123
|
+
review: reviewSpy
|
124
|
+
});
|
125
|
+
|
126
|
+
const { reviewCommand } = await import("../src/commands/reviewCommand.js");
|
127
|
+
const program = new Command();
|
128
|
+
const context = {};
|
129
|
+
|
130
|
+
// Mock the gh provider
|
131
|
+
const ghProvider = td.func();
|
132
|
+
td.when(ghProvider('123')).thenResolve('PR Diff Content');
|
133
|
+
|
134
|
+
// Replace the dynamic import with our mock
|
135
|
+
await td.replaceEsm('../src/providers/ghPrDiffProvider.js', {
|
136
|
+
get: ghProvider
|
137
|
+
});
|
138
|
+
|
139
|
+
await reviewCommand(program, context);
|
140
|
+
await program.parseAsync(['na', 'na', 'pr', '123']);
|
141
|
+
|
142
|
+
td.verify(reviewSpy('sloth-PR-123-review', "INTERNAL PREAMBLE\nPROJECT PREAMBLE", "PR Diff Content"));
|
143
|
+
});
|
144
|
+
});
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import { reviewInner } from '../src/
|
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
4
|
|
5
|
-
describe('
|
5
|
+
describe('reviewModule', () => {
|
6
6
|
|
7
7
|
it('should invoke LLM', async () => {
|
8
8
|
// Setup mock for slothContext
|
package/spec/support/jasmine.mjs
CHANGED
File without changes
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { readInternalPreamble } from "../prompt.js";
|
2
|
+
import { readFileFromCurrentDir } from "../utils.js";
|
3
|
+
import { initConfig } from "../config.js";
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Adds the ask command to the program
|
7
|
+
* @param {Object} program - The commander program
|
8
|
+
* @param {Object} context - The context object
|
9
|
+
*/
|
10
|
+
export function askCommand(program, context) {
|
11
|
+
program.command('ask')
|
12
|
+
.description('Ask a question')
|
13
|
+
.argument('<message>', 'A message')
|
14
|
+
.option('-f, --file <file>', 'Input file. Content of this file will be added BEFORE the diff')
|
15
|
+
// TODO add option consuming extra message as argument
|
16
|
+
.action(async (message, options) => {
|
17
|
+
const preamble = [readInternalPreamble()];
|
18
|
+
const content = [message];
|
19
|
+
if (options.file) {
|
20
|
+
content.push(readFileFromCurrentDir(options.file));
|
21
|
+
}
|
22
|
+
await initConfig();
|
23
|
+
const { askQuestion } = await import('../modules/questionAnsweringModule.js');
|
24
|
+
await askQuestion('sloth-ASK', preamble.join("\n"), content.join("\n"));
|
25
|
+
});
|
26
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { Argument } from 'commander';
|
2
|
+
import { availableDefaultConfigs, createProjectConfig } from "../config.js";
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Adds the init command to the program
|
6
|
+
* @param {Object} program - The commander program
|
7
|
+
* @param {Object} context - The context object
|
8
|
+
*/
|
9
|
+
export function initCommand(program, context) {
|
10
|
+
program.command('init')
|
11
|
+
.description('Initialize the Gaunt Sloth Assistant in your project. This will write necessary config files.')
|
12
|
+
.addArgument(new Argument('<type>', 'Config type').choices(availableDefaultConfigs))
|
13
|
+
.action(async (config) => {
|
14
|
+
await createProjectConfig(config);
|
15
|
+
});
|
16
|
+
}
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import {Option} from 'commander';
|
2
|
+
import {USER_PROJECT_REVIEW_PREAMBLE} from "../config.js";
|
3
|
+
import {readInternalPreamble, readPreamble} from "../prompt.js";
|
4
|
+
import {readFileFromCurrentDir} from "../utils.js";
|
5
|
+
import {displayError} from "../consoleUtils.js";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Requirements providers. Expected to be in `.providers/` dir
|
9
|
+
*/
|
10
|
+
const REQUIREMENTS_PROVIDERS = {
|
11
|
+
'jira-legacy': 'jiraIssueLegacyAccessTokenProvider.js',
|
12
|
+
'text': 'text.js'
|
13
|
+
};
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Content providers. Expected to be in `.providers/` dir
|
17
|
+
*/
|
18
|
+
const CONTENT_PROVIDERS = {
|
19
|
+
'gh': 'ghPrDiffProvider.js',
|
20
|
+
'text': 'text.js'
|
21
|
+
};
|
22
|
+
|
23
|
+
export function reviewCommand(program, context) {
|
24
|
+
|
25
|
+
program.command('review')
|
26
|
+
.description('Review provided diff or other content')
|
27
|
+
.argument('[contentId]', 'Optional content ID argument to retrieve content with content provider')
|
28
|
+
.alias('r')
|
29
|
+
// TODO add provider to get results of git --no-pager diff
|
30
|
+
// TODO add support to include multiple files
|
31
|
+
.option('-f, --file <file>', 'Input file. Content of this file will be added BEFORE the diff, but after requirements')
|
32
|
+
// TODO figure out what to do with this (we probably want to merge it with requirementsId)?
|
33
|
+
.option('-r, --requirements <requirements>', 'Requirements for this review.')
|
34
|
+
.addOption(
|
35
|
+
new Option('-p, --requirements-provider <requirementsProvider>', 'Requirements provider for this review.')
|
36
|
+
.choices(Object.keys(REQUIREMENTS_PROVIDERS))
|
37
|
+
)
|
38
|
+
.addOption(
|
39
|
+
new Option('--content-provider <contentProvider>', 'Content provider')
|
40
|
+
.choices(Object.keys(CONTENT_PROVIDERS))
|
41
|
+
)
|
42
|
+
.option('-m, --message <message>', 'Extra message to provide just before the content')
|
43
|
+
.action(async (contentId, options) => {
|
44
|
+
const {initConfig} = await import("../config.js");
|
45
|
+
await initConfig();
|
46
|
+
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
47
|
+
const content = [];
|
48
|
+
const requirementsId = options.requirements;
|
49
|
+
const requirementsProvider = options.requirementsProvider ?? context.config?.requirementsProvider;
|
50
|
+
const contentProvider = options.contentProvider ?? context.config?.contentProvider;
|
51
|
+
|
52
|
+
// TODO consider calling these in parallel
|
53
|
+
const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
|
54
|
+
if (requirements) {
|
55
|
+
content.push(requirements);
|
56
|
+
}
|
57
|
+
|
58
|
+
const providedContent = await getContentFromProvider(contentProvider, contentId);
|
59
|
+
if (providedContent) {
|
60
|
+
content.push(providedContent);
|
61
|
+
}
|
62
|
+
|
63
|
+
if (options.file) {
|
64
|
+
content.push(`${options.file}:\n\`\`\`\n${readFileFromCurrentDir(options.file)}\n\`\`\``);
|
65
|
+
}
|
66
|
+
if (context.stdin) {
|
67
|
+
content.push(context.stdin);
|
68
|
+
}
|
69
|
+
if (options.message) {
|
70
|
+
content.push(options.message);
|
71
|
+
}
|
72
|
+
const {review} = await import('../modules/reviewModule.js');
|
73
|
+
await review('sloth-DIFF-review', preamble.join("\n"), content.join("\n"));
|
74
|
+
});
|
75
|
+
|
76
|
+
program.command('pr')
|
77
|
+
.description('Review provided Pull Request in current directory. ' +
|
78
|
+
'This command is similar to `review`, but default content provider is `gh`. ' +
|
79
|
+
'(assuming that GH cli is installed and authenticated for current project')
|
80
|
+
.argument('<prId>', 'Pull request ID to review.')
|
81
|
+
.argument('[requirementsId]', 'Optional requirements ID argument to retrieve requirements with requirements provider')
|
82
|
+
.addOption(
|
83
|
+
new Option('-p, --requirements-provider <requirementsProvider>', 'Requirements provider for this review.')
|
84
|
+
.choices(Object.keys(REQUIREMENTS_PROVIDERS))
|
85
|
+
)
|
86
|
+
.option('-f, --file <file>', 'Input file. Content of this file will be added BEFORE the diff, but after requirements')
|
87
|
+
.action(async (prId, requirementsId, options) => {
|
88
|
+
const {initConfig} = await import("../config.js");
|
89
|
+
await initConfig();
|
90
|
+
|
91
|
+
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
92
|
+
const content = [];
|
93
|
+
const requirementsProvider = options.requirementsProvider ?? context.config?.requirementsProvider;
|
94
|
+
|
95
|
+
// Handle requirements
|
96
|
+
const requirements = await getRequirementsFromProvider(requirementsProvider, requirementsId);
|
97
|
+
if (requirements) {
|
98
|
+
content.push(requirements);
|
99
|
+
}
|
100
|
+
|
101
|
+
if (options.file) {
|
102
|
+
content.push(`${options.file}:\n\`\`\`\n${readFileFromCurrentDir(options.file)}\n\`\`\``);
|
103
|
+
}
|
104
|
+
|
105
|
+
// Get PR diff using the 'gh' provider
|
106
|
+
const providerPath = `../providers/${CONTENT_PROVIDERS['gh']}`;
|
107
|
+
const {get} = await import(providerPath);
|
108
|
+
content.push(await get(null, prId));
|
109
|
+
|
110
|
+
const {review} = await import('../modules/reviewModule.js');
|
111
|
+
await review(`sloth-PR-${prId}-review`, preamble.join("\n"), content.join("\n"));
|
112
|
+
});
|
113
|
+
|
114
|
+
async function getRequirementsFromProvider(requirementsProvider, requirementsId) {
|
115
|
+
return getFromProvider(
|
116
|
+
requirementsProvider,
|
117
|
+
requirementsId,
|
118
|
+
(context.config?.requirementsProviderConfig ?? {})[requirementsProvider],
|
119
|
+
REQUIREMENTS_PROVIDERS
|
120
|
+
);
|
121
|
+
}
|
122
|
+
|
123
|
+
async function getContentFromProvider(contentProvider, contentId) {
|
124
|
+
return getFromProvider(
|
125
|
+
contentProvider,
|
126
|
+
contentId,
|
127
|
+
(context.config?.contentProviderConfig ?? {})[contentProvider],
|
128
|
+
CONTENT_PROVIDERS
|
129
|
+
)
|
130
|
+
}
|
131
|
+
|
132
|
+
async function getFromProvider(provider, id, config, legitPredefinedProviders) {
|
133
|
+
if (typeof provider === 'string') {
|
134
|
+
// Use one of the predefined providers
|
135
|
+
if (legitPredefinedProviders[provider]) {
|
136
|
+
const providerPath = `../providers/${legitPredefinedProviders[provider]}`;
|
137
|
+
const {get} = await import(providerPath);
|
138
|
+
return await get(config, id);
|
139
|
+
} else {
|
140
|
+
displayError(`Unknown provider: ${provider}. Continuing without it.`);
|
141
|
+
}
|
142
|
+
} else if (typeof provider === 'function') {
|
143
|
+
return await provider(id);
|
144
|
+
}
|
145
|
+
return '';
|
146
|
+
}
|
147
|
+
}
|
package/src/config.js
CHANGED
@@ -27,7 +27,7 @@ export const slothContext = {
|
|
27
27
|
};
|
28
28
|
|
29
29
|
export async function initConfig() {
|
30
|
-
const configFileUrl = url.pathToFileURL(path.join(process.cwd(), USER_PROJECT_CONFIG_FILE));
|
30
|
+
const configFileUrl = url.pathToFileURL(path.join(process.cwd(), USER_PROJECT_CONFIG_FILE));
|
31
31
|
return import(configFileUrl)
|
32
32
|
.then((i) => i.configure((module) => import(module)))
|
33
33
|
.then((config) => {
|
package/src/configs/anthropic.js
CHANGED
File without changes
|
package/src/configs/groq.js
CHANGED
File without changes
|
package/src/configs/vertexai.js
CHANGED
File without changes
|
package/src/consoleUtils.js
CHANGED
@@ -1,68 +1,87 @@
|
|
1
|
-
import {
|
2
|
-
END,
|
3
|
-
MemorySaver,
|
4
|
-
MessagesAnnotation,
|
5
|
-
START,
|
6
|
-
StateGraph,
|
7
|
-
} from "@langchain/langgraph";
|
8
|
-
import { writeFileSync } from "node:fs";
|
9
|
-
import path from "node:path";
|
10
|
-
import {
|
11
|
-
import { display, displayError, displaySuccess } from "
|
12
|
-
import { fileSafeLocalDate, toFileSafeString } from "
|
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
|
-
|
1
|
+
import {
|
2
|
+
END,
|
3
|
+
MemorySaver,
|
4
|
+
MessagesAnnotation,
|
5
|
+
START,
|
6
|
+
StateGraph,
|
7
|
+
} from "@langchain/langgraph";
|
8
|
+
import { writeFileSync } from "node:fs";
|
9
|
+
import * as path from "node:path";
|
10
|
+
import { slothContext } from "../config.js";
|
11
|
+
import { display, displayError, displaySuccess } from "../consoleUtils.js";
|
12
|
+
import { fileSafeLocalDate, toFileSafeString, ProgressIndicator, extractLastMessageContent } from "../utils.js";
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Ask a question and get an answer from the LLM
|
16
|
+
* @param {string} source - The source of the question (used for file naming)
|
17
|
+
* @param {string} preamble - The preamble to send to the LLM
|
18
|
+
* @param {string} content - The content of the question
|
19
|
+
*/
|
20
|
+
export async function askQuestion(source, preamble, content) {
|
21
|
+
const progressIndicator = new ProgressIndicator("Thinking.");
|
22
|
+
const outputContent = await askQuestionInner(slothContext, () => progressIndicator.indicate(), preamble, content);
|
23
|
+
const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
|
24
|
+
display(`\nwriting ${filePath}`);
|
25
|
+
// TODO highlight LLM output with something like Prism.JS
|
26
|
+
display('\n' + outputContent);
|
27
|
+
try {
|
28
|
+
writeFileSync(filePath, outputContent);
|
29
|
+
displaySuccess(`This report can be found in ${filePath}`);
|
30
|
+
} catch (error) {
|
31
|
+
displayError(`Failed to write answer to file: ${filePath}`);
|
32
|
+
displayError(error.message);
|
33
|
+
// Consider if you want to exit or just log the error
|
34
|
+
// process.exit(1);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Inner function to ask a question and get an answer from the LLM
|
40
|
+
* @param {Object} context - The context object
|
41
|
+
* @param {Function} indicateProgress - Function to indicate progress
|
42
|
+
* @param {string} preamble - The preamble to send to the LLM
|
43
|
+
* @param {string} content - The content of the question
|
44
|
+
* @returns {string} The answer from the LLM
|
45
|
+
*/
|
46
|
+
export async function askQuestionInner(context, indicateProgress, preamble, content) {
|
47
|
+
// This node receives the current state (messages) and invokes the LLM
|
48
|
+
const callModel = async (state) => {
|
49
|
+
// state.messages will contain the list including the system preamble and user diff
|
50
|
+
const response = await context.config.llm.invoke(state.messages);
|
51
|
+
// MessagesAnnotation expects the node to return the new message(s) to be added to the state.
|
52
|
+
// Wrap the response in an array if it's a single message object.
|
53
|
+
return { messages: response };
|
54
|
+
};
|
55
|
+
|
56
|
+
// Define the graph structure with MessagesAnnotation state
|
57
|
+
const workflow = new StateGraph(MessagesAnnotation)
|
58
|
+
// Define the node and edge
|
59
|
+
.addNode("model", callModel)
|
60
|
+
.addEdge(START, "model") // Start at the 'model' node
|
61
|
+
.addEdge("model", END); // End after the 'model' node completes
|
62
|
+
|
63
|
+
// Set up memory (optional but good practice for potential future multi-turn interactions)
|
64
|
+
const memory = new MemorySaver();
|
65
|
+
|
66
|
+
// Compile the workflow into a runnable app
|
67
|
+
const app = workflow.compile({ checkpointer: memory });
|
68
|
+
|
69
|
+
// Construct the initial the messages including the preamble as a system message
|
70
|
+
const messages = [
|
71
|
+
{
|
72
|
+
role: "system",
|
73
|
+
content: preamble, // The preamble goes here
|
74
|
+
},
|
75
|
+
{
|
76
|
+
role: "user",
|
77
|
+
content, // The question goes here
|
78
|
+
},
|
79
|
+
];
|
80
|
+
|
81
|
+
indicateProgress();
|
82
|
+
// TODO create proper progress indicator for async tasks.
|
83
|
+
const progress = setInterval(() => indicateProgress(), 1000);
|
84
|
+
const output = await app.invoke({messages}, context.session);
|
85
|
+
clearInterval(progress);
|
86
|
+
return extractLastMessageContent(output);
|
87
|
+
}
|
@@ -7,12 +7,11 @@ 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 "
|
11
|
-
import { display, displayError, displaySuccess } from "
|
12
|
-
import { fileSafeLocalDate, toFileSafeString, ProgressIndicator } from "
|
10
|
+
import { initConfig, slothContext } from "../config.js";
|
11
|
+
import { display, displayError, displaySuccess } from "../consoleUtils.js";
|
12
|
+
import { fileSafeLocalDate, toFileSafeString, ProgressIndicator, extractLastMessageContent } from "../utils.js";
|
13
13
|
|
14
14
|
export async function review(source, preamble, diff) {
|
15
|
-
await initConfig();
|
16
15
|
const progressIndicator = new ProgressIndicator("Reviewing.");
|
17
16
|
const outputContent = await reviewInner(slothContext, () => progressIndicator.indicate(), preamble, diff);
|
18
17
|
const filePath = path.resolve(process.cwd(), toFileSafeString(source)+'-'+fileSafeLocalDate()+".md");
|
@@ -72,6 +71,5 @@ export async function reviewInner(context, indicateProgress, preamble, diff) {
|
|
72
71
|
const progress = setInterval(() => indicateProgress(), 1000);
|
73
72
|
const output = await app.invoke({messages}, context.session);
|
74
73
|
clearInterval(progress);
|
75
|
-
|
76
|
-
return output.messages[output.messages.length - 1].content;
|
74
|
+
return extractLastMessageContent(output);
|
77
75
|
}
|
package/src/prompt.js
CHANGED
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import {spawnCommand} from "../utils.js";
|
2
|
+
import {displayWarning} from "../consoleUtils.js";
|
3
|
+
|
4
|
+
export async function get(_, pr) {
|
5
|
+
// TODO makes sense to check if gh is available and authenticated
|
6
|
+
if (!pr) {
|
7
|
+
displayWarning("No PR provided, skipping PR diff fetching.")
|
8
|
+
return "";
|
9
|
+
}
|
10
|
+
return spawnCommand('gh', ['pr', 'diff', pr], 'Loading PR diff...', 'Loaded PR diff.');
|
11
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import {display, displayWarning} from "../consoleUtils.js";
|
2
|
+
|
3
|
+
export async function get(config, prId) {
|
4
|
+
const issueData = await getJiraIssue(config, prId);
|
5
|
+
return `## ${prId} Requirements - ${issueData.fields?.summary}\n\n${issueData.fields?.description}`
|
6
|
+
}
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Fetches a Jira issue using the Atlassian REST API v2.
|
10
|
+
*
|
11
|
+
* @async
|
12
|
+
* @param {object} config - Configuration object.
|
13
|
+
* @param {string} config.username - Your Jira email address or username used for authentication.
|
14
|
+
* @param {string} config.token - Your Jira API token (legacy access token).
|
15
|
+
* @param {string} config.baseUrl - The base URL of your Jira instance API (e.g., 'https://your-domain.atlassian.net/rest/api/2/issue/').
|
16
|
+
* @param {string} jiraKey - The Jira issue key (e.g., 'UI-1005').
|
17
|
+
* @returns {Promise<object>} A promise that resolves with the Jira issue data as a JSON object.
|
18
|
+
* @throws {Error} Throws an error if the fetch fails, authentication is wrong, the issue is not found, or the response status is not OK.
|
19
|
+
*/
|
20
|
+
async function getJiraIssue(config, jiraKey) {
|
21
|
+
const { username, token, baseUrl } = config;
|
22
|
+
if (!jiraKey) {
|
23
|
+
displayWarning("No jiraKey provided, skipping Jira issue fetching.")
|
24
|
+
return "";
|
25
|
+
}
|
26
|
+
|
27
|
+
// Validate essential inputs
|
28
|
+
if (!username || !token || !baseUrl) {
|
29
|
+
throw new Error('Missing required parameters in config (username, token, baseUrl) or missing jiraKey.');
|
30
|
+
}
|
31
|
+
|
32
|
+
// Ensure baseUrl doesn't end with a slash to avoid double slashes in the URL
|
33
|
+
const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
34
|
+
|
35
|
+
// Construct the full API URL
|
36
|
+
const apiUrl = `${cleanBaseUrl}/${jiraKey}`;
|
37
|
+
|
38
|
+
// Encode credentials for Basic Authentication header
|
39
|
+
const credentials = `${username}:${token}`;
|
40
|
+
const encodedCredentials = Buffer.from(credentials).toString('base64');
|
41
|
+
const authHeader = `Basic ${encodedCredentials}`;
|
42
|
+
|
43
|
+
// Define request headers
|
44
|
+
const headers = {
|
45
|
+
'Authorization': authHeader,
|
46
|
+
'Accept': 'application/json', // Tell the server we expect JSON back
|
47
|
+
// 'Content-Type': 'application/json' // Usually not needed for GET requests
|
48
|
+
};
|
49
|
+
|
50
|
+
display(`Fetching Jira issue: ${apiUrl}`);
|
51
|
+
|
52
|
+
try {
|
53
|
+
const response = await fetch(apiUrl, {
|
54
|
+
method: 'GET',
|
55
|
+
headers: headers,
|
56
|
+
});
|
57
|
+
|
58
|
+
// Check if the response status code indicates success (e.g., 200 OK)
|
59
|
+
if (!response.ok) {
|
60
|
+
let errorBody = 'Could not read error body.';
|
61
|
+
try {
|
62
|
+
// Attempt to get more details from the response body for non-OK statuses
|
63
|
+
errorBody = await response.text();
|
64
|
+
} catch (e) {
|
65
|
+
// Silent fail - we already have a generic error message
|
66
|
+
}
|
67
|
+
// Throw a detailed error including status, status text, URL, and body if available
|
68
|
+
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}. URL: ${apiUrl}. Response Body: ${errorBody}`);
|
69
|
+
}
|
70
|
+
|
71
|
+
// Parse the JSON response body if the request was successful
|
72
|
+
const issueData = await response.json();
|
73
|
+
return issueData;
|
74
|
+
|
75
|
+
} catch (error) {
|
76
|
+
// Handle network errors (e.g., DNS resolution failure, connection refused)
|
77
|
+
// or errors thrown from the non-OK response check above
|
78
|
+
// Re-throw the error so the caller can handle it appropriately
|
79
|
+
throw error;
|
80
|
+
}
|
81
|
+
}
|