UNPKG

neuronagent

Version:

AI agent for Internet Computer governance and neuron management

747 lines (662 loc) 22.9 kB
import { join } from "https://deno.land/std/path/mod.ts"; import { bold, red, yellow } from "https://deno.land/std/fmt/colors.ts"; import { DB } from "https://deno.land/x/sqlite/mod.ts"; // WASM-based SQLite import { generateRandomSecret } from "./utils.ts"; import { OscillumConfig, DEFAULT_CONFIG } from "./types.ts"; import { toState } from "./utils.ts"; const currentDir = Deno.cwd(); const DB_PATH = join(currentDir, "oscillum.db"); function ensureDB(): DB { const db = new DB(DB_PATH); db.execute(` CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) `); db.execute(` CREATE TABLE IF NOT EXISTS proposals ( id TEXT PRIMARY KEY, data TEXT NOT NULL, processed INTEGER DEFAULT 0 ) `); // Scheduled votes table with added error tracking db.execute(` CREATE TABLE IF NOT EXISTS scheduled_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, proposal_id TEXT NOT NULL, vote_type TEXT NOT NULL, scheduled_time INTEGER NOT NULL, executed INTEGER DEFAULT 0, executed_time INTEGER, error_message TEXT, error_details TEXT, UNIQUE(proposal_id) ) `); // Check if we need to add new columns for error tracking try { // Check if executed_time column exists let hasExecutedTime = false; for (const _ of db.query("PRAGMA table_info(scheduled_votes)")) { if (_[1] === "executed_time") { hasExecutedTime = true; break; } } // Add executed_time column if it doesn't exist if (!hasExecutedTime) { db.execute("ALTER TABLE scheduled_votes ADD COLUMN executed_time INTEGER"); } // Check if error_message column exists let hasErrorMessage = false; for (const _ of db.query("PRAGMA table_info(scheduled_votes)")) { if (_[1] === "error_message") { hasErrorMessage = true; break; } } // Add error_message column if it doesn't exist if (!hasErrorMessage) { db.execute("ALTER TABLE scheduled_votes ADD COLUMN error_message TEXT"); } // Check if error_details column exists let hasErrorDetails = false; for (const _ of db.query("PRAGMA table_info(scheduled_votes)")) { if (_[1] === "error_details") { hasErrorDetails = true; break; } } // Add error_details column if it doesn't exist if (!hasErrorDetails) { db.execute("ALTER TABLE scheduled_votes ADD COLUMN error_details TEXT"); } } catch (error) { console.error(red(bold(`❌ Error updating scheduled_votes table schema: ${error instanceof Error ? error.message : String(error)}`))); } // New table for AI agent votes db.execute(` CREATE TABLE IF NOT EXISTS agent_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, proposal_id TEXT NOT NULL, vote_type TEXT NOT NULL, reasoning TEXT NOT NULL, created_at INTEGER NOT NULL, scheduled BOOLEAN DEFAULT FALSE, UNIQUE(proposal_id) ) `); // New table for agent logs (communication with OpenAI) db.execute(` CREATE TABLE IF NOT EXISTS agent_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, proposal_id TEXT NOT NULL, request TEXT NOT NULL, response TEXT NOT NULL, created_at INTEGER NOT NULL ) `); return db; } export function getConfigValue(key: string): string | undefined { const db = ensureDB(); try { for (const [value] of db.query("SELECT value FROM config WHERE key = ?", [key])) { return String(value); } return undefined; } catch (error) { console.error(red(bold(`❌ Error retrieving config: ${error instanceof Error ? error.message : String(error)}`))); return undefined; } finally { db.close(); } } export function setConfigValue(key: string, value: string): void { const db = ensureDB(); try { db.query( `INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value], ); } catch (error) { console.error(red(bold(`❌ Error setting config: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } } export function getOrCreateConfig(): OscillumConfig { const config: OscillumConfig = { ...DEFAULT_CONFIG }; try { const storedKey = getConfigValue("IC_AUTHENTICATION_KEY"); if (!storedKey) { const newSecret = generateRandomSecret(); setConfigValue("IC_AUTHENTICATION_KEY", newSecret); config.IC_AUTHENTICATION_KEY = newSecret; console.log(yellow(bold("⚠️ Generated and saved a new authentication key to DB"))); } else { config.IC_AUTHENTICATION_KEY = storedKey; } const prompt = getConfigValue("USER_PROMPT"); if (!prompt) { setConfigValue("USER_PROMPT", DEFAULT_CONFIG.USER_PROMPT); config.USER_PROMPT = DEFAULT_CONFIG.USER_PROMPT; } else { config.USER_PROMPT = prompt; } // Initialize vote schedule delay if not set const scheduleDelay = getConfigValue("VOTE_SCHEDULE_DELAY"); if (!scheduleDelay) { setConfigValue("VOTE_SCHEDULE_DELAY", DEFAULT_CONFIG.VOTE_SCHEDULE_DELAY || "3600"); config.VOTE_SCHEDULE_DELAY = DEFAULT_CONFIG.VOTE_SCHEDULE_DELAY; } else { config.VOTE_SCHEDULE_DELAY = scheduleDelay; } // Load OpenAI API key const openaiKey = getConfigValue("OPENAI_KEY"); if (openaiKey) { config.OPENAI_KEY = openaiKey; } } catch (error) { console.error(red(bold(`❌ Database error: ${error instanceof Error ? error.message : String(error)}`))); } return config; } export function updateConfig(key: string, value: string): void { try { setConfigValue(key, value); } catch (error) { console.error(red(bold(`❌ Error updating config: ${error instanceof Error ? error.message : String(error)}`))); } } /** * Stores a proposal in the database */ export function storeProposal(id: string, data: unknown): void { const db = ensureDB(); try { // Use the toState function to safely serialize the proposal data const serializedData = JSON.stringify(toState(data)); db.query( `INSERT INTO proposals (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, [id, serializedData], ); } catch (error) { console.error(red(bold(`❌ Error storing proposal: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } } /** * Gets a proposal from the database by ID */ export function getProposal(id: string): any | undefined { const db = ensureDB(); try { for (const [data] of db.query("SELECT data FROM proposals WHERE id = ?", [id])) { return JSON.parse(String(data)); } return undefined; } catch (error) { console.error(red(bold(`❌ Error retrieving proposal: ${error instanceof Error ? error.message : String(error)}`))); return undefined; } finally { db.close(); } } /** * Checks if a proposal exists in the database */ export function proposalExists(id: string): boolean { const db = ensureDB(); try { for (const [count] of db.query("SELECT COUNT(*) FROM proposals WHERE id = ?", [id])) { return Number(count) > 0; } return false; } catch (error) { console.error(red(bold(`❌ Error checking proposal existence: ${error instanceof Error ? error.message : String(error)}`))); return false; } finally { db.close(); } } /** * Gets or sets the minimum proposal ID we've encountered */ export function getMinimumProposalId(): string | undefined { return getConfigValue("minimum_proposal_id"); } /** * Sets the minimum proposal ID if it doesn't exist or is greater than the current value */ export function updateMinimumProposalId(proposalId: string): void { const currentMin = getMinimumProposalId(); // If no minimum exists or the new ID is lower, update it if (!currentMin || BigInt(proposalId) < BigInt(currentMin)) { setConfigValue("minimum_proposal_id", proposalId); } } /** * Gets unprocessed proposals from the database */ export function getUnprocessedProposals(): any[] { const db = ensureDB(); const proposals: any[] = []; try { for (const [id, data] of db.query("SELECT id, data FROM proposals WHERE processed = 0")) { try { proposals.push({ id: String(id), ...JSON.parse(String(data)) }); } catch (parseError) { console.error(red(bold(`❌ Error parsing proposal data: ${parseError instanceof Error ? parseError.message : String(parseError)}`))); } } } catch (error) { console.error(red(bold(`❌ Error retrieving unprocessed proposals: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } return proposals; } /** * Marks a proposal as processed */ export function markProposalProcessed(id: string): void { const db = ensureDB(); try { db.query("UPDATE proposals SET processed = 1 WHERE id = ?", [id]); } catch (error) { console.error(red(bold(`❌ Error marking proposal as processed: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } } /** * Schedules a vote to be executed in the future * @param proposalId The proposal ID to vote on * @param voteType "yes" or "no" * @param delaySeconds Number of seconds to delay vote (default 3600 - 1 hour) * @returns The ID of the scheduled vote */ export function scheduleVote(proposalId: string, voteType: string, delaySeconds: number = 3600): number { // Ensure the proposal exists in the database even if just as a placeholder ensureProposalExists(proposalId); const db = ensureDB(); try { // Calculate the execution time const scheduledTime = Math.floor(Date.now() / 1000) + delaySeconds; // Delete any existing scheduled votes for this proposal db.query("DELETE FROM scheduled_votes WHERE proposal_id = ?", [proposalId]); // Insert the new scheduled vote db.query( "INSERT INTO scheduled_votes (proposal_id, vote_type, scheduled_time) VALUES (?, ?, ?)", [proposalId, voteType, scheduledTime] ); // Get the inserted ID return db.lastInsertRowId; } catch (error) { console.error(red(bold(`❌ Error scheduling vote: ${error instanceof Error ? error.message : String(error)}`))); return 0; } finally { db.close(); } } /** * Gets the scheduled vote for a proposal * @param proposalId The proposal ID * @returns Scheduled vote details or undefined */ export function getScheduledVote(proposalId: string): { id: number, voteType: string, scheduledTime: number } | undefined { // Ensure the proposal exists in the database even if just as a placeholder ensureProposalExists(proposalId); const db = ensureDB(); try { for (const [id, voteType, scheduledTime] of db.query( "SELECT id, vote_type, scheduled_time FROM scheduled_votes WHERE proposal_id = ? AND executed = 0", [proposalId] )) { return { id: Number(id), voteType: String(voteType), scheduledTime: Number(scheduledTime) }; } return undefined; } catch (error) { console.error(red(bold(`❌ Error retrieving scheduled vote: ${error instanceof Error ? error.message : String(error)}`))); return undefined; } finally { db.close(); } } /** * Cancels a scheduled vote * @param proposalId The proposal ID * @returns true if successful */ export function cancelScheduledVote(proposalId: string): boolean { const db = ensureDB(); try { db.query("DELETE FROM scheduled_votes WHERE proposal_id = ?", [proposalId]); return true; } catch (error) { console.error(red(bold(`❌ Error canceling scheduled vote: ${error instanceof Error ? error.message : String(error)}`))); return false; } finally { db.close(); } } /** * Gets all pending votes that are due for execution * @returns Array of scheduled votes ready to be executed */ export function getPendingVotes(): { id: number, proposalId: string, voteType: string }[] { const db = ensureDB(); const pendingVotes: { id: number, proposalId: string, voteType: string }[] = []; try { const currentTime = Math.floor(Date.now() / 1000); for (const [id, proposalId, voteType] of db.query( "SELECT id, proposal_id, vote_type FROM scheduled_votes WHERE scheduled_time <= ? AND executed = 0", [currentTime] )) { pendingVotes.push({ id: Number(id), proposalId: String(proposalId), voteType: String(voteType) }); } } catch (error) { console.error(red(bold(`❌ Error retrieving pending votes: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } return pendingVotes; } /** * Marks a scheduled vote as executed * @param id The ID of the scheduled vote */ export function markVoteExecuted(id: number): void { const db = ensureDB(); try { const executedTime = Math.floor(Date.now() / 1000); db.query("UPDATE scheduled_votes SET executed = 1, executed_time = ? WHERE id = ?", [executedTime, id]); } catch (error) { console.error(red(bold(`❌ Error marking vote as executed: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } } /** * Records an error that occurred during vote execution * @param id The ID of the scheduled vote * @param errorMessage Brief error message * @param errorDetails Detailed error information (optional) */ export function recordVoteError(id: number, errorMessage: string, errorDetails?: string): void { const db = ensureDB(); try { const executedTime = Math.floor(Date.now() / 1000); db.query( "UPDATE scheduled_votes SET executed = 1, executed_time = ?, error_message = ?, error_details = ? WHERE id = ?", [executedTime, errorMessage, errorDetails || null, id] ); } catch (error) { console.error(red(bold(`❌ Error recording vote error: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } } // Get proposals that have not been analyzed by the agent export function getUnanalyzedProposals(limit: number = 1, neuronId?: string): any[] { const db = ensureDB(); const proposals: any[] = []; try { const query = ` SELECT p.id, p.data FROM proposals p LEFT JOIN agent_votes av ON p.id = av.proposal_id WHERE av.id IS NULL ORDER BY CAST(p.id as INTEGER) DESC LIMIT ? `; for (const [id, data] of db.query(query, [limit])) { try { const proposal = { id: String(id), ...JSON.parse(String(data)) }; // If neuronId is provided, check if the neuron is eligible to vote if (neuronId) { let isEligible = false; // Check ballots in object format if (proposal.ballots && typeof proposal.ballots === 'object' && !Array.isArray(proposal.ballots)) { if (proposal.ballots[neuronId]) { isEligible = true; } } // Check ballots in array format if (Array.isArray(proposal.ballots)) { for (const ballot of proposal.ballots) { if (ballot.neuronId?.toString() === neuronId) { isEligible = true; break; } } } // Only add eligible proposals when filtering by neuronId if (isEligible) { proposals.push(proposal); } } else { // If no neuronId filtering, add all proposals proposals.push(proposal); } } catch (parseError) { console.error(red(bold(`❌ Error parsing proposal data: ${parseError instanceof Error ? parseError.message : String(parseError)}`))); } } } catch (error) { console.error(red(bold(`❌ Error retrieving unanalyzed proposals: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } return proposals; } // Store agent vote decision export function storeAgentVote(proposalId: string, voteType: string, reasoning: string): number { const db = ensureDB(); try { const createdAt = Math.floor(Date.now() / 1000); // Delete any existing agent vote for this proposal db.query("DELETE FROM agent_votes WHERE proposal_id = ?", [proposalId]); // Insert the new agent vote db.query( "INSERT INTO agent_votes (proposal_id, vote_type, reasoning, created_at) VALUES (?, ?, ?, ?)", [proposalId, voteType, reasoning, createdAt] ); // Get the inserted ID return db.lastInsertRowId; } catch (error) { console.error(red(bold(`❌ Error storing agent vote: ${error instanceof Error ? error.message : String(error)}`))); return 0; } finally { db.close(); } } // Update agent vote to mark it as scheduled export function markAgentVoteScheduled(proposalId: string): boolean { const db = ensureDB(); try { db.query("UPDATE agent_votes SET scheduled = TRUE WHERE proposal_id = ?", [proposalId]); return true; } catch (error) { console.error(red(bold(`❌ Error marking agent vote as scheduled: ${error instanceof Error ? error.message : String(error)}`))); return false; } finally { db.close(); } } // Log agent communication with OpenAI export function logAgentCommunication(proposalId: string, request: string, response: string): number { const db = ensureDB(); try { const createdAt = Math.floor(Date.now() / 1000); // Insert the log entry db.query( "INSERT INTO agent_logs (proposal_id, request, response, created_at) VALUES (?, ?, ?, ?)", [proposalId, request, response, createdAt] ); // Get the inserted ID return db.lastInsertRowId; } catch (error) { console.error(red(bold(`❌ Error logging agent communication: ${error instanceof Error ? error.message : String(error)}`))); return 0; } finally { db.close(); } } // Get agent votes export function getAgentVotes(limit: number = 100, offset: number = 0): any[] { const db = ensureDB(); const votes: any[] = []; try { for (const [id, proposalId, voteType, reasoning, createdAt, scheduled] of db.query( "SELECT id, proposal_id, vote_type, reasoning, created_at, scheduled FROM agent_votes ORDER BY created_at DESC LIMIT ? OFFSET ?", [limit, offset] )) { votes.push({ id: Number(id), proposalId: String(proposalId), voteType: String(voteType), reasoning: String(reasoning), createdAt: Number(createdAt), scheduled: Boolean(scheduled) }); } } catch (error) { console.error(red(bold(`❌ Error retrieving agent votes: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } return votes; } // Get agent logs for a specific proposal export function getAgentLogs(proposalId: string): any[] { const db = ensureDB(); const logs: any[] = []; try { for (const [id, request, response, createdAt] of db.query( "SELECT id, request, response, created_at FROM agent_logs WHERE proposal_id = ? ORDER BY created_at ASC", [proposalId] )) { logs.push({ id: Number(id), request: String(request), response: String(response), createdAt: Number(createdAt) }); } } catch (error) { console.error(red(bold(`❌ Error retrieving agent logs: ${error instanceof Error ? error.message : String(error)}`))); } finally { db.close(); } return logs; } // Get agent vote for a specific proposal export function getAgentVote(proposalId: string): any | undefined { const db = ensureDB(); try { for (const [id, voteType, reasoning, createdAt, scheduled] of db.query( "SELECT id, vote_type, reasoning, created_at, scheduled FROM agent_votes WHERE proposal_id = ?", [proposalId] )) { return { id: Number(id), proposalId: proposalId, voteType: String(voteType), reasoning: String(reasoning), createdAt: Number(createdAt), scheduled: Boolean(scheduled) }; } return undefined; } catch (error) { console.error(red(bold(`❌ Error retrieving agent vote: ${error instanceof Error ? error.message : String(error)}`))); return undefined; } finally { db.close(); } } // Reset all agent data (votes and logs) for a specific proposal export function resetAgentData(proposalId: string): boolean { const db = ensureDB(); try { // Begin a transaction to ensure all operations succeed or fail together db.query("BEGIN TRANSACTION"); // Delete agent votes for this proposal db.query("DELETE FROM agent_votes WHERE proposal_id = ?", [proposalId]); // Delete agent logs for this proposal db.query("DELETE FROM agent_logs WHERE proposal_id = ?", [proposalId]); // Commit the transaction db.query("COMMIT"); return true; } catch (error) { // Rollback on error db.query("ROLLBACK"); console.error(red(bold(`❌ Error resetting agent data: ${error instanceof Error ? error.message : String(error)}`))); return false; } finally { db.close(); } } export function getDB() { // Return a new database connection return ensureDB(); } /** * Ensures that a proposal exists in the database * If it doesn't exist, creates a minimal placeholder entry * @param id The proposal ID to check/create * @returns true if exists/created successfully, false otherwise */ export function ensureProposalExists(id: string): boolean { if (proposalExists(id)) { return true; // Already exists } // Proposal doesn't exist, create a placeholder const db = ensureDB(); try { const placeholderData = { id: id, placeholder: true, status: 0, // Unknown status topic: 0, // Unspecified topic proposalTimestampSeconds: Math.floor(Date.now() / 1000), proposal: { title: `Proposal ${id} (Placeholder)`, summary: "This is a placeholder entry for a proposal that was referenced but couldn't be fetched from the IC." } }; const serializedData = JSON.stringify(placeholderData); db.query( `INSERT INTO proposals (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, [id, serializedData], ); console.log(yellow(bold(`⚠️ Created placeholder for proposal ${id} to prevent data loss`))); return true; } catch (error) { console.error(red(bold(`❌ Error creating proposal placeholder: ${error instanceof Error ? error.message : String(error)}`))); return false; } finally { db.close(); } }