feat(M4-004,M4-005): default routing rules seed data and task classifier
- Add DefaultRoutingRulesSeed service that inserts 11 default routing rules on startup if table is empty - Implement classifyTask() using deterministic regex/keyword matching for taskType, complexity, and domain - Add unit tests covering all task types, complexity levels, and domain detection with 60+ test cases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,14 @@
|
||||
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { routingRules, type Db } from '@mosaic/db';
|
||||
import { sql } from '@mosaic/db';
|
||||
import { routingRules, type Db, sql } from '@mosaic/db';
|
||||
import { DB } from '../../database/database.module.js';
|
||||
|
||||
/** Shape of a routing condition */
|
||||
interface RuleCondition {
|
||||
field: string;
|
||||
operator: 'eq' | 'includes';
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** Shape of a routing action */
|
||||
interface RuleAction {
|
||||
provider: string;
|
||||
model: string;
|
||||
}
|
||||
import type { RoutingCondition, RoutingAction } from './routing.types.js';
|
||||
|
||||
/** Seed-time routing rule descriptor */
|
||||
interface RoutingRuleSeed {
|
||||
name: string;
|
||||
priority: number;
|
||||
conditions: RuleCondition[];
|
||||
action: RuleAction;
|
||||
conditions: RoutingCondition[];
|
||||
action: RoutingAction;
|
||||
}
|
||||
|
||||
export const DEFAULT_ROUTING_RULES: RoutingRuleSeed[] = [
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('classifyTask — taskType', () => {
|
||||
});
|
||||
|
||||
it('detects creative from "blog post"', () => {
|
||||
expect(classifyTask('Write a blog post about TypeScript').taskType).toBe('creative');
|
||||
expect(classifyTask('Write a blog post about productivity habits').taskType).toBe('creative');
|
||||
});
|
||||
|
||||
it('detects analysis from "analyze"', () => {
|
||||
|
||||
159
apps/gateway/src/agent/routing/task-classifier.ts
Normal file
159
apps/gateway/src/agent/routing/task-classifier.ts
Normal file
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user