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
3 changes: 3 additions & 0 deletions dato-agent/src/entrypoints/AgentFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
createAgentRuntime,
type FieldReferenceInput,
type GetModelSchemaCallback,
type GetRecordPreviewLinksCallback,
MAX_AGENT_ATTACHMENT_TOTAL_BYTES,
MAX_AGENT_ATTACHMENTS_PER_MESSAGE,
MAX_AGENT_ATTACHMENTS_PER_REQUEST,
Expand Down Expand Up @@ -170,6 +171,7 @@ export type AgentFrameProps = {
| undefined
| Promise<AgentHostContextSnapshot | undefined>;
getModelSchema?: GetModelSchemaCallback;
getRecordPreviewLinks?: GetRecordPreviewLinksCallback;
/**
* Sidebar-only verifier for field receipts. It must resolve against the
* latest current-model schema before a receipt is shown.
Expand Down Expand Up @@ -3578,6 +3580,7 @@ export default function AgentFrame(props: AgentFrameProps) {
additionalInstructions: props.config.additionalInstructions,
hostContext,
getModelSchema: props.getModelSchema,
getRecordPreviewLinks: props.getRecordPreviewLinks,
...(!readOnlyRef.current && createDatoAsset ? { createDatoAsset } : {}),
context: runtimeSystemContext(),
navigation: {
Expand Down
20 changes: 19 additions & 1 deletion dato-agent/src/entrypoints/AgentInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { resolveFieldsWithinDeadline } from '../lib/hostContextDeadline';
import { createAgentMentionHost } from '../lib/mentionHost';
import { createInspectorNavigator } from '../lib/navigation';
import { canRoleUseDatoAgent } from '../lib/permissions';
import { getRecordPreviewLinks } from '../lib/previewLinks';
import AgentFrame from './AgentFrame';
import AgentUnavailableFrame from './AgentUnavailableFrame';

Expand Down Expand Up @@ -264,6 +265,7 @@ function AuthorizedAgentInspector({ ctx }: Props) {
'project',
].join(':');
const mentionHost = createAgentMentionHost(ctx);
const config = normalizeConfig(ctx.plugin.attributes.parameters);

return (
<Canvas ctx={ctx} noAutoResizer>
Expand All @@ -279,11 +281,27 @@ function AuthorizedAgentInspector({ ctx }: Props) {
scope={{ type: 'project' }}
navigator={createInspectorNavigator(ctx)}
mentionHost={mentionHost}
config={normalizeConfig(ctx.plugin.attributes.parameters)}
config={config}
loadHostContext={loadHostContext}
getModelSchema={async ({ identifier, cursor }) => ({
schema: (await schemaResolver(identifier, cursor)).text,
})}
getRecordPreviewLinks={(request, signal) =>
getRecordPreviewLinks(
{
siteId: ctx.site.id,
environment: ctx.environment,
locale: ctx.ui.locale,
cmaBaseUrl: ctx.cmaBaseUrl,
accessToken: ctx.currentUserAccessToken,
currentUser: ctx.currentUser,
savedFrontends: config.previewFrontends,
websiteBaseUrl: config.websiteBaseUrl,
},
request,
signal,
)
}
onReviewApprovalDetails={(approval) =>
openApprovalDetailsModal(ctx, approval)
}
Expand Down
20 changes: 19 additions & 1 deletion dato-agent/src/entrypoints/AgentSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { createAgentMentionHost } from '../lib/mentionHost';
import { createSidebarNavigator } from '../lib/navigation';
import { canRoleUseDatoAgent } from '../lib/permissions';
import { usePersistedSidebarWidth } from '../lib/persistedWidth';
import { getRecordPreviewLinks } from '../lib/previewLinks';
import AgentFrame, { type AgentFrameProps } from './AgentFrame';
import AgentUnavailableFrame from './AgentUnavailableFrame';

Expand Down Expand Up @@ -487,6 +488,7 @@ function AuthorizedAgentSidebar({ ctx }: Props) {
const mentionHost = createAgentMentionHost(ctx, {
currentModelId: ctx.itemType.id,
});
const config = normalizeConfig(ctx.plugin.attributes.parameters);

return (
<Canvas ctx={ctx} noAutoResizer>
Expand All @@ -508,14 +510,30 @@ function AuthorizedAgentSidebar({ ctx }: Props) {
onMigrationAttempted={onMigrationAttempted}
navigator={createSidebarNavigator(ctx)}
mentionHost={mentionHost}
config={normalizeConfig(ctx.plugin.attributes.parameters)}
config={config}
loadHostContext={loadHostContext}
prepareCurrentFieldReferences={prepareCurrentFieldReferences}
readCurrentRecordLiveFormState={readCurrentRecordLiveFormState}
openCurrentField={openCurrentField}
getModelSchema={async ({ identifier, cursor }) => ({
schema: (await schemaResolver(identifier, cursor)).text,
})}
getRecordPreviewLinks={(request, signal) =>
getRecordPreviewLinks(
{
siteId: ctx.site.id,
environment: ctx.environment,
locale: ctx.locale,
cmaBaseUrl: ctx.cmaBaseUrl,
accessToken: ctx.currentUserAccessToken,
currentUser: ctx.currentUser,
savedFrontends: config.previewFrontends,
websiteBaseUrl: config.websiteBaseUrl,
},
request,
signal,
)
}
onReviewApprovalDetails={(approval) =>
openApprovalDetailsModal(ctx, approval)
}
Expand Down
123 changes: 123 additions & 0 deletions dato-agent/src/entrypoints/ConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
withActiveReasoningEffort,
} from '../lib/config';
import { ACCOUNT_ROLE_ID } from '../lib/permissions';
import {
loadWebPreviewFrontends,
type PreviewFrontend,
} from '../lib/previewLinks';
import {
listProviderModels,
type ProviderModel,
Expand Down Expand Up @@ -797,6 +801,111 @@ function ProviderConfigurationFields({
);
}

function WebsiteLinksSection({
accessToken,
baseUrl,
canEdit,
environment,
onPreviewFrontendsChange,
onWebsiteBaseUrlChange,
previewFrontends,
saving,
websiteBaseUrl,
}: {
accessToken: string | null | undefined;
baseUrl: string;
canEdit: boolean;
environment: string;
onPreviewFrontendsChange: (frontends: PreviewFrontend[]) => void;
onWebsiteBaseUrlChange: (value: string) => void;
previewFrontends: readonly PreviewFrontend[];
saving: boolean;
websiteBaseUrl: string;
}) {
const [importing, setImporting] = useState(false);
const [error, setError] = useState<string>();
const [message, setMessage] = useState<string>();

const importFrontends = async () => {
if (!accessToken) {
setError('The current user access token is unavailable.');
setMessage(undefined);
return;
}

setImporting(true);
setError(undefined);
setMessage(undefined);
try {
const frontends = await loadWebPreviewFrontends(
buildClient({
apiToken: accessToken,
environment,
baseUrl,
}),
);
if (frontends.length === 0) {
setError(
'Web Previews is not installed, or none of its frontends have a preview link endpoint.',
);
return;
}

onPreviewFrontendsChange(frontends);
const noun = frontends.length === 1 ? 'frontend' : 'frontends';
const pronoun = frontends.length === 1 ? 'it' : 'them';
setMessage(
`Imported ${frontends.length} ${noun}. Save settings to keep ${pronoun}.`,
);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Could not read the Web Previews settings.',
);
} finally {
setImporting(false);
}
};

return (
<Section title="Website links">
<FieldGroup>
<TextField
id="website-base-url"
name="website-base-url"
label="Website address"
value={websiteBaseUrl}
placeholder="https://www.example.com"
hint="Used only when Web Previews does not return a link. The agent adds the record slug to this address."
onChange={onWebsiteBaseUrlChange}
textInputProps={{
disabled: !canEdit || saving,
}}
/>
<Button
type="button"
buttonType="muted"
disabled={!canEdit || saving || importing}
onClick={() => {
void importFrontends();
}}
>
{importing ? 'Importing…' : 'Import from Web Previews'}
</Button>
{previewFrontends.length > 0 && (
<p>
Saved preview endpoints:{' '}
{previewFrontends.map((frontend) => frontend.name).join(', ')}.
</p>
)}
{message && <p>{message}</p>}
{error && <p role="alert">{error}</p>}
</FieldGroup>
</Section>
);
}

export default function ConfigScreen({ ctx }: Props) {
const incomingConfig = useMemo(
() => normalizeConfig(ctx.plugin.attributes.parameters),
Expand Down Expand Up @@ -1054,6 +1163,20 @@ export default function ConfigScreen({ ctx }: Props) {
</FieldGroup>
</Section>

<WebsiteLinksSection
accessToken={ctx.currentUserAccessToken}
baseUrl={ctx.cmaBaseUrl}
canEdit={canEditSchema}
environment={ctx.environment}
onPreviewFrontendsChange={(frontends) =>
update('previewFrontends', frontends)
}
onWebsiteBaseUrlChange={(value) => update('websiteBaseUrl', value)}
previewFrontends={config.previewFrontends}
saving={saving}
websiteBaseUrl={config.websiteBaseUrl}
/>

<Section title="Permissions">
<FieldGroup>
<SwitchField
Expand Down
6 changes: 6 additions & 0 deletions dato-agent/src/lib/activityLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,8 @@ function localInProgressLabel(
}
case 'open_record':
return 'Opening a record';
case 'get_record_preview_links':
return 'Looking up the website URL';
case 'show_records': {
return prefixedCountLabel(
argumentsRecord,
Expand Down Expand Up @@ -813,6 +815,8 @@ function localCompletedLabel(
}
case 'open_record':
return 'Record ready';
case 'get_record_preview_links':
return 'Website URL ready';
case 'show_records': {
return suffixedCountLabel(
argumentsRecord,
Expand Down Expand Up @@ -894,6 +898,8 @@ function localFailedLabel(toolName: string): string {
switch (toolName) {
case 'get_model_schema':
return 'Could not read model fields';
case 'get_record_preview_links':
return 'Could not look up the website URL';
case 'present_records':
return 'Could not add record links';
case 'present_fields':
Expand Down
38 changes: 38 additions & 0 deletions dato-agent/src/lib/agentRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,44 @@ describe('AgentRuntime', () => {
expect(toolNames).not.toContain('present_fields');
expect(toolNames).not.toContain('read_current_record_live_form_state');
expect(toolNames).toContain('present_assets');
expect(toolNames).not.toContain('open_record_in_full_editor');
});

it('looks up a record website URL through the host callback', async () => {
const previewCall = {
type: 'function_call',
id: 'function-preview',
call_id: 'call-preview',
name: 'get_record_preview_links',
arguments: JSON.stringify({
item_id: 'item-1',
locale: null,
}),
status: 'completed',
} satisfies ResponseFunctionToolCall;
const client = new QueueResponsesClient([
eventsFor(response('resp-preview', [previewCall])),
eventsFor(response('resp-preview-done'), ['Here is the link.']),
]);
const getRecordPreviewLinks = vi.fn().mockResolvedValue({
source: 'web_previews',
links: [
{
label: 'Published',
url: 'https://example.com/thanksgiving',
},
],
});
const runtime = runtimeWith(client, { getRecordPreviewLinks });

await drain(runtime.streamTurn({ message: 'What is the page URL?' }));

expect(getRecordPreviewLinks.mock.calls[0]?.[0]).toEqual({
itemId: 'item-1',
});
expect(JSON.stringify(client.requests[1]?.input)).toContain(
'https://example.com/thanksgiving',
);
});

it('injects host context once and keeps continuations on the stored response chain', async () => {
Expand Down
Loading