From 4c8700d253fac51c28b7c22a64dd66f29635f34f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:37:48 +0000 Subject: [PATCH 1/3] Initial plan From 8c031fd09042fc80f13650946885deb2ca91bf51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:53:34 +0000 Subject: [PATCH 2/3] Fix CI-blocking failures: wildcard route, Express 5 req.query, BraceCase corruption Co-authored-by: MightyPrytanis <219587333+MightyPrytanis@users.noreply.github.com> --- Cyrano/src/http-bridge.ts | 4 ++-- Cyrano/src/middleware/security.ts | 7 ++++++- Cyrano/tests/routes/onboarding.test.ts | 3 --- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cyrano/src/http-bridge.ts b/Cyrano/src/http-bridge.ts index 4e3d9223..10f5d723 100644 --- a/Cyrano/src/http-bridge.ts +++ b/Cyrano/src/http-bridge.ts @@ -504,7 +504,7 @@ async function loadTool(toolName: string, loadDependencies: boolean = true): Pro // Get tool config const toolConfig = toolImportMap[toolName]; if (!toolConfig) { - throw new Error(`Unknown tool: ${toolName}`); + throw new Error(`Tool not found: ${toolName}`); } // Initialize metadata @@ -1242,7 +1242,7 @@ app.get('/mcp/tools/info', async (req, res) => { }); // Catch-all for unknown /mcp routes - always return JSON -app.all('/mcp/*', (req, res) => { +app.all('/mcp/*path', (req, res) => { res.status(404).json({ isError: true, content: [{ text: `Unknown MCP route: ${req.path}` }] }); }); diff --git a/Cyrano/src/middleware/security.ts b/Cyrano/src/middleware/security.ts index 4efb3c49..cfa875d9 100644 --- a/Cyrano/src/middleware/security.ts +++ b/Cyrano/src/middleware/security.ts @@ -432,7 +432,12 @@ export function sanitizeInputs(req: Request, res: Response, next: NextFunction) sanitizedQuery[key] = value; } } - req.query = sanitizedQuery; + Object.defineProperty(req, 'query', { + value: sanitizedQuery, + writable: true, + enumerable: true, + configurable: true, + }); } // Sanitize body parameters (only strings) diff --git a/Cyrano/tests/routes/onboarding.test.ts b/Cyrano/tests/routes/onboarding.test.ts index ecdbc68f..939e8abd 100644 --- a/Cyrano/tests/routes/onboarding.test.ts +++ b/Cyrano/tests/routes/onboarding.test.ts @@ -499,6 +499,3 @@ describe('Onboarding API Integration Tests', () => { }); }); }); -} -) -) From 907c96d53e04e744ac3c068f97e42b8be65bb628 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:33:19 +0000 Subject: [PATCH 3/3] Fix mock tools: add real AI integration with proper demo-mode gating and disclaimers Co-authored-by: MightyPrytanis <219587333+MightyPrytanis@users.noreply.github.com> --- Cyrano/src/tools/contract-comparator.ts | 153 ++++++++++++++++++++++-- Cyrano/src/tools/document-analyzer.ts | 2 +- Cyrano/src/tools/legal-reviewer.ts | 5 +- Cyrano/src/tools/quality-assessor.ts | 98 ++++++++++++++- 4 files changed, 240 insertions(+), 18 deletions(-) diff --git a/Cyrano/src/tools/contract-comparator.ts b/Cyrano/src/tools/contract-comparator.ts index 3c357277..e6ab7bb8 100644 --- a/Cyrano/src/tools/contract-comparator.ts +++ b/Cyrano/src/tools/contract-comparator.ts @@ -6,6 +6,10 @@ import { BaseTool } from './base-tool.js'; import { z } from 'zod'; +import { aiService, AIProvider } from '../services/ai-service.js'; +import { apiValidator } from '../utils/api-validator.js'; +import { aiProviderSelector } from '../services/ai-provider-selector.js'; +import { isDemoModeEnabled, markAsDemo } from '../utils/demo-mode.js'; /** * Escape special regex characters in a string @@ -19,6 +23,7 @@ const ContractComparatorSchema = z.object({ document2_text: z.string().describe('Second contract/agreement to compare'), comparison_type: z.enum(['comprehensive', 'clauses', 'terms', 'structure', 'risk_analysis', 'obligations', 'choice_of_law', 'financial', 'rights_remedies', 'term_termination']).default('comprehensive'), focus_areas: z.array(z.string()).optional().describe('Specific areas to focus comparison on'), + ai_provider: z.enum(['openai', 'anthropic', 'perplexity', 'google', 'xai', 'deepseek', 'auto']).optional().default('auto').describe('AI provider to use (default: auto-select based on task and performance)'), }); export const contractComparator = new (class extends BaseTool { @@ -48,6 +53,12 @@ export const contractComparator = new (class extends BaseTool { items: { type: 'string' }, description: 'Specific areas to focus comparison on', }, + ai_provider: { + type: 'string', + enum: ['openai', 'anthropic', 'perplexity', 'google', 'xai', 'deepseek', 'auto'], + default: 'auto', + description: 'AI provider to use (default: auto-select based on task and performance)', + }, }, required: ['document1_text', 'document2_text'], }, @@ -56,26 +67,127 @@ export const contractComparator = new (class extends BaseTool { async execute(args: any) { try { - const { document1_text, document2_text, comparison_type, focus_areas } = ContractComparatorSchema.parse(args); + const { document1_text, document2_text, comparison_type, focus_areas, ai_provider } = ContractComparatorSchema.parse(args); + + // Demo mode: return structural (regex) comparison with disclaimer (opt-in only via DEMO_MODE=true) + if (isDemoModeEnabled()) { + const structuralComparison = this.performComparison(document1_text, document2_text, comparison_type, focus_areas); + const demoComparison = markAsDemo(structuralComparison, 'Contract Comparator'); + return this.createSuccessResult(JSON.stringify(demoComparison, null, 2), { + comparison_type, + document1_word_count: document1_text.split(' ').length, + document2_word_count: document2_text.split(' ').length, + focus_areas: focus_areas || [], + }); + } - const comparison = this.performComparison(document1_text, document2_text, comparison_type, focus_areas); + if (!apiValidator.hasAnyValidProviders()) { + return this.createErrorResult( + 'No AI providers configured. Contract comparison requires an AI provider. Configure API keys, or set DEMO_MODE=true to enable demo mode.' + ); + } - // Use formatted output for comprehensive analysis - const output = comparison_type === 'comprehensive' - ? this.formatEnhancedAnalysis(comparison) - : JSON.stringify(comparison, null, 2); + let provider: AIProvider; + if (ai_provider === 'auto' || !ai_provider) { + provider = aiProviderSelector.getProviderForTask({ + taskType: 'legal_review', + complexity: comparison_type === 'comprehensive' ? 'high' : 'medium', + requiresSafety: true, + preferredProvider: 'auto', + balanceQualitySpeed: 'quality', + }); + } else { + const validation = apiValidator.validateProvider(ai_provider as AIProvider); + if (!validation.valid) { + return this.createErrorResult( + `Selected AI provider ${ai_provider} is not configured: ${validation.error}` + ); + } + provider = ai_provider as AIProvider; + } - return this.createSuccessResult(output, { - comparison_type, - document1_word_count: document1_text.split(' ').length, - document2_word_count: document2_text.split(' ').length, - focus_areas: focus_areas || [], - }); + try { + const prompt = this.buildComparisonPrompt(document1_text, document2_text, comparison_type, focus_areas); + const aiResponse = await aiService.call(provider, prompt, { + systemPrompt: 'You are an expert contract attorney. Provide precise, actionable legal analysis when comparing contracts.', + maxTokens: 4000, + temperature: 0.3, + metadata: { + toolName: 'contract_comparator', + actionType: 'content_generation', + }, + }); + + const structuralComparison = this.performComparison(document1_text, document2_text, comparison_type, focus_areas); + const output = JSON.stringify({ + ...structuralComparison, + ai_analysis: { + provider, + analysis: aiResponse, + timestamp: new Date().toISOString(), + }, + }, null, 2); + + return this.createSuccessResult(output, { + comparison_type, + document1_word_count: document1_text.split(' ').length, + document2_word_count: document2_text.split(' ').length, + focus_areas: focus_areas || [], + ai_provider: provider, + }); + } catch (aiError) { + return this.createErrorResult(`AI contract comparison failed: ${aiError instanceof Error ? aiError.message : String(aiError)}`); + } } catch (error) { return this.createErrorResult(`Legal comparison failed: ${error instanceof Error ? error.message : String(error)}`); } } + public buildComparisonPrompt(doc1: string, doc2: string, comparisonType: 'comprehensive' | 'clauses' | 'terms' | 'structure' | 'risk_analysis' | 'obligations' | 'choice_of_law' | 'financial' | 'rights_remedies' | 'term_termination', focusAreas?: string[]): string { + let prompt = `Compare the following two contracts and provide a detailed legal analysis:\n\n`; + prompt += `CONTRACT 1:\n${doc1}\n\n`; + prompt += `CONTRACT 2:\n${doc2}\n\n`; + + switch (comparisonType) { + case 'comprehensive': + prompt += `Provide a comprehensive comparison including: +- Key differences in obligations, rights, and remedies +- Risk profile differences between the two contracts +- Choice of law and jurisdiction differences +- Financial term differences +- Term and termination differences +- Which contract is more favorable and why +- Specific recommendations for the reviewing attorney`; + break; + case 'risk_analysis': + prompt += 'Focus on risk analysis: identify which contract exposes the client to greater risk and explain why.'; + break; + case 'obligations': + prompt += 'Focus on obligations: compare the duties and responsibilities assigned to each party in both contracts.'; + break; + case 'choice_of_law': + prompt += 'Focus on choice of law and jurisdiction clauses, and the legal implications of any differences.'; + break; + case 'financial': + prompt += 'Focus on financial terms: payment amounts, schedules, penalties, and any financial obligations.'; + break; + case 'rights_remedies': + prompt += 'Focus on rights and remedies: what recourse each party has upon breach or dispute.'; + break; + case 'term_termination': + prompt += 'Focus on term and termination: contract duration, renewal options, and termination conditions.'; + break; + default: + prompt += `Focus the comparison on: ${comparisonType}`; + } + + if (focusAreas && focusAreas.length > 0) { + prompt += `\n\nAdditionally focus on these specific areas: ${focusAreas.join(', ')}`; + } + + return prompt; + } + public performComparison(doc1: string, doc2: string, comparisonType: string, focusAreas?: string[]) { const comparison: any = { metadata: { @@ -404,7 +516,7 @@ export const contractComparator = new (class extends BaseTool { result.compliance = this.compareComplianceElements(doc1, doc2); break; default: - result[area] = `Focused comparison for ${area} not implemented`; + result[area] = this.compareByKeyword(doc1, doc2, area); } }); @@ -436,6 +548,21 @@ export const contractComparator = new (class extends BaseTool { }; } + public compareByKeyword(doc1: string, doc2: string, area: string): { area: string; doc1_mentions: number; doc2_mentions: number; difference: number; note?: string } { + const keyword = escapeRegExp(area.toLowerCase()); + // Input sanitized via escapeRegExp() to prevent regex injection + const count1 = (doc1.toLowerCase().match(new RegExp(keyword, 'g')) || []).length; // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp + // Input sanitized via escapeRegExp() to prevent regex injection + const count2 = (doc2.toLowerCase().match(new RegExp(keyword, 'g')) || []).length; // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp + return { + area, + doc1_mentions: count1, + doc2_mentions: count2, + difference: count1 - count2, + note: count1 === 0 && count2 === 0 ? `"${area}" not found in either document` : undefined, + }; + } + public extractParties(text: string): string[] { const partyPattern = /(?:party|parties|between|and)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/gi; const matches = text.match(partyPattern) || []; diff --git a/Cyrano/src/tools/document-analyzer.ts b/Cyrano/src/tools/document-analyzer.ts index ca49c029..4087de2d 100644 --- a/Cyrano/src/tools/document-analyzer.ts +++ b/Cyrano/src/tools/document-analyzer.ts @@ -268,7 +268,7 @@ export const documentAnalyzer = new (class extends BaseTool { result.compliance = await this.analyzeWithAI(text, 'compliance', provider); break; default: - result[area] = `Analysis for ${area} not implemented`; + result[area] = await this.analyzeWithAI(text, area, provider); } } diff --git a/Cyrano/src/tools/legal-reviewer.ts b/Cyrano/src/tools/legal-reviewer.ts index 2473a8c4..33b25c66 100644 --- a/Cyrano/src/tools/legal-reviewer.ts +++ b/Cyrano/src/tools/legal-reviewer.ts @@ -659,7 +659,10 @@ export const legalReviewer = new (class extends BaseTool { }; return areaSpecificChecks[practiceArea.toLowerCase()] || { - message: `Practice area specific review for ${practiceArea} not implemented` + practice_area: practiceArea, + keywords_found: (document.toLowerCase().match(new RegExp(practiceArea.replace(/_/g, '\\s*'), 'g')) || []).length, + general_legal_elements: this.identifyLegalRequirements(document), + note: `Structural analysis for "${practiceArea}" — for full AI-powered review, use the main execute() path`, }; } diff --git a/Cyrano/src/tools/quality-assessor.ts b/Cyrano/src/tools/quality-assessor.ts index c5ed1240..fd99857b 100644 --- a/Cyrano/src/tools/quality-assessor.ts +++ b/Cyrano/src/tools/quality-assessor.ts @@ -6,11 +6,16 @@ import { BaseTool } from './base-tool.js'; import { z } from 'zod'; +import { aiService, AIProvider } from '../services/ai-service.js'; +import { apiValidator } from '../utils/api-validator.js'; +import { aiProviderSelector } from '../services/ai-provider-selector.js'; +import { isDemoModeEnabled, markAsDemo } from '../utils/demo-mode.js'; const QualityAssessorSchema = z.object({ document_text: z.string().describe('The document to assess for quality'), assessment_criteria: z.array(z.string()).optional().describe('Specific quality criteria to assess'), quality_standard: z.enum(['basic', 'professional', 'excellent']).default('professional'), + ai_provider: z.enum(['openai', 'anthropic', 'perplexity', 'google', 'xai', 'deepseek', 'auto']).optional().default('auto').describe('AI provider to use (default: auto-select)'), }); export const qualityAssessor = new (class extends BaseTool { @@ -36,6 +41,12 @@ export const qualityAssessor = new (class extends BaseTool { default: 'professional', description: 'Quality standard to measure against', }, + ai_provider: { + type: 'string', + enum: ['openai', 'anthropic', 'perplexity', 'google', 'xai', 'deepseek', 'auto'], + default: 'auto', + description: 'AI provider to use (default: auto-select)', + }, }, required: ['document_text'], }, @@ -44,14 +55,95 @@ export const qualityAssessor = new (class extends BaseTool { async execute(args: any) { try { - const { document_text, assessment_criteria, quality_standard } = QualityAssessorSchema.parse(args); - const assessment = this.assessQuality(document_text, assessment_criteria, quality_standard); - return this.createSuccessResult(JSON.stringify(assessment, null, 2)); + const { document_text, assessment_criteria, quality_standard, ai_provider } = QualityAssessorSchema.parse(args); + + // Demo mode: return placeholder scores with disclaimer (opt-in only via DEMO_MODE=true) + if (isDemoModeEnabled()) { + const demoAssessment = markAsDemo( + this.assessQuality(document_text, assessment_criteria, quality_standard), + 'Quality Assessor' + ); + return this.createSuccessResult(JSON.stringify(demoAssessment, null, 2)); + } + + if (!apiValidator.hasAnyValidProviders()) { + return this.createErrorResult( + 'No AI providers configured. Quality assessment requires an AI provider. Configure API keys, or set DEMO_MODE=true to enable demo mode.' + ); + } + + let provider: AIProvider; + if (ai_provider === 'auto' || !ai_provider) { + provider = aiProviderSelector.getProviderForTask({ + taskType: 'analysis', + complexity: quality_standard === 'excellent' ? 'high' : 'medium', + requiresSafety: false, + preferredProvider: 'auto', + }); + } else { + const validation = apiValidator.validateProvider(ai_provider as AIProvider); + if (!validation.valid) { + return this.createErrorResult( + `Selected AI provider ${ai_provider} is not configured: ${validation.error}` + ); + } + provider = ai_provider as AIProvider; + } + + try { + const prompt = this.buildAssessmentPrompt(document_text, assessment_criteria, quality_standard); + const aiResponse = await aiService.call(provider, prompt, { + systemPrompt: 'You are an expert document quality analyst. Provide objective, evidence-based quality assessment with specific scores and actionable recommendations.', + maxTokens: 2000, + temperature: 0.3, + metadata: { + toolName: 'quality_assessor', + actionType: 'content_generation', + }, + }); + + const structuralAssessment = this.assessQuality(document_text, assessment_criteria, quality_standard); + const assessment = { + ...structuralAssessment, + ai_analysis: { + provider, + analysis: aiResponse, + timestamp: new Date().toISOString(), + }, + }; + + return this.createSuccessResult(JSON.stringify(assessment, null, 2), { + quality_standard, + ai_provider: provider, + document_length: document_text.length, + }); + } catch (aiError) { + return this.createErrorResult(`AI quality assessment failed: ${aiError instanceof Error ? aiError.message : String(aiError)}`); + } } catch (error) { return this.createErrorResult(`Quality assessment failed: ${error instanceof Error ? error.message : String(error)}`); } } + public buildAssessmentPrompt(document: string, criteria?: string[], standard: 'basic' | 'professional' | 'excellent' = 'professional'): string { + let prompt = `Assess the quality of the following document to a ${standard} standard:\n\n${document}\n\n`; + + prompt += `Provide a structured quality assessment including: +- Overall quality score (0-10) with justification +- Writing quality: clarity, grammar, style, and professional tone +- Structure quality: organization, flow, and completeness +- Content quality: accuracy, relevance, depth, and coherence +- Specific strengths +- Specific weaknesses +- Actionable recommendations for improvement`; + + if (criteria && criteria.length > 0) { + prompt += `\n\nPay special attention to these criteria: ${criteria.join(', ')}`; + } + + return prompt; + } + public assessQuality(document: string, criteria?: string[], standard: string = 'professional') { return { metadata: {