Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
417 changes: 417 additions & 0 deletions agentic/src/clients/solutionServerClient.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions agentic/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./types";
export * from "./fsCache";
export * from "./workflows";
export * from "./clients/solutionServerClient";
90 changes: 82 additions & 8 deletions agentic/src/nodes/analysisIssueFix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
SystemMessage,
} from "@langchain/core/messages";
import { promises as fsPromises } from "fs";
import { type EnhancedIncident } from "@editor-extensions/shared";
import { type DynamicStructuredTool } from "@langchain/core/tools";
import { createPatch } from "diff";

import {
type SummarizeAdditionalInfoInputState,
Expand All @@ -19,6 +19,7 @@ import {
} from "../schemas/analysisIssueFix";
import { BaseNode, type ModelInfo } from "./base";
import { type KaiFsCache, KaiWorkflowMessageType } from "../types";
import { type GetBestHintResult, SolutionServerClient } from "src/clients/solutionServerClient";

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

Expand All @@ -28,10 +29,12 @@ export class AnalysisIssueFix extends BaseNode {
tools: DynamicStructuredTool[],
private readonly fsCache: KaiFsCache,
private readonly workspaceDir: string,
private readonly solutionServerClient: SolutionServerClient,
) {
super("AnalysisIssueFix", modelInfo, tools);
this.fsCache = fsCache;
this.workspaceDir = workspaceDir;
this.solutionServerClient = solutionServerClient;

this.fixAnalysisIssue = this.fixAnalysisIssue.bind(this);
this.summarizeHistory = this.summarizeHistory.bind(this);
Expand All @@ -51,17 +54,15 @@ export class AnalysisIssueFix extends BaseNode {
...state,
// since we are using a reducer, allResponses has to be reset
outputAllResponses: [],
outputHints: [],
inputFileUri: undefined,
inputFileContent: undefined,
inputIncidentsDescription: undefined,
inputIncidents: [],
};
// we have to fix the incidents if there's at least one present in state
if (state.currentIdx < state.inputIncidentsByUris.length) {
const nextEntry = state.inputIncidentsByUris[state.currentIdx];
if (nextEntry) {
const incidentsDescription = (nextEntry.incidents as EnhancedIncident[])
.map((incident) => `* ${incident.lineNumber}: ${incident.message}`)
.join();
try {
const cachedContent = await this.fsCache.get(nextEntry.uri);
if (cachedContent) {
Expand All @@ -70,7 +71,7 @@ export class AnalysisIssueFix extends BaseNode {
const fileContent = await fsPromises.readFile(nextEntry.uri, "utf8");
nextState.inputFileContent = fileContent;
nextState.inputFileUri = nextEntry.uri;
nextState.inputIncidentsDescription = incidentsDescription;
nextState.inputIncidents = nextEntry.incidents;
} catch (err) {
this.emitWorkflowMessage({
type: KaiWorkflowMessageType.Error,
Expand All @@ -92,13 +93,52 @@ export class AnalysisIssueFix extends BaseNode {
content: state.outputUpdatedFile,
},
});
if (this.solutionServerClient) {
if (!state.inputFileUri || !state.inputFileContent || !state.outputReasoning) {
console.warn("Missing required fields for solution creation");
}
const incidentIds = await Promise.all(
state.inputIncidents.map((incident) =>
this.solutionServerClient.createIncident(incident),
),
);
try {
await this.solutionServerClient.createSolution(
incidentIds,
{
diff: createPatch(
state.inputFileUri!,
state.inputFileContent!,
state.outputUpdatedFile,
),
before: [
{
uri: state.inputFileUri!,
content: state.inputFileContent!,
},
],
after: [
{
uri: state.inputFileUri!,
content: state.outputUpdatedFile!,
},
],
},
state.outputReasoning!,
state.outputHints || [],
);
} catch (error) {
console.warn(`Failed to create solution: ${error}`);
}
}
nextState.outputAllResponses = [
{
...state,
},
];
nextState.outputUpdatedFile = undefined;
nextState.outputAdditionalInfo = undefined;
nextState.outputHints = [];
}
// if this was the last file we worked on, accumulate additional infromation
if (state.currentIdx === state.inputIncidentsByUris.length) {
Expand Down Expand Up @@ -127,15 +167,42 @@ export class AnalysisIssueFix extends BaseNode {
async fixAnalysisIssue(
state: typeof AnalysisIssueFixInputState.State,
): Promise<typeof AnalysisIssueFixOutputState.State> {
if (!state.inputFileUri || !state.inputFileContent || !state.inputIncidentsDescription) {
if (!state.inputFileUri || !state.inputFileContent || state.inputIncidents.length === 0) {
return {
outputUpdatedFile: undefined,
outputAdditionalInfo: undefined,
outputReasoning: undefined,
outputUpdatedFileUri: state.inputFileUri,
outputHints: [],
};
}

// Process incidents in a single loop, collecting hints and creating incidents
const seenViolationTypes = new Set<string>();
const hints: GetBestHintResult[] = [];

for (const incident of state.inputIncidents) {
// Check if we need to get a hint for this violation type
if (incident.ruleset_name && incident.violation_name) {
const violationKey = `${incident.ruleset_name}::${incident.violation_name}`;

if (!seenViolationTypes.has(violationKey)) {
seenViolationTypes.add(violationKey);
try {
const hint = await this.solutionServerClient.getBestHint(
incident.ruleset_name,
incident.violation_name,
);
if (hint) {
hints.push(hint);
}
} catch (error) {
console.warn(`Failed to get hint for ${violationKey}: ${error}`);
}
}
}
}

const fileName = basename(state.inputFileUri);

const sysMessage = new SystemMessage(
Expand Down Expand Up @@ -164,7 +231,12 @@ ${state.inputFileContent}
\`\`\`

## Issues
${state.inputIncidentsDescription}
${state.inputIncidents
.map((incident) => {
return `* ${incident.lineNumber}: ${incident.message}`;
})
.join("\n")}
${hints.length > 0 ? `\n## Hints\n${hints.map((hint) => `* ${hint.hint}`).join("\n")}` : ""}

# Output Instructions
Structure your output in Markdown format such as:
Expand All @@ -190,6 +262,7 @@ If you have any additional details or steps that need to be performed, put it he
outputUpdatedFile: undefined,
outputReasoning: undefined,
outputUpdatedFileUri: state.inputFileUri,
outputHints: [],
};
}

Expand All @@ -200,6 +273,7 @@ If you have any additional details or steps that need to be performed, put it he
outputUpdatedFile: updatedFile,
outputAdditionalInfo: additionalInfo,
outputUpdatedFileUri: state.inputFileUri,
outputHints: hints.map((hint) => hint.hint_id),
};
}

Expand Down
3 changes: 2 additions & 1 deletion agentic/src/schemas/analysisIssueFix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const AnalysisIssueFixInputState = Annotation.Root({
...BaseInputMetaState.spec,
inputFileUri: Annotation<string | undefined>,
inputFileContent: Annotation<string | undefined>,
inputIncidentsDescription: Annotation<string | undefined>,
inputIncidents: Annotation<Array<EnhancedIncident>>,
});

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

// input state for nodes that summarize changes made so far and also outline additional info to address
Expand Down
2 changes: 2 additions & 0 deletions agentic/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type RunnableConfig } from "@langchain/core/runnables";
import { type EnhancedIncident } from "@editor-extensions/shared";
import { type AIMessageChunk, type AIMessage } from "@langchain/core/messages";
import { type BaseChatModel } from "@langchain/core/language_models/chat_models";
import { SolutionServerClient } from "./clients/solutionServerClient";

export interface BaseWorkflowMessage<KaiWorkflowMessageType, D> {
type: KaiWorkflowMessageType;
Expand Down Expand Up @@ -69,6 +70,7 @@ export interface KaiWorkflowInitOptions {
model: BaseChatModel;
workspaceDir: string;
fsCache: KaiFsCache;
solutionServerClient: SolutionServerClient;
}

export interface KaiWorkflowInput {
Expand Down
35 changes: 32 additions & 3 deletions agentic/src/workflows/interactiveWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class KaiInteractiveWorkflow
fsTools.all(),
options.fsCache,
workspaceDir,
options.solutionServerClient,
);
// relay events from nodes back to callers
analysisIssueFixNodes.on("workflowMessage", async (msg: KaiWorkflowMessage) => {
Expand Down Expand Up @@ -243,14 +244,15 @@ export class KaiInteractiveWorkflow
// internal fields
inputFileContent: undefined,
inputFileUri: undefined,
inputIncidentsDescription: undefined,
inputIncidents: [],
inputAllAdditionalInfo: undefined,
inputAllReasoning: undefined,
inputAllModifiedFiles: [],
outputAdditionalInfo: undefined,
outputReasoning: undefined,
outputUpdatedFile: undefined,
outputUpdatedFileUri: undefined,
outputHints: [],
outputAllResponses: [],
};

Expand Down Expand Up @@ -363,28 +365,55 @@ export class KaiInteractiveWorkflow
async analysisIssueFixRouterEdge(
state: typeof AnalysisIssueFixOrchestratorState.State,
): Promise<string | string[]> {
console.log(`[DEBUG] Edge function called with state:`, {
hasInputFileContent: !!state.inputFileContent,
hasInputFileUri: !!state.inputFileUri,
hasInputIncidents: !!state.inputIncidents && state.inputIncidents.length > 0,
hasOutputUpdatedFile: !!state.outputUpdatedFile,
hasOutputUpdatedFileUri: !!state.outputUpdatedFileUri,
currentIdx: state.currentIdx,
totalIncidents: state.inputIncidentsByUris.length,
enableAdditionalInformation: state.enableAdditionalInformation,
hasInputAllAdditionalInfo: !!state.inputAllAdditionalInfo,
hasInputAllReasoning: !!state.inputAllReasoning,
});

// if these attributes are available, router meant to solve the analysis issue
if (state.inputFileContent && state.inputFileUri && state.inputIncidentsDescription) {
if (state.inputFileContent && state.inputFileUri && state.inputIncidents.length > 0) {
return "fix_analysis_issue";
}

// if there was a response, router needs to accumulate it
if (state.outputUpdatedFile && state.outputUpdatedFileUri) {
console.log(`[DEBUG] Going to fix_analysis_issue_router to accumulate response`);
return "fix_analysis_issue_router";
}

// if there were any errors earlier in the router, go back to router
// this will make router pick up the next incident in list
if (
state.currentIdx < state.inputIncidentsByUris.length &&
(!state.inputFileContent || !state.inputFileUri || !state.inputIncidentsDescription)
(!state.inputFileContent || !state.inputFileUri || state.inputIncidents.length === 0)
) {
console.log(`[DEBUG] Going back to fix_analysis_issue_router for next incident`);
return "fix_analysis_issue_router";
}

// if the router accumulated all responses, we need to go to additional information
if (state.enableAdditionalInformation) {
if (state.inputAllAdditionalInfo && state.inputAllReasoning) {
console.log(`[DEBUG] Going to summarize both additional info and history`);
return ["summarize_additional_information", "summarize_history"];
} else if (state.inputAllAdditionalInfo) {
console.log(`[DEBUG] Going to summarize additional information only`);
return "summarize_additional_information";
} else {
// Additional information is enabled but not accumulated yet
// This means we need to go back to router to continue processing
console.log(
`[DEBUG] Additional info enabled but not accumulated, going to fix_analysis_issue_router`,
);
return "fix_analysis_issue_router";
}
}
return END;
Expand Down
Loading
Loading