Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to upload/download character from DSN #118

Closed
wants to merge 7 commits into from
Closed
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
13 changes: 13 additions & 0 deletions auto-agents-framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ Note: When specifying a character file, omit both `.ts` and `.js` extensions. Th
};
```


## Upload Characters to DSN

```bash
yarn manage-character upload <character-name>
```

## Download Characters from DSN

```bash
yarn manage-character download <cid> <character-name>
```

## Autonomys Network Integration

The framework uses the Autonomys Network for permanent storage of agent memory and interactions. This enables:
Expand Down
1 change: 1 addition & 0 deletions auto-agents-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"example:twitter": "tsx examples/twitter.ts",
"extract-kol-dsn-schemas": "tsx src/agents/workflows/kol/cli/extractDsnSchemas.ts",
"copy-characters": "cp -r src/agents/workflows/kol/characters dist/agents/workflows/kol/",
"manage-character": "tsx src/agents/workflows/kol/cli/characterManager.ts"
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
Expand Down
43 changes: 43 additions & 0 deletions auto-agents-framework/src/agents/tools/utils/dsnDownload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createAutoDriveApi, downloadFile } from '@autonomys/auto-drive';
import { config } from '../../../config/index.js';
import { createLogger } from '../../../utils/logger.js';
import { withRetry } from './retry.js';

const logger = createLogger('dsn-download');

export async function download(cid: string): Promise<any> {
return withRetry(
async () => {
const api = createAutoDriveApi({
apiKey: config.autoDriveConfig.AUTO_DRIVE_API_KEY || '',
});
logger.info(`Downloading file: ${cid}`);
const stream = await downloadFile(api, cid);

const chunks: Uint8Array[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
}

const allChunks = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}

const jsonString = new TextDecoder().decode(allChunks);
const data = JSON.parse(jsonString);

return data;
},
{
shouldRetry: error => {
const errorMessage = error instanceof Error ? error.message : String(error);
return !(
errorMessage.includes('Not Found') || errorMessage.includes('incorrect header check')
);
},
},
);
}
80 changes: 41 additions & 39 deletions auto-agents-framework/src/agents/tools/utils/dsnUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,13 @@ import { stringToCid, blake3HashFromCid } from '@autonomys/auto-dag-data';
import { config, agentVersion } from '../../../config/index.js';
import { wallet, signMessage } from './agentWallet.js';
import { setLastMemoryHash, getLastMemoryCid } from './agentMemoryContract.js';
import { withRetry } from './retry.js';
import { saveHashLocally } from './localHashStorage.js';

const logger = createLogger('dsn-upload-tool');
const dsnApi = createAutoDriveApi({ apiKey: config.autoDriveConfig.AUTO_DRIVE_API_KEY! });
let currentNonce = await wallet.getNonce();

// New retry utility function
const withRetry = async <T>(
operation: () => Promise<T>,
{
maxRetries = 5,
initialDelayMs = 1000,
operationName = 'Operation',
}: {
maxRetries?: number;
initialDelayMs?: number;
operationName?: string;
} = {},
): Promise<T> => {
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

const attempt = async (retriesLeft: number, currentDelay: number): Promise<T> => {
try {
return await operation();
} catch (error) {
if (retriesLeft <= 0) {
logger.error(`${operationName} failed after all retry attempts`, { error });
throw error;
}

logger.warn(`${operationName} failed, retrying... (${retriesLeft} attempts left)`, {
error,
nextDelayMs: currentDelay,
});
await delay(currentDelay);
// Exponential backoff with jitter
const jitter = Math.random() * 0.3 + 0.85; // Random value between 0.85 and 1.15
const nextDelay = Math.min(currentDelay * 2 * jitter, 30000); // Cap at 30 seconds
return attempt(retriesLeft - 1, nextDelay);
}
};

return attempt(maxRetries, initialDelayMs);
};
let currentNonce = await wallet.getNonce();

// Helper function for file upload
const uploadFileToDsn = async (file: any, options: any) =>
Expand Down Expand Up @@ -121,3 +84,42 @@ export async function uploadToDsn(data: object) {
throw error;
}
}

export async function uploadCharacterToDsn(characterName: string, data: object) {
logger.info('Upload Character to Dsn - Starting upload');

try {
const timestamp = new Date().toISOString();
const jsonBuffer = Buffer.from(JSON.stringify(data, null, 2));
const file = {
read: async function* () {
yield jsonBuffer;
},
name: `character-${characterName}-${timestamp}.json`,
mimeType: 'application/json',
size: jsonBuffer.length,
};

const uploadedCid = await withRetry(
() =>
uploadFileToDsn(file, {
compression: true,
password: config.autoDriveConfig.AUTO_DRIVE_ENCRYPTION_PASSWORD || undefined,
}),
{ operationName: `Upload character ${characterName}` },
);

logger.info('Character uploaded successfully', {
cid: uploadedCid,
name: characterName,
});

return {
success: true,
cid: uploadedCid,
};
} catch (error) {
logger.error('Error uploading character to Dsn:', error);
throw error;
}
}
36 changes: 36 additions & 0 deletions auto-agents-framework/src/agents/tools/utils/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createLogger } from '../../../utils/logger.js';

const logger = createLogger('retry-utility');

export async function withRetry<T>(
operation: () => Promise<T>,
{
maxRetries = 5,
initialDelayMs = 1000,
operationName = 'Operation',
shouldRetry = (_error: unknown): boolean => true,
} = {},
): Promise<T> {
const attempt = async (retriesLeft: number, currentDelay: number): Promise<T> => {
try {
return await operation();
} catch (error) {
if (!shouldRetry(error) || retriesLeft <= 0) {
logger.error(`${operationName} failed after all retry attempts`, { error });
throw error;
}

logger.warn(`${operationName} failed, retrying... (${retriesLeft} attempts left)`, {
error,
nextDelayMs: currentDelay,
});

await new Promise(resolve => setTimeout(resolve, currentDelay));
const jitter = Math.random() * 0.3 + 0.85; // Random value between 0.85 and 1.15
const nextDelay = Math.min(currentDelay * 2 * jitter, 30000); // Cap at 30 seconds
return attempt(retriesLeft - 1, nextDelay);
}
};

return attempt(maxRetries, initialDelayMs);
}
117 changes: 117 additions & 0 deletions auto-agents-framework/src/agents/workflows/kol/cli/characterManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { createLogger } from '../../../../utils/logger.js';
import { uploadCharacterToDsn } from '../../../tools/utils/dsnUpload.js';
import { download as downloadCharacter } from '../../../tools/utils/dsnDownload.js';

const logger = createLogger('character-manager');

const CHARACTERS_DIR = join(process.cwd(), 'src/agents/workflows/kol/characters');

async function uploadCharacter(name: string) {
try {
const fullPath = join(CHARACTERS_DIR, `${name}.ts`);
if (!existsSync(fullPath)) {
throw new Error(`Character file not found: ${fullPath}`);
}

const fileContent = readFileSync(fullPath, 'utf-8');
const { character } = await import(`file://${fullPath}`);

const result = await uploadCharacterToDsn(name, {
content: fileContent,
metadata: character,
});

logger.info('Character uploaded successfully', {
name,
cid: result.cid,
});

return result.cid;
} catch (error) {
logger.error('Error uploading character:', error);
throw error;
}
}

async function download(cid: string, characterName: string) {
try {
if (!existsSync(CHARACTERS_DIR)) {
mkdirSync(CHARACTERS_DIR, { recursive: true });
}

const characterData = await downloadCharacter(cid);
const metadata = characterData.metadata;

const formatValue = (value: string) => {
if (typeof value !== 'string') return JSON.stringify(value);
return `\`\n${value.trim()}\``;
};

const constDeclarations = Object.entries(metadata)
.map(([key, value]) => `const ${key} = ${formatValue(value as string)};`)
.join('\n\n');

const fileContent = `// Generated from DSN CID: ${cid}
${constDeclarations}

export const character = {
${Object.keys(metadata)
.map(key => ` ${key},`)
.join('\n')}
};
`;

const tsPath = join(CHARACTERS_DIR, `${characterName}.ts`);
const jsPath = join(CHARACTERS_DIR, `${characterName}.js`);

writeFileSync(tsPath, fileContent, 'utf-8');
writeFileSync(jsPath, fileContent, 'utf-8');

logger.info('Character downloaded successfully', {
name: characterName,
cid,
paths: [tsPath, jsPath],
});

return { tsPath, jsPath };
} catch (error) {
logger.error('Error downloading character:', error);
throw error;
}
}

async function main() {
const command = process.argv[2];
const arg = process.argv[3];

if (!command || !arg) {
console.error(
'Usage: \n yarn character upload <name>\n yarn character download <cid> <name>',
);
process.exit(1);
}

try {
switch (command) {
case 'upload':
await uploadCharacter(arg);
break;
case 'download':
const name = process.argv[4];
if (!name) {
throw new Error('Download requires both CID and character name');
}
await download(arg, name);
break;
default:
throw new Error(`Unknown command: ${command}`);
}
} catch (error) {
logger.error('Command failed:', error);
process.exit(1);
}
}

main();
Loading