activity-simulator / src /services /aiProvider.js
abedelbahnasy55's picture
feat: massive AI diversity upgrade
736d3a6
import logger from '../utils/logger.js';
import { withRetry, withTimeout } from '../utils/retry.js';
class AIProvider {
constructor(config) {
this.provider = config.ai.provider || 'fallback';
this.config = config.ai;
this._requestCount = 0;
this._errorCount = 0;
this._lastError = null;
this._lastRequestTime = 0;
this._minRequestInterval = 500;
this._usedIndices = new Map();
this._sessionStart = Date.now();
}
async generate(prompt, options = {}) {
const { maxTokens = 1024, temperature = 0.7, systemPrompt = null } = options;
this._requestCount++;
const now = Date.now();
const timeSinceLastRequest = now - this._lastRequestTime;
if (timeSinceLastRequest < this._minRequestInterval) {
await new Promise(resolve => setTimeout(resolve, this._minRequestInterval - timeSinceLastRequest));
}
try {
if (this.provider === 'ollama') return await this._ollama(prompt, { maxTokens, temperature, systemPrompt });
if (this.provider === 'huggingface') return await this._huggingface(prompt, { maxTokens, temperature, systemPrompt });
if (this.provider === 'groq') return await this._groq(prompt, { maxTokens, temperature, systemPrompt });
if (this.provider === 'openrouter') return await this._openrouter(prompt, { maxTokens, temperature, systemPrompt });
if (this.provider === 'fallback') return this._fallbackResponse(prompt);
throw new Error(`Unknown AI provider: ${this.provider}`);
} catch (error) {
this._errorCount++;
this._lastError = error;
logger.error(`AI generation failed (attempt ${this._requestCount}): ${error.message}`);
return this._fallbackResponse(prompt);
} finally {
this._lastRequestTime = Date.now();
}
}
async _ollama(prompt, options) {
const { maxTokens, temperature, systemPrompt } = options;
const timeout = this.config.ollama.timeout;
const maxRetries = this.config.ollama.maxRetries;
return withRetry(async () => {
const body = {
model: this.config.ollama.model,
prompt: systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt,
stream: false,
options: { temperature, num_predict: maxTokens },
};
const response = await withTimeout(
fetch(`${this.config.ollama.baseUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
timeout,
new Error(`Ollama request timed out after ${timeout}ms`)
);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Ollama API error ${response.status}: ${errorText}`);
}
const data = await response.json();
if (!data.response || data.response.trim() === '') throw new Error('Ollama returned empty response');
return data.response;
}, { maxRetries, baseDelay: 2000, maxDelay: 30000 });
}
async _huggingface(prompt, options) {
const { maxTokens, temperature, systemPrompt } = options;
const timeout = this.config.huggingface.timeout;
const maxRetries = this.config.huggingface.maxRetries;
if (!this.config.huggingface.apiKey) throw new Error('HuggingFace API key is required');
return withRetry(async () => {
const fullPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
const response = await withTimeout(
fetch(`https://router.huggingface.co/hf-inference/models/${this.config.huggingface.model}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.huggingface.apiKey}` },
body: JSON.stringify({ inputs: fullPrompt, parameters: { max_new_tokens: maxTokens, temperature, return_full_text: false } }),
}),
timeout,
new Error(`HuggingFace request timed out after ${timeout}ms`)
);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HuggingFace API error ${response.status}: ${errorText}`);
}
const data = await response.json();
if (Array.isArray(data) && data[0]?.generated_text) return data[0].generated_text.trim();
if (data.generated_text) return data.generated_text.trim();
if (data.error) throw new Error(`HuggingFace error: ${data.error}`);
return '';
}, { maxRetries, baseDelay: 3000, maxDelay: 60000 });
}
async _groq(prompt, options) {
const { maxTokens, temperature, systemPrompt } = options;
const timeout = this.config.groq?.timeout || 30000;
const maxRetries = this.config.groq?.maxRetries || 3;
const model = this.config.groq?.model || 'llama-3.1-8b-instant';
return withRetry(async () => {
const messages = [];
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
messages.push({ role: 'user', content: prompt });
const response = await withTimeout(
fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.groq?.apiKey}` },
body: JSON.stringify({ model, messages, max_tokens: maxTokens, temperature }),
}),
timeout,
new Error(`Groq request timed out after ${timeout}ms`)
);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Groq API error ${response.status}: ${errorText}`);
}
const data = await response.json();
if (data.choices && data.choices[0]?.message?.content) return data.choices[0].message.content.trim();
if (data.error) throw new Error(`Groq error: ${data.error.message || data.error}`);
return '';
}, { maxRetries, baseDelay: 1000, maxDelay: 30000 });
}
async _openrouter(prompt, options) {
const { maxTokens, temperature, systemPrompt } = options;
const timeout = this.config.openrouter?.timeout || 30000;
const maxRetries = this.config.openrouter?.maxRetries || 3;
const model = this.config.openrouter?.model || 'meta-llama/llama-3.1-8b-instruct:free';
return withRetry(async () => {
const messages = [];
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
messages.push({ role: 'user', content: prompt });
const response = await withTimeout(
fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.openrouter?.apiKey}`,
'HTTP-Referer': 'https://github.com/abedmohamed258/activity-simulator',
'X-Title': 'Activity Simulator',
},
body: JSON.stringify({ model, messages, max_tokens: maxTokens, temperature }),
}),
timeout,
new Error(`OpenRouter request timed out after ${timeout}ms`)
);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`OpenRouter API error ${response.status}: ${errorText}`);
}
const data = await response.json();
if (data.choices && data.choices[0]?.message?.content) return data.choices[0].message.content.trim();
if (data.error) throw new Error(`OpenRouter error: ${data.error.message || data.error}`);
return '';
}, { maxRetries, baseDelay: 1000, maxDelay: 30000 });
}
_getUnusedItem(key, items) {
if (!this._usedIndices.has(key)) this._usedIndices.set(key, new Set());
const used = this._usedIndices.get(key);
if (used.size >= items.length) {
used.clear();
}
let idx;
do {
idx = Math.floor(Math.random() * items.length);
} while (used.has(idx) && used.size < items.length);
used.add(idx);
return items[idx];
}
_fallbackResponse(prompt) {
const lowerPrompt = prompt.toLowerCase();
if (lowerPrompt.includes('github issue title') || lowerPrompt.includes('issue title')) {
return this._getUnusedItem('issueTitles', this._issueTitles);
}
if (lowerPrompt.includes('issue body') || lowerPrompt.includes('github issue')) {
return this._getUnusedItem('issueBodies', this._issueBodies);
}
if (lowerPrompt.includes('review decision') || lowerPrompt.includes('approve')) {
return this._getUnusedItem('reviewDecisions', this._reviewDecisions);
}
if (lowerPrompt.includes('issue') || lowerPrompt.includes('problem')) {
return this._getUnusedItem('issueComments', this._issueComments);
}
if (lowerPrompt.includes('review') || lowerPrompt.includes('comment')) {
return this._getUnusedItem('reviewComments', this._reviewComments);
}
if (lowerPrompt.includes('code') || lowerPrompt.includes('function')) {
return this._getUnusedItem('codeSnippets', this._codeSnippets);
}
if (lowerPrompt.includes('pull request') || lowerPrompt.includes('pr description')) {
return this._getUnusedItem('prDescriptions', this._prDescriptions);
}
if (lowerPrompt.includes('clarif') || lowerPrompt.includes('question')) {
return this._getUnusedItem('clarifications', this._clarifications);
}
return this._getUnusedItem('defaultResponses', this._defaultResponses);
}
get _issueTitles() {
return [
'Add input validation to user registration form',
'Fix race condition in concurrent file processing',
'Refactor database connection pooling for better performance',
'Add proper error handling to API endpoints',
'Implement caching layer for frequently accessed data',
'Fix memory leak in event listener cleanup',
'Add unit tests for utility functions',
'Improve accessibility for screen readers',
'Optimize database queries for large datasets',
'Add rate limiting to prevent abuse',
'Update dependencies to latest versions',
'Add logging for debugging production issues',
'Fix CSS layout shift on page load',
'Implement proper TypeScript types for API responses',
'Add CI pipeline for automated testing',
'Fix broken link in documentation',
'Add dark mode support to the UI',
'Improve startup time by lazy loading modules',
'Add pagination to the dashboard table',
'Fix timezone handling in date picker',
'Add WebSocket support for real-time updates',
'Fix null pointer exception in data processor',
'Implement graceful shutdown for background workers',
'Add request timeout configuration to HTTP client',
'Fix duplicate event listener registration',
'Add health check endpoint for monitoring',
'Implement exponential backoff for retry logic',
'Fix circular dependency in module imports',
'Add request ID tracing for distributed logging',
'Implement connection pooling for database client',
'Fix incorrect date formatting in reports',
'Add support for custom error pages',
'Implement file upload size validation',
'Fix memory usage spike during batch processing',
'Add CORS configuration for cross-origin requests',
'Implement proper session management',
'Fix inconsistent error response format',
'Add support for environment-specific configurations',
'Implement database migration rollback capability',
'Fix incorrect pagination offset calculation',
'Add support for bulk operations in API',
'Implement request deduplication for concurrent calls',
'Fix thread safety issue in shared cache',
'Add support for custom middleware pipeline',
'Implement graceful degradation for external service failures',
'Fix incorrect content-type header in responses',
'Add support for API versioning',
'Implement proper input sanitization for XSS prevention',
'Fix incorrect sorting order in list endpoints',
'Add support for background job scheduling',
];
}
get _issueBodies() {
return [
'## Description\n\nThe current implementation does not handle null inputs gracefully, which can cause unexpected crashes in production.\n\n## Steps to Reproduce\n\n1. Send a request with null payload\n2. Observe the server crash\n3. Check error logs\n\n## Expected Behavior\n\nThe server should return a 400 Bad Request error with a helpful message.\n\n## Actual Behavior\n\nThe server crashes with an unhandled exception.',
'## Description\n\nPerformance degrades significantly when processing large datasets (>10k items).\n\n## Steps to Reproduce\n\n1. Load a dataset with 50k items\n2. Run the processing pipeline\n3. Observe memory usage and response time\n\n## Expected Behavior\n\nProcessing should complete within 5 seconds with stable memory usage.\n\n## Actual Behavior\n\nProcessing takes over 30 seconds and memory usage spikes to 2GB.',
'## Description\n\nThe documentation is outdated and does not reflect the current API surface.\n\n## Steps to Reproduce\n\n1. Follow the quickstart guide\n2. Try to run the example code\n\n## Expected Behavior\n\nThe example should work as documented.\n\n## Actual Behavior\n\nSeveral methods have been renamed or removed without documentation updates.',
'## Description\n\nThe authentication flow has a race condition that can cause token refresh failures.\n\n## Steps to Reproduce\n\n1. Log in with valid credentials\n2. Wait for token to expire\n3. Make multiple concurrent API calls\n\n## Expected Behavior\n\nAll requests should use the refreshed token.\n\n## Actual Behavior\n\nSome requests fail with 401 Unauthorized due to stale tokens.',
'## Description\n\nThe error handling middleware does not catch all exception types.\n\n## Steps to Reproduce\n\n1. Trigger a database connection error\n2. Observe the error response\n\n## Expected Behavior\n\nA structured 500 error response should be returned.\n\n## Actual Behavior\n\nRaw error details are exposed in the response.',
'## Description\n\nThe caching layer does not invalidate properly when data changes.\n\n## Steps to Reproduce\n\n1. Fetch a resource (gets cached)\n2. Update the resource\n3. Fetch again\n\n## Expected Behavior\n\nThe updated resource should be returned.\n\n## Actual Behavior\n\nThe stale cached version is returned until TTL expires.',
'## Description\n\nThe logging system does not include request context in log entries.\n\n## Steps to Reproduce\n\n1. Enable debug logging\n2. Make several concurrent requests\n3. Check the logs\n\n## Expected Behavior\n\nEach log entry should include request ID and user context.\n\n## Actual Behavior\n\nLog entries are mixed and hard to correlate with specific requests.',
'## Description\n\nThe file upload feature does not validate file types properly.\n\n## Steps to Reproduce\n\n1. Upload a file with an incorrect extension\n2. Observe the server response\n\n## Expected Behavior\n\nThe server should reject unsupported file types.\n\n## Actual Behavior\n\nThe file is accepted but causes processing errors later.',
];
}
get _reviewComments() {
return [
'Looks good! Just one suggestion: add error handling for the edge case where the input is null.',
'Nice work on the refactoring. The code is much cleaner now.',
'I noticed a potential race condition here. Should we add a lock?',
'Great implementation! Consider adding unit tests for the new functions.',
'This approach works, but we might want to use a more efficient algorithm for large inputs.',
'LGTM! Ready to merge.',
'Small nit: the variable name could be more descriptive.',
'Good catch on the bug fix. The test coverage looks solid.',
'I\'d suggest extracting this logic into a separate function for reusability.',
'The error messages here are really helpful for debugging. Nice touch!',
'Should we add a timeout to this external API call?',
'The documentation update is thorough. Thanks for keeping it current.',
'I wonder if we could simplify this with a reduce instead of the for loop.',
'The performance improvement is significant. Great optimization!',
'Could we add a comment explaining why this workaround is needed?',
'The test cases cover all the edge cases I could think of. Well done.',
'I\'m not sure about hardcoding this value. Maybe a config option?',
'The code structure is much better now. Much easier to follow.',
'Should we consider adding a retry mechanism here?',
'The type definitions are comprehensive. This will save us from bugs later.',
'I like the approach. One thing: we should probably validate the input earlier.',
'The commit history is clean and well-organized. Great job.',
'This change makes the API much more intuitive to use.',
'Should we add a deprecation warning for the old method?',
'The benchmark results are impressive. Nice performance win.',
];
}
get _issueComments() {
return [
'I can reproduce this. Looking into it now.',
'Good find! This has been causing issues in production too.',
'I think the root cause is in the validation layer. Let me check.',
'Thanks for the detailed report. This helps a lot.',
'I\'ve seen this before. It\'s related to the caching issue we fixed last week.',
'Can you share the exact version you\'re running? This might be version-specific.',
'I\'ll take a look at this tomorrow. Seems like a priority fix.',
'This is on my radar. Working on a fix this sprint.',
'Interesting. Does this happen consistently or intermittently?',
'I suspect this is related to the recent dependency update.',
'Let me know if you need help reproducing this. I have a test environment ready.',
'This affects our SLA. Marking as high priority.',
'I\'ve added a workaround for now. Will implement a proper fix next week.',
'The fix is in progress. Should have a PR ready by end of day.',
'This is a known limitation. We\'re planning to address it in the next major release.',
];
}
get _reviewDecisions() {
return ['approve', 'approve', 'approve', 'approve', 'comment', 'comment', 'request_changes'];
}
get _codeSnippets() {
return [
'```javascript\nfunction processData(items) {\n return items\n .filter(item => item != null)\n .map(item => ({ ...item, processed: true }));\n}\n```',
'```javascript\nclass Cache {\n constructor(ttl = 300) {\n this.store = new Map();\n this.ttl = ttl;\n }\n\n get(key) {\n const entry = this.store.get(key);\n if (!entry) return null;\n if (Date.now() - entry.timestamp > this.ttl * 1000) {\n this.store.delete(key);\n return null;\n }\n return entry.value;\n }\n\n set(key, value) {\n this.store.set(key, { value, timestamp: Date.now() });\n }\n}\n```',
'```javascript\nasync function fetchWithRetry(url, options = {}, maxRetries = 3) {\n for (let i = 0; i < maxRetries; i++) {\n try {\n const response = await fetch(url, options);\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return response;\n } catch (error) {\n if (i === maxRetries - 1) throw error;\n await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));\n }\n }\n}\n```',
'```javascript\nfunction validateInput(schema, data) {\n const errors = [];\n for (const [key, rules] of Object.entries(schema)) {\n if (rules.required && (data[key] === undefined || data[key] === null)) {\n errors.push(`${key} is required`);\n }\n if (rules.type && data[key] !== undefined && typeof data[key] !== rules.type) {\n errors.push(`${key} must be of type ${rules.type}`);\n }\n }\n return { valid: errors.length === 0, errors };\n}\n```',
'```javascript\nclass EventEmitter {\n constructor() {\n this.listeners = new Map();\n }\n\n on(event, callback) {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, []);\n }\n this.listeners.get(event).push(callback);\n return () => this.off(event, callback);\n }\n\n off(event, callback) {\n const callbacks = this.listeners.get(event) || [];\n this.listeners.set(event, callbacks.filter(cb => cb !== callback));\n }\n\n emit(event, ...args) {\n const callbacks = this.listeners.get(event) || [];\n callbacks.forEach(cb => cb(...args));\n }\n}\n```',
];
}
get _prDescriptions() {
return [
'## Summary\n\nThis PR implements the requested feature with proper error handling and tests.\n\n## Changes\n\n- Added new functionality\n- Updated existing code\n- Added tests\n\n## Testing\n\n- [x] Manual testing completed\n- [x] Unit tests pass',
'## Summary\n\nRefactors the data processing pipeline for better performance and maintainability.\n\n## Changes\n\n- Extracted common logic into utility functions\n- Added proper error boundaries\n- Improved test coverage to 85%\n\n## Performance\n\nProcessing time reduced by 40% for large datasets.',
'## Summary\n\nFixes the authentication race condition by implementing proper token synchronization.\n\n## Changes\n\n- Added token refresh lock\n- Implemented request queue during refresh\n- Added integration tests\n\n## Testing\n\nVerified with concurrent request simulation (50 simultaneous requests).',
];
}
get _clarifications() {
return [
'Can you share the exact error message you\'re seeing?',
'What version of the library are you using?',
'Does this happen on all environments or just production?',
'Can you provide a minimal reproduction case?',
'What browser/OS are you seeing this on?',
'Is this a regression from a previous version?',
];
}
get _defaultResponses() {
return [
'This looks reasonable. Let me review the implementation details.',
'Good approach. I\'ll take a closer look at the edge cases.',
'Thanks for the update. I\'ll review this shortly.',
'Makes sense. Let me check if there are any dependencies affected.',
'I\'ll add some thoughts after I test this locally.',
];
}
async generateCode(context, task) {
const prompt = `You are a senior software developer writing production-quality JavaScript code.
Context: ${context}
Task: ${task}
Requirements:
- Write clean, well-structured code
- Include JSDoc comments for public functions
- Handle edge cases and errors gracefully
- Use modern JavaScript features (ES2022+)
- Export functions properly
Return ONLY the code, no explanations.`;
return this.generate(prompt, { maxTokens: 2048, temperature: 0.6 });
}
async generateIssueTitle(projectContext) {
const prompt = `Generate a realistic GitHub issue title for a software project.
Current project context: ${projectContext}
Make it specific, actionable, and realistic. Examples:
- "Add input validation to user registration form"
- "Fix race condition in concurrent file processing"
- "Refactor database connection pooling for better performance"
Return ONLY the title, nothing else.`;
return (await this.generate(prompt, { maxTokens: 100, temperature: 0.8 })).trim();
}
async generateIssueBody(title, projectContext) {
const prompt = `Write a realistic GitHub issue body for the following issue:
Title: ${title}
Project context: ${projectContext}
Include:
- Problem description
- Steps to reproduce (if applicable)
- Expected vs actual behavior
- Environment details
Keep it concise and professional.`;
return this.generate(prompt, { maxTokens: 1024, temperature: 0.7 });
}
async generatePRDescription(branchName, changes) {
const prompt = `Write a professional GitHub Pull Request description.
Branch: ${branchName}
Changes summary: ${changes}
Include:
- What this PR does
- Why these changes are needed
- Testing done
- Any breaking changes
Use markdown formatting.`;
return this.generate(prompt, { maxTokens: 1024, temperature: 0.7 });
}
async generateReviewComment(code, fileName) {
const prompt = `You are reviewing code in a GitHub Pull Request.
File: ${fileName}
Code:
\`\`\`javascript
${code}
\`\`\`
Write a realistic, professional code review comment. Be specific about the code.
Possible angles:
- Suggest improvements
- Point out potential bugs
- Ask clarifying questions
- Praise good patterns
Keep it to 1-3 sentences. Be natural, not robotic.`;
return this.generate(prompt, { maxTokens: 256, temperature: 0.8 });
}
async generateCommitMessage(changes) {
const prompt = `Write a concise, professional git commit message for these changes:
${changes}
Follow conventional commits format when appropriate.
Examples: "feat: add user authentication middleware"
"fix: handle null values in data processor"
"refactor: simplify database query logic"
Return ONLY the commit message, nothing else.`;
return (await this.generate(prompt, { maxTokens: 100, temperature: 0.7 })).trim();
}
async generateReviewDecision(prTitle, prBody) {
const prompt = `You are reviewing a Pull Request.
Title: ${prTitle}
Body: ${prBody?.slice(0, 300) || 'No description provided'}
Decide on the review outcome. Return ONLY one of these words:
- "approve" if the code looks good and ready to merge
- "comment" if you have minor suggestions but overall it's fine
- "request_changes" if there are significant issues
Return ONLY the decision word, nothing else.`;
return (await this.generate(prompt, { maxTokens: 20, temperature: 0.8 })).trim().toLowerCase();
}
getStats() {
return {
totalRequests: this._requestCount,
totalErrors: this._errorCount,
errorRate: this._requestCount > 0 ? (this._errorCount / this._requestCount * 100).toFixed(1) : 0,
lastError: this._lastError?.message || null,
provider: this.provider,
sessionDuration: Date.now() - this._sessionStart,
};
}
}
export default AIProvider;