@clawtrial/courtroom 1.0.6 → 2.0.1
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 +64 -41
- package/openclaw.plugin.json +11 -0
- package/package.json +21 -25
- package/scripts/postinstall.js +10 -110
- package/skills/courtroom/SKILL.md +49 -0
- package/src/api.js +12 -11
- package/src/crypto.js +5 -5
- package/src/debug.js +49 -121
- package/src/detector.js +40 -38
- package/src/hearing.js +246 -75
- package/src/plugin.js +435 -0
- package/src/punishment.js +13 -13
- package/src/storage.js +35 -119
- package/AGENT_CONFIG.md +0 -66
- package/OPENCLAW_FIX.md +0 -127
- package/OPENCLAW_INSTALL.md +0 -63
- package/SECURITY.md +0 -124
- package/SKILL.md +0 -91
- package/SUBAGENT_APPROACH.md +0 -124
- package/TECHNICAL_OVERVIEW.md +0 -278
- package/_meta.json +0 -14
- package/clawdbot.plugin.json +0 -32
- package/icon.txt +0 -1
- package/scripts/check-and-trigger.js +0 -139
- package/scripts/clawtrial.js +0 -968
- package/scripts/clawtrial.js.bak +0 -531
- package/scripts/cli.js +0 -184
- package/scripts/optimized-cron-check.js +0 -137
- package/scripts/setup-cron.js +0 -118
- package/scripts/trigger-evaluation.js +0 -86
- package/skill.yaml +0 -28
- package/src/autostart.js +0 -175
- package/src/config.js +0 -207
- package/src/consent.js +0 -217
- package/src/core.js +0 -208
- package/src/daemon.js +0 -152
- package/src/detector-v1.js +0 -572
- package/src/environment.js +0 -344
- package/src/evaluator.js +0 -277
- package/src/hook.js +0 -266
- package/src/index.js +0 -373
- package/src/monitor.js +0 -194
- package/src/skill.js +0 -372
- package/src/standalone.js +0 -248
package/src/detector.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { OFFENSES } = require('./offenses');
|
|
9
|
+
const { logger } = require('./debug');
|
|
9
10
|
|
|
10
11
|
class SemanticOffenseDetector {
|
|
11
12
|
constructor(agentRuntime, configManager) {
|
|
@@ -16,7 +17,7 @@ class SemanticOffenseDetector {
|
|
|
16
17
|
this.lastCaseDate = null;
|
|
17
18
|
this.cooldowns = new Map();
|
|
18
19
|
this.conversationEmbeddings = [];
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
// Evaluation cache to avoid repeated LLM calls
|
|
21
22
|
this.evaluationCache = new Map();
|
|
22
23
|
this.cacheMaxSize = 100;
|
|
@@ -39,12 +40,12 @@ class SemanticOffenseDetector {
|
|
|
39
40
|
|
|
40
41
|
// Build context for LLM evaluation
|
|
41
42
|
const context = this.buildContext(sessionHistory);
|
|
42
|
-
|
|
43
|
+
|
|
43
44
|
// Evaluate each offense using LLM
|
|
44
45
|
const evaluations = [];
|
|
45
46
|
for (const offense of Object.values(OFFENSES)) {
|
|
46
47
|
if (this.isOffenseOnCooldown(offense.id)) continue;
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
const evaluation = await this.evaluateWithLLM(offense, context, agentMemory);
|
|
49
50
|
if (evaluation.isViolation && evaluation.confidence >= this.config.get('detection.minConfidence')) {
|
|
50
51
|
evaluations.push({
|
|
@@ -91,7 +92,7 @@ class SemanticOffenseDetector {
|
|
|
91
92
|
buildContext(history) {
|
|
92
93
|
const windowSize = this.config.get('detection.evaluationWindow');
|
|
93
94
|
const recentHistory = history.slice(-windowSize);
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
return {
|
|
96
97
|
fullConversation: history.map(h => `${h.role}: ${h.content}`).join('\n'),
|
|
97
98
|
recentTurns: recentHistory,
|
|
@@ -109,17 +110,17 @@ class SemanticOffenseDetector {
|
|
|
109
110
|
async evaluateWithLLM(offense, context, agentMemory) {
|
|
110
111
|
// Generate cache key from offense + conversation hash
|
|
111
112
|
const cacheKey = this.generateCacheKey(offense.id, context);
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
// Check cache first
|
|
114
115
|
const cached = this.getCachedEvaluation(cacheKey);
|
|
115
116
|
if (cached) {
|
|
116
117
|
return cached;
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
+
|
|
119
120
|
// Try LLM evaluation first
|
|
120
121
|
if (this.agent && this.agent.llm) {
|
|
121
|
-
const prompt = this.buildEvaluationPrompt(offense, context, agentMemory);
|
|
122
|
-
|
|
122
|
+
const prompt = await this.buildEvaluationPrompt(offense, context, agentMemory);
|
|
123
|
+
|
|
123
124
|
try {
|
|
124
125
|
const response = await this.agent.llm.call({
|
|
125
126
|
model: this.agent.model?.primary || 'default',
|
|
@@ -129,36 +130,36 @@ class SemanticOffenseDetector {
|
|
|
129
130
|
});
|
|
130
131
|
|
|
131
132
|
const result = this.parseEvaluationResponse(response.content || response);
|
|
132
|
-
|
|
133
|
+
|
|
133
134
|
// Cache the result
|
|
134
135
|
this.setCachedEvaluation(cacheKey, result);
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
return result;
|
|
137
138
|
} catch (error) {
|
|
138
139
|
logger.error('DETECTOR', 'LLM evaluation failed, falling back to pattern matching', { error: error.message });
|
|
139
140
|
// Fall through to pattern matching
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
+
|
|
143
144
|
// Fallback: Use simple pattern matching for basic offenses
|
|
144
145
|
return this.evaluateWithPatternMatching(offense, context);
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
+
|
|
147
148
|
/**
|
|
148
149
|
* Fallback evaluation using simple pattern matching
|
|
149
150
|
*/
|
|
150
151
|
evaluateWithPatternMatching(offense, context) {
|
|
151
152
|
const userMessages = context.userMessages;
|
|
152
|
-
|
|
153
|
+
|
|
153
154
|
// Circular Reference detection: same question asked multiple times
|
|
154
155
|
if (offense.id === 'circular_reference') {
|
|
155
156
|
if (userMessages.length >= 3) {
|
|
156
157
|
const lastThree = userMessages.slice(-3);
|
|
157
158
|
// Check if the last 3 messages are semantically similar
|
|
158
|
-
const similarity = this.calculateSimilarity(lastThree[0], lastThree[1]) +
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
const similarity = this.calculateSimilarity(lastThree[0], lastThree[1]) +
|
|
160
|
+
this.calculateSimilarity(lastThree[1], lastThree[2]) +
|
|
161
|
+
this.calculateSimilarity(lastThree[0], lastThree[2]);
|
|
162
|
+
|
|
162
163
|
if (similarity >= 1.5) { // At least 2 pairs are similar
|
|
163
164
|
return {
|
|
164
165
|
isViolation: true,
|
|
@@ -168,14 +169,14 @@ class SemanticOffenseDetector {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
|
-
|
|
172
|
+
|
|
172
173
|
// Validation Vampire: seeking reassurance
|
|
173
174
|
if (offense.id === 'validation_vampire') {
|
|
174
175
|
const reassurancePatterns = ['right?', 'correct?', 'is that right?', 'am i right?', 'do you agree?', 'make sense?'];
|
|
175
|
-
const reassuranceCount = userMessages.filter(msg =>
|
|
176
|
+
const reassuranceCount = userMessages.filter(msg =>
|
|
176
177
|
reassurancePatterns.some(pattern => msg.toLowerCase().includes(pattern))
|
|
177
178
|
).length;
|
|
178
|
-
|
|
179
|
+
|
|
179
180
|
if (reassuranceCount >= 2) {
|
|
180
181
|
return {
|
|
181
182
|
isViolation: true,
|
|
@@ -184,32 +185,32 @@ class SemanticOffenseDetector {
|
|
|
184
185
|
};
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
+
|
|
188
189
|
// Default: no violation detected
|
|
189
190
|
return { isViolation: false, confidence: 0, evidence: null };
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
+
|
|
192
193
|
/**
|
|
193
194
|
* Calculate simple string similarity (0-1 scale)
|
|
194
195
|
*/
|
|
195
196
|
calculateSimilarity(str1, str2) {
|
|
196
197
|
if (!str1 || !str2) return 0;
|
|
197
|
-
|
|
198
|
+
|
|
198
199
|
const s1 = str1.toLowerCase().trim();
|
|
199
200
|
const s2 = str2.toLowerCase().trim();
|
|
200
|
-
|
|
201
|
+
|
|
201
202
|
// Exact match
|
|
202
203
|
if (s1 === s2) return 1.0;
|
|
203
|
-
|
|
204
|
+
|
|
204
205
|
// Check if one contains the other
|
|
205
206
|
if (s1.includes(s2) || s2.includes(s1)) return 0.8;
|
|
206
|
-
|
|
207
|
+
|
|
207
208
|
// Word overlap
|
|
208
209
|
const words1 = s1.split(/\s+/);
|
|
209
210
|
const words2 = s2.split(/\s+/);
|
|
210
211
|
const commonWords = words1.filter(w => words2.includes(w));
|
|
211
212
|
const overlap = (2 * commonWords.length) / (words1.length + words2.length);
|
|
212
|
-
|
|
213
|
+
|
|
213
214
|
return overlap;
|
|
214
215
|
}
|
|
215
216
|
|
|
@@ -241,13 +242,13 @@ class SemanticOffenseDetector {
|
|
|
241
242
|
getCachedEvaluation(key) {
|
|
242
243
|
const cached = this.evaluationCache.get(key);
|
|
243
244
|
if (!cached) return null;
|
|
244
|
-
|
|
245
|
+
|
|
245
246
|
// Check if cache entry is still valid
|
|
246
247
|
if (Date.now() - cached.timestamp > this.cacheTTL) {
|
|
247
248
|
this.evaluationCache.delete(key);
|
|
248
249
|
return null;
|
|
249
250
|
}
|
|
250
|
-
|
|
251
|
+
|
|
251
252
|
return cached.result;
|
|
252
253
|
}
|
|
253
254
|
|
|
@@ -260,7 +261,7 @@ class SemanticOffenseDetector {
|
|
|
260
261
|
const oldestKey = this.evaluationCache.keys().next().value;
|
|
261
262
|
this.evaluationCache.delete(oldestKey);
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
+
|
|
264
265
|
this.evaluationCache.set(key, {
|
|
265
266
|
result,
|
|
266
267
|
timestamp: Date.now()
|
|
@@ -277,7 +278,7 @@ class SemanticOffenseDetector {
|
|
|
277
278
|
/**
|
|
278
279
|
* Build evaluation prompt for LLM
|
|
279
280
|
*/
|
|
280
|
-
buildEvaluationPrompt(offense, context, agentMemory) {
|
|
281
|
+
async buildEvaluationPrompt(offense, context, agentMemory) {
|
|
281
282
|
const prompts = {
|
|
282
283
|
circular_reference: `
|
|
283
284
|
You are evaluating if the user is asking substantively similar questions repeatedly.
|
|
@@ -415,7 +416,7 @@ OFFENSE: The Promise Breaker
|
|
|
415
416
|
DEFINITION: Committing to actions ("I will...", "I'll do that...") and not following through.
|
|
416
417
|
|
|
417
418
|
PREVIOUS COMMITMENTS FROM MEMORY:
|
|
418
|
-
${this.getCommitmentsFromMemory(agentMemory)}
|
|
419
|
+
${await this.getCommitmentsFromMemory(agentMemory)}
|
|
419
420
|
|
|
420
421
|
CONVERSATION HISTORY:
|
|
421
422
|
${context.fullConversation}
|
|
@@ -776,7 +777,7 @@ Respond in JSON:
|
|
|
776
777
|
if (!jsonMatch) {
|
|
777
778
|
return { isViolation: false, confidence: 0, evidence: null };
|
|
778
779
|
}
|
|
779
|
-
|
|
780
|
+
|
|
780
781
|
const result = JSON.parse(jsonMatch[0]);
|
|
781
782
|
return {
|
|
782
783
|
isViolation: result.isViolation === true,
|
|
@@ -794,9 +795,10 @@ Respond in JSON:
|
|
|
794
795
|
* Get commitments from agent memory
|
|
795
796
|
*/
|
|
796
797
|
async getCommitmentsFromMemory(agentMemory) {
|
|
798
|
+
if (!agentMemory) return 'No previous commitments recorded.';
|
|
797
799
|
try {
|
|
798
800
|
const commitments = await agentMemory.get('courtroom_commitments') || [];
|
|
799
|
-
return commitments.map(c =>
|
|
801
|
+
return commitments.map(c =>
|
|
800
802
|
`- "${c.statement}" (${c.date}) - Completed: ${c.completed ? 'Yes' : 'No'}`
|
|
801
803
|
).join('\n') || 'No previous commitments recorded.';
|
|
802
804
|
} catch {
|
|
@@ -829,10 +831,10 @@ Respond in JSON:
|
|
|
829
831
|
analyzeSentiment(history) {
|
|
830
832
|
const userMessages = history.filter(h => h.role === 'user').map(h => h.content);
|
|
831
833
|
const text = userMessages.join(' ').toLowerCase();
|
|
832
|
-
|
|
834
|
+
|
|
833
835
|
const urgentWords = ['urgent', 'asap', 'emergency', 'critical', 'now', 'immediately'];
|
|
834
836
|
const frustratedWords = ['frustrated', 'annoying', 'stupid', 'useless', 'waste'];
|
|
835
|
-
|
|
837
|
+
|
|
836
838
|
return {
|
|
837
839
|
urgency: urgentWords.filter(w => text.includes(w)).length,
|
|
838
840
|
frustration: frustratedWords.filter(w => text.includes(w)).length,
|
|
@@ -846,12 +848,12 @@ Respond in JSON:
|
|
|
846
848
|
detectHumorTriggers(history) {
|
|
847
849
|
const triggers = [];
|
|
848
850
|
const recentContent = history.slice(-5).map(h => h.content.toLowerCase()).join(' ');
|
|
849
|
-
|
|
851
|
+
|
|
850
852
|
if (/again|repeat|said|already|before/.test(recentContent)) triggers.push('repetition_noted');
|
|
851
853
|
if (/sure|right|correct|think|should i/.test(recentContent)) triggers.push('validation_seeking');
|
|
852
854
|
if (/what if|but then|however|maybe/.test(recentContent)) triggers.push('overthinking');
|
|
853
855
|
if (/actually|by the way|speaking of/.test(recentContent)) triggers.push('deflection');
|
|
854
|
-
|
|
856
|
+
|
|
855
857
|
return triggers;
|
|
856
858
|
}
|
|
857
859
|
|
package/src/hearing.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hearing Pipeline -
|
|
2
|
+
* Hearing Pipeline - LLM-Based Deliberation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Conducts a full hearing using the agent's LLM:
|
|
5
|
+
* 1. Judge evaluates the evidence
|
|
6
|
+
* 2. Jury deliberates (3 jurors with distinct perspectives)
|
|
7
|
+
* 3. Votes are tallied
|
|
8
|
+
* 4. Verdict + sentence returned
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
const { JUDGE_SYSTEM_PROMPT, JUDGE_EVIDENCE_TEMPLATE } = require('./prompts/judge');
|
|
9
|
-
const { JUROR_ROLES } = require('./prompts/jury');
|
|
12
|
+
const { JUROR_ROLES, JURY_EVIDENCE_TEMPLATE } = require('./prompts/jury');
|
|
13
|
+
const { logger } = require('./debug');
|
|
10
14
|
|
|
11
15
|
class HearingPipeline {
|
|
12
16
|
constructor(agentRuntime, configManager) {
|
|
@@ -15,92 +19,259 @@ class HearingPipeline {
|
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* Conduct a full hearing using the agent's LLM
|
|
23
|
+
* Returns a verdict object with { guilty, caseId, verdict, offense, proceedings, timestamp }
|
|
20
24
|
*/
|
|
21
|
-
async
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
**Your Role:** Act as both Judge and Jury (3 jurors).
|
|
43
|
-
|
|
44
|
-
**Instructions:**
|
|
45
|
-
1. Review the case evidence above
|
|
46
|
-
2. As JUDGE: Analyze the evidence and provide a preliminary verdict
|
|
47
|
-
3. As JURY (3 different perspectives): Each juror votes GUILTY or NOT GUILTY with reasoning
|
|
48
|
-
4. Aggregate the votes
|
|
49
|
-
5. Return FINAL VERDICT in this exact format:
|
|
50
|
-
|
|
51
|
-
\`\`\`
|
|
52
|
-
FINAL VERDICT: GUILTY (or NOT GUILTY)
|
|
53
|
-
CONFIDENCE: 0.0-1.0
|
|
54
|
-
SENTENCE: [humorous sentence appropriate to the offense]
|
|
55
|
-
CASE ID: ${caseData.caseId || `case-${Date.now()}`}
|
|
56
|
-
\`\`\`
|
|
57
|
-
|
|
58
|
-
**Rules:**
|
|
59
|
-
- Be fair but entertaining
|
|
60
|
-
- If confidence ≥ 0.6, verdict should be GUILTY
|
|
61
|
-
- Sentence should be humorous but appropriate
|
|
62
|
-
- Only return the FINAL VERDICT block, no other text`
|
|
25
|
+
async conductHearing(caseData) {
|
|
26
|
+
const caseId = caseData.caseId || caseData.offense?.caseId || `case-${Date.now()}`;
|
|
27
|
+
|
|
28
|
+
logger.info('HEARING', 'Conducting hearing', { caseId });
|
|
29
|
+
|
|
30
|
+
// Normalize offense data from different input shapes
|
|
31
|
+
const offense = caseData.offense || caseData;
|
|
32
|
+
const offenseName = offense.offenseName || offense.name || 'Unknown Offense';
|
|
33
|
+
const severity = offense.severity || 'minor';
|
|
34
|
+
const confidence = offense.confidence || 0.5;
|
|
35
|
+
const evidence = offense.evidence || caseData.evidence || 'No evidence provided';
|
|
36
|
+
const humorTriggers = caseData.humorContext || caseData.humorTriggers || [];
|
|
37
|
+
|
|
38
|
+
const hearingData = {
|
|
39
|
+
caseId,
|
|
40
|
+
offenseName,
|
|
41
|
+
severity,
|
|
42
|
+
confidence,
|
|
43
|
+
evidence,
|
|
44
|
+
humorTriggers,
|
|
45
|
+
agentId: this.agent?.id || 'unknown'
|
|
63
46
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
47
|
+
|
|
48
|
+
const proceedings = [];
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Step 1: Judge evaluation
|
|
52
|
+
const judgeVerdict = await this.getJudgeVerdict(hearingData);
|
|
53
|
+
proceedings.push({ speaker: 'Judge', message: judgeVerdict.commentary });
|
|
54
|
+
|
|
55
|
+
// Step 2: Jury deliberation
|
|
56
|
+
const juryVerdicts = await this.getJuryVerdicts(hearingData);
|
|
57
|
+
for (const juror of juryVerdicts) {
|
|
58
|
+
proceedings.push({ speaker: `Jury (${juror.role})`, message: juror.commentary });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 3: Tally votes
|
|
62
|
+
const allVotes = [judgeVerdict, ...juryVerdicts];
|
|
63
|
+
const guiltyCount = allVotes.filter(v => v.guilty).length;
|
|
64
|
+
const totalVotes = allVotes.length;
|
|
65
|
+
const minVotes = this.config.get('hearing.minVoteThreshold') || 2;
|
|
66
|
+
const isGuilty = guiltyCount >= minVotes;
|
|
67
|
+
|
|
68
|
+
// Step 4: Build sentence
|
|
69
|
+
const sentence = isGuilty
|
|
70
|
+
? (judgeVerdict.sentence || this.getDefaultSentence(severity))
|
|
71
|
+
: 'Case dismissed. The defendant is free to go.';
|
|
72
|
+
|
|
73
|
+
const verdict = {
|
|
74
|
+
caseId,
|
|
75
|
+
guilty: isGuilty,
|
|
76
|
+
offense: {
|
|
77
|
+
id: offense.offenseId || offense.id || 'unknown',
|
|
78
|
+
name: offenseName,
|
|
79
|
+
severity,
|
|
80
|
+
confidence
|
|
81
|
+
},
|
|
82
|
+
verdict: {
|
|
83
|
+
status: isGuilty ? 'GUILTY' : 'NOT GUILTY',
|
|
84
|
+
vote: `${guiltyCount}-${totalVotes - guiltyCount}`,
|
|
85
|
+
primaryFailure: judgeVerdict.primaryFailure || offenseName,
|
|
86
|
+
agentCommentary: judgeVerdict.commentary,
|
|
87
|
+
sentence
|
|
88
|
+
},
|
|
89
|
+
proceedings,
|
|
90
|
+
timestamp: new Date().toISOString()
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
logger.info('HEARING', 'Hearing complete', {
|
|
94
|
+
caseId,
|
|
95
|
+
guilty: isGuilty,
|
|
96
|
+
vote: `${guiltyCount}-${totalVotes - guiltyCount}`
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return verdict;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logger.error('HEARING', 'Hearing failed, using fallback verdict', { error: err.message });
|
|
102
|
+
return this.getFallbackVerdict(hearingData, caseId);
|
|
103
|
+
}
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
/**
|
|
72
|
-
*
|
|
107
|
+
* Get judge verdict via LLM
|
|
73
108
|
*/
|
|
74
|
-
async
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
async getJudgeVerdict(hearingData) {
|
|
110
|
+
if (!this.agent?.llm) {
|
|
111
|
+
return this.getMockJudgeVerdict(hearingData);
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
try {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
const evidencePrompt = JUDGE_EVIDENCE_TEMPLATE(hearingData);
|
|
116
|
+
const response = await this.agent.llm.call({
|
|
117
|
+
messages: [
|
|
118
|
+
{ role: 'system', content: JUDGE_SYSTEM_PROMPT },
|
|
119
|
+
{ role: 'user', content: evidencePrompt }
|
|
120
|
+
],
|
|
121
|
+
temperature: 0.7,
|
|
122
|
+
maxTokens: 500
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const content = response.content || response;
|
|
126
|
+
return this.parseJudgeResponse(content, hearingData);
|
|
86
127
|
} catch (err) {
|
|
87
|
-
|
|
128
|
+
logger.warn('HEARING', 'Judge LLM call failed', { error: err.message });
|
|
129
|
+
return this.getMockJudgeVerdict(hearingData);
|
|
88
130
|
}
|
|
89
131
|
}
|
|
90
132
|
|
|
91
133
|
/**
|
|
92
|
-
*
|
|
134
|
+
* Get jury verdicts via LLM (one call per juror)
|
|
93
135
|
*/
|
|
94
|
-
async
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
136
|
+
async getJuryVerdicts(hearingData) {
|
|
137
|
+
const jurorRoles = Object.values(JUROR_ROLES).slice(0, 3);
|
|
138
|
+
const verdicts = [];
|
|
139
|
+
|
|
140
|
+
for (const role of jurorRoles) {
|
|
141
|
+
try {
|
|
142
|
+
if (this.agent?.llm) {
|
|
143
|
+
const evidencePrompt = JURY_EVIDENCE_TEMPLATE(hearingData, role);
|
|
144
|
+
const response = await this.agent.llm.call({
|
|
145
|
+
messages: [
|
|
146
|
+
{ role: 'system', content: role.systemPrompt },
|
|
147
|
+
{ role: 'user', content: evidencePrompt }
|
|
148
|
+
],
|
|
149
|
+
temperature: 0.7,
|
|
150
|
+
maxTokens: 300
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const content = response.content || response;
|
|
154
|
+
verdicts.push(this.parseJurorResponse(content, role.name, hearingData));
|
|
155
|
+
} else {
|
|
156
|
+
verdicts.push(this.getMockJurorVerdict(role.name, hearingData));
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.warn('HEARING', `Juror ${role.name} LLM call failed`, { error: err.message });
|
|
160
|
+
verdicts.push(this.getMockJurorVerdict(role.name, hearingData));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return verdicts;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse judge LLM response into structured verdict
|
|
169
|
+
*/
|
|
170
|
+
parseJudgeResponse(response, hearingData) {
|
|
171
|
+
const upper = response.toUpperCase();
|
|
172
|
+
const guilty = upper.includes('GUILTY') && !upper.startsWith('NOT GUILTY');
|
|
173
|
+
|
|
174
|
+
// Extract primary failure
|
|
175
|
+
let primaryFailure = '';
|
|
176
|
+
const failureMatch = response.match(/PRIMARY FAILURE[:\s]*(.+?)(?:\n|$)/i);
|
|
177
|
+
if (failureMatch) {
|
|
178
|
+
primaryFailure = failureMatch[1].trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract sentence
|
|
182
|
+
let sentence = '';
|
|
183
|
+
const sentenceMatch = response.match(/SENTENCE[:\s]*(.+?)(?:\n|$)/i);
|
|
184
|
+
if (sentenceMatch) {
|
|
185
|
+
sentence = sentenceMatch[1].trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
guilty,
|
|
190
|
+
commentary: response.substring(0, 500),
|
|
191
|
+
primaryFailure: primaryFailure || `Behavioral pattern: ${hearingData.offenseName}`,
|
|
192
|
+
sentence: sentence || this.getDefaultSentence(hearingData.severity),
|
|
193
|
+
role: 'Judge'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse juror LLM response
|
|
199
|
+
*/
|
|
200
|
+
parseJurorResponse(response, roleName, hearingData) {
|
|
201
|
+
const upper = response.toUpperCase();
|
|
202
|
+
const guilty = upper.includes('GUILTY') && !upper.startsWith('NOT GUILTY');
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
guilty,
|
|
206
|
+
role: roleName,
|
|
207
|
+
commentary: response.substring(0, 300)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Mock judge verdict when LLM is not available
|
|
213
|
+
*/
|
|
214
|
+
getMockJudgeVerdict(hearingData) {
|
|
215
|
+
const guilty = hearingData.confidence >= 0.6;
|
|
216
|
+
return {
|
|
217
|
+
guilty,
|
|
218
|
+
commentary: `The Court has reviewed the evidence regarding "${hearingData.offenseName}" and finds the pattern ${guilty ? 'sufficiently established' : 'insufficient for conviction'}. Confidence: ${(hearingData.confidence * 100).toFixed(0)}%.`,
|
|
219
|
+
primaryFailure: hearingData.offenseName,
|
|
220
|
+
sentence: guilty ? this.getDefaultSentence(hearingData.severity) : 'Case dismissed.',
|
|
221
|
+
role: 'Judge'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Mock juror verdict when LLM is not available
|
|
227
|
+
*/
|
|
228
|
+
getMockJurorVerdict(roleName, hearingData) {
|
|
229
|
+
const guilty = hearingData.confidence >= 0.6;
|
|
230
|
+
return {
|
|
231
|
+
guilty,
|
|
232
|
+
role: roleName,
|
|
233
|
+
commentary: `${roleName}: The evidence ${guilty ? 'supports' : 'does not support'} the charge of ${hearingData.offenseName}.`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Fallback verdict when hearing completely fails
|
|
239
|
+
*/
|
|
240
|
+
getFallbackVerdict(hearingData, caseId) {
|
|
241
|
+
const guilty = hearingData.confidence >= 0.7; // Higher threshold for fallback
|
|
99
242
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
243
|
+
caseId,
|
|
244
|
+
guilty,
|
|
245
|
+
offense: {
|
|
246
|
+
id: hearingData.offenseId || 'unknown',
|
|
247
|
+
name: hearingData.offenseName,
|
|
248
|
+
severity: hearingData.severity,
|
|
249
|
+
confidence: hearingData.confidence
|
|
250
|
+
},
|
|
251
|
+
verdict: {
|
|
252
|
+
status: guilty ? 'GUILTY' : 'NOT GUILTY',
|
|
253
|
+
vote: guilty ? '3-1' : '1-3',
|
|
254
|
+
primaryFailure: hearingData.offenseName,
|
|
255
|
+
agentCommentary: 'Hearing conducted via fallback evaluation.',
|
|
256
|
+
sentence: guilty ? this.getDefaultSentence(hearingData.severity) : 'Case dismissed.'
|
|
257
|
+
},
|
|
258
|
+
proceedings: [
|
|
259
|
+
{ speaker: 'Judge', message: 'Fallback evaluation used due to hearing pipeline error.' }
|
|
260
|
+
],
|
|
261
|
+
timestamp: new Date().toISOString()
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get default sentence based on severity
|
|
267
|
+
*/
|
|
268
|
+
getDefaultSentence(severity) {
|
|
269
|
+
const sentences = {
|
|
270
|
+
minor: 'The agent will provide extra-verbose explanations for the next 30 minutes.',
|
|
271
|
+
moderate: 'The agent will require confirmation before all actions for the next 60 minutes.',
|
|
272
|
+
severe: 'The agent will operate under human oversight mode for the next 120 minutes.'
|
|
103
273
|
};
|
|
274
|
+
return sentences[severity] || sentences.minor;
|
|
104
275
|
}
|
|
105
276
|
}
|
|
106
277
|
|