Skip to content

Commit aef26d4

Browse files
authored
feat: meritocratic voting prompt (#938)
* feat: meritocratic voting prompt * chore: change model and prompt * chore: code fences * feat: remove useless cerberus spam
1 parent d738b6b commit aef26d4

14 files changed

Lines changed: 124 additions & 59 deletions

File tree

platforms/cerberus/client/src/index.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -138,47 +138,19 @@ app.listen(port, () => {
138138
console.log(`Cerberus API running on port ${port}`);
139139
});
140140

141-
// Initialize Cerberus intervals and periodic check-ins for groups with charters
141+
// Initialize Cerberus intervals for groups with charters
142142
setTimeout(async () => {
143143
try {
144144
console.log("🐕 Starting Cerberus services...");
145-
145+
146146
// Import services after server is running
147-
const { CharterMonitoringService } = await import("./services/CharterMonitoringService");
148-
const { GroupService } = await import("./services/GroupService");
149147
const { CerberusIntervalService } = await import("./services/CerberusIntervalService");
150-
151-
const charterMonitoringService = new CharterMonitoringService();
152-
const groupService = new GroupService();
148+
153149
const intervalService = new CerberusIntervalService();
154-
150+
155151
// Initialize Cerberus intervals for all groups with charters
156152
await intervalService.initializeIntervals();
157-
158-
// Send periodic check-ins every 24 hours (separate from charter-based intervals)
159-
setInterval(async () => {
160-
try {
161-
const groups = await groupService.getAllGroups();
162-
const groupsWithCharters = groups.filter(group => group.charter && group.charter.trim() !== '');
163-
164-
console.log(`🐕 Sending periodic check-ins to ${groupsWithCharters.length} groups with charters...`);
165-
166-
for (const group of groupsWithCharters) {
167-
try {
168-
await charterMonitoringService.sendPeriodicCheckIn(group.id, group.name);
169-
// Add a small delay between messages to avoid overwhelming the system
170-
await new Promise(resolve => setTimeout(resolve, 1000));
171-
} catch (error) {
172-
console.error(`Error sending check-in to group ${group.name}:`, error);
173-
}
174-
}
175-
176-
console.log("✅ Periodic check-ins completed");
177-
} catch (error) {
178-
console.error("Error during periodic check-ins:", error);
179-
}
180-
}, 24 * 60 * 60 * 1000); // 24 hours
181-
153+
182154
console.log("✅ Cerberus services initialized");
183155

184156
// Graceful shutdown cleanup

platforms/cerberus/client/src/services/CerberusTriggerService.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ export class CerberusTriggerService {
115115
}
116116
}
117117

118+
/**
119+
* Check if a message is a Cerberus system message (including "check skipped" messages)
120+
*/
121+
private isCerberusSystemMessage(message: Message): boolean {
122+
return message.isSystemMessage && message.text.startsWith('$$system-message$$ Cerberus:');
123+
}
124+
125+
/**
126+
* Find the last message in the group that isn't the current trigger message
127+
*/
128+
private findLastNonTriggerMessage(messages: Message[], triggerMessageId: string): Message | null {
129+
for (let i = messages.length - 1; i >= 0; i--) {
130+
const msg = messages[i];
131+
if (msg.id !== triggerMessageId && !this.isCerberusTrigger(msg.text)) {
132+
return msg;
133+
}
134+
}
135+
return null;
136+
}
137+
118138
/**
119139
* Get the last message sent by Cerberus in a group
120140
*/
@@ -574,9 +594,18 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
574594
return;
575595
}
576596

597+
// If the last message was a Cerberus system message or a "check skipped" message,
598+
// silently return without sending any message to avoid stacking Cerberus messages
599+
const allGroupMessages = await this.messageService.getGroupMessages(triggerMessage.group.id);
600+
const lastNonTriggerMessage = this.findLastNonTriggerMessage(allGroupMessages, triggerMessage.id);
601+
if (lastNonTriggerMessage && this.isCerberusSystemMessage(lastNonTriggerMessage)) {
602+
console.log(`⏭️ Skipping Cerberus check for group ${triggerMessage.group.id} - previous message was a Cerberus system message`);
603+
return;
604+
}
605+
577606
// Get messages since last Cerberus message
578607
const messages = await this.getMessagesSinceLastCerberus(
579-
triggerMessage.group.id,
608+
triggerMessage.group.id,
580609
triggerMessage.id
581610
);
582611

platforms/ereputation/api/src/controllers/WebhookController.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,12 @@ export class WebhookController {
258258
poll.mode = local.data.mode as "normal" | "point" | "rank";
259259
poll.visibility = local.data.visibility as "public" | "private";
260260
poll.votingWeight = (local.data.votingWeight || "1p1v") as "1p1v" | "ereputation";
261-
poll.options = Array.isArray(local.data.options)
262-
? local.data.options
261+
poll.options = Array.isArray(local.data.options)
262+
? local.data.options
263263
: (local.data.options as string).split(",");
264264
poll.deadline = local.data.deadline ? new Date(local.data.deadline as string) : null;
265265
poll.groupId = groupId;
266+
poll.customPrompt = (local.data.customPrompt as string) || null;
266267

267268
await pollRepository.save(poll);
268269
finalLocalId = poll.id;
@@ -281,11 +282,12 @@ export class WebhookController {
281282
mode: local.data.mode as "normal" | "point" | "rank",
282283
visibility: local.data.visibility as "public" | "private",
283284
votingWeight: (local.data.votingWeight || "1p1v") as "1p1v" | "ereputation",
284-
options: Array.isArray(local.data.options)
285-
? local.data.options
285+
options: Array.isArray(local.data.options)
286+
? local.data.options
286287
: (local.data.options as string).split(","),
287288
deadline: local.data.deadline ? new Date(local.data.deadline as string) : null,
288-
groupId: groupId
289+
groupId: groupId,
290+
customPrompt: (local.data.customPrompt as string) || null
289291
});
290292

291293
const savedPoll = await pollRepository.save(poll);
@@ -443,10 +445,12 @@ export class WebhookController {
443445
const group = await this.groupService.getGroupById(poll.groupId);
444446
if (!group) return;
445447

446-
const charter = (group.charter && group.charter.trim()) ? group.charter : "";
448+
const evaluationCriteria = (poll.customPrompt && poll.customPrompt.trim())
449+
? poll.customPrompt
450+
: (group.charter && group.charter.trim()) ? group.charter : "";
447451
const reputationResults = await this.votingReputationService.calculateGroupMemberReputations(
448452
poll.groupId,
449-
charter
453+
evaluationCriteria
450454
);
451455

452456
const voteReputationResult = await this.votingReputationService.saveReputationResults(

platforms/ereputation/api/src/database/entities/Poll.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export class Poll {
4646
@Column("uuid", { nullable: true })
4747
groupId!: string | null; // Group this poll belongs to
4848

49+
@Column("text", { nullable: true })
50+
customPrompt!: string | null;
51+
4952
@OneToMany(
5053
() => Vote,
5154
(vote) => vote.poll,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class AddCustomPrompt1775035663491 implements MigrationInterface {
4+
name = 'AddCustomPrompt1775035663491'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`ALTER TABLE "polls" ADD "customPrompt" text`);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "customPrompt"`);
12+
}
13+
}

platforms/ereputation/api/src/services/VotingReputationService.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ export class VotingReputationService {
169169

170170
// Call OpenAI once for all members
171171
const response = await this.openai.chat.completions.create({
172-
model: "gpt-4",
172+
model: "gpt-4o",
173173
messages: [
174174
{
175175
role: "system",
176-
content: "You are an expert reputation analyst for voting systems. Analyze the group charter and references to calculate reputation scores for voting purposes. Always respond with valid JSON containing an array of results, each with ename (user's ename identifier), score (1-5), and a one-sentence justification."
176+
content: "You are an expert reputation analyst for voting systems. Analyze the evaluation criteria and references to calculate reputation scores for voting purposes. Always respond with valid JSON containing an array of results, each with ename (user's ename identifier), score (1-5), and a one-sentence justification."
177177
},
178178
{
179179
role: "user",
@@ -196,7 +196,12 @@ export class VotingReputationService {
196196

197197
let result;
198198
try {
199-
result = JSON.parse(aiResponseContent);
199+
// Strip markdown code fences if present (e.g. ```json ... ```)
200+
let jsonContent = aiResponseContent.trim();
201+
if (jsonContent.startsWith("```")) {
202+
jsonContent = jsonContent.replace(/^```(?:json)?\s*/, "").replace(/```\s*$/, "").trim();
203+
}
204+
result = JSON.parse(jsonContent);
200205
console.log(` → Successfully parsed JSON response`);
201206
console.log(` Results array length: ${Array.isArray(result) ? result.length : 'not an array'}`);
202207
} catch (parseError) {
@@ -302,7 +307,7 @@ export class VotingReputationService {
302307

303308
// Call OpenAI
304309
const response = await this.openai.chat.completions.create({
305-
model: "gpt-4",
310+
model: "gpt-4o",
306311
messages: [
307312
{
308313
role: "system",
@@ -401,19 +406,19 @@ ${refsText}`;
401406
return `
402407
You are analyzing the reputation of multiple users for voting purposes within a group.
403408
404-
GROUP CHARTER:
409+
EVALUATION CRITERIA:
405410
${charter}
406411
407412
USERS AND THEIR REFERENCES:
408413
${membersCSV}
409414
410415
TASK:
411-
Based on the group charter and the references provided, calculate a reputation score from 1-5 for EACH user that will be used for weighted voting.
416+
Based on the evaluation criteria and the references provided, calculate a reputation score from 1-5 for EACH user that will be used for weighted voting.
412417
413-
IMPORTANT:
418+
IMPORTANT:
414419
- Each score must be between 1 and 5 (inclusive)
415-
- Consider how well the references align with the group's charter and values
416-
- Focus on voting-relevant reputation factors mentioned in the charter
420+
- Consider how well the references align with the evaluation criteria and values
421+
- Focus on voting-relevant reputation factors mentioned in the evaluation criteria
417422
- Provide a ONE SENTENCE justification explaining each score
418423
419424
Respond with a JSON array in this exact format:

platforms/ereputation/api/src/web3adapter/mappings/poll.mapping.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"deadline": "deadline",
1212
"creatorId": "creatorId",
1313
"group": "groups(group.id),group",
14+
"customPrompt": "customPrompt",
1415
"createdAt": "createdAt",
1516
"updatedAt": "updatedAt"
1617
},

platforms/evoting/api/src/controllers/PollController.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ export class PollController {
5353
createPoll = async (req: Request, res: Response) => {
5454
try {
5555
console.log('🔍 Full request body:', req.body);
56-
const { title, mode, visibility, votingWeight, options, deadline, groupId } = req.body;
56+
const { title, mode, visibility, votingWeight, options, deadline, groupId, customPrompt } = req.body;
5757
const creatorId = (req as any).user.id;
58-
59-
console.log('🔍 Extracted data:', { title, mode, visibility, votingWeight, options, deadline, groupId, creatorId });
58+
59+
console.log('🔍 Extracted data:', { title, mode, visibility, votingWeight, options, deadline, groupId, customPrompt, creatorId });
6060
console.log('🔍 groupId type:', typeof groupId, 'value:', groupId);
6161

6262
// groupId is optional - only required for system messages
@@ -69,7 +69,8 @@ export class PollController {
6969
options,
7070
deadline,
7171
creatorId,
72-
groupId
72+
groupId,
73+
customPrompt
7374
});
7475

7576
console.log('🔍 Created poll:', poll);

platforms/evoting/api/src/database/entities/Poll.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export class Poll {
6262
@Column("uuid", { nullable: true })
6363
groupId!: string | null; // Group this poll belongs to
6464

65+
@Column("text", { nullable: true })
66+
customPrompt!: string | null;
67+
6568
@OneToMany(
6669
() => Vote,
6770
(vote) => vote.poll,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class AddCustomPrompt1775035663491 implements MigrationInterface {
4+
name = 'AddCustomPrompt1775035663491'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`ALTER TABLE "polls" ADD "customPrompt" text`);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "customPrompt"`);
12+
}
13+
}

0 commit comments

Comments
 (0)