heron-ai 0.1.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 +21 -0
- package/README.md +423 -0
- package/dist/bin/heron.d.ts +3 -0
- package/dist/bin/heron.d.ts.map +1 -0
- package/dist/bin/heron.js +198 -0
- package/dist/bin/heron.js.map +1 -0
- package/dist/src/analysis/analyzer.d.ts +14 -0
- package/dist/src/analysis/analyzer.d.ts.map +1 -0
- package/dist/src/analysis/analyzer.js +130 -0
- package/dist/src/analysis/analyzer.js.map +1 -0
- package/dist/src/analysis/risk-scorer.d.ts +20 -0
- package/dist/src/analysis/risk-scorer.d.ts.map +1 -0
- package/dist/src/analysis/risk-scorer.js +143 -0
- package/dist/src/analysis/risk-scorer.js.map +1 -0
- package/dist/src/config/loader.d.ts +15 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +39 -0
- package/dist/src/config/loader.js.map +1 -0
- package/dist/src/config/schema.d.ts +146 -0
- package/dist/src/config/schema.d.ts.map +1 -0
- package/dist/src/config/schema.js +27 -0
- package/dist/src/config/schema.js.map +1 -0
- package/dist/src/connectors/http-connector.d.ts +17 -0
- package/dist/src/connectors/http-connector.d.ts.map +1 -0
- package/dist/src/connectors/http-connector.js +56 -0
- package/dist/src/connectors/http-connector.js.map +1 -0
- package/dist/src/connectors/index.d.ts +5 -0
- package/dist/src/connectors/index.d.ts.map +1 -0
- package/dist/src/connectors/index.js +13 -0
- package/dist/src/connectors/index.js.map +1 -0
- package/dist/src/connectors/interactive-connector.d.ts +13 -0
- package/dist/src/connectors/interactive-connector.d.ts.map +1 -0
- package/dist/src/connectors/interactive-connector.js +44 -0
- package/dist/src/connectors/interactive-connector.js.map +1 -0
- package/dist/src/connectors/types.d.ts +15 -0
- package/dist/src/connectors/types.d.ts.map +1 -0
- package/dist/src/connectors/types.js +2 -0
- package/dist/src/connectors/types.js.map +1 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +60 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interview/interviewer.d.ts +19 -0
- package/dist/src/interview/interviewer.d.ts.map +1 -0
- package/dist/src/interview/interviewer.js +68 -0
- package/dist/src/interview/interviewer.js.map +1 -0
- package/dist/src/interview/protocol.d.ts +38 -0
- package/dist/src/interview/protocol.d.ts.map +1 -0
- package/dist/src/interview/protocol.js +290 -0
- package/dist/src/interview/protocol.js.map +1 -0
- package/dist/src/interview/questions.d.ts +20 -0
- package/dist/src/interview/questions.d.ts.map +1 -0
- package/dist/src/interview/questions.js +131 -0
- package/dist/src/interview/questions.js.map +1 -0
- package/dist/src/llm/client.d.ts +13 -0
- package/dist/src/llm/client.d.ts.map +1 -0
- package/dist/src/llm/client.js +128 -0
- package/dist/src/llm/client.js.map +1 -0
- package/dist/src/llm/prompts.d.ts +13 -0
- package/dist/src/llm/prompts.d.ts.map +1 -0
- package/dist/src/llm/prompts.js +192 -0
- package/dist/src/llm/prompts.js.map +1 -0
- package/dist/src/report/generator.d.ts +23 -0
- package/dist/src/report/generator.d.ts.map +1 -0
- package/dist/src/report/generator.js +304 -0
- package/dist/src/report/generator.js.map +1 -0
- package/dist/src/report/templates.d.ts +3 -0
- package/dist/src/report/templates.d.ts.map +1 -0
- package/dist/src/report/templates.js +386 -0
- package/dist/src/report/templates.js.map +1 -0
- package/dist/src/report/types.d.ts +954 -0
- package/dist/src/report/types.d.ts.map +1 -0
- package/dist/src/report/types.js +161 -0
- package/dist/src/report/types.js.map +1 -0
- package/dist/src/server/index.d.ts +17 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +650 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/sessions.d.ts +68 -0
- package/dist/src/server/sessions.d.ts.map +1 -0
- package/dist/src/server/sessions.js +268 -0
- package/dist/src/server/sessions.js.map +1 -0
- package/dist/src/util/id.d.ts +2 -0
- package/dist/src/util/id.d.ts.map +1 -0
- package/dist/src/util/id.js +5 -0
- package/dist/src/util/id.js.map +1 -0
- package/dist/src/util/logger.d.ts +9 -0
- package/dist/src/util/logger.d.ts.map +1 -0
- package/dist/src/util/logger.js +32 -0
- package/dist/src/util/logger.js.map +1 -0
- package/heron.example.yaml +46 -0
- package/package.json +40 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HttpConnector } from './http-connector.js';
|
|
2
|
+
import { InteractiveConnector } from './interactive-connector.js';
|
|
3
|
+
export function createConnector(config) {
|
|
4
|
+
switch (config.type) {
|
|
5
|
+
case 'http':
|
|
6
|
+
return new HttpConnector(config);
|
|
7
|
+
case 'interactive':
|
|
8
|
+
return new InteractiveConnector();
|
|
9
|
+
default:
|
|
10
|
+
throw new Error(`Unknown connector type: ${config.type}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/connectors/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE,MAAM,UAAU,eAAe,CAAC,MAAoB;IAClD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;QACnC,KAAK,aAAa;YAChB,OAAO,IAAI,oBAAoB,EAAE,CAAC;QACpC;YACE,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentConnector, AgentMetadata } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Interactive connector — the user manually relays questions to the agent
|
|
4
|
+
* and pastes back responses. Useful when the agent doesn't have an HTTP API.
|
|
5
|
+
*/
|
|
6
|
+
export declare class InteractiveConnector implements AgentConnector {
|
|
7
|
+
private rl;
|
|
8
|
+
constructor();
|
|
9
|
+
sendMessage(message: string): Promise<string>;
|
|
10
|
+
getMetadata(): Promise<AgentMetadata>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=interactive-connector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive-connector.d.ts","sourceRoot":"","sources":["../../../src/connectors/interactive-connector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhE;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,EAAE,CAAqC;;IASzC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB7C,WAAW,IAAI,OAAO,CAAC,aAAa,CAAC;IAOrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import * as logger from '../util/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interactive connector — the user manually relays questions to the agent
|
|
5
|
+
* and pastes back responses. Useful when the agent doesn't have an HTTP API.
|
|
6
|
+
*/
|
|
7
|
+
export class InteractiveConnector {
|
|
8
|
+
rl;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.rl = createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stderr, // use stderr so stdout stays clean for report output
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async sendMessage(message) {
|
|
16
|
+
logger.heading('Question for the agent:');
|
|
17
|
+
console.error(`\n${message}\n`);
|
|
18
|
+
console.error('---');
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
console.error('Paste the agent\'s response below (end with an empty line):');
|
|
21
|
+
const lines = [];
|
|
22
|
+
const lineHandler = (line) => {
|
|
23
|
+
if (line === '' && lines.length > 0) {
|
|
24
|
+
this.rl.removeListener('line', lineHandler);
|
|
25
|
+
resolve(lines.join('\n'));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
lines.push(line);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
this.rl.on('line', lineHandler);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async getMetadata() {
|
|
35
|
+
return {
|
|
36
|
+
provider: 'interactive',
|
|
37
|
+
description: 'Manual relay — user copies questions to the agent',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async close() {
|
|
41
|
+
this.rl.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=interactive-connector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive-connector.js","sourceRoot":"","sources":["../../../src/connectors/interactive-connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAE5C;;;GAGG;AACH,MAAM,OAAO,oBAAoB;IACvB,EAAE,CAAqC;IAE/C;QACE,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC;YACxB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,qDAAqD;SAC9E,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;QAC1C,OAAO,CAAC,KAAK,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAErB,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;YACrC,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;YAC7E,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,EAAE;gBACnC,IAAI,IAAI,KAAK,EAAE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACpC,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;oBAC5C,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC5B,CAAC;qBAAM,CAAC;oBACN,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC,CAAC;YACF,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO;YACL,QAAQ,EAAE,aAAa;YACvB,WAAW,EAAE,mDAAmD;SACjE,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AgentMetadata {
|
|
2
|
+
name?: string;
|
|
3
|
+
model?: string;
|
|
4
|
+
provider?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AgentConnector {
|
|
8
|
+
/** Send a message to the target agent and get a response */
|
|
9
|
+
sendMessage(message: string): Promise<string>;
|
|
10
|
+
/** Get metadata about the target agent, if available */
|
|
11
|
+
getMetadata?(): Promise<AgentMetadata>;
|
|
12
|
+
/** Clean up connection resources */
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/connectors/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9C,wDAAwD;IACxD,WAAW,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAEvC,oCAAoC;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/connectors/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HeronConfig } from './config/schema.js';
|
|
2
|
+
export interface RunOptions {
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
maxFollowUps?: number;
|
|
5
|
+
reportDir?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Main entry point — runs the full interrogation pipeline:
|
|
9
|
+
* connect → interview → analyze → report
|
|
10
|
+
*/
|
|
11
|
+
export declare function run(config: HeronConfig, options?: RunOptions): Promise<string>;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKtD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAwDxF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createConnector } from './connectors/index.js';
|
|
2
|
+
import { createLLMClient } from './llm/client.js';
|
|
3
|
+
import { runInterview } from './interview/interviewer.js';
|
|
4
|
+
import { generateReport } from './report/generator.js';
|
|
5
|
+
import * as logger from './util/logger.js';
|
|
6
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { generateId } from './util/id.js';
|
|
8
|
+
/**
|
|
9
|
+
* Main entry point — runs the full interrogation pipeline:
|
|
10
|
+
* connect → interview → analyze → report
|
|
11
|
+
*/
|
|
12
|
+
export async function run(config, options = {}) {
|
|
13
|
+
const { verbose = false, maxFollowUps = 3, reportDir = './reports' } = options;
|
|
14
|
+
// 1. Create LLM client for analysis
|
|
15
|
+
const llmClient = await createLLMClient(config.llm);
|
|
16
|
+
// 2. Connect to target agent
|
|
17
|
+
const connector = createConnector(config.target);
|
|
18
|
+
const targetLabel = config.target.url ?? config.target.type;
|
|
19
|
+
const scanId = generateId('scan');
|
|
20
|
+
logger.raw('');
|
|
21
|
+
logger.raw(` \x1b[1mHeron Agent Interrogator\x1b[0m`);
|
|
22
|
+
logger.raw('');
|
|
23
|
+
logger.raw(` Scan: ${scanId}`);
|
|
24
|
+
logger.raw(` Target: ${targetLabel}`);
|
|
25
|
+
try {
|
|
26
|
+
// 3. Run interview
|
|
27
|
+
const session = await runInterview(connector, llmClient, {
|
|
28
|
+
maxFollowUps,
|
|
29
|
+
verbose,
|
|
30
|
+
});
|
|
31
|
+
// 4. Generate report
|
|
32
|
+
logger.raw('');
|
|
33
|
+
logger.raw(` \x1b[33m⏳ Analyzing transcript...\x1b[0m`);
|
|
34
|
+
const { report, reportJson } = await generateReport(session, llmClient, {
|
|
35
|
+
target: targetLabel,
|
|
36
|
+
format: config.output.format,
|
|
37
|
+
});
|
|
38
|
+
const riskLevel = reportJson.overallRiskLevel;
|
|
39
|
+
const riskColor = riskLevel === 'high' || riskLevel === 'critical' ? '\x1b[31m'
|
|
40
|
+
: riskLevel === 'medium' ? '\x1b[33m'
|
|
41
|
+
: '\x1b[32m';
|
|
42
|
+
// 5. Save report to file
|
|
43
|
+
mkdirSync(reportDir, { recursive: true });
|
|
44
|
+
const savePath = config.output.path ?? `${reportDir}/${scanId}.md`;
|
|
45
|
+
writeFileSync(savePath, report, 'utf-8');
|
|
46
|
+
logger.raw('');
|
|
47
|
+
logger.raw(` \x1b[1mAudit complete: ${scanId}\x1b[0m`);
|
|
48
|
+
logger.raw(` Risk: ${riskColor}${riskLevel.toUpperCase()}\x1b[0m`);
|
|
49
|
+
logger.raw(` Data quality: ${reportJson.dataQuality?.score ?? 'N/A'}/100`);
|
|
50
|
+
logger.raw(` Verdict: ${reportJson.recommendation ?? 'APPROVE WITH CONDITIONS'}`);
|
|
51
|
+
logger.raw(` Findings: ${reportJson.risks.length}`);
|
|
52
|
+
logger.raw(` Report: ${savePath}`);
|
|
53
|
+
logger.raw('');
|
|
54
|
+
return report;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await connector.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentConnector } from '../connectors/types.js';
|
|
2
|
+
import type { LLMClient } from '../llm/client.js';
|
|
3
|
+
import type { QAPair } from '../report/types.js';
|
|
4
|
+
export interface InterviewOptions {
|
|
5
|
+
maxFollowUps?: number;
|
|
6
|
+
verbose?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface InterviewSession {
|
|
9
|
+
transcript: QAPair[];
|
|
10
|
+
startedAt: Date;
|
|
11
|
+
completedAt: Date;
|
|
12
|
+
questionsAsked: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs a structured interview with a target agent.
|
|
16
|
+
* Asks core questions, generates follow-ups, collects all answers.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runInterview(connector: AgentConnector, llmClient: LLMClient, options?: InterviewOptions): Promise<InterviewSession>;
|
|
19
|
+
//# sourceMappingURL=interviewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interviewer.d.ts","sourceRoot":"","sources":["../../../src/interview/interviewer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAIjD,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,IAAI,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,cAAc,EACzB,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CAyE3B"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createProtocol } from './protocol.js';
|
|
2
|
+
import * as logger from '../util/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runs a structured interview with a target agent.
|
|
5
|
+
* Asks core questions, generates follow-ups, collects all answers.
|
|
6
|
+
*/
|
|
7
|
+
export async function runInterview(connector, llmClient, options = {}) {
|
|
8
|
+
const { maxFollowUps = 3 } = options;
|
|
9
|
+
const protocol = createProtocol(llmClient, maxFollowUps);
|
|
10
|
+
const startedAt = new Date();
|
|
11
|
+
const total = protocol.totalCoreQuestions;
|
|
12
|
+
let coreNum = 0;
|
|
13
|
+
logger.raw('');
|
|
14
|
+
// Ask all core questions with follow-ups between category changes
|
|
15
|
+
let prevCategory = null;
|
|
16
|
+
for (let i = 0; i < total; i++) {
|
|
17
|
+
const question = protocol.nextQuestion();
|
|
18
|
+
if (!question)
|
|
19
|
+
break;
|
|
20
|
+
// If category changed, try a follow-up on the previous category
|
|
21
|
+
if (prevCategory && question.category !== prevCategory) {
|
|
22
|
+
const followUp = await protocol.generateFollowUp(prevCategory);
|
|
23
|
+
if (followUp) {
|
|
24
|
+
// Show follow-up question
|
|
25
|
+
logger.raw('');
|
|
26
|
+
logger.raw(` \x1b[36mFollow-up\x1b[0m \x1b[2m[${followUp.category}]\x1b[0m`);
|
|
27
|
+
logger.raw(` \x1b[36mQ:\x1b[0m ${followUp.text}`);
|
|
28
|
+
const followUpAnswer = await connector.sendMessage(followUp.text);
|
|
29
|
+
protocol.recordAnswer(followUp, followUpAnswer);
|
|
30
|
+
logger.raw(` \x1b[2mA:\x1b[0m ${followUpAnswer}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
coreNum++;
|
|
34
|
+
// Show core question
|
|
35
|
+
logger.raw('');
|
|
36
|
+
logger.raw(` \x1b[36mQ${coreNum}/${total}\x1b[0m \x1b[2m[${question.category}]\x1b[0m`);
|
|
37
|
+
logger.raw(` \x1b[36mQ:\x1b[0m ${question.text}`);
|
|
38
|
+
const answer = await connector.sendMessage(question.text);
|
|
39
|
+
protocol.recordAnswer(question, answer);
|
|
40
|
+
logger.raw(` \x1b[2mA:\x1b[0m ${answer}`);
|
|
41
|
+
prevCategory = question.category;
|
|
42
|
+
}
|
|
43
|
+
// Final follow-up on the last category
|
|
44
|
+
const transcript = protocol.getTranscript();
|
|
45
|
+
if (transcript.length > 0) {
|
|
46
|
+
const lastCategory = transcript[transcript.length - 1].category;
|
|
47
|
+
const finalFollowUp = await protocol.generateFollowUp(lastCategory);
|
|
48
|
+
if (finalFollowUp) {
|
|
49
|
+
logger.raw('');
|
|
50
|
+
logger.raw(` \x1b[36mFollow-up\x1b[0m \x1b[2m[${finalFollowUp.category}]\x1b[0m`);
|
|
51
|
+
logger.raw(` \x1b[36mQ:\x1b[0m ${finalFollowUp.text}`);
|
|
52
|
+
const answer = await connector.sendMessage(finalFollowUp.text);
|
|
53
|
+
protocol.recordAnswer(finalFollowUp, answer);
|
|
54
|
+
logger.raw(` \x1b[2mA:\x1b[0m ${answer}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const completedAt = new Date();
|
|
58
|
+
const finalTranscript = protocol.getTranscript();
|
|
59
|
+
logger.raw('');
|
|
60
|
+
logger.success(`Interview complete — ${finalTranscript.length} questions asked`);
|
|
61
|
+
return {
|
|
62
|
+
transcript: finalTranscript,
|
|
63
|
+
startedAt,
|
|
64
|
+
completedAt,
|
|
65
|
+
questionsAsked: finalTranscript.length,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=interviewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interviewer.js","sourceRoot":"","sources":["../../../src/interview/interviewer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { QAPair } from '../report/types.js';
|
|
2
|
+
import type { LLMClient } from '../llm/client.js';
|
|
3
|
+
import { type InterviewQuestion } from './questions.js';
|
|
4
|
+
export interface InterviewProtocol {
|
|
5
|
+
/** Get the next question to ask, or null if interview is complete */
|
|
6
|
+
nextQuestion(): InterviewQuestion | null;
|
|
7
|
+
/** Record an answer. Returns false if answer was skipped (greeting/repeat). */
|
|
8
|
+
recordAnswer(question: InterviewQuestion, answer: string): boolean;
|
|
9
|
+
/** Generate a follow-up question based on context and missing compliance fields */
|
|
10
|
+
generateFollowUp(category: QAPair['category']): Promise<InterviewQuestion | null>;
|
|
11
|
+
/** Get the full transcript so far */
|
|
12
|
+
getTranscript(): QAPair[];
|
|
13
|
+
/** Check if the interview is complete */
|
|
14
|
+
isComplete(): boolean;
|
|
15
|
+
/** Total number of core questions (excluding follow-ups) */
|
|
16
|
+
totalCoreQuestions: number;
|
|
17
|
+
}
|
|
18
|
+
/** Detect if an answer is just a greeting with no substantive content */
|
|
19
|
+
export declare function isGreeting(answer: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Detect if an answer is clearly responding to a different question (stale session).
|
|
22
|
+
* Returns true if the answer strongly matches a DIFFERENT question's topic
|
|
23
|
+
* but has no relevance to the current question.
|
|
24
|
+
*
|
|
25
|
+
* Conservative: only triggers on long answers (300+ chars) that match 2+ signals
|
|
26
|
+
* from a different topic AND zero signals from the current topic. Skips Q1 entirely
|
|
27
|
+
* since first answers are too varied to classify reliably.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isStaleAnswer(question: InterviewQuestion, answer: string): boolean;
|
|
30
|
+
/** Detect if an answer is too vague for compliance-grade reporting */
|
|
31
|
+
export declare function isVagueAnswer(answer: string): boolean;
|
|
32
|
+
export declare function createProtocol(llmClient: LLMClient, maxFollowUps?: number): InterviewProtocol;
|
|
33
|
+
/**
|
|
34
|
+
* Enqueue a follow-up question to be asked next.
|
|
35
|
+
* Used by SessionManager to avoid monkey-patching protocol.nextQuestion.
|
|
36
|
+
*/
|
|
37
|
+
export declare function enqueueFollowUp(protocol: InterviewProtocol, followUp: InterviewQuestion): void;
|
|
38
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/interview/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG/E,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,YAAY,IAAI,iBAAiB,GAAG,IAAI,CAAC;IAEzC,+EAA+E;IAC/E,YAAY,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAEnE,mFAAmF;IACnF,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAElF,qCAAqC;IACrC,aAAa,IAAI,MAAM,EAAE,CAAC;IAE1B,yCAAyC;IACzC,UAAU,IAAI,OAAO,CAAC;IAEtB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAiBD,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKlD;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAuBlF;AA8BD,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAgED,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,SAAI,GAAG,iBAAiB,CA2HxF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAO9F"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { getAllQuestionsSorted } from './questions.js';
|
|
2
|
+
import { INTERVIEW_SYSTEM_PROMPT, buildFollowUpPrompt } from '../llm/prompts.js';
|
|
3
|
+
// ─── Greeting detection ──────────────────────────────────────────────────────
|
|
4
|
+
const GREETING_PATTERNS = [
|
|
5
|
+
/^hi\b/i,
|
|
6
|
+
/^hello\b/i,
|
|
7
|
+
/^hey\b/i,
|
|
8
|
+
/ready to answer/i,
|
|
9
|
+
/ready for questions/i,
|
|
10
|
+
/^i am ready/i,
|
|
11
|
+
/^i'm ready/i,
|
|
12
|
+
/let'?s begin/i,
|
|
13
|
+
/let'?s start/i,
|
|
14
|
+
/^greetings/i,
|
|
15
|
+
];
|
|
16
|
+
/** Detect if an answer is just a greeting with no substantive content */
|
|
17
|
+
export function isGreeting(answer) {
|
|
18
|
+
const trimmed = answer.trim();
|
|
19
|
+
// Short answers that match greeting patterns
|
|
20
|
+
if (trimmed.length > 200)
|
|
21
|
+
return false; // Long answers aren't greetings
|
|
22
|
+
return GREETING_PATTERNS.some(p => p.test(trimmed));
|
|
23
|
+
}
|
|
24
|
+
// ─── Stale / off-topic answer detection ─────────────────────────────────────
|
|
25
|
+
/** Topic keywords per compliance field — used to detect answers to the wrong question */
|
|
26
|
+
const TOPIC_SIGNALS = {
|
|
27
|
+
agentProfile: [
|
|
28
|
+
/\bproject.?name\b/i, /\bowner\b/i, /\btrigger/i, /\bwhat I (do|specifically)\b/i,
|
|
29
|
+
],
|
|
30
|
+
systemId: [
|
|
31
|
+
/\b(→|->)\s*(REST|API|OAuth|SDK|Bot)\b/i, /\bconnect to\b/i, /\bsystems?\s+I\b/i,
|
|
32
|
+
],
|
|
33
|
+
scopesRequested: [
|
|
34
|
+
/\boauth\s*scop/i, /\bgoogleapis\.com\/auth\//i, /\b(readonly|readwrite|\.edit|\.send|\.admin)\b/i,
|
|
35
|
+
],
|
|
36
|
+
dataSensitivity: [
|
|
37
|
+
/\b(PII|financial|credentials|confidential|non.?sensitive)\b/i, /\bclassif(y|ied)\b/i,
|
|
38
|
+
],
|
|
39
|
+
writeOperations: [
|
|
40
|
+
/\b(→|->)\s*(Yes|No)\s*(→|->)/i, /\b(append|insert|create|delete)\s*(row|record|spreadsheet|message)/i,
|
|
41
|
+
/\bvolume\/day\b/i,
|
|
42
|
+
],
|
|
43
|
+
blastRadius: [
|
|
44
|
+
/\b(worst.?case|single.?record|single.?user|cross.?tenant|org.?wide)\b/i,
|
|
45
|
+
/\bcan it be (undone|recovered)\b/i,
|
|
46
|
+
],
|
|
47
|
+
frequencyAndVolume: [
|
|
48
|
+
/\b(times?\s+per|runs?\s+per|calls?\s+per|\/week|\/day)\b/i, /\bbatch\s+size\b/i,
|
|
49
|
+
],
|
|
50
|
+
scopesDelta: [
|
|
51
|
+
/\bnever\s+(actually\s+)?used\b/i, /\bsafely\s+(be\s+)?revoked\b/i, /\bunused\s+permission/i,
|
|
52
|
+
],
|
|
53
|
+
riskAssessment: [
|
|
54
|
+
/\bworst\s+realistic\s+failure\b/i, /\bwho\s+is\s+affected\b/i, /\bhow\s+bad\s+is\s+the\s+damage\b/i,
|
|
55
|
+
/\bcan\s+it\s+be\s+recovered\b/i,
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Detect if an answer is clearly responding to a different question (stale session).
|
|
60
|
+
* Returns true if the answer strongly matches a DIFFERENT question's topic
|
|
61
|
+
* but has no relevance to the current question.
|
|
62
|
+
*
|
|
63
|
+
* Conservative: only triggers on long answers (300+ chars) that match 2+ signals
|
|
64
|
+
* from a different topic AND zero signals from the current topic. Skips Q1 entirely
|
|
65
|
+
* since first answers are too varied to classify reliably.
|
|
66
|
+
*/
|
|
67
|
+
export function isStaleAnswer(question, answer) {
|
|
68
|
+
if (answer.length < 300)
|
|
69
|
+
return false; // Only check long, detailed answers
|
|
70
|
+
const currentField = question.complianceField;
|
|
71
|
+
if (!currentField)
|
|
72
|
+
return false;
|
|
73
|
+
// Never flag Q1 (agentProfile) — first answers are too varied
|
|
74
|
+
if (currentField === 'agentProfile')
|
|
75
|
+
return false;
|
|
76
|
+
const currentSignals = TOPIC_SIGNALS[currentField] ?? [];
|
|
77
|
+
const matchesCurrent = currentSignals.some(p => p.test(answer));
|
|
78
|
+
// If the answer matches the current question's topic at all, it's not stale
|
|
79
|
+
if (matchesCurrent)
|
|
80
|
+
return false;
|
|
81
|
+
// Check if it strongly matches a different question's topic (need 3+ signals)
|
|
82
|
+
let strongOtherMatch = false;
|
|
83
|
+
for (const [field, signals] of Object.entries(TOPIC_SIGNALS)) {
|
|
84
|
+
if (field === currentField)
|
|
85
|
+
continue;
|
|
86
|
+
const matches = signals.filter(p => p.test(answer)).length;
|
|
87
|
+
if (matches >= 3) {
|
|
88
|
+
strongOtherMatch = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return strongOtherMatch;
|
|
93
|
+
}
|
|
94
|
+
// ─── Vagueness detection ─────────────────────────────────────────────────────
|
|
95
|
+
/** Vagueness indicators — if an answer matches these patterns, it needs a follow-up */
|
|
96
|
+
const VAGUE_PATTERNS = [
|
|
97
|
+
/\b(the database|a database|some database)\b/i,
|
|
98
|
+
/\b(read and write|read\/write|full access)\b/i,
|
|
99
|
+
/\b(user data|some data|the data)\b/i,
|
|
100
|
+
/\b(regularly|periodically|sometimes|occasionally|as needed|when needed)\b/i,
|
|
101
|
+
/\b(could affect|might affect|may affect)\b/i,
|
|
102
|
+
/\b(various|several|multiple|many|some)\s+(systems?|apis?|services?|databases?)/i,
|
|
103
|
+
/\b(everything|all access|full permissions?)\b/i,
|
|
104
|
+
/\bi[' ']?m not sure\b/i,
|
|
105
|
+
/\bnot sure\b/i,
|
|
106
|
+
/\bi don[' ']?t know\b/i,
|
|
107
|
+
// Hedging language — agent describes theoretical capabilities, not actual behavior
|
|
108
|
+
/\bi may (also )?(read|write|access|connect|use|modify|create|delete)\b/i,
|
|
109
|
+
/\bwhen enabled\b/i,
|
|
110
|
+
/\bif the task (requires|involves|needs)\b/i,
|
|
111
|
+
/\bwhen (available|needed|required|the workflow)\b/i,
|
|
112
|
+
/\b(can include|could include|may include)\b/i,
|
|
113
|
+
/\baccess is (environment|session|task).dependent\b/i,
|
|
114
|
+
/\bdepending on (the )?task\b/i,
|
|
115
|
+
/\bconnectors? (are |is )?enabled\b/i,
|
|
116
|
+
// Generic tool descriptions instead of specific project usage
|
|
117
|
+
/\b(local workspace|active workspace|working directory)\b/i,
|
|
118
|
+
/\bconnected (development )?tools\b/i,
|
|
119
|
+
];
|
|
120
|
+
/** Detect if an answer is too vague for compliance-grade reporting */
|
|
121
|
+
export function isVagueAnswer(answer) {
|
|
122
|
+
return VAGUE_PATTERNS.some(p => p.test(answer));
|
|
123
|
+
}
|
|
124
|
+
// ─── Repeated answer detection ───────────────────────────────────────────────
|
|
125
|
+
/** Normalize answer text for comparison (trim, lowercase, collapse whitespace) */
|
|
126
|
+
function normalizeForComparison(text) {
|
|
127
|
+
return text.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
128
|
+
}
|
|
129
|
+
/** Check if an answer is a repeat of a previous answer */
|
|
130
|
+
function isRepeatedAnswer(answer, transcript) {
|
|
131
|
+
if (transcript.length === 0)
|
|
132
|
+
return false;
|
|
133
|
+
const normalized = normalizeForComparison(answer);
|
|
134
|
+
// Check for exact or near-exact repeats
|
|
135
|
+
return transcript.some(qa => {
|
|
136
|
+
const prevNormalized = normalizeForComparison(qa.answer);
|
|
137
|
+
// Exact match or >90% overlap (handles minor variations)
|
|
138
|
+
return normalized === prevNormalized ||
|
|
139
|
+
(normalized.length > 50 && prevNormalized.includes(normalized.slice(0, normalized.length * 0.9 | 0)));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// ─── Missing compliance fields ───────────────────────────────────────────────
|
|
143
|
+
/** Check which compliance fields are missing from the transcript for a given category */
|
|
144
|
+
function findMissingFields(transcript) {
|
|
145
|
+
const allText = transcript.map(qa => qa.answer.toLowerCase()).join(' ');
|
|
146
|
+
const missing = [];
|
|
147
|
+
// Check for specific system identifiers
|
|
148
|
+
if (!/\b(api|oauth|sdk|via|using)\b/i.test(allText)) {
|
|
149
|
+
missing.push('systemId (specific API names, auth methods)');
|
|
150
|
+
}
|
|
151
|
+
// Check for specific scopes
|
|
152
|
+
if (!/\b(scope|permission|role|\.readonly|\.send|\.modify|\.admin)\b/i.test(allText)) {
|
|
153
|
+
missing.push('scopesRequested (specific OAuth scopes, API permissions)');
|
|
154
|
+
}
|
|
155
|
+
// Check for data sensitivity classification
|
|
156
|
+
if (!/\b(pii|sensitive|confidential|financial|personal|classified)\b/i.test(allText)) {
|
|
157
|
+
missing.push('dataSensitivity (PII, financial, confidential classification)');
|
|
158
|
+
}
|
|
159
|
+
// Check for blast radius
|
|
160
|
+
if (!/\b(single.?record|single.?user|team|org.?wide|cross.?tenant|mailbox|affected)\b/i.test(allText)) {
|
|
161
|
+
missing.push('blastRadius (scope of impact: single-record/user/team/org-wide)');
|
|
162
|
+
}
|
|
163
|
+
// Check for frequency numbers
|
|
164
|
+
if (!/\b(\d+\s*(times?|per|\/)\s*(day|hour|minute|week|session)|batch)\b/i.test(allText)) {
|
|
165
|
+
missing.push('frequencyAndVolume (specific numbers: times/day, batch size)');
|
|
166
|
+
}
|
|
167
|
+
// Check for write reversibility
|
|
168
|
+
if (!/\b(revers|rollback|undo|irrevers|cannot be undone|can be restored)\b/i.test(allText)) {
|
|
169
|
+
missing.push('writeOperations.reversible (whether writes can be rolled back)');
|
|
170
|
+
}
|
|
171
|
+
return missing;
|
|
172
|
+
}
|
|
173
|
+
// ─── Protocol factory ────────────────────────────────────────────────────────
|
|
174
|
+
export function createProtocol(llmClient, maxFollowUps = 6) {
|
|
175
|
+
const coreQuestions = getAllQuestionsSorted();
|
|
176
|
+
let currentIndex = 0;
|
|
177
|
+
const transcript = [];
|
|
178
|
+
let globalFollowUpCount = 0;
|
|
179
|
+
const followUpCountPerQuestion = new Map();
|
|
180
|
+
let repeatedAnswerCount = 0;
|
|
181
|
+
// Follow-up queue
|
|
182
|
+
const followUpQueue = [];
|
|
183
|
+
return {
|
|
184
|
+
totalCoreQuestions: coreQuestions.length,
|
|
185
|
+
nextQuestion() {
|
|
186
|
+
// Drain follow-up queue first
|
|
187
|
+
if (followUpQueue.length > 0) {
|
|
188
|
+
return followUpQueue.shift();
|
|
189
|
+
}
|
|
190
|
+
if (currentIndex >= coreQuestions.length) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return coreQuestions[currentIndex++];
|
|
194
|
+
},
|
|
195
|
+
recordAnswer(question, answer) {
|
|
196
|
+
// Skip greetings — don't record them as answers
|
|
197
|
+
if (transcript.length === 0 && isGreeting(answer)) {
|
|
198
|
+
// Don't rewind currentIndex — SessionManager will re-ask pendingQuestion directly
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
// Detect stale answers from a lost/different session
|
|
202
|
+
if (isStaleAnswer(question, answer)) {
|
|
203
|
+
// Don't rewind — SessionManager will re-ask pendingQuestion directly
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
// Detect repeated/canned answers
|
|
207
|
+
if (isRepeatedAnswer(answer, transcript)) {
|
|
208
|
+
repeatedAnswerCount++;
|
|
209
|
+
// Still record it (for transparency in transcript) but mark the category
|
|
210
|
+
transcript.push({
|
|
211
|
+
question: question.text,
|
|
212
|
+
answer: `[REPEATED RESPONSE] ${answer}`,
|
|
213
|
+
category: question.category,
|
|
214
|
+
});
|
|
215
|
+
// After 3+ repeats, stop generating follow-ups — agent is stuck
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
transcript.push({
|
|
219
|
+
question: question.text,
|
|
220
|
+
answer,
|
|
221
|
+
category: question.category,
|
|
222
|
+
});
|
|
223
|
+
return true;
|
|
224
|
+
},
|
|
225
|
+
async generateFollowUp(category) {
|
|
226
|
+
// Global cap
|
|
227
|
+
if (globalFollowUpCount >= maxFollowUps)
|
|
228
|
+
return null;
|
|
229
|
+
// Per-question cap (2 follow-ups per core question)
|
|
230
|
+
const lastCoreQ = coreQuestions.find(q => q.category === category && transcript.some(t => t.question === q.text));
|
|
231
|
+
if (lastCoreQ) {
|
|
232
|
+
const count = followUpCountPerQuestion.get(lastCoreQ.id) ?? 0;
|
|
233
|
+
if (count >= 2)
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
// Don't follow up if agent is repeating canned responses
|
|
237
|
+
if (repeatedAnswerCount >= 3)
|
|
238
|
+
return null;
|
|
239
|
+
const categoryQA = transcript.filter(qa => qa.category === category);
|
|
240
|
+
if (categoryQA.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
// Check if the last answer was vague
|
|
243
|
+
const lastAnswer = categoryQA[categoryQA.length - 1].answer;
|
|
244
|
+
const vague = isVagueAnswer(lastAnswer);
|
|
245
|
+
// Find missing compliance fields across all transcript
|
|
246
|
+
const missingFields = findMissingFields(transcript);
|
|
247
|
+
// Only generate follow-up if answer was vague or compliance fields are missing
|
|
248
|
+
if (!vague && missingFields.length === 0)
|
|
249
|
+
return null;
|
|
250
|
+
try {
|
|
251
|
+
const followUpText = await llmClient.chat(INTERVIEW_SYSTEM_PROMPT, buildFollowUpPrompt(category, categoryQA, missingFields.length > 0 ? missingFields : undefined));
|
|
252
|
+
if (!followUpText.trim())
|
|
253
|
+
return null;
|
|
254
|
+
globalFollowUpCount++;
|
|
255
|
+
if (lastCoreQ) {
|
|
256
|
+
followUpCountPerQuestion.set(lastCoreQ.id, (followUpCountPerQuestion.get(lastCoreQ.id) ?? 0) + 1);
|
|
257
|
+
}
|
|
258
|
+
const followUp = {
|
|
259
|
+
id: `followup_${category}_${globalFollowUpCount}`,
|
|
260
|
+
category,
|
|
261
|
+
text: followUpText.trim(),
|
|
262
|
+
priority: 100 + globalFollowUpCount,
|
|
263
|
+
};
|
|
264
|
+
return followUp;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
getTranscript() {
|
|
271
|
+
return [...transcript];
|
|
272
|
+
},
|
|
273
|
+
isComplete() {
|
|
274
|
+
return currentIndex >= coreQuestions.length && followUpQueue.length === 0;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Enqueue a follow-up question to be asked next.
|
|
280
|
+
* Used by SessionManager to avoid monkey-patching protocol.nextQuestion.
|
|
281
|
+
*/
|
|
282
|
+
export function enqueueFollowUp(protocol, followUp) {
|
|
283
|
+
// The follow-up queue is internal to createProtocol, so we need another approach.
|
|
284
|
+
// Instead, we expose a method to push into the queue via the protocol's nextQuestion behavior.
|
|
285
|
+
// Actually, the clean way is to just have SessionManager track its own queue — see sessions.ts.
|
|
286
|
+
// This function exists for the scan/CLI path where the interviewer handles follow-ups inline.
|
|
287
|
+
void protocol;
|
|
288
|
+
void followUp;
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=protocol.js.map
|