quay-ai-factory 0.3.0 β 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -39
- package/package.json +1 -1
- package/src/lib/dialog.ts +388 -0
- package/src/lib/ensemble.ts +494 -0
- package/src/server/agents/agentRunner.ts +335 -130
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Quay - Autonomous AI Software Factory
|
|
2
2
|
|
|
3
|
-
> Self-hosted AI agents with MCP integration, customizable pipelines, real-time Mission Control dashboard, cost transparency, and full audit trails.
|
|
3
|
+
> Self-hosted AI agents with MCP integration, A3M routing, customizable pipelines, real-time Mission Control dashboard, cost transparency, and full audit trails.
|
|
4
4
|
|
|
5
5
|
## π Features
|
|
6
6
|
|
|
7
7
|
### 5 AI-Powered Features (Built on A3M Router)
|
|
8
8
|
|
|
9
|
-
| Feature | Description |
|
|
10
|
-
|
|
11
|
-
| **Human-in-Loop Verification** | Require approval before critical actions
|
|
12
|
-
| **Determinism & Replay** | Black-box recorder captures every step
|
|
13
|
-
| **Visual Pipeline Builder** | Drag-drop YAML pipeline generation
|
|
14
|
-
| **Agent RAG** | Knowledge grounding to reduce hallucinations |
|
|
15
|
-
| **Self-Healing Agents** | Auto-detect and recover from failures |
|
|
9
|
+
| Feature | Description | Agent Integration |
|
|
10
|
+
|---------|-------------|-------------------|
|
|
11
|
+
| **Human-in-Loop Verification** | Require approval before critical actions | Auto-triggers on deploy, delete, rm, etc. |
|
|
12
|
+
| **Determinism & Replay** | Black-box recorder captures every step | Snapshots after each LLM call |
|
|
13
|
+
| **Visual Pipeline Builder** | Drag-drop YAML pipeline generation | Built-in templates for common workflows |
|
|
14
|
+
| **Agent RAG** | Knowledge grounding to reduce hallucinations | Applied before first LLM call |
|
|
15
|
+
| **Self-Healing Agents** | Auto-detect and recover from failures | Detects 7 failure types, 5 recovery strategies |
|
|
16
16
|
|
|
17
17
|
### Core Capabilities
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
## π¦ Installation
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npm install
|
|
28
|
+
npm install quay-ai-factory
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## πββοΈ Running
|
|
@@ -42,6 +42,88 @@ node build
|
|
|
42
42
|
bun test
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## π€ Agent Runner
|
|
46
|
+
|
|
47
|
+
The `AgentRunner` orchestrates the observeβplanβact loop with all 5 AI features:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { agentRunner } from './server/agents/agentRunner.js';
|
|
51
|
+
|
|
52
|
+
// Run an agent with all features enabled
|
|
53
|
+
const result = await agentRunner.run(
|
|
54
|
+
{ id: 'task-1', title: 'Deploy to production', description: '...' },
|
|
55
|
+
{ id: 'agent-1', name: 'DeployBot', type: 'coder', provider: 'openai', model: 'gpt-4o', config: '{}' },
|
|
56
|
+
'run-123',
|
|
57
|
+
'You are a deployment agent...',
|
|
58
|
+
['bash', 'deploy'],
|
|
59
|
+
{
|
|
60
|
+
knowledgeBaseId: 'kb-1', // Enable RAG grounding
|
|
61
|
+
verificationConfig: { // Custom verification rules
|
|
62
|
+
criticalActions: ['deploy', 'delete'],
|
|
63
|
+
autoApproveBelow: 0.10,
|
|
64
|
+
},
|
|
65
|
+
maxSteps: 30, // Custom step limit
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### How Features Integrate
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
User Request
|
|
74
|
+
β
|
|
75
|
+
AgentRunner.run()
|
|
76
|
+
β
|
|
77
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
78
|
+
β 1. RAG Grounding β
|
|
79
|
+
β buildGroundedPrompt() β
|
|
80
|
+
β β Adds relevant knowledge to prompt β
|
|
81
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
82
|
+
β
|
|
83
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
84
|
+
β 2. LLM Call β
|
|
85
|
+
β callLLM() with A3M routing β
|
|
86
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
87
|
+
β
|
|
88
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
89
|
+
β 3. Self-Healing Check β
|
|
90
|
+
β detectFailure() β getRecoveryStrategyβ
|
|
91
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
92
|
+
β
|
|
93
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
94
|
+
β 4. Determinism Snapshot β
|
|
95
|
+
β createSnapshot() with state hash β
|
|
96
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
97
|
+
β
|
|
98
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
99
|
+
β 5. Health Update β
|
|
100
|
+
β updateHealthStatus() β
|
|
101
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
102
|
+
β
|
|
103
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
104
|
+
β 6. Critical Action Verification β
|
|
105
|
+
β createVerificationRequest() β
|
|
106
|
+
β β Pauses if approval needed β
|
|
107
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
108
|
+
β
|
|
109
|
+
Tool Execution β Loop
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## π§ Configuration
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Environment variables
|
|
116
|
+
OPENAI_API_KEY=sk-... # OpenAI API key
|
|
117
|
+
ANTHROPIC_API_KEY=sk-ant-... # Anthropic API key
|
|
118
|
+
QUAY_DEFAULT_LLM_URL=https://api.openai.com/v1 # Default LLM endpoint
|
|
119
|
+
|
|
120
|
+
EMBEDDING_PROVIDER=openai # openai, local, or mock
|
|
121
|
+
EMBEDDING_ENDPOINT=http://localhost:11434/api/embeddings
|
|
122
|
+
QUAY_DB_PATH=./quay.db # Database path
|
|
123
|
+
SSE_ENABLED=true # Enable SSE
|
|
124
|
+
NODE_ENV=production # production, development, test
|
|
125
|
+
```
|
|
126
|
+
|
|
45
127
|
## π§ͺ Features
|
|
46
128
|
|
|
47
129
|
### Verification Dashboard (`/verification`)
|
|
@@ -64,6 +146,8 @@ const request = await createVerificationRequest(
|
|
|
64
146
|
approveRequest(request.id, 'admin');
|
|
65
147
|
```
|
|
66
148
|
|
|
149
|
+
**Critical Actions** (auto-detected): `deploy`, `delete`, `rm`, `drop`, `truncate`, `shutdown`, `restart`
|
|
150
|
+
|
|
67
151
|
### Health Monitor (`/health`)
|
|
68
152
|
Real-time agent health status with self-healing.
|
|
69
153
|
|
|
@@ -76,8 +160,13 @@ updateHealthStatus('agent-1', { latencyMs: 150, success: true });
|
|
|
76
160
|
// Detect failure and get recovery strategy
|
|
77
161
|
const failure = detectFailure(new Error('timeout'));
|
|
78
162
|
const strategy = getRecoveryStrategy(failure.type);
|
|
163
|
+
// β { action: 'retry', maxRetries: 3, fallbackModel: 'gpt-4o-mini' }
|
|
79
164
|
```
|
|
80
165
|
|
|
166
|
+
**Failure Types**: `timeout`, `rate_limit`, `api_error`, `context_overflow`, `tool_error`, `unknown`
|
|
167
|
+
|
|
168
|
+
**Recovery Strategies**: `retry`, `fallback_model`, `fallback_agent`, `abort`, `skip_step`
|
|
169
|
+
|
|
81
170
|
### Knowledge Base (`/knowledge`)
|
|
82
171
|
RAG-powered knowledge grounding.
|
|
83
172
|
|
|
@@ -103,6 +192,8 @@ const templates = getTemplates('code-review');
|
|
|
103
192
|
const yaml = generateYAML(nodes, edges);
|
|
104
193
|
```
|
|
105
194
|
|
|
195
|
+
**Built-in Templates**: `code-review`, `security-scan`, `data-processing`, `research-agent`
|
|
196
|
+
|
|
106
197
|
### Replay (`/replay`)
|
|
107
198
|
Deterministic replay of agent runs.
|
|
108
199
|
|
|
@@ -113,43 +204,34 @@ await createSnapshot(runId, stepIndex, state);
|
|
|
113
204
|
const session = await startReplay(runId);
|
|
114
205
|
```
|
|
115
206
|
|
|
116
|
-
## π§ Configuration
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
# Environment variables
|
|
120
|
-
OPENAI_API_KEY=sk-... # OpenAI API key
|
|
121
|
-
EMBEDDING_PROVIDER=openai # openai, local, or mock
|
|
122
|
-
EMBEDDING_ENDPOINT=http://localhost:11434/api/embeddings
|
|
123
|
-
QUAY_DB_PATH=./quay.db # Database path
|
|
124
|
-
SSE_ENABLED=true # Enable SSE
|
|
125
|
-
NODE_ENV=production # production, development, test
|
|
126
|
-
```
|
|
127
|
-
|
|
128
207
|
## π Project Structure
|
|
129
208
|
|
|
130
209
|
```
|
|
131
210
|
quay/
|
|
132
211
|
βββ src/
|
|
133
212
|
β βββ lib/
|
|
134
|
-
β β βββ features/
|
|
135
|
-
β β β βββ verification.ts
|
|
136
|
-
β β β βββ determinism.ts
|
|
137
|
-
β β β βββ visualBuilder.ts
|
|
138
|
-
β β β βββ agentRAG.ts
|
|
139
|
-
β β β βββ selfHealing.ts
|
|
140
|
-
β β βββ components/
|
|
141
|
-
β β βββ sse/
|
|
142
|
-
β β βββ a3m.ts
|
|
143
|
-
β β βββ db.ts
|
|
144
|
-
β β βββ embedding.ts
|
|
213
|
+
β β βββ features/ # 5 AI features
|
|
214
|
+
β β β βββ verification.ts # Human-in-loop
|
|
215
|
+
β β β βββ determinism.ts # Snapshots & replay
|
|
216
|
+
β β β βββ visualBuilder.ts # Pipeline builder
|
|
217
|
+
β β β βββ agentRAG.ts # Knowledge grounding
|
|
218
|
+
β β β βββ selfHealing.ts # Auto-recovery
|
|
219
|
+
β β βββ components/ # Svelte UI components
|
|
220
|
+
β β βββ sse/ # SSE broadcaster
|
|
221
|
+
β β βββ a3m.ts # A3M router integration
|
|
222
|
+
β β βββ db.ts # Database layer
|
|
223
|
+
β β βββ embedding.ts # Embedding service
|
|
145
224
|
β βββ routes/
|
|
146
|
-
β β βββ api/
|
|
147
|
-
β β βββ [page].svelte
|
|
225
|
+
β β βββ api/ # API endpoints
|
|
226
|
+
β β βββ [page].svelte # UI pages
|
|
148
227
|
β βββ server/
|
|
149
|
-
β βββ
|
|
150
|
-
β βββ
|
|
228
|
+
β βββ agents/
|
|
229
|
+
β β βββ agentRunner.ts # Main agent loop (integrates all features)
|
|
230
|
+
β βββ db/ # Database schema
|
|
231
|
+
β βββ features/ # Feature implementations
|
|
232
|
+
β βββ ...
|
|
151
233
|
βββ tests/
|
|
152
|
-
βββ __tests__/
|
|
234
|
+
βββ __tests__/ # Integration tests
|
|
153
235
|
```
|
|
154
236
|
|
|
155
237
|
## π§ͺ Testing
|
|
@@ -172,7 +254,6 @@ Total: 56 tests passing
|
|
|
172
254
|
| Endpoint | Description |
|
|
173
255
|
|----------|-------------|
|
|
174
256
|
| `GET /api/verification` | List verification requests |
|
|
175
|
-
| `POST /api/verification` | Create verification request |
|
|
176
257
|
| `POST /api/verification/[id]/approve` | Approve request |
|
|
177
258
|
| `POST /api/verification/[id]/reject` | Reject request |
|
|
178
259
|
| `GET /api/agent-health` | Get all agent health status |
|
|
@@ -188,7 +269,7 @@ Total: 56 tests passing
|
|
|
188
269
|
|
|
189
270
|
| Route | Description |
|
|
190
271
|
|-------|-------------|
|
|
191
|
-
| `/` | Home |
|
|
272
|
+
| `/` | Home / Mission Control |
|
|
192
273
|
| `/features` | Features dashboard |
|
|
193
274
|
| `/verification` | Verification dashboard |
|
|
194
275
|
| `/health` | Agent health monitor |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quay-ai-factory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Quay β Autonomous AI Software Factory. Self-hosted AI agents, MCP integration, customizable pipelines, real-time Mission Control dashboard, cost transparency, and full audit trails.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-software-factory",
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Multi-Round Dialog Optimizer
|
|
3
|
+
// Tracks conversation context over multiple turns and optimizes
|
|
4
|
+
// routing decisions based on accumulated dialogue history.
|
|
5
|
+
//
|
|
6
|
+
// Based on A3M/TMLPD multi-round dialog module
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
export interface DialogTurn {
|
|
10
|
+
turn: number;
|
|
11
|
+
role: 'user' | 'assistant' | 'system';
|
|
12
|
+
content: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
routingDecision?: RoutingDecision;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DialogState {
|
|
19
|
+
conversationId: string;
|
|
20
|
+
turns: DialogTurn[];
|
|
21
|
+
topics: Set<string>;
|
|
22
|
+
topicHistory: Map<string, number>;
|
|
23
|
+
modelPerformance: Map<string, ModelStats>;
|
|
24
|
+
currentTopic: string;
|
|
25
|
+
sentiment: 'positive' | 'negative' | 'neutral';
|
|
26
|
+
complexity: number;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
lastTurnAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModelStats {
|
|
32
|
+
totalTurns: number;
|
|
33
|
+
successfulTurns: number;
|
|
34
|
+
avgLatency: number;
|
|
35
|
+
avgConfidence: number;
|
|
36
|
+
topicAffinity: Map<string, number>; // topic -> success rate
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RoutingDecision {
|
|
40
|
+
selectedModel: string;
|
|
41
|
+
reasoning: string;
|
|
42
|
+
predictedQuality: number;
|
|
43
|
+
estimatedCost: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DialogConfig {
|
|
47
|
+
maxContextTurns?: number; // Default 20
|
|
48
|
+
topicThreshold?: number; // Default 0.7
|
|
49
|
+
performanceWindow?: number; // Default 10
|
|
50
|
+
trackComplexity?: boolean; // Default true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CONFIG: Required<DialogConfig> = {
|
|
54
|
+
maxContextTurns: 20,
|
|
55
|
+
topicThreshold: 0.7,
|
|
56
|
+
performanceWindow: 10,
|
|
57
|
+
trackComplexity: true,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ============================================================
|
|
61
|
+
// Topic Keywords for classification
|
|
62
|
+
// ============================================================
|
|
63
|
+
|
|
64
|
+
const TOPIC_KEYWORDS: Record<string, string[]> = {
|
|
65
|
+
'coding': ['code', 'function', 'class', 'api', 'debug', 'error', 'bug', 'implement', 'refactor'],
|
|
66
|
+
'data': ['data', 'database', 'sql', 'query', 'analytics', 'table', 'schema', 'migration'],
|
|
67
|
+
'web': ['html', 'css', 'javascript', 'frontend', 'backend', 'http', 'api', 'rest', 'graphql'],
|
|
68
|
+
'ml': ['machine learning', 'model', 'training', 'inference', 'neural', 'ai', 'deep learning', 'classifier'],
|
|
69
|
+
'devops': ['deploy', 'docker', 'kubernetes', 'ci/cd', 'pipeline', 'infrastructure', 'cloud'],
|
|
70
|
+
'security': ['auth', 'security', 'encryption', 'jwt', 'oauth', 'permission', 'vulnerability'],
|
|
71
|
+
'general': [], // Default
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// Multi-Round Dialog Optimizer
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
export class MultiRoundDialogOptimizer {
|
|
79
|
+
private dialogStates: Map<string, DialogState> = new Map();
|
|
80
|
+
private config: Required<DialogConfig>;
|
|
81
|
+
|
|
82
|
+
constructor(config: DialogConfig = {}) {
|
|
83
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get or create dialog state
|
|
88
|
+
*/
|
|
89
|
+
getDialogState(conversationId: string): DialogState {
|
|
90
|
+
if (!this.dialogStates.has(conversationId)) {
|
|
91
|
+
this.dialogStates.set(conversationId, this.createNewState(conversationId));
|
|
92
|
+
}
|
|
93
|
+
return this.dialogStates.get(conversationId)!;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create new dialog state
|
|
98
|
+
*/
|
|
99
|
+
private createNewState(conversationId: string): DialogState {
|
|
100
|
+
return {
|
|
101
|
+
conversationId,
|
|
102
|
+
turns: [],
|
|
103
|
+
topics: new Set(['general']),
|
|
104
|
+
topicHistory: new Map([['general', 1]]),
|
|
105
|
+
modelPerformance: new Map(),
|
|
106
|
+
currentTopic: 'general',
|
|
107
|
+
sentiment: 'neutral',
|
|
108
|
+
complexity: 0,
|
|
109
|
+
startedAt: Date.now(),
|
|
110
|
+
lastTurnAt: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add a turn to the conversation
|
|
116
|
+
*/
|
|
117
|
+
addTurn(
|
|
118
|
+
conversationId: string,
|
|
119
|
+
role: 'user' | 'assistant' | 'system',
|
|
120
|
+
content: string,
|
|
121
|
+
model?: string,
|
|
122
|
+
routingDecision?: RoutingDecision
|
|
123
|
+
): DialogState {
|
|
124
|
+
const state = this.getDialogState(conversationId);
|
|
125
|
+
|
|
126
|
+
const turn: DialogTurn = {
|
|
127
|
+
turn: state.turns.length,
|
|
128
|
+
role,
|
|
129
|
+
content,
|
|
130
|
+
model,
|
|
131
|
+
routingDecision,
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
state.turns.push(turn);
|
|
136
|
+
state.lastTurnAt = Date.now();
|
|
137
|
+
|
|
138
|
+
// Update topic tracking
|
|
139
|
+
const topics = this.extractTopics(content);
|
|
140
|
+
for (const topic of topics) {
|
|
141
|
+
state.topics.add(topic);
|
|
142
|
+
const count = state.topicHistory.get(topic) || 0;
|
|
143
|
+
state.topicHistory.set(topic, count + 1);
|
|
144
|
+
}
|
|
145
|
+
state.currentTopic = this.getMostFrequentTopic(state.topicHistory);
|
|
146
|
+
|
|
147
|
+
// Update complexity
|
|
148
|
+
if (this.config.trackComplexity) {
|
|
149
|
+
state.complexity = this.calculateComplexity(content, state.turns);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Update model performance
|
|
153
|
+
if (role === 'assistant' && model && routingDecision) {
|
|
154
|
+
this.updateModelPerformance(state, model, content, routingDecision);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Trim old turns
|
|
158
|
+
if (state.turns.length > this.config.maxContextTurns) {
|
|
159
|
+
state.turns = state.turns.slice(-this.config.maxContextTurns);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract topics from content
|
|
167
|
+
*/
|
|
168
|
+
extractTopics(content: string): string[] {
|
|
169
|
+
const topics: string[] = [];
|
|
170
|
+
const lower = content.toLowerCase();
|
|
171
|
+
|
|
172
|
+
for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
|
|
173
|
+
if (topic === 'general') continue;
|
|
174
|
+
const matches = keywords.filter(kw => lower.includes(kw)).length;
|
|
175
|
+
if (matches > 0) {
|
|
176
|
+
topics.push(topic);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return topics.length > 0 ? topics : ['general'];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get most frequent topic
|
|
185
|
+
*/
|
|
186
|
+
private getMostFrequentTopic(topicHistory: Map<string, number>): string {
|
|
187
|
+
let maxCount = 0;
|
|
188
|
+
let maxTopic = 'general';
|
|
189
|
+
|
|
190
|
+
for (const [topic, count] of topicHistory) {
|
|
191
|
+
if (count > maxCount) {
|
|
192
|
+
maxCount = count;
|
|
193
|
+
maxTopic = topic;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return maxTopic;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Calculate complexity score (0-1)
|
|
202
|
+
*/
|
|
203
|
+
calculateComplexity(content: string, turns: DialogTurn[]): number {
|
|
204
|
+
const words = content.split(/\s+/).length;
|
|
205
|
+
|
|
206
|
+
// Base complexity from length
|
|
207
|
+
let complexity = Math.min(1, words / 500);
|
|
208
|
+
|
|
209
|
+
// Indicators of complexity
|
|
210
|
+
const complexIndicators = ['however', 'therefore', 'consequently', 'specifically', 'although'];
|
|
211
|
+
const simpleIndicators = ['yes', 'no', 'okay', 'sure', 'thanks'];
|
|
212
|
+
|
|
213
|
+
const lower = content.toLowerCase();
|
|
214
|
+
for (const ind of complexIndicators) {
|
|
215
|
+
if (lower.includes(ind)) complexity += 0.1;
|
|
216
|
+
}
|
|
217
|
+
for (const ind of simpleIndicators) {
|
|
218
|
+
if (lower.includes(ind)) complexity -= 0.1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Turn count adds context complexity
|
|
222
|
+
if (turns.length > 5) complexity += 0.1;
|
|
223
|
+
if (turns.length > 10) complexity += 0.1;
|
|
224
|
+
|
|
225
|
+
return Math.max(0, Math.min(1, complexity));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Update model performance metrics
|
|
230
|
+
*/
|
|
231
|
+
private updateModelPerformance(
|
|
232
|
+
state: DialogState,
|
|
233
|
+
model: string,
|
|
234
|
+
content: string,
|
|
235
|
+
routingDecision: RoutingDecision
|
|
236
|
+
): void {
|
|
237
|
+
if (!state.modelPerformance.has(model)) {
|
|
238
|
+
state.modelPerformance.set(model, {
|
|
239
|
+
totalTurns: 0,
|
|
240
|
+
successfulTurns: 0,
|
|
241
|
+
avgLatency: 0,
|
|
242
|
+
avgConfidence: 0,
|
|
243
|
+
topicAffinity: new Map(),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const stats = state.modelPerformance.get(model)!;
|
|
248
|
+
stats.totalTurns++;
|
|
249
|
+
|
|
250
|
+
// Estimate success by response length and confidence
|
|
251
|
+
const isSuccessful = content.split(/\s+/).length > 10 && routingDecision.predictedQuality > 0.7;
|
|
252
|
+
if (isSuccessful) stats.successfulTurns++;
|
|
253
|
+
|
|
254
|
+
// Update latency (EMA)
|
|
255
|
+
const latency = routingDecision.estimatedCost > 0 ? 1000 / routingDecision.estimatedCost : 500;
|
|
256
|
+
stats.avgLatency = 0.9 * stats.avgLatency + 0.1 * latency;
|
|
257
|
+
|
|
258
|
+
// Update confidence (EMA)
|
|
259
|
+
stats.avgConfidence = 0.9 * stats.avgConfidence + 0.1 * routingDecision.predictedQuality;
|
|
260
|
+
|
|
261
|
+
// Update topic affinity
|
|
262
|
+
const topics = this.extractTopics(content);
|
|
263
|
+
for (const topic of topics) {
|
|
264
|
+
const current = stats.topicAffinity.get(topic) || 0.5;
|
|
265
|
+
stats.topicAffinity.set(topic, 0.9 * current + 0.1 * (isSuccessful ? 1 : 0));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Recommend best model for current conversation
|
|
271
|
+
*/
|
|
272
|
+
recommendModel(state: DialogState): { model: string; reasoning: string } {
|
|
273
|
+
const models = Array.from(state.modelPerformance.keys());
|
|
274
|
+
|
|
275
|
+
if (models.length === 0) {
|
|
276
|
+
return { model: 'openai/gpt-4o-mini', reasoning: 'No history - default to balanced model' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (models.length === 1) {
|
|
280
|
+
return { model: models[0], reasoning: 'Only one model used in this conversation' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Score each model for current topic
|
|
284
|
+
const scores: Map<string, number> = new Map();
|
|
285
|
+
|
|
286
|
+
for (const model of models) {
|
|
287
|
+
const stats = state.modelPerformance.get(model)!;
|
|
288
|
+
|
|
289
|
+
// Topic affinity for current topic
|
|
290
|
+
const topicScore = stats.topicAffinity.get(state.currentTopic) || 0.5;
|
|
291
|
+
|
|
292
|
+
// Success rate
|
|
293
|
+
const successRate = stats.successfulTurns / Math.max(1, stats.totalTurns);
|
|
294
|
+
|
|
295
|
+
// Confidence score
|
|
296
|
+
const confidenceScore = stats.avgConfidence;
|
|
297
|
+
|
|
298
|
+
// Combined score
|
|
299
|
+
const combinedScore = topicScore * 0.4 + successRate * 0.3 + confidenceScore * 0.3;
|
|
300
|
+
scores.set(model, combinedScore);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Find best
|
|
304
|
+
let bestModel = models[0];
|
|
305
|
+
let bestScore = scores.get(models[0])!;
|
|
306
|
+
|
|
307
|
+
for (const [model, score] of scores) {
|
|
308
|
+
if (score > bestScore) {
|
|
309
|
+
bestScore = score;
|
|
310
|
+
bestModel = model;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
model: bestModel,
|
|
316
|
+
reasoning: `Best for ${state.currentTopic} (score: ${bestScore.toFixed(2)})`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get routing hint for next turn
|
|
322
|
+
*/
|
|
323
|
+
getRoutingHint(conversationId: string): {
|
|
324
|
+
suggestedModel?: string;
|
|
325
|
+
topic: string;
|
|
326
|
+
complexity: number;
|
|
327
|
+
turnCount: number;
|
|
328
|
+
} {
|
|
329
|
+
const state = this.getDialogState(conversationId);
|
|
330
|
+
const recommendation = this.recommendModel(state);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
suggestedModel: recommendation.model,
|
|
334
|
+
topic: state.currentTopic,
|
|
335
|
+
complexity: state.complexity,
|
|
336
|
+
turnCount: state.turns.length,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Build enhanced context with history
|
|
342
|
+
*/
|
|
343
|
+
buildEnhancedContext(conversationId: string, maxTurns?: number): string {
|
|
344
|
+
const state = this.getDialogState(conversationId);
|
|
345
|
+
const turnsToInclude = maxTurns
|
|
346
|
+
? state.turns.slice(-maxTurns)
|
|
347
|
+
: state.turns.slice(-this.config.maxContextTurns);
|
|
348
|
+
|
|
349
|
+
const parts: string[] = [];
|
|
350
|
+
|
|
351
|
+
// Add topic context
|
|
352
|
+
parts.push(`[Conversation on: ${state.currentTopic}]`);
|
|
353
|
+
|
|
354
|
+
// Add turns
|
|
355
|
+
for (const turn of turnsToInclude) {
|
|
356
|
+
const role = turn.role === 'user' ? 'User' : turn.model || 'Assistant';
|
|
357
|
+
const truncated = turn.content.slice(0, 500);
|
|
358
|
+
parts.push(`${role}: ${truncated}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add complexity hint
|
|
362
|
+
if (state.complexity > 0.7) {
|
|
363
|
+
parts.push('[Complex conversation - consider using higher quality model]');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return parts.join('\n');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Reset conversation state
|
|
371
|
+
*/
|
|
372
|
+
resetConversation(conversationId: string): void {
|
|
373
|
+
this.dialogStates.delete(conversationId);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get all active conversations
|
|
378
|
+
*/
|
|
379
|
+
getActiveConversations(): string[] {
|
|
380
|
+
return Array.from(this.dialogStates.keys());
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================
|
|
385
|
+
// Convenience export
|
|
386
|
+
// ============================================================
|
|
387
|
+
|
|
388
|
+
export const dialogOptimizer = new MultiRoundDialogOptimizer();
|