From 58130b88da9218b1fc7f60a797a565957d778b88 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 26 Jun 2026 13:56:22 +0530 Subject: [PATCH 1/2] fix: cover llm config setup for all robot types --- docker-compose.yml | 3 + server/src/api/record.ts | 7 +- server/src/api/sdk.ts | 3 +- server/src/routes/storage.ts | 64 +++- server/src/task-runner.ts | 7 +- .../workflow-management/scheduler/index.ts | 7 +- src/api/storage.ts | 30 +- src/components/robot/pages/RobotCreate.tsx | 222 ++++++++++++-- src/components/robot/pages/RobotEditPage.tsx | 280 +++++++++++++++++- 9 files changed, 580 insertions(+), 43 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 194c608a6..aee720083 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,9 @@ services: # DEBUG: pw:api # PWDEBUG: 1 CHROMIUM_FLAGS: '--disable-gpu --no-sandbox --headless=new' + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} + extra_hosts: + - "host.docker.internal:host-gateway" security_opt: - seccomp=unconfined shm_size: '2gb' diff --git a/server/src/api/record.ts b/server/src/api/record.ts index 77f7ab5e4..ffca8625b 100644 --- a/server/src/api/record.ts +++ b/server/src/api/record.ts @@ -17,6 +17,7 @@ import { addGoogleSheetUpdateTask, processGoogleSheetUpdates } from "../workflow import { addAirtableUpdateTask, processAirtableUpdates } from "../workflow-management/integrations/airtable"; import { sendWebhook } from "../routes/webhook"; import { convertPageToHTML, convertPageToLinks, convertPageToMarkdown, convertPageToScreenshot, convertPageToText } from '../markdownify/scrape'; +import { decrypt } from '../utils/auth'; import { executeBrowserAgent } from '../sdk/browserAgent'; import { OutputFormats } from '../constants/output-formats'; import { processRobotOutputFormats } from '../utils/output-post-processor'; @@ -898,7 +899,7 @@ async function executeRun(id: string, userId: string) { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; const summaryText = await summarizeMarkdown(markdown, llmConfig); @@ -944,7 +945,7 @@ async function executeRun(id: string, userId: string) { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; logger.log('info', `Running smart query for API scrape run ${plainRun.runId}`); @@ -1139,7 +1140,7 @@ async function executeRun(id: string, userId: string) { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; try { diff --git a/server/src/api/sdk.ts b/server/src/api/sdk.ts index 5dfb41323..ff2a51a8f 100644 --- a/server/src/api/sdk.ts +++ b/server/src/api/sdk.ts @@ -15,6 +15,7 @@ import { capture } from "../utils/analytics"; import { handleRunRecording } from "./record"; import { WorkflowEnricher } from "../sdk/workflowEnricher"; import { cancelScheduledWorkflow, scheduleWorkflow } from '../storage/schedule'; +import { encrypt } from '../utils/auth'; import { computeNextRun } from "../utils/schedule"; import moment from 'moment-timezone'; import { @@ -286,7 +287,7 @@ router.post("/sdk/robots", requireAPIKey, async (req: AuthenticatedRequest, res: ...(promptInstructionsForMeta ? { promptInstructions: promptInstructionsForMeta } : {}), ...((workflowFile.meta as any).promptLlmProvider ? { promptLlmProvider: (workflowFile.meta as any).promptLlmProvider } : {}), ...((workflowFile.meta as any).promptLlmModel ? { promptLlmModel: (workflowFile.meta as any).promptLlmModel } : {}), - ...((workflowFile.meta as any).promptLlmApiKey ? { promptLlmApiKey: (workflowFile.meta as any).promptLlmApiKey } : {}), + ...((workflowFile.meta as any).promptLlmApiKey ? { promptLlmApiKey: encrypt((workflowFile.meta as any).promptLlmApiKey) } : {}), ...((workflowFile.meta as any).promptLlmBaseUrl ? { promptLlmBaseUrl: (workflowFile.meta as any).promptLlmBaseUrl } : {}), }; diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 183860ff8..eef1d46c3 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -179,7 +179,14 @@ router.get('/recordings', requireSignIn, async (req: AuthenticatedRequest, res) return res.status(401).send({ error: 'Unauthorized' }); } const data = await Robot.findAll({ where: { userId: req.user.id } }); - return res.send(data); + const sanitized = data.map(robot => { + const plain = robot.toJSON() as any; + if (plain.recording_meta?.promptLlmApiKey) { + delete plain.recording_meta.promptLlmApiKey; + } + return plain; + }); + return res.send(sanitized); } catch (e) { logger.log('info', 'Error while reading robots'); return res.send(null); @@ -206,6 +213,12 @@ router.get('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r ); } + if (data?.recording_meta) { + const meta = { ...(data.recording_meta as any) }; + delete meta.promptLlmApiKey; + data.recording_meta = meta; + } + return res.send(data); } catch (e) { logger.log('info', 'Error while reading robots'); @@ -373,10 +386,10 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, res) => { try { const { id } = req.params; - const { name, limits, credentials, targetUrl, workflow: incomingWorkflow, formats } = req.body; + const { name, limits, credentials, targetUrl, workflow: incomingWorkflow, formats, promptLlmProvider, promptLlmModel, promptLlmApiKey, promptLlmBaseUrl } = req.body; - if (!name && !limits && !credentials && !targetUrl && !incomingWorkflow && formats === undefined) { - return res.status(400).json({ error: 'Either "name", "limits", "credentials", "target_url", "workflow" or "formats" must be provided.' }); + if (!name && !limits && !credentials && !targetUrl && !incomingWorkflow && formats === undefined && !promptLlmProvider && !promptLlmModel && !promptLlmApiKey && !promptLlmBaseUrl) { + return res.status(400).json({ error: 'Either "name", "limits", "credentials", "target_url", "workflow", "formats" or LLM config must be provided.' }); } const robot = await Robot.findOne({ where: { 'recording_meta.id': id, userId: req.user!.id } }); @@ -523,10 +536,27 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r } } + const effectiveFormats = normalizedFormats ?? (robot.recording_meta?.formats || []); + const effectiveProvider = promptLlmProvider ?? robot.recording_meta?.promptLlmProvider; + const robotType = robot.recording_meta?.type; + if ( + (robotType === 'crawl' || robotType === 'search') && + effectiveFormats.includes('summary' as OutputFormats) && + effectiveProvider && effectiveProvider !== 'ollama' && + !promptLlmApiKey && + !(robot.recording_meta as any).promptLlmApiKey + ) { + return res.status(400).json({ error: 'An API key is required when using a non-Ollama LLM provider for summary output.' }); + } + let updatedMeta = { ...robot.recording_meta }; if (trimmedName) updatedMeta.name = trimmedName; if (targetUrl) updatedMeta.url = normalizeRobotUrl(targetUrl); if (normalizedFormats !== undefined) updatedMeta.formats = normalizedFormats; + if (promptLlmProvider !== undefined) updatedMeta.promptLlmProvider = promptLlmProvider || undefined; + if (promptLlmModel !== undefined) updatedMeta.promptLlmModel = promptLlmModel || undefined; + if (promptLlmApiKey) updatedMeta.promptLlmApiKey = encrypt(promptLlmApiKey); + if (promptLlmBaseUrl !== undefined) updatedMeta.promptLlmBaseUrl = promptLlmBaseUrl || undefined; const updates: any = { recording: { ...robot.recording, workflow: normalizeWorkflowUrls(workflow) }, @@ -599,6 +629,10 @@ router.post('/recordings/scrape', requireSignIn, async (req: AuthenticatedReques return res.status(409).json({ error: `A robot with the name "${robotName}" already exists.` }); } + if (finalFormats.includes('summary' as OutputFormats) && promptLlmProvider && promptLlmProvider !== 'ollama' && !promptLlmApiKey) { + return res.status(400).json({ error: 'An API key is required when using a non-Ollama LLM provider for summary output.' }); + } + if (scrapeFormats.length === 0 && formats !== undefined) { return res.status(400).json({ error: 'At least one output format must be selected.' }); } @@ -622,7 +656,7 @@ router.post('/recordings/scrape', requireSignIn, async (req: AuthenticatedReques ...(promptInstructions ? { promptInstructions: String(promptInstructions).substring(0, 1000) } : {}), ...(promptLlmProvider ? { promptLlmProvider } : {}), ...(promptLlmModel ? { promptLlmModel } : {}), - ...(promptLlmApiKey ? { promptLlmApiKey } : {}), + ...(promptLlmApiKey ? { promptLlmApiKey: encrypt(promptLlmApiKey) } : {}), ...(promptLlmBaseUrl ? { promptLlmBaseUrl } : {}), }, recording: { workflow: [] }, @@ -1823,7 +1857,7 @@ export async function recoverStuckRunningRuns() { */ router.post('/recordings/crawl', requireSignIn, async (req: AuthenticatedRequest, res) => { try { - const { url, name, crawlConfig, formats } = req.body; + const { url, name, crawlConfig, formats, promptLlmProvider, promptLlmModel, promptLlmApiKey, promptLlmBaseUrl } = req.body; if (!url || !crawlConfig) { return res.status(400).json({ error: 'URL and crawl configuration are required.' }); @@ -1861,6 +1895,10 @@ router.post('/recordings/crawl', requireSignIn, async (req: AuthenticatedRequest ? requestedFormats : [...DEFAULT_OUTPUT_FORMATS]; + if (crawlFormats.includes('summary' as OutputFormats) && promptLlmProvider && promptLlmProvider !== 'ollama' && !promptLlmApiKey) { + return res.status(400).json({ error: 'An API key is required when using a non-Ollama LLM provider for summary output.' }); + } + const currentTimestamp = new Date().toLocaleString('en-US'); const robotId = uuid(); @@ -1877,6 +1915,10 @@ router.post('/recordings/crawl', requireSignIn, async (req: AuthenticatedRequest type: 'crawl', url: normalizedUrl, formats: crawlFormats, + ...(promptLlmProvider ? { promptLlmProvider } : {}), + ...(promptLlmModel ? { promptLlmModel } : {}), + ...(promptLlmApiKey ? { promptLlmApiKey: encrypt(promptLlmApiKey) } : {}), + ...(promptLlmBaseUrl ? { promptLlmBaseUrl } : {}), }, recording: { workflow: [ @@ -1958,7 +2000,7 @@ router.post('/recordings/crawl', requireSignIn, async (req: AuthenticatedRequest */ router.post('/recordings/search', requireSignIn, async (req: AuthenticatedRequest, res) => { try { - const { searchConfig, name, formats } = req.body; + const { searchConfig, name, formats, promptLlmProvider, promptLlmModel, promptLlmApiKey, promptLlmBaseUrl } = req.body; if (!searchConfig || !searchConfig.query) { return res.status(400).json({ error: 'Search configuration with query is required.' }); @@ -1996,6 +2038,10 @@ router.post('/recordings/search', requireSignIn, async (req: AuthenticatedReques searchFormats = requestedFormats.length > 0 ? requestedFormats : [...DEFAULT_OUTPUT_FORMATS]; } + if (searchFormats.includes('summary' as OutputFormats) && promptLlmProvider && promptLlmProvider !== 'ollama' && !promptLlmApiKey) { + return res.status(400).json({ error: 'An API key is required when using a non-Ollama LLM provider for summary output.' }); + } + const currentTimestamp = new Date().toLocaleString('en-US'); const robotId = uuid(); @@ -2011,6 +2057,10 @@ router.post('/recordings/search', requireSignIn, async (req: AuthenticatedReques params: [], type: 'search', formats: searchFormats, + ...(promptLlmProvider ? { promptLlmProvider } : {}), + ...(promptLlmModel ? { promptLlmModel } : {}), + ...(promptLlmApiKey ? { promptLlmApiKey: encrypt(promptLlmApiKey) } : {}), + ...(promptLlmBaseUrl ? { promptLlmBaseUrl } : {}), }, recording: { workflow: [ diff --git a/server/src/task-runner.ts b/server/src/task-runner.ts index 9d51065a6..cba6e279a 100644 --- a/server/src/task-runner.ts +++ b/server/src/task-runner.ts @@ -13,6 +13,7 @@ import Robot from './models/Robot'; import { browserPool } from './server'; import { Page } from 'playwright-core'; import { capture } from './utils/analytics'; +import { decrypt } from './utils/auth'; import { addGoogleSheetUpdateTask, processGoogleSheetUpdates } from './workflow-management/integrations/gsheet'; import { addAirtableUpdateTask, processAirtableUpdates } from './workflow-management/integrations/airtable'; import { io as serverIo } from './server'; @@ -289,7 +290,7 @@ async function processRunExecution(data: ExecuteRunData): Promise { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; const summaryText = await summarizeMarkdown(markdown, llmConfig); @@ -320,7 +321,7 @@ async function processRunExecution(data: ExecuteRunData): Promise { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; await run.update({ log: 'Running smart query...' }); @@ -442,7 +443,7 @@ async function processRunExecution(data: ExecuteRunData): Promise { llmConfig: { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }, }); diff --git a/server/src/workflow-management/scheduler/index.ts b/server/src/workflow-management/scheduler/index.ts index 8017f43da..211acb104 100644 --- a/server/src/workflow-management/scheduler/index.ts +++ b/server/src/workflow-management/scheduler/index.ts @@ -16,6 +16,7 @@ import { addAirtableUpdateTask, airtableUpdateTasks, processAirtableUpdates } fr import { convertPageToMarkdown, convertPageToHTML, convertPageToLinks, convertPageToScreenshot, convertPageToText } from "../../markdownify/scrape"; import { executeBrowserAgent } from "../../sdk/browserAgent"; import { processRobotOutputFormats } from "../../utils/output-post-processor"; +import { decrypt } from "../../utils/auth"; import { getInterpretationFailureReason, hasExpectedRobotOutput } from "../../utils/output-validation"; import { addJob } from '../../storage/graphileWorker'; import { QUEUE_NAMES } from '../../task-runner'; @@ -376,7 +377,7 @@ async function executeRun(id: string, userId: string) { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; const summaryText = await summarizeMarkdown(markdown, llmConfig); @@ -416,7 +417,7 @@ async function executeRun(id: string, userId: string) { const llmConfig = { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }; logger.log('info', `Running smart query for scheduled scrape run ${plainRun.runId}`); @@ -618,7 +619,7 @@ async function executeRun(id: string, userId: string) { llmConfig: { provider: ((recording.recording_meta as any).promptLlmProvider || 'ollama') as 'anthropic' | 'openai' | 'ollama', model: (recording.recording_meta as any).promptLlmModel as string | undefined, - apiKey: (recording.recording_meta as any).promptLlmApiKey as string | undefined, + apiKey: (recording.recording_meta as any).promptLlmApiKey ? decrypt((recording.recording_meta as any).promptLlmApiKey) : undefined, baseUrl: (recording.recording_meta as any).promptLlmBaseUrl as string | undefined, }, }); diff --git a/src/api/storage.ts b/src/api/storage.ts index a4d458c3b..f2d820079 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -112,13 +112,17 @@ export const createLLMRobot = async ( } }; -export const updateRecording = async (id: string, data: { - name?: string; +export const updateRecording = async (id: string, data: { + name?: string; limits?: Array<{pairIndex: number, actionIndex: number, argIndex: number, limit: number}>; - credentials?: Credentials; + credentials?: Credentials; targetUrl?: string; workflow?: any[]; formats?: OutputFormats[]; + promptLlmProvider?: 'anthropic' | 'openai' | 'ollama'; + promptLlmModel?: string; + promptLlmApiKey?: string; + promptLlmBaseUrl?: string; }): Promise => { try { const response = await axios.put(`${apiUrl}/storage/recordings/${id}`, data); @@ -390,7 +394,11 @@ export const createCrawlRobot = async ( followLinks: boolean; respectRobots: boolean; }, - formats: string[] = ['markdown'] + formats: string[] = ['markdown'], + promptLlmProvider?: 'anthropic' | 'openai' | 'ollama', + promptLlmModel?: string, + promptLlmApiKey?: string, + promptLlmBaseUrl?: string ): Promise => { try { const response = await axios.post( @@ -400,6 +408,10 @@ export const createCrawlRobot = async ( name, crawlConfig, formats, + ...(promptLlmProvider ? { promptLlmProvider } : {}), + ...(promptLlmModel ? { promptLlmModel } : {}), + ...(promptLlmApiKey ? { promptLlmApiKey } : {}), + ...(promptLlmBaseUrl ? { promptLlmBaseUrl } : {}), }, { headers: { 'Content-Type': 'application/json' }, @@ -563,7 +575,11 @@ export const createSearchRobot = async ( }; mode: 'discover' | 'scrape'; }, - formats?: OutputFormats[] + formats?: OutputFormats[], + promptLlmProvider?: 'anthropic' | 'openai' | 'ollama', + promptLlmModel?: string, + promptLlmApiKey?: string, + promptLlmBaseUrl?: string ): Promise => { try { const response = await axios.post( @@ -572,6 +588,10 @@ export const createSearchRobot = async ( name, searchConfig, formats: formats || [], + ...(promptLlmProvider ? { promptLlmProvider } : {}), + ...(promptLlmModel ? { promptLlmModel } : {}), + ...(promptLlmApiKey ? { promptLlmApiKey } : {}), + ...(promptLlmBaseUrl ? { promptLlmBaseUrl } : {}), }, { headers: { 'Content-Type': 'application/json' }, diff --git a/src/components/robot/pages/RobotCreate.tsx b/src/components/robot/pages/RobotCreate.tsx index 0cb5f39d0..614b9edf9 100644 --- a/src/components/robot/pages/RobotCreate.tsx +++ b/src/components/robot/pages/RobotCreate.tsx @@ -263,7 +263,7 @@ const RobotCreate: React.FC = () => { const [scrapePromptOpenAICompatiblePreset, setScrapePromptOpenAICompatiblePreset] = useState(DEFAULT_OPENAI_COMPATIBLE_PRESET_ID); const [scrapePromptLlmModel, setScrapePromptLlmModel] = useState(''); const [scrapePromptLlmApiKey, setScrapePromptLlmApiKey] = useState(''); - const [scrapePromptLlmBaseUrl, setScrapePromptLlmBaseUrl] = useState(OLLAMA_DEFAULT_BASE_URL); + const [scrapePromptLlmBaseUrl, setScrapePromptLlmBaseUrl] = useState(''); const [aiRobotName, setAiRobotName] = useState(''); const [crawlRobotName, setCrawlRobotName] = useState(''); @@ -288,6 +288,18 @@ const RobotCreate: React.FC = () => { const [crawlOutputFormats, setCrawlOutputFormats] = useState(DEFAULT_OUTPUT_FORMATS); const [searchOutputFormats, setSearchOutputFormats] = useState(DEFAULT_OUTPUT_FORMATS); + const [crawlSummaryLlmProvider, setCrawlSummaryLlmProvider] = useState('ollama'); + const [crawlSummaryOpenAIPreset, setCrawlSummaryOpenAIPreset] = useState(DEFAULT_OPENAI_COMPATIBLE_PRESET_ID); + const [crawlSummaryLlmModel, setCrawlSummaryLlmModel] = useState(''); + const [crawlSummaryLlmApiKey, setCrawlSummaryLlmApiKey] = useState(''); + const [crawlSummaryLlmBaseUrl, setCrawlSummaryLlmBaseUrl] = useState(''); + + const [searchSummaryLlmProvider, setSearchSummaryLlmProvider] = useState('ollama'); + const [searchSummaryOpenAIPreset, setSearchSummaryOpenAIPreset] = useState(DEFAULT_OPENAI_COMPATIBLE_PRESET_ID); + const [searchSummaryLlmModel, setSearchSummaryLlmModel] = useState(''); + const [searchSummaryLlmApiKey, setSearchSummaryLlmApiKey] = useState(''); + const [searchSummaryLlmBaseUrl, setSearchSummaryLlmBaseUrl] = useState(''); + const [documentFile, setDocumentFile] = useState(null); const [documentPrompt, setDocumentPrompt] = useState(''); const [documentRobotName, setDocumentRobotName] = useState(''); @@ -308,7 +320,7 @@ const RobotCreate: React.FC = () => { }; const getBaseUrlForProvider = (provider: LlmProvider, presetId: OpenAICompatiblePresetId) => { - if (provider === 'ollama') return OLLAMA_DEFAULT_BASE_URL; + if (provider === 'ollama') return ''; if (provider === 'openai') return getOpenAICompatiblePreset(presetId).baseUrl; return ''; }; @@ -389,7 +401,6 @@ const RobotCreate: React.FC = () => { window.open(`/recording-setup?session=${sessionId}`, '_blank'); window.sessionStorage.setItem('nextTabIsRecording', 'true'); - // Reset loading state immediately after opening new tab setIsLoading(false); navigate('/robots'); } catch (error) { @@ -408,7 +419,6 @@ const RobotCreate: React.FC = () => { setWarningModalOpen(false); setIsLoading(false); - // Continue with the original Recording logic setBrowserId('new-recording'); setRecordingUrl(url); @@ -440,6 +450,10 @@ const RobotCreate: React.FC = () => { notify('error', 'Please select at least one output format'); return; } + if (crawlOutputFormats.includes('summary' as OutputFormats) && crawlSummaryLlmProvider !== 'ollama' && !crawlSummaryLlmApiKey.trim()) { + notify('error', `An API key is required when using ${crawlSummaryLlmProvider === 'anthropic' ? 'Anthropic' : 'an OpenAI-compatible'} provider for summaries`); + return; + } const normalizedCrawlUrl = normalizeUrl(crawlUrl); setCrawlUrl(normalizedCrawlUrl); @@ -459,7 +473,11 @@ const RobotCreate: React.FC = () => { followLinks: crawlFollowLinks, respectRobots: crawlRespectRobots }, - crawlOutputFormats + crawlOutputFormats, + crawlOutputFormats.includes('summary' as OutputFormats) ? crawlSummaryLlmProvider : undefined, + crawlOutputFormats.includes('summary' as OutputFormats) ? crawlSummaryLlmModel.trim() || undefined : undefined, + crawlOutputFormats.includes('summary' as OutputFormats) && crawlSummaryLlmProvider !== 'ollama' ? crawlSummaryLlmApiKey || undefined : undefined, + crawlOutputFormats.includes('summary' as OutputFormats) ? crawlSummaryLlmBaseUrl.trim() || undefined : undefined ); setIsLoading(false); if (result) { @@ -488,6 +506,10 @@ const RobotCreate: React.FC = () => { notify('error', 'Please select at least one output format'); return; } + if (searchMode === 'scrape' && searchOutputFormats.includes('summary' as OutputFormats) && searchSummaryLlmProvider !== 'ollama' && !searchSummaryLlmApiKey.trim()) { + notify('error', `An API key is required when using ${searchSummaryLlmProvider === 'anthropic' ? 'Anthropic' : 'an OpenAI-compatible'} provider for summaries`); + return; + } setIsLoading(true); try { @@ -504,7 +526,11 @@ const RobotCreate: React.FC = () => { }, mode: searchMode }, - formatsForRequest + formatsForRequest, + searchMode === 'scrape' && searchOutputFormats.includes('summary' as OutputFormats) ? searchSummaryLlmProvider : undefined, + searchMode === 'scrape' && searchOutputFormats.includes('summary' as OutputFormats) ? searchSummaryLlmModel.trim() || undefined : undefined, + searchMode === 'scrape' && searchOutputFormats.includes('summary' as OutputFormats) && searchSummaryLlmProvider !== 'ollama' ? searchSummaryLlmApiKey || undefined : undefined, + searchMode === 'scrape' && searchOutputFormats.includes('summary' as OutputFormats) ? searchSummaryLlmBaseUrl.trim() || undefined : undefined ); setIsLoading(false); if (result) { @@ -832,12 +858,14 @@ const RobotCreate: React.FC = () => { {llmProvider === 'ollama' && ( setLlmBaseUrl(e.target.value)} label="Ollama Base URL (Optional)" + helperText="Defaults to http://localhost:11434. Use http://host.docker.internal:11434 if running via Docker" + FormHelperTextProps={{ sx: { ml: 0.5 } }} /> )} @@ -846,7 +874,6 @@ const RobotCreate: React.FC = () => { variant="contained" fullWidth onClick={async () => { - // URL is optional for AI mode - it will auto-search if not provided if (!extractRobotName.trim()) { notify('error', 'Please enter a robot name'); return; @@ -1170,13 +1197,15 @@ const RobotCreate: React.FC = () => { )} {scrapePromptLlmProvider === 'ollama' && ( setScrapePromptLlmBaseUrl(e.target.value)} - label="Ollama Base URL" + label="Ollama Base URL (Optional)" + helperText="Defaults to http://localhost:11434. Use http://host.docker.internal:11434 if running via Docker" sx={{ mb: 2 }} + FormHelperTextProps={{ sx: { ml: 0.5 } }} /> )} @@ -1203,15 +1232,16 @@ const RobotCreate: React.FC = () => { setIsLoading(true); try { const hasPrompt = !!scrapePromptInstructions.trim(); + const needsLlm = hasPrompt || outputFormats.includes('summary'); const result = await createScrapeRobot( normalizedUrl, scrapeRobotName, outputFormats, hasPrompt ? scrapePromptInstructions : undefined, - hasPrompt ? scrapePromptLlmProvider : undefined, - hasPrompt ? scrapePromptLlmModel.trim() || undefined : undefined, - hasPrompt && scrapePromptLlmProvider !== 'ollama' ? scrapePromptLlmApiKey || undefined : undefined, - hasPrompt && scrapePromptLlmProvider !== 'anthropic' ? scrapePromptLlmBaseUrl || undefined : undefined, + needsLlm ? scrapePromptLlmProvider : undefined, + needsLlm ? scrapePromptLlmModel.trim() || undefined : undefined, + needsLlm && scrapePromptLlmProvider !== 'ollama' ? scrapePromptLlmApiKey || undefined : undefined, + needsLlm && scrapePromptLlmProvider !== 'anthropic' ? scrapePromptLlmBaseUrl || undefined : undefined, ); setIsLoading(false); if (result) { @@ -1319,6 +1349,83 @@ const RobotCreate: React.FC = () => { + {crawlOutputFormats.includes('summary' as OutputFormats) && ( + + + + Summary LLM Provider + + + setCrawlSummaryLlmModel(e.target.value)} + FormHelperTextProps={{ sx: { ml: 0.5 } }} + /> + + {crawlSummaryLlmProvider === 'openai' && ( + applyOpenAICompatiblePreset( + id, + setCrawlSummaryOpenAIPreset, + setCrawlSummaryLlmModel, + setCrawlSummaryLlmBaseUrl, + setCrawlSummaryLlmApiKey + )} + baseUrl={crawlSummaryLlmBaseUrl} + onBaseUrlChange={setCrawlSummaryLlmBaseUrl} + apiKey={crawlSummaryLlmApiKey} + onApiKeyChange={setCrawlSummaryLlmApiKey} + bottomMargin={2} + /> + )} + {crawlSummaryLlmProvider === 'anthropic' && ( + setCrawlSummaryLlmApiKey(e.target.value)} + label="API Key (Optional if set in .env)" + sx={{ mb: 2 }} + /> + )} + {crawlSummaryLlmProvider === 'ollama' && ( + setCrawlSummaryLlmBaseUrl(e.target.value)} + label="Ollama Base URL (Optional)" + placeholder={OLLAMA_DEFAULT_BASE_URL} + helperText="Defaults to http://localhost:11434. Use http://host.docker.internal:11434 if running via Docker" + sx={{ mb: 2 }} + FormHelperTextProps={{ sx: { ml: 0.5 } }} + /> + )} + + )} +