neuronagent
Version:
AI agent for Internet Computer governance and neuron management
1,145 lines (971 loc) • 38.6 kB
text/typescript
import express, { Request, Response } from "npm:express@4.18.2";
import { join } from "https://deno.land/std/path/mod.ts";
import { createIdentityFromKey, setupAgent } from "./identity.ts";
import { getProposal, proposalExists, getUnprocessedProposals, markProposalProcessed, getDB, storeProposal,
scheduleVote, getScheduledVote, cancelScheduledVote, getPendingVotes, markVoteExecuted, getConfigValue, setConfigValue,
getAgentVote, getAgentVotes, getAgentLogs, resetAgentData, storeAgentVote, markAgentVoteScheduled, recordVoteError,
ensureProposalExists } from "./db.ts";
import { bold, red, green, yellow, cyan } from "https://deno.land/std/fmt/colors.ts";
import { SIX_MONTHS_AND_ONE_DAY } from "./types.ts";
import { analyzeProposal } from "./agent.ts";
// Server configuration
const PORT = 3014;
// Seven months in seconds for minimum dissolve delay
const SEVEN_MONTHS_SECONDS = 7 * 30 * 24 * 60 * 60; // approximately 7 months
// Proposal refresh configuration
const PROPOSAL_REFRESH_INTERVAL = 30 * 60 * 1000; // 30 minutes in milliseconds
const LATEST_PROPOSALS_COUNT = 30;
// Vote processing interval (check every 10 seconds)
const VOTE_PROCESSING_INTERVAL = 10 * 1000;
// Modified to use the getDB function
function ensureDB() {
return getDB();
}
// Process scheduled votes that are ready to be executed
async function processScheduledVotes() {
try {
const pendingVotes = getPendingVotes();
if (pendingVotes.length === 0) {
return; // No pending votes to process
}
console.log(cyan(`Processing ${pendingVotes.length} pending votes...`));
// Get identity and set up governance
const IC_AUTHENTICATION_KEY = getConfigValue("IC_AUTHENTICATION_KEY");
if (!IC_AUTHENTICATION_KEY) {
console.error(red("No IC authentication key found in config."));
return;
}
const identity = await createIdentityFromKey(IC_AUTHENTICATION_KEY);
const { governance } = await setupAgent(identity);
// Get neurons
const neurons = await governance.listNeurons({ certified: true });
if (neurons.length === 0) {
console.error(red("No neurons found, cannot execute votes."));
return;
}
// Use the first neuron
const neuron = neurons[0];
// Process each pending vote
for (const vote of pendingVotes) {
try {
// Ensure the proposal exists in the database before attempting to vote
// This guarantees we'll have at least a placeholder entry even if the vote fails
ensureProposalExists(vote.proposalId);
console.log(cyan(`Executing vote ${vote.voteType} on proposal ${vote.proposalId}...`));
// Cast vote - FIXED: "no" votes should use ID 2, not 0
await governance.registerVote({
neuronId: neuron.neuronId,
proposalId: BigInt(vote.proposalId),
vote: vote.voteType === "yes" ? 1 : 2, // 1 for yes, 2 for no
});
// Mark the vote as executed
markVoteExecuted(vote.id);
// Refresh the proposal to get updated ballot information
const response = await governance.getProposal({
proposalId: BigInt(vote.proposalId)
});
if (response && response.proposal) {
// Update the proposal in the database
storeProposal(vote.proposalId, response.proposal);
}
console.log(green(`✅ Successfully executed ${vote.voteType} vote on proposal ${vote.proposalId}`));
} catch (error) {
console.error(red(`❌ Error executing vote on proposal ${vote.proposalId}:`));
// Enhanced error logging - display comprehensive error information
console.error(red("Error details:"));
// Capture error message and details for database
let errorMessage = "Unknown error";
let errorDetails = "";
// Log the basic error message
if (error instanceof Error) {
errorMessage = error.message;
errorDetails = error.stack || "";
console.error(red(`- Message: ${error.message}`));
console.error(red(`- Stack: ${error.stack}`));
} else {
errorMessage = String(error);
console.error(red(`- Error object: ${String(error)}`));
}
// Check for IC-specific error properties
if (error && typeof error === 'object') {
try {
// Prepare detailed error information
const detailsObj = {};
if ('code' in error) {
console.error(red(`- Error code: ${error.code}`));
detailsObj.code = error.code;
}
if ('detail' in error) {
console.error(red(`- Error detail: ${JSON.stringify(error.detail, null, 2)}`));
detailsObj.detail = error.detail;
}
// Log all properties of the error for completeness
console.error(red("- All error properties:"));
for (const key in error) {
try {
const value = error[key];
const strValue = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
console.error(red(` ${key}: ${strValue}`));
if (key !== 'stack') {
detailsObj[key] = value;
}
} catch (e) {
console.error(red(` ${key}: [Error stringifying property]`));
}
}
// Update error details JSON
errorDetails = JSON.stringify(detailsObj, null, 2);
} catch (jsonError) {
console.error(red(`Error stringifying error details: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`));
}
}
// Record vote error in the database
recordVoteError(vote.id, errorMessage, errorDetails);
// Even if vote fails, try to fetch and store the proposal to ensure it's not lost
try {
console.log(yellow(`Attempting to refresh proposal ${vote.proposalId} data after vote error...`));
const proposal = await governance.getProposal({
proposalId: BigInt(vote.proposalId)
});
if (proposal && proposal.proposal) {
// Check proposal status
const status = proposal.proposal.status || 0;
const statusText = ['Unknown', 'Open', 'Rejected', 'Adopted', 'Executed', 'Failed'][status] || 'Unknown';
console.log(yellow(`Proposal status: ${statusText} (${status})`));
// Ensure the proposal is stored in the database regardless of vote outcome
storeProposal(vote.proposalId, proposal.proposal);
console.log(green(`✅ Successfully refreshed proposal ${vote.proposalId} data after error`));
} else {
console.error(red(`❌ Failed to refresh proposal ${vote.proposalId} data: No proposal data returned`));
// Ensure we still have at least a placeholder entry in the database
ensureProposalExists(vote.proposalId);
}
} catch (refreshError) {
console.error(red(`❌ Failed to refresh proposal ${vote.proposalId} data: ${refreshError instanceof Error ? refreshError.message : String(refreshError)}`));
// Ensure we still have at least a placeholder entry in the database
ensureProposalExists(vote.proposalId);
}
}
}
} catch (error) {
console.error(red(`❌ Error processing scheduled votes: ${error instanceof Error ? error.message : String(error)}`));
}
}
// Fetch and store the latest proposals
async function refreshLatestProposals() {
console.log(bold(cyan("Refreshing latest proposals...")));
try {
const IC_AUTHENTICATION_KEY = getConfigValue("IC_AUTHENTICATION_KEY");
const identity = await createIdentityFromKey(IC_AUTHENTICATION_KEY);
const { governance } = await setupAgent(identity);
// Fetch the latest proposals
const response = await governance.listProposals({
request: {
limit: LATEST_PROPOSALS_COUNT,
includeRewardStatus: [],
beforeProposal: undefined, // Start with the most recent
excludeTopic: [],
includeAllManageNeuronProposals: false,
includeStatus: [],
omitLargeFields: false // Include all fields
},
certified: true
});
const proposals = response.proposals || [];
console.log(`Retrieved ${proposals.length} latest proposals`);
// Store each proposal in the database
let newProposals = 0;
for (const proposal of proposals) {
if (!proposal.id) continue;
const proposalId = proposal.id.toString();
// Check if the proposal already exists
const exists = proposalExists(proposalId);
if (!exists) {
storeProposal(proposalId, proposal);
newProposals++;
}
}
console.log(green(`✅ Stored ${newProposals} new proposals in the database`));
} catch (error) {
console.error(red(`❌ Error refreshing proposals: ${error instanceof Error ? error.message : String(error)}`));
}
}
// Check and increase neuron dissolve delay if needed
async function ensureMinimumDissolveDelay() {
console.log(bold("Checking neuron dissolve delay..."));
try {
const IC_AUTHENTICATION_KEY = getConfigValue("IC_AUTHENTICATION_KEY");
const identity = await createIdentityFromKey(IC_AUTHENTICATION_KEY);
const { governance } = await setupAgent(identity);
// Get the user's neurons
const neurons = await governance.listNeurons({ certified: true });
if (neurons.length === 0) {
console.log(yellow("No neurons found for this identity."));
return;
}
// Process each neuron (although we expect just one)
for (const neuron of neurons) {
const neuronId = neuron.neuronId;
const currentDissolve = Number(neuron.dissolveDelaySeconds || 0);
console.log(`Neuron ${neuronId.toString()} has a dissolve delay of ${(currentDissolve / (24 * 60 * 60)).toFixed(1)} days`);
// Check if the dissolve delay is less than our minimum (7 months)
if (currentDissolve < SEVEN_MONTHS_SECONDS) {
const additionalSeconds = SEVEN_MONTHS_SECONDS - currentDissolve;
console.log(`Increasing dissolve delay by ${(additionalSeconds / (24 * 60 * 60)).toFixed(1)} days to reach 7 months...`);
// Increase the dissolve delay
await governance.increaseDissolveDelay({
neuronId,
additionalDissolveDelaySeconds: BigInt(additionalSeconds)
});
console.log(green(`✅ Successfully increased dissolve delay for neuron ${neuronId.toString()} to 7 months`));
} else {
console.log(green(`✅ Neuron ${neuronId.toString()} already has sufficient dissolve delay`));
}
}
} catch (error) {
console.error(red(`❌ Error checking/updating dissolve delay: ${error instanceof Error ? error.message : String(error)}`));
}
}
// Web server functionality
export async function startWebServer() {
// Ensure the neuron's dissolve delay is at least 7 months
await ensureMinimumDissolveDelay();
// Run the initial proposal refresh
await refreshLatestProposals();
// Set up the periodic proposal refresh
console.log(bold(cyan(`Setting up periodic proposal refresh every 30 minutes`)));
setInterval(refreshLatestProposals, PROPOSAL_REFRESH_INTERVAL);
// Set up the vote processing interval
console.log(bold(cyan(`Setting up vote processing check every 10 seconds`)));
setInterval(processScheduledVotes, VOTE_PROCESSING_INTERVAL);
const app = express();
// Serve static files from the 'public' directory (built by Vite)
const currentDir = Deno.cwd();
const publicPath = join(currentDir, "public");
// Add CORS middleware
app.use((req: Request, res: Response, next: express.NextFunction) => {
// Allow requests from any origin during development
res.setHeader('Access-Control-Allow-Origin', '*');
// Allow common HTTP methods
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// Allow common headers
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});
// Configure middleware
app.use(express.static(publicPath));
app.use(express.json());
// API endpoints
app.get("/api/status", async (req: Request, res: Response) => {
try {
const IC_AUTHENTICATION_KEY = getConfigValue("IC_AUTHENTICATION_KEY");
const identity = await createIdentityFromKey(IC_AUTHENTICATION_KEY);
const principal = identity.getPrincipal();
res.json({
status: "online",
principal: principal.toText(),
userPreference: getConfigValue("USER_PROMPT")
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get proposals with pagination
app.get("/api/proposals", (req: Request, res: Response) => {
try {
// Parse pagination parameters
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 20;
const status = req.query.status as string || "all"; // all, processed, unprocessed
const db = ensureDB();
try {
// Build the query based on status filter
let query = "SELECT id, data, processed FROM proposals";
const params: any[] = [];
if (status === "processed") {
query += " WHERE processed = 1";
} else if (status === "unprocessed") {
query += " WHERE processed = 0";
}
// Add ordering and pagination
query += " ORDER BY CAST(id as INTEGER) DESC LIMIT ? OFFSET ?";
params.push(limit, (page - 1) * limit);
// Get total count for pagination info
let totalQuery = "SELECT COUNT(*) as total FROM proposals";
if (status === "processed") {
totalQuery += " WHERE processed = 1";
} else if (status === "unprocessed") {
totalQuery += " WHERE processed = 0";
}
// Execute count query
let total = 0;
for (const [count] of db.query(totalQuery)) {
total = Number(count);
break;
}
// Execute main query
const proposals: any[] = [];
for (const [id, data, processed] of db.query(query, params)) {
try {
const parsedData = JSON.parse(String(data));
proposals.push({
id: String(id),
processed: Boolean(processed),
...parsedData
});
} catch (parseError) {
console.error(red(bold(`❌ Error parsing proposal data: ${parseError instanceof Error ? parseError.message : String(parseError)}`)));
}
}
// Return with pagination info
res.json({
proposals,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} finally {
db.close();
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get proposal by ID
app.get("/api/proposals/:id", (req: Request, res: Response) => {
try {
const id = req.params.id;
if (!proposalExists(id)) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
const proposal = getProposal(id);
if (!proposal) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
res.json({
status: "success",
proposal: {
id,
...proposal
}
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Schedule a vote on a specific proposal
app.post("/api/proposals/:id/schedule-vote", (req: Request, res: Response) => {
try {
const id = req.params.id;
const { vote } = req.body;
if (!proposalExists(id)) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
if (vote !== "yes" && vote !== "no") {
return res.status(400).json({
status: "error",
message: "Invalid vote value. Must be 'yes' or 'no'"
});
}
const delaySeconds = req.body.delaySeconds || 3600; // Default 3600 seconds (1 hour)
const voteId = scheduleVote(id, vote, delaySeconds);
if (voteId) {
// Get the scheduled time
const scheduledVote = getScheduledVote(id);
res.json({
status: "success",
message: `Vote ${vote} scheduled on proposal ${id}`,
scheduledVote: scheduledVote
});
} else {
res.status(500).json({
status: "error",
message: "Failed to schedule vote"
});
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Cancel a scheduled vote
app.post("/api/proposals/:id/cancel-vote", (req: Request, res: Response) => {
try {
const id = req.params.id;
// Check if there's a scheduled vote
const scheduledVote = getScheduledVote(id);
if (!scheduledVote) {
return res.status(404).json({
status: "error",
message: "No scheduled vote found for this proposal"
});
}
// Cancel the vote
const result = cancelScheduledVote(id);
if (result) {
res.json({
status: "success",
message: `Vote on proposal ${id} has been canceled`
});
} else {
res.status(500).json({
status: "error",
message: "Failed to cancel the vote"
});
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get the status of a scheduled vote
app.get("/api/proposals/:id/vote-status", (req: Request, res: Response) => {
try {
const id = req.params.id;
// Check if there's a scheduled vote
const scheduledVote = getScheduledVote(id);
res.json({
status: "success",
scheduledVote: scheduledVote || null
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Vote on a specific proposal (keep for immediate voting if needed)
app.post("/api/proposals/:id/vote", async (req: Request, res: Response) => {
try {
const id = req.params.id;
const { vote, immediate } = req.body;
// Always ensure the proposal exists before proceeding with any vote action
// This guarantees we'll have at least a placeholder entry even if checks fail
ensureProposalExists(id);
if (!proposalExists(id)) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
if (vote !== "yes" && vote !== "no") {
return res.status(400).json({
status: "error",
message: "Invalid vote value. Must be 'yes' or 'no'"
});
}
// If immediate is not explicitly true, schedule the vote instead
if (immediate !== true) {
const delaySeconds = req.body.delaySeconds || 3600; // Default 3600 seconds (1 hour)
const voteId = scheduleVote(id, vote, delaySeconds);
const scheduledVote = getScheduledVote(id);
return res.json({
status: "success",
message: `Vote ${vote} scheduled on proposal ${id}`,
scheduled: true,
scheduledVote: scheduledVote
});
}
// Continue with immediate voting if requested
// Get identity and set up governance
const IC_AUTHENTICATION_KEY = getConfigValue("IC_AUTHENTICATION_KEY");
if (!IC_AUTHENTICATION_KEY) {
return res.status(500).json({
status: "error",
message: "IC authentication key not found in config"
});
}
const identity = await createIdentityFromKey(IC_AUTHENTICATION_KEY);
const { governance } = await setupAgent(identity);
// Get neurons
const neurons = await governance.listNeurons({ certified: true });
if (neurons.length === 0) {
return res.status(400).json({
status: "error",
message: "No neurons found for this identity"
});
}
// Use the first neuron
const neuron = neurons[0];
// Cast vote (immediate)
try {
await governance.registerVote({
neuronId: neuron.neuronId,
proposalId: BigInt(id),
vote: vote === "yes" ? 1 : 2, // FIXED: 1 for yes, 2 for no (was using 0 for no)
});
// Refresh the proposal to get updated ballot information
const response = await governance.getProposal({
proposalId: BigInt(id)
});
if (response && response.proposal) {
// Update the proposal in the database
storeProposal(id, response.proposal);
} else {
// If no response or no proposal data, ensure we still have at least a placeholder
ensureProposalExists(id);
}
res.json({
status: "success",
message: `Successfully voted ${vote} on proposal ${id}`,
neuronId: neuron.neuronId.toString(),
scheduled: false
});
} catch (voteError) {
console.error("Vote error details:", voteError);
// Extract the error message from the error object
const errorMessage = voteError instanceof Error ? voteError.message : String(voteError);
const errorDetail = voteError?.detail?.error_message || "";
// Ensure the proposal still exists in the database even if voting failed
ensureProposalExists(id);
// Try to refresh the proposal data to keep it up-to-date
try {
const proposal = await governance.getProposal({
proposalId: BigInt(id)
});
if (proposal && proposal.proposal) {
storeProposal(id, proposal.proposal);
}
} catch (refreshError) {
console.error(`Error refreshing proposal after vote error: ${refreshError instanceof Error ? refreshError.message : String(refreshError)}`);
}
// Check for specific error types
if (errorMessage.includes("already voted") || errorDetail.includes("already voted")) {
return res.status(400).json({
status: "error",
message: `Your neuron has already voted on this proposal`
});
}
if (errorMessage.includes("not authorized") || errorDetail.includes("not authorized")) {
return res.status(403).json({
status: "error",
message: `Your neuron is not authorized to vote on this proposal. This may be due to insufficient voting power, neuron age, or dissolve delay.`
});
}
// For other errors, return a generic message
return res.status(400).json({
status: "error",
message: `Failed to vote: ${errorMessage || errorDetail || "Unknown error"}`
});
}
} catch (error) {
console.error("Error voting on proposal:", error);
// Ensure the proposal exists even if an unexpected error occurs
if (req.params.id) {
ensureProposalExists(req.params.id);
}
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
app.get("/api/neurons", async (req: Request, res: Response) => {
try {
const config = getOrCreateConfig();
const identity = await createIdentityFromKey(config.IC_AUTHENTICATION_KEY);
const { governance } = await setupAgent(identity);
const neurons = await governance.listNeurons({ certified: true });
res.json({
neurons: neurons.map(neuron => {
// Extract basic information that should be available on all neurons
const neuronInfo = {
id: neuron.neuronId.toString(),
// Use optional chaining and nullish coalescing to handle potentially missing properties
stake: (neuron.cachedNeuronStake || 0).toString(),
dissolveDelay: (neuron.dissolveDelaySeconds || 0).toString()
};
// Add additional information if available in your governance API
// This depends on the actual structure you get from governance.listNeurons()
return neuronInfo;
}),
principalId: identity.getPrincipal().toText()
});
} catch (error) {
console.error("Error fetching neurons:", error);
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
app.post("/api/config", (req: Request, res: Response) => {
try {
const { key, value } = req.body;
if (!key || value === undefined) {
return res.status(400).json({
status: "error",
message: "Missing key or value"
});
}
// Use setConfigValue directly instead of dynamic import
setConfigValue(key, value);
res.json({ status: "success" });
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get and set neuron ID for the current user
app.get("/api/user/neuron", (req: Request, res: Response) => {
try {
const db = ensureDB();
try {
// Get the stored neuron ID from the config table
const neuronId = getConfigValue("user_neuron_id");
if (neuronId) {
res.json({
status: "success",
neuronId: neuronId
});
} else {
res.json({
status: "not_found",
neuronId: null
});
}
} finally {
db.close();
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
app.post("/api/user/neuron", (req: Request, res: Response) => {
try {
const { neuronId } = req.body;
if (!neuronId) {
return res.status(400).json({
status: "error",
message: "Missing neuron ID"
});
}
// Store the neuron ID in the config table
setConfigValue("user_neuron_id", neuronId);
res.json({
status: "success",
message: "Neuron ID stored successfully"
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// NEW ENDPOINT: Get a specific config value
app.get("/api/config", (req: Request, res: Response) => {
try {
const key = req.query.key as string;
if (!key) {
return res.status(400).json({
status: "error",
message: "Missing key parameter"
});
}
const db = ensureDB();
try {
const value = getConfigValue(key);
if (value !== undefined) {
res.json({
status: "success",
value: value
});
} else {
res.json({
status: "not_found",
message: `No configuration found for key: ${key}`
});
}
} finally {
db.close();
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get agent vote for a specific proposal
app.get("/api/proposals/:id/agent-vote", (req: Request, res: Response) => {
try {
const id = req.params.id;
if (!proposalExists(id)) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
const agentVote = getAgentVote(id);
if (agentVote) {
res.json({
status: "success",
agentVote: agentVote
});
} else {
res.json({
status: "not_found",
message: "No agent vote found for this proposal"
});
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get agent votes (paginated)
app.get("/api/agent-votes", (req: Request, res: Response) => {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 20;
const offset = (page - 1) * limit;
const votes = getAgentVotes(limit, offset);
res.json({
status: "success",
votes: votes,
pagination: {
page,
limit,
hasMore: votes.length === limit // Simple way to check if there might be more
}
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get agent logs for a specific proposal
app.get("/api/proposals/:id/agent-logs", (req: Request, res: Response) => {
try {
const id = req.params.id;
if (!proposalExists(id)) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
const logs = getAgentLogs(id);
res.json({
status: "success",
logs: logs
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Reset agent analysis for a specific proposal
app.post("/api/proposals/:id/reset-agent-analysis", async (req: Request, res: Response) => {
try {
const id = req.params.id;
if (!proposalExists(id)) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
// Reset agent data (votes, logs, scheduled votes)
resetAgentData(id);
// Cancel any scheduled votes for this proposal
cancelScheduledVote(id);
res.json({
status: "success",
message: `Agent analysis data for proposal ${id} has been reset`
});
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// This endpoint for manual reevaluation is left unchanged so users can manually trigger analysis for any proposal,
// even when not eligible to vote with their neuron.
app.post("/api/proposals/:id/trigger-analysis", async (req: Request, res: Response) => {
try {
const id = req.params.id;
// First ensure the proposal exists in the database - prevents data loss
ensureProposalExists(id);
// Get proposal data
const proposal = getProposal(id);
if (!proposal) {
return res.status(404).json({
status: "error",
message: "Proposal not found"
});
}
// Reset any existing agent data for this proposal
resetAgentData(id);
// Run the analysis in the background so we can return a response immediately
setTimeout(async () => {
try {
// Analyze the proposal directly using the simplified approach
console.log(cyan(`Starting analysis for proposal ${id} using OpenAI with function calling`));
// Use the simplified analyzeProposal function
const result = await analyzeProposal(proposal);
if (result.success && result.voteType && result.reasoning) {
// Store the agent vote
storeAgentVote(id, result.voteType, result.reasoning);
console.log(green(`✅ Analysis completed for proposal ${id}: Vote ${result.voteType.toUpperCase()}`));
// Ensure proposal still exists (may have been deleted during long-running analysis)
ensureProposalExists(id);
// If vote type is yes or no, schedule the vote
if (result.voteType === "yes" || result.voteType === "no") {
// Get the configured delay from the database
const delaySeconds = parseInt(getConfigValue("VOTE_SCHEDULE_DELAY") || "3600");
console.log(cyan(`Using vote schedule delay of ${delaySeconds} seconds (${delaySeconds / 60} minutes)`));
// Schedule the vote with the configured delay
const scheduleId = scheduleVote(id, result.voteType, delaySeconds);
if (scheduleId) {
console.log(green(`✅ Scheduled ${result.voteType} vote for proposal ${id} (with ${delaySeconds} seconds delay)`));
// Mark agent vote as scheduled
markAgentVoteScheduled(id);
} else {
console.error(red(`❌ Failed to schedule vote for proposal ${id}`));
// Even if scheduling fails, ensure proposal exists
ensureProposalExists(id);
}
}
} else {
console.error(yellow(`⚠️ Analysis did not produce a clear decision for proposal ${id}`));
// Ensure the proposal still exists in the database
ensureProposalExists(id);
}
} catch (err) {
console.error(red(`❌ Error analyzing proposal ${id}: ${err instanceof Error ? err.message : String(err)}`));
// Ensure the proposal still exists in the database even after an error
ensureProposalExists(id);
}
}, 0);
res.json({
status: "success",
message: `Analysis triggered for proposal ${id} using OpenAI with function calling`
});
} catch (error) {
const id = req.params.id;
if (id) {
// Ensure the proposal exists even if an error occurs
ensureProposalExists(id);
}
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Get voting history for a specific proposal
app.get("/api/proposals/:id/vote-history", (req: Request, res: Response) => {
try {
const id = req.params.id;
// Create the database connection
const db = ensureDB();
try {
// Ensure the proposal exists even if just as a placeholder
ensureProposalExists(id);
// Get the proposal data
const proposal = getProposal(id);
// Get both executed and pending scheduled votes
const votes = [];
for (const [voteId, voteType, scheduledTime, executed, executedTime] of db.query(
`SELECT id, vote_type, scheduled_time, executed,
CASE WHEN executed = 1 THEN executed_time ELSE NULL END as executed_time
FROM scheduled_votes
WHERE proposal_id = ?
ORDER BY scheduled_time DESC`,
[id]
)) {
votes.push({
id: Number(voteId),
voteType: String(voteType),
scheduledTime: Number(scheduledTime),
executed: Boolean(executed),
executedTime: executedTime ? Number(executedTime) : null,
});
}
// Get agent votes for this proposal
const agentVote = getAgentVote(id);
res.json({
status: "success",
proposal: {
id,
exists: !!proposal,
placeholder: proposal?.placeholder || false,
data: proposal ? {
status: proposal.status,
topic: proposal.topic,
title: proposal.proposal?.title || `Proposal ${id}`,
summary: proposal.proposal?.summary?.substring(0, 100) + (proposal.proposal?.summary?.length > 100 ? '...' : '') || 'No summary',
} : null
},
voting: {
scheduledVotes: votes,
agentVote: agentVote,
}
});
} catch (error) {
console.error(red(`Error retrieving vote history: ${error instanceof Error ? error.message : String(error)}`));
res.status(500).json({
status: "error",
message: `Error retrieving vote history: ${error instanceof Error ? error.message : "Unknown error"}`
});
} finally {
db.close();
}
} catch (error) {
res.status(500).json({
status: "error",
message: error instanceof Error ? error.message : String(error)
});
}
});
// Catch-all route to return the main index.html for client-side routing
app.get("*", (req: Request, res: Response) => {
res.sendFile(join(publicPath, "index.html"));
});
// Start the server
app.listen(PORT);
return app;
}
export { PORT };