Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
190 changes: 190 additions & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import { formatResponse } from "../prompts/responses"
import { validateToolUse } from "../tools/validateToolUse"
import { Task } from "../task/Task"
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
import { AudioProcessor } from "../../core/processors/AudioProcessor"; // Added for analyze_multimodal_data
import { CsvProcessor } from "../../core/processors/CsvProcessor"; // Added for analyze_multimodal_data
import fs from "fs/promises"; // Added for analyze_multimodal_data
import path from "path"; // Added for analyze_multimodal_data

/**
* Processes and presents assistant message content to the user interface.
Expand Down Expand Up @@ -466,6 +470,192 @@ export async function presentAssistantMessage(cline: Task) {
askFinishSubTaskApproval,
)
break
// --- synthesize_and_plan case START ---
case "synthesize_and_plan": {
const goal: string | undefined = block.params.goal;
const toolName: ToolName = "synthesize_and_plan";

try {
if (block.partial) {
await cline.ask(
"tool",
JSON.stringify({ tool: toolName, goal: removeClosingTag("goal", goal) }),
block.partial,
).catch(() => {});
break;
}

if (!goal) {
cline.consecutiveMistakeCount++;
cline.recordToolError(toolName);
pushToolResult(await cline.sayAndCreateMissingParamError(toolName, "goal"));
break;
}
cline.consecutiveMistakeCount = 0;

const didApprove = await askApproval("tool", `Synthesizing a plan for goal: ${goal}`);
if (!didApprove) {
pushToolResult(formatResponse.toolDenied());
break;
}

const conversationSummary = cline.clineMessages
.map(m => `[${new Date(m.ts).toLocaleTimeString()}] ${m.type} ${m.say || m.ask}: ${m.text?.substring(0, 200)}`)
.join('\n');

const environmentDetails = await cline.getEnvironmentDetails(false, false);

const metaPrompt = `You are a strategic AI planning assistant. Analyze the situation and formulate a plan.

GOAL: "${goal}"

CURRENT CONTEXT:
<conversation_history>
${conversationSummary}
</conversation_history>

<workspace_state>
${environmentDetails}
</workspace_state>

Based on all information, update the agent's mental model. Respond ONLY with a JSON object with keys "synthesis" (a brief summary of the current state) and "plan" (a string array of concrete next steps).`;

await cline.say("api_req_started", JSON.stringify({ request: `Synthesizing plan for: "${goal}"` }), [], false, undefined, undefined, {isNonInteractive: true});

let planJson = "";
const stream = cline.api.createMessage(metaPrompt, [{role: "user", content: "Generate the plan."}]);
for await (const chunk of stream) {
if (chunk.type === "text") {
planJson += chunk.text;
} else if (chunk.type === "usage") {
// Not explicitly handling usage for this internal LLM call in this tool
}
}
planJson = planJson.trim();

try {
const parsedState = JSON.parse(planJson);
if (parsedState.synthesis && Array.isArray(parsedState.plan)) {
cline.agentState = {
synthesis: parsedState.synthesis,
plan: parsedState.plan,
};
await cline.say("completion_result", `New plan synthesized and adopted:\n- ${cline.agentState.plan.join("\n- ")}`, [], false, undefined, undefined, {isNonInteractive: true});
pushToolResult(formatResponse.toolResult("Internal state and plan have been updated successfully."));
} else {
throw new Error("LLM response for plan did not contain correct JSON structure (synthesis and plan array).");
}
} catch (parseError: any) {
cline.recordToolError(toolName, `Failed to parse LLM response as JSON: ${parseError.message}. Response: ${planJson}`);
pushToolResult(formatResponse.toolError(`Failed to update mental model. LLM response was not valid JSON: ${planJson.substring(0, 200)}...`));
}

cline.recordToolUsage(toolName);
break;
} catch (error) {
cline.recordToolError(toolName, error instanceof Error ? error.message : String(error));
await handleError("synthesizing and planning", error instanceof Error ? error : new Error(String(error)));
break;
}
}
// --- synthesize_and_plan case END ---
// --- analyze_multimodal_data case START ---
case "analyze_multimodal_data": {
const file_paths_param: string | undefined = block.params.file_paths;
const toolName: ToolName = "analyze_multimodal_data";

// `this` inside presentAssistantMessage refers to `cline` (the Task instance)
// `askApproval`, `handleError`, `pushToolResult`, `removeClosingTag` are passed into `presentAssistantMessage`

try {
if (block.partial) {
await cline.ask( // Use cline directly
"tool",
JSON.stringify({ tool: toolName, paths: removeClosingTag("file_paths", file_paths_param) }),
block.partial,
).catch(() => {});
break;
}

if (!file_paths_param) {
cline.consecutiveMistakeCount++;
cline.recordToolError(toolName);
pushToolResult(await cline.sayAndCreateMissingParamError(toolName, "file_paths"));
break;
}
cline.consecutiveMistakeCount = 0;

const relPaths = file_paths_param.split('\n').map(p => p.trim()).filter(Boolean);
if (relPaths.length === 0) {
cline.recordToolError(toolName, "No file paths provided after splitting and filtering.");
pushToolResult(formatResponse.toolError("No file paths provided."));
break;
}

// Use the askApproval passed into presentAssistantMessage
const didApprove = await askApproval("tool", `Analyzing data from: ${relPaths.join(', ')}`);
if (!didApprove) {
pushToolResult(formatResponse.toolDenied());
break;
}

await cline.say("api_req_started", JSON.stringify({ request: `Analyzing ${relPaths.length} file(s)...`}), [], false, undefined, undefined, { isNonInteractive: true });

let analysisResults = "";
for (const relPath of relPaths) {
const absolutePath = path.resolve(cline.cwd, relPath);
const extension = path.extname(relPath).toLowerCase();
let result = `\n--- Analysis for ${relPath} ---\n`;

try {
if (!cline.rooIgnoreController?.validateAccess(relPath)) {
result += formatResponse.rooIgnoreError(relPath);
analysisResults += result;
continue;
}
await fs.access(absolutePath);

switch (extension) {
case '.wav':
case '.mp3':
result += await AudioProcessor.process(absolutePath);
break;
case '.csv':
result += await CsvProcessor.process(absolutePath);
break;
case '.json':
const jsonContent = await fs.readFile(absolutePath, 'utf-8');
JSON.parse(jsonContent);
result += `File is a valid JSON. Content length: ${jsonContent.length} characters. First 500 chars:\n${jsonContent.substring(0, 500)}`;
break;
case '.txt':
default:
const textContent = await fs.readFile(absolutePath, 'utf-8');
result += `File treated as plain text. Content length: ${textContent.length} characters. First 500 chars:\n${textContent.substring(0, 500)}`;
break;
}
} catch (e: any) {
if (e.code === 'ENOENT') {
result += `Error processing file: File not found at ${relPath}`;
} else {
result += `Error processing file ${relPath}: ${e.message}`;
}
}
analysisResults += result + "\n";
}

await cline.say("completion_result", `Analysis complete for ${relPaths.length} file(s). Results included in tool output.`, [], false, undefined, undefined, { isNonInteractive: true });
pushToolResult(formatResponse.toolResult(analysisResults.trim()));
cline.recordToolUsage(toolName);
break;
} catch (error) {
cline.recordToolError(toolName, error instanceof Error ? error.message : String(error));
// Use handleError passed into presentAssistantMessage
await handleError("analyzing multimodal data", error instanceof Error ? error : new Error(String(error)));
break;
}
}
// --- analyze_multimodal_data case END ---
}

break
Expand Down
21 changes: 21 additions & 0 deletions src/core/processors/AudioProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// src/core/processors/AudioProcessor.ts
// import { exec } from "child_process"; // Commented out for now
// import { promisify } from "util"; // Commented out for now

export class AudioProcessor {
static async process(filePath: string): Promise<string> {
// In a real scenario, this would call a local model or cloud STT API.
// For example, using a CLI tool like 'whisper':
// const { stdout } = await promisify(exec)(`whisper "${filePath}" --model tiny --language en`);
// return stdout;

// Simulate a delay as if processing audio
await new Promise(resolve => setTimeout(resolve, 500)); // 0.5 second delay

// Extract filename for more dynamic simulated message
const fileName = filePath.split(/[\/\\]/).pop() || filePath; // Handles both / and \ separators

return `[Simulated Transcription for ${fileName}]
User reported a critical bug in the data processing pipeline. It seems to be related to the 'user_id' field during the nightly aggregation job. The error logs are inconclusive. Please check the 'user_transactions.csv' file for anomalies around the last run.`;
}
}
61 changes: 61 additions & 0 deletions src/core/processors/CsvProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// src/core/processors/CsvProcessor.ts
import fs from "fs/promises";
import path from "path"; // For extracting filename

export class CsvProcessor {
static async process(filePath: string): Promise<string> {
const fileName = path.basename(filePath);
try {
const content = await fs.readFile(filePath, "utf-8");
const lines = content.split('\n').filter(Boolean); // Filter out empty lines

if (lines.length === 0) {
return `CSV file '${fileName}' is empty.`;
}

const headers = lines[0].split(',').map(h => h.trim()); // Trim headers
const rowCount = lines.length - 1;

// Perform a simple analysis: find potential anomalies if 'user_id' exists.
let anomaly_report = "No specific anomalies detected in initial scan.";
const userIdHeaderIndex = headers.findIndex(h => h.toLowerCase() === 'user_id'); // Case-insensitive search

if (userIdHeaderIndex !== -1 && rowCount > 0) {
let missingOrMalformedCount = 0;
for (let i = 1; i < lines.length; i++) { // Start from 1 to skip header line
const row = lines[i].split(',');
if (row.length > userIdHeaderIndex) {
const userIdValue = row[userIdHeaderIndex]?.trim();
if (!userIdValue || userIdValue.length < 3) { // Example: malformed if less than 3 chars
missingOrMalformedCount++;
}
} else {
missingOrMalformedCount++; // Row doesn't even have enough columns for user_id
}
}
if (missingOrMalformedCount > 0) {
anomaly_report = `Found column with potential issues: 'user_id'. ${missingOrMalformedCount} out of ${rowCount} rows have missing or potentially malformed 'user_id' values (e.g., empty or < 3 chars).`;
} else {
anomaly_report = "Column 'user_id' checked, no obvious missing or malformed values in initial scan.";
}
} else if (userIdHeaderIndex === -1 && rowCount > 0) {
anomaly_report = "Column 'user_id' not found in CSV headers.";
} else if (rowCount === 0) {
anomaly_report = "CSV has headers but no data rows to analyze.";
}


return `CSV file '${fileName}' processed.
Headers: ${headers.join(", ")}
Row Count (excluding header): ${rowCount}
Analysis: ${anomaly_report}`;
} catch (error) {
// Narrow down error type if possible (e.g. NodeJS.ErrnoException)
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') {
return `Error processing CSV file '${fileName}': File not found at path '${filePath}'.`;
}
return `Error processing CSV file '${fileName}': ${nodeError.message}`;
}
}
}
18 changes: 18 additions & 0 deletions src/core/prompts/tools/analyze-multimodal-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ToolArgs } from "./types";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function getAnalyzeMultimodalDataDescription(args: ToolArgs): string {
return `
<tool_description>
<tool_name>analyze_multimodal_data</tool_name>
<description>Analyzes content from a list of specified files, supporting various modalities. It can process audio files (wav, mp3) for transcription, CSV files for data analysis, JSON files for validation and snippet extraction, and other files as plain text. The tool returns a consolidated report of its findings for all processed files.</description>
<parameters>
<parameter>
<name>file_paths</name>
<type>string</type>
<description>A newline-separated list of relative file paths to analyze (e.g., 'data/report.wav\ndata/stats.csv').</description>
</parameter>
</parameters>
</tool_description>
`.trim();
}
24 changes: 21 additions & 3 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import { getFetchInstructionsDescription } from "./fetch-instructions"
import { getWriteToFileDescription } from "./write-to-file"
import { getSearchFilesDescription } from "./search-files"
import { getListFilesDescription } from "./list-files"
import { getInsertContentDescription } from "./insert-content"
// Removed: import { getInsertContentDescription } from "./insert-content"
import { getSearchAndReplaceDescription } from "./search-and-replace"
import { getListCodeDefinitionNamesDescription } from "./list-code-definition-names"
// Removed: import { getDeleteLineDescription } from "./delete-line" // Added
// Removed: import { getReplaceLineDescription } from "./replace-line" // Added
import { getUndoEditDescription } from "./undo-edit" // Added
import { getReplaceTextRangeDescription } from "./replace-text-range"; // Added
import { getAnalyzeMultimodalDataDescription } from "./analyze-multimodal-data"; // Added
import { getSynthesizeAndPlanDescription } from "./synthesize-and-plan"; // Added
import { getBrowserActionDescription } from "./browser-action"
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
import { getAttemptCompletionDescription } from "./attempt-completion"
Expand Down Expand Up @@ -41,8 +47,14 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
codebase_search: () => getCodebaseSearchDescription(),
switch_mode: () => getSwitchModeDescription(),
new_task: (args) => getNewTaskDescription(args),
insert_content: (args) => getInsertContentDescription(args),
// Removed: insert_content: (args) => getInsertContentDescription(args),
search_and_replace: (args) => getSearchAndReplaceDescription(args),
// Removed: delete_line: (args) => getDeleteLineDescription(args), // Added
// Removed: replace_line: (args) => getReplaceLineDescription(args), // Added
undo_edit: (args) => getUndoEditDescription(args), // Added
replace_text_range: (args) => getReplaceTextRangeDescription(args), // Added
analyze_multimodal_data: (args) => getAnalyzeMultimodalDataDescription(args), // Added
synthesize_and_plan: (args) => getSynthesizeAndPlanDescription(args), // Added
apply_diff: (args) =>
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
}
Expand Down Expand Up @@ -137,7 +149,13 @@ export {
getUseMcpToolDescription,
getAccessMcpResourceDescription,
getSwitchModeDescription,
getInsertContentDescription,
// Removed: getInsertContentDescription,
getSearchAndReplaceDescription,
getCodebaseSearchDescription,
// Removed: getDeleteLineDescription, // Added
// Removed: getReplaceLineDescription, // Added
getUndoEditDescription, // Added
getReplaceTextRangeDescription, // Added
getAnalyzeMultimodalDataDescription, // Added
getSynthesizeAndPlanDescription, // Added
}
33 changes: 0 additions & 33 deletions src/core/prompts/tools/insert-content.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/core/prompts/tools/read-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ${args.partialReadsEnabled ? `By specifying line ranges, you can efficiently rea
Parameters:
- args: Contains one or more file elements, where each file contains:
- path: (required) File path (relative to workspace directory ${args.cwd})
${args.partialReadsEnabled ? `- line_range: (optional) One or more line range elements in format "start-end" (1-based, inclusive)` : ""}
${args.partialReadsEnabled ? `- line_range: (optional) One or more line range elements in format "START-END" (1-based, inclusive). END can be -1 to read until the end of the file.` : ""}

Usage:
<read_file>
Expand Down
Loading
Loading