mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-14 08:03:58 +00:00
Compare commits
6 Commits
fix/github
...
scheduled-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b21e4557e | ||
|
|
45bf13d6f0 | ||
|
|
b8744fc037 | ||
|
|
25d3444b88 | ||
|
|
120db3a142 | ||
|
|
599ba2e6dd |
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { ChevronDown, Loader2, Sparkles, Undo2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod/v3'
|
||||
import { ChatProviderSelector } from '@/components/chat/ChatProviderSelector'
|
||||
@@ -40,6 +40,10 @@ import {
|
||||
providersStorage,
|
||||
} from '@/lib/llm-providers/storage'
|
||||
import type { LlmProviderConfig, ProviderType } from '@/lib/llm-providers/types'
|
||||
import { SCHEDULED_TASK_PROMPT_REFINED_EVENT } from '@/lib/constants/analyticsEvents'
|
||||
import { track } from '@/lib/metrics/track'
|
||||
import { refinePrompt } from '@/lib/schedules/refine-prompt'
|
||||
import { toast } from 'sonner'
|
||||
import type { ScheduledJob } from './types'
|
||||
|
||||
const formSchema = z
|
||||
@@ -109,6 +113,10 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
|
||||
|
||||
const scheduleType = form.watch('scheduleType')
|
||||
const selectedProviderId = form.watch('providerId')
|
||||
const queryValue = form.watch('query')
|
||||
const [isRefining, setIsRefining] = useState(false)
|
||||
const originalPromptRef = useRef<string | null>(null)
|
||||
const refineRequestIdRef = useRef(0)
|
||||
|
||||
// Load providers from storage
|
||||
useEffect(() => {
|
||||
@@ -124,6 +132,9 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
refineRequestIdRef.current++
|
||||
originalPromptRef.current = null
|
||||
setIsRefining(false)
|
||||
if (initialValues) {
|
||||
form.reset({
|
||||
name: initialValues.name,
|
||||
@@ -168,6 +179,42 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
|
||||
type: p.type,
|
||||
}))
|
||||
|
||||
const handleRefinePrompt = async () => {
|
||||
const currentQuery = form.getValues('query').trim()
|
||||
const currentName = form.getValues('name').trim()
|
||||
if (!currentQuery) return
|
||||
|
||||
const requestId = ++refineRequestIdRef.current
|
||||
setIsRefining(true)
|
||||
originalPromptRef.current = currentQuery
|
||||
|
||||
try {
|
||||
const refined = await refinePrompt({
|
||||
prompt: currentQuery,
|
||||
name: currentName || 'Untitled Task',
|
||||
providerId: form.getValues('providerId'),
|
||||
})
|
||||
if (requestId !== refineRequestIdRef.current) return
|
||||
form.setValue('query', refined)
|
||||
track(SCHEDULED_TASK_PROMPT_REFINED_EVENT)
|
||||
} catch {
|
||||
if (requestId !== refineRequestIdRef.current) return
|
||||
toast.error('Failed to rewrite prompt. Please try again.')
|
||||
originalPromptRef.current = null
|
||||
} finally {
|
||||
if (requestId === refineRequestIdRef.current) {
|
||||
setIsRefining(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUndoRefine = () => {
|
||||
if (originalPromptRef.current !== null) {
|
||||
form.setValue('query', originalPromptRef.current)
|
||||
originalPromptRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
onSave({
|
||||
name: values.name.trim(),
|
||||
@@ -181,6 +228,7 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
|
||||
enabled: values.enabled,
|
||||
})
|
||||
form.reset()
|
||||
originalPromptRef.current = null
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
@@ -218,17 +266,51 @@ export const NewScheduledTaskDialog: FC<NewScheduledTaskDialogProps> = ({
|
||||
name="query"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Prompt</FormLabel>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Prompt</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto gap-1 px-2 py-1 text-xs text-muted-foreground"
|
||||
disabled={!queryValue?.trim() || isRefining}
|
||||
onClick={handleRefinePrompt}
|
||||
>
|
||||
{isRefining ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-3 w-3" />
|
||||
)}
|
||||
{isRefining ? 'Rewriting...' : 'Rewrite with AI'}
|
||||
</Button>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="What should the agent do? e.g., Check my email and summarize important messages"
|
||||
className="min-h-[100px] resize-none"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e)
|
||||
if (originalPromptRef.current !== null) {
|
||||
originalPromptRef.current = null
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The instruction that will be sent to the agent
|
||||
</FormDescription>
|
||||
{!isRefining && originalPromptRef.current !== null ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={handleUndoRefine}
|
||||
>
|
||||
<Undo2 className="h-3 w-3" />
|
||||
Undo rewrite
|
||||
</button>
|
||||
) : (
|
||||
<FormDescription>
|
||||
The instruction that will be sent to the agent
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -56,6 +56,10 @@ export const SCHEDULED_TASK_DELETED_EVENT = 'settings.scheduled_task.deleted'
|
||||
/** @public */
|
||||
export const SCHEDULED_TASK_TOGGLED_EVENT = 'settings.scheduled_task.toggled'
|
||||
|
||||
/** @public */
|
||||
export const SCHEDULED_TASK_PROMPT_REFINED_EVENT =
|
||||
'settings.scheduled_task.prompt_refined'
|
||||
|
||||
/** @public */
|
||||
export const SCHEDULED_TASK_TESTED_EVENT = 'settings.scheduled_task.tested'
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { getAgentServerUrl } from '@/lib/browseros/helpers'
|
||||
import {
|
||||
createDefaultBrowserOSProvider,
|
||||
defaultProviderIdStorage,
|
||||
providersStorage,
|
||||
} from '@/lib/llm-providers/storage'
|
||||
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
|
||||
|
||||
const resolveProvider = async (
|
||||
providerId?: string,
|
||||
): Promise<LlmProviderConfig> => {
|
||||
const providers = await providersStorage.getValue()
|
||||
if (providerId && providers?.length) {
|
||||
const match = providers.find((p) => p.id === providerId)
|
||||
if (match) return match
|
||||
}
|
||||
if (providers?.length) {
|
||||
const defaultProviderId = await defaultProviderIdStorage.getValue()
|
||||
const defaultProvider = providers.find((p) => p.id === defaultProviderId)
|
||||
if (defaultProvider) return defaultProvider
|
||||
if (providers[0]) return providers[0]
|
||||
}
|
||||
return createDefaultBrowserOSProvider()
|
||||
}
|
||||
|
||||
interface RefinePromptResponse {
|
||||
success: boolean
|
||||
refined?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export async function refinePrompt(params: {
|
||||
prompt: string
|
||||
name: string
|
||||
providerId?: string
|
||||
}): Promise<string> {
|
||||
const agentServerUrl = await getAgentServerUrl()
|
||||
const provider = await resolveProvider(params.providerId)
|
||||
|
||||
const response = await fetch(`${agentServerUrl}/refine-prompt`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: params.prompt,
|
||||
name: params.name,
|
||||
provider: provider.type,
|
||||
model: provider.modelId ?? 'default',
|
||||
apiKey: provider.apiKey,
|
||||
baseUrl: provider.baseUrl,
|
||||
resourceName: provider.resourceName,
|
||||
accessKeyId: provider.accessKeyId,
|
||||
secretAccessKey: provider.secretAccessKey,
|
||||
region: provider.region,
|
||||
sessionToken: provider.sessionToken,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response
|
||||
.json()
|
||||
.catch(() => null)) as RefinePromptResponse | null
|
||||
throw new Error(errorData?.message ?? `Request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as RefinePromptResponse
|
||||
if (!data.success || !data.refined) {
|
||||
throw new Error(data.message ?? 'Failed to refine prompt')
|
||||
}
|
||||
|
||||
return data.refined
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import { z } from 'zod'
|
||||
import { refinePrompt } from '../../lib/clients/llm/refine-prompt'
|
||||
import { logger } from '../../lib/logger'
|
||||
import { AgentLLMConfigSchema } from '../types'
|
||||
|
||||
const RefinePromptRequestSchema = AgentLLMConfigSchema.extend({
|
||||
prompt: z.string().min(1, 'Prompt cannot be empty'),
|
||||
name: z.string().min(1, 'Task name cannot be empty'),
|
||||
})
|
||||
|
||||
export function createRefinePromptRoutes() {
|
||||
return new Hono().post(
|
||||
'/',
|
||||
zValidator('json', RefinePromptRequestSchema),
|
||||
async (c) => {
|
||||
const { prompt, name, ...llmConfig } = c.req.valid('json')
|
||||
|
||||
logger.info('Refine prompt request', {
|
||||
provider: llmConfig.provider,
|
||||
model: llmConfig.model,
|
||||
taskName: name,
|
||||
})
|
||||
|
||||
const result = await refinePrompt(llmConfig, { prompt, name })
|
||||
|
||||
logger.info('Refine prompt result', {
|
||||
provider: llmConfig.provider,
|
||||
success: result.success,
|
||||
})
|
||||
|
||||
return c.json(result, result.success ? 200 : 400)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { createKlavisRoutes } from './routes/klavis'
|
||||
import { createMcpRoutes } from './routes/mcp'
|
||||
import { createMemoryRoutes } from './routes/memory'
|
||||
import { createProviderRoutes } from './routes/provider'
|
||||
import { createRefinePromptRoutes } from './routes/refine-prompt'
|
||||
import { createSdkRoutes } from './routes/sdk'
|
||||
import { createShutdownRoute } from './routes/shutdown'
|
||||
import { createSkillsRoutes } from './routes/skills'
|
||||
@@ -113,6 +114,7 @@ export async function createHttpServer(config: HttpServerConfig) {
|
||||
.route('/memory', createMemoryRoutes())
|
||||
.route('/skills', createSkillsRoutes())
|
||||
.route('/test-provider', createProviderRoutes())
|
||||
.route('/refine-prompt', createRefinePromptRoutes())
|
||||
.route('/klavis', createKlavisRoutes({ browserosId: browserosId || '' }))
|
||||
.route(
|
||||
'/mcp',
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { TIMEOUTS } from '@browseros/shared/constants/timeouts'
|
||||
import type { LLMConfig } from '@browseros/shared/schemas/llm'
|
||||
import { generateText } from 'ai'
|
||||
import { resolveLLMConfig } from './config'
|
||||
import { createLLMProvider } from './provider'
|
||||
|
||||
export interface RefinePromptConfig extends LLMConfig {
|
||||
model: string
|
||||
upstreamProvider?: string
|
||||
}
|
||||
|
||||
export interface RefinePromptRequest {
|
||||
prompt: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface RefinePromptResult {
|
||||
success: boolean
|
||||
refined?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
function buildSystemPrompt(name: string): string {
|
||||
return `You are helping a user write a prompt for a scheduled browser automation task called "${name}".
|
||||
|
||||
This prompt will be executed automatically on a recurring schedule by an AI agent that can fully control a browser — navigate sites, click, type, read content, and take screenshots.
|
||||
|
||||
Rewrite the user's rough prompt into a clear, natural instruction. Make it:
|
||||
- Specific about what to do and where (which websites, what pages, what to look for)
|
||||
- Clear about what result to return at the end (a summary, key data points, changes detected, etc.)
|
||||
- Complete enough to run unattended — the agent can't ask follow-up questions
|
||||
|
||||
If the user's prompt is too vague to fill in specifics, use natural placeholders like [your competitor's URL] that they can easily spot and replace.
|
||||
|
||||
Write it as a natural instruction — like telling a capable assistant what to do. Keep it concise. Return ONLY the rewritten prompt, nothing else.`
|
||||
}
|
||||
|
||||
export async function refinePrompt(
|
||||
llmConfig: RefinePromptConfig,
|
||||
request: RefinePromptRequest,
|
||||
): Promise<RefinePromptResult> {
|
||||
try {
|
||||
const resolvedConfig = await resolveLLMConfig(llmConfig)
|
||||
const model = createLLMProvider(resolvedConfig)
|
||||
const response = await generateText({
|
||||
model,
|
||||
system: buildSystemPrompt(request.name),
|
||||
messages: [{ role: 'user', content: request.prompt }],
|
||||
abortSignal: AbortSignal.timeout(TIMEOUTS.REFINE_PROMPT),
|
||||
})
|
||||
|
||||
const refined = response.text?.trim()
|
||||
if (!refined) {
|
||||
return { success: false, message: 'Provider returned an empty response' }
|
||||
}
|
||||
|
||||
return { success: true, refined }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, message: errorMessage }
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export const TIMEOUTS = {
|
||||
TOOL_CALL: 120_000,
|
||||
TOOL_POST_ACTION: 2_000,
|
||||
TEST_PROVIDER: 15_000,
|
||||
REFINE_PROMPT: 30_000,
|
||||
|
||||
// Controller communication
|
||||
CONTROLLER_DEFAULT: 60_000,
|
||||
|
||||
Reference in New Issue
Block a user