From c8c58eef54b1b369ddc008d99a262d98628bed71 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Thu, 11 Dec 2025 02:07:15 +0530 Subject: [PATCH] feat: add ai powered robot generation --- server/src/routes/storage.ts | 89 +++++++- src/api/storage.ts | 40 +++- src/components/robot/RecordingsTable.tsx | 46 +++- src/components/robot/pages/RobotCreate.tsx | 249 ++++++++++++++++++++- 4 files changed, 417 insertions(+), 7 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 3941b01f..7da96179 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -15,6 +15,7 @@ import { encrypt, decrypt } from '../utils/auth'; import { WorkflowFile } from 'maxun-core'; import { cancelScheduledWorkflow, scheduleWorkflow } from '../storage/schedule'; import { pgBossClient } from '../storage/pgboss'; +import { WorkflowEnricher } from '../sdk/workflowEnricher'; 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. */ @@ -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}`); } - const jobId = await scheduleWorkflow(id, req.user.id, cronExpression, timezone); + await scheduleWorkflow(id, req.user.id, cronExpression, timezone); const nextRunAt = computeNextRun(cronExpression, timezone); diff --git a/src/api/storage.ts b/src/api/storage.ts index d2b28d5e..4ac2f01b 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -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 => { + 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: { name?: string; limits?: Array<{pairIndex: number, actionIndex: number, argIndex: number, limit: number}>; credentials?: Credentials; targetUrl?: string; - // optional full workflow replacement (useful for action renames) workflow?: any[]; }): Promise => { try { diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index aed9ea74..704a7f97 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -81,8 +81,46 @@ interface RecordingsTableProps { handleDuplicateRobot: (id: string, name: string, params: string[]) => void; } +const LoadingRobotRow = memo(({ row, columns }: any) => { + return ( + + {columns.map((column: Column) => { + if (column.id === 'name') { + return ( + + + + + {row.name} (Creating...) + + + + ); + } else if (column.id === 'interpret') { + return ( + + + + ); + } else { + return ( + + - + + ); + } + })} + + ); +}); + // Virtualized row component for efficient rendering const TableRowMemoized = memo(({ row, columns, handlers }: any) => { + // If robot is loading, show loading row + if (row.isLoading) { + return ; + } + return ( {columns.map((column: Column) => { @@ -261,7 +299,9 @@ export const RecordingsTable = ({ id: index, ...recording.recording_meta, content: recording.recording, - parsedDate + parsedDate, + isLoading: recording.isLoading || false, + isOptimistic: recording.isOptimistic || false }; } return null; @@ -552,7 +592,7 @@ export const RecordingsTable = ({ <> - {/* */} + {columns.map((column) => ( ))} - {/* */} + {visibleRows.map((row) => ( { const [activeBrowserId, setActiveBrowserId] = useState(''); const [outputFormats, setOutputFormats] = useState([]); + // 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 { user } = state; + const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings } = useCacheInvalidation(); const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setTabValue(newValue); @@ -206,6 +215,7 @@ const RobotCreate: React.FC = () => { }} > + @@ -362,6 +372,241 @@ const RobotCreate: React.FC = () => { + + + Maxun Logo + + + AI-powered extraction: Describe what you want to extract in natural language. + + + + setAiRobotName(e.target.value)} + sx={{ mb: 2 }} + label="Robot Name (Optional)" + /> + setUrl(e.target.value)} + label="Website URL" + sx={{ mb: 2 }} + /> + setAiPrompt(e.target.value)} + label="Extraction Prompt" + sx={{ mb: 2 }} + /> + + + + LLM Provider + + + + + Model (Optional) + + + + + {llmProvider !== 'ollama' && ( + setLlmApiKey(e.target.value)} + label="API Key (Optional if set in .env)" + sx={{ mb: 2 }} + /> + )} + + {llmProvider === 'ollama' && ( + setLlmBaseUrl(e.target.value)} + label="Ollama Base URL (Optional)" + sx={{ mb: 2 }} + /> + )} + + + + + + + +