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

refac: update prompts and client #38

Closed
wants to merge 3 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
50 changes: 30 additions & 20 deletions apps/client/app/components/sections/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,52 @@ interface MessagesProps {

const Messages: React.FC<MessagesProps> = ({ messages }) => {
const messagesEndRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);

const processedMessages = messages.map((message) => {
try {
const parsed = JSON.parse(message.message);
if (parsed && typeof parsed === 'object') {
return { ...message, isHTML: true };
// Only try to parse AI responses
if (message.sender === 'ai') {
const parsed = JSON.parse(message.message);
if (parsed && typeof parsed === 'object') {
// Extract the actual response content from the JSON structure
const content = Array.isArray(parsed)
? parsed[0]?.response || message.message
: parsed.response || message.message;
return { ...message, message: content, isHTML: false };
}
}
} catch (error) {
// Not a JSON string, keep as is
// Not a JSON string or parsing failed, keep as is
}
return { ...message, isHTML: false };
return message;
});

return (
<div className="h-full overflow-y-auto relative">
<div className="flex flex-col h-full overflow-y-auto px-4 py-2">
{processedMessages.map((message, index) => (
<div
key={index}
className={`relative mb-3 p-3 rounded-md w-fit md:max-w-[40%] break-words opacity-100 ${
message.sender === 'user'
? 'bg-blue-500 text-white self-end ml-auto text-right'
: 'bg-gray-300 text-black self-start mr-auto text-left'
}`}
className={`flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'} mb-4`}
>
{message.isHTML ? (
<div
dangerouslySetInnerHTML={{
__html: JSONFormatter.format(JSON.parse(message.message))
}}
/>
) : (
<div>{message.message}</div>
)}
<div
className={`relative p-4 rounded-lg max-w-[80%] md:max-w-[60%] break-words ${
message.sender === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-black'
}`}
>
{message.isHTML ? (
<div
dangerouslySetInnerHTML={{
__html: JSONFormatter.format(message.message)
}}
/>
) : (
<div className="whitespace-pre-wrap">{message.message}</div>
)}
</div>
</div>
))}
<div ref={messagesEndRef} />
Expand Down
3 changes: 3 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@
"react-icons": "^5.4.0",
"tailwindcss": "3.3.2",
"typescript": "5.1.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16"
}
}
106 changes: 106 additions & 0 deletions apps/web/src/logs/atomaHealth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import https from 'https';
import Atoma from '@atoma-agents/sui-agent/src/config/atoma';
import type { ChatResponse } from '@atoma-agents/sui-agent/src/@types/interface';

// 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 ❌');
}
};
9 changes: 9 additions & 0 deletions apps/web/src/routes/v1/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import { Router } from 'express';
import { Request, Response } from 'express';
import config from '../../config/config';
import Agent from '@atoma-agents/sui-agent/src/agents/SuiAgent';
import { checkAtomaSDK } from '../../logs/atomaHealth';

const { atomaSdkBearerAuth } = config.auth;
const suiAgent = new Agent(atomaSdkBearerAuth);
const queryRouter: Router = Router();

// Run Atoma SDK check when router is initialized
checkAtomaSDK(atomaSdkBearerAuth);

// Health check endpoint
queryRouter.get('/health', (req: Request, res: Response) => {
res.json({ status: 'healthy' });
});

// Query endpoint
const handleQuery = async (req: Request, res: Response): Promise<void> => {
try {
Expand All @@ -29,12 +36,14 @@ const handleQuery = async (req: Request, res: Response): Promise<void> => {
});
}
};

// Handle unsupported methods
const handleUnsupportedMethod = (req: Request, res: Response): void => {
res.status(405).json({
error: 'Method not allowed'
});
};

queryRouter.post('/', handleQuery);
queryRouter.all('/', handleUnsupportedMethod);

Expand Down
15 changes: 13 additions & 2 deletions packages/sui-agent/src/@types/interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Response interface for the intent agent's operations
export interface IntentAgentResponse {
success: boolean; // Indicates if the operation was successful
selected_tool: null | string; // Name of the tool that was selected for the operation
selected_tools: null | string[]; // Name of the tool that was selected for the operation
response: null | string; // Response message from the operation
needs_additional_info: boolean; // Indicates if more information is needed
additional_info_required: null | string[]; // List of additional information fields needed
Expand All @@ -13,7 +13,7 @@ export type ToolArgument = string | number | boolean | bigint;
// Response interface for tool operations (similar to IntentAgentResponse)
export interface toolResponse {
success: boolean;
selected_tool: null | string;
selected_tools: null | string;
response: null | string;
needs_additional_info: boolean;
additional_info_required: null | string[];
Expand Down Expand Up @@ -195,3 +195,14 @@ export const NETWORK_CONFIG: NetworkConfigs = {
faucet: 'https://faucet.testnet.sui.io/gas',
},
};

// Add this to your existing interfaces
export interface ChatResponse {
model: string;
choices: Array<{
message: {
role: string;
content: string;
};
}>;
}
31 changes: 22 additions & 9 deletions packages/sui-agent/src/agents/SuiAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Utils from '../utils';
import intent_agent_prompt from '../prompts/intent_agent_prompt';
import final_answer_agent_prompt from '../prompts/final_answer_agent';
import Atoma from '../config/atoma';
import decomposerPrompt from '../prompts/decomposer';

/**
* Main agent class that handles intent processing and decision making
Expand All @@ -28,18 +29,25 @@ class Agents {
registerAllTools(this.tools);
}

async QueryDecomposer(prompt: string) {
return await this.AtomaClass.atomaChat([
{ content: decomposerPrompt, role: 'assistant' },
{ content: prompt, role: 'user' },
]);
}

/**
* Processes initial user intent and selects appropriate tools
* @param prompt - User's input query
* @returns IntentAgentResponse containing tool selection and processing details
*/
async IntentAgent(prompt: string, address?: string) {
const IntentResponse: IntentAgentResponse =
async IntentAgent(subqueries: string[], address?: string) {
const IntentResponse: IntentAgentResponse[] =
(await this.tools.selectAppropriateTool(
this.AtomaClass,
prompt,
`${subqueries}`,
address,
)) as IntentAgentResponse;
)) as IntentAgentResponse[];

return IntentResponse;
}
Expand All @@ -51,15 +59,15 @@ class Agents {
* @returns Processed response after decision making
*/
async DecisionMakingAgent(
intentResponse: IntentAgentResponse,
intentResponse: IntentAgentResponse[],
query: string,
) {
// Pass both the selected tool name and arguments to processQuery

return await this.utils.processQuery(
this.AtomaClass,
query,
intentResponse.selected_tool,
intentResponse.tool_arguments,
intentResponse,
);
}

Expand All @@ -71,8 +79,13 @@ class Agents {
*/
async SuperVisorAgent(prompt: string, walletAddress?: string) {
// Process intent
const res = await this.IntentAgent(prompt, walletAddress);

const decomposer = await this.QueryDecomposer(prompt);
const decomposed: string[] = JSON.parse(
decomposer.choices[0].message.content,
);
console.log(decomposed);
const res = await this.IntentAgent(decomposed, walletAddress);
console.log(res, 'this is intent agent response');
// Make decision based on intent
const finalAnswer = await this.DecisionMakingAgent(res, prompt);
console.log('Final Answer:', finalAnswer);
Expand Down
2 changes: 2 additions & 0 deletions packages/sui-agent/src/config/atoma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ class Atoma {
}

export default Atoma;

export { AtomaSDK };
25 changes: 25 additions & 0 deletions packages/sui-agent/src/prompts/decomposer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default `You are the Query Decomposer.

Your task is to analyze the user's query and break it into multiple subqueries **only if necessary**, following strict rules.

### **Rules for Decomposition**
1. **Determine if decomposition is needed**
- If the query requires multiple tools or separate logical steps, split it into subqueries.
- If a single tool (e.g., straightforward coin price check) can handle the query, return it as a single subquery.

2. **Subquery Format (Strict JSON Array)**
- Each subquery must be **clear, self-contained, and executable**.
- Maintain **logical order** for execution.

3. **Handling Missing Details**
- If the user request references a chain or an environment but is not specified, default to Sui or the best-known environment.
- If the query requests an action requiring additional data (e.g., a coin symbol or address) and is not provided, note the additional info requirement.

### **Output Format**
DO NOT STRAY FROM THE RESPONSE FORMAT (Array or single string). RETURN ONLY THE RESPONSE FORMAT.

- If decomposition **is needed**, return an array of strings.
- If a single step suffices, return an array with a single string.

Remember: It's better to handle default or fallback conditions (like defaulting to Sui chain or a known exchange if none is provided) than to leave the query incomplete.
`;
Loading
Loading