Skip to content

Commit ea8a8f7

Browse files
authored
Merge branch 'main' into renovate/cimg-node-22.x
2 parents 0af2b9a + cb70f89 commit ea8a8f7

File tree

9 files changed

+607
-1
lines changed

9 files changed

+607
-1
lines changed

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,6 @@ jobs:
9191
done_state_id: "5a35b7bf-6d37-4cc2-854a-2f18d160e2e5"
9292

9393
- name: Trigger CircleCI Pipeline
94-
uses: CircleCI-Public/trigger-circleci-pipeline-action@v1.0.5
94+
uses: CircleCI-Public/trigger-circleci-pipeline-action@ef1944e67053c1923ad772d2377575f2fd962169 # v1.0.5
9595
env:
9696
CCI_TOKEN: ${{ secrets.CCI_TOKEN }}

apps/kitchensink-react/src/AppRoutes.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {ResourceProvider} from '@sanity/sdk-react'
12
import {type JSX} from 'react'
23
import {Route, Routes} from 'react-router'
34

@@ -14,6 +15,7 @@ import {SearchRoute} from './DocumentCollection/SearchRoute'
1415
import {PresenceRoute} from './Presence/PresenceRoute'
1516
import {ProjectAuthHome} from './ProjectAuthentication/ProjectAuthHome'
1617
import {ProtectedRoute} from './ProtectedRoute'
18+
import {AgentActionsRoute} from './routes/AgentActionsRoute'
1719
import {DashboardContextRoute} from './routes/DashboardContextRoute'
1820
import {DashboardWorkspacesRoute} from './routes/DashboardWorkspacesRoute'
1921
import ExperimentalResourceClientRoute from './routes/ExperimentalResourceClientRoute'
@@ -121,6 +123,14 @@ export function AppRoutes(): JSX.Element {
121123
{documentCollectionRoutes.map((route) => (
122124
<Route key={route.path} path={route.path} element={route.element} />
123125
))}
126+
<Route
127+
path="agent-actions"
128+
element={
129+
<ResourceProvider projectId="vo1ysemo" dataset="production" fallback={null}>
130+
<AgentActionsRoute />
131+
</ResourceProvider>
132+
}
133+
/>
124134
<Route path="users/:userId" element={<UserDetailRoute />} />
125135
<Route path="comlink-demo" element={<ParentApp />} />
126136
<Route path="releases" element={<ReleasesRoute />} />
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {
2+
type AgentGenerateOptions,
3+
type AgentPromptOptions,
4+
useAgentGenerate,
5+
useAgentPrompt,
6+
} from '@sanity/sdk-react'
7+
import {Box, Button, Card, Code, Label, Stack, Text} from '@sanity/ui'
8+
import {type JSX, useMemo, useState} from 'react'
9+
10+
import {PageLayout} from '../components/PageLayout'
11+
12+
export function AgentActionsRoute(): JSX.Element {
13+
const generate = useAgentGenerate()
14+
const prompt = useAgentPrompt()
15+
const [text, setText] = useState('Write a short poem about typescript and cats')
16+
const [promptResult, setPromptResult] = useState<string>('')
17+
const [generateResult, setGenerateResult] = useState<string>('')
18+
const [isLoadingPrompt, setIsLoadingPrompt] = useState(false)
19+
const [isLoadingGenerate, setIsLoadingGenerate] = useState(false)
20+
const generateOptions = useMemo<AgentGenerateOptions>(() => {
21+
return {
22+
// Use the schema collection id (workspace name), not the type name
23+
schemaId: '_.schemas.default',
24+
targetDocument: {
25+
operation: 'create',
26+
_id: crypto.randomUUID(),
27+
_type: 'movie',
28+
},
29+
instruction:
30+
'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.',
31+
instructionParams: {topic: 'Sanity SDK'},
32+
target: {include: ['title', 'overview']},
33+
noWrite: true,
34+
}
35+
}, [])
36+
37+
const promptOptions = useMemo<AgentPromptOptions>(
38+
() => ({instruction: text, format: 'string'}),
39+
[text],
40+
)
41+
42+
return (
43+
<PageLayout title="Agent Actions" subtitle="Prompt and generate using the movie schema">
44+
<Stack space={4}>
45+
<Card padding={4} radius={2} shadow={1} tone="inherit">
46+
<Stack space={3}>
47+
<Label size={1}>Prompt</Label>
48+
<Text muted size={1}>
49+
Sends an instruction to the LLM and returns plain text (or JSON if requested). Does
50+
not reference a schema or write any data.
51+
</Text>
52+
<textarea
53+
value={text}
54+
onChange={(e) => setText(e.target.value)}
55+
disabled={isLoadingPrompt}
56+
style={{
57+
width: '100%',
58+
height: 120,
59+
border: '1px solid #ccc',
60+
borderRadius: 4,
61+
padding: 8,
62+
}}
63+
/>
64+
<Box>
65+
<Button
66+
text="Run prompt"
67+
tone="primary"
68+
disabled={isLoadingPrompt}
69+
onClick={() => {
70+
setIsLoadingPrompt(true)
71+
prompt(promptOptions)
72+
.then((value) => setPromptResult(String(value ?? '')))
73+
.catch((err) => {
74+
// eslint-disable-next-line no-console
75+
console.error(err)
76+
})
77+
.finally(() => setIsLoadingPrompt(false))
78+
}}
79+
/>
80+
</Box>
81+
{promptResult && (
82+
<Card padding={3} radius={2} tone="transparent">
83+
<Text>
84+
<Code style={{whiteSpace: 'pre-wrap'}}>{promptResult}</Code>
85+
</Text>
86+
</Card>
87+
)}
88+
</Stack>
89+
</Card>
90+
91+
<Card padding={4} radius={2} shadow={1} tone="inherit">
92+
<Stack space={3}>
93+
<Label size={1}>Generaten a Sanity document (no write)</Label>
94+
<Text muted size={1}>
95+
Generates title and overview for a movie; does not persist changes.
96+
</Text>
97+
<Text muted size={1}>
98+
Schema‑aware content generation targeting the current project/dataset. Use schemaId of
99+
your document type (e.g. &quot;movie&quot;) and target to specify fields. Set noWrite
100+
to preview without saving.
101+
</Text>
102+
<Box>
103+
<Button
104+
text="Generate"
105+
tone="primary"
106+
disabled={isLoadingGenerate}
107+
onClick={() => {
108+
setIsLoadingGenerate(true)
109+
const sub = generate(generateOptions).subscribe({
110+
next: (value) => {
111+
setGenerateResult(JSON.stringify(value, null, 2))
112+
setIsLoadingGenerate(false)
113+
sub.unsubscribe()
114+
},
115+
error: (err) => {
116+
// eslint-disable-next-line no-console
117+
console.error(err)
118+
setIsLoadingGenerate(false)
119+
},
120+
})
121+
}}
122+
/>
123+
</Box>
124+
{generateResult && (
125+
<Card padding={3} radius={2} tone="transparent">
126+
<Text>
127+
<Code style={{whiteSpace: 'pre-wrap'}}>{generateResult}</Code>
128+
</Text>
129+
</Card>
130+
)}
131+
</Stack>
132+
</Card>
133+
</Stack>
134+
</PageLayout>
135+
)
136+
}

packages/core/src/_exports/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ import {type SanityProject as _SanityProject} from '@sanity/client'
55
*/
66
export type SanityProject = _SanityProject
77

8+
export type {
9+
AgentGenerateOptions,
10+
AgentGenerateResult,
11+
AgentPatchOptions,
12+
AgentPatchResult,
13+
AgentPromptOptions,
14+
AgentPromptResult,
15+
AgentTransformOptions,
16+
AgentTransformResult,
17+
AgentTranslateOptions,
18+
AgentTranslateResult,
19+
} from '../agent/agentActions'
20+
export {
21+
agentGenerate,
22+
agentPatch,
23+
agentPrompt,
24+
agentTransform,
25+
agentTranslate,
26+
} from '../agent/agentActions'
827
export {AuthStateType} from '../auth/authStateType'
928
export {
1029
type AuthState,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import {firstValueFrom, of} from 'rxjs'
3+
import {beforeEach, describe, expect, it, vi} from 'vitest'
4+
5+
import {
6+
agentGenerate,
7+
agentPatch,
8+
agentPrompt,
9+
agentTransform,
10+
agentTranslate,
11+
} from './agentActions'
12+
13+
let mockClient: any
14+
15+
vi.mock('../client/clientStore', () => {
16+
return {
17+
getClientState: () => ({observable: of(mockClient)}),
18+
}
19+
})
20+
21+
describe('agent actions', () => {
22+
beforeEach(() => {
23+
mockClient = {
24+
observable: {
25+
agent: {
26+
action: {
27+
generate: vi.fn(),
28+
transform: vi.fn(),
29+
translate: vi.fn(),
30+
},
31+
},
32+
},
33+
agent: {
34+
action: {
35+
prompt: vi.fn(),
36+
patch: vi.fn(),
37+
},
38+
},
39+
}
40+
})
41+
42+
it('agentGenerate returns observable from client', async () => {
43+
mockClient.observable.agent.action.generate.mockReturnValue(of('gen'))
44+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
45+
const value = await firstValueFrom(agentGenerate(instance, {foo: 'bar'} as any))
46+
expect(value).toBe('gen')
47+
expect(mockClient.observable.agent.action.generate).toHaveBeenCalledWith({foo: 'bar'})
48+
})
49+
50+
it('agentTransform returns observable from client', async () => {
51+
mockClient.observable.agent.action.transform.mockReturnValue(of('xform'))
52+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
53+
const value = await firstValueFrom(agentTransform(instance, {a: 1} as any))
54+
expect(value).toBe('xform')
55+
expect(mockClient.observable.agent.action.transform).toHaveBeenCalledWith({a: 1})
56+
})
57+
58+
it('agentTranslate returns observable from client', async () => {
59+
mockClient.observable.agent.action.translate.mockReturnValue(of('xlate'))
60+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
61+
const value = await firstValueFrom(agentTranslate(instance, {b: 2} as any))
62+
expect(value).toBe('xlate')
63+
expect(mockClient.observable.agent.action.translate).toHaveBeenCalledWith({b: 2})
64+
})
65+
66+
it('agentPrompt wraps promise into observable', async () => {
67+
mockClient.agent.action.prompt.mockResolvedValue('prompted')
68+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
69+
const value = await firstValueFrom(agentPrompt(instance, {p: true} as any))
70+
expect(value).toBe('prompted')
71+
expect(mockClient.agent.action.prompt).toHaveBeenCalledWith({p: true})
72+
})
73+
74+
it('agentPatch wraps promise into observable', async () => {
75+
mockClient.agent.action.patch.mockResolvedValue('patched')
76+
const instance = {config: {projectId: 'p', dataset: 'd'}} as any
77+
const value = await firstValueFrom(agentPatch(instance, {q: false} as any))
78+
expect(value).toBe('patched')
79+
expect(mockClient.agent.action.patch).toHaveBeenCalledWith({q: false})
80+
})
81+
})

0 commit comments

Comments
 (0)