refactor(ai): Implement unified AI service layer and fix subtask update
- Unified Service: Introduced 'scripts/modules/ai-services-unified.js' to centralize AI interactions using provider modules ('src/ai-providers/') and the Vercel AI SDK.
- Provider Modules: Implemented 'anthropic.js' and 'perplexity.js' wrappers for Vercel SDK.
- 'updateSubtaskById' Fix: Refactored the AI call within 'updateSubtaskById' to use 'generateTextService' from the unified layer, resolving runtime errors related to parameter passing and streaming. This serves as the pattern for refactoring other AI calls in 'scripts/modules/task-manager/'.
- Task Status: Marked Subtask 61.19 as 'done'.
- Rules: Added new 'ai-services.mdc' rule.
This centralizes AI logic, replacing previous direct SDK calls and custom implementations. API keys are resolved via 'resolveEnvVariable' within the service layer. The refactoring of 'updateSubtaskById' establishes the standard approach for migrating other AI-dependent functions in the task manager module to use the unified service.
Relates to Task 61.
This commit is contained in:
5
.changeset/fancy-cities-march.md
Normal file
5
.changeset/fancy-cities-march.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'task-master-ai': minor
|
||||
---
|
||||
|
||||
Refactor AI service interaction to use unified layer and Vercel SDK
|
||||
118
.cursor/rules/ai_services.mdc
Normal file
118
.cursor/rules/ai_services.mdc
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
description: Guidelines for interacting with the unified AI service layer.
|
||||
globs: scripts/modules/ai-services-unified.js, scripts/modules/task-manager/*.js, scripts/modules/commands.js
|
||||
---
|
||||
|
||||
# AI Services Layer Guidelines
|
||||
|
||||
This document outlines the architecture and usage patterns for interacting with Large Language Models (LLMs) via the Task Master's unified AI service layer. The goal is to centralize configuration, provider selection, API key management, fallback logic, and error handling.
|
||||
|
||||
**Core Components:**
|
||||
|
||||
* **Configuration (`.taskmasterconfig` & [`config-manager.js`](mdc:scripts/modules/config-manager.js)):**
|
||||
* Defines the AI provider and model ID for different roles (`main`, `research`, `fallback`).
|
||||
* Stores parameters like `maxTokens` and `temperature` per role.
|
||||
* Managed via `task-master models --setup`.
|
||||
* [`config-manager.js`](mdc:scripts/modules/config-manager.js) provides getters (e.g., `getMainProvider()`, `getMainModelId()`, `getParametersForRole()`) to access these settings.
|
||||
* API keys are **NOT** stored here; they are resolved via `resolveEnvVariable` from `.env` or MCP session env. See [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc).
|
||||
* Relies on `data/supported-models.json` for model validation and metadata.
|
||||
|
||||
* **Unified Service (`ai-services-unified.js`):**
|
||||
* Exports primary interaction functions: `generateTextService`, `streamTextService`, `generateObjectService`.
|
||||
* Contains the core `_unifiedServiceRunner` logic.
|
||||
* Uses `config-manager.js` getters to determine the provider/model based on the requested `role`.
|
||||
* Implements the fallback sequence (main -> fallback -> research or variations).
|
||||
* Constructs the `messages` array (`[{ role: 'system', ... }, { role: 'user', ... }]`) required by the Vercel AI SDK.
|
||||
* Calls internal retry logic (`_attemptProviderCallWithRetries`).
|
||||
* Resolves API keys via `_resolveApiKey`.
|
||||
* Maps requests to the correct provider implementation via `PROVIDER_FUNCTIONS`.
|
||||
|
||||
* **Provider Implementations (`src/ai-providers/*.js`):**
|
||||
* Contain provider-specific code (e.g., `src/ai-providers/anthropic.js`).
|
||||
* Import Vercel AI SDK provider adapters (`@ai-sdk/anthropic`, `@ai-sdk/perplexity`, etc.).
|
||||
* Wrap core Vercel AI SDK functions (`generateText`, `streamText`, `generateObject`).
|
||||
* Accept standard parameters (`apiKey`, `modelId`, `messages`, `maxTokens`, etc.).
|
||||
* Return results in the format expected by `_unifiedServiceRunner`.
|
||||
|
||||
**Usage Pattern (from Core Logic like `task-manager`):**
|
||||
|
||||
1. **Choose Service:** Decide whether you need a full text response (`generateTextService`) or a stream (`streamTextService`).
|
||||
* ✅ **DO**: **Prefer `generateTextService`** for interactions that send large context payloads (e.g., stringified JSON) and **do not** require incremental display in the UI. This is currently more reliable, especially if Anthropic is the configured provider.
|
||||
* ⚠️ **CAUTION**: `streamTextService` may be unreliable with the Vercel SDK's Anthropic adapter when sending large user messages. Use with caution or stick to `generateTextService` for such cases until SDK improvements are confirmed.
|
||||
|
||||
2. **Import Service:** Import the chosen service function from `../ai-services-unified.js`.
|
||||
```javascript
|
||||
// Preferred for updateSubtaskById, parsePRD, etc.
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
|
||||
// Use only if incremental display is implemented AND provider streaming is reliable
|
||||
// import { streamTextService } from '../ai-services-unified.js';
|
||||
```
|
||||
|
||||
3. **Prepare Parameters:** Construct the parameters object.
|
||||
* `role`: `'main'`, `'research'`, or `'fallback'`. Determines the initial provider/model attempt.
|
||||
* `session`: Pass the MCP `session` object if available (for API key resolution), otherwise `null` or omit.
|
||||
* `systemPrompt`: Your system instruction string.
|
||||
* `prompt`: The user message string (can be long, include stringified data, etc.).
|
||||
* (For `generateObjectService`): `schema`, `objectName`.
|
||||
|
||||
4. **Call Service:** Use `await` to call the service function.
|
||||
```javascript
|
||||
// Example using generateTextService
|
||||
try {
|
||||
const resultText = await generateTextService({
|
||||
role: 'main', // Or 'research'/'fallback'
|
||||
session: session, // Or null
|
||||
systemPrompt: "You are...",
|
||||
prompt: userMessageContent // Can include stringified JSON etc.
|
||||
});
|
||||
additionalInformation = resultText.trim();
|
||||
// ... process resultText ...
|
||||
} catch (error) {
|
||||
// Handle errors thrown if all providers/retries fail
|
||||
report(`AI service call failed: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Example using streamTextService (Use with caution for Anthropic/large payloads)
|
||||
try {
|
||||
const streamResult = await streamTextService({
|
||||
role: 'main',
|
||||
session: session,
|
||||
systemPrompt: "You are...",
|
||||
prompt: userMessageContent
|
||||
});
|
||||
|
||||
// Check if a stream was actually returned (might be null if overridden)
|
||||
if (streamResult.textStream) {
|
||||
for await (const chunk of streamResult.textStream) {
|
||||
additionalInformation += chunk;
|
||||
}
|
||||
additionalInformation = additionalInformation.trim();
|
||||
} else if (streamResult.text) {
|
||||
// Handle case where generateText was used internally (Anthropic override)
|
||||
// NOTE: This override logic is currently REMOVED as we prefer generateTextService directly
|
||||
additionalInformation = streamResult.text.trim();
|
||||
} else {
|
||||
additionalInformation = ''; // Should not happen
|
||||
}
|
||||
// ... process additionalInformation ...
|
||||
} catch (error) {
|
||||
report(`AI service call failed: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
5. **Handle Results/Errors:** Process the returned text/stream/object or handle errors thrown by the service layer.
|
||||
|
||||
**Key Implementation Rules & Gotchas:**
|
||||
|
||||
* ✅ **DO**: Centralize all AI calls through `generateTextService` / `streamTextService`.
|
||||
* ✅ **DO**: Ensure `.taskmasterconfig` has valid provider names, model IDs, and parameters (`maxTokens` appropriate for the model).
|
||||
* ✅ **DO**: Ensure API keys are correctly configured in `.env` / `.cursor/mcp.json`.
|
||||
* ✅ **DO**: Pass the `session` object to the service call if available (for MCP calls).
|
||||
* ❌ **DON'T**: Call Vercel AI SDK functions (`streamText`, `generateText`) directly from `task-manager` or commands.
|
||||
* ❌ **DON'T**: Implement fallback or retry logic outside `ai-services-unified.js`.
|
||||
* ❌ **DON'T**: Handle API key resolution outside the service layer.
|
||||
* ⚠️ **Streaming Caution**: Be aware of potential reliability issues using `streamTextService` with Anthropic/large payloads via the SDK. Prefer `generateTextService` for these cases until proven otherwise.
|
||||
* ⚠️ **Debugging Imports**: If you get `"X is not defined"` errors related to service functions, check for internal errors within `ai-services-unified.js` (like incorrect import paths or syntax errors).
|
||||
@@ -71,18 +71,40 @@ alwaysApply: false
|
||||
- `getStatusWithColor(status)`: Returns status string with color formatting.
|
||||
- `formatDependenciesWithStatus(dependencies, allTasks, inTable)`: Formats dependency list with status indicators.
|
||||
|
||||
- **[`ai-services.js`](mdc:scripts/modules/ai-services.js) (Conceptual): AI Integration**
|
||||
- **Purpose**: Abstracts interactions with AI models (like Anthropic Claude and Perplexity AI) for various features. *Note: This module might be implicitly implemented within `task-manager.js` and `utils.js` or could be explicitly created for better organization as the project evolves.*
|
||||
- **Responsibilities**:
|
||||
- Handles API calls to AI services.
|
||||
- Manages prompts and parameters for AI requests.
|
||||
- Parses AI responses and extracts relevant information.
|
||||
- Implements logic for task complexity analysis, task expansion, and PRD parsing using AI.
|
||||
- **Potential Functions**:
|
||||
- `getAIResponse(prompt, model, maxTokens, temperature)`: Generic function to interact with AI model.
|
||||
- `analyzeTaskComplexityWithAI(taskDescription)`: Sends task description to AI for complexity analysis.
|
||||
- `expandTaskWithAI(taskDescription, numSubtasks, researchContext)`: Generates subtasks using AI.
|
||||
- `parsePRDWithAI(prdContent)`: Extracts tasks from PRD content using AI.
|
||||
- **[`ai-services-unified.js`](mdc:scripts/modules/ai-services-unified.js): Unified AI Service Layer**
|
||||
- **Purpose**: Provides a centralized interface for interacting with various Large Language Models (LLMs) using the Vercel AI SDK.
|
||||
- **Responsibilities** (See also: [`ai_services.mdc`](mdc:.cursor/rules/ai_services.mdc)):
|
||||
- Exports primary functions (`generateTextService`, `streamTextService`, `generateObjectService`) for core modules to use.
|
||||
- Implements provider selection logic based on configuration roles (`main`, `research`, `fallback`) retrieved from [`config-manager.js`](mdc:scripts/modules/config-manager.js).
|
||||
- Manages API key resolution (via [`utils.js`](mdc:scripts/modules/utils.js)) from environment or MCP session.
|
||||
- Handles fallback sequences between configured providers.
|
||||
- Implements retry logic for specific API errors.
|
||||
- Constructs the `messages` array format required by the Vercel AI SDK.
|
||||
- Delegates actual API calls to provider-specific implementation modules.
|
||||
- **Key Components**:
|
||||
- `_unifiedServiceRunner`: Core logic for provider selection, fallback, and retries.
|
||||
- `PROVIDER_FUNCTIONS`: Map linking provider names to their implementation functions.
|
||||
- `generateTextService`, `streamTextService`, `generateObjectService`: Exported functions.
|
||||
|
||||
- **[`src/ai-providers/*.js`](mdc:src/ai-providers/): Provider-Specific Implementations**
|
||||
- **Purpose**: Contains the wrapper code for interacting with specific LLM providers via the Vercel AI SDK.
|
||||
- **Responsibilities** (See also: [`ai_services.mdc`](mdc:.cursor/rules/ai_services.mdc)):
|
||||
- Imports Vercel AI SDK provider adapters (e.g., `@ai-sdk/anthropic`).
|
||||
- Implements standardized functions (e.g., `generateAnthropicText`, `streamAnthropicText`) that wrap the core Vercel AI SDK functions (`generateText`, `streamText`).
|
||||
- Accepts standardized parameters (`apiKey`, `modelId`, `messages`, etc.) from `ai-services-unified.js`.
|
||||
- Returns results in the format expected by `ai-services-unified.js`.
|
||||
|
||||
- **[`config-manager.js`](mdc:scripts/modules/config-manager.js): Configuration Management**
|
||||
- **Purpose**: Manages loading, validation, and access to configuration settings, primarily from `.taskmasterconfig`.
|
||||
- **Responsibilities** (See also: [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc)):
|
||||
- Reads and parses the `.taskmasterconfig` file.
|
||||
- Merges file configuration with default values.
|
||||
- Provides getters for accessing specific configuration values (e.g., `getMainProvider()`, `getMainModelId()`, `getParametersForRole()`, `getLogLevel()`).
|
||||
- **Note**: Does *not* handle API key storage (keys are in `.env` or MCP `session.env`).
|
||||
- **Key Components**:
|
||||
- `getConfig()`: Loads and returns the merged configuration object.
|
||||
- Role-specific getters (e.g., `getMainProvider`, `getMainModelId`, `getMainMaxTokens`).
|
||||
- Global setting getters (e.g., `getLogLevel`, `getDebugFlag`).
|
||||
|
||||
- **[`utils.js`](mdc:scripts/modules/utils.js): Core Utility Functions**
|
||||
- **Purpose**: Provides low-level, reusable utility functions used across the **CLI application**. **Note:** Configuration management is now handled by [`config-manager.js`](mdc:scripts/modules/config-manager.js).
|
||||
@@ -153,10 +175,12 @@ alwaysApply: false
|
||||
|
||||
- **Commands Initiate Actions**: User commands entered via the CLI (parsed by `commander` based on definitions in [`commands.js`](mdc:scripts/modules/commands.js)) are the entry points for most operations.
|
||||
- **Command Handlers Delegate to Core Logic**: Action handlers within [`commands.js`](mdc:scripts/modules/commands.js) call functions in core modules like [`task-manager.js`](mdc:scripts/modules/task-manager.js), [`dependency-manager.js`](mdc:scripts/modules/dependency-manager.js), and [`init.js`](mdc:scripts/init.js) (for the `init` command) to perform the actual work.
|
||||
- **UI for Presentation**: [`ui.js`](mdc:scripts/modules/ui.js) is used by command handlers and task/dependency managers to display information to the user. UI functions primarily consume data and format it for output, without modifying core application state.
|
||||
- **Utilities for Common Tasks**: [`utils.js`](mdc:scripts/modules/utils.js) provides helper functions used by all other modules for configuration, logging, file operations, and common data manipulations.
|
||||
- **AI Services Integration**: AI functionalities (complexity analysis, task expansion, PRD parsing) are invoked from [`task-manager.js`](mdc:scripts/modules/task-manager.js) and potentially [`commands.js`](mdc:scripts/modules/commands.js), likely using functions that would reside in a dedicated `ai-services.js` module or be integrated within `utils.js` or `task-manager.js`.
|
||||
- **MCP Server Interaction**: External tools interact with the `mcp-server`. MCP Tool `execute` methods use `getProjectRootFromSession` to find the project root, then call direct function wrappers (in `mcp-server/src/core/direct-functions/`) passing the root in `args`. These wrappers handle path finding for `tasks.json` (using `path-utils.js`), validation, caching, call the core logic from `scripts/modules/` (passing logging context via the standard wrapper pattern detailed in mcp.mdc), and return a standardized result. The final MCP response is formatted by `mcp-server/src/tools/utils.js`. See [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details.
|
||||
- **Core Logic Calls AI Service Layer**: Core modules requiring AI functionality (like [`task-manager.js`](mdc:scripts/modules/task-manager.js)) **import and call functions from the unified AI service layer (`ai-services-unified.js`)**, such as `generateTextService`.
|
||||
- **AI Service Layer Orchestrates**: [`ai-services-unified.js`](mdc:scripts/modules/ai-services-unified.js) uses [`config-manager.js`](mdc:scripts/modules/config-manager.js) to get settings, selects the appropriate provider function from [`src/ai-providers/*.js`](mdc:src/ai-providers/), resolves API keys (using `resolveEnvVariable` from [`utils.js`](mdc:scripts/modules/utils.js)), and handles fallbacks/retries.
|
||||
- **Provider Implementation Executes**: The selected function in [`src/ai-providers/*.js`](mdc:src/ai-providers/) interacts with the Vercel AI SDK core functions (`generateText`, `streamText`) using the Vercel provider adapters.
|
||||
- **UI for Presentation**: [`ui.js`](mdc:scripts/modules/ui.js) is used by command handlers and core modules to display information to the user. UI functions primarily consume data and format it for output.
|
||||
- **Utilities for Common Tasks**: [`utils.js`](mdc:scripts/modules/utils.js) provides helper functions (logging, file I/O, string manipulation, API key resolution) used by various modules.
|
||||
- **MCP Server Interaction**: External tools interact with the `mcp-server`. MCP Tool `execute` methods call direct function wrappers (`*Direct` functions) which then call the core logic from `scripts/modules/`. If AI is needed, the core logic calls the unified AI service layer as described above. See [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details.
|
||||
|
||||
## Silent Mode Implementation Pattern in MCP Direct Functions
|
||||
|
||||
@@ -349,6 +373,11 @@ The `initialize_project` command provides a way to set up a new Task Master proj
|
||||
- **CLI Command**: `task-master init`
|
||||
- **MCP Tool**: `initialize_project`
|
||||
- **Functionality**:
|
||||
- Creates necessary directories and files for a new project
|
||||
- Sets up `tasks.json` and initial task files
|
||||
- Configures project metadata (name, description, version)
|
||||
- Handles shell alias creation if requested
|
||||
- Works in both interactive and non-interactive modes
|
||||
- Creates necessary directories and files for a new project
|
||||
- Sets up `tasks.json` and initial task files
|
||||
- Configures project metadata (name, description, version)
|
||||
|
||||
@@ -25,11 +25,17 @@ alwaysApply: false
|
||||
The standard pattern for adding a feature follows this workflow:
|
||||
|
||||
1. **Core Logic**: Implement the business logic in the appropriate module (e.g., [`task-manager.js`](mdc:scripts/modules/task-manager.js)).
|
||||
2. **UI Components**: Add any display functions to [`ui.js`](mdc:scripts/modules/ui.js) following [`ui.mdc`](mdc:.cursor/rules/ui.mdc).
|
||||
3. **Command Integration**: Add the CLI command to [`commands.js`](mdc:scripts/modules/commands.js) following [`commands.mdc`](mdc:.cursor/rules/commands.mdc).
|
||||
4. **Testing**: Write tests for all components of the feature (following [`tests.mdc`](mdc:.cursor/rules/tests.mdc))
|
||||
5. **Configuration**: Update configuration settings or add new ones in [`config-manager.js`](mdc:scripts/modules/config-manager.js) and ensure getters/setters are appropriate. Update documentation in [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc) and [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc). Update the `.taskmasterconfig` structure if needed.
|
||||
6. **Documentation**: Update help text and documentation in [dev_workflow.mdc](mdc:scripts/modules/dev_workflow.mdc)
|
||||
2. **AI Integration (If Applicable)**:
|
||||
- Import necessary service functions (e.g., `generateTextService`, `streamTextService`) from [`ai-services-unified.js`](mdc:scripts/modules/ai-services-unified.js).
|
||||
- Prepare parameters (`role`, `session`, `systemPrompt`, `prompt`).
|
||||
- Call the service function.
|
||||
- Handle the response (direct text or stream object).
|
||||
- **Important**: Prefer `generateTextService` for calls sending large context (like stringified JSON) where incremental display is not needed. See [`ai_services.mdc`](mdc:.cursor/rules/ai_services.mdc) for detailed usage patterns and cautions.
|
||||
3. **UI Components**: Add any display functions to [`ui.js`](mdc:scripts/modules/ui.js) following [`ui.mdc`](mdc:.cursor/rules/ui.mdc).
|
||||
4. **Command Integration**: Add the CLI command to [`commands.js`](mdc:scripts/modules/commands.js) following [`commands.mdc`](mdc:.cursor/rules/commands.mdc).
|
||||
5. **Testing**: Write tests for all components of the feature (following [`tests.mdc`](mdc:.cursor/rules/tests.mdc))
|
||||
6. **Configuration**: Update configuration settings or add new ones in [`config-manager.js`](mdc:scripts/modules/config-manager.js) and ensure getters/setters are appropriate. Update documentation in [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc) and [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc). Update the `.taskmasterconfig` structure if needed.
|
||||
7. **Documentation**: Update help text and documentation in [`dev_workflow.mdc`](mdc:.cursor/rules/dev_workflow.mdc) and [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc).
|
||||
|
||||
## Critical Checklist for New Features
|
||||
|
||||
@@ -211,7 +217,29 @@ export {
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 2. UI COMPONENTS: Add display function to ui.js
|
||||
// 2. AI Integration: Add import and use necessary service functions
|
||||
import { generateTextService } from './ai-services-unified.js';
|
||||
|
||||
// Example usage:
|
||||
async function handleAIInteraction() {
|
||||
const role = 'user';
|
||||
const session = 'exampleSession';
|
||||
const systemPrompt = 'You are a helpful assistant.';
|
||||
const prompt = 'What is the capital of France?';
|
||||
|
||||
const result = await generateTextService(role, session, systemPrompt, prompt);
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
// Export from the module
|
||||
export {
|
||||
// ... existing exports ...
|
||||
handleAIInteraction,
|
||||
};
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 3. UI COMPONENTS: Add display function to ui.js
|
||||
/**
|
||||
* Display archive operation results
|
||||
* @param {string} archivePath - Path to the archive file
|
||||
@@ -232,7 +260,7 @@ export {
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 3. COMMAND INTEGRATION: Add to commands.js
|
||||
// 4. COMMAND INTEGRATION: Add to commands.js
|
||||
import { archiveTasks } from './task-manager.js';
|
||||
import { displayArchiveResults } from './ui.js';
|
||||
|
||||
@@ -452,7 +480,7 @@ npm test
|
||||
For each new feature:
|
||||
|
||||
1. Add help text to the command definition
|
||||
2. Update [`dev_workflow.mdc`](mdc:scripts/modules/dev_workflow.mdc) with command reference
|
||||
2. Update [`dev_workflow.mdc`](mdc:.cursor/rules/dev_workflow.mdc) with command reference
|
||||
3. Consider updating [`architecture.mdc`](mdc:.cursor/rules/architecture.mdc) if the feature significantly changes module responsibilities.
|
||||
|
||||
Follow the existing command reference format:
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
/**
|
||||
* ai-services-unified.js
|
||||
* Centralized AI service layer using ai-client-factory and AI SDK core functions.
|
||||
* Centralized AI service layer using provider modules and config-manager.
|
||||
*/
|
||||
|
||||
import { generateText } from 'ai';
|
||||
import { getClient } from './ai-client-factory.js';
|
||||
import { log } from './utils.js'; // Import log for retry logging
|
||||
// Import logger from utils later when needed
|
||||
// import { log } from './utils.js';
|
||||
// Vercel AI SDK functions are NOT called directly anymore.
|
||||
// import { generateText, streamText, generateObject } from 'ai';
|
||||
|
||||
// --- Core Dependencies ---
|
||||
import {
|
||||
// REMOVED: getProviderAndModelForRole, // This was incorrect
|
||||
getMainProvider, // ADD individual getters
|
||||
getMainModelId,
|
||||
getResearchProvider,
|
||||
getResearchModelId,
|
||||
getFallbackProvider,
|
||||
getFallbackModelId,
|
||||
getParametersForRole
|
||||
// ConfigurationError // Import if needed for specific handling
|
||||
} from './config-manager.js'; // Corrected: Removed getProviderAndModelForRole
|
||||
import { log, resolveEnvVariable } from './utils.js';
|
||||
|
||||
// --- Provider Service Imports ---
|
||||
// Corrected path from scripts/ai-providers/... to ../../src/ai-providers/...
|
||||
import * as anthropic from '../../src/ai-providers/anthropic.js';
|
||||
import * as perplexity from '../../src/ai-providers/perplexity.js';
|
||||
// TODO: Import other provider modules when implemented (openai, ollama, etc.)
|
||||
|
||||
// --- Provider Function Map ---
|
||||
// Maps provider names (lowercase) to their respective service functions
|
||||
const PROVIDER_FUNCTIONS = {
|
||||
anthropic: {
|
||||
generateText: anthropic.generateAnthropicText,
|
||||
streamText: anthropic.streamAnthropicText,
|
||||
generateObject: anthropic.generateAnthropicObject
|
||||
// streamObject: anthropic.streamAnthropicObject, // Add when implemented
|
||||
},
|
||||
perplexity: {
|
||||
generateText: perplexity.generatePerplexityText,
|
||||
streamText: perplexity.streamPerplexityText,
|
||||
generateObject: perplexity.generatePerplexityObject
|
||||
// streamObject: perplexity.streamPerplexityObject, // Add when implemented
|
||||
}
|
||||
// TODO: Add entries for openai, ollama, etc. when implemented
|
||||
};
|
||||
|
||||
// --- Configuration for Retries ---
|
||||
const MAX_RETRIES = 2; // Total attempts = 1 + MAX_RETRIES
|
||||
@@ -30,39 +65,86 @@ function isRetryableError(error) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to attempt an AI SDK API call with retries.
|
||||
* Internal helper to resolve the API key for a given provider.
|
||||
* @param {string} providerName - The name of the provider (lowercase).
|
||||
* @param {object|null} session - Optional MCP session object.
|
||||
* @returns {string|null} The API key or null if not found/needed.
|
||||
* @throws {Error} If a required API key is missing.
|
||||
*/
|
||||
function _resolveApiKey(providerName, session) {
|
||||
const keyMap = {
|
||||
openai: 'OPENAI_API_KEY',
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
google: 'GOOGLE_API_KEY',
|
||||
perplexity: 'PERPLEXITY_API_KEY',
|
||||
grok: 'GROK_API_KEY',
|
||||
mistral: 'MISTRAL_API_KEY',
|
||||
azure: 'AZURE_OPENAI_API_KEY',
|
||||
openrouter: 'OPENROUTER_API_KEY',
|
||||
xai: 'XAI_API_KEY'
|
||||
// ollama doesn't need an API key mapped here
|
||||
};
|
||||
|
||||
if (providerName === 'ollama') {
|
||||
return null; // Ollama typically doesn't require an API key for basic setup
|
||||
}
|
||||
|
||||
const envVarName = keyMap[providerName];
|
||||
if (!envVarName) {
|
||||
throw new Error(
|
||||
`Unknown provider '${providerName}' for API key resolution.`
|
||||
);
|
||||
}
|
||||
|
||||
const apiKey = resolveEnvVariable(envVarName, session);
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
`Required API key ${envVarName} for provider '${providerName}' is not set in environment or session.`
|
||||
);
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to attempt a provider-specific AI API call with retries.
|
||||
*
|
||||
* @param {object} client - The AI client instance.
|
||||
* @param {function} apiCallFn - The AI SDK function to call (e.g., generateText).
|
||||
* @param {object} apiParams - Parameters for the AI SDK function (excluding model).
|
||||
* @param {function} providerApiFn - The specific provider function to call (e.g., generateAnthropicText).
|
||||
* @param {object} callParams - Parameters object for the provider function.
|
||||
* @param {string} providerName - Name of the provider (for logging).
|
||||
* @param {string} modelId - Specific model ID (for logging).
|
||||
* @param {string} attemptRole - The role being attempted (for logging).
|
||||
* @returns {Promise<object>} The result from the successful API call.
|
||||
* @throws {Error} If the call fails after all retries.
|
||||
*/
|
||||
async function _attemptApiCallWithRetries(
|
||||
client,
|
||||
apiCallFn,
|
||||
apiParams,
|
||||
async function _attemptProviderCallWithRetries(
|
||||
providerApiFn,
|
||||
callParams,
|
||||
providerName,
|
||||
modelId,
|
||||
attemptRole
|
||||
) {
|
||||
let retries = 0;
|
||||
const fnName = providerApiFn.name; // Get function name for logging
|
||||
|
||||
while (retries <= MAX_RETRIES) {
|
||||
try {
|
||||
log(
|
||||
'info',
|
||||
`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${apiCallFn.name} for role ${attemptRole}`
|
||||
`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
|
||||
);
|
||||
// Call the provided AI SDK function (generateText, streamText, etc.)
|
||||
const result = await apiCallFn({ model: client, ...apiParams });
|
||||
|
||||
// Call the specific provider function directly
|
||||
const result = await providerApiFn(callParams);
|
||||
|
||||
log(
|
||||
'info',
|
||||
`${apiCallFn.name} succeeded for role ${attemptRole} on attempt ${retries + 1}`
|
||||
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
|
||||
);
|
||||
return result; // Success!
|
||||
} catch (error) {
|
||||
log(
|
||||
'warn',
|
||||
`Attempt ${retries + 1} failed for role ${attemptRole} (${apiCallFn.name}): ${error.message}`
|
||||
`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
|
||||
);
|
||||
|
||||
if (isRetryableError(error) && retries < MAX_RETRIES) {
|
||||
@@ -76,140 +158,35 @@ async function _attemptApiCallWithRetries(
|
||||
} else {
|
||||
log(
|
||||
'error',
|
||||
`Non-retryable error or max retries reached for role ${attemptRole} (${apiCallFn.name}).`
|
||||
`Non-retryable error or max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
|
||||
);
|
||||
throw error; // Final failure for this attempt chain
|
||||
}
|
||||
}
|
||||
}
|
||||
// Should theoretically not be reached due to throw in the else block, but needed for linting/type safety
|
||||
// Should not be reached due to throw in the else block
|
||||
throw new Error(
|
||||
`Exhausted all retries for role ${attemptRole} (${apiCallFn.name})`
|
||||
`Exhausted all retries for role ${attemptRole} (${fnName} / ${providerName})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified service function for generating text.
|
||||
* Handles client retrieval, retries, and fallback (main -> fallback -> research).
|
||||
* TODO: Add detailed logging.
|
||||
*
|
||||
* @param {object} params - Parameters for the service call.
|
||||
* @param {string} params.role - The initial client role ('main', 'research', 'fallback').
|
||||
* @param {object} [params.session=null] - Optional MCP session object.
|
||||
* @param {object} [params.overrideOptions={}] - Optional overrides for ai-client-factory { provider, modelId }.
|
||||
* @param {string} params.prompt - The prompt for the AI.
|
||||
* @param {number} [params.maxTokens] - Max tokens for the generation.
|
||||
* @param {number} [params.temperature] - Temperature setting.
|
||||
* // ... include other standard generateText options as needed ...
|
||||
* @returns {Promise<object>} The result from the AI SDK's generateText function.
|
||||
* Base logic for unified service functions.
|
||||
* @param {string} serviceType - Type of service ('generateText', 'streamText', 'generateObject').
|
||||
* @param {object} params - Original parameters passed to the service function.
|
||||
* @returns {Promise<any>} Result from the underlying provider call.
|
||||
*/
|
||||
async function generateTextService(params) {
|
||||
async function _unifiedServiceRunner(serviceType, params) {
|
||||
const {
|
||||
role: initialRole,
|
||||
session,
|
||||
overrideOptions,
|
||||
...generateTextParams
|
||||
systemPrompt,
|
||||
prompt,
|
||||
schema,
|
||||
objectName,
|
||||
...restApiParams
|
||||
} = params;
|
||||
log('info', 'generateTextService called', { role: initialRole });
|
||||
|
||||
// Determine the sequence explicitly based on the initial role
|
||||
let sequence;
|
||||
if (initialRole === 'main') {
|
||||
sequence = ['main', 'fallback', 'research'];
|
||||
} else if (initialRole === 'fallback') {
|
||||
sequence = ['fallback', 'research']; // Try fallback, then research
|
||||
} else if (initialRole === 'research') {
|
||||
sequence = ['research', 'fallback']; // Try research, then fallback
|
||||
} else {
|
||||
// Default sequence if initialRole is unknown or invalid
|
||||
log(
|
||||
'warn',
|
||||
`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
|
||||
);
|
||||
sequence = ['main', 'fallback', 'research'];
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
|
||||
// Iterate through the determined sequence
|
||||
for (const currentRole of sequence) {
|
||||
// Removed the complex conditional check, as the sequence is now pre-determined
|
||||
|
||||
log('info', `Attempting service call with role: ${currentRole}`);
|
||||
let client;
|
||||
try {
|
||||
client = await getClient(currentRole, session, overrideOptions);
|
||||
const clientInfo = {
|
||||
provider: client?.provider || 'unknown',
|
||||
model: client?.modelId || client?.model || 'unknown'
|
||||
};
|
||||
log('info', 'Retrieved AI client', clientInfo);
|
||||
|
||||
// Attempt the API call with retries using the helper
|
||||
const result = await _attemptApiCallWithRetries(
|
||||
client,
|
||||
generateText,
|
||||
generateTextParams,
|
||||
currentRole
|
||||
);
|
||||
log('info', `generateTextService succeeded using role: ${currentRole}`); // Add success log
|
||||
return result; // Success!
|
||||
} catch (error) {
|
||||
log(
|
||||
'error', // Log as error since this role attempt failed
|
||||
`Service call failed for role ${currentRole}: ${error.message}`
|
||||
);
|
||||
lastError = error; // Store the error to throw if all roles in sequence fail
|
||||
|
||||
// Log the reason for moving to the next role
|
||||
if (!client) {
|
||||
log(
|
||||
'warn',
|
||||
`Could not get client for role ${currentRole}, trying next role in sequence...`
|
||||
);
|
||||
} else {
|
||||
// Error happened during API call after client was retrieved
|
||||
log(
|
||||
'warn',
|
||||
`Retries exhausted or non-retryable error for role ${currentRole}, trying next role in sequence...`
|
||||
);
|
||||
}
|
||||
// Continue to the next role in the sequence automatically
|
||||
}
|
||||
}
|
||||
|
||||
// If loop completes, all roles in the sequence failed
|
||||
log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
|
||||
throw (
|
||||
lastError ||
|
||||
new Error(
|
||||
'AI service call failed for all configured roles in the sequence.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Implement streamTextService, generateObjectService etc.
|
||||
|
||||
/**
|
||||
* Unified service function for streaming text.
|
||||
* Handles client retrieval, retries, and fallback sequence.
|
||||
*
|
||||
* @param {object} params - Parameters for the service call.
|
||||
* @param {string} params.role - The initial client role ('main', 'research', 'fallback').
|
||||
* @param {object} [params.session=null] - Optional MCP session object.
|
||||
* @param {object} [params.overrideOptions={}] - Optional overrides for ai-client-factory.
|
||||
* @param {string} params.prompt - The prompt for the AI.
|
||||
* // ... include other standard streamText options as needed ...
|
||||
* @returns {Promise<object>} The result from the AI SDK's streamText function (typically a Streamable object).
|
||||
*/
|
||||
async function streamTextService(params) {
|
||||
const {
|
||||
role: initialRole,
|
||||
session,
|
||||
overrideOptions,
|
||||
...streamTextParams // Collect remaining params for streamText
|
||||
} = params;
|
||||
log('info', 'streamTextService called', { role: initialRole });
|
||||
log('info', `${serviceType}Service called`, { role: initialRole });
|
||||
|
||||
let sequence;
|
||||
if (initialRole === 'main') {
|
||||
@@ -229,54 +206,190 @@ async function streamTextService(params) {
|
||||
let lastError = null;
|
||||
|
||||
for (const currentRole of sequence) {
|
||||
log('info', `Attempting service call with role: ${currentRole}`);
|
||||
let client;
|
||||
let providerName, modelId, apiKey, roleParams, providerFnSet, providerApiFn;
|
||||
|
||||
try {
|
||||
client = await getClient(currentRole, session, overrideOptions);
|
||||
const clientInfo = {
|
||||
provider: client?.provider || 'unknown',
|
||||
model: client?.modelId || client?.model || 'unknown'
|
||||
};
|
||||
log('info', 'Retrieved AI client', clientInfo);
|
||||
log('info', `Attempting service call with role: ${currentRole}`);
|
||||
|
||||
const result = await _attemptApiCallWithRetries(
|
||||
client,
|
||||
streamText, // Pass streamText function
|
||||
streamTextParams,
|
||||
currentRole
|
||||
);
|
||||
log('info', `streamTextService succeeded using role: ${currentRole}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
log(
|
||||
'error',
|
||||
`Service call failed for role ${currentRole}: ${error.message}`
|
||||
);
|
||||
lastError = error;
|
||||
|
||||
if (!client) {
|
||||
log(
|
||||
'warn',
|
||||
`Could not get client for role ${currentRole}, trying next role in sequence...`
|
||||
);
|
||||
// --- Corrected Config Fetching ---
|
||||
// 1. Get Config: Provider, Model, Parameters for the current role
|
||||
// Call individual getters based on the current role
|
||||
if (currentRole === 'main') {
|
||||
providerName = getMainProvider(); // Use individual getter
|
||||
modelId = getMainModelId(); // Use individual getter
|
||||
} else if (currentRole === 'research') {
|
||||
providerName = getResearchProvider(); // Use individual getter
|
||||
modelId = getResearchModelId(); // Use individual getter
|
||||
} else if (currentRole === 'fallback') {
|
||||
providerName = getFallbackProvider(); // Use individual getter
|
||||
modelId = getFallbackModelId(); // Use individual getter
|
||||
} else {
|
||||
log(
|
||||
'warn',
|
||||
`Retries exhausted or non-retryable error for role ${currentRole}, trying next role in sequence...`
|
||||
'error',
|
||||
`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
|
||||
);
|
||||
lastError =
|
||||
lastError || new Error(`Unknown AI role specified: ${currentRole}`);
|
||||
continue; // Skip to the next role attempt
|
||||
}
|
||||
// --- End Corrected Config Fetching ---
|
||||
|
||||
if (!providerName || !modelId) {
|
||||
log(
|
||||
'warn',
|
||||
`Skipping role '${currentRole}': Provider or Model ID not configured.`
|
||||
);
|
||||
lastError =
|
||||
lastError ||
|
||||
new Error(
|
||||
`Configuration missing for role '${currentRole}'. Provider: ${providerName}, Model: ${modelId}`
|
||||
);
|
||||
continue; // Skip to the next role
|
||||
}
|
||||
|
||||
roleParams = getParametersForRole(currentRole); // Get { maxTokens, temperature }
|
||||
|
||||
// 2. Get Provider Function Set
|
||||
providerFnSet = PROVIDER_FUNCTIONS[providerName?.toLowerCase()];
|
||||
if (!providerFnSet) {
|
||||
log(
|
||||
'warn',
|
||||
`Skipping role '${currentRole}': Provider '${providerName}' not supported or map entry missing.`
|
||||
);
|
||||
lastError =
|
||||
lastError ||
|
||||
new Error(`Unsupported provider configured: ${providerName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the original service type to get the function
|
||||
providerApiFn = providerFnSet[serviceType];
|
||||
if (typeof providerApiFn !== 'function') {
|
||||
log(
|
||||
'warn',
|
||||
`Skipping role '${currentRole}': Service type '${serviceType}' not implemented for provider '${providerName}'.`
|
||||
);
|
||||
lastError =
|
||||
lastError ||
|
||||
new Error(
|
||||
`Service '${serviceType}' not implemented for provider ${providerName}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Resolve API Key (will throw if required and missing)
|
||||
apiKey = _resolveApiKey(providerName?.toLowerCase(), session); // Throws on failure
|
||||
|
||||
// 4. Construct Messages Array
|
||||
const messages = [];
|
||||
if (systemPrompt) {
|
||||
messages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
|
||||
// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
|
||||
// {
|
||||
// type: 'text',
|
||||
// text: 'Large cached context here like a tasks json',
|
||||
// providerOptions: {
|
||||
// anthropic: { cacheControl: { type: 'ephemeral' } }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Example
|
||||
// if (params.context) { // context is a json string of a tasks object or some other stu
|
||||
// messages.push({
|
||||
// type: 'text',
|
||||
// text: params.context,
|
||||
// providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
|
||||
// });
|
||||
// }
|
||||
|
||||
if (prompt) {
|
||||
// Ensure prompt exists before adding
|
||||
messages.push({ role: 'user', content: prompt });
|
||||
} else {
|
||||
// Throw an error if the prompt is missing, as it's essential
|
||||
throw new Error('User prompt content is missing.');
|
||||
}
|
||||
|
||||
// 5. Prepare call parameters (using messages array)
|
||||
const callParams = {
|
||||
apiKey,
|
||||
modelId,
|
||||
maxTokens: roleParams.maxTokens,
|
||||
temperature: roleParams.temperature,
|
||||
messages, // *** Pass the constructed messages array ***
|
||||
// Add specific params for generateObject if needed
|
||||
...(serviceType === 'generateObject' && { schema, objectName }),
|
||||
...restApiParams // Include other params like maxRetries
|
||||
};
|
||||
|
||||
// 6. Attempt the call with retries
|
||||
const result = await _attemptProviderCallWithRetries(
|
||||
providerApiFn,
|
||||
callParams,
|
||||
providerName,
|
||||
modelId,
|
||||
currentRole
|
||||
);
|
||||
|
||||
log('info', `${serviceType}Service succeeded using role: ${currentRole}`);
|
||||
|
||||
return result; // Return original result for other cases
|
||||
} catch (error) {
|
||||
log(
|
||||
'error', // Log as error since this role attempt failed
|
||||
`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}): ${error.message}`
|
||||
);
|
||||
lastError = error; // Store the error to throw if all roles fail
|
||||
// Log reason and continue (handled within the loop now)
|
||||
}
|
||||
}
|
||||
|
||||
// If loop completes, all roles failed
|
||||
log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
|
||||
throw (
|
||||
lastError ||
|
||||
new Error(
|
||||
'AI service call (streamText) failed for all configured roles in the sequence.'
|
||||
`AI service call (${serviceType}) failed for all configured roles in the sequence.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified service function for generating text.
|
||||
* Handles client retrieval, retries, and fallback sequence.
|
||||
*
|
||||
* @param {object} params - Parameters for the service call.
|
||||
* @param {string} params.role - The initial client role ('main', 'research', 'fallback').
|
||||
* @param {object} [params.session=null] - Optional MCP session object.
|
||||
* @param {string} params.prompt - The prompt for the AI.
|
||||
* @param {string} [params.systemPrompt] - Optional system prompt.
|
||||
* // Other specific generateText params can be included here.
|
||||
* @returns {Promise<string>} The generated text content.
|
||||
*/
|
||||
async function generateTextService(params) {
|
||||
// Now directly returns the text string or throws error
|
||||
return _unifiedServiceRunner('generateText', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified service function for streaming text.
|
||||
* Handles client retrieval, retries, and fallback sequence.
|
||||
*
|
||||
* @param {object} params - Parameters for the service call.
|
||||
* @param {string} params.role - The initial client role ('main', 'research', 'fallback').
|
||||
* @param {object} [params.session=null] - Optional MCP session object.
|
||||
* @param {string} params.prompt - The prompt for the AI.
|
||||
* @param {string} [params.systemPrompt] - Optional system prompt.
|
||||
* // Other specific streamText params can be included here.
|
||||
* @returns {Promise<ReadableStream<string>>} A readable stream of text deltas.
|
||||
*/
|
||||
async function streamTextService(params) {
|
||||
// Now directly returns the stream object or throws error
|
||||
return _unifiedServiceRunner('streamText', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified service function for generating structured objects.
|
||||
* Handles client retrieval, retries, and fallback sequence.
|
||||
@@ -284,85 +397,22 @@ async function streamTextService(params) {
|
||||
* @param {object} params - Parameters for the service call.
|
||||
* @param {string} params.role - The initial client role ('main', 'research', 'fallback').
|
||||
* @param {object} [params.session=null] - Optional MCP session object.
|
||||
* @param {object} [params.overrideOptions={}] - Optional overrides for ai-client-factory.
|
||||
* @param {z.Schema} params.schema - The Zod schema for the expected object.
|
||||
* @param {import('zod').ZodSchema} params.schema - The Zod schema for the expected object.
|
||||
* @param {string} params.prompt - The prompt for the AI.
|
||||
* // ... include other standard generateObject options as needed ...
|
||||
* @returns {Promise<object>} The result from the AI SDK's generateObject function.
|
||||
* @param {string} [params.systemPrompt] - Optional system prompt.
|
||||
* @param {string} [params.objectName='generated_object'] - Name for object/tool.
|
||||
* @param {number} [params.maxRetries=3] - Max retries for object generation.
|
||||
* // Other specific generateObject params can be included here.
|
||||
* @returns {Promise<object>} The generated object matching the schema.
|
||||
*/
|
||||
async function generateObjectService(params) {
|
||||
const {
|
||||
role: initialRole,
|
||||
session,
|
||||
overrideOptions,
|
||||
...generateObjectParams // Collect remaining params for generateObject
|
||||
} = params;
|
||||
log('info', 'generateObjectService called', { role: initialRole });
|
||||
|
||||
let sequence;
|
||||
if (initialRole === 'main') {
|
||||
sequence = ['main', 'fallback', 'research'];
|
||||
} else if (initialRole === 'fallback') {
|
||||
sequence = ['fallback', 'research'];
|
||||
} else if (initialRole === 'research') {
|
||||
sequence = ['research', 'fallback'];
|
||||
} else {
|
||||
log(
|
||||
'warn',
|
||||
`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
|
||||
);
|
||||
sequence = ['main', 'fallback', 'research'];
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
|
||||
for (const currentRole of sequence) {
|
||||
log('info', `Attempting service call with role: ${currentRole}`);
|
||||
let client;
|
||||
try {
|
||||
client = await getClient(currentRole, session, overrideOptions);
|
||||
const clientInfo = {
|
||||
provider: client?.provider || 'unknown',
|
||||
model: client?.modelId || client?.model || 'unknown'
|
||||
};
|
||||
log('info', 'Retrieved AI client', clientInfo);
|
||||
|
||||
const result = await _attemptApiCallWithRetries(
|
||||
client,
|
||||
generateObject, // Pass generateObject function
|
||||
generateObjectParams,
|
||||
currentRole
|
||||
);
|
||||
log('info', `generateObjectService succeeded using role: ${currentRole}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
log(
|
||||
'error',
|
||||
`Service call failed for role ${currentRole}: ${error.message}`
|
||||
);
|
||||
lastError = error;
|
||||
|
||||
if (!client) {
|
||||
log(
|
||||
'warn',
|
||||
`Could not get client for role ${currentRole}, trying next role in sequence...`
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'warn',
|
||||
`Retries exhausted or non-retryable error for role ${currentRole}, trying next role in sequence...`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
|
||||
throw (
|
||||
lastError ||
|
||||
new Error(
|
||||
'AI service call (generateObject) failed for all configured roles in the sequence.'
|
||||
)
|
||||
);
|
||||
const defaults = {
|
||||
objectName: 'generated_object',
|
||||
maxRetries: 3
|
||||
};
|
||||
const combinedParams = { ...defaults, ...params };
|
||||
// Now directly returns the generated object or throws error
|
||||
return _unifiedServiceRunner('generateObject', combinedParams);
|
||||
}
|
||||
|
||||
export { generateTextService, streamTextService, generateObjectService };
|
||||
|
||||
@@ -338,6 +338,20 @@ function getOllamaBaseUrl(explicitRoot = null) {
|
||||
return getGlobalConfig(explicitRoot).ollamaBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model parameters (maxTokens, temperature) for a specific role.
|
||||
* @param {string} role - The role ('main', 'research', 'fallback').
|
||||
* @param {string|null} explicitRoot - Optional explicit path to the project root.
|
||||
* @returns {{maxTokens: number, temperature: number}}
|
||||
*/
|
||||
function getParametersForRole(role, explicitRoot = null) {
|
||||
const roleConfig = getModelConfigForRole(role, explicitRoot);
|
||||
return {
|
||||
maxTokens: roleConfig.maxTokens,
|
||||
temperature: roleConfig.temperature
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the API key for a given provider is set in the environment.
|
||||
* Checks process.env first, then session.env if session is provided.
|
||||
@@ -561,6 +575,7 @@ export {
|
||||
getDefaultPriority,
|
||||
getProjectName,
|
||||
getOllamaBaseUrl,
|
||||
getParametersForRole,
|
||||
|
||||
// API Key Checkers (still relevant)
|
||||
isApiKeySet,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
stopLoadingIndicator
|
||||
} from '../ui.js';
|
||||
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
|
||||
import { getAvailableAIModel } from '../ai-services.js';
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import {
|
||||
getDebugFlag,
|
||||
getMainModelId,
|
||||
@@ -54,6 +54,7 @@ async function updateSubtaskById(
|
||||
};
|
||||
|
||||
let loadingIndicator = null;
|
||||
|
||||
try {
|
||||
report(`Updating subtask ${subtaskId} with prompt: "${prompt}"`, 'info');
|
||||
|
||||
@@ -193,236 +194,55 @@ async function updateSubtaskById(
|
||||
);
|
||||
}
|
||||
|
||||
// Create the system prompt (as before)
|
||||
const systemPrompt = `You are an AI assistant helping to update software development subtasks with additional information.
|
||||
let additionalInformation = '';
|
||||
try {
|
||||
// Reverted: Keep the original system prompt
|
||||
const systemPrompt = `You are an AI assistant helping to update software development subtasks with additional information.
|
||||
Given a subtask, you will provide additional details, implementation notes, or technical insights based on user request.
|
||||
Focus only on adding content that enhances the subtask - don't repeat existing information.
|
||||
Be technical, specific, and implementation-focused rather than general.
|
||||
Provide concrete examples, code snippets, or implementation details when relevant.`;
|
||||
|
||||
// Replace the old research/Claude code with the new model selection approach
|
||||
let additionalInformation = '';
|
||||
let modelAttempts = 0;
|
||||
const maxModelAttempts = 2; // Try up to 2 models before giving up
|
||||
// Reverted: Use the full JSON stringification for the user message
|
||||
const subtaskData = JSON.stringify(subtask, null, 2);
|
||||
const userMessageContent = `Here is the subtask to enhance:\n${subtaskData}\n\nPlease provide additional information addressing this request:\n${prompt}\n\nReturn ONLY the new information to add - do not repeat existing content.`;
|
||||
|
||||
while (modelAttempts < maxModelAttempts && !additionalInformation) {
|
||||
modelAttempts++; // Increment attempt counter at the start
|
||||
const isLastAttempt = modelAttempts >= maxModelAttempts;
|
||||
let modelType = null; // Declare modelType outside the try block
|
||||
const serviceRole = useResearch ? 'research' : 'main';
|
||||
report(`Calling AI stream service with role: ${serviceRole}`, 'info');
|
||||
|
||||
try {
|
||||
// Get the best available model based on our current state
|
||||
const result = getAvailableAIModel({
|
||||
claudeOverloaded,
|
||||
requiresResearch: useResearch
|
||||
});
|
||||
modelType = result.type;
|
||||
const client = result.client;
|
||||
const streamResult = await generateTextService({
|
||||
role: serviceRole,
|
||||
session: session,
|
||||
systemPrompt: systemPrompt, // Pass the original system prompt
|
||||
prompt: userMessageContent // Pass the original user message content
|
||||
});
|
||||
|
||||
report(
|
||||
`Attempt ${modelAttempts}/${maxModelAttempts}: Generating subtask info using ${modelType}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// Update loading indicator text - only for text output
|
||||
if (outputFormat === 'text') {
|
||||
if (loadingIndicator) {
|
||||
stopLoadingIndicator(loadingIndicator); // Stop previous indicator
|
||||
}
|
||||
loadingIndicator = startLoadingIndicator(
|
||||
`Attempt ${modelAttempts}: Using ${modelType.toUpperCase()}...`
|
||||
);
|
||||
}
|
||||
|
||||
const subtaskData = JSON.stringify(subtask, null, 2);
|
||||
const userMessageContent = `Here is the subtask to enhance:\n${subtaskData}\n\nPlease provide additional information addressing this request:\n${prompt}\n\nReturn ONLY the new information to add - do not repeat existing content.`;
|
||||
|
||||
if (modelType === 'perplexity') {
|
||||
// Construct Perplexity payload
|
||||
const perplexityModel = getResearchModelId(session);
|
||||
const response = await client.chat.completions.create({
|
||||
model: perplexityModel,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userMessageContent }
|
||||
],
|
||||
temperature: getResearchTemperature(session),
|
||||
max_tokens: getResearchMaxTokens(session)
|
||||
});
|
||||
additionalInformation = response.choices[0].message.content.trim();
|
||||
} else {
|
||||
// Claude
|
||||
let responseText = '';
|
||||
let streamingInterval = null;
|
||||
|
||||
try {
|
||||
// Only update streaming indicator for text output
|
||||
if (outputFormat === 'text') {
|
||||
let dotCount = 0;
|
||||
const readline = await import('readline');
|
||||
streamingInterval = setInterval(() => {
|
||||
readline.cursorTo(process.stdout, 0);
|
||||
process.stdout.write(
|
||||
`Receiving streaming response from Claude${'.'.repeat(dotCount)}`
|
||||
);
|
||||
dotCount = (dotCount + 1) % 4;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Construct Claude payload using config getters
|
||||
const stream = await client.messages.create({
|
||||
model: getMainModelId(session),
|
||||
max_tokens: getMainMaxTokens(session),
|
||||
temperature: getMainTemperature(session),
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: userMessageContent }],
|
||||
stream: true
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === 'content_block_delta' && chunk.delta.text) {
|
||||
responseText += chunk.delta.text;
|
||||
}
|
||||
if (reportProgress) {
|
||||
await reportProgress({
|
||||
progress:
|
||||
(responseText.length / getMainMaxTokens(session)) * 100
|
||||
});
|
||||
}
|
||||
if (mcpLog) {
|
||||
mcpLog.info(
|
||||
`Progress: ${(responseText.length / getMainMaxTokens(session)) * 100}%`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (streamingInterval) clearInterval(streamingInterval);
|
||||
// Clear the loading dots line - only for text output
|
||||
if (outputFormat === 'text') {
|
||||
const readline = await import('readline');
|
||||
readline.cursorTo(process.stdout, 0);
|
||||
process.stdout.clearLine(0);
|
||||
}
|
||||
}
|
||||
|
||||
report(
|
||||
`Completed streaming response from Claude API! (Attempt ${modelAttempts})`,
|
||||
'info'
|
||||
);
|
||||
additionalInformation = responseText.trim();
|
||||
}
|
||||
|
||||
// Success - break the loop
|
||||
if (additionalInformation) {
|
||||
report(
|
||||
`Successfully generated information using ${modelType} on attempt ${modelAttempts}.`,
|
||||
'info'
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
// Handle case where AI gave empty response without erroring
|
||||
report(
|
||||
`AI (${modelType}) returned empty response on attempt ${modelAttempts}.`,
|
||||
'warn'
|
||||
);
|
||||
if (isLastAttempt) {
|
||||
throw new Error(
|
||||
'AI returned empty response after maximum attempts.'
|
||||
);
|
||||
}
|
||||
// Allow loop to continue to try another model/attempt if possible
|
||||
}
|
||||
} catch (modelError) {
|
||||
const failedModel =
|
||||
modelType || modelError.modelType || 'unknown model';
|
||||
report(
|
||||
`Attempt ${modelAttempts} failed using ${failedModel}: ${modelError.message}`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
// --- More robust overload check ---
|
||||
let isOverload = false;
|
||||
// Check 1: SDK specific property (common pattern)
|
||||
if (modelError.type === 'overloaded_error') {
|
||||
isOverload = true;
|
||||
}
|
||||
// Check 2: Check nested error property (as originally intended)
|
||||
else if (modelError.error?.type === 'overloaded_error') {
|
||||
isOverload = true;
|
||||
}
|
||||
// Check 3: Check status code if available (e.g., 429 Too Many Requests or 529 Overloaded)
|
||||
else if (modelError.status === 429 || modelError.status === 529) {
|
||||
isOverload = true;
|
||||
}
|
||||
// Check 4: Check the message string itself (less reliable)
|
||||
else if (modelError.message?.toLowerCase().includes('overloaded')) {
|
||||
isOverload = true;
|
||||
}
|
||||
// --- End robust check ---
|
||||
|
||||
if (isOverload) {
|
||||
// Use the result of the check
|
||||
claudeOverloaded = true; // Mark Claude as overloaded for the *next* potential attempt
|
||||
if (!isLastAttempt) {
|
||||
report(
|
||||
'Claude overloaded. Will attempt fallback model if available.',
|
||||
'info'
|
||||
);
|
||||
// Stop the current indicator before continuing - only for text output
|
||||
if (outputFormat === 'text' && loadingIndicator) {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
loadingIndicator = null; // Reset indicator
|
||||
}
|
||||
continue; // Go to next iteration of the while loop to try fallback
|
||||
} else {
|
||||
// It was the last attempt, and it failed due to overload
|
||||
report(
|
||||
`Overload error on final attempt (${modelAttempts}/${maxModelAttempts}). No fallback possible.`,
|
||||
'error'
|
||||
);
|
||||
// Let the error be thrown after the loop finishes, as additionalInformation will be empty.
|
||||
// We don't throw immediately here, let the loop exit and the check after the loop handle it.
|
||||
}
|
||||
} else {
|
||||
// Error was NOT an overload
|
||||
// If it's not an overload, throw it immediately to be caught by the outer catch.
|
||||
report(
|
||||
`Non-overload error on attempt ${modelAttempts}: ${modelError.message}`,
|
||||
'error'
|
||||
);
|
||||
throw modelError; // Re-throw non-overload errors immediately.
|
||||
}
|
||||
} // End inner catch
|
||||
} // End while loop
|
||||
|
||||
// If loop finished without getting information
|
||||
if (!additionalInformation) {
|
||||
// Only show debug info for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
'>>> DEBUG: additionalInformation is falsy! Value:',
|
||||
additionalInformation
|
||||
);
|
||||
if (outputFormat === 'text' && loadingIndicator) {
|
||||
// Stop indicator immediately since generateText is blocking
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
loadingIndicator = null;
|
||||
}
|
||||
throw new Error(
|
||||
'Failed to generate additional information after all attempts.'
|
||||
);
|
||||
}
|
||||
|
||||
// Only show debug info for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
console.log(
|
||||
'>>> DEBUG: Got additionalInformation:',
|
||||
additionalInformation.substring(0, 50) + '...'
|
||||
);
|
||||
}
|
||||
// Assign the result directly (generateTextService returns the text string)
|
||||
additionalInformation = streamResult ? streamResult.trim() : '';
|
||||
|
||||
if (!additionalInformation) {
|
||||
throw new Error('AI returned empty response.'); // Changed error message slightly
|
||||
}
|
||||
report(
|
||||
// Corrected log message to reflect generateText
|
||||
`Successfully generated text using AI role: ${serviceRole}.`,
|
||||
'info'
|
||||
);
|
||||
} catch (aiError) {
|
||||
report(`AI service call failed: ${aiError.message}`, 'error');
|
||||
throw aiError;
|
||||
} // Removed the inner finally block as streamingInterval is gone
|
||||
|
||||
// Create timestamp
|
||||
const currentDate = new Date();
|
||||
const timestamp = currentDate.toISOString();
|
||||
|
||||
// Format the additional information with timestamp
|
||||
const formattedInformation = `\n\n<info added on ${timestamp}>\n${additionalInformation}\n</info added on ${timestamp}>`;
|
||||
const formattedInformation = `\n\n<info added on ${currentDate.toISOString()}>\n${additionalInformation}\n</info added on ${currentDate.toISOString()}>`;
|
||||
|
||||
// Only show debug info for text output (CLI)
|
||||
if (outputFormat === 'text') {
|
||||
@@ -556,9 +376,9 @@ Provide concrete examples, code snippets, or implementation details when relevan
|
||||
' 1. Run task-master list --with-subtasks to see all available subtask IDs'
|
||||
);
|
||||
console.log(
|
||||
' 2. Use a valid subtask ID with the --id parameter in format \"parentId.subtaskId\"'
|
||||
' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"'
|
||||
);
|
||||
} else if (error.message?.includes('empty response from AI')) {
|
||||
} else if (error.message?.includes('empty stream response')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nThe AI model returned an empty response. This might be due to the prompt or API issues. Try rephrasing or trying again later.'
|
||||
@@ -575,11 +395,6 @@ Provide concrete examples, code snippets, or implementation details when relevan
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
// Final cleanup check for the indicator, although it should be stopped by now
|
||||
if (outputFormat === 'text' && loadingIndicator) {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,8 @@ function getClient(apiKey) {
|
||||
*
|
||||
* @param {object} params - Parameters for the text generation.
|
||||
* @param {string} params.apiKey - The Anthropic API key.
|
||||
* @param {string} params.modelId - The specific Anthropic model ID to use (e.g., 'claude-3-haiku-20240307').
|
||||
* @param {string} params.systemPrompt - The system prompt.
|
||||
* @param {string} params.userPrompt - The user prompt.
|
||||
* @param {string} params.modelId - The specific Anthropic model ID.
|
||||
* @param {Array<object>} params.messages - The messages array (e.g., [{ role: 'user', content: '...' }]).
|
||||
* @param {number} [params.maxTokens] - Maximum tokens for the response.
|
||||
* @param {number} [params.temperature] - Temperature for generation.
|
||||
* @returns {Promise<string>} The generated text content.
|
||||
@@ -53,8 +52,7 @@ function getClient(apiKey) {
|
||||
export async function generateAnthropicText({
|
||||
apiKey,
|
||||
modelId,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
messages,
|
||||
maxTokens,
|
||||
temperature
|
||||
}) {
|
||||
@@ -62,11 +60,13 @@ export async function generateAnthropicText({
|
||||
try {
|
||||
const client = getClient(apiKey);
|
||||
const result = await generateText({
|
||||
model: client(modelId), // Pass the model ID to the client instance
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
model: client(modelId),
|
||||
messages: messages,
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature
|
||||
temperature: temperature,
|
||||
headers: {
|
||||
'anthropic-beta': 'output-128k-2025-02-19'
|
||||
}
|
||||
// TODO: Add other relevant parameters like topP, topK if needed
|
||||
});
|
||||
log(
|
||||
@@ -87,38 +87,59 @@ export async function generateAnthropicText({
|
||||
* @param {object} params - Parameters for the text streaming.
|
||||
* @param {string} params.apiKey - The Anthropic API key.
|
||||
* @param {string} params.modelId - The specific Anthropic model ID.
|
||||
* @param {string} params.systemPrompt - The system prompt.
|
||||
* @param {string} params.userPrompt - The user prompt.
|
||||
* @param {Array<object>} params.messages - The messages array.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens for the response.
|
||||
* @param {number} [params.temperature] - Temperature for generation.
|
||||
* @returns {Promise<ReadableStream<string>>} A readable stream of text deltas.
|
||||
* @returns {Promise<object>} The full stream result object from the Vercel AI SDK.
|
||||
* @throws {Error} If the API call fails to initiate the stream.
|
||||
*/
|
||||
export async function streamAnthropicText({
|
||||
apiKey,
|
||||
modelId,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
messages,
|
||||
maxTokens,
|
||||
temperature
|
||||
}) {
|
||||
log('debug', `Streaming Anthropic text with model: ${modelId}`);
|
||||
try {
|
||||
const client = getClient(apiKey);
|
||||
|
||||
// --- DEBUG LOGGING --- >>
|
||||
log(
|
||||
'debug',
|
||||
'[streamAnthropicText] Parameters received by streamText:',
|
||||
JSON.stringify(
|
||||
{
|
||||
modelId: modelId, // Log modelId being used
|
||||
messages: messages, // Log the messages array
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
// --- << DEBUG LOGGING ---
|
||||
|
||||
const stream = await streamText({
|
||||
model: client(modelId),
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
messages: messages,
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature
|
||||
temperature: temperature,
|
||||
headers: {
|
||||
'anthropic-beta': 'output-128k-2025-02-19'
|
||||
}
|
||||
// TODO: Add other relevant parameters
|
||||
});
|
||||
|
||||
// We return the stream directly. The consumer will handle reading it.
|
||||
// We could potentially wrap it or add logging within the stream pipe if needed.
|
||||
return stream.textStream;
|
||||
// *** RETURN THE FULL STREAM OBJECT, NOT JUST stream.textStream ***
|
||||
return stream;
|
||||
} catch (error) {
|
||||
log('error', `Anthropic streamText failed: ${error.message}`);
|
||||
log(
|
||||
'error',
|
||||
`Anthropic streamText failed: ${error.message}`,
|
||||
error.stack // Log stack trace for more details
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -132,8 +153,7 @@ export async function streamAnthropicText({
|
||||
* @param {object} params - Parameters for object generation.
|
||||
* @param {string} params.apiKey - The Anthropic API key.
|
||||
* @param {string} params.modelId - The specific Anthropic model ID.
|
||||
* @param {string} params.systemPrompt - The system prompt (optional).
|
||||
* @param {string} params.userPrompt - The user prompt describing the desired object.
|
||||
* @param {Array<object>} params.messages - The messages array.
|
||||
* @param {import('zod').ZodSchema} params.schema - The Zod schema for the object.
|
||||
* @param {string} params.objectName - A name for the object/tool.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens for the response.
|
||||
@@ -145,10 +165,9 @@ export async function streamAnthropicText({
|
||||
export async function generateAnthropicObject({
|
||||
apiKey,
|
||||
modelId,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
messages,
|
||||
schema,
|
||||
objectName = 'generated_object', // Provide a default name
|
||||
objectName = 'generated_object',
|
||||
maxTokens,
|
||||
temperature,
|
||||
maxRetries = 3
|
||||
@@ -163,11 +182,10 @@ export async function generateAnthropicObject({
|
||||
model: client(modelId),
|
||||
mode: 'tool', // Anthropic generally uses 'tool' mode for structured output
|
||||
schema: schema,
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
messages: messages,
|
||||
tool: {
|
||||
name: objectName, // Use the provided or default name
|
||||
description: `Generate a ${objectName} based on the prompt.` // Simple description
|
||||
name: objectName,
|
||||
description: `Generate a ${objectName} based on the prompt.`
|
||||
},
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature,
|
||||
|
||||
@@ -25,20 +25,19 @@ function getClient(apiKey) {
|
||||
/**
|
||||
* Generates text using a Perplexity model.
|
||||
*
|
||||
* @param {object} params - Parameters for text generation.
|
||||
* @param {object} params - Parameters for the text generation.
|
||||
* @param {string} params.apiKey - The Perplexity API key.
|
||||
* @param {string} params.modelId - The Perplexity model ID (e.g., 'sonar-small-32k-online').
|
||||
* @param {string} [params.systemPrompt] - The system prompt (optional for some models).
|
||||
* @param {string} params.userPrompt - The user prompt.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens.
|
||||
* @param {number} [params.temperature] - Temperature.
|
||||
* @returns {Promise<string>} Generated text.
|
||||
* @param {string} params.modelId - The specific Perplexity model ID.
|
||||
* @param {Array<object>} params.messages - The messages array.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens for the response.
|
||||
* @param {number} [params.temperature] - Temperature for generation.
|
||||
* @returns {Promise<string>} The generated text content.
|
||||
* @throws {Error} If the API call fails.
|
||||
*/
|
||||
export async function generatePerplexityText({
|
||||
apiKey,
|
||||
modelId,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
messages,
|
||||
maxTokens,
|
||||
temperature
|
||||
}) {
|
||||
@@ -47,8 +46,7 @@ export async function generatePerplexityText({
|
||||
const client = getClient(apiKey);
|
||||
const result = await generateText({
|
||||
model: client(modelId),
|
||||
system: systemPrompt, // Pass system prompt if provided
|
||||
prompt: userPrompt,
|
||||
messages: messages,
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature
|
||||
});
|
||||
@@ -66,20 +64,19 @@ export async function generatePerplexityText({
|
||||
/**
|
||||
* Streams text using a Perplexity model.
|
||||
*
|
||||
* @param {object} params - Parameters for text streaming.
|
||||
* @param {object} params - Parameters for the text streaming.
|
||||
* @param {string} params.apiKey - The Perplexity API key.
|
||||
* @param {string} params.modelId - The Perplexity model ID.
|
||||
* @param {string} [params.systemPrompt] - The system prompt.
|
||||
* @param {string} params.userPrompt - The user prompt.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens.
|
||||
* @param {number} [params.temperature] - Temperature.
|
||||
* @returns {Promise<ReadableStream<string>>} Stream of text deltas.
|
||||
* @param {string} params.modelId - The specific Perplexity model ID.
|
||||
* @param {Array<object>} params.messages - The messages array.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens for the response.
|
||||
* @param {number} [params.temperature] - Temperature for generation.
|
||||
* @returns {Promise<object>} The full stream result object from the Vercel AI SDK.
|
||||
* @throws {Error} If the API call fails to initiate the stream.
|
||||
*/
|
||||
export async function streamPerplexityText({
|
||||
apiKey,
|
||||
modelId,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
messages,
|
||||
maxTokens,
|
||||
temperature
|
||||
}) {
|
||||
@@ -88,12 +85,11 @@ export async function streamPerplexityText({
|
||||
const client = getClient(apiKey);
|
||||
const stream = await streamText({
|
||||
model: client(modelId),
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
messages: messages,
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature
|
||||
});
|
||||
return stream.textStream;
|
||||
return stream;
|
||||
} catch (error) {
|
||||
log('error', `Perplexity streamText failed: ${error.message}`);
|
||||
throw error;
|
||||
@@ -102,49 +98,48 @@ export async function streamPerplexityText({
|
||||
|
||||
/**
|
||||
* Generates a structured object using a Perplexity model.
|
||||
* Note: Perplexity's support for structured output/tool use might vary.
|
||||
* We assume it follows OpenAI's function/tool calling conventions if supported by the SDK.
|
||||
* Note: Perplexity API might not directly support structured object generation
|
||||
* in the same way as OpenAI or Anthropic. This function might need
|
||||
* adjustments or might not be feasible depending on the model's capabilities
|
||||
* and the Vercel AI SDK's support for Perplexity in this context.
|
||||
*
|
||||
* @param {object} params - Parameters for object generation.
|
||||
* @param {string} params.apiKey - The Perplexity API key.
|
||||
* @param {string} params.modelId - The Perplexity model ID.
|
||||
* @param {string} [params.systemPrompt] - System prompt.
|
||||
* @param {string} params.userPrompt - User prompt.
|
||||
* @param {import('zod').ZodSchema} params.schema - Zod schema.
|
||||
* @param {string} params.objectName - Name for the object/tool.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens.
|
||||
* @param {number} [params.temperature] - Temperature.
|
||||
* @param {number} [params.maxRetries] - Max retries.
|
||||
* @returns {Promise<object>} Generated object.
|
||||
* @param {string} params.modelId - The specific Perplexity model ID.
|
||||
* @param {Array<object>} params.messages - The messages array.
|
||||
* @param {import('zod').ZodSchema} params.schema - The Zod schema for the object.
|
||||
* @param {string} params.objectName - A name for the object/tool.
|
||||
* @param {number} [params.maxTokens] - Maximum tokens for the response.
|
||||
* @param {number} [params.temperature] - Temperature for generation.
|
||||
* @param {number} [params.maxRetries] - Max retries for validation/generation.
|
||||
* @returns {Promise<object>} The generated object matching the schema.
|
||||
* @throws {Error} If generation or validation fails or is unsupported.
|
||||
*/
|
||||
export async function generatePerplexityObject({
|
||||
apiKey,
|
||||
modelId,
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
messages,
|
||||
schema,
|
||||
objectName = 'generated_object',
|
||||
maxTokens,
|
||||
temperature,
|
||||
maxRetries = 3
|
||||
maxRetries = 1 // Lower retries as support might be limited
|
||||
}) {
|
||||
log(
|
||||
'debug',
|
||||
`Generating Perplexity object ('${objectName}') with model: ${modelId}`
|
||||
`Attempting to generate Perplexity object ('${objectName}') with model: ${modelId}`
|
||||
);
|
||||
log(
|
||||
'warn',
|
||||
'generateObject support for Perplexity might be limited or experimental.'
|
||||
);
|
||||
try {
|
||||
const client = getClient(apiKey);
|
||||
// Assuming Perplexity follows OpenAI-like tool mode if supported by SDK
|
||||
// Attempt using generateObject, but be prepared for potential issues
|
||||
const result = await generateObject({
|
||||
model: client(modelId),
|
||||
mode: 'tool',
|
||||
schema: schema,
|
||||
system: systemPrompt,
|
||||
prompt: userPrompt,
|
||||
tool: {
|
||||
name: objectName,
|
||||
description: `Generate a ${objectName} based on the prompt.`
|
||||
},
|
||||
messages: messages,
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature,
|
||||
maxRetries: maxRetries
|
||||
@@ -159,18 +154,10 @@ export async function generatePerplexityObject({
|
||||
'error',
|
||||
`Perplexity generateObject ('${objectName}') failed: ${error.message}`
|
||||
);
|
||||
// Check if the error indicates lack of tool support
|
||||
if (
|
||||
error.message.includes('tool use') ||
|
||||
error.message.includes('structured output')
|
||||
) {
|
||||
log(
|
||||
'warn',
|
||||
`Model ${modelId} might not support structured output via tools.`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
throw new Error(
|
||||
`Failed to generate object with Perplexity: ${error.message}. Structured output might not be fully supported.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement streamPerplexityObject if needed and supported.
|
||||
// TODO: Implement streamPerplexityObject if needed and feasible.
|
||||
|
||||
@@ -1046,7 +1046,7 @@ The refactoring of callers to AI parsing utilities should align with the new con
|
||||
5. When calling `generateObjectService`, pass the appropriate configuration context to ensure it uses the correct settings from the centralized configuration system.
|
||||
</info added on 2025-04-20T03:52:45.518Z>
|
||||
|
||||
## 19. Refactor `updateSubtaskById` AI Call [pending]
|
||||
## 19. Refactor `updateSubtaskById` AI Call [done]
|
||||
### Dependencies: 61.23
|
||||
### Description: Refactor the AI call within `updateSubtaskById` in `task-manager.js` (which generates additional information based on a prompt) to use the appropriate unified service function (e.g., `generateTextService`) from `ai-services-unified.js`.
|
||||
### Details:
|
||||
@@ -1085,6 +1085,197 @@ const completion = await generateTextService({
|
||||
```
|
||||
</info added on 2025-04-20T03:52:28.196Z>
|
||||
|
||||
<info added on 2025-04-22T06:05:42.437Z>
|
||||
- When testing the non-streaming `generateTextService` call within `updateSubtaskById`, ensure that the function awaits the full response before proceeding with subtask updates. This allows you to validate that the unified service returns the expected structure (e.g., `completion.choices.message.content`) and that error handling logic correctly interprets any error objects or status codes returned by the service.
|
||||
|
||||
- Mock or stub the `generateTextService` in unit tests to simulate both successful and failed completions. For example, verify that when the service returns a valid completion, the subtask is updated with the generated content, and when an error is returned, the error handling path is triggered and logged appropriately.
|
||||
|
||||
- Confirm that the non-streaming mode does not emit partial results or require event-based handling; the function should only process the final, complete response.
|
||||
|
||||
- Example test assertion:
|
||||
```javascript
|
||||
// Mocked response from generateTextService
|
||||
const mockCompletion = {
|
||||
choices: [{ message: { content: "Generated subtask details." } }]
|
||||
};
|
||||
generateTextService.mockResolvedValue(mockCompletion);
|
||||
|
||||
// Call updateSubtaskById and assert the subtask is updated
|
||||
await updateSubtaskById(...);
|
||||
expect(subtask.details).toBe("Generated subtask details.");
|
||||
```
|
||||
|
||||
- If the unified service supports both streaming and non-streaming modes, explicitly set or verify the `stream` parameter is `false` (or omitted) to ensure non-streaming behavior during these tests.
|
||||
</info added on 2025-04-22T06:05:42.437Z>
|
||||
|
||||
<info added on 2025-04-22T06:20:19.747Z>
|
||||
When testing the non-streaming `generateTextService` call in `updateSubtaskById`, implement these verification steps:
|
||||
|
||||
1. Add unit tests that verify proper parameter transformation between the old and new implementation:
|
||||
```javascript
|
||||
test('should correctly transform parameters when calling generateTextService', async () => {
|
||||
// Setup mocks for config values
|
||||
jest.spyOn(configManager, 'getMainModel').mockReturnValue('gpt-4');
|
||||
jest.spyOn(configManager, 'getMainTemperature').mockReturnValue(0.7);
|
||||
jest.spyOn(configManager, 'getMainMaxTokens').mockReturnValue(1000);
|
||||
|
||||
const generateTextServiceSpy = jest.spyOn(aiServices, 'generateTextService')
|
||||
.mockResolvedValue({ choices: [{ message: { content: 'test content' } }] });
|
||||
|
||||
await updateSubtaskById(/* params */);
|
||||
|
||||
// Verify the service was called with correct transformed parameters
|
||||
expect(generateTextServiceSpy).toHaveBeenCalledWith({
|
||||
model: 'gpt-4',
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
messages: expect.any(Array)
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
2. Implement response validation to ensure the subtask content is properly extracted:
|
||||
```javascript
|
||||
// In updateSubtaskById function
|
||||
try {
|
||||
const completion = await generateTextService({
|
||||
// parameters
|
||||
});
|
||||
|
||||
// Validate response structure before using
|
||||
if (!completion?.choices?.[0]?.message?.content) {
|
||||
throw new Error('Invalid response structure from AI service');
|
||||
}
|
||||
|
||||
// Continue with updating subtask
|
||||
} catch (error) {
|
||||
// Enhanced error handling
|
||||
}
|
||||
```
|
||||
|
||||
3. Add integration tests that verify the end-to-end flow with actual configuration values.
|
||||
</info added on 2025-04-22T06:20:19.747Z>
|
||||
|
||||
<info added on 2025-04-22T06:23:23.247Z>
|
||||
<info added on 2025-04-22T06:35:14.892Z>
|
||||
When testing the non-streaming `generateTextService` call in `updateSubtaskById`, implement these specific verification steps:
|
||||
|
||||
1. Create a dedicated test fixture that isolates the AI service interaction:
|
||||
```javascript
|
||||
describe('updateSubtaskById AI integration', () => {
|
||||
beforeEach(() => {
|
||||
// Reset all mocks and spies
|
||||
jest.clearAllMocks();
|
||||
// Setup environment with controlled config values
|
||||
process.env.OPENAI_API_KEY = 'test-key';
|
||||
});
|
||||
|
||||
// Test cases follow...
|
||||
});
|
||||
```
|
||||
|
||||
2. Test error propagation from the unified service:
|
||||
```javascript
|
||||
test('should properly handle AI service errors', async () => {
|
||||
const mockError = new Error('Service unavailable');
|
||||
mockError.status = 503;
|
||||
jest.spyOn(aiServices, 'generateTextService').mockRejectedValue(mockError);
|
||||
|
||||
// Capture console errors if needed
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
// Execute with error expectation
|
||||
await expect(updateSubtaskById(1, { prompt: 'test' })).rejects.toThrow();
|
||||
|
||||
// Verify error was logged with appropriate context
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('AI service error'),
|
||||
expect.objectContaining({ status: 503 })
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
3. Verify that the function correctly preserves existing subtask content when appending new AI-generated information:
|
||||
```javascript
|
||||
test('should preserve existing content when appending AI-generated details', async () => {
|
||||
// Setup mock subtask with existing content
|
||||
const mockSubtask = {
|
||||
id: 1,
|
||||
details: 'Existing details.\n\n'
|
||||
};
|
||||
|
||||
// Mock database retrieval
|
||||
getSubtaskById.mockResolvedValue(mockSubtask);
|
||||
|
||||
// Mock AI response
|
||||
generateTextService.mockResolvedValue({
|
||||
choices: [{ message: { content: 'New AI content.' } }]
|
||||
});
|
||||
|
||||
await updateSubtaskById(1, { prompt: 'Enhance this subtask' });
|
||||
|
||||
// Verify the update preserves existing content
|
||||
expect(updateSubtaskInDb).toHaveBeenCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
details: expect.stringContaining('Existing details.\n\n<info added on')
|
||||
})
|
||||
);
|
||||
|
||||
// Verify the new content was added
|
||||
expect(updateSubtaskInDb).toHaveBeenCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
details: expect.stringContaining('New AI content.')
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
4. Test that the function correctly formats the timestamp and wraps the AI-generated content:
|
||||
```javascript
|
||||
test('should format timestamp and wrap content correctly', async () => {
|
||||
// Mock date for consistent testing
|
||||
const mockDate = new Date('2025-04-22T10:00:00Z');
|
||||
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
||||
|
||||
// Setup and execute test
|
||||
// ...
|
||||
|
||||
// Verify correct formatting
|
||||
expect(updateSubtaskInDb).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
expect.objectContaining({
|
||||
details: expect.stringMatching(
|
||||
/<info added on 2025-04-22T10:00:00\.000Z>\n.*\n<\/info added on 2025-04-22T10:00:00\.000Z>/s
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
5. Verify that the function correctly handles the case when no existing details are present:
|
||||
```javascript
|
||||
test('should handle subtasks with no existing details', async () => {
|
||||
// Setup mock subtask with no details
|
||||
const mockSubtask = { id: 1 };
|
||||
getSubtaskById.mockResolvedValue(mockSubtask);
|
||||
|
||||
// Execute test
|
||||
// ...
|
||||
|
||||
// Verify details were initialized properly
|
||||
expect(updateSubtaskInDb).toHaveBeenCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
details: expect.stringMatching(/^<info added on/)
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
</info added on 2025-04-22T06:35:14.892Z>
|
||||
</info added on 2025-04-22T06:23:23.247Z>
|
||||
|
||||
## 20. Implement `anthropic.js` Provider Module using Vercel AI SDK [done]
|
||||
### Dependencies: None
|
||||
### Description: Create and implement the `anthropic.js` module within `src/ai-providers/`. This module should contain functions to interact with the Anthropic API (streaming and non-streaming) using the **Vercel AI SDK**, adhering to the standardized input/output format defined for `ai-services-unified.js`.
|
||||
@@ -1103,7 +1294,7 @@ const completion = await generateTextService({
|
||||
### Details:
|
||||
|
||||
|
||||
## 23. Implement Conditional Provider Logic in `ai-services-unified.js` [pending]
|
||||
## 23. Implement Conditional Provider Logic in `ai-services-unified.js` [done]
|
||||
### Dependencies: 61.20,61.21,61.22,61.24,61.25,61.26,61.27,61.28,61.29,61.30,61.34
|
||||
### Description: Implement logic within the functions of `ai-services-unified.js` (e.g., `generateTextService`, `generateObjectService`, `streamChatService`) to dynamically select and call the appropriate provider module (`anthropic.js`, `perplexity.js`, etc.) based on configuration (e.g., environment variables like `AI_PROVIDER` and `AI_MODEL` from `process.env` or `session.env`).
|
||||
### Details:
|
||||
@@ -1440,7 +1631,7 @@ For the integration tests of the Unified AI Service, consider the following impl
|
||||
6. Include tests for configuration changes at runtime and their effect on service behavior.
|
||||
</info added on 2025-04-20T03:51:23.368Z>
|
||||
|
||||
## 32. Update Documentation for New AI Architecture [pending]
|
||||
## 32. Update Documentation for New AI Architecture [done]
|
||||
### Dependencies: 61.31
|
||||
### Description: Update relevant documentation files (e.g., `architecture.mdc`, `taskmaster.mdc`, environment variable guides, README) to accurately reflect the new AI service architecture using `ai-services-unified.js`, provider modules, the Vercel AI SDK, and the updated configuration approach.
|
||||
### Details:
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user