automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
301 lines (300 loc) ⢠13 kB
JavaScript
import { request as gaxiosRequest } from 'gaxios';
import { appConfig } from './settings.js';
// Events to completely ignore for performance
const IGNORE_EVENTS = new Set([
'TeamToolCallStarted',
'ToolCallStarted',
'MemoryUpdateStarted',
'RAGQueryStarted',
'ThinkingStarted',
]);
export class StreamingAPIClient {
baseUrl;
constructor() {
this.baseUrl = appConfig.apiBaseUrl;
}
getDefaultHeaders() {
const headers = {
'accept': 'application/json',
};
if (appConfig.apiKey) {
headers['x-api-key'] = appConfig.apiKey;
}
return headers;
}
createRawCollector() {
let rawBuffer = '';
const stats = {
startTime: Date.now(),
agentCalls: 0,
toolCalls: 0,
memoryUpdates: 0,
ragQueries: 0,
toolMetrics: [],
eventCounts: {}
};
function getAllEvents(buffer) {
const allEvents = [];
let start = 0;
let braceCount = 0;
for (let i = 0; i < buffer.length; i++) {
if (buffer[i] === '{') {
if (braceCount === 0)
start = i;
braceCount++;
}
else if (buffer[i] === '}') {
braceCount--;
if (braceCount === 0) {
try {
allEvents.push(JSON.parse(buffer.substring(start, i + 1)));
}
catch (e) { }
}
}
}
return allEvents;
}
return {
stats,
collectChunk(chunk) {
rawBuffer += chunk;
},
parseAll() {
// Post-process stats at the end
const allEvents = getAllEvents(rawBuffer);
stats.agentCalls = allEvents.filter((e) => e.event === 'RunStarted' || e.event === 'TeamRunStarted').length;
stats.toolCalls = allEvents.filter((e) => e.event === 'ToolCallCompleted' || e.event === 'TeamToolCallCompleted').length;
stats.memoryUpdates = allEvents.filter((e) => e.event === 'MemoryUpdateCompleted').length;
stats.ragQueries = allEvents.filter((e) => e.event === 'RAGQueryCompleted').length;
// Return only content events
return allEvents.filter((e) => e.event === 'TeamRunResponseContent' || e.event === 'RunResponseContent');
},
getAllEvents
};
}
async streamAgent(request, onMessage, onError, onComplete, abortSignal) {
try {
const formData = new FormData();
formData.append('message', request.message);
formData.append('stream', 'true');
formData.append('monitor', 'true');
if (request.session_id)
formData.append('session_id', request.session_id);
if (request.user_id)
formData.append('user_id', request.user_id);
const url = `${this.baseUrl}/playground/agents/${request.agent_id}/runs`;
const response = await gaxiosRequest({
url,
method: 'POST',
data: formData,
headers: this.getDefaultHeaders(),
responseType: 'stream',
timeout: 0,
signal: abortSignal,
});
const collector = this.createRawCollector();
const { stats } = collector;
let sessionId = request.session_id;
// Handle streaming data - just collect raw data and extract content
response.data.on('data', (chunk) => {
collector.collectChunk(chunk.toString());
// Fast content extraction
const chunkStr = chunk.toString();
// Simple search for content events - look for both RunResponseContent and TeamRunResponseContent
if (chunkStr.includes('ResponseContent')) {
const lines = chunkStr.split('\n');
for (const line of lines) {
if ((line.includes('RunResponseContent') || line.includes('TeamRunResponseContent')) && line.includes('"content_type":"str"')) {
try {
const event = JSON.parse(line);
// Extract from both thinking and content fields
const textContent = event.thinking || event.content;
if (textContent) {
onMessage({
content: textContent,
done: false,
session_id: sessionId,
metadata: { type: 'content' }
});
}
}
catch (e) {
// Try regex fallback for both thinking and content
const thinkingMatch = line.match(/"thinking":"([^"]*)"/);
const contentMatch = line.match(/"content":"([^"]*)"/);
const extractedText = thinkingMatch?.[1] || contentMatch?.[1];
if (extractedText) {
onMessage({
content: extractedText,
done: false,
session_id: sessionId,
metadata: { type: 'content' }
});
}
}
}
}
}
// Check for completion and post-process stats
if (chunkStr.includes('"event":"TeamRunCompleted"') || chunkStr.includes('"event":"RunCompleted"')) {
stats.endTime = Date.now();
stats.totalDuration = stats.endTime - stats.startTime;
collector.parseAll(); // This calculates stats
onComplete(stats);
}
});
response.data.on('end', () => {
onComplete(stats);
});
response.data.on('error', (error) => {
if (error.message?.includes('aborted')) {
onComplete(stats);
}
else {
onError(error);
}
});
}
catch (error) {
onError(error instanceof Error ? error : new Error('Unknown streaming error'));
}
}
async streamTeam(request, onMessage, onError, onComplete, abortSignal) {
// Similar implementation to streamAgent
try {
const formData = new FormData();
formData.append('message', request.message);
formData.append('stream', 'true');
formData.append('monitor', 'true');
if (request.session_id)
formData.append('session_id', request.session_id);
if (request.user_id)
formData.append('user_id', request.user_id);
const url = `${this.baseUrl}/playground/teams/${request.team_id}/runs`;
const response = await gaxiosRequest({
url,
method: 'POST',
data: formData,
headers: this.getDefaultHeaders(),
responseType: 'stream',
timeout: 0,
signal: abortSignal,
});
const collector = this.createRawCollector();
const { stats } = collector;
let sessionId = request.session_id;
// Handle streaming data - just collect raw data and extract content
response.data.on('data', (chunk) => {
collector.collectChunk(chunk.toString());
// Fast content extraction
const chunkStr = chunk.toString();
// Simple search for content events - look for both RunResponseContent and TeamRunResponseContent
if (chunkStr.includes('ResponseContent')) {
const lines = chunkStr.split('\n');
for (const line of lines) {
if ((line.includes('RunResponseContent') || line.includes('TeamRunResponseContent')) && line.includes('"content_type":"str"')) {
try {
const event = JSON.parse(line);
// Extract from both thinking and content fields
const textContent = event.thinking || event.content;
if (textContent) {
onMessage({
content: textContent,
done: false,
session_id: sessionId,
metadata: { type: 'content' }
});
}
}
catch (e) {
// Try regex fallback for both thinking and content
const thinkingMatch = line.match(/"thinking":"([^"]*)"/);
const contentMatch = line.match(/"content":"([^"]*)"/);
const extractedText = thinkingMatch?.[1] || contentMatch?.[1];
if (extractedText) {
onMessage({
content: extractedText,
done: false,
session_id: sessionId,
metadata: { type: 'content' }
});
}
}
}
}
}
// Check for completion and post-process stats
if (chunkStr.includes('"event":"TeamRunCompleted"') || chunkStr.includes('"event":"RunCompleted"')) {
stats.endTime = Date.now();
stats.totalDuration = stats.endTime - stats.startTime;
collector.parseAll(); // This calculates stats
onComplete(stats);
}
});
response.data.on('end', () => {
onComplete(stats);
});
response.data.on('error', (error) => {
if (error.message?.includes('aborted')) {
onComplete(stats);
}
else {
onError(error);
}
});
}
catch (error) {
onError(error instanceof Error ? error : new Error('Unknown streaming error'));
}
}
// Reuse methods from localClient for non-streaming operations
async listAgents() {
const url = `${this.baseUrl}/playground/agents`;
const response = await gaxiosRequest({
url,
method: 'GET',
headers: this.getDefaultHeaders(),
timeout: 5000,
});
return { data: response.data };
}
async listTeams() {
const url = `${this.baseUrl}/playground/teams`;
const response = await gaxiosRequest({
url,
method: 'GET',
headers: this.getDefaultHeaders(),
timeout: 5000,
});
return { data: response.data };
}
async listWorkflows() {
const url = `${this.baseUrl}/playground/workflows`;
const response = await gaxiosRequest({
url,
method: 'GET',
headers: this.getDefaultHeaders(),
timeout: 5000,
});
return { data: response.data };
}
async healthCheck() {
const url = `${this.baseUrl}/api/v1/health`;
const response = await gaxiosRequest({
url,
method: 'GET',
headers: this.getDefaultHeaders(),
timeout: 2000,
});
return { data: response.data };
}
}
export const streamingAPIClient = new StreamingAPIClient();
// Enhanced stats formatter with all metrics
export function formatRunStats(stats) {
if (!stats)
return '';
const duration = stats.totalDuration ? (stats.totalDuration / 1000).toFixed(2) + 's' : 'N/A';
return `\nš ${duration} | š¤${stats.agentCalls} | š§${stats.toolCalls} | š§ ${stats.memoryUpdates} | š${stats.ragQueries}`;
}