diff --git a/apps/gateway/src/agent/__tests__/provider.service.test.ts b/apps/gateway/src/agent/__tests__/provider.service.test.ts index 142dfef..0fb85c2 100644 --- a/apps/gateway/src/agent/__tests__/provider.service.test.ts +++ b/apps/gateway/src/agent/__tests__/provider.service.test.ts @@ -35,7 +35,7 @@ describe('ProviderService', () => { }); it('skips API-key providers when env vars are missing (no models become available)', async () => { - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); // Pi's built-in registry may include model definitions for all providers, but @@ -57,7 +57,7 @@ describe('ProviderService', () => { it('registers Anthropic provider with correct models when ANTHROPIC_API_KEY is set', async () => { process.env['ANTHROPIC_API_KEY'] = 'test-anthropic'; - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); const providers = service.listProviders(); @@ -65,42 +65,41 @@ describe('ProviderService', () => { expect(anthropic).toBeDefined(); expect(anthropic!.available).toBe(true); expect(anthropic!.models.map((m) => m.id)).toEqual([ - 'claude-sonnet-4-6', 'claude-opus-4-6', + 'claude-sonnet-4-6', 'claude-haiku-4-5', ]); - // contextWindow override from Pi built-in (200000) + // All Anthropic models have 200k context window for (const m of anthropic!.models) { expect(m.contextWindow).toBe(200000); - // maxTokens capped at 8192 per task spec - expect(m.maxTokens).toBe(8192); } }); it('registers OpenAI provider with correct models when OPENAI_API_KEY is set', async () => { process.env['OPENAI_API_KEY'] = 'test-openai'; - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); const providers = service.listProviders(); const openai = providers.find((p) => p.id === 'openai'); expect(openai).toBeDefined(); expect(openai!.available).toBe(true); - expect(openai!.models.map((m) => m.id)).toEqual(['gpt-4o', 'gpt-4o-mini', 'o3-mini']); + expect(openai!.models.map((m) => m.id)).toEqual(['codex-gpt-5-4']); }); it('registers Z.ai provider with correct models when ZAI_API_KEY is set', async () => { process.env['ZAI_API_KEY'] = 'test-zai'; - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); const providers = service.listProviders(); const zai = providers.find((p) => p.id === 'zai'); expect(zai).toBeDefined(); expect(zai!.available).toBe(true); - expect(zai!.models.map((m) => m.id)).toEqual(['glm-4.5', 'glm-4.5-air', 'glm-4.5-flash']); + // Pi's registry may include additional glm variants; verify our registered model is present + expect(zai!.models.map((m) => m.id)).toContain('glm-5'); }); it('registers all three providers when all keys are set', async () => { @@ -108,7 +107,7 @@ describe('ProviderService', () => { process.env['OPENAI_API_KEY'] = 'test-openai'; process.env['ZAI_API_KEY'] = 'test-zai'; - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); const providerIds = service.listProviders().map((p) => p.id); @@ -120,7 +119,7 @@ describe('ProviderService', () => { it('can find registered Anthropic models by provider+id', async () => { process.env['ANTHROPIC_API_KEY'] = 'test-anthropic'; - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); const sonnet = service.findModel('anthropic', 'claude-sonnet-4-6'); @@ -132,7 +131,7 @@ describe('ProviderService', () => { it('can find registered Z.ai models by provider+id', async () => { process.env['ZAI_API_KEY'] = 'test-zai'; - const service = new ProviderService(); + const service = new ProviderService(null); await service.onModuleInit(); const glm = service.findModel('zai', 'glm-4.5'); diff --git a/apps/gateway/src/agent/routing/default-rules.ts b/apps/gateway/src/agent/routing/default-rules.ts new file mode 100644 index 0000000..63a6f67 --- /dev/null +++ b/apps/gateway/src/agent/routing/default-rules.ts @@ -0,0 +1,138 @@ +import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common'; +import { routingRules, type Db, sql } from '@mosaic/db'; +import { DB } from '../../database/database.module.js'; +import type { RoutingCondition, RoutingAction } from './routing.types.js'; + +/** Seed-time routing rule descriptor */ +interface RoutingRuleSeed { + name: string; + priority: number; + conditions: RoutingCondition[]; + action: RoutingAction; +} + +export const DEFAULT_ROUTING_RULES: RoutingRuleSeed[] = [ + { + name: 'Complex coding → Opus', + priority: 1, + conditions: [ + { field: 'taskType', operator: 'eq', value: 'coding' }, + { field: 'complexity', operator: 'eq', value: 'complex' }, + ], + action: { provider: 'anthropic', model: 'claude-opus-4-6' }, + }, + { + name: 'Moderate coding → Sonnet', + priority: 2, + conditions: [ + { field: 'taskType', operator: 'eq', value: 'coding' }, + { field: 'complexity', operator: 'eq', value: 'moderate' }, + ], + action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, + }, + { + name: 'Simple coding → Codex', + priority: 3, + conditions: [ + { field: 'taskType', operator: 'eq', value: 'coding' }, + { field: 'complexity', operator: 'eq', value: 'simple' }, + ], + action: { provider: 'openai', model: 'codex-gpt-5-4' }, + }, + { + name: 'Research → Codex', + priority: 4, + conditions: [{ field: 'taskType', operator: 'eq', value: 'research' }], + action: { provider: 'openai', model: 'codex-gpt-5-4' }, + }, + { + name: 'Summarization → GLM-5', + priority: 5, + conditions: [{ field: 'taskType', operator: 'eq', value: 'summarization' }], + action: { provider: 'zai', model: 'glm-5' }, + }, + { + name: 'Analysis with reasoning → Opus', + priority: 6, + conditions: [ + { field: 'taskType', operator: 'eq', value: 'analysis' }, + { field: 'requiredCapabilities', operator: 'includes', value: 'reasoning' }, + ], + action: { provider: 'anthropic', model: 'claude-opus-4-6' }, + }, + { + name: 'Conversation → Sonnet', + priority: 7, + conditions: [{ field: 'taskType', operator: 'eq', value: 'conversation' }], + action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, + }, + { + name: 'Creative → Sonnet', + priority: 8, + conditions: [{ field: 'taskType', operator: 'eq', value: 'creative' }], + action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, + }, + { + name: 'Cheap/general → Haiku', + priority: 9, + conditions: [{ field: 'costTier', operator: 'eq', value: 'cheap' }], + action: { provider: 'anthropic', model: 'claude-haiku-4-5' }, + }, + { + name: 'Fallback → Sonnet', + priority: 10, + conditions: [], + action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, + }, + { + name: 'Offline → Ollama', + priority: 99, + conditions: [{ field: 'costTier', operator: 'eq', value: 'local' }], + action: { provider: 'ollama', model: 'llama3.2' }, + }, +]; + +@Injectable() +export class DefaultRoutingRulesSeed implements OnModuleInit { + private readonly logger = new Logger(DefaultRoutingRulesSeed.name); + + constructor(@Inject(DB) private readonly db: Db) {} + + async onModuleInit(): Promise { + await this.seedDefaultRules(); + } + + /** + * Insert default routing rules into the database if the table is empty. + * Skips seeding if any system-scoped rules already exist. + */ + async seedDefaultRules(): Promise { + const rows = await this.db + .select({ count: sql`count(*)::int` }) + .from(routingRules) + .where(sql`scope = 'system'`); + + const count = rows[0]?.count ?? 0; + if (count > 0) { + this.logger.debug( + `Skipping default routing rules seed — ${count} system rule(s) already exist`, + ); + return; + } + + this.logger.log(`Seeding ${DEFAULT_ROUTING_RULES.length} default routing rules`); + + await this.db.insert(routingRules).values( + DEFAULT_ROUTING_RULES.map((rule) => ({ + name: rule.name, + priority: rule.priority, + scope: 'system' as const, + conditions: rule.conditions as unknown as Record[], + action: rule.action as unknown as Record, + enabled: true, + })), + ); + + this.logger.log('Default routing rules seeded successfully'); + } +} diff --git a/apps/gateway/src/agent/routing/task-classifier.test.ts b/apps/gateway/src/agent/routing/task-classifier.test.ts new file mode 100644 index 0000000..0530add --- /dev/null +++ b/apps/gateway/src/agent/routing/task-classifier.test.ts @@ -0,0 +1,366 @@ +import { describe, it, expect } from 'vitest'; +import { classifyTask } from './task-classifier.js'; + +// ─── Task Type Detection ────────────────────────────────────────────────────── + +describe('classifyTask — taskType', () => { + it('detects coding from "code" keyword', () => { + expect(classifyTask('Can you write some code for me?').taskType).toBe('coding'); + }); + + it('detects coding from "implement" keyword', () => { + expect(classifyTask('Implement a binary search algorithm').taskType).toBe('coding'); + }); + + it('detects coding from "function" keyword', () => { + expect(classifyTask('Write a function that reverses a string').taskType).toBe('coding'); + }); + + it('detects coding from "debug" keyword', () => { + expect(classifyTask('Help me debug this error').taskType).toBe('coding'); + }); + + it('detects coding from "fix" keyword', () => { + expect(classifyTask('fix the broken test').taskType).toBe('coding'); + }); + + it('detects coding from "refactor" keyword', () => { + expect(classifyTask('Please refactor this module').taskType).toBe('coding'); + }); + + it('detects coding from "typescript" keyword', () => { + expect(classifyTask('How do I use generics in TypeScript?').taskType).toBe('coding'); + }); + + it('detects coding from "javascript" keyword', () => { + expect(classifyTask('JavaScript promises explained').taskType).toBe('coding'); + }); + + it('detects coding from "python" keyword', () => { + expect(classifyTask('Write a Python script to parse CSV').taskType).toBe('coding'); + }); + + it('detects coding from "SQL" keyword', () => { + expect(classifyTask('Write a SQL query to join these tables').taskType).toBe('coding'); + }); + + it('detects coding from "API" keyword', () => { + expect(classifyTask('Design an API for user management').taskType).toBe('coding'); + }); + + it('detects coding from "endpoint" keyword', () => { + expect(classifyTask('Add a new endpoint for user profiles').taskType).toBe('coding'); + }); + + it('detects coding from "class" keyword', () => { + expect(classifyTask('Create a class for handling payments').taskType).toBe('coding'); + }); + + it('detects coding from "method" keyword', () => { + expect(classifyTask('Add a method to validate emails').taskType).toBe('coding'); + }); + + it('detects coding from inline backtick code', () => { + expect(classifyTask('What does `Array.prototype.reduce` do?').taskType).toBe('coding'); + }); + + it('detects summarization from "summarize"', () => { + expect(classifyTask('Please summarize this document').taskType).toBe('summarization'); + }); + + it('detects summarization from "summary"', () => { + expect(classifyTask('Give me a summary of the meeting').taskType).toBe('summarization'); + }); + + it('detects summarization from "tldr"', () => { + expect(classifyTask('TLDR this article for me').taskType).toBe('summarization'); + }); + + it('detects summarization from "condense"', () => { + expect(classifyTask('Condense this into 3 bullet points').taskType).toBe('summarization'); + }); + + it('detects summarization from "brief"', () => { + expect(classifyTask('Give me a brief overview of this topic').taskType).toBe('summarization'); + }); + + it('detects creative from "write"', () => { + expect(classifyTask('Write a short story about a dragon').taskType).toBe('creative'); + }); + + it('detects creative from "story"', () => { + expect(classifyTask('Tell me a story about space exploration').taskType).toBe('creative'); + }); + + it('detects creative from "poem"', () => { + expect(classifyTask('Write a poem about autumn').taskType).toBe('creative'); + }); + + it('detects creative from "generate"', () => { + expect(classifyTask('Generate some creative marketing copy').taskType).toBe('creative'); + }); + + it('detects creative from "create content"', () => { + expect(classifyTask('Help me create content for my website').taskType).toBe('creative'); + }); + + it('detects creative from "blog post"', () => { + expect(classifyTask('Write a blog post about productivity habits').taskType).toBe('creative'); + }); + + it('detects analysis from "analyze"', () => { + expect(classifyTask('Analyze the performance of this system').taskType).toBe('analysis'); + }); + + it('detects analysis from "review"', () => { + expect(classifyTask('Please review my pull request changes').taskType).toBe('analysis'); + }); + + it('detects analysis from "evaluate"', () => { + expect(classifyTask('Evaluate the pros and cons of this approach').taskType).toBe('analysis'); + }); + + it('detects analysis from "assess"', () => { + expect(classifyTask('Assess the security risks here').taskType).toBe('analysis'); + }); + + it('detects analysis from "audit"', () => { + expect(classifyTask('Audit this codebase for vulnerabilities').taskType).toBe('analysis'); + }); + + it('detects research from "research"', () => { + expect(classifyTask('Research the best state management libraries').taskType).toBe('research'); + }); + + it('detects research from "find"', () => { + expect(classifyTask('Find all open issues in our backlog').taskType).toBe('research'); + }); + + it('detects research from "search"', () => { + expect(classifyTask('Search for papers on transformer architectures').taskType).toBe( + 'research', + ); + }); + + it('detects research from "what is"', () => { + expect(classifyTask('What is the difference between REST and GraphQL?').taskType).toBe( + 'research', + ); + }); + + it('detects research from "explain"', () => { + expect(classifyTask('Explain how OAuth2 works').taskType).toBe('research'); + }); + + it('detects research from "how does"', () => { + expect(classifyTask('How does garbage collection work in V8?').taskType).toBe('research'); + }); + + it('detects research from "compare"', () => { + expect(classifyTask('Compare Postgres and MySQL for this use case').taskType).toBe('research'); + }); + + it('falls back to conversation with no strong signal', () => { + expect(classifyTask('Hello, how are you?').taskType).toBe('conversation'); + }); + + it('falls back to conversation for generic greetings', () => { + expect(classifyTask('Good morning!').taskType).toBe('conversation'); + }); + + // Priority: coding wins over research when both keywords present + it('coding takes priority over research', () => { + expect(classifyTask('find a code example for sorting').taskType).toBe('coding'); + }); + + // Priority: summarization wins over creative + it('summarization takes priority over creative', () => { + expect(classifyTask('write a summary of this article').taskType).toBe('summarization'); + }); +}); + +// ─── Complexity Estimation ──────────────────────────────────────────────────── + +describe('classifyTask — complexity', () => { + it('classifies short message as simple', () => { + expect(classifyTask('Fix typo').complexity).toBe('simple'); + }); + + it('classifies single question as simple', () => { + expect(classifyTask('What is a closure?').complexity).toBe('simple'); + }); + + it('classifies message > 500 chars as complex', () => { + const long = 'a'.repeat(501); + expect(classifyTask(long).complexity).toBe('complex'); + }); + + it('classifies message with "architecture" keyword as complex', () => { + expect( + classifyTask('Can you help me think through the architecture of this system?').complexity, + ).toBe('complex'); + }); + + it('classifies message with "design" keyword as complex', () => { + expect(classifyTask('Design a data model for this feature').complexity).toBe('complex'); + }); + + it('classifies message with "complex" keyword as complex', () => { + expect(classifyTask('This is a complex problem involving multiple services').complexity).toBe( + 'complex', + ); + }); + + it('classifies message with "system" keyword as complex', () => { + expect(classifyTask('Explain the whole system behavior').complexity).toBe('complex'); + }); + + it('classifies message with multiple code blocks as complex', () => { + const msg = '```\nconst a = 1;\n```\n\nAlso look at\n\n```\nconst b = 2;\n```'; + expect(classifyTask(msg).complexity).toBe('complex'); + }); + + it('classifies moderate-length message as moderate', () => { + const msg = + 'Please help me implement a small utility function that parses query strings. It should handle arrays and nested objects properly.'; + expect(classifyTask(msg).complexity).toBe('moderate'); + }); +}); + +// ─── Domain Detection ───────────────────────────────────────────────────────── + +describe('classifyTask — domain', () => { + it('detects frontend from "react"', () => { + expect(classifyTask('How do I use React hooks?').domain).toBe('frontend'); + }); + + it('detects frontend from "css"', () => { + expect(classifyTask('Fix the CSS layout issue').domain).toBe('frontend'); + }); + + it('detects frontend from "html"', () => { + expect(classifyTask('Add an HTML form element').domain).toBe('frontend'); + }); + + it('detects frontend from "component"', () => { + expect(classifyTask('Create a reusable component').domain).toBe('frontend'); + }); + + it('detects frontend from "UI"', () => { + expect(classifyTask('Update the UI spacing').domain).toBe('frontend'); + }); + + it('detects frontend from "tailwind"', () => { + expect(classifyTask('Style this button with Tailwind').domain).toBe('frontend'); + }); + + it('detects frontend from "next.js"', () => { + expect(classifyTask('Configure Next.js routing').domain).toBe('frontend'); + }); + + it('detects backend from "server"', () => { + expect(classifyTask('Set up the server to handle requests').domain).toBe('backend'); + }); + + it('detects backend from "database"', () => { + expect(classifyTask('Optimize this database query').domain).toBe('backend'); + }); + + it('detects backend from "endpoint"', () => { + expect(classifyTask('Add an endpoint for authentication').domain).toBe('backend'); + }); + + it('detects backend from "nest"', () => { + expect(classifyTask('Add a NestJS guard for this route').domain).toBe('backend'); + }); + + it('detects backend from "express"', () => { + expect(classifyTask('Middleware in Express explained').domain).toBe('backend'); + }); + + it('detects devops from "docker"', () => { + expect(classifyTask('Write a Dockerfile for this app').domain).toBe('devops'); + }); + + it('detects devops from "deploy"', () => { + expect(classifyTask('Deploy this service to production').domain).toBe('devops'); + }); + + it('detects devops from "pipeline"', () => { + expect(classifyTask('Set up a CI pipeline').domain).toBe('devops'); + }); + + it('detects devops from "kubernetes"', () => { + expect(classifyTask('Configure a Kubernetes deployment').domain).toBe('devops'); + }); + + it('detects docs from "documentation"', () => { + expect(classifyTask('Write documentation for this module').domain).toBe('docs'); + }); + + it('detects docs from "readme"', () => { + expect(classifyTask('Update the README').domain).toBe('docs'); + }); + + it('detects docs from "guide"', () => { + expect(classifyTask('Create a user guide for this feature').domain).toBe('docs'); + }); + + it('falls back to general domain', () => { + expect(classifyTask('What time is it?').domain).toBe('general'); + }); + + // devops takes priority over backend when both match + it('devops takes priority over backend (both keywords)', () => { + expect(classifyTask('Deploy the API server using Docker').domain).toBe('devops'); + }); + + // docs takes priority over frontend when both match + it('docs takes priority over frontend (both keywords)', () => { + expect(classifyTask('Write documentation for React components').domain).toBe('docs'); + }); +}); + +// ─── Combined Classification ────────────────────────────────────────────────── + +describe('classifyTask — combined', () => { + it('returns full classification object', () => { + const result = classifyTask('Fix the bug?'); + expect(result).toHaveProperty('taskType'); + expect(result).toHaveProperty('complexity'); + expect(result).toHaveProperty('domain'); + }); + + it('classifies complex TypeScript architecture request', () => { + const msg = + 'Design the architecture for a multi-tenant TypeScript system using NestJS with proper database isolation and role-based access control. The system needs to support multiple organizations each with their own data namespace.'; + const result = classifyTask(msg); + expect(result.taskType).toBe('coding'); + expect(result.complexity).toBe('complex'); + expect(result.domain).toBe('backend'); + }); + + it('classifies simple frontend question', () => { + const result = classifyTask('How do I center a div in CSS?'); + expect(result.taskType).toBe('research'); + expect(result.domain).toBe('frontend'); + }); + + it('classifies a DevOps pipeline task as complex', () => { + const msg = + 'Design a complete CI/CD pipeline architecture using Docker and Kubernetes with blue-green deployments and automatic rollback capabilities for a complex microservices system.'; + const result = classifyTask(msg); + expect(result.domain).toBe('devops'); + expect(result.complexity).toBe('complex'); + }); + + it('classifies summarization task correctly', () => { + const result = classifyTask('Summarize the key points from this document'); + expect(result.taskType).toBe('summarization'); + }); + + it('classifies creative writing task correctly', () => { + const result = classifyTask('Write a poem about the ocean'); + expect(result.taskType).toBe('creative'); + }); +}); diff --git a/apps/gateway/src/agent/routing/task-classifier.ts b/apps/gateway/src/agent/routing/task-classifier.ts new file mode 100644 index 0000000..4555e6c --- /dev/null +++ b/apps/gateway/src/agent/routing/task-classifier.ts @@ -0,0 +1,159 @@ +import type { TaskType, Complexity, Domain, TaskClassification } from './routing.types.js'; + +// ─── Pattern Banks ────────────────────────────────────────────────────────── + +const CODING_PATTERNS: RegExp[] = [ + /\bcode\b/i, + /\bfunction\b/i, + /\bimplement\b/i, + /\bdebug\b/i, + /\bfix\b/i, + /\brefactor\b/i, + /\btypescript\b/i, + /\bjavascript\b/i, + /\bpython\b/i, + /\bSQL\b/i, + /\bAPI\b/i, + /\bendpoint\b/i, + /\bclass\b/i, + /\bmethod\b/i, + /`[^`]*`/, +]; + +const RESEARCH_PATTERNS: RegExp[] = [ + /\bresearch\b/i, + /\bfind\b/i, + /\bsearch\b/i, + /\bwhat is\b/i, + /\bexplain\b/i, + /\bhow do(es)?\b/i, + /\bcompare\b/i, + /\banalyze\b/i, +]; + +const SUMMARIZATION_PATTERNS: RegExp[] = [ + /\bsummariz(e|ation)\b/i, + /\bsummary\b/i, + /\btldr\b/i, + /\bcondense\b/i, + /\bbrief\b/i, +]; + +const CREATIVE_PATTERNS: RegExp[] = [ + /\bwrite\b/i, + /\bstory\b/i, + /\bpoem\b/i, + /\bgenerate\b/i, + /\bcreate content\b/i, + /\bblog post\b/i, +]; + +const ANALYSIS_PATTERNS: RegExp[] = [ + /\banalyze\b/i, + /\breview\b/i, + /\bevaluate\b/i, + /\bassess\b/i, + /\baudit\b/i, +]; + +// ─── Complexity Indicators ─────────────────────────────────────────────────── + +const COMPLEX_KEYWORDS: RegExp[] = [ + /\barchitecture\b/i, + /\bdesign\b/i, + /\bcomplex\b/i, + /\bsystem\b/i, +]; + +const SIMPLE_QUESTION_PATTERN = /^[^.!?]+[?]$/; + +/** Counts occurrences of triple-backtick code fences in the message */ +function countCodeBlocks(message: string): number { + return (message.match(/```/g) ?? []).length / 2; +} + +// ─── Domain Indicators ─────────────────────────────────────────────────────── + +const FRONTEND_PATTERNS: RegExp[] = [ + /\breact\b/i, + /\bcss\b/i, + /\bhtml\b/i, + /\bcomponent\b/i, + /\bUI\b/, + /\btailwind\b/i, + /\bnext\.js\b/i, +]; + +const BACKEND_PATTERNS: RegExp[] = [ + /\bAPI\b/i, + /\bserver\b/i, + /\bdatabase\b/i, + /\bendpoint\b/i, + /\bnest(js)?\b/i, + /\bexpress\b/i, +]; + +const DEVOPS_PATTERNS: RegExp[] = [ + /\bdocker(file|compose|hub)?\b/i, + /\bCI\b/, + /\bdeploy\b/i, + /\bpipeline\b/i, + /\bkubernetes\b/i, +]; + +const DOCS_PATTERNS: RegExp[] = [/\bdocumentation\b/i, /\breadme\b/i, /\bguide\b/i]; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function matchesAny(message: string, patterns: RegExp[]): boolean { + return patterns.some((p) => p.test(message)); +} + +// ─── Classifier ────────────────────────────────────────────────────────────── + +/** + * Classify a task based on the user's message using deterministic regex/keyword matching. + * No LLM calls are made — this is a pure, fast, synchronous classification. + */ +export function classifyTask(message: string): TaskClassification { + return { + taskType: detectTaskType(message), + complexity: estimateComplexity(message), + domain: detectDomain(message), + requiredCapabilities: [], + }; +} + +function detectTaskType(message: string): TaskType { + if (matchesAny(message, CODING_PATTERNS)) return 'coding'; + if (matchesAny(message, SUMMARIZATION_PATTERNS)) return 'summarization'; + if (matchesAny(message, CREATIVE_PATTERNS)) return 'creative'; + if (matchesAny(message, ANALYSIS_PATTERNS)) return 'analysis'; + if (matchesAny(message, RESEARCH_PATTERNS)) return 'research'; + return 'conversation'; +} + +function estimateComplexity(message: string): Complexity { + const trimmed = message.trim(); + const codeBlocks = countCodeBlocks(trimmed); + + // Complex: long messages, multiple code blocks, or complexity keywords + if (trimmed.length > 500 || codeBlocks > 1 || matchesAny(trimmed, COMPLEX_KEYWORDS)) { + return 'complex'; + } + + // Simple: short messages or a single direct question + if (trimmed.length < 100 || SIMPLE_QUESTION_PATTERN.test(trimmed)) { + return 'simple'; + } + + return 'moderate'; +} + +function detectDomain(message: string): Domain { + if (matchesAny(message, DEVOPS_PATTERNS)) return 'devops'; + if (matchesAny(message, DOCS_PATTERNS)) return 'docs'; + if (matchesAny(message, FRONTEND_PATTERNS)) return 'frontend'; + if (matchesAny(message, BACKEND_PATTERNS)) return 'backend'; + return 'general'; +} diff --git a/apps/gateway/src/chat/__tests__/chat-security.test.ts b/apps/gateway/src/chat/__tests__/chat-security.test.ts index 6515b3a..0871000 100644 --- a/apps/gateway/src/chat/__tests__/chat-security.test.ts +++ b/apps/gateway/src/chat/__tests__/chat-security.test.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { validateSync } from 'class-validator';