feat(refactor): Finalize AI service migration and cleanup obsolete files

This commit completes the major refactoring initiative (Task 61) to migrate all AI-interacting task management functions to the unified service layer (`ai-services-unified.js`) and standardized configuration (`config-manager.js`).

Key Changes:

1.  **Refactor `update-task-by-id` & `update-subtask-by-id`:**

    *   Replaced direct AI client logic and config fetching with calls to `generateTextService`.

    *   Preserved original prompt logic while ensuring JSON output format is requested.

    *   Implemented robust manual JSON parsing and Zod validation for text-based AI responses.

    *   Corrected logger implementation (`logFn`/`isMCP`/`report` pattern) for both CLI and MCP contexts.

    *   Ensured correct passing of `session` context to the unified service.

    *   Refactored associated direct function wrappers (`updateTaskByIdDirect`, `updateSubtaskByIdDirect`) to remove AI client initialization and call core logic appropriately.

2.  **CLI Environment Loading:**

    *   Added `dotenv.config()` to `scripts/dev.js` to ensure consistent loading of the `.env` file for CLI operations.

3.  **Obsolete Code Removal:**

    *   Deleted unused helper files:

        *   `scripts/modules/task-manager/get-subtasks-from-ai.js`

        *   `scripts/modules/task-manager/generate-subtask-prompt.js`

        *   `scripts/modules/ai-services.js`

        *   `scripts/modules/ai-client-factory.js`

        *   `mcp-server/src/core/utils/ai-client-utils.js`

    *   Removed corresponding imports/exports from `scripts/modules/task-manager.js` and `mcp-server/src/core/task-master-core.js`.

4.  **Verification:**

    *   Successfully tested `update-task` and `update-subtask` via both CLI and MCP after refactoring.

5.  **Task Management:**

    *   Marked subtasks 61.38, 61.39, 61.40, 61.41, and 61.33 as 'done'.

    *   Includes other task content/status updates as reflected in the diff.

This completes the migration of core AI features to the new architecture, enhancing maintainability and flexibility.
This commit is contained in:
Eyal Toledano
2025-04-25 13:24:15 -04:00
parent 3721359782
commit afb47584bd
17 changed files with 496 additions and 6375 deletions

View File

@@ -11,8 +11,6 @@ import {
disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js';
// Removed AI client imports:
// import { getAnthropicClientForMCP, getModelConfig } from '../utils/ai-client-utils.js';
import path from 'path';
import fs from 'fs';

View File

@@ -8,10 +8,6 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getPerplexityClientForMCP
} from '../utils/ai-client-utils.js';
/**
* Direct function wrapper for updateSubtaskById with error handling.
@@ -95,27 +91,6 @@ export async function updateSubtaskByIdDirect(args, log, context = {}) {
`Updating subtask with ID ${subtaskIdStr} with prompt "${prompt}" and research: ${useResearch}`
);
// Initialize the appropriate AI client based on research flag
try {
if (useResearch) {
// Initialize Perplexity client
await getPerplexityClientForMCP(session);
} else {
// Initialize Anthropic client
await getAnthropicClientForMCP(session);
}
} catch (error) {
log.error(`AI client initialization error: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: error.message || 'Failed to initialize AI client'
},
fromCache: false
};
}
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();

View File

@@ -8,10 +8,6 @@ import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getPerplexityClientForMCP
} from '../utils/ai-client-utils.js';
/**
* Direct function wrapper for updateTaskById with error handling.
@@ -92,28 +88,6 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
// Get research flag
const useResearch = research === true;
// Initialize appropriate AI client based on research flag
let aiClient;
try {
if (useResearch) {
log.info('Using Perplexity AI for research-backed task update');
aiClient = await getPerplexityClientForMCP(session, log);
} else {
log.info('Using Claude AI for task update');
aiClient = getAnthropicClientForMCP(session, log);
}
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
log.info(
`Updating task with ID ${taskId} with prompt "${prompt}" and research: ${useResearch}`
);

View File

@@ -34,15 +34,6 @@ import { modelsDirect } from './direct-functions/models.js';
// Re-export utility functions
export { findTasksJsonPath } from './utils/path-utils.js';
// Re-export AI client utilities
export {
getAnthropicClientForMCP,
getPerplexityClientForMCP,
getModelConfig,
getBestAvailableAIModel,
handleClaudeError
} from './utils/ai-client-utils.js';
// Use Map for potential future enhancements like introspection or dynamic dispatch
export const directFunctions = new Map([
['listTasksDirect', listTasksDirect],

View File

@@ -1,213 +0,0 @@
/**
* ai-client-utils.js
* Utility functions for initializing AI clients in MCP context
*/
import { Anthropic } from '@anthropic-ai/sdk';
import dotenv from 'dotenv';
// Load environment variables for CLI mode
dotenv.config();
// Default model configuration from CLI environment
const DEFAULT_MODEL_CONFIG = {
model: 'claude-3-7-sonnet-20250219',
maxTokens: 64000,
temperature: 0.2
};
/**
* Get an Anthropic client instance initialized with MCP session environment variables
* @param {Object} [session] - Session object from MCP containing environment variables
* @param {Object} [log] - Logger object to use (defaults to console)
* @returns {Anthropic} Anthropic client instance
* @throws {Error} If API key is missing
*/
export function getAnthropicClientForMCP(session, log = console) {
try {
// Extract API key from session.env or fall back to environment variables
const apiKey =
session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error(
'ANTHROPIC_API_KEY not found in session environment or process.env'
);
}
// Initialize and return a new Anthropic client
return new Anthropic({
apiKey,
defaultHeaders: {
'anthropic-beta': 'output-128k-2025-02-19' // Include header for increased token limit
}
});
} catch (error) {
log.error(`Failed to initialize Anthropic client: ${error.message}`);
throw error;
}
}
/**
* Get a Perplexity client instance initialized with MCP session environment variables
* @param {Object} [session] - Session object from MCP containing environment variables
* @param {Object} [log] - Logger object to use (defaults to console)
* @returns {OpenAI} OpenAI client configured for Perplexity API
* @throws {Error} If API key is missing or OpenAI package can't be imported
*/
export async function getPerplexityClientForMCP(session, log = console) {
try {
// Extract API key from session.env or fall back to environment variables
const apiKey =
session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY;
if (!apiKey) {
throw new Error(
'PERPLEXITY_API_KEY not found in session environment or process.env'
);
}
// Dynamically import OpenAI (it may not be used in all contexts)
const { default: OpenAI } = await import('openai');
// Initialize and return a new OpenAI client configured for Perplexity
return new OpenAI({
apiKey,
baseURL: 'https://api.perplexity.ai'
});
} catch (error) {
log.error(`Failed to initialize Perplexity client: ${error.message}`);
throw error;
}
}
/**
* Get model configuration from session environment or fall back to defaults
* @param {Object} [session] - Session object from MCP containing environment variables
* @param {Object} [defaults] - Default model configuration to use if not in session
* @returns {Object} Model configuration with model, maxTokens, and temperature
*/
export function getModelConfig(session, defaults = DEFAULT_MODEL_CONFIG) {
// Get values from session or fall back to defaults
return {
model: session?.env?.MODEL || defaults.model,
maxTokens: parseInt(session?.env?.MAX_TOKENS || defaults.maxTokens),
temperature: parseFloat(session?.env?.TEMPERATURE || defaults.temperature)
};
}
/**
* Returns the best available AI model based on specified options
* @param {Object} session - Session object from MCP containing environment variables
* @param {Object} options - Options for model selection
* @param {boolean} [options.requiresResearch=false] - Whether the operation requires research capabilities
* @param {boolean} [options.claudeOverloaded=false] - Whether Claude is currently overloaded
* @param {Object} [log] - Logger object to use (defaults to console)
* @returns {Promise<Object>} Selected model info with type and client
* @throws {Error} If no AI models are available
*/
export async function getBestAvailableAIModel(
session,
options = {},
log = console
) {
const { requiresResearch = false, claudeOverloaded = false } = options;
// Test case: When research is needed but no Perplexity, use Claude
if (
requiresResearch &&
!(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY) &&
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
) {
try {
log.warn('Perplexity not available for research, using Claude');
const client = getAnthropicClientForMCP(session, log);
return { type: 'claude', client };
} catch (error) {
log.error(`Claude not available: ${error.message}`);
throw new Error('No AI models available for research');
}
}
// Regular path: Perplexity for research when available
if (
requiresResearch &&
(session?.env?.PERPLEXITY_API_KEY || process.env.PERPLEXITY_API_KEY)
) {
try {
const client = await getPerplexityClientForMCP(session, log);
return { type: 'perplexity', client };
} catch (error) {
log.warn(`Perplexity not available: ${error.message}`);
// Fall through to Claude as backup
}
}
// Test case: Claude for overloaded scenario
if (
claudeOverloaded &&
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
) {
try {
log.warn(
'Claude is overloaded but no alternatives are available. Proceeding with Claude anyway.'
);
const client = getAnthropicClientForMCP(session, log);
return { type: 'claude', client };
} catch (error) {
log.error(
`Claude not available despite being overloaded: ${error.message}`
);
throw new Error('No AI models available');
}
}
// Default case: Use Claude when available and not overloaded
if (
!claudeOverloaded &&
(session?.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
) {
try {
const client = getAnthropicClientForMCP(session, log);
return { type: 'claude', client };
} catch (error) {
log.warn(`Claude not available: ${error.message}`);
// Fall through to error if no other options
}
}
// If we got here, no models were successfully initialized
throw new Error('No AI models available. Please check your API keys.');
}
/**
* Handle Claude API errors with user-friendly messages
* @param {Error} error - The error from Claude API
* @returns {string} User-friendly error message
*/
export function handleClaudeError(error) {
// Check if it's a structured error response
if (error.type === 'error' && error.error) {
switch (error.error.type) {
case 'overloaded_error':
return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.';
case 'rate_limit_error':
return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.';
case 'invalid_request_error':
return 'There was an issue with the request format. If this persists, please report it as a bug.';
default:
return `Claude API error: ${error.error.message}`;
}
}
// Check for network/timeout errors
if (error.message?.toLowerCase().includes('timeout')) {
return 'The request to Claude timed out. Please try again.';
}
if (error.message?.toLowerCase().includes('network')) {
return 'There was a network error connecting to Claude. Please check your internet connection and try again.';
}
// Default error message
return `Error communicating with Claude: ${error.message}`;
}