Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 5 additions & 33 deletions platforms/cerberus/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,47 +138,19 @@ app.listen(port, () => {
console.log(`Cerberus API running on port ${port}`);
});

// Initialize Cerberus intervals and periodic check-ins for groups with charters
// Initialize Cerberus intervals for groups with charters
setTimeout(async () => {
try {
console.log("🐕 Starting Cerberus services...");

// Import services after server is running
const { CharterMonitoringService } = await import("./services/CharterMonitoringService");
const { GroupService } = await import("./services/GroupService");
const { CerberusIntervalService } = await import("./services/CerberusIntervalService");

const charterMonitoringService = new CharterMonitoringService();
const groupService = new GroupService();

const intervalService = new CerberusIntervalService();

// Initialize Cerberus intervals for all groups with charters
await intervalService.initializeIntervals();

// Send periodic check-ins every 24 hours (separate from charter-based intervals)
setInterval(async () => {
try {
const groups = await groupService.getAllGroups();
const groupsWithCharters = groups.filter(group => group.charter && group.charter.trim() !== '');

console.log(`🐕 Sending periodic check-ins to ${groupsWithCharters.length} groups with charters...`);

for (const group of groupsWithCharters) {
try {
await charterMonitoringService.sendPeriodicCheckIn(group.id, group.name);
// Add a small delay between messages to avoid overwhelming the system
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`Error sending check-in to group ${group.name}:`, error);
}
}

console.log("✅ Periodic check-ins completed");
} catch (error) {
console.error("Error during periodic check-ins:", error);
}
}, 24 * 60 * 60 * 1000); // 24 hours


console.log("✅ Cerberus services initialized");

// Graceful shutdown cleanup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ export class CerberusTriggerService {
}
}

/**
* Check if a message is a Cerberus system message (including "check skipped" messages)
*/
private isCerberusSystemMessage(message: Message): boolean {
return message.isSystemMessage && message.text.startsWith('$$system-message$$ Cerberus:');
}

/**
* Find the last message in the group that isn't the current trigger message
*/
private findLastNonTriggerMessage(messages: Message[], triggerMessageId: string): Message | null {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg.id !== triggerMessageId && !this.isCerberusTrigger(msg.text)) {
return msg;
}
}
return null;
}

/**
* Get the last message sent by Cerberus in a group
*/
Expand Down Expand Up @@ -536,9 +556,18 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
return;
}

// If the last message was a Cerberus system message or a "check skipped" message,
// silently return without sending any message to avoid stacking Cerberus messages
const allGroupMessages = await this.messageService.getGroupMessages(triggerMessage.group.id);
const lastNonTriggerMessage = this.findLastNonTriggerMessage(allGroupMessages, triggerMessage.id);
if (lastNonTriggerMessage && this.isCerberusSystemMessage(lastNonTriggerMessage)) {
console.log(`⏭️ Skipping Cerberus check for group ${triggerMessage.group.id} - previous message was a Cerberus system message`);
return;
}

// Get messages since last Cerberus message
const messages = await this.getMessagesSinceLastCerberus(
triggerMessage.group.id,
triggerMessage.group.id,
triggerMessage.id
);

Expand Down
18 changes: 11 additions & 7 deletions platforms/ereputation/api/src/controllers/WebhookController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,12 @@ export class WebhookController {
poll.mode = local.data.mode as "normal" | "point" | "rank";
poll.visibility = local.data.visibility as "public" | "private";
poll.votingWeight = (local.data.votingWeight || "1p1v") as "1p1v" | "ereputation";
poll.options = Array.isArray(local.data.options)
? local.data.options
poll.options = Array.isArray(local.data.options)
? local.data.options
: (local.data.options as string).split(",");
poll.deadline = local.data.deadline ? new Date(local.data.deadline as string) : null;
poll.groupId = groupId;
poll.customPrompt = (local.data.customPrompt as string) || null;

await pollRepository.save(poll);
finalLocalId = poll.id;
Expand All @@ -280,11 +281,12 @@ export class WebhookController {
mode: local.data.mode as "normal" | "point" | "rank",
visibility: local.data.visibility as "public" | "private",
votingWeight: (local.data.votingWeight || "1p1v") as "1p1v" | "ereputation",
options: Array.isArray(local.data.options)
? local.data.options
options: Array.isArray(local.data.options)
? local.data.options
: (local.data.options as string).split(","),
deadline: local.data.deadline ? new Date(local.data.deadline as string) : null,
groupId: groupId
groupId: groupId,
customPrompt: (local.data.customPrompt as string) || null
});

const savedPoll = await pollRepository.save(poll);
Expand Down Expand Up @@ -384,10 +386,12 @@ export class WebhookController {
const group = await this.groupService.getGroupById(poll.groupId);
if (!group) return;

const charter = (group.charter && group.charter.trim()) ? group.charter : "";
const evaluationCriteria = (poll.customPrompt && poll.customPrompt.trim())
? poll.customPrompt
: (group.charter && group.charter.trim()) ? group.charter : "";
const reputationResults = await this.votingReputationService.calculateGroupMemberReputations(
poll.groupId,
charter
evaluationCriteria
);

const voteReputationResult = await this.votingReputationService.saveReputationResults(
Expand Down
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export class Poll {
@Column("uuid", { nullable: true })
groupId!: string | null; // Group this poll belongs to

@Column("text", { nullable: true })
customPrompt!: string | null;

@OneToMany(
() => Vote,
(vote) => vote.poll,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddCustomPrompt1775035663491 implements MigrationInterface {
name = 'AddCustomPrompt1775035663491'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" ADD "customPrompt" text`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "customPrompt"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@ export class VotingReputationService {

// Call OpenAI once for all members
const response = await this.openai.chat.completions.create({
model: "gpt-4",
model: "gpt-4o",
messages: [
{
role: "system",
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."
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."
},
{
role: "user",
Expand All @@ -196,7 +196,12 @@ export class VotingReputationService {

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

// Call OpenAI
const response = await this.openai.chat.completions.create({
model: "gpt-4",
model: "gpt-4o",
messages: [
{
role: "system",
Expand Down Expand Up @@ -401,19 +406,19 @@ ${refsText}`;
return `
You are analyzing the reputation of multiple users for voting purposes within a group.

GROUP CHARTER:
EVALUATION CRITERIA:
${charter}

USERS AND THEIR REFERENCES:
${membersCSV}

TASK:
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.
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.

IMPORTANT:
IMPORTANT:
- Each score must be between 1 and 5 (inclusive)
- Consider how well the references align with the group's charter and values
- Focus on voting-relevant reputation factors mentioned in the charter
- Consider how well the references align with the evaluation criteria and values
- Focus on voting-relevant reputation factors mentioned in the evaluation criteria
- Provide a ONE SENTENCE justification explaining each score

Respond with a JSON array in this exact format:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"deadline": "deadline",
"creatorId": "creatorId",
"group": "groups(group.id),group",
"customPrompt": "customPrompt",
"createdAt": "createdAt",
"updatedAt": "updatedAt"
},
Expand Down
9 changes: 5 additions & 4 deletions platforms/evoting/api/src/controllers/PollController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export class PollController {
createPoll = async (req: Request, res: Response) => {
try {
console.log('🔍 Full request body:', req.body);
const { title, mode, visibility, votingWeight, options, deadline, groupId } = req.body;
const { title, mode, visibility, votingWeight, options, deadline, groupId, customPrompt } = req.body;
const creatorId = (req as any).user.id;
console.log('🔍 Extracted data:', { title, mode, visibility, votingWeight, options, deadline, groupId, creatorId });

console.log('🔍 Extracted data:', { title, mode, visibility, votingWeight, options, deadline, groupId, customPrompt, creatorId });
console.log('🔍 groupId type:', typeof groupId, 'value:', groupId);

// groupId is optional - only required for system messages
Expand All @@ -69,7 +69,8 @@ export class PollController {
options,
deadline,
creatorId,
groupId
groupId,
customPrompt
});

console.log('🔍 Created poll:', poll);
Expand Down
3 changes: 3 additions & 0 deletions platforms/evoting/api/src/database/entities/Poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export class Poll {
@Column("uuid", { nullable: true })
groupId!: string | null; // Group this poll belongs to

@Column("text", { nullable: true })
customPrompt!: string | null;

@OneToMany(
() => Vote,
(vote) => vote.poll,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddCustomPrompt1775035663491 implements MigrationInterface {
name = 'AddCustomPrompt1775035663491'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" ADD "customPrompt" text`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "polls" DROP COLUMN "customPrompt"`);
}
}
4 changes: 3 additions & 1 deletion platforms/evoting/api/src/services/PollService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export class PollService {
deadline?: string;
creatorId: string;
groupId?: string; // Optional groupId for system messages
customPrompt?: string;
}): Promise<Poll> {
console.log('🔍 PollService.createPoll called with:', pollData);

Expand Down Expand Up @@ -229,7 +230,8 @@ export class PollService {
deadline: hasDeadline ? new Date(pollData.deadline!) : null,
creator,
creatorId: pollData.creatorId,
groupId: pollData.groupId || null
groupId: pollData.groupId || null,
customPrompt: pollData.customPrompt || null
};
console.log('🔍 Creating poll entity with data:', pollDataForEntity);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"deadline": "deadline",
"creatorId": "creatorId",
"group": "groups(group.id),group",
"customPrompt": "customPrompt",
"createdAt": "createdAt",
"updatedAt": "updatedAt"
},
Expand Down
24 changes: 20 additions & 4 deletions platforms/evoting/client/src/app/(app)/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const createPollSchema = z.object({
return true;
}, "Please select a valid group"),
votingWeight: z.enum(["1p1v", "ereputation"]).default("1p1v"),
customPrompt: z.string().optional(),
options: z
.array(z.string()
.min(1, "Option cannot be empty")
Expand Down Expand Up @@ -74,6 +75,7 @@ export default function CreatePoll() {
visibility: "public",
groupId: "",
votingWeight: "1p1v",
customPrompt: "",
options: ["", ""],
deadline: "",
},
Expand Down Expand Up @@ -219,7 +221,8 @@ export default function CreatePoll() {
votingWeight: data.votingWeight,
groupId: data.groupId,
options: data.options.filter(option => option.trim() !== ""),
...(utcDeadline ? { deadline: utcDeadline } : {})
...(utcDeadline ? { deadline: utcDeadline } : {}),
...(data.customPrompt?.trim() ? { customPrompt: data.customPrompt.trim() } : {})
});

toast({
Expand Down Expand Up @@ -597,9 +600,22 @@ export default function CreatePoll() {
</div>
</RadioGroup>
{watchedVotingWeight === "ereputation" && (
<p className="mt-2 text-sm text-gray-600">
Votes will be weighted by each voter's eReputation score.
</p>
<div className="mt-4 space-y-2">
<p className="text-sm text-gray-600">
Votes will be weighted by each voter's eReputation score.
</p>
<Label className="text-sm font-semibold text-gray-700">
Custom Evaluation Prompt (Optional)
</Label>
<textarea
{...register("customPrompt")}
placeholder="Enter custom criteria for evaluating member reputations. If left empty, the group's charter will be used."
className="w-full min-h-[100px] p-3 border border-gray-300 rounded-lg focus:ring-(--crimson) focus:border-(--crimson) text-sm"
/>
<p className="text-sm text-gray-500">
If provided, this prompt will be used instead of the group charter for evaluating member reputations in this poll.
</p>
</div>
)}
{errors.votingWeight && (
<p className="mt-1 text-sm text-red-600">
Expand Down
2 changes: 2 additions & 0 deletions platforms/evoting/client/src/lib/pollApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Poll {
deadline?: string | null;
creatorId: string;
groupId?: string;
customPrompt?: string | null;
group?: {
id: string;
name: string;
Expand Down Expand Up @@ -61,6 +62,7 @@ export interface CreatePollData {
options: string[];
deadline?: string;
groupId: string;
customPrompt?: string;
}

export interface Group {
Expand Down
Loading