6
6
SystemMessage ,
7
7
} from "@langchain/core/messages" ;
8
8
import { promises as fsPromises } from "fs" ;
9
- import { type EnhancedIncident } from "@editor-extensions/shared" ;
10
9
import { type DynamicStructuredTool } from "@langchain/core/tools" ;
10
+ import { createPatch } from "diff" ;
11
11
12
12
import {
13
13
type SummarizeAdditionalInfoInputState ,
@@ -19,6 +19,7 @@ import {
19
19
} from "../schemas/analysisIssueFix" ;
20
20
import { BaseNode , type ModelInfo } from "./base" ;
21
21
import { type KaiFsCache , KaiWorkflowMessageType } from "../types" ;
22
+ import { type GetBestHintResult , SolutionServerClient } from "../clients/solutionServerClient" ;
22
23
23
24
type IssueFixResponseParserState = "reasoning" | "updatedFile" | "additionalInfo" ;
24
25
@@ -28,10 +29,12 @@ export class AnalysisIssueFix extends BaseNode {
28
29
tools : DynamicStructuredTool [ ] ,
29
30
private readonly fsCache : KaiFsCache ,
30
31
private readonly workspaceDir : string ,
32
+ private readonly solutionServerClient : SolutionServerClient ,
31
33
) {
32
34
super ( "AnalysisIssueFix" , modelInfo , tools ) ;
33
35
this . fsCache = fsCache ;
34
36
this . workspaceDir = workspaceDir ;
37
+ this . solutionServerClient = solutionServerClient ;
35
38
36
39
this . fixAnalysisIssue = this . fixAnalysisIssue . bind ( this ) ;
37
40
this . summarizeHistory = this . summarizeHistory . bind ( this ) ;
@@ -51,17 +54,15 @@ export class AnalysisIssueFix extends BaseNode {
51
54
...state ,
52
55
// since we are using a reducer, allResponses has to be reset
53
56
outputAllResponses : [ ] ,
57
+ outputHints : [ ] ,
54
58
inputFileUri : undefined ,
55
59
inputFileContent : undefined ,
56
- inputIncidentsDescription : undefined ,
60
+ inputIncidents : [ ] ,
57
61
} ;
58
62
// we have to fix the incidents if there's at least one present in state
59
63
if ( state . currentIdx < state . inputIncidentsByUris . length ) {
60
64
const nextEntry = state . inputIncidentsByUris [ state . currentIdx ] ;
61
65
if ( nextEntry ) {
62
- const incidentsDescription = ( nextEntry . incidents as EnhancedIncident [ ] )
63
- . map ( ( incident ) => `* ${ incident . lineNumber } : ${ incident . message } ` )
64
- . join ( ) ;
65
66
try {
66
67
const cachedContent = await this . fsCache . get ( nextEntry . uri ) ;
67
68
if ( cachedContent ) {
@@ -70,7 +71,7 @@ export class AnalysisIssueFix extends BaseNode {
70
71
const fileContent = await fsPromises . readFile ( nextEntry . uri , "utf8" ) ;
71
72
nextState . inputFileContent = fileContent ;
72
73
nextState . inputFileUri = nextEntry . uri ;
73
- nextState . inputIncidentsDescription = incidentsDescription ;
74
+ nextState . inputIncidents = nextEntry . incidents ;
74
75
} catch ( err ) {
75
76
this . emitWorkflowMessage ( {
76
77
type : KaiWorkflowMessageType . Error ,
@@ -92,13 +93,61 @@ export class AnalysisIssueFix extends BaseNode {
92
93
content : state . outputUpdatedFile ,
93
94
} ,
94
95
} ) ;
96
+
97
+ // Only create solution if all required fields are available
98
+ if (
99
+ this . solutionServerClient &&
100
+ state . inputFileUri &&
101
+ state . inputFileContent &&
102
+ state . outputReasoning &&
103
+ state . inputIncidents . length > 0
104
+ ) {
105
+ const incidentIds = await Promise . all (
106
+ state . inputIncidents . map ( ( incident ) =>
107
+ this . solutionServerClient . createIncident ( incident ) ,
108
+ ) ,
109
+ ) ;
110
+
111
+ try {
112
+ await this . solutionServerClient . createSolution (
113
+ incidentIds ,
114
+ {
115
+ diff : createPatch (
116
+ state . inputFileUri ,
117
+ state . inputFileContent ,
118
+ state . outputUpdatedFile ,
119
+ ) ,
120
+ before : [
121
+ {
122
+ uri : state . inputFileUri ,
123
+ content : state . inputFileContent ,
124
+ } ,
125
+ ] ,
126
+ after : [
127
+ {
128
+ uri : state . inputFileUri ,
129
+ content : state . outputUpdatedFile ,
130
+ } ,
131
+ ] ,
132
+ } ,
133
+ state . outputReasoning ,
134
+ state . outputHints || [ ] ,
135
+ ) ;
136
+ } catch ( error ) {
137
+ console . error ( `Failed to create solution: ${ error } ` ) ;
138
+ }
139
+ } else {
140
+ console . error ( "Missing required fields for solution creation" ) ;
141
+ }
142
+
95
143
nextState . outputAllResponses = [
96
144
{
97
145
...state ,
98
146
} ,
99
147
] ;
100
148
nextState . outputUpdatedFile = undefined ;
101
149
nextState . outputAdditionalInfo = undefined ;
150
+ nextState . outputHints = [ ] ;
102
151
}
103
152
// if this was the last file we worked on, accumulate additional infromation
104
153
if ( state . currentIdx === state . inputIncidentsByUris . length ) {
@@ -107,7 +156,9 @@ export class AnalysisIssueFix extends BaseNode {
107
156
return {
108
157
reasoning : `${ acc . reasoning } \n${ val . outputReasoning } ` ,
109
158
additionalInfo : `${ acc . additionalInfo } \n${ val . outputAdditionalInfo } ` ,
110
- uris : acc . uris . concat ( [ relative ( this . workspaceDir , val . outputUpdatedFileUri ! ) ] ) ,
159
+ uris : val . outputUpdatedFileUri
160
+ ? acc . uris . concat ( [ relative ( this . workspaceDir , val . outputUpdatedFileUri ) ] )
161
+ : acc . uris ,
111
162
} ;
112
163
} ,
113
164
{
@@ -127,15 +178,42 @@ export class AnalysisIssueFix extends BaseNode {
127
178
async fixAnalysisIssue (
128
179
state : typeof AnalysisIssueFixInputState . State ,
129
180
) : Promise < typeof AnalysisIssueFixOutputState . State > {
130
- if ( ! state . inputFileUri || ! state . inputFileContent || ! state . inputIncidentsDescription ) {
181
+ if ( ! state . inputFileUri || ! state . inputFileContent || state . inputIncidents . length === 0 ) {
131
182
return {
132
183
outputUpdatedFile : undefined ,
133
184
outputAdditionalInfo : undefined ,
134
185
outputReasoning : undefined ,
135
186
outputUpdatedFileUri : state . inputFileUri ,
187
+ outputHints : [ ] ,
136
188
} ;
137
189
}
138
190
191
+ // Process incidents in a single loop, collecting hints and creating incidents
192
+ const seenViolationTypes = new Set < string > ( ) ;
193
+ const hints : GetBestHintResult [ ] = [ ] ;
194
+
195
+ for ( const incident of state . inputIncidents ) {
196
+ // Check if we need to get a hint for this violation type
197
+ if ( incident . ruleset_name && incident . violation_name ) {
198
+ const violationKey = `${ incident . ruleset_name } ::${ incident . violation_name } ` ;
199
+
200
+ if ( ! seenViolationTypes . has ( violationKey ) ) {
201
+ seenViolationTypes . add ( violationKey ) ;
202
+ try {
203
+ const hint = await this . solutionServerClient . getBestHint (
204
+ incident . ruleset_name ,
205
+ incident . violation_name ,
206
+ ) ;
207
+ if ( hint ) {
208
+ hints . push ( hint ) ;
209
+ }
210
+ } catch ( error ) {
211
+ console . warn ( `Failed to get hint for ${ violationKey } : ${ error } ` ) ;
212
+ }
213
+ }
214
+ }
215
+ }
216
+
139
217
const fileName = basename ( state . inputFileUri ) ;
140
218
141
219
const sysMessage = new SystemMessage (
@@ -164,7 +242,12 @@ ${state.inputFileContent}
164
242
\`\`\`
165
243
166
244
## Issues
167
- ${ state . inputIncidentsDescription }
245
+ ${ state . inputIncidents
246
+ . map ( ( incident ) => {
247
+ return `* ${ incident . lineNumber } : ${ incident . message } ` ;
248
+ } )
249
+ . join ( "\n" ) }
250
+ ${ hints . length > 0 ? `\n## Hints\n${ hints . map ( ( hint ) => `* ${ hint . hint } ` ) . join ( "\n" ) } ` : "" }
168
251
169
252
# Output Instructions
170
253
Structure your output in Markdown format such as:
@@ -190,6 +273,7 @@ If you have any additional details or steps that need to be performed, put it he
190
273
outputUpdatedFile : undefined ,
191
274
outputReasoning : undefined ,
192
275
outputUpdatedFileUri : state . inputFileUri ,
276
+ outputHints : [ ] ,
193
277
} ;
194
278
}
195
279
@@ -200,6 +284,7 @@ If you have any additional details or steps that need to be performed, put it he
200
284
outputUpdatedFile : updatedFile ,
201
285
outputAdditionalInfo : additionalInfo ,
202
286
outputUpdatedFileUri : state . inputFileUri ,
287
+ outputHints : hints . map ( ( hint ) => hint . hint_id ) ,
203
288
} ;
204
289
}
205
290
0 commit comments