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
2 changes: 1 addition & 1 deletion extensions/chrome/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"preview": "vite preview"
},
"dependencies": {
"@browserai/browserai": "^1.0.29",
"@browserai/browserai": "^1.0.30",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-aspect-ratio": "^1.1.1",
Expand Down
2 changes: 1 addition & 1 deletion extensions/chrome/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "BrowserAgent - AI Agents in Browser",
"version": "1.0.3",
"version": "1.0.5",
"description": "Run private, cost-free AI agents directly in your browser. Automate tasks, enhance browsing, and control your data with local AI.",
"action": {
"default_title": "Browser AI",
Expand Down
172 changes: 152 additions & 20 deletions extensions/chrome/src/helpers/executors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,26 @@ const nodeExecutors = {
}
}

// Get the system prompt
let systemPrompt = node.nodeData?.systemPrompt || '';
let jsonSchemaStr = '';
// If we have a JSON schema from an OutputFormat node, add it to the system prompt
if (node.nodeData?.jsonSchema) {
console.debug("Using JSON schema in chatAgent:", node.nodeData.jsonSchema);
jsonSchemaStr = typeof node.nodeData.jsonSchema === 'string'
? node.nodeData.jsonSchema
: JSON.stringify(node.nodeData.jsonSchema, null, 2);

// Add the JSON schema to the system prompt
systemPrompt += `\n\nYou MUST format your response according to this JSON schema:\n${jsonSchemaStr}`;
}

// Use node's prompt if available, otherwise use the processed input
const finalPrompt = node.nodeData?.prompt || promptInput;

// Estimate token usage for system prompt and JSON schema
const systemPrompt = node.nodeData?.systemPrompt || '';
const jsonSchema = node.nodeData?.outputFormat ? JSON.stringify(node.nodeData.outputFormat) : '';

// Rough estimation: ~4 chars per token
const systemPromptTokens = Math.ceil(systemPrompt.length / 4);
const jsonSchemaTokens = Math.ceil(jsonSchema.length / 4);
const jsonSchemaTokens = Math.ceil(jsonSchemaStr.length / 4);
const reservedTokens = systemPromptTokens + jsonSchemaTokens;

const overheadTokens = 100; // For model instructions and formatting
Expand Down Expand Up @@ -223,8 +233,8 @@ const nodeExecutors = {
{
temperature: node.nodeData?.temperature || 0.7,
max_tokens: node.nodeData?.maxTokens || 2048,
system_prompt: node.nodeData?.systemPrompt,
json_schema: node.nodeData?.outputFormat,
system_prompt: systemPrompt,
json_schema: jsonSchemaStr,
stream: true
}
);
Expand Down Expand Up @@ -592,6 +602,60 @@ const nodeExecutors = {
}
},

'outputFormat': async (node: WorkflowStep, input: any, params?: ExecuteWorkflowParams) => {
try {
// Get the value from node data or use input if available
const value = node.data?.value || input || '';

// Only process JSON format
try {
// Test if valid JSON
const parsedJson = JSON.parse(typeof value === 'string' ? value : JSON.stringify(value));

// Update node with formatted output
params?.setNodes?.((prev: WorkflowStep[]) =>
prev.map(n =>
n.id === node.id ? {
...n,
data: { ...n.data, output: value },
logs: [...(n.logs || []), 'JSON schema validated successfully']
} : n
)
);

return {
success: true,
output: value,
log: 'JSON schema validated successfully',
// Store the parsed JSON schema for use by chatAgent
jsonSchema: parsedJson
};
} catch (e) {
console.error('Error in output format node:', e);

// Update node with error state
params?.setNodes?.((prev: WorkflowStep[]) =>
prev.map(n =>
n.id === node.id ? {
...n,
data: { ...n.data, hasError: true },
logs: [...(n.logs || []), 'Invalid JSON format']
} : n
)
);

return {
success: false,
output: value, // Still pass through the value
log: 'Invalid JSON format'
};
}
} catch (error) {
console.error('Error in output format node:', error);
throw error;
}
},

'iterator': async (node: WorkflowStep, input: any) => {
try {
// Combine input with items if present
Expand Down Expand Up @@ -625,6 +689,60 @@ export const executeWorkflow = async ({
setNodes,
}: ExecuteWorkflowParams): Promise<WorkflowResult> => {
try {
// First, preprocess the nodes to merge OutputFormat nodes with subsequent ChatAgent nodes
let processedNodes = [...nodes];
let outputFormatSchemas: Record<string, any> = {};

// Find all OutputFormat nodes and store their schemas
for (let i = 0; i < processedNodes.length; i++) {
const node = processedNodes[i];
if (node.nodeType === 'outputFormat') {
try {
// Parse the schema from the node
const schemaValue = node.data?.value || node.nodeData?.value;
if (schemaValue) {
const parsedSchema = typeof schemaValue === 'string'
? JSON.parse(schemaValue)
: schemaValue;

// Store the schema with the node ID
outputFormatSchemas[node.id] = parsedSchema;
console.debug(`Stored schema from OutputFormat node ${node.id}:`, parsedSchema);
}
} catch (error) {
console.error(`Failed to parse schema from OutputFormat node ${node.id}:`, error);
}
}
}

// Find the next ChatAgent node after each OutputFormat node
for (let i = 0; i < processedNodes.length; i++) {
if (processedNodes[i].nodeType === 'outputFormat') {
// Look for the next ChatAgent node
for (let j = i + 1; j < processedNodes.length; j++) {
if (processedNodes[j].nodeType === 'chatAgent') {
// Merge the schema into the ChatAgent node
const schema = outputFormatSchemas[processedNodes[i].id];
if (schema) {
processedNodes[j] = {
...processedNodes[j],
nodeData: {
...processedNodes[j].nodeData,
jsonSchema: schema
}
};
console.debug(`Merged schema into ChatAgent node ${processedNodes[j].id}`);
}
break; // Only merge with the first ChatAgent node found
}
}
}
}

// Filter out OutputFormat nodes for execution
const executableNodes = processedNodes.filter(node => node.nodeType !== 'outputFormat');
console.debug(`Removed ${processedNodes.length - executableNodes.length} OutputFormat nodes for execution`);

// Reset all nodes to pending with theme-aware styling
setNodes((prev: WorkflowStep[]) =>
prev.map(node => ({
Expand All @@ -642,7 +760,7 @@ export const executeWorkflow = async ({
let workflowData: Record<string, any> = {};

// Get the first node's input if it exists
const firstNode = nodes[0];
const firstNode = executableNodes[0];
if (firstNode?.nodeType?.toLowerCase().includes('input')) {
const inputValue = firstNode.data?.value;
console.debug('First Node Input Value:', inputValue);
Expand All @@ -661,11 +779,26 @@ export const executeWorkflow = async ({
}

let lastOutput = null;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
for (let i = 0; i < executableNodes.length; i++) {
const node = executableNodes[i];
console.debug(`\n--- Executing node: ${node.name} ---`);
// console.debug("Node parameters:", node.nodeData);
// console.debug("Current workflowData:", workflowData);
let jsonSchemaStr = '';

if (node.nodeType === 'outputFormat') {
console.debug("node.nodeData?", node.nodeData);
jsonSchemaStr = node.nodeData?.jsonSchema;

// Find the next chatAgent node
for (let j = i + 1; j < executableNodes.length; j++) {
if (executableNodes[j].nodeType === 'chatAgent') {
executableNodes[j] = { ...executableNodes[j], nodeData: { ...executableNodes[j].nodeData, jsonSchema: jsonSchemaStr } };
}
}
continue;
}
// Find the original node in the full nodes array
const originalNodeIndex = nodes.findIndex(n => n.id === node.id);
if (originalNodeIndex === -1) continue;

// Update current node to running
setNodes((prev: WorkflowStep[]) =>
Expand All @@ -674,7 +807,7 @@ export const executeWorkflow = async ({
? {
...n,
status: 'running',
logs: [...n.logs, `Starting ${n.name}...`]
logs: [...(n.logs || []), `Starting ${n.name}...`]
}
: n
)
Expand All @@ -685,7 +818,7 @@ export const executeWorkflow = async ({
}

try {
console.log("node", node)
console.debug("node", node);
const executor = nodeExecutors[node.nodeType as keyof typeof nodeExecutors];
if (!executor) {
throw new Error(`No executor found for node type: ${node.nodeType}`);
Expand All @@ -703,15 +836,14 @@ export const executeWorkflow = async ({
console.debug("Final nodeInput:", nodeInput);

// Special handling for textOutput nodes that follow a chatAgent
// Mark them as running immediately so they can receive streaming updates
if (node.nodeType === 'textOutput' && i > 0 && nodes[i-1].nodeType === 'chatAgent') {
if (node.nodeType === 'textOutput' && i > 0 && executableNodes[i-1].nodeType === 'chatAgent') {
setNodes((prev: WorkflowStep[]) =>
prev.map(n =>
n.id === node.id
? {
...n,
status: 'running',
logs: [...n.logs, 'Receiving streaming content...']
logs: [...(n.logs || []), 'Receiving streaming content...']
}
: n
)
Expand Down Expand Up @@ -743,7 +875,7 @@ export const executeWorkflow = async ({
? {
...n,
status: 'completed',
logs: [...n.logs, result.log],
logs: [...(n.logs || []), result.log || ''],
style: {
background: 'var(--background)',
color: 'var(--foreground)',
Expand All @@ -763,7 +895,7 @@ export const executeWorkflow = async ({
? {
...n,
status: 'error',
logs: [...n.logs, `Error: ${error}`],
logs: [...(n.logs || []), `Error: ${error instanceof Error ? error.message : String(error)}`],
style: {
background: 'var(--destructive)',
color: 'var(--destructive-foreground)',
Expand All @@ -789,6 +921,6 @@ export const executeWorkflow = async ({
};
} catch (error) {
console.error('Workflow execution failed:', error);
return { success: false, error };
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
};
29 changes: 18 additions & 11 deletions extensions/chrome/src/popup/workflow-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,16 @@ export function WorkflowView({ workflow, onBack }: WorkflowViewProps) {
const isIteratorNode = (node: WorkflowStep) =>
node.nodeType?.toLowerCase() === 'iterator';

// Add this helper function to identify outputFormat nodes
const isOutputFormatNode = (node: WorkflowStep) =>
node.nodeType?.toLowerCase() === 'outputformat';

useEffect(() => {
console.log('Workflow data received:', workflow);
console.log('Initial nodes:', nodes);
console.debug('Workflow data received:', workflow);
console.debug('Initial nodes:', nodes);
// Add detailed logging for each node
nodes.forEach(node => {
console.log(`Node ${node.id} details:`, {
console.debug(`Node ${node.id} details:`, {
nodeType: node.nodeType,
nodeData: node.nodeData,
value: node.nodeData?.value,
Expand Down Expand Up @@ -267,18 +271,21 @@ export function WorkflowView({ workflow, onBack }: WorkflowViewProps) {
};

const hasPersistedInput = (node: WorkflowStep) => {
console.log('Checking node for persisted input:', {
nodeId: node.id,
nodeType: node.nodeType,
nodeData: node.nodeData,
value: node.nodeData?.value,
fullNode: node
});
// console.debug('Checking node for persisted input:', {
// nodeId: node.id,
// nodeType: node.nodeType,
// nodeData: node.nodeData,
// value: node.nodeData?.value,
// fullNode: node
// });
return node.nodeData?.value && node.nodeData.value.trim().length > 0;
};

// Update shouldShowContentSection to handle new node types
// Update shouldShowContentSection to exclude outputFormat nodes completely
const shouldShowContentSection = (node: WorkflowStep) => {
// Don't show content section for outputFormat nodes at all
if (isOutputFormatNode(node)) return false;

const inputTypes = ['input', 'output', 'systemprompt', 'stringmanipulation', 'webhook', 'openwebpage', 'iterator'];
return inputTypes.some(type => node.nodeType?.toLowerCase().includes(type)) ||
hasPersistedInput(node) ||
Expand Down