newo
Version:
NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, r
340 lines (288 loc) • 11.9 kB
text/typescript
/**
* Sandbox Chat Utility Module
* Handles chat session management, message sending, and polling for responses
*/
import type { AxiosInstance } from 'axios';
import { randomBytes } from 'crypto';
import {
listIntegrations,
listConnectors,
createSandboxPersona,
createActor,
sendChatMessage,
getChatHistory
} from '../api.js';
import type {
SandboxChatSession,
Connector,
ConversationAct,
ChatDebugInfo
} from '../types.js';
const SANDBOX_INTEGRATION_IDN = 'sandbox';
const DEFAULT_TIMEZONE = 'America/Los_Angeles';
const POLL_INTERVAL_MS = 1000; // 1 second
const MAX_POLL_ATTEMPTS = 60; // Max 60 seconds wait
/**
* Generate a random external ID for chat session
*/
function generateExternalId(): string {
return randomBytes(3).toString('hex');
}
/**
* Generate a unique persona name with NEWO CLI prefix
*/
function generatePersonaName(): string {
const guid = randomBytes(8).toString('hex');
return `newo-cli-${guid}`;
}
/**
* Find a sandbox connector from the customer's connectors list
*/
export async function findSandboxConnector(client: AxiosInstance, verbose: boolean = false): Promise<Connector | null> {
if (verbose) console.log('🔍 Searching for sandbox integration...');
// First, get all integrations to find the sandbox integration
const integrations = await listIntegrations(client);
const sandboxIntegration = integrations.find(i => i.idn === SANDBOX_INTEGRATION_IDN);
if (!sandboxIntegration) {
if (verbose) console.log('❌ Sandbox integration not found');
return null;
}
if (verbose) console.log(`✓ Found sandbox integration: ${sandboxIntegration.id}`);
// Now get connectors for the sandbox integration
if (verbose) console.log('🔍 Searching for sandbox connectors...');
const connectors = await listConnectors(client, sandboxIntegration.id);
const sandboxConnectors = connectors.filter(c => c.status === 'running');
if (sandboxConnectors.length === 0) {
if (verbose) console.log('❌ No running sandbox connectors found');
return null;
}
if (verbose) {
console.log(`✓ Found ${sandboxConnectors.length} running sandbox connector(s)`);
const firstConnector = sandboxConnectors[0];
if (firstConnector) {
console.log(` Using: ${firstConnector.connector_idn}`);
}
}
return sandboxConnectors[0] || null;
}
/**
* Create a new sandbox chat session
*/
export async function createChatSession(
client: AxiosInstance,
connector: Connector,
verbose: boolean = false
): Promise<SandboxChatSession> {
const personaName = generatePersonaName();
const externalId = generateExternalId();
if (verbose) console.log(`📝 Creating persona: ${personaName}`);
// Create user persona
const personaResponse = await createSandboxPersona(client, {
name: personaName,
title: personaName
});
if (verbose) console.log(`✓ Persona created: ${personaResponse.id}`);
// Create actor (ties persona to sandbox connector)
if (verbose) console.log(`🔗 Creating actor for ${connector.connector_idn}...`);
const actorResponse = await createActor(client, personaResponse.id, {
name: personaName,
external_id: externalId,
integration_idn: SANDBOX_INTEGRATION_IDN,
connector_idn: connector.connector_idn,
time_zone_identifier: DEFAULT_TIMEZONE
});
if (verbose) console.log(`✓ Actor created: ${actorResponse.id} (Chat ID)`);
return {
user_persona_id: personaResponse.id,
user_actor_id: actorResponse.id,
agent_persona_id: null, // Will be populated from first response
connector_idn: connector.connector_idn,
session_id: null,
external_id: externalId
};
}
/**
* Send a message in the chat session
* Returns the timestamp when message was sent (for filtering responses)
*/
export async function sendMessage(
client: AxiosInstance,
session: SandboxChatSession,
text: string,
verbose: boolean = false
): Promise<Date> {
if (verbose) console.log(`💬 Sending message: "${text}"`);
const sentAt = new Date();
await sendChatMessage(client, session.user_actor_id, {
text,
arguments: []
});
if (verbose) console.log('✓ Message sent');
return sentAt;
}
/**
* Poll for new conversation acts (messages and debug info)
* Continues polling until we get an agent response, not just any new message
*/
export async function pollForResponse(
client: AxiosInstance,
session: SandboxChatSession,
messageSentAt: Date | null = null,
verbose: boolean = false
): Promise<{ acts: ConversationAct[]; agentPersonaId: string | null }> {
let attempts = 0;
let agentPersonaId = session.agent_persona_id;
if (verbose) console.log('⏳ Waiting for agent response...');
// Add small delay before first poll to allow message to be processed
await new Promise(resolve => setTimeout(resolve, 500));
while (attempts < MAX_POLL_ATTEMPTS) {
try {
if (verbose && attempts % 5 === 0) {
console.log(` [Poll attempt ${attempts + 1}/${MAX_POLL_ATTEMPTS}] Checking for messages...`);
}
// Use Chat History API instead of acts API (doesn't require account_id)
const response = await getChatHistory(client, {
user_actor_id: session.user_actor_id,
page: 1,
per: 100
});
if (verbose && attempts === 0) {
console.log(` Initial poll returned ${response.items.length} message(s)`);
}
if (response.items && response.items.length > 0) {
// Convert chat history format to acts format
const convertedActs: ConversationAct[] = response.items.map((item: any) => ({
id: item.id || `chat_${Math.random()}`,
command_act_id: null,
external_event_id: item.external_event_id || 'chat_history',
arguments: item.arguments || [],
reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
runtime_context_id: item.runtime_context_id || 'chat_history',
source_text: item.payload?.text || item.message || item.content || item.text || '',
original_text: item.payload?.text || item.message || item.content || item.text || '',
datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
user_actor_id: session.user_actor_id,
agent_actor_id: item.agent_actor_id || null,
user_persona_id: session.user_persona_id,
user_persona_name: 'User',
agent_persona_id: item.agent_persona_id || agentPersonaId || 'unknown',
external_id: item.external_id || null,
integration_idn: 'sandbox',
connector_idn: session.connector_idn,
to_integration_idn: null,
to_connector_idn: null,
is_agent: Boolean(item.is_agent === true),
project_idn: item.project_idn || null,
flow_idn: item.flow_idn || 'unknown',
skill_idn: item.skill_idn || 'unknown',
session_id: item.session_id || session.session_id || 'unknown',
recordings: item.recordings || [],
contact_information: item.contact_information || null
}));
// Extract agent_persona_id from the first act if we don't have it yet
if (!agentPersonaId && convertedActs.length > 0) {
const firstItem = convertedActs[0];
if (firstItem && firstItem.agent_persona_id !== 'unknown') {
agentPersonaId = firstItem.agent_persona_id;
if (verbose) console.log(`✓ Extracted agent_persona_id: ${agentPersonaId}`);
}
}
// Filter for agent messages that came AFTER our message was sent
const agentMessages = convertedActs.filter(act => {
if (!act.is_agent) return false;
// If we have a messageSentAt timestamp, ONLY include messages with datetime after it
if (messageSentAt) {
// Parse the act datetime - it may not have timezone, assume UTC
let actDatetime = act.datetime;
if (!actDatetime.endsWith('Z') && !actDatetime.includes('+') && !actDatetime.includes('-', 10)) {
actDatetime = actDatetime + 'Z'; // Assume UTC if no timezone
}
const actTime = new Date(actDatetime);
const sentTime = messageSentAt.getTime();
const actTimeMs = actTime.getTime();
const timeDiff = actTimeMs - sentTime;
if (verbose && attempts === 0) {
console.log(` Checking agent message:`);
console.log(` Original datetime: ${act.datetime}`);
console.log(` Parsed datetime: ${actDatetime}`);
console.log(` Act timestamp: ${actTimeMs} (${new Date(actTimeMs).toISOString()})`);
console.log(` Sent timestamp: ${sentTime} (${messageSentAt.toISOString()})`);
console.log(` Difference: ${timeDiff}ms (${(timeDiff/1000).toFixed(1)}s)`);
console.log(` Include: ${timeDiff > -100 ? 'YES' : 'NO'}`);
}
// Only include messages sent AFTER our message (allow small negative buffer for processing time)
return timeDiff > -100;
}
// For first message (no messageSentAt), include all agent messages
return true;
});
if (agentMessages.length > 0) {
if (verbose) console.log(`✓ Received ${agentMessages.length} agent message(s) after our message (${messageSentAt?.toISOString()})`);
// Return ONLY the single newest agent message (first one, since API returns newest first)
const latestAgentMessage = agentMessages[0];
if (latestAgentMessage) {
return { acts: [latestAgentMessage], agentPersonaId };
}
} else if (verbose && attempts % 10 === 0) {
console.log(` No new agent messages yet (checked ${response.items.length} total messages, sentAt: ${messageSentAt?.toISOString()}), continuing...`);
}
}
} catch (error: any) {
if (verbose && attempts < 3) {
console.log(`⚠️ Error polling (attempt ${attempts + 1}): ${error.message}`);
}
// Continue polling despite errors
}
attempts++;
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
}
if (verbose) console.log('⏱️ Timeout waiting for response');
return { acts: [], agentPersonaId };
}
/**
* Extract agent messages from acts
*/
export function extractAgentMessages(acts: ConversationAct[]): ConversationAct[] {
return acts.filter(act => act.is_agent && act.reference_idn === 'agent_message');
}
/**
* Extract debug information from acts
*/
export function extractDebugInfo(acts: ConversationAct[]): ChatDebugInfo[] {
return acts.map(act => ({
flow_idn: act.flow_idn,
skill_idn: act.skill_idn,
session_id: act.session_id,
runtime_context_id: act.runtime_context_id,
reference_idn: act.reference_idn,
arguments: act.arguments
}));
}
/**
* Format debug info for display
*/
export function formatDebugInfo(acts: ConversationAct[]): string {
const lines: string[] = [];
for (const act of acts) {
if (act.is_agent) {
lines.push(`\n[Agent Act] ${act.reference_idn}`);
} else {
lines.push(`\n[User Act] ${act.reference_idn}`);
}
lines.push(` Flow: ${act.flow_idn || 'N/A'}`);
lines.push(` Skill: ${act.skill_idn || 'N/A'}`);
lines.push(` Session: ${act.session_id}`);
if (act.runtime_context_id) {
lines.push(` Context: ${act.runtime_context_id}`);
}
if (act.arguments && act.arguments.length > 0) {
lines.push(` Arguments:`);
for (const arg of act.arguments) {
if (typeof arg === 'object' && arg !== null && 'name' in arg) {
lines.push(` ${arg.name}: ${JSON.stringify(arg.value).substring(0, 100)}`);
}
}
}
}
return lines.join('\n');
}