Skip to content
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
82 changes: 71 additions & 11 deletions libs/remix-ai-core/src/helpers/streamHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ export const HandleStreamResponse = async (streamResponse, cb: (streamText: stri
// Check for missing body in the streamResponse
if (!reader) {
// most likely no stream response, so we can just return the result
cb(streamResponse.result)
done_cb?.("");
if (streamResponse.result) {
cb(streamResponse.result)
done_cb?.(streamResponse.result);
} else {
const errorMessage = "Error: Unable to to process your request. Try again!";
cb(errorMessage);
done_cb?.(errorMessage);
}
return;
}

Expand All @@ -44,7 +50,10 @@ export const HandleStreamResponse = async (streamResponse, cb: (streamText: stri
}
} catch (error) {
console.error('Error parsing JSON:', error);
return; // Just log the error, without unnecessary return value
const errorMessage = "Error: Unable to decode the AI response. Please try again.";
cb(errorMessage);
done_cb?.(errorMessage);
return;
}
}

Expand All @@ -68,8 +77,14 @@ export const HandleOpenAIResponse = async (aiResponse: IAIStreamResponse | any,
const toolCalls: Map<number, any> = new Map(); // Accumulate tool calls by index

if (!reader) { // normal response, not a stream
cb(streamResponse.result)
done_cb?.(streamResponse.result, streamResponse?.threadId || "");
if (streamResponse.result) {
cb(streamResponse.result)
done_cb?.(streamResponse.result, streamResponse?.threadId || "");
} else {
const errorMessage = "Error: Unable to to process your request. Try again!";
cb(errorMessage);
done_cb?.(errorMessage, streamResponse?.threadId || "");
}
return;
}

Expand All @@ -90,6 +105,12 @@ export const HandleOpenAIResponse = async (aiResponse: IAIStreamResponse | any,
done_cb?.(resultText, threadId);
return;
}

// Skip empty JSON strings
if (!jsonStr || jsonStr.length === 0) {
continue;
}

try {
const json = JSON.parse(jsonStr);
threadId = json?.thread_id;
Expand Down Expand Up @@ -158,6 +179,9 @@ export const HandleOpenAIResponse = async (aiResponse: IAIStreamResponse | any,
}
} catch (e) {
console.error("⚠️ OpenAI Stream parse error:", e);
console.error("Problematic JSON string:", jsonStr);
// Skip this chunk and continue processing the stream
continue;
}
}
}
Expand All @@ -175,8 +199,14 @@ export const HandleMistralAIResponse = async (aiResponse: IAIStreamResponse | an
let resultText = "";

if (!reader) { // normal response, not a stream
cb(streamResponse.result)
done_cb?.(streamResponse.result, streamResponse?.threadId || "");
if (streamResponse.result) {
cb(streamResponse.result)
done_cb?.(streamResponse.result, streamResponse?.threadId || "");
} else {
const errorMessage = "Error: Unable to to process your request. Try again!";
cb(errorMessage);
done_cb?.(errorMessage, streamResponse?.threadId || "");
}
return;
}

Expand All @@ -196,6 +226,11 @@ export const HandleMistralAIResponse = async (aiResponse: IAIStreamResponse | an
return;
}

// Skip empty JSON strings
if (!jsonStr || jsonStr.length === 0) {
continue;
}

try {
const json = JSON.parse(jsonStr);
threadId = json?.id || threadId;
Expand All @@ -212,6 +247,9 @@ export const HandleMistralAIResponse = async (aiResponse: IAIStreamResponse | an
}
} catch (e) {
console.error("MistralAI Stream parse error:", e);
console.error("Problematic JSON string:", jsonStr);
// Skip this chunk and continue processing the stream
continue;
}
}
}
Expand All @@ -230,8 +268,14 @@ export const HandleAnthropicResponse = async (aiResponse: IAIStreamResponse | an
let currentBlockIndex: number = -1;

if (!reader) { // normal response, not a stream
cb(streamResponse.result)
done_cb?.(streamResponse.result, streamResponse?.threadId || "");
if (streamResponse.result) {
cb(streamResponse.result)
done_cb?.(streamResponse.result, streamResponse?.threadId || "");
} else {
const errorMessage = "Error: Unable to to process your request. Try again!";
cb(errorMessage);
done_cb?.(errorMessage, streamResponse?.threadId || "");
}
return;
}

Expand All @@ -246,6 +290,12 @@ export const HandleAnthropicResponse = async (aiResponse: IAIStreamResponse | an
for (const line of lines) {
if (line.startsWith("data: ")) {
const jsonStr = line.replace(/^data: /, "").trim();

// Skip empty or invalid JSON strings
if (!jsonStr || jsonStr.length === 0) {
continue;
}

try {
const json = JSON.parse(jsonStr);

Expand Down Expand Up @@ -299,6 +349,9 @@ export const HandleAnthropicResponse = async (aiResponse: IAIStreamResponse | an
}
} catch (e) {
console.error("Anthropic Stream parse error:", e);
console.error("Problematic JSON string:", jsonStr);
// Skip this chunk and continue processing the stream
continue;
}
}
}
Expand All @@ -315,8 +368,15 @@ export const HandleOllamaResponse = async (aiResponse: IAIStreamResponse | any,
let inThinking = false;

if (!reader) { // normal response, not a stream
cb(streamResponse.result || streamResponse.response || "");
done_cb?.(streamResponse.result || streamResponse.response || "");
const result = streamResponse.result || streamResponse.response;
if (result) {
cb(result);
done_cb?.(result);
} else {
const errorMessage = "Error: Unable to to process your request. Try again!";
cb(errorMessage);
done_cb?.(errorMessage);
}
return;
}

Expand Down
3 changes: 2 additions & 1 deletion libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,8 @@ export class MCPInferencer extends RemoteInferencer implements ICompletions, IGe

// Send empty prompt - the tool results are in toolsMessages
// Don't add extra prompts as they cause Anthropic to summarize instead of using full tool results
return { streamResponse: await super.answer('', followUpOptions), callback: toolExecutionCallback } as IAIStreamResponse;
if (options.provider === 'openai' || options.provider === 'mistralai') return { streamResponse: await super.answer(prompt, followUpOptions), callback: toolExecutionCallback } as IAIStreamResponse;
else return { streamResponse: await super.answer("", followUpOptions), callback: toolExecutionCallback } as IAIStreamResponse;
}
}
}
Expand Down
18 changes: 14 additions & 4 deletions libs/remix-ai-core/src/inferencers/remote/remoteInference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ export class RemoteInferencer implements ICompletions, IGeneration {
this.event = new EventEmitter()
}

protected sanitizePromptByteSize(prompt: string, maxBytes: number = 50000): string {
protected sanitizePromptByteSize(prompt: string, provider?: string): string {
// Provider-specific max byte limits
const providerLimits: Record<string, number> = {
'mistralai': 30000,
'anthropic': 40000,
'openai': 40000
};

// Get max bytes based on provider, default to 50KB
const maxBytes = provider ? (providerLimits[provider.toLowerCase()] || 50000) : 50000;

const encoder = new TextEncoder();
const promptBytes = encoder.encode(prompt); // rough estimation, real size might be 10% more

Expand All @@ -39,7 +49,7 @@ export class RemoteInferencer implements ICompletions, IGeneration {
currentBytes = encoder.encode(trimmedPrompt).length;
}

console.warn(`[RemoteInferencer] Prompt exceeded ${maxBytes} bytes. Trimmed from ${promptBytes.length} to ${currentBytes} bytes.`);
console.warn(`[RemoteInferencer] Prompt exceeded ${maxBytes} bytes for provider '${provider || 'default'}'. Trimmed from ${promptBytes.length} to ${currentBytes} bytes.`);
return trimmedPrompt;
}

Expand All @@ -49,7 +59,7 @@ export class RemoteInferencer implements ICompletions, IGeneration {

// Sanitize prompt in payload if it exists
if (payload.prompt) {
payload.prompt = this.sanitizePromptByteSize(payload.prompt);
payload.prompt = this.sanitizePromptByteSize(payload.prompt, payload.provider);
}

try {
Expand Down Expand Up @@ -88,7 +98,7 @@ export class RemoteInferencer implements ICompletions, IGeneration {

// Sanitize prompt in payload if it exists
if (payload.prompt) {
payload.prompt = this.sanitizePromptByteSize(payload.prompt);
payload.prompt = this.sanitizePromptByteSize(payload.prompt, payload.provider);
}

try {
Expand Down
16 changes: 12 additions & 4 deletions libs/remix-ai-core/src/prompts/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ export abstract class ChatHistory{
static queueSize:number = 7 // change the queue size wrt the GPU size

public static pushHistory(prompt, result){
if (result === "") return // do not allow empty assistant message due to nested stream handles on toolcalls
const chat:ChatEntry = [prompt, result]
this.chatEntries.push(chat)
if (this.chatEntries.length > this.queueSize){this.chatEntries.shift()}
if (result === "" || !result) return // do not allow empty assistant message due to nested stream handles on toolcalls

// Check if an entry with the same prompt already exists
const existingEntryIndex = this.chatEntries.findIndex(entry => entry[0] === prompt)

if (existingEntryIndex !== -1) {
this.chatEntries[existingEntryIndex][1] = result
} else {
const chat:ChatEntry = [prompt, result]
this.chatEntries.push(chat)
if (this.chatEntries.length > this.queueSize){this.chatEntries.shift()}
}
}

public static getHistory(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,8 @@ export class DeployContractHandler extends BaseToolHandler {
const callbacks = { continueCb: (error, continueTxExecution, cancelCb) => {
continueTxExecution()
}, promptCb: () => {}, statusCb: (error) => {
console.log(error)
}, finalCb: (error, contractObject, address: string, txResult: TxResult) => {
if (error) return reject(error)
if (error) reject(error)
resolve({ contractObject, address, txResult })
} }
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => {
Expand Down Expand Up @@ -261,7 +260,7 @@ export class CallContractHandler extends BaseToolHandler {
let txReturn
try {
txReturn = await new Promise((resolve, reject) => {
const params = funcABI.type !== 'fallback' ? args.args.join(',') : ''
const params = funcABI.type !== 'fallback' ? (args.args? args.args.join(',') : ''): ''
plugin.call('blockchain', 'runOrCallContractMethod',
args.contractName,
args.abi,
Expand Down Expand Up @@ -304,7 +303,6 @@ export class CallContractHandler extends BaseToolHandler {

// TODO: Execute contract call via Remix Run Tab API
const receipt = (txReturn.txResult.receipt)
console.log('function call transaction payload:', txReturn)
const result: ContractInteractionResult = {
result: isView ? txFormat.decodeResponse(txReturn.txResult.result, funcABI) : txReturn.returnValue,
transactionHash: isView ? txReturn.txResult.transactionHash : receipt.hash,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export class TutorialsResourceProvider extends BaseResourceProvider {
}

async getResourceContent(uri: string, plugin: Plugin): Promise<IMCPResourceContent> {
console.log('Getting resource content for URI:', uri);
if (uri === 'tutorials://list') {
return this.getTutorialsList(plugin);
}
Expand All @@ -58,7 +57,6 @@ export class TutorialsResourceProvider extends BaseResourceProvider {
private async getTutorialsList(plugin: Plugin): Promise<IMCPResourceContent> {
try {
const tutorials = await axios('https://raw.githubusercontent.com/remix-project-org/remix-workshops/refs/heads/json_desc/config-properties.json')
console.log(tutorials)
return this.createJsonContent('tutorials://list', tutorials);
} catch (error) {
return this.createTextContent('tutorials://list', `Error getting tutorials: ${error.message}`);
Expand Down