konsilio 0.3.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/LICENSE.md +24 -0
- package/README.md +170 -0
- package/build/cli/history.d.ts +2 -0
- package/build/cli/history.js +179 -0
- package/build/cli/history.js.map +1 -0
- package/build/config.d.ts +38 -0
- package/build/config.js +118 -0
- package/build/config.js.map +1 -0
- package/build/container.d.ts +32 -0
- package/build/container.js +102 -0
- package/build/container.js.map +1 -0
- package/build/db/schema.d.ts +1 -0
- package/build/db/schema.js +67 -0
- package/build/db/schema.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +140 -0
- package/build/index.js.map +1 -0
- package/build/konsilio.json +25 -0
- package/build/konsilio.schema.json +140 -0
- package/build/logger.d.ts +84 -0
- package/build/logger.js +121 -0
- package/build/logger.js.map +1 -0
- package/build/personas/expert.d.ts +33 -0
- package/build/personas/expert.js +46 -0
- package/build/personas/expert.js.map +1 -0
- package/build/personas/index.d.ts +9 -0
- package/build/personas/index.js +11 -0
- package/build/personas/index.js.map +1 -0
- package/build/personas/lead.d.ts +33 -0
- package/build/personas/lead.js +51 -0
- package/build/personas/lead.js.map +1 -0
- package/build/personas/schemas.d.ts +313 -0
- package/build/personas/schemas.js +97 -0
- package/build/personas/schemas.js.map +1 -0
- package/build/prompts/consolidation/critique.md +59 -0
- package/build/prompts/consolidation/decision.md +51 -0
- package/build/prompts/consolidation/extraction.md +46 -0
- package/build/prompts/consolidation/synthesis.md +42 -0
- package/build/prompts/expert-rules.md +50 -0
- package/build/prompts/personas/dev-tooling.md +27 -0
- package/build/prompts/personas/devops.md +26 -0
- package/build/prompts/personas/distributed-systems.md +28 -0
- package/build/prompts/personas/graph-dba.md +27 -0
- package/build/prompts/personas/node-fullstack.md +27 -0
- package/build/prompts/personas/performance.md +26 -0
- package/build/prompts/personas/security.md +26 -0
- package/build/prompts/personas/test-architect.md +29 -0
- package/build/prompts/personas/test-quoted.md +14 -0
- package/build/prompts/personas/typescript.md +26 -0
- package/build/prompts/personas/ux-dx.md +29 -0
- package/build/prompts/workflow-rules.md +3 -0
- package/build/server.d.ts +24 -0
- package/build/server.js +181 -0
- package/build/server.js.map +1 -0
- package/build/services/cache.service.d.ts +49 -0
- package/build/services/cache.service.js +85 -0
- package/build/services/cache.service.js.map +1 -0
- package/build/services/council.service.d.ts +111 -0
- package/build/services/council.service.js +361 -0
- package/build/services/council.service.js.map +1 -0
- package/build/services/database.service.d.ts +70 -0
- package/build/services/database.service.js +221 -0
- package/build/services/database.service.js.map +1 -0
- package/build/services/formatter.service.d.ts +52 -0
- package/build/services/formatter.service.js +133 -0
- package/build/services/formatter.service.js.map +1 -0
- package/build/services/index.d.ts +18 -0
- package/build/services/index.js +13 -0
- package/build/services/index.js.map +1 -0
- package/build/services/openrouter.service.d.ts +58 -0
- package/build/services/openrouter.service.js +128 -0
- package/build/services/openrouter.service.js.map +1 -0
- package/build/services/persona.service.d.ts +43 -0
- package/build/services/persona.service.js +93 -0
- package/build/services/persona.service.js.map +1 -0
- package/build/services/prompt.service.d.ts +59 -0
- package/build/services/prompt.service.js +209 -0
- package/build/services/prompt.service.js.map +1 -0
- package/konsilio.schema.json +140 -0
- package/package.json +68 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Service
|
|
3
|
+
*
|
|
4
|
+
* Provides SQLite database operations for session persistence using sql.js (WASM).
|
|
5
|
+
* Designed for dependency injection to enable testing.
|
|
6
|
+
*/
|
|
7
|
+
import initSqlJs from 'sql.js';
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
/**
|
|
11
|
+
* DatabaseService - Handles SQLite persistence via sql.js (WASM)
|
|
12
|
+
*
|
|
13
|
+
* Uses an in-memory database that is loaded from / saved to disk.
|
|
14
|
+
* All queries run against the in-memory copy; changes are persisted
|
|
15
|
+
* explicitly after writes.
|
|
16
|
+
*/
|
|
17
|
+
export class DatabaseService {
|
|
18
|
+
db = null;
|
|
19
|
+
dbPath;
|
|
20
|
+
maxHistorySessions;
|
|
21
|
+
logger;
|
|
22
|
+
constructor(config, logger) {
|
|
23
|
+
this.dbPath = config.dbPath;
|
|
24
|
+
this.maxHistorySessions = config.maxHistorySessions ?? 10;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Factory method to create and initialize the DatabaseService.
|
|
29
|
+
* Async because sql.js WASM loading and DB file reading are async.
|
|
30
|
+
*/
|
|
31
|
+
static async create(config, logger, schema) {
|
|
32
|
+
const service = new DatabaseService(config, logger);
|
|
33
|
+
await service.initialize(schema);
|
|
34
|
+
return service;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initialize database: load WASM, read existing DB file (or create new), apply schema
|
|
38
|
+
*/
|
|
39
|
+
async initialize(schema) {
|
|
40
|
+
try {
|
|
41
|
+
// In Node.js, sql.js auto-locates the WASM file from node_modules.
|
|
42
|
+
// No locateFile needed - per sql.js docs: "You can omit locateFile completely when running in node"
|
|
43
|
+
const SQL = await initSqlJs();
|
|
44
|
+
mkdirSync(dirname(this.dbPath), { recursive: true });
|
|
45
|
+
if (existsSync(this.dbPath)) {
|
|
46
|
+
const buffer = readFileSync(this.dbPath);
|
|
47
|
+
this.db = new SQL.Database(buffer);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.db = new SQL.Database();
|
|
51
|
+
}
|
|
52
|
+
// Apply schema (idempotent — uses IF NOT EXISTS)
|
|
53
|
+
this.db.run(schema);
|
|
54
|
+
this.save();
|
|
55
|
+
this.logger.info('Database service initialized', { path: this.dbPath });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
this.logger.error('Failed to initialize database service', {
|
|
59
|
+
path: this.dbPath,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err)
|
|
61
|
+
});
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Persist the in-memory database to disk
|
|
67
|
+
*/
|
|
68
|
+
save() {
|
|
69
|
+
if (!this.db)
|
|
70
|
+
return;
|
|
71
|
+
const data = this.db.export();
|
|
72
|
+
const buffer = Buffer.from(data);
|
|
73
|
+
writeFileSync(this.dbPath, buffer);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check database health
|
|
77
|
+
*/
|
|
78
|
+
checkHealth() {
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
if (!this.db) {
|
|
81
|
+
return { status: 'unhealthy', latencyMs: 0, error: 'Database not initialized' };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
this.db.exec('SELECT 1');
|
|
85
|
+
return { status: 'healthy', latencyMs: Date.now() - start };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return { status: 'unhealthy', latencyMs: Date.now() - start, error: err instanceof Error ? err.message : String(err) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Save a council session result
|
|
93
|
+
*/
|
|
94
|
+
saveCouncilResult(result, draftPlan, techStack, constraints, correlationId) {
|
|
95
|
+
if (!this.db) {
|
|
96
|
+
this.logger.warn('Database not initialized, skipping save', {}, correlationId);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
// Save session
|
|
101
|
+
this.db.run(`INSERT INTO sessions (id, draft_plan_summary, tech_stack, constraints) VALUES (?, ?, ?, ?)`, [result.sessionId, draftPlan.slice(0, 500), techStack ?? null, constraints ?? null]);
|
|
102
|
+
// Save user message
|
|
103
|
+
this.db.run(`INSERT INTO messages (session_id, role, content) VALUES (?, 'user', ?)`, [result.sessionId, draftPlan]);
|
|
104
|
+
// Save expert findings
|
|
105
|
+
for (const report of result.expertReports) {
|
|
106
|
+
for (const finding of report.structuredOutput.findings) {
|
|
107
|
+
const decision = result.decisionOutput.decisions.find(d => d.findingId === finding.id);
|
|
108
|
+
const accepted = decision?.action === 'ACCEPT' ? 1 : 0;
|
|
109
|
+
const rejectionReason = decision?.action === 'REJECT' ? decision.reasoning : null;
|
|
110
|
+
this.db.run(`INSERT INTO expert_findings (
|
|
111
|
+
id, session_id, persona_id, persona_name, persona_emoji,
|
|
112
|
+
severity, component, issue, mitigation, accepted, rejection_reason,
|
|
113
|
+
duration_ms, model_used
|
|
114
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
115
|
+
finding.id,
|
|
116
|
+
result.sessionId,
|
|
117
|
+
report.personaId,
|
|
118
|
+
report.personaName,
|
|
119
|
+
report.personaEmoji,
|
|
120
|
+
finding.severity,
|
|
121
|
+
finding.component,
|
|
122
|
+
finding.issue,
|
|
123
|
+
finding.mitigation,
|
|
124
|
+
accepted,
|
|
125
|
+
rejectionReason,
|
|
126
|
+
report.durationMs,
|
|
127
|
+
report.modelUsed
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
// Save expert risks
|
|
131
|
+
for (const risk of report.structuredOutput.risks) {
|
|
132
|
+
this.db.run(`INSERT INTO expert_risks (
|
|
133
|
+
id, session_id, persona_id, category, probability, impact, description
|
|
134
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
135
|
+
risk.id,
|
|
136
|
+
result.sessionId,
|
|
137
|
+
report.personaId,
|
|
138
|
+
risk.category,
|
|
139
|
+
risk.probability,
|
|
140
|
+
risk.impact,
|
|
141
|
+
risk.description
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Save consolidation phase outputs
|
|
146
|
+
this.db.run(`INSERT INTO consolidation_phases (session_id, phase_name, phase_output) VALUES (?, 'extraction', ?)`, [result.sessionId, JSON.stringify(result.extractionOutput)]);
|
|
147
|
+
this.db.run(`INSERT INTO consolidation_phases (session_id, phase_name, phase_output) VALUES (?, 'critique', ?)`, [result.sessionId, JSON.stringify(result.critiqueOutput)]);
|
|
148
|
+
this.db.run(`INSERT INTO consolidation_phases (session_id, phase_name, phase_output) VALUES (?, 'decision', ?)`, [result.sessionId, JSON.stringify(result.decisionOutput)]);
|
|
149
|
+
this.db.run(`INSERT INTO consolidation_phases (session_id, phase_name, phase_output) VALUES (?, 'synthesis', ?)`, [result.sessionId, JSON.stringify(result.synthesisOutput)]);
|
|
150
|
+
// Save final blueprint as assistant message
|
|
151
|
+
this.db.run(`INSERT INTO messages (session_id, role, persona_id, content) VALUES (?, 'assistant', 'consolidation', ?)`, [result.sessionId, result.finalBlueprint]);
|
|
152
|
+
this.save();
|
|
153
|
+
this.pruneOldSessions();
|
|
154
|
+
this.logger.debug('Council result saved', { sessionId: result.sessionId }, correlationId);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.logger.error('Failed to save council result', {
|
|
158
|
+
sessionId: result.sessionId,
|
|
159
|
+
error: err instanceof Error ? err.message : String(err),
|
|
160
|
+
}, correlationId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get recent sessions
|
|
165
|
+
*/
|
|
166
|
+
getRecentSessions(limit = 10) {
|
|
167
|
+
if (!this.db)
|
|
168
|
+
return [];
|
|
169
|
+
const result = this.db.exec(`
|
|
170
|
+
SELECT id, created_at, draft_plan_summary, tech_stack
|
|
171
|
+
FROM sessions ORDER BY created_at DESC LIMIT ${limit}
|
|
172
|
+
`);
|
|
173
|
+
if (result.length === 0)
|
|
174
|
+
return [];
|
|
175
|
+
const columns = result[0].columns;
|
|
176
|
+
const rows = [];
|
|
177
|
+
for (const values of result[0].values) {
|
|
178
|
+
const row = {};
|
|
179
|
+
for (let i = 0; i < columns.length; i++) {
|
|
180
|
+
row[columns[i]] = values[i];
|
|
181
|
+
}
|
|
182
|
+
rows.push(row);
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get a session's blueprint
|
|
188
|
+
*/
|
|
189
|
+
getSessionBlueprint(sessionId) {
|
|
190
|
+
if (!this.db)
|
|
191
|
+
return null;
|
|
192
|
+
const result = this.db.exec(`
|
|
193
|
+
SELECT content FROM messages
|
|
194
|
+
WHERE session_id = '${sessionId.replace(/'/g, "''")}' AND role = 'assistant' AND persona_id = 'consolidation'
|
|
195
|
+
ORDER BY created_at DESC LIMIT 1
|
|
196
|
+
`);
|
|
197
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
198
|
+
return null;
|
|
199
|
+
return result[0].values[0][0];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Close database (no-op for sql.js, but we clear the reference)
|
|
203
|
+
*/
|
|
204
|
+
close() {
|
|
205
|
+
if (this.db) {
|
|
206
|
+
this.save();
|
|
207
|
+
this.db.close();
|
|
208
|
+
this.db = null;
|
|
209
|
+
this.logger.info('Database service closed');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
pruneOldSessions() {
|
|
213
|
+
if (!this.db)
|
|
214
|
+
return;
|
|
215
|
+
this.db.run(`DELETE FROM sessions WHERE id NOT IN (
|
|
216
|
+
SELECT id FROM sessions ORDER BY created_at DESC LIMIT ?
|
|
217
|
+
)`, [this.maxHistorySessions]);
|
|
218
|
+
this.save();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=database.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.service.js","sourceRoot":"","sources":["../../src/services/database.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter Service
|
|
3
|
+
*
|
|
4
|
+
* Converts expert prose analysis into structured JSON output.
|
|
5
|
+
* Uses OpenAI's response_format with JSON schema for reliable formatting.
|
|
6
|
+
* Leverages gpt-4o-mini as a dedicated formatter model.
|
|
7
|
+
*/
|
|
8
|
+
import type { Logger } from '../logger.js';
|
|
9
|
+
import type { OpenRouterService } from './openrouter.service.js';
|
|
10
|
+
import type { StructuredExpertOutput } from '../personas/schemas.js';
|
|
11
|
+
export interface FormatterConfig {
|
|
12
|
+
model: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
maxRetries: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FormatResult {
|
|
17
|
+
output: StructuredExpertOutput;
|
|
18
|
+
formattingConfidence: number;
|
|
19
|
+
originalProse: string;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
retries: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* FormatterService - Converts prose to structured JSON using response_format
|
|
25
|
+
*/
|
|
26
|
+
export declare class FormatterService {
|
|
27
|
+
private readonly deps;
|
|
28
|
+
private readonly config;
|
|
29
|
+
private readonly responseFormat;
|
|
30
|
+
constructor(deps: {
|
|
31
|
+
openRouterService: OpenRouterService;
|
|
32
|
+
logger: Logger;
|
|
33
|
+
config: FormatterConfig;
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Format expert prose into structured JSON output.
|
|
37
|
+
* Uses OpenAI's response_format feature for reliable structured output.
|
|
38
|
+
*/
|
|
39
|
+
formatProse(prose: string, personaId: string, correlationId?: string): Promise<FormatResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Build the system prompt for the formatter
|
|
42
|
+
*/
|
|
43
|
+
private buildFormatterSystemPrompt;
|
|
44
|
+
/**
|
|
45
|
+
* Build the user message with the prose to format
|
|
46
|
+
*/
|
|
47
|
+
private buildFormatUserMessage;
|
|
48
|
+
/**
|
|
49
|
+
* Calculate formatting confidence based on output quality
|
|
50
|
+
*/
|
|
51
|
+
private calculateConfidence;
|
|
52
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter Service
|
|
3
|
+
*
|
|
4
|
+
* Converts expert prose analysis into structured JSON output.
|
|
5
|
+
* Uses OpenAI's response_format with JSON schema for reliable formatting.
|
|
6
|
+
* Leverages gpt-4o-mini as a dedicated formatter model.
|
|
7
|
+
*/
|
|
8
|
+
import { StructuredExpertOutputSchema, toResponseFormat } from '../personas/schemas.js';
|
|
9
|
+
/**
|
|
10
|
+
* FormatterService - Converts prose to structured JSON using response_format
|
|
11
|
+
*/
|
|
12
|
+
export class FormatterService {
|
|
13
|
+
deps;
|
|
14
|
+
config;
|
|
15
|
+
responseFormat;
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
this.config = deps.config;
|
|
19
|
+
this.responseFormat = toResponseFormat(StructuredExpertOutputSchema, 'expert_output');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format expert prose into structured JSON output.
|
|
23
|
+
* Uses OpenAI's response_format feature for reliable structured output.
|
|
24
|
+
*/
|
|
25
|
+
async formatProse(prose, personaId, correlationId) {
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
let retries = 0;
|
|
28
|
+
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
const messages = [
|
|
31
|
+
{
|
|
32
|
+
role: 'system',
|
|
33
|
+
content: this.buildFormatterSystemPrompt(personaId)
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
role: 'user',
|
|
37
|
+
content: this.buildFormatUserMessage(prose)
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
const rawOutput = await this.deps.openRouterService.call({
|
|
41
|
+
model: this.config.model,
|
|
42
|
+
messages,
|
|
43
|
+
maxTokens: 4096,
|
|
44
|
+
temperature: 0.1,
|
|
45
|
+
timeoutMs: this.config.timeoutMs,
|
|
46
|
+
responseFormat: this.responseFormat
|
|
47
|
+
}, correlationId);
|
|
48
|
+
const parsed = StructuredExpertOutputSchema.parse(JSON.parse(rawOutput));
|
|
49
|
+
// Validate personaId matches
|
|
50
|
+
if (parsed.personaId !== personaId) {
|
|
51
|
+
parsed.personaId = personaId;
|
|
52
|
+
}
|
|
53
|
+
const result = {
|
|
54
|
+
output: parsed,
|
|
55
|
+
formattingConfidence: this.calculateConfidence(parsed),
|
|
56
|
+
originalProse: prose,
|
|
57
|
+
durationMs: Date.now() - start,
|
|
58
|
+
retries: attempt
|
|
59
|
+
};
|
|
60
|
+
this.deps.logger.debug('Prose formatted successfully', {
|
|
61
|
+
personaId,
|
|
62
|
+
findingsCount: parsed.findings.length,
|
|
63
|
+
durationMs: result.durationMs,
|
|
64
|
+
retries: attempt
|
|
65
|
+
}, correlationId);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
retries = attempt + 1;
|
|
70
|
+
if (attempt < this.config.maxRetries - 1) {
|
|
71
|
+
this.deps.logger.warn('Formatting attempt failed, retrying', {
|
|
72
|
+
personaId,
|
|
73
|
+
attempt: attempt + 1,
|
|
74
|
+
error: err instanceof Error ? err.message : String(err)
|
|
75
|
+
}, correlationId);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Formatter failed after ${this.config.maxRetries} attempts: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error('Formatter exhausted all retries');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the system prompt for the formatter
|
|
85
|
+
*/
|
|
86
|
+
buildFormatterSystemPrompt(personaId) {
|
|
87
|
+
return `You are a precise JSON formatter. Your job is to convert expert analysis prose into a structured JSON output.
|
|
88
|
+
|
|
89
|
+
Extract the following from the prose:
|
|
90
|
+
- findings: Specific issues with severity, component, issue description, and mitigation steps
|
|
91
|
+
- risks: Potential risks with category, probability, impact, and description
|
|
92
|
+
- missingAssumptions: Assumptions not stated in the analysis
|
|
93
|
+
- dependencies: External dependencies or prerequisites mentioned
|
|
94
|
+
|
|
95
|
+
Rules:
|
|
96
|
+
- Use severity values: CRITICAL, HIGH, MEDIUM, LOW
|
|
97
|
+
- Use category values: security, performance, operational, ux, technical-debt
|
|
98
|
+
- Use probability/impact values: high, medium, low
|
|
99
|
+
- Each finding must have a unique id (use kebab-case like "auth-rate-limit")
|
|
100
|
+
- Each risk must have a unique id
|
|
101
|
+
- Be precise and complete - capture all findings from the prose
|
|
102
|
+
- Do not add findings not supported by the prose
|
|
103
|
+
- Set personaId to: ${personaId}
|
|
104
|
+
|
|
105
|
+
Output ONLY valid JSON. No markdown, no explanations.`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build the user message with the prose to format
|
|
109
|
+
*/
|
|
110
|
+
buildFormatUserMessage(prose) {
|
|
111
|
+
return `Convert the following expert analysis into structured JSON:\n\n${prose}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculate formatting confidence based on output quality
|
|
115
|
+
*/
|
|
116
|
+
calculateConfidence(output) {
|
|
117
|
+
let score = 10;
|
|
118
|
+
// Penalize empty sections
|
|
119
|
+
if (output.findings.length === 0)
|
|
120
|
+
score -= 3;
|
|
121
|
+
if (output.risks.length === 0)
|
|
122
|
+
score -= 1;
|
|
123
|
+
// Penalize very short findings
|
|
124
|
+
for (const finding of output.findings) {
|
|
125
|
+
if (finding.issue.length < 20)
|
|
126
|
+
score -= 1;
|
|
127
|
+
if (finding.mitigation.length < 20)
|
|
128
|
+
score -= 1;
|
|
129
|
+
}
|
|
130
|
+
return Math.max(1, Math.min(10, score));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=formatter.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.service.js","sourceRoot":"","sources":["../../src/services/formatter.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Services Index
|
|
3
|
+
*
|
|
4
|
+
* Exports all service classes for dependency injection.
|
|
5
|
+
*/
|
|
6
|
+
export { OpenRouterService } from './openrouter.service.js';
|
|
7
|
+
export type { Message, OpenRouterCallOptions, OpenRouterServiceConfig } from './openrouter.service.js';
|
|
8
|
+
export { DatabaseService } from './database.service.js';
|
|
9
|
+
export type { DatabaseServiceConfig, SessionSummary } from './database.service.js';
|
|
10
|
+
export { CouncilService } from './council.service.js';
|
|
11
|
+
export type { CouncilConfig, CouncilParams, CouncilOptions } from './council.service.js';
|
|
12
|
+
export { CacheService } from './cache.service.js';
|
|
13
|
+
export type { CacheEntry } from './cache.service.js';
|
|
14
|
+
export { PromptService } from './prompt.service.js';
|
|
15
|
+
export type { PromptServiceConfig, PersonaPromptData } from './prompt.service.js';
|
|
16
|
+
export { PersonaService } from './persona.service.js';
|
|
17
|
+
export { FormatterService } from './formatter.service.js';
|
|
18
|
+
export type { FormatterConfig, FormatResult } from './formatter.service.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Services Index
|
|
3
|
+
*
|
|
4
|
+
* Exports all service classes for dependency injection.
|
|
5
|
+
*/
|
|
6
|
+
export { OpenRouterService } from './openrouter.service.js';
|
|
7
|
+
export { DatabaseService } from './database.service.js';
|
|
8
|
+
export { CouncilService } from './council.service.js';
|
|
9
|
+
export { CacheService } from './cache.service.js';
|
|
10
|
+
export { PromptService } from './prompt.service.js';
|
|
11
|
+
export { PersonaService } from './persona.service.js';
|
|
12
|
+
export { FormatterService } from './formatter.service.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter Service
|
|
3
|
+
*
|
|
4
|
+
* Provides LLM API calls through OpenRouter with retry logic and error handling.
|
|
5
|
+
* Designed for dependency injection to enable testing.
|
|
6
|
+
*/
|
|
7
|
+
import type { Logger } from '../logger.js';
|
|
8
|
+
export interface Message {
|
|
9
|
+
role: 'system' | 'user' | 'assistant';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResponseFormat {
|
|
13
|
+
type: 'json_schema';
|
|
14
|
+
json_schema: {
|
|
15
|
+
name: string;
|
|
16
|
+
strict: boolean;
|
|
17
|
+
schema: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface OpenRouterCallOptions {
|
|
21
|
+
model: string;
|
|
22
|
+
messages: Message[];
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
temperature?: number;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
responseFormat?: ResponseFormat;
|
|
27
|
+
}
|
|
28
|
+
export interface OpenRouterServiceConfig {
|
|
29
|
+
apiKey: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
maxRetries?: number;
|
|
32
|
+
baseDelayMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* OpenRouterService - Handles LLM API calls
|
|
36
|
+
*/
|
|
37
|
+
export declare class OpenRouterService {
|
|
38
|
+
private readonly apiKey;
|
|
39
|
+
private readonly baseUrl;
|
|
40
|
+
private readonly maxRetries;
|
|
41
|
+
private readonly baseDelayMs;
|
|
42
|
+
private readonly logger;
|
|
43
|
+
constructor(config: OpenRouterServiceConfig, logger: Logger);
|
|
44
|
+
/**
|
|
45
|
+
* Call the OpenRouter API
|
|
46
|
+
*/
|
|
47
|
+
call(opts: OpenRouterCallOptions, correlationId?: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Check API health by calling the models endpoint
|
|
50
|
+
*/
|
|
51
|
+
checkHealth(): Promise<{
|
|
52
|
+
status: 'healthy' | 'unhealthy';
|
|
53
|
+
latencyMs: number;
|
|
54
|
+
error?: string;
|
|
55
|
+
}>;
|
|
56
|
+
private isRetryable;
|
|
57
|
+
private delay;
|
|
58
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter Service
|
|
3
|
+
*
|
|
4
|
+
* Provides LLM API calls through OpenRouter with retry logic and error handling.
|
|
5
|
+
* Designed for dependency injection to enable testing.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* OpenRouterService - Handles LLM API calls
|
|
9
|
+
*/
|
|
10
|
+
export class OpenRouterService {
|
|
11
|
+
apiKey;
|
|
12
|
+
baseUrl;
|
|
13
|
+
maxRetries;
|
|
14
|
+
baseDelayMs;
|
|
15
|
+
logger;
|
|
16
|
+
constructor(config, logger) {
|
|
17
|
+
this.apiKey = config.apiKey;
|
|
18
|
+
this.baseUrl = config.baseUrl;
|
|
19
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
20
|
+
this.baseDelayMs = config.baseDelayMs ?? 1000;
|
|
21
|
+
this.logger = logger;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Call the OpenRouter API
|
|
25
|
+
*/
|
|
26
|
+
async call(opts, correlationId) {
|
|
27
|
+
if (!this.apiKey) {
|
|
28
|
+
throw new Error('Missing OPENROUTER_API_KEY. Set it in your .env file.\n' +
|
|
29
|
+
'Get your key at https://openrouter.ai/keys');
|
|
30
|
+
}
|
|
31
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 90_000);
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'HTTP-Referer': 'https://github.com/konsilio',
|
|
41
|
+
'X-Title': 'Konsilio Council',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
model: opts.model,
|
|
45
|
+
messages: opts.messages,
|
|
46
|
+
max_tokens: opts.maxTokens ?? 4096,
|
|
47
|
+
temperature: opts.temperature ?? 0.3,
|
|
48
|
+
...(opts.responseFormat && { response_format: opts.responseFormat }),
|
|
49
|
+
}),
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
});
|
|
52
|
+
if (res.status === 401)
|
|
53
|
+
throw new Error('Unauthorized: Invalid OpenRouter API key.');
|
|
54
|
+
if (res.status === 402)
|
|
55
|
+
throw new Error('No credits remaining on OpenRouter.');
|
|
56
|
+
if (res.status === 429) {
|
|
57
|
+
if (attempt < this.maxRetries - 1) {
|
|
58
|
+
await this.delay(this.baseDelayMs * Math.pow(2, attempt));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw new Error('Rate limited by OpenRouter.');
|
|
62
|
+
}
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (data.error)
|
|
65
|
+
throw new Error(`OpenRouter error: ${data.error.message}`);
|
|
66
|
+
const content = data.choices?.[0]?.message?.content;
|
|
67
|
+
if (!content)
|
|
68
|
+
throw new Error('Empty response from OpenRouter.');
|
|
69
|
+
this.logger.debug('OpenRouter call successful', { model: opts.model }, correlationId);
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
74
|
+
throw new Error(`Request timeout (>${(opts.timeoutMs ?? 90_000) / 1000}s).`);
|
|
75
|
+
}
|
|
76
|
+
if (this.isRetryable(err) && attempt < this.maxRetries - 1) {
|
|
77
|
+
this.logger.warn('Retrying OpenRouter call', { attempt: attempt + 1 }, correlationId);
|
|
78
|
+
await this.delay(this.baseDelayMs * Math.pow(2, attempt));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error('Max retries exceeded.');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check API health by calling the models endpoint
|
|
91
|
+
*/
|
|
92
|
+
async checkHealth() {
|
|
93
|
+
const start = Date.now();
|
|
94
|
+
if (!this.apiKey) {
|
|
95
|
+
return { status: 'unhealthy', latencyMs: 0, error: 'OPENROUTER_API_KEY not configured' };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
100
|
+
const res = await fetch(`${this.baseUrl}/models`, {
|
|
101
|
+
method: 'GET',
|
|
102
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
});
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
if (res.ok) {
|
|
107
|
+
return { status: 'healthy', latencyMs: Date.now() - start };
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return { status: 'unhealthy', latencyMs: Date.now() - start, error: `HTTP ${res.status}: ${res.statusText}` };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return { status: 'unhealthy', latencyMs: Date.now() - start, error: err instanceof Error ? err.message : String(err) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
isRetryable(error) {
|
|
118
|
+
if (error instanceof Error) {
|
|
119
|
+
const msg = error.message.toLowerCase();
|
|
120
|
+
return msg.includes('429') || msg.includes('503') || msg.includes('rate');
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
delay(ms) {
|
|
125
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=openrouter.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter.service.js","sourceRoot":"","sources":["../../src/services/openrouter.service.ts"],"names":[],"mappings":"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"}
|