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

feat: add support for multi-tools #37

Merged
merged 10 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const suiAgent = new Agent('your_atoma_sdk_bearer_token');
// Use the agent to process queries
async function processQuery(query: string) {
try {
const result = await suiAgent.SuperVisorAgent(query);
const result = await suiAgent.processUserQueryPipeline(query);
return result;
} catch (error) {
console.error('Error processing query:', error);
Expand All @@ -72,7 +72,7 @@ const suiAgent = new Agent(config.atomaSdkBearerAuth);
app.post('/query', async (req, res) => {
try {
const { query } = req.body;
const result = await suiAgent.SuperVisorAgent(query);
const result = await suiAgent.processUserQueryPipeline(query);
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
Expand Down
64 changes: 57 additions & 7 deletions apps/client/app/components/ui/pulseLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,59 @@
const PulseLoader = () => (
<div className="flex space-x-2">
<div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse"></div>
<div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse [animation-delay:0.2s]"></div>
<div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse [animation-delay:0.4s]"></div>
</div>
);
import React, { useEffect, useState } from 'react';

const PulseLoader = () => {
const [dots, setDots] = useState('');

useEffect(() => {
const interval = setInterval(() => {
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
}, 500);

return () => clearInterval(interval);
}, []);

return (
<div className="flex flex-col items-center space-y-4 p-4">
<div className="flex items-center space-x-3">
<div className="relative w-10 h-10">
{/* Circular pulse animation */}
<div className="absolute inset-0 bg-blue-500/20 rounded-full animate-ping" />
<div className="relative w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
</div>
<div className="flex flex-col">
<span className="text-gray-700 font-medium">Atomasage</span>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded-full" />
<span className="text-gray-500 text-sm">thinking{dots}</span>
</div>
</div>
</div>

{/* Typing indicator */}
<div className="flex space-x-1">
{[0, 1, 2].map((i) => (
<div
key={i}
className={`w-2 h-2 bg-blue-500 rounded-full animate-bounce`}
style={{ animationDelay: `${i * 200}ms` }}
/>
))}
</div>
</div>
);
};

export default PulseLoader;
141 changes: 123 additions & 18 deletions apps/client/app/utils/JSONFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,30 @@ class JSONFormatter {
* @returns A string containing the formatted HTML.
*/
public static format(json: any): string {
return this.formatJSON(json, 0);
return `
<div class="json-formatter" style="
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
">
${this.formatJSON(json, 0)}
</div>
`;
}

/**
* Transforms camelCase, snake_case, and PascalCase text to space-separated words
* @param text - The text to transform
* @returns The transformed text
*/
private static transformKey(text: string): string {
return text
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.trim()
.toLowerCase()
.replace(/^\w/, (c) => c.toUpperCase());
}

/**
Expand All @@ -31,20 +54,42 @@ class JSONFormatter {
* @returns A string containing the formatted HTML.
*/
private static formatArray(array: any[], indentLevel: number): string {
let html = '';
if (array.length === 0) {
return '<div style="color: #666;">(empty array)</div>';
}

let html = '<div class="json-array" style="width: 100%;">';
array.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
// Add a styled section for arrays of objects
html += `<div style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #f9f9f9;">
${this.formatJSON(item, indentLevel + 1)}
</div>`;
html += `
<div class="json-object-container" style="
margin: 12px 0;
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
">
${this.formatJSON(item, indentLevel + 1)}
</div>
`;
} else {
// Regular array items (non-objects)
html += `<div style="margin-left: ${
indentLevel * 20
}px;">- ${this.formatJSON(item, indentLevel + 1)}</div>`;
html += `
<div class="json-array-item" style="
padding: 4px 0;
margin-left: ${indentLevel * 24}px;
display: flex;
align-items: flex-start;
">
<span style="margin-right: 8px;">•</span>
<div style="flex: 1;">${this.formatJSON(item, indentLevel + 1)}</div>
</div>
`;
}
});
html += '</div>';
return html;
}

Expand All @@ -55,13 +100,64 @@ class JSONFormatter {
* @returns A string containing the formatted HTML.
*/
private static formatObject(obj: { [key: string]: any }, indentLevel: number): string {
let html = '';
if (Object.keys(obj).length === 0) {
return '<div style="color: #666;">(empty object)</div>';
}

let html = '<div class="json-object" style="width: 100%;">';
const keys = Object.keys(obj);
keys.forEach((key, index) => {
html += `<div style="margin-left: ${indentLevel * 20}px;">
<strong>${key}:</strong> ${this.formatJSON(obj[key], indentLevel + 1)}
</div>`;
const value = obj[key];
const isNested = typeof value === 'object' && value !== null;

if (isNested) {
// Nested objects/arrays get their own block
html += `
<div class="json-property nested" style="
padding: 8px 0;
margin-left: ${indentLevel * 24}px;
">
<div style="
font-weight: bold;
margin-bottom: 8px;
">${this.transformKey(key)}:</div>
<div style="
padding-left: 24px;
">
${this.formatJSON(value, indentLevel + 1)}
</div>
</div>
`;
} else {
// Primitive values stay inline
html += `
<div class="json-property" style="
padding: 8px 0;
margin-left: ${indentLevel * 24}px;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
">
<strong style="
min-width: 120px;
max-width: 100%;
margin-right: 12px;
margin-bottom: 4px;
">${this.transformKey(key)}:</strong>
<div style="
flex: 1;
min-width: 200px;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
">
${this.formatJSON(value, indentLevel + 1)}
</div>
</div>
`;
}
});
html += '</div>';
return html;
}

Expand All @@ -72,15 +168,24 @@ class JSONFormatter {
* @returns A string containing the formatted HTML.
*/
private static formatPrimitive(value: any, indentLevel: number): string {
const style = `
color: #444;
font-family: monospace;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
`;

if (typeof value === 'string') {
return `<span>"${value}"</span>`; // Wrap strings in quotes for clarity
return `<span style="${style}">"${value}"</span>`;
} else if (value === null) {
return '<span>null</span>'; // Handle null values
return `<span style="${style}">null</span>`;
} else if (typeof value === 'undefined') {
return '<span>undefined</span>'; // Handle undefined values
return `<span style="${style}">undefined</span>`;
} else {
return `<span>${value.toString()}</span>`; // Numbers, booleans, etc.
return `<span style="${style}">${value.toString()}</span>`;
}
}
}

export default JSONFormatter;
2 changes: 1 addition & 1 deletion apps/web/src/controllers/conversation.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ConversationController {
res.status(400).json({ error: 'Invalid conversation ID' });
return;
}
const result = await suiAgent.SuperVisorAgent(message, walletAddress);
const result = await suiAgent.processUserQueryPipeline(message, walletAddress);
const newMessage = await this.messageService.createMessage({
sender: sender ? sender : 'user',
walletAddress,
Expand Down
115 changes: 115 additions & 0 deletions apps/web/src/logs/atomaHealth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import https from 'https';
import Atoma from '@atoma-agents/sui-agent/src/config/atoma';

interface ChatResponse {
model: string;
choices: Array<{
message: {
role: string;
content: string;
};
}>;
}

// Function to check API availability
const checkApiAvailability = (): Promise<boolean> => {
return new Promise((resolve) => {
const req = https.get('https://api.atoma.network/health', (res) => {
resolve(res.statusCode === 200);
});

req.on('error', () => {
resolve(false);
});

req.end();
});
};

// Function to create a timeout promise
const timeoutPromise = (ms: number): Promise<never> =>
new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), ms));

interface AtomaError {
statusCode: number;
body: string;
contentType: string;
rawResponse: unknown;
}

// Atoma SDK health check
export const checkAtomaSDK = async (bearerAuth: string): Promise<void> => {
const atomaSDK = new Atoma(bearerAuth);

try {
console.log('\n=== Atoma SDK Diagnostic Check ===');
console.log(`Bearer Token length: ${bearerAuth.length} characters`);
console.log(
`Bearer Token: ${bearerAuth.substring(0, 4)}...${bearerAuth.substring(bearerAuth.length - 4)}`
);

const apiAvailable = await checkApiAvailability();
console.log(`API Health Check: ${apiAvailable ? 'OK' : 'Failed'}`);

if (!apiAvailable) {
throw new Error('API endpoint is not available');
}

console.log('\nAttempting to connect to Atoma API...');

const result = (await Promise.race([
atomaSDK.atomaChat([
{
role: 'user',
content: 'Hi, are you there?'
}
]),
timeoutPromise(30000)
])) as ChatResponse;

console.log('=== Chat Completion Response ===');
console.log(`Timestamp: ${new Date().toISOString()}`);
console.log(`Model: ${result.model}`);
console.log('\nResponse Content:');
result.choices.forEach((choice, index: number) => {
console.log(`Choice ${index + 1}:`);
console.log(` Role: ${choice.message.role}`);
console.log(` Content: ${choice.message.content}`);
});
console.log('\nAtoma SDK Check Complete ✅');
} catch (error) {
console.error('\n=== Atoma SDK Check Error ===');
if (error && typeof error === 'object' && 'rawResponse' in error) {
const atomaError = error as AtomaError;
console.error(`Status Code: ${atomaError.statusCode}`);
console.error(`Response Body: ${atomaError.body}`);
console.error(`Content Type: ${atomaError.contentType}`);

switch (atomaError.statusCode) {
case 402:
console.error('\nBalance Error:');
console.error('Your account has insufficient balance to make this request.');
console.error('\nSuggested actions:');
console.error('1. Check your account balance at https://atoma.network');
console.error('2. Add credits to your account');
console.error('3. Consider using a different model with lower cost');
console.error('4. Contact support if you believe this is an error');
break;

case 500:
console.error('\nPossible issues:');
console.error('1. Invalid or expired bearer token');
console.error('2. Server-side issue with the model');
console.error('3. Rate limiting or quota exceeded');
console.error('\nSuggested actions:');
console.error('- Verify your bearer token is valid');
console.error('- Try a different model');
console.error('- Check your API usage quota');
console.error('- Contact support if the issue persists');
break;
}
}
console.error('\nFull Error Stack:', error);
console.error('\nAtoma SDK Check Failed ❌');
}
};
Loading