diff --git a/auto-chain-agent/.gitignore b/auto-chain-agent/.gitignore index 4f707ba..0750022 100644 --- a/auto-chain-agent/.gitignore +++ b/auto-chain-agent/.gitignore @@ -33,6 +33,8 @@ Thumbs.db # Database *.sqlite *.sqlite3 - +summary-differences.json +summary-*.json +diffs/ # Test coverage coverage \ No newline at end of file diff --git a/auto-chain-agent/README.md b/auto-chain-agent/README.md index 1d1bf35..3a0d4a6 100644 --- a/auto-chain-agent/README.md +++ b/auto-chain-agent/README.md @@ -5,13 +5,40 @@ A blockchain interaction service that provides natural language processing capab ## Features - Natural language processing for blockchain interactions -- Balance checking -- Transaction sending -- Transaction history retrieval +- Memory-enabled conversations with context retention +- Balance checking and transaction management +- Wallet management +- Transaction history tracking ## Available Tools The agent supports the following blockchain operations: - `get_balance`: Check the balance of a blockchain address -- `send_transaction`: Send tokens to another address \ No newline at end of file +- `send_transaction`: Send tokens to another address (supports AI3 token transfers) +- `get_transaction_history`: Retrieve transaction history and account information + +The service consists of three main components: + +- **Agents**: Core blockchain interaction and natural language processing +- **Backend**: API service for handling requests +- **Frontend**: User interface for interacting with the agent + +## Setup + +### Prerequisites + +- Node.js (v18 or higher) +- Yarn package manager +- SQLite3 + +### Environment Variables + +Create `.env` files in the respective directories following the `.env.example` file. + +### Running the Services + +```bash +yarn install +yarn dev +``` diff --git a/auto-chain-agent/agents/.env.sample b/auto-chain-agent/agents/.env.sample index b865189..da1ab82 100644 --- a/auto-chain-agent/agents/.env.sample +++ b/auto-chain-agent/agents/.env.sample @@ -1,4 +1,5 @@ OPENAI_API_KEY= ANTHROPIC_API_KEY= -TEST_MNEMONIC= +AGENT_KEY= AGENTS_PORT=3000 +DSN_API_KEY= \ No newline at end of file diff --git a/auto-chain-agent/agents/package.json b/auto-chain-agent/agents/package.json index 2e3e458..786842b 100644 --- a/auto-chain-agent/agents/package.json +++ b/auto-chain-agent/agents/package.json @@ -10,9 +10,9 @@ "start": "node dist/index.js" }, "dependencies": { - "@autonomys/auto-consensus": "0.7.3", - "@autonomys/auto-drive": "0.7.3", - "@autonomys/auto-utils": "0.7.3", + "@autonomys/auto-consensus": "1.0.9", + "@autonomys/auto-drive": "1.0.9", + "@autonomys/auto-utils": "1.0.9", "@langchain/anthropic": "0.3.7", "@langchain/community": "0.3.11", "@langchain/core": "0.3.3", diff --git a/auto-chain-agent/agents/src/config/index.ts b/auto-chain-agent/agents/src/config/index.ts index fc79578..d6865b3 100644 --- a/auto-chain-agent/agents/src/config/index.ts +++ b/auto-chain-agent/agents/src/config/index.ts @@ -1,17 +1,20 @@ import dotenv from 'dotenv'; +import path from 'path'; dotenv.config(); export const config = { + CHECK_INTERVAL: 20 * 1000, + SUMMARY_DIR: path.join(process.cwd(), 'diffs'), + SUMMARY_FILE_PATH : path.join(process.cwd(), 'diffs', 'summary-differences.json'), + DIFF_FILE_PREFIX: 'summary-diff', + LLM_MODEL: "gpt-4o-mini", + TEMPERATURE: 0.5, + AGENT_KEY: process.env.AGENT_KEY || '//Alice', + NETWORK: 'taurus', port: process.env.AGENTS_PORT || 3000, anthropicApiKey: process.env.ANTHROPIC_API_KEY, openaiApiKey: process.env.OPENAI_API_KEY, environment: process.env.NODE_ENV || 'development', - autoConsensus: { - apiKey: process.env.AUTO_CONSENSUS_API_KEY, - }, - llmConfig: { - temperature: 0.4, - maxTokens: 1500 - } + dsnApiKey: process.env.DSN_API_KEY, }; \ No newline at end of file diff --git a/auto-chain-agent/agents/src/services/chainAgent.ts b/auto-chain-agent/agents/src/services/chainAgent.ts index c779405..36eb908 100644 --- a/auto-chain-agent/agents/src/services/chainAgent.ts +++ b/auto-chain-agent/agents/src/services/chainAgent.ts @@ -5,9 +5,10 @@ import { ToolNode } from "@langchain/langgraph/prebuilt"; import { blockchainTools } from './tools'; import { config } from "../config/index"; import logger from "../logger"; -import { createThreadStorage } from "./threadStorage"; - -// Define state schema for the graph +import { startWithHistory } from "./utils"; +import { loadThreadSummary, startSummarySystem } from './thread/summarySystem'; +import { createThreadStorage } from './thread/threadStorage'; +import { ConversationState } from './thread/interface'; const StateAnnotation = Annotation.Root({ messages: Annotation({ reducer: (curr, prev) => [...curr, ...prev], @@ -22,23 +23,26 @@ const StateAnnotation = Annotation.Root({ }; result?: string; }>>({ - reducer: (_, next) => next, // Only keep current interaction's tool calls + reducer: (_, next) => next, default: () => [], }) }); -// Initialize core components const model = new ChatOpenAI({ openAIApiKey: config.openaiApiKey, - modelName: "gpt-4o-mini", - temperature: 0.7, + modelName: config.LLM_MODEL, + temperature: config.TEMPERATURE, }).bindTools(blockchainTools); const threadStorage = createThreadStorage(); const checkpointer = new MemorySaver(); const toolNode = new ToolNode(blockchainTools); -// Define node functions +const conversationState: ConversationState = { + isInitialLoad: true, + needsHistoryRebuild: false +}; + const agentNode = async (state: typeof StateAnnotation.State) => { try { const systemMessage = new SystemMessage({ @@ -47,6 +51,23 @@ const agentNode = async (state: typeof StateAnnotation.State) => { - When blockchain operations are needed, you can check balances and perform transactions` }); + if (conversationState.needsHistoryRebuild) { + await startWithHistory().then(async () => { + const prevMessages = (await loadThreadSummary()) + .map((content: string) => new HumanMessage({ content })); + state.messages = [...state.messages, ...prevMessages]; + conversationState.isInitialLoad = false; + }); + conversationState.needsHistoryRebuild = false; + } + + if (conversationState.isInitialLoad) { + const prevMessages = (await loadThreadSummary()) + .map(content => new HumanMessage({ content })); + state.messages = [...state.messages, ...prevMessages]; + conversationState.isInitialLoad = false; + } + const messages = [systemMessage, ...state.messages]; const response = await model.invoke(messages); @@ -108,7 +129,6 @@ const shouldContinue = (state: typeof StateAnnotation.State) => { return 'agent'; }; -// Create and initialize the graph const createBlockchainGraph = async () => { try { return new StateGraph(StateAnnotation) @@ -124,18 +144,17 @@ const createBlockchainGraph = async () => { } }; -// Initialize graph let agentGraph: Awaited>; (async () => { try { agentGraph = await createBlockchainGraph(); - logger.info('Blockchain agent initialized successfully'); + await startSummarySystem(); + logger.info('Blockchain agent and summary system initialized successfully'); } catch (error) { logger.error('Failed to initialize blockchain agent:', error); } })(); -// Export the agent interface export const blockchainAgent = { async handleMessage({ message, threadId }: { message: string; threadId?: string }) { try { @@ -145,14 +164,32 @@ export const blockchainAgent = { const currentThreadId = threadId || `blockchain_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; const previousState = threadId ? await threadStorage.loadThread(threadId) : null; - + + let relevantContext: BaseMessage[] = []; + if (!previousState || previousState.messages.length <= 2) { + const summaries = await loadThreadSummary(); + if (summaries.length > 0) { + relevantContext = [new SystemMessage({ + content: `Previous conversations context: ${ + summaries + .slice(0, 100) + .map(summary => summary.trim()) + .join(' | ') + }` + })]; + } + } + logger.info(`Relevant context: ${relevantContext}`); const initialState = { messages: previousState ? [ ...previousState.messages, new HumanMessage({ content: message }) ] : [ + ...relevantContext, new SystemMessage({ - content: `You are a helpful AI assistant. You can engage in general conversation and also help with blockchain operations like checking balances and performing transactions.` + content: ` + You are a helpful AI assistant. + You can engage in general conversation and also help with blockchain operations like checking balances and performing transactions.` }), new HumanMessage({ content: message }) ] diff --git a/auto-chain-agent/agents/src/services/thread/db.ts b/auto-chain-agent/agents/src/services/thread/db.ts new file mode 100644 index 0000000..82bbc74 --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/db.ts @@ -0,0 +1,30 @@ +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; +import logger from '../../logger'; + +export const initializeDb = async (dbPath: string) => { + logger.info('Initializing SQLite database at:', dbPath); + const db = await open({ + filename: dbPath, + driver: sqlite3.Database + }); + + await db.exec(` + CREATE TABLE IF NOT EXISTS threads ( + thread_id TEXT PRIMARY KEY, + messages TEXT NOT NULL, + tool_calls TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS summary_uploads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + upload_id TEXT NOT NULL, + CID TEXT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + + return db; +}; \ No newline at end of file diff --git a/auto-chain-agent/agents/src/services/thread/index.ts b/auto-chain-agent/agents/src/services/thread/index.ts new file mode 100644 index 0000000..ec576c2 --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/index.ts @@ -0,0 +1,3 @@ +import { createThreadStorage } from './threadStorage'; + +export type ThreadStorage = ReturnType; diff --git a/auto-chain-agent/agents/src/services/thread/interface.ts b/auto-chain-agent/agents/src/services/thread/interface.ts new file mode 100644 index 0000000..057f51c --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/interface.ts @@ -0,0 +1,33 @@ +import { BaseMessage, MessageContent } from '@langchain/core/messages'; + +export interface ThreadState { + messages: BaseMessage[]; + toolCalls: Array<{ + id: string; + type: string; + function: { + name: string; + arguments: string; + }; + result?: string; + }>; +} +export interface ConversationState { + isInitialLoad: boolean; + needsHistoryRebuild: boolean; +} + +export interface SummaryDifference { + timestamp: string; + threadId: string; + previousSummary: string | MessageContent; + currentSummary: string | MessageContent; + difference: string | MessageContent; + previousCID?: string; +} + +export interface SummaryState { + lastCheck: string; + differences: SummaryDifference[]; +} + diff --git a/auto-chain-agent/agents/src/services/thread/summaryState.ts b/auto-chain-agent/agents/src/services/thread/summaryState.ts new file mode 100644 index 0000000..372630a --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/summaryState.ts @@ -0,0 +1,67 @@ +import fs from 'fs'; +import logger from '../../logger'; +import { SummaryState } from './interface'; +import { saveDiffFile } from './utils'; +import { config } from '../../config'; + +export const loadSummaryState = async (): Promise => { + try { + if (fs.existsSync(config.SUMMARY_FILE_PATH)) { + const data = await fs.promises.readFile(config.SUMMARY_FILE_PATH, 'utf8'); + return JSON.parse(data); + } + return { + lastCheck: new Date().toISOString(), + differences: [] + }; + } catch (error) { + logger.error('Error loading summary state:', error); + return { + lastCheck: new Date().toISOString(), + differences: [] + }; + } +}; + +export const saveSummaryState = async (state: SummaryState) => { + try { + const existingState = await (async (): Promise => { + if (fs.existsSync(config.SUMMARY_FILE_PATH)) { + const existingData = await fs.promises.readFile(config.SUMMARY_FILE_PATH, 'utf8'); + return JSON.parse(existingData); + } + return null; + })(); + + // Save main summary file + await fs.promises.writeFile( + config.SUMMARY_FILE_PATH, + JSON.stringify(state, null, 2), + 'utf8' + ); + + // Only create and upload diff file if there are actual changes + if (!existingState || + JSON.stringify(existingState.differences) !== JSON.stringify(state.differences)) { + + // Get new differences since last state + const newDifferences = existingState + ? state.differences.filter(diff => + !existingState.differences.some( + existingDiff => existingDiff.timestamp === diff.timestamp + ) + ) + : state.differences; + + if (newDifferences.length > 0) { + await saveDiffFile(newDifferences); + } + } else { + logger.info('No changes detected in summary differences, skipping diff file creation'); + } + } catch (error) { + logger.error('Error saving summary state:', error); + throw error; + } +}; + diff --git a/auto-chain-agent/agents/src/services/thread/summarySystem.ts b/auto-chain-agent/agents/src/services/thread/summarySystem.ts new file mode 100644 index 0000000..406d86d --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/summarySystem.ts @@ -0,0 +1,213 @@ +import { loadSummaryState, saveSummaryState } from './summaryState'; +import { createThreadStorage } from './threadStorage'; +import logger from '../../logger'; +import fs from 'fs'; +import path from 'path'; +import { ChatOpenAI } from '@langchain/openai'; +import { SystemMessage, HumanMessage } from '@langchain/core/messages'; +import { SummaryDifference, SummaryState } from './interface'; +import { initializeDb } from './db'; +import { config } from '../../config'; + + + +export const loadThreadSummary = async () => { + const summaryState = await loadSummaryState(); + + // Group differences by threadId and sort by timestamp + const threadSummaries = new Map(); + + // Sort differences by timestamp (newest first) before processing + const sortedDifferences = [...summaryState.differences].sort( + (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() + ); + + // Take the first (most recent) difference for each threadId + for (const diff of sortedDifferences) { + if (!threadSummaries.has(diff.threadId)) { + threadSummaries.set(diff.threadId, diff); + } + } + + const summaries = Array.from(threadSummaries.values()) + .map(diff => String(diff.currentSummary)) + .filter(summary => summary.trim().length > 0); + + logger.info(`Loaded ${summaries.length} thread summaries from differences`); + return summaries; +}; + + +export const checkAndUpdateSummaries = async () => { + logger.info('Checking for updates to summarize...'); + const threadStorage = createThreadStorage(); + const summaryState = await loadSummaryState(); + const lastCheckDate = new Date(summaryState.lastCheck); + + // Get only the most recent thread modified since last check + const modifiedThreads = await threadStorage.getAllThreads().then(threads => + threads + .filter(thread => new Date(thread.updated_at) > lastCheckDate) + .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) + .slice(0, 1) + ); + + if (modifiedThreads.length === 0) { + logger.info('No thread modifications found since last check'); + summaryState.lastCheck = new Date().toISOString(); + await saveSummaryState(summaryState); + return; + } + + logger.info(`Found ${modifiedThreads.length} modified threads since last check`); + + const model = new ChatOpenAI({ + modelName: config.LLM_MODEL, + temperature: config.TEMPERATURE + }); + + for (const thread of modifiedThreads) { + logger.info(`Checking thread ${thread.thread_id}`); + const state = await threadStorage.loadThread(thread.thread_id); + if (!state?.messages.length) continue; + + const lastMessages = state.messages.slice(-5); + const currentContent = lastMessages + .map(msg => `${msg._getType()}: ${String(msg.content)}`) + .join('\n'); + + // Find previous summary for this thread + const previousSummary = summaryState.differences + .filter(diff => diff.threadId === thread.thread_id) + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .shift()?.currentSummary || ''; + + // Generate new summary + const summaryResponse = await model.invoke([ + new SystemMessage({ + content: `IMPORTANT CHANGES: + - New transactions or blockchain operations like checking balances or performing transactions + - Remember user wallet address + - Changes in wallet addresses or balances + - New user interactions or requests + - Changes in decisions or outcomes + Compare the previous summary (IF THERE IS ANY) with the current conversation and highlight only the new or changed information. + Summarize the important information if it is the initial summary like user's wallet address, name and etc. + If there are no meaningful changes, explicitly state "NO_CHANGES". + DON'T ADD MNEMONICS IN THE SUMMARY AT ALL. + Format as a brief diff summary. + ` + }), + new HumanMessage({ + content: `Previous summary: ${String(previousSummary)}\n\nCurrent conversation: ${currentContent}` + }) + ]); + logger.info(`Summary response: ${summaryResponse.content}`); + const summaryContent = String(summaryResponse.content); + + const hasNoChanges = + summaryContent.includes('NO_CHANGES') || + (typeof summaryContent === 'string' && typeof previousSummary === 'string' && summaryContent.trim() === previousSummary.trim()) || + (typeof summaryContent === 'string' && summaryContent.toLowerCase().includes('no new')) || + (typeof summaryContent === 'string' && summaryContent.toLowerCase().includes('no changes')) || + summaryContent.toLowerCase().includes('same as before'); + + if (!hasNoChanges) { + logger.info(`Adding new summary for thread ${thread.thread_id} - Found changes`); + summaryState.differences.push({ + timestamp: new Date().toISOString(), + threadId: thread.thread_id, + previousSummary: String(previousSummary), + currentSummary: summaryContent, + difference: summaryContent, + previousCID: undefined + }); + } else { + logger.info(`Skipping summary for thread ${thread.thread_id} - No meaningful changes`); + } + } + + summaryState.lastCheck = new Date().toISOString(); + await saveSummaryState(summaryState); +}; + + +export const initializeSummaries = async () => { + logger.info('Initializing summary system...'); + + // Check if summaries already exist + const summaryExists = fs.existsSync(config.SUMMARY_FILE_PATH); + if (summaryExists) { + logger.info('Summary file already exists, skipping initialization'); + return; + } + + const initialState: SummaryState = { + lastCheck: new Date().toISOString(), + differences: [] + }; + + // Get all existing threads + const threadStorage = createThreadStorage(); + const allThreads = await threadStorage.getAllThreads(); + + logger.info(`Found ${allThreads.length} existing threads to summarize`); + + const model = new ChatOpenAI({ + modelName: config.LLM_MODEL, + temperature: config.TEMPERATURE + }); + + // Create initial summaries for all threads + for (const thread of allThreads) { + const state = await threadStorage.loadThread(thread.thread_id); + if (!state?.messages.length) continue; + + const messages = state.messages; + const currentContent = messages + .map(msg => `${msg._getType()}: ${String(msg.content)}`) + .join('\n'); + + const summaryResponse = await model.invoke([ + new SystemMessage({ + content: `Provide a brief summary of this conversation. Focus on: + - Key decisions or outcomes + - Main user requests + - Important actions taken` + }), + new HumanMessage({ + content: currentContent + }) + ]); + + initialState.differences.push({ + timestamp: new Date().toISOString(), + threadId: thread.thread_id, + previousSummary: '', + currentSummary: String(summaryResponse.content), + difference: String(summaryResponse.content), + previousCID: undefined + }); + + logger.info(`Created initial summary for thread ${thread.thread_id}`); + } + + await saveSummaryState(initialState); + logger.info('Summary system initialized successfully'); +}; + + +export const startSummarySystem = async () => { + await initializeSummaries(); + setInterval(checkAndUpdateSummaries, config.CHECK_INTERVAL); + logger.info('Summary system started'); +}; + +export const getSummaryUploads = async () => { + const db = await initializeDb(path.join(process.cwd(), 'thread-storage.sqlite')); + return db.all( + 'SELECT * FROM summary_uploads ORDER BY timestamp DESC' + ); +}; + + diff --git a/auto-chain-agent/agents/src/services/thread/threadStorage.ts b/auto-chain-agent/agents/src/services/thread/threadStorage.ts new file mode 100644 index 0000000..dd008f6 --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/threadStorage.ts @@ -0,0 +1,125 @@ + +import path from 'path'; +import fs from 'fs'; +import logger from '../../logger'; +import { initializeDb } from './db'; +import { ThreadState } from './interface'; +import { serializeMessage, deserializeMessage } from './utils'; +import { config } from '../../config'; + + + +export const createThreadStorage = () => { + + // Initialize if database doesn't exist + const dbPath = path.join(process.cwd(), 'thread-storage.sqlite'); + const dbExists = fs.existsSync(dbPath); + + if (!dbExists) { + logger.info('Database file not found, creating new database'); + } + + const dbPromise = initializeDb(dbPath); + + // Create diff folder if it doesn't exist + if (!fs.existsSync(config.SUMMARY_DIR)) { + fs.mkdirSync(config.SUMMARY_DIR, { recursive: true }); + } + + return { + async saveThread(threadId: string, state: ThreadState) { + try { + const db = await dbPromise; + const threadData = { + threadId, + messageCount: state.messages.length, + toolCallCount: state.toolCalls.length + }; + logger.info(`Preparing to save thread data: ${JSON.stringify(threadData)}`); + + const serializedMessages = JSON.stringify(state.messages.map(serializeMessage)); + const serializedToolCalls = JSON.stringify(state.toolCalls); + + const serializedInfo = { + messagesLength: serializedMessages.length, + toolCallsLength: serializedToolCalls.length + }; + logger.info(`Serialized data: ${JSON.stringify(serializedInfo)}`); + + const result = await db.run( + `INSERT OR REPLACE INTO threads (thread_id, messages, tool_calls, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, + [ + threadId, + serializedMessages, + serializedToolCalls + ] + ); + + logger.info(`Database operation result: ${JSON.stringify(result)}`); + + // Verify the save immediately + const verification = await db.get( + 'SELECT thread_id, length(messages) as msg_len FROM threads WHERE thread_id = ?', + threadId + ); + + logger.info(`Verification result: ${JSON.stringify(verification)}`); + + if (!verification) { + throw new Error('Failed to verify saved data'); + } + } catch (error) { + logger.error('Error in saveThread:', error); + logger.error('Full error details:', { + name: (error as Error).name, + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }); + throw error; + } + }, + + async loadThread(threadId: string): Promise { + const db = await dbPromise; + const row = await db.get( + 'SELECT messages, tool_calls FROM threads WHERE thread_id = ?', + threadId + ); + + if (!row) { + logger.warn(`Thread not found: ${threadId}`); + return null; + } + + return { + messages: JSON.parse(row.messages).map(deserializeMessage), + toolCalls: JSON.parse(row.tool_calls || '[]') + }; + }, + + async getAllThreads() { + const db = await dbPromise; + return db.all( + 'SELECT thread_id, created_at, updated_at FROM threads ORDER BY updated_at DESC' + ); + }, + + async deleteThread(threadId: string) { + const db = await dbPromise; + await db.run('DELETE FROM threads WHERE thread_id = ?', threadId); + logger.info(`Thread deleted: ${threadId}`); + }, + + async cleanup(olderThanDays = 30) { + const db = await dbPromise; + const result = await db.run( + 'DELETE FROM threads WHERE updated_at < datetime("now", ?)', + [`-${olderThanDays} days`] + ); + const deletedCount = result.changes || 0; + logger.info(`Cleaned up ${deletedCount} old threads`); + return deletedCount; + } + }; +}; diff --git a/auto-chain-agent/agents/src/services/thread/utils.ts b/auto-chain-agent/agents/src/services/thread/utils.ts new file mode 100644 index 0000000..48ee8d3 --- /dev/null +++ b/auto-chain-agent/agents/src/services/thread/utils.ts @@ -0,0 +1,71 @@ +import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages'; +import fs from 'fs'; +import path from 'path'; +import logger from '../../logger'; +import { initializeDb } from './db'; +import { SummaryDifference } from './interface'; +import { uploadFile } from '../utils'; +import { config } from '../../config'; + +const createMessage = (type: 'human' | 'ai', content: string) => + type === 'human' ? new HumanMessage({ content }) : new AIMessage({ content }); + +export const serializeMessage = (msg: BaseMessage) => ({ + _type: msg._getType(), + content: msg.content, + additional_kwargs: msg.additional_kwargs +}); + +export const deserializeMessage = (msg: any) => { + if (!msg) return createMessage('ai', 'Invalid message'); + return createMessage( + msg._type === 'human' ? 'human' : 'ai', + msg.content + ); +}; + + +export const saveDiffFile = async (differences: SummaryDifference[]) => { + try { + // Create diffs directory if it doesn't exist + if (!fs.existsSync(config.SUMMARY_DIR)) { + fs.mkdirSync(config.SUMMARY_DIR, { recursive: true }); + } + + const db = await initializeDb(path.join(process.cwd(), 'thread-storage.sqlite')); + const lastUpload = await db.get( + 'SELECT CID FROM summary_uploads ORDER BY timestamp DESC LIMIT 1' + ); + + const differencesWithPrevCID = differences.map(diff => ({ + ...diff, + previousCID: lastUpload?.CID || null + })); + + // Generate unique filename based on timestamp + const timestamp = new Date().getTime(); + const diffFileName = `${config.DIFF_FILE_PREFIX}${timestamp}.json`; + const diffFilePath = path.join(config.SUMMARY_DIR, diffFileName); + + await fs.promises.writeFile( + diffFilePath, + JSON.stringify(differencesWithPrevCID, null, 2), + 'utf8' + ); + + // Upload the diff file + const fileBuffer = await fs.promises.readFile(diffFilePath); + const uploadResult = await uploadFile(fileBuffer, diffFileName); + + await db.run( + 'INSERT INTO summary_uploads (upload_id, CID) VALUES (?, ?)', + [uploadResult.upload_id, uploadResult.completion.cid] + ); + + logger.info(`Diff file uploaded successfully with ID: ${uploadResult.upload_id}`); + return diffFileName; + } catch (error) { + logger.error('Error saving diff file:', error); + throw error; + } +}; diff --git a/auto-chain-agent/agents/src/services/threadStorage.ts b/auto-chain-agent/agents/src/services/threadStorage.ts deleted file mode 100644 index 755a977..0000000 --- a/auto-chain-agent/agents/src/services/threadStorage.ts +++ /dev/null @@ -1,123 +0,0 @@ -import sqlite3 from 'sqlite3'; -import { open } from 'sqlite'; -import logger from '../logger'; -import { HumanMessage, AIMessage, BaseMessage } from '@langchain/core/messages'; -import path from 'path'; - -export interface ThreadState { - messages: BaseMessage[]; - toolCalls: Array<{ - id: string; - type: string; - function: { - name: string; - arguments: string; - }; - result?: string; - }>; -} - -const initializeDb = async (dbPath: string) => { - logger.info('Initializing SQLite database at:', dbPath); - - const db = await open({ - filename: dbPath, - driver: sqlite3.Database - }); - - await db.exec(` - DROP TABLE IF EXISTS threads; - CREATE TABLE threads ( - thread_id TEXT PRIMARY KEY, - messages TEXT NOT NULL, - tool_calls TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - return db; -}; - -const createMessage = (type: 'human' | 'ai', content: string) => - type === 'human' ? new HumanMessage({ content }) : new AIMessage({ content }); - -const serializeMessage = (msg: BaseMessage) => ({ - _type: msg._getType(), - content: msg.content, - additional_kwargs: msg.additional_kwargs -}); - -const deserializeMessage = (msg: any) => { - if (!msg) return createMessage('ai', 'Invalid message'); - return createMessage( - msg._type === 'human' ? 'human' : 'ai', - msg.content - ); -}; - -export const createThreadStorage = () => { - const dbPath = path.join(process.cwd(), 'thread-storage.sqlite'); - const dbPromise = initializeDb(dbPath); - - return { - async saveThread(threadId: string, state: ThreadState) { - const db = await dbPromise; - await db.run( - `INSERT OR REPLACE INTO threads (thread_id, messages, tool_calls, updated_at) - VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, - [ - threadId, - JSON.stringify(state.messages.map(serializeMessage)), - JSON.stringify(state.toolCalls) - ] - ); - - logger.info(`Thread saved: ${threadId}`); - }, - - async loadThread(threadId: string): Promise { - const db = await dbPromise; - const row = await db.get( - 'SELECT messages, tool_calls FROM threads WHERE thread_id = ?', - threadId - ); - - if (!row) { - logger.warn(`Thread not found: ${threadId}`); - return null; - } - - return { - messages: JSON.parse(row.messages).map(deserializeMessage), - toolCalls: JSON.parse(row.tool_calls || '[]') - }; - }, - - async getAllThreads() { - const db = await dbPromise; - return db.all( - 'SELECT thread_id, created_at, updated_at FROM threads ORDER BY updated_at DESC' - ); - }, - - async deleteThread(threadId: string) { - const db = await dbPromise; - await db.run('DELETE FROM threads WHERE thread_id = ?', threadId); - logger.info(`Thread deleted: ${threadId}`); - }, - - async cleanup(olderThanDays = 30) { - const db = await dbPromise; - const result = await db.run( - 'DELETE FROM threads WHERE updated_at < datetime("now", ?)', - [`-${olderThanDays} days`] - ); - const deletedCount = result.changes || 0; - logger.info(`Cleaned up ${deletedCount} old threads`); - return deletedCount; - } - }; -}; - -export type ThreadStorage = ReturnType; diff --git a/auto-chain-agent/agents/src/services/tools.ts b/auto-chain-agent/agents/src/services/tools.ts index be9e7f5..41cc03d 100644 --- a/auto-chain-agent/agents/src/services/tools.ts +++ b/auto-chain-agent/agents/src/services/tools.ts @@ -1,22 +1,11 @@ import { tool } from '@langchain/core/tools'; import { z } from 'zod'; import logger from '../logger'; -import { activate, activateWallet } from '@autonomys/auto-utils'; +import { activate, activateWallet, generateWallet } from '@autonomys/auto-utils'; import { balance, transfer, account } from '@autonomys/auto-consensus'; +import { formatTokenValue, toShannons } from './utils'; +import { config } from '../config'; -const formatTokenValue = (tokenValue: bigint, decimals: number = 18) => { - return Number(tokenValue) / 10 ** decimals; -}; - -const toShannons = (amount: string): string => { - // Remove 'ai3' or any other unit suffix and convert to number - const numericAmount = parseFloat(amount.replace(/\s*ai3\s*/i, '')); - // Convert to smallest unit (18 decimals) - return (BigInt(Math.floor(numericAmount * 10 ** 18)).toString()); -}; - -// For development/testing, use a test wallet -const TEST_MNEMONIC = process.env.TEST_MNEMONIC || '//Alice'; // Default to //Alice for development export const getBalanceTool = tool( async (input: { address: string }) => { @@ -57,8 +46,8 @@ export const sendTransactionTool = tool( // Use test wallet const { api, accounts } = await activateWallet({ - uri: TEST_MNEMONIC, // Use test mnemonic or Alice for development - networkId: 'taurus' + uri: config.AGENT_KEY, + networkId: config.NETWORK }); const sender = accounts[0]; @@ -67,7 +56,14 @@ export const sendTransactionTool = tool( // Create transfer transaction const tx = transfer(api, input.to, formattedAmount); - const txHash: { status: string, hash: string, block: string, from: string, to: string, amount: string } = await new Promise((resolve, reject) => { + const txHash: { + status: string, + hash: string, + block: string, + from: string, + to: string, + amount: string + } = await new Promise((resolve, reject) => { tx.signAndSend(sender, ({ status, txHash }) => { if (status.isInBlock) { resolve({ @@ -116,16 +112,16 @@ export const getTransactionHistoryTool = tool( logger.info(`Getting transaction history for: ${input.address}`); // Activate the API connection - const api = await activate({ networkId: 'gemini-3h' }); + const api = await activate({ networkId: config.NETWORK }); // Get account information including nonce (transaction count) const accountInfo = await account(api, input.address); - + // Format the response const response = `Account ${input.address}: Nonce (Transaction Count): ${accountInfo.nonce} - Free Balance: ${accountInfo.data.free} - Reserved Balance: ${accountInfo.data.reserved}`; + Free Balance: ${formatTokenValue(accountInfo.data.free)} + Reserved Balance: ${formatTokenValue(accountInfo.data.reserved)}`; await api.disconnect(); return response; @@ -143,9 +139,31 @@ export const getTransactionHistoryTool = tool( } ); -// Export all tools as an array for convenience +export const createWalletTool = tool( + async () => { + try { + const wallet = await generateWallet(); + + return { + address: wallet.keyringPair?.address, + publicKey: wallet.keyringPair?.publicKey.toString(), + mnemonic: wallet.mnemonic + }; + } catch (error) { + logger.error('Error creating wallet:', error); + throw error; + } + }, + { + name: "create_wallet", + description: "Create a new wallet", + schema: z.object({}) + } +); + export const blockchainTools = [ getBalanceTool, sendTransactionTool, - getTransactionHistoryTool + getTransactionHistoryTool, + createWalletTool ]; diff --git a/auto-chain-agent/agents/src/services/utils.ts b/auto-chain-agent/agents/src/services/utils.ts new file mode 100644 index 0000000..9f800ba --- /dev/null +++ b/auto-chain-agent/agents/src/services/utils.ts @@ -0,0 +1,187 @@ +import axios from 'axios'; +import FormData from 'form-data'; +import { config } from '../config'; +import fs from 'fs'; +import path from 'path'; +import logger from '../logger'; +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; + + +// TODO - Solve ESM-only conflict with CommonJS - auto-drive +// import { uploadFile as uploadFileDSN, createAutoDriveApi } from '@autonomys/auto-drive' + +interface UploadResponse { + upload_id: string; + status: string; + completion: any; +} + +interface RetrieveResponse { + filename: string; + filepath: string; +} + +export const formatTokenValue = (tokenValue: bigint, decimals: number = 18) => { + return Number(tokenValue) / 10 ** decimals; +}; + +export const toShannons = (amount: string): string => { + // Remove 'ai3' or any other unit suffix and convert to number + const numericAmount = parseFloat(amount.replace(/\s*ai3\s*/i, '')); + // Convert to smallest unit (18 decimals) + return (BigInt(Math.floor(numericAmount * 10 ** 18)).toString()); +}; + + +export const uploadFile = async (fileBuffer: Buffer, filename: string): Promise => { + const baseUrl = 'https://demo.auto-drive.autonomys.xyz'; + const headers = { + Authorization: `Bearer ${config.dsnApiKey}`, + 'X-Auth-Provider': 'apikey', + }; + + // Create upload request + const createData = { + filename: filename, + mimeType: 'application/json', + uploadOptions: null, + }; + + const { data: uploadData } = await axios.post(`${baseUrl}/uploads/file`, createData, { headers }); + const uploadId = uploadData.id; + + const formData = new FormData(); + formData.append('file', fileBuffer, { + filename: filename, + contentType: 'application/json', + }); + formData.append('index', '0'); + + await axios.post(`${baseUrl}/uploads/file/${uploadId}/chunk`, formData, { + headers: { ...headers, ...formData.getHeaders() }, + }); + + const { data: completionData } = await axios.post(`${baseUrl}/uploads/${uploadId}/complete`, null, { headers }); + + return { + upload_id: uploadId, + status: 'success', + completion: completionData, + }; +}; + +export const retrieveFile = async (CID: string): Promise => { + const baseUrl = 'https://demo.auto-drive.autonomys.xyz'; + const headers = { + Authorization: `Bearer ${config.dsnApiKey}`, + 'X-Auth-Provider': 'apikey', + }; + + try { + // Create diffs directory if it doesn't exist + const diffsDir = path.join(process.cwd(), 'diffs'); + if (!fs.existsSync(diffsDir)) { + fs.mkdirSync(diffsDir, { recursive: true }); + } + + // Download file from DSN + const response = await axios({ + method: 'get', + url: `${baseUrl}/objects/${CID}/download`, + headers, + responseType: 'arraybuffer' + }); + + if (response.status !== 200) { + throw new Error(`Failed to download file from DSN: ${response.status}`); + } + + // TODO: Get filename from metadata + const contentDisposition = response.headers['content-disposition']; + let filename = `${CID}.json`; + + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/); + if (filenameMatch) { + filename = filenameMatch[1]; + } + } + + // Save file + const filepath = path.join(diffsDir, filename); + await fs.promises.writeFile(filepath, response.data); + + return { + filename, + filepath + }; + + } catch (error) { + logger.error('Error retrieving file:', error); + throw new Error(`Failed to retrieve file: ${error instanceof Error ? error.message : String(error)}`); + } +}; + +export const concatenateDiffFiles = async (): Promise => { + try { + const diffsDir = path.join(process.cwd(), 'diffs'); + const summaryFile = path.join(diffsDir, 'summary-differences.json'); + + // Read all files in the diffs directory + const files = await fs.promises.readdir(diffsDir); + const jsonFiles = files.filter(file => + file.endsWith('.json') && file !== 'summary-differences.json' + ); + + // Combine all diff files + const allDiffs = []; + for (const file of jsonFiles) { + const content = await fs.promises.readFile( + path.join(diffsDir, file), + 'utf-8' + ); + const diffData = JSON.parse(content); + allDiffs.push(diffData); + } + + // Write combined data to summary file + await fs.promises.writeFile( + summaryFile, + JSON.stringify(allDiffs, null, 2) + ); + + logger.info(`Successfully created summary file with ${allDiffs.length} diffs`); + } catch (error) { + logger.error('Error concatenating diff files:', error); + throw new Error(`Failed to concatenate diff files: ${error instanceof Error ? error.message : String(error)}`); + } +}; + +export const fetchAllCIDs = async (): Promise => { + try { + const db = await open({ + filename: path.join(process.cwd(), 'thread-storage.sqlite'), + driver: sqlite3.Database + }); + + const records = await db.all( + 'SELECT upload_id, CID FROM summary_uploads ORDER BY timestamp DESC' + ); + + const uploadIds = records.map(record => record.CID); + logger.info(`Successfully fetched ${uploadIds.length} upload IDs from database`); + + return uploadIds; + + } catch (error) { + logger.error('Error fetching upload IDs from database:', error); + throw new Error(`Failed to fetch upload IDs: ${error instanceof Error ? error.message : String(error)}`); + } +}; + +export const startWithHistory = async (): Promise => { + const cids = await fetchAllCIDs(); + await Promise.all(cids.map(retrieveFile)); + await concatenateDiffFiles(); +} \ No newline at end of file diff --git a/auto-chain-agent/agents/threads.json b/auto-chain-agent/agents/threads.json deleted file mode 100644 index fe51488..0000000 --- a/auto-chain-agent/agents/threads.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/auto-chain-agent/frontend/package.json b/auto-chain-agent/frontend/package.json index 40cba88..2e8e312 100644 --- a/auto-chain-agent/frontend/package.json +++ b/auto-chain-agent/frontend/package.json @@ -14,11 +14,12 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", - "framer-motion": "11.11.11", "axios": "^1.6.7", "date-fns": "4.1.0", + "framer-motion": "^11.11.17", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.3.0", "react-markdown": "^9.0.1" }, "devDependencies": { @@ -34,4 +35,4 @@ "typescript": "^5.2.2", "vite": "^5.1.0" } -} \ No newline at end of file +} diff --git a/auto-chain-agent/frontend/src/App.tsx b/auto-chain-agent/frontend/src/App.tsx index 2f1cd2b..c6b55c5 100644 --- a/auto-chain-agent/frontend/src/App.tsx +++ b/auto-chain-agent/frontend/src/App.tsx @@ -4,6 +4,9 @@ import ChatInput from './components/ChatInput'; import MessageList from './components/MessageList'; import { Message } from './types'; import { sendMessage } from './api'; +import { styles } from './styles/App.styles'; +import logo from './assets/logo.png'; +import { TbBrain } from 'react-icons/tb'; function App() { const [messages, setMessages] = useState([]); @@ -56,28 +59,34 @@ function App() { }; return ( - - + + + + - Autonomys Network - Chain Agent + + + Autonomys Network - Chain Agent + + + + Memory Enabled ⚡ + + + - - + + + - +
- + void; @@ -28,37 +29,26 @@ function ChatInput({ onSendMessage, disabled }: ChatInputProps) { } }; - const buttonBg = useColorModeValue('blue.500', 'blue.300'); - const buttonHoverBg = useColorModeValue('blue.600', 'blue.400'); - return ( - +