feat: add ai powered robot generation
This commit is contained in:
@@ -15,6 +15,7 @@ import { encrypt, decrypt } from '../utils/auth';
|
|||||||
import { WorkflowFile } from 'maxun-core';
|
import { WorkflowFile } from 'maxun-core';
|
||||||
import { cancelScheduledWorkflow, scheduleWorkflow } from '../storage/schedule';
|
import { cancelScheduledWorkflow, scheduleWorkflow } from '../storage/schedule';
|
||||||
import { pgBossClient } from '../storage/pgboss';
|
import { pgBossClient } from '../storage/pgboss';
|
||||||
|
import { WorkflowEnricher } from '../sdk/workflowEnricher';
|
||||||
|
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
@@ -518,6 +519,92 @@ router.post('/recordings/scrape', requireSignIn, async (req: AuthenticatedReques
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST endpoint for creating an LLM-powered extraction robot
|
||||||
|
*/
|
||||||
|
router.post('/recordings/llm', requireSignIn, async (req: AuthenticatedRequest, res) => {
|
||||||
|
try {
|
||||||
|
const { url, prompt, llmProvider, llmModel, llmApiKey, llmBaseUrl, robotName } = req.body;
|
||||||
|
|
||||||
|
if (!url || !prompt) {
|
||||||
|
return res.status(400).json({ error: 'Both "url" and "prompt" fields are required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).send({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).json({ error: 'Invalid URL format' });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Starting LLM workflow generation for URL: ${url}`);
|
||||||
|
|
||||||
|
const workflowResult = await WorkflowEnricher.generateWorkflowFromPrompt(url, prompt, {
|
||||||
|
provider: llmProvider || 'ollama',
|
||||||
|
model: llmModel,
|
||||||
|
apiKey: llmApiKey,
|
||||||
|
baseUrl: llmBaseUrl
|
||||||
|
}, req.user.id);
|
||||||
|
|
||||||
|
if (!workflowResult.success || !workflowResult.workflow) {
|
||||||
|
logger.log('error', `Failed to generate workflow: ${JSON.stringify(workflowResult.errors)}`);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Failed to generate workflow from prompt',
|
||||||
|
details: workflowResult.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const robotId = uuid();
|
||||||
|
const currentTimestamp = new Date().toISOString();
|
||||||
|
const finalRobotName = robotName || `LLM Extract: ${prompt.substring(0, 50)}`;
|
||||||
|
|
||||||
|
const newRobot = await Robot.create({
|
||||||
|
id: uuid(),
|
||||||
|
userId: req.user.id,
|
||||||
|
recording_meta: {
|
||||||
|
name: finalRobotName,
|
||||||
|
id: robotId,
|
||||||
|
createdAt: currentTimestamp,
|
||||||
|
updatedAt: currentTimestamp,
|
||||||
|
pairs: workflowResult.workflow.length,
|
||||||
|
params: [],
|
||||||
|
type: 'extract',
|
||||||
|
url: workflowResult.url || url,
|
||||||
|
},
|
||||||
|
recording: { workflow: workflowResult.workflow },
|
||||||
|
google_sheet_email: null,
|
||||||
|
google_sheet_name: null,
|
||||||
|
google_sheet_id: null,
|
||||||
|
google_access_token: null,
|
||||||
|
google_refresh_token: null,
|
||||||
|
schedule: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log('info', `LLM robot created with id: ${newRobot.id}`);
|
||||||
|
capture('maxun-oss-robot-created', {
|
||||||
|
robot_meta: newRobot.recording_meta,
|
||||||
|
recording: newRobot.recording,
|
||||||
|
llm_provider: llmProvider || 'ollama',
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
message: 'LLM robot created successfully.',
|
||||||
|
robot: newRobot,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
logger.log('error', `Error creating LLM robot: ${error.message}`);
|
||||||
|
return res.status(500).json({ error: error.message });
|
||||||
|
} else {
|
||||||
|
logger.log('error', 'Unknown error creating LLM robot');
|
||||||
|
return res.status(500).json({ error: 'An unknown error occurred.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE endpoint for deleting a recording from the storage.
|
* DELETE endpoint for deleting a recording from the storage.
|
||||||
*/
|
*/
|
||||||
@@ -894,7 +981,7 @@ router.put('/schedule/:id/', requireSignIn, async (req: AuthenticatedRequest, re
|
|||||||
logger.log('warn', `Failed to cancel existing schedule for robot ${id}: ${cancelError}`);
|
logger.log('warn', `Failed to cancel existing schedule for robot ${id}: ${cancelError}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobId = await scheduleWorkflow(id, req.user.id, cronExpression, timezone);
|
await scheduleWorkflow(id, req.user.id, cronExpression, timezone);
|
||||||
|
|
||||||
const nextRunAt = computeNextRun(cronExpression, timezone);
|
const nextRunAt = computeNextRun(cronExpression, timezone);
|
||||||
|
|
||||||
|
|||||||
@@ -58,12 +58,50 @@ export const createScrapeRobot = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createLLMRobot = async (
|
||||||
|
url: string,
|
||||||
|
prompt: string,
|
||||||
|
llmProvider?: 'anthropic' | 'openai' | 'ollama',
|
||||||
|
llmModel?: string,
|
||||||
|
llmApiKey?: string,
|
||||||
|
llmBaseUrl?: string,
|
||||||
|
robotName?: string
|
||||||
|
): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${apiUrl}/storage/recordings/llm`,
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
prompt,
|
||||||
|
llmProvider,
|
||||||
|
llmModel,
|
||||||
|
llmApiKey,
|
||||||
|
llmBaseUrl,
|
||||||
|
robotName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
withCredentials: true,
|
||||||
|
timeout: 300000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 201) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to create LLM robot');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error creating LLM robot:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const updateRecording = async (id: string, data: {
|
export const updateRecording = async (id: string, data: {
|
||||||
name?: string;
|
name?: string;
|
||||||
limits?: Array<{pairIndex: number, actionIndex: number, argIndex: number, limit: number}>;
|
limits?: Array<{pairIndex: number, actionIndex: number, argIndex: number, limit: number}>;
|
||||||
credentials?: Credentials;
|
credentials?: Credentials;
|
||||||
targetUrl?: string;
|
targetUrl?: string;
|
||||||
// optional full workflow replacement (useful for action renames)
|
|
||||||
workflow?: any[];
|
workflow?: any[];
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -81,8 +81,46 @@ interface RecordingsTableProps {
|
|||||||
handleDuplicateRobot: (id: string, name: string, params: string[]) => void;
|
handleDuplicateRobot: (id: string, name: string, params: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LoadingRobotRow = memo(({ row, columns }: any) => {
|
||||||
|
return (
|
||||||
|
<TableRow hover role="checkbox" tabIndex={-1} sx={{ backgroundColor: 'action.hover' }}>
|
||||||
|
{columns.map((column: Column) => {
|
||||||
|
if (column.id === 'name') {
|
||||||
|
return (
|
||||||
|
<MemoizedTableCell key={column.id} align={column.align}>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{row.name} (Creating...)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</MemoizedTableCell>
|
||||||
|
);
|
||||||
|
} else if (column.id === 'interpret') {
|
||||||
|
return (
|
||||||
|
<MemoizedTableCell key={column.id} align={column.align}>
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
</MemoizedTableCell>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<MemoizedTableCell key={column.id} align={column.align}>
|
||||||
|
<Box sx={{ opacity: 0.3 }}>-</Box>
|
||||||
|
</MemoizedTableCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Virtualized row component for efficient rendering
|
// Virtualized row component for efficient rendering
|
||||||
const TableRowMemoized = memo(({ row, columns, handlers }: any) => {
|
const TableRowMemoized = memo(({ row, columns, handlers }: any) => {
|
||||||
|
// If robot is loading, show loading row
|
||||||
|
if (row.isLoading) {
|
||||||
|
return <LoadingRobotRow row={row} columns={columns} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow hover role="checkbox" tabIndex={-1}>
|
<TableRow hover role="checkbox" tabIndex={-1}>
|
||||||
{columns.map((column: Column) => {
|
{columns.map((column: Column) => {
|
||||||
@@ -261,7 +299,9 @@ export const RecordingsTable = ({
|
|||||||
id: index,
|
id: index,
|
||||||
...recording.recording_meta,
|
...recording.recording_meta,
|
||||||
content: recording.recording,
|
content: recording.recording,
|
||||||
parsedDate
|
parsedDate,
|
||||||
|
isLoading: recording.isLoading || false,
|
||||||
|
isOptimistic: recording.isOptimistic || false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -552,7 +592,7 @@ export const RecordingsTable = ({
|
|||||||
<>
|
<>
|
||||||
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
|
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
|
||||||
<Table stickyHeader aria-label="sticky table">
|
<Table stickyHeader aria-label="sticky table">
|
||||||
{/* <TableHead> */}
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<MemoizedTableCell
|
<MemoizedTableCell
|
||||||
@@ -563,7 +603,7 @@ export const RecordingsTable = ({
|
|||||||
</MemoizedTableCell>
|
</MemoizedTableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/* </TableHead> */}
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{visibleRows.map((row) => (
|
{visibleRows.map((row) => (
|
||||||
<TableRowMemoized
|
<TableRowMemoized
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import {
|
|||||||
InputLabel
|
InputLabel
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ArrowBack, PlayCircleOutline, Article, Code, Description } from '@mui/icons-material';
|
import { ArrowBack, PlayCircleOutline, Article, Code, Description } from '@mui/icons-material';
|
||||||
import { useGlobalInfoStore } from '../../../context/globalInfo';
|
import { useGlobalInfoStore, useCacheInvalidation } from '../../../context/globalInfo';
|
||||||
import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording';
|
import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording';
|
||||||
import { createScrapeRobot } from "../../../api/storage";
|
import { createScrapeRobot, createLLMRobot, createAndRunRecording } from "../../../api/storage";
|
||||||
import { AuthContext } from '../../../context/auth';
|
import { AuthContext } from '../../../context/auth';
|
||||||
import { GenericModal } from '../../ui/GenericModal';
|
import { GenericModal } from '../../ui/GenericModal';
|
||||||
|
|
||||||
@@ -65,8 +65,17 @@ const RobotCreate: React.FC = () => {
|
|||||||
const [activeBrowserId, setActiveBrowserId] = useState('');
|
const [activeBrowserId, setActiveBrowserId] = useState('');
|
||||||
const [outputFormats, setOutputFormats] = useState<string[]>([]);
|
const [outputFormats, setOutputFormats] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// AI Extract tab state
|
||||||
|
const [aiPrompt, setAiPrompt] = useState('');
|
||||||
|
const [llmProvider, setLlmProvider] = useState<'anthropic' | 'openai' | 'ollama'>('ollama');
|
||||||
|
const [llmModel, setLlmModel] = useState('');
|
||||||
|
const [llmApiKey, setLlmApiKey] = useState('');
|
||||||
|
const [llmBaseUrl, setLlmBaseUrl] = useState('');
|
||||||
|
const [aiRobotName, setAiRobotName] = useState('');
|
||||||
|
|
||||||
const { state } = React.useContext(AuthContext);
|
const { state } = React.useContext(AuthContext);
|
||||||
const { user } = state;
|
const { user } = state;
|
||||||
|
const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings } = useCacheInvalidation();
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
setTabValue(newValue);
|
setTabValue(newValue);
|
||||||
@@ -206,6 +215,7 @@ const RobotCreate: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab label="Extract" id="extract-robot" aria-controls="extract-robot" />
|
<Tab label="Extract" id="extract-robot" aria-controls="extract-robot" />
|
||||||
|
<Tab label="AI Extract" id="ai-extract-robot" aria-controls="ai-extract-robot" />
|
||||||
<Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" />
|
<Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -362,6 +372,241 @@ const RobotCreate: React.FC = () => {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={tabValue} index={1}>
|
<TabPanel value={tabValue} index={1}>
|
||||||
|
<Card sx={{ mb: 4, p: 4, textAlign: 'center' }}>
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
|
<img
|
||||||
|
src="https://ik.imagekit.io/ys1blv5kv/maxunlogo.png"
|
||||||
|
width={73}
|
||||||
|
height={65}
|
||||||
|
style={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
alt="Maxun Logo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" mb={3}>
|
||||||
|
AI-powered extraction: Describe what you want to extract in natural language.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 700, mb: 2 }}>
|
||||||
|
<TextField
|
||||||
|
placeholder="Example: AI Product Extractor"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={aiRobotName}
|
||||||
|
onChange={(e) => setAiRobotName(e.target.value)}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
label="Robot Name (Optional)"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
placeholder="Example: https://www.ycombinator.com/companies/"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
label="Website URL"
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
placeholder="Example: Extract first 15 company names, descriptions, and batch information"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
value={aiPrompt}
|
||||||
|
onChange={(e) => setAiPrompt(e.target.value)}
|
||||||
|
label="Extraction Prompt"
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', gap: 2, mb: 2 }}>
|
||||||
|
<FormControl sx={{ flex: 1 }}>
|
||||||
|
<InputLabel id="llm-provider-label">LLM Provider</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="llm-provider-label"
|
||||||
|
id="llm-provider"
|
||||||
|
value={llmProvider}
|
||||||
|
label="LLM Provider"
|
||||||
|
onChange={(e) => {
|
||||||
|
const provider = e.target.value as 'anthropic' | 'openai' | 'ollama';
|
||||||
|
setLlmProvider(provider);
|
||||||
|
setLlmModel('');
|
||||||
|
if (provider === 'ollama') {
|
||||||
|
setLlmBaseUrl('http://localhost:11434');
|
||||||
|
} else {
|
||||||
|
setLlmBaseUrl('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="ollama">Ollama (Local)</MenuItem>
|
||||||
|
<MenuItem value="anthropic">Anthropic (Claude)</MenuItem>
|
||||||
|
<MenuItem value="openai">OpenAI (GPT-4)</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl sx={{ flex: 1 }}>
|
||||||
|
<InputLabel id="llm-model-label">Model (Optional)</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="llm-model-label"
|
||||||
|
id="llm-model"
|
||||||
|
value={llmModel}
|
||||||
|
label="Model (Optional)"
|
||||||
|
onChange={(e) => setLlmModel(e.target.value)}
|
||||||
|
>
|
||||||
|
{llmProvider === 'ollama' && (
|
||||||
|
<>
|
||||||
|
<MenuItem value="">Default (llama3.2-vision)</MenuItem>
|
||||||
|
<MenuItem value="llama3.2-vision">llama3.2-vision</MenuItem>
|
||||||
|
<MenuItem value="llama3.2">llama3.2</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{llmProvider === 'anthropic' && (
|
||||||
|
<>
|
||||||
|
<MenuItem value="">Default (claude-3-5-sonnet)</MenuItem>
|
||||||
|
<MenuItem value="claude-3-5-sonnet-20241022">claude-3-5-sonnet-20241022</MenuItem>
|
||||||
|
<MenuItem value="claude-3-opus-20240229">claude-3-opus-20240229</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{llmProvider === 'openai' && (
|
||||||
|
<>
|
||||||
|
<MenuItem value="">Default (gpt-4-vision-preview)</MenuItem>
|
||||||
|
<MenuItem value="gpt-4-vision-preview">gpt-4-vision-preview</MenuItem>
|
||||||
|
<MenuItem value="gpt-4o">gpt-4o</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{llmProvider !== 'ollama' && (
|
||||||
|
<TextField
|
||||||
|
placeholder={`${llmProvider === 'anthropic' ? 'Anthropic' : 'OpenAI'} API Key (or set in .env)`}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
type="password"
|
||||||
|
value={llmApiKey}
|
||||||
|
onChange={(e) => setLlmApiKey(e.target.value)}
|
||||||
|
label="API Key (Optional if set in .env)"
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{llmProvider === 'ollama' && (
|
||||||
|
<TextField
|
||||||
|
placeholder="http://localhost:11434"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={llmBaseUrl}
|
||||||
|
onChange={(e) => setLlmBaseUrl(e.target.value)}
|
||||||
|
label="Ollama Base URL (Optional)"
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={async () => {
|
||||||
|
if (!url.trim()) {
|
||||||
|
notify('error', 'Please enter a valid URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!aiPrompt.trim()) {
|
||||||
|
notify('error', 'Please enter an extraction prompt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRobotId = `temp-${Date.now()}`;
|
||||||
|
const robotDisplayName = aiRobotName || `LLM Extract: ${aiPrompt.substring(0, 50)}`;
|
||||||
|
|
||||||
|
const optimisticRobot = {
|
||||||
|
id: tempRobotId,
|
||||||
|
recording_meta: {
|
||||||
|
id: tempRobotId,
|
||||||
|
name: robotDisplayName,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
pairs: 0,
|
||||||
|
params: [],
|
||||||
|
type: 'extract',
|
||||||
|
url: url,
|
||||||
|
},
|
||||||
|
recording: { workflow: [] },
|
||||||
|
isLoading: true,
|
||||||
|
isOptimistic: true
|
||||||
|
};
|
||||||
|
|
||||||
|
addOptimisticRobot(optimisticRobot);
|
||||||
|
|
||||||
|
notify('info', `Robot ${robotDisplayName} creation started (AI Powered)`);
|
||||||
|
navigate('/robots');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await createLLMRobot(
|
||||||
|
url,
|
||||||
|
aiPrompt,
|
||||||
|
llmProvider,
|
||||||
|
llmModel || undefined,
|
||||||
|
llmApiKey || undefined,
|
||||||
|
llmBaseUrl || undefined,
|
||||||
|
aiRobotName || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
removeOptimisticRobot(tempRobotId);
|
||||||
|
|
||||||
|
if (!result || !result.robot) {
|
||||||
|
notify('error', 'Failed to create AI robot. Please check your LLM configuration.');
|
||||||
|
invalidateRecordings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const robotMetaId = result.robot.recording_meta.id;
|
||||||
|
notify('success', `${result.robot.recording_meta.name} created successfully!`);
|
||||||
|
|
||||||
|
invalidateRecordings();
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
notify('info', 'Starting robot execution...');
|
||||||
|
const runResponse = await createAndRunRecording(robotMetaId, {
|
||||||
|
maxConcurrency: 1,
|
||||||
|
maxRepeats: 1,
|
||||||
|
debug: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (runResponse && runResponse.runId) {
|
||||||
|
notify('success', 'Robot is now running!');
|
||||||
|
navigate(`/runs/${robotMetaId}/run/${runResponse.runId}`);
|
||||||
|
} else {
|
||||||
|
notify('warning', 'Robot created but failed to start execution. You can run it manually from the robots page.');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error in AI robot creation:', error);
|
||||||
|
removeOptimisticRobot(tempRobotId);
|
||||||
|
invalidateRecordings();
|
||||||
|
notify('error', error?.message || 'Failed to create and run AI robot');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!url.trim() || !aiPrompt.trim() || isLoading}
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#ff00c3',
|
||||||
|
py: 1.4,
|
||||||
|
fontSize: '1rem',
|
||||||
|
textTransform: 'none',
|
||||||
|
maxWidth: 700,
|
||||||
|
borderRadius: 2
|
||||||
|
}}
|
||||||
|
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Creating & Running...' : 'Create & Run AI Robot'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={2}>
|
||||||
<Card sx={{ mb: 4, p: 4, textAlign: 'center' }}>
|
<Card sx={{ mb: 4, p: 4, textAlign: 'center' }}>
|
||||||
<Box display="flex" flexDirection="column" alignItems="center">
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
<img
|
<img
|
||||||
|
|||||||
Reference in New Issue
Block a user