Skip to content
Open
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
10 changes: 10 additions & 0 deletions apps/kitchensink-react/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ResourceProvider} from '@sanity/sdk-react'
import {type JSX} from 'react'
import {Route, Routes} from 'react-router'

Expand All @@ -14,6 +15,7 @@ import {SearchRoute} from './DocumentCollection/SearchRoute'
import {PresenceRoute} from './Presence/PresenceRoute'
import {ProjectAuthHome} from './ProjectAuthentication/ProjectAuthHome'
import {ProtectedRoute} from './ProtectedRoute'
import {AgentActionsRoute} from './routes/AgentActionsRoute'
import {DashboardContextRoute} from './routes/DashboardContextRoute'
import {DashboardWorkspacesRoute} from './routes/DashboardWorkspacesRoute'
import ExperimentalResourceClientRoute from './routes/ExperimentalResourceClientRoute'
Expand Down Expand Up @@ -121,6 +123,14 @@ export function AppRoutes(): JSX.Element {
{documentCollectionRoutes.map((route) => (
<Route key={route.path} path={route.path} element={route.element} />
))}
<Route
path="agent-actions"
element={
<ResourceProvider projectId="vo1ysemo" dataset="production" fallback={null}>
<AgentActionsRoute />
</ResourceProvider>
}
/>
<Route path="users/:userId" element={<UserDetailRoute />} />
<Route path="comlink-demo" element={<ParentApp />} />
<Route path="releases" element={<ReleasesRoute />} />
Expand Down
136 changes: 136 additions & 0 deletions apps/kitchensink-react/src/routes/AgentActionsRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
type AgentGenerateOptions,
type AgentPromptOptions,
useAgentGenerate,
useAgentPrompt,
} from '@sanity/sdk-react'
import {Box, Button, Card, Code, Label, Stack, Text} from '@sanity/ui'
import {type JSX, useMemo, useState} from 'react'

import {PageLayout} from '../components/PageLayout'

export function AgentActionsRoute(): JSX.Element {
const generate = useAgentGenerate()
const prompt = useAgentPrompt()
const [text, setText] = useState('Write a short poem about typescript and cats')
const [promptResult, setPromptResult] = useState<string>('')
const [generateResult, setGenerateResult] = useState<string>('')
const [isLoadingPrompt, setIsLoadingPrompt] = useState(false)
const [isLoadingGenerate, setIsLoadingGenerate] = useState(false)
const generateOptions = useMemo<AgentGenerateOptions>(() => {
return {
// Use the schema collection id (workspace name), not the type name
schemaId: '_.schemas.default',
targetDocument: {
operation: 'create',
_id: crypto.randomUUID(),
_type: 'movie',
},
instruction:
'Generate a title and overview for a movie about $topic based on a famous movie. Try to not pick the same movie as someone else would pick.',
instructionParams: {topic: 'Sanity SDK'},
target: {include: ['title', 'overview']},
noWrite: true,
}
}, [])

const promptOptions = useMemo<AgentPromptOptions>(
() => ({instruction: text, format: 'string'}),
[text],
)

return (
<PageLayout title="Agent Actions" subtitle="Prompt and generate using the movie schema">
<Stack space={4}>
<Card padding={4} radius={2} shadow={1} tone="inherit">
<Stack space={3}>
<Label size={1}>Prompt</Label>
<Text muted size={1}>
Sends an instruction to the LLM and returns plain text (or JSON if requested). Does
not reference a schema or write any data.
</Text>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
disabled={isLoadingPrompt}
style={{
width: '100%',
height: 120,
border: '1px solid #ccc',
borderRadius: 4,
padding: 8,
}}
/>
<Box>
<Button
text="Run prompt"
tone="primary"
disabled={isLoadingPrompt}
onClick={() => {
setIsLoadingPrompt(true)
prompt(promptOptions)
.then((value) => setPromptResult(String(value ?? '')))
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err)
})
.finally(() => setIsLoadingPrompt(false))
}}
/>
</Box>
{promptResult && (
<Card padding={3} radius={2} tone="transparent">
<Text>
<Code style={{whiteSpace: 'pre-wrap'}}>{promptResult}</Code>
</Text>
</Card>
)}
</Stack>
</Card>

<Card padding={4} radius={2} shadow={1} tone="inherit">
<Stack space={3}>
<Label size={1}>Generaten a Sanity document (no write)</Label>
<Text muted size={1}>
Generates title and overview for a movie; does not persist changes.
</Text>
<Text muted size={1}>
Schema‑aware content generation targeting the current project/dataset. Use schemaId of
your document type (e.g. &quot;movie&quot;) and target to specify fields. Set noWrite
to preview without saving.
</Text>
<Box>
<Button
text="Generate"
tone="primary"
disabled={isLoadingGenerate}
onClick={() => {
setIsLoadingGenerate(true)
const sub = generate(generateOptions).subscribe({
next: (value) => {
setGenerateResult(JSON.stringify(value, null, 2))
setIsLoadingGenerate(false)
sub.unsubscribe()
},
error: (err) => {
// eslint-disable-next-line no-console
console.error(err)
setIsLoadingGenerate(false)
},
})
}}
/>
</Box>
{generateResult && (
<Card padding={3} radius={2} tone="transparent">
<Text>
<Code style={{whiteSpace: 'pre-wrap'}}>{generateResult}</Code>
</Text>
</Card>
)}
</Stack>
</Card>
</Stack>
</PageLayout>
)
}
19 changes: 19 additions & 0 deletions packages/core/src/_exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ import {type SanityProject as _SanityProject} from '@sanity/client'
*/
export type SanityProject = _SanityProject

export type {
AgentGenerateOptions,
AgentGenerateResult,
AgentPatchOptions,
AgentPatchResult,
AgentPromptOptions,
AgentPromptResult,
AgentTransformOptions,
AgentTransformResult,
AgentTranslateOptions,
AgentTranslateResult,
} from '../agent/agentActions'
export {
agentGenerate,
agentPatch,
agentPrompt,
agentTransform,
agentTranslate,
} from '../agent/agentActions'
export {AuthStateType} from '../auth/authStateType'
export {
type AuthState,
Expand Down
81 changes: 81 additions & 0 deletions packages/core/src/agent/agentActions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {firstValueFrom, of} from 'rxjs'
import {beforeEach, describe, expect, it, vi} from 'vitest'

import {
agentGenerate,
agentPatch,
agentPrompt,
agentTransform,
agentTranslate,
} from './agentActions'

let mockClient: any

vi.mock('../client/clientStore', () => {
return {
getClientState: () => ({observable: of(mockClient)}),
}
})

describe('agent actions', () => {
beforeEach(() => {
mockClient = {
observable: {
agent: {
action: {
generate: vi.fn(),
transform: vi.fn(),
translate: vi.fn(),
},
},
},
agent: {
action: {
prompt: vi.fn(),
patch: vi.fn(),
},
},
}
})

it('agentGenerate returns observable from client', async () => {
mockClient.observable.agent.action.generate.mockReturnValue(of('gen'))
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
const value = await firstValueFrom(agentGenerate(instance, {foo: 'bar'} as any))
expect(value).toBe('gen')
expect(mockClient.observable.agent.action.generate).toHaveBeenCalledWith({foo: 'bar'})
})

it('agentTransform returns observable from client', async () => {
mockClient.observable.agent.action.transform.mockReturnValue(of('xform'))
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
const value = await firstValueFrom(agentTransform(instance, {a: 1} as any))
expect(value).toBe('xform')
expect(mockClient.observable.agent.action.transform).toHaveBeenCalledWith({a: 1})
})

it('agentTranslate returns observable from client', async () => {
mockClient.observable.agent.action.translate.mockReturnValue(of('xlate'))
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
const value = await firstValueFrom(agentTranslate(instance, {b: 2} as any))
expect(value).toBe('xlate')
expect(mockClient.observable.agent.action.translate).toHaveBeenCalledWith({b: 2})
})

it('agentPrompt wraps promise into observable', async () => {
mockClient.agent.action.prompt.mockResolvedValue('prompted')
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
const value = await firstValueFrom(agentPrompt(instance, {p: true} as any))
expect(value).toBe('prompted')
expect(mockClient.agent.action.prompt).toHaveBeenCalledWith({p: true})
})

it('agentPatch wraps promise into observable', async () => {
mockClient.agent.action.patch.mockResolvedValue('patched')
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
const value = await firstValueFrom(agentPatch(instance, {q: false} as any))
expect(value).toBe('patched')
expect(mockClient.agent.action.patch).toHaveBeenCalledWith({q: false})
})
})
Loading
Loading