Skip to content

Commit 35cf89b

Browse files
authored
✨ add solution server (#547)
Fixes #483 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added integration with a remote solution server, enabling incident and solution management, success rate retrieval, and hint suggestions. * Introduced new user-configurable settings for solution server URL and enablement in the extension. * Enhanced incident handling with structured data and improved prompt formatting. * Added support for tracking solution and client identifiers across incidents and solutions. * Integrated solution server client lifecycle and configuration into the extension with connection management and restart prompts. * Added debug logging and refined routing logic based on structured incident data. * **Bug Fixes** * Improved validation and error handling for solution server interactions. * **Chores** * Updated development scripts to support the new agentic workspace. * Added new dependencies to support solution server communication and UI enhancements. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: David Zager <[email protected]>
1 parent 3ba05d3 commit 35cf89b

File tree

17 files changed

+7840
-2981
lines changed

17 files changed

+7840
-2981
lines changed

agentic/src/clients/solutionServerClient.ts

Lines changed: 417 additions & 0 deletions
Large diffs are not rendered by default.

agentic/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./types";
22
export * from "./fsCache";
33
export * from "./workflows";
4+
export * from "./clients/solutionServerClient";

agentic/src/nodes/analysisIssueFix.ts

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
SystemMessage,
77
} from "@langchain/core/messages";
88
import { promises as fsPromises } from "fs";
9-
import { type EnhancedIncident } from "@editor-extensions/shared";
109
import { type DynamicStructuredTool } from "@langchain/core/tools";
10+
import { createPatch } from "diff";
1111

1212
import {
1313
type SummarizeAdditionalInfoInputState,
@@ -19,6 +19,7 @@ import {
1919
} from "../schemas/analysisIssueFix";
2020
import { BaseNode, type ModelInfo } from "./base";
2121
import { type KaiFsCache, KaiWorkflowMessageType } from "../types";
22+
import { type GetBestHintResult, SolutionServerClient } from "../clients/solutionServerClient";
2223

2324
type IssueFixResponseParserState = "reasoning" | "updatedFile" | "additionalInfo";
2425

@@ -28,10 +29,12 @@ export class AnalysisIssueFix extends BaseNode {
2829
tools: DynamicStructuredTool[],
2930
private readonly fsCache: KaiFsCache,
3031
private readonly workspaceDir: string,
32+
private readonly solutionServerClient: SolutionServerClient,
3133
) {
3234
super("AnalysisIssueFix", modelInfo, tools);
3335
this.fsCache = fsCache;
3436
this.workspaceDir = workspaceDir;
37+
this.solutionServerClient = solutionServerClient;
3538

3639
this.fixAnalysisIssue = this.fixAnalysisIssue.bind(this);
3740
this.summarizeHistory = this.summarizeHistory.bind(this);
@@ -51,17 +54,15 @@ export class AnalysisIssueFix extends BaseNode {
5154
...state,
5255
// since we are using a reducer, allResponses has to be reset
5356
outputAllResponses: [],
57+
outputHints: [],
5458
inputFileUri: undefined,
5559
inputFileContent: undefined,
56-
inputIncidentsDescription: undefined,
60+
inputIncidents: [],
5761
};
5862
// we have to fix the incidents if there's at least one present in state
5963
if (state.currentIdx < state.inputIncidentsByUris.length) {
6064
const nextEntry = state.inputIncidentsByUris[state.currentIdx];
6165
if (nextEntry) {
62-
const incidentsDescription = (nextEntry.incidents as EnhancedIncident[])
63-
.map((incident) => `* ${incident.lineNumber}: ${incident.message}`)
64-
.join();
6566
try {
6667
const cachedContent = await this.fsCache.get(nextEntry.uri);
6768
if (cachedContent) {
@@ -70,7 +71,7 @@ export class AnalysisIssueFix extends BaseNode {
7071
const fileContent = await fsPromises.readFile(nextEntry.uri, "utf8");
7172
nextState.inputFileContent = fileContent;
7273
nextState.inputFileUri = nextEntry.uri;
73-
nextState.inputIncidentsDescription = incidentsDescription;
74+
nextState.inputIncidents = nextEntry.incidents;
7475
} catch (err) {
7576
this.emitWorkflowMessage({
7677
type: KaiWorkflowMessageType.Error,
@@ -92,13 +93,61 @@ export class AnalysisIssueFix extends BaseNode {
9293
content: state.outputUpdatedFile,
9394
},
9495
});
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+
95143
nextState.outputAllResponses = [
96144
{
97145
...state,
98146
},
99147
];
100148
nextState.outputUpdatedFile = undefined;
101149
nextState.outputAdditionalInfo = undefined;
150+
nextState.outputHints = [];
102151
}
103152
// if this was the last file we worked on, accumulate additional infromation
104153
if (state.currentIdx === state.inputIncidentsByUris.length) {
@@ -107,7 +156,9 @@ export class AnalysisIssueFix extends BaseNode {
107156
return {
108157
reasoning: `${acc.reasoning}\n${val.outputReasoning}`,
109158
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,
111162
};
112163
},
113164
{
@@ -127,15 +178,42 @@ export class AnalysisIssueFix extends BaseNode {
127178
async fixAnalysisIssue(
128179
state: typeof AnalysisIssueFixInputState.State,
129180
): Promise<typeof AnalysisIssueFixOutputState.State> {
130-
if (!state.inputFileUri || !state.inputFileContent || !state.inputIncidentsDescription) {
181+
if (!state.inputFileUri || !state.inputFileContent || state.inputIncidents.length === 0) {
131182
return {
132183
outputUpdatedFile: undefined,
133184
outputAdditionalInfo: undefined,
134185
outputReasoning: undefined,
135186
outputUpdatedFileUri: state.inputFileUri,
187+
outputHints: [],
136188
};
137189
}
138190

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+
139217
const fileName = basename(state.inputFileUri);
140218

141219
const sysMessage = new SystemMessage(
@@ -164,7 +242,12 @@ ${state.inputFileContent}
164242
\`\`\`
165243
166244
## 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")}` : ""}
168251
169252
# Output Instructions
170253
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
190273
outputUpdatedFile: undefined,
191274
outputReasoning: undefined,
192275
outputUpdatedFileUri: state.inputFileUri,
276+
outputHints: [],
193277
};
194278
}
195279

@@ -200,6 +284,7 @@ If you have any additional details or steps that need to be performed, put it he
200284
outputUpdatedFile: updatedFile,
201285
outputAdditionalInfo: additionalInfo,
202286
outputUpdatedFileUri: state.inputFileUri,
287+
outputHints: hints.map((hint) => hint.hint_id),
203288
};
204289
}
205290

agentic/src/schemas/analysisIssueFix.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const AnalysisIssueFixInputState = Annotation.Root({
1616
...BaseInputMetaState.spec,
1717
inputFileUri: Annotation<string | undefined>,
1818
inputFileContent: Annotation<string | undefined>,
19-
inputIncidentsDescription: Annotation<string | undefined>,
19+
inputIncidents: Annotation<Array<EnhancedIncident>>,
2020
});
2121

2222
// output state for node that fixes an analysis issue
@@ -25,6 +25,7 @@ export const AnalysisIssueFixOutputState = Annotation.Root({
2525
outputUpdatedFile: Annotation<string | undefined>,
2626
outputAdditionalInfo: Annotation<string | undefined>,
2727
outputReasoning: Annotation<string | undefined>,
28+
outputHints: Annotation<Array<number>>,
2829
});
2930

3031
// input state for nodes that summarize changes made so far and also outline additional info to address

agentic/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type RunnableConfig } from "@langchain/core/runnables";
22
import { type EnhancedIncident } from "@editor-extensions/shared";
33
import { type AIMessageChunk, type AIMessage } from "@langchain/core/messages";
44
import { type BaseChatModel } from "@langchain/core/language_models/chat_models";
5+
import { SolutionServerClient } from "./clients/solutionServerClient";
56

67
export interface BaseWorkflowMessage<KaiWorkflowMessageType, D> {
78
type: KaiWorkflowMessageType;
@@ -69,6 +70,7 @@ export interface KaiWorkflowInitOptions {
6970
model: BaseChatModel;
7071
workspaceDir: string;
7172
fsCache: KaiFsCache;
73+
solutionServerClient: SolutionServerClient;
7274
}
7375

7476
export interface KaiWorkflowInput {

agentic/src/workflows/interactiveWorkflow.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export class KaiInteractiveWorkflow
112112
fsTools.all(),
113113
options.fsCache,
114114
workspaceDir,
115+
options.solutionServerClient,
115116
);
116117
// relay events from nodes back to callers
117118
analysisIssueFixNodes.on("workflowMessage", async (msg: KaiWorkflowMessage) => {
@@ -243,14 +244,15 @@ export class KaiInteractiveWorkflow
243244
// internal fields
244245
inputFileContent: undefined,
245246
inputFileUri: undefined,
246-
inputIncidentsDescription: undefined,
247+
inputIncidents: [],
247248
inputAllAdditionalInfo: undefined,
248249
inputAllReasoning: undefined,
249250
inputAllModifiedFiles: [],
250251
outputAdditionalInfo: undefined,
251252
outputReasoning: undefined,
252253
outputUpdatedFile: undefined,
253254
outputUpdatedFileUri: undefined,
255+
outputHints: [],
254256
outputAllResponses: [],
255257
};
256258

@@ -363,28 +365,55 @@ export class KaiInteractiveWorkflow
363365
async analysisIssueFixRouterEdge(
364366
state: typeof AnalysisIssueFixOrchestratorState.State,
365367
): Promise<string | string[]> {
368+
console.debug(`Edge function called with state:`, {
369+
hasInputFileContent: !!state.inputFileContent,
370+
hasInputFileUri: !!state.inputFileUri,
371+
hasInputIncidents: !!state.inputIncidents && state.inputIncidents.length > 0,
372+
hasOutputUpdatedFile: !!state.outputUpdatedFile,
373+
hasOutputUpdatedFileUri: !!state.outputUpdatedFileUri,
374+
currentIdx: state.currentIdx,
375+
totalIncidents: state.inputIncidentsByUris.length,
376+
enableAdditionalInformation: state.enableAdditionalInformation,
377+
hasInputAllAdditionalInfo: !!state.inputAllAdditionalInfo,
378+
hasInputAllReasoning: !!state.inputAllReasoning,
379+
});
380+
366381
// if these attributes are available, router meant to solve the analysis issue
367-
if (state.inputFileContent && state.inputFileUri && state.inputIncidentsDescription) {
382+
if (state.inputFileContent && state.inputFileUri && state.inputIncidents.length > 0) {
368383
return "fix_analysis_issue";
369384
}
385+
370386
// if there was a response, router needs to accumulate it
371387
if (state.outputUpdatedFile && state.outputUpdatedFileUri) {
388+
console.debug(`Going to fix_analysis_issue_router to accumulate response`);
372389
return "fix_analysis_issue_router";
373390
}
391+
374392
// if there were any errors earlier in the router, go back to router
375393
// this will make router pick up the next incident in list
376394
if (
377395
state.currentIdx < state.inputIncidentsByUris.length &&
378-
(!state.inputFileContent || !state.inputFileUri || !state.inputIncidentsDescription)
396+
(!state.inputFileContent || !state.inputFileUri || state.inputIncidents.length === 0)
379397
) {
398+
console.debug(`Going back to fix_analysis_issue_router for next incident`);
380399
return "fix_analysis_issue_router";
381400
}
401+
382402
// if the router accumulated all responses, we need to go to additional information
383403
if (state.enableAdditionalInformation) {
384404
if (state.inputAllAdditionalInfo && state.inputAllReasoning) {
405+
console.debug(`Going to summarize both additional info and history`);
385406
return ["summarize_additional_information", "summarize_history"];
386407
} else if (state.inputAllAdditionalInfo) {
408+
console.debug(`Going to summarize additional information only`);
387409
return "summarize_additional_information";
410+
} else {
411+
// Additional information is enabled but not accumulated yet
412+
// This means we need to go back to router to continue processing
413+
console.debug(
414+
`Additional info enabled but not accumulated, going to fix_analysis_issue_router`,
415+
);
416+
return "fix_analysis_issue_router";
388417
}
389418
}
390419
return END;

0 commit comments

Comments
 (0)