diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/contract.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/contract.ts index 317a5923c52bf..e9660b08d6db4 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/contract.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/contract.ts @@ -5,9 +5,51 @@ * 2.0. */ +import type { ReactNode } from 'react'; import type { IconType } from '@elastic/eui'; import type { UnknownAttachment, AttachmentVersion } from '@kbn/agent-builder-common/attachments'; +export enum ActionButtonType { + PRIMARY = 'primary', + SECONDARY = 'secondary', + OVERFLOW = 'overflow', +} +/** + * Props passed to custom attachment content renderers. + */ +export interface AttachmentRenderProps { + /** The attachment to render */ + attachment: TAttachment; + /** Whether the attachment is being rendered in a sidebar context */ + isSidebar: boolean; +} + +/** + * Parameters passed when requesting action buttons for an inline-rendered attachment. + */ +export interface GetActionButtonsParams { + /** The attachment for which to provide action buttons */ + attachment: TAttachment; + /** Whether the attachment is being rendered in a sidebar context */ + isSidebar: boolean; + /** Function to update the attachment's origin reference */ + updateOrigin: (originId: string) => Promise; +} + +/** + * Action button definition for inline-rendered attachments. + */ +export interface ActionButton { + /** Button label text */ + label: string; + /** Optional icon to display in the button (EUI icon name or custom React element) */ + icon?: IconType; + /** Whether this is the primary action button */ + type: ActionButtonType; + /** Handler function called when the button is clicked */ + handler: () => void | Promise; +} + /** * UI definition for rendering attachments of a specific type. */ @@ -25,6 +67,17 @@ export interface AttachmentUIDefinition void; + /** + * Optional custom content renderer for inline attachment display. + * When provided, attachments can be rendered inline in the conversation + * using the tag. + */ + renderContent?: (props: AttachmentRenderProps) => ReactNode; + /** + * Optional function to provide action buttons for inline-rendered attachments. + * Buttons will appear alongside or below the rendered content. + */ + getActionButtons?: (params: GetActionButtonsParams) => ActionButton[]; } /** diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/index.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/index.ts index 6ea45d951d120..b590a82158ed3 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/index.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-browser/attachments/index.ts @@ -5,4 +5,11 @@ * 2.0. */ -export type { AttachmentUIDefinition, AttachmentServiceStartContract } from './contract'; +export type { + AttachmentUIDefinition, + AttachmentServiceStartContract, + AttachmentRenderProps, + GetActionButtonsParams, + ActionButton, +} from './contract'; +export { ActionButtonType } from './contract'; diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-common/tools/custom_rendering.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-common/tools/custom_rendering.ts index 69c2a435d3dcf..9d92d6c28ae18 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-common/tools/custom_rendering.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-common/tools/custom_rendering.ts @@ -30,3 +30,16 @@ export const dashboardElement = { toolResultId: 'tool-result-id', }, }; + +export interface RenderAttachmentElementAttributes { + attachmentId?: string; + version?: number | string; +} + +export const renderAttachmentElement = { + tagName: 'render_attachment', + attributes: { + attachmentId: 'id', + version: 'version', + }, +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/conversation_rounds.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/conversation_rounds.tsx index 33903ee8cc338..64d609dcaeefd 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/conversation_rounds.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/conversation_rounds.tsx @@ -41,6 +41,7 @@ export const ConversationRounds: React.FC = ({ scrollContainerHeight={scrollContainerHeight} isCurrentRound={isCurrentRound} rawRound={round} + conversationId={conversation?.id} conversationAttachments={conversation?.attachments} /> ); diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_layout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_layout.tsx index 94fa0c5a2e841..50d60be766738 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_layout.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_layout.tsx @@ -27,6 +27,7 @@ interface RoundLayoutProps { scrollContainerHeight: number; rawRound: ConversationRound; conversationAttachments?: VersionedAttachment[]; + conversationId?: string; } const labels = { @@ -40,6 +41,7 @@ export const RoundLayout: React.FC = ({ scrollContainerHeight, rawRound, conversationAttachments, + conversationId, }) => { const [roundContainerMinHeight, setRoundContainerMinHeight] = useState(0); const [hasBeenLoading, setHasBeenLoading] = useState(false); @@ -153,6 +155,9 @@ export const RoundLayout: React.FC = ({ steps={steps} isLoading={isLoadingCurrentRound} isLastRound={isCurrentRound} + conversationAttachments={conversationAttachments} + attachmentRefs={input.attachment_refs} + conversationId={conversationId} /> diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/attachment_with_actions.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/attachment_with_actions.tsx new file mode 100644 index 0000000000000..09e586ed0db21 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/attachment_with_actions.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { UnknownAttachment } from '@kbn/agent-builder-common/attachments'; +import { EuiButton } from '@elastic/eui'; +import { AGENT_BUILDER_EXPERIMENTAL_FEATURES_SETTING_ID } from '@kbn/management-settings-ids'; +import type { AttachmentsService } from '../../../../../services/attachments/attachements_service'; +import { useKibana } from '../../../../hooks/use_kibana'; + +interface AttachmentWithActionsProps { + attachment: UnknownAttachment; + attachmentsService: AttachmentsService; + isSidebar: boolean; + conversationId: string; +} + +/** + * Component that renders an attachment with its action buttons. + */ +export const AttachmentWithActions: React.FC = ({ + attachment, + attachmentsService, + isSidebar, + conversationId, +}) => { + const { + services: { settings }, + } = useKibana(); + const isExperimentalFeaturesEnabled = settings?.client.get( + AGENT_BUILDER_EXPERIMENTAL_FEATURES_SETTING_ID, + false + ); + + if (isExperimentalFeaturesEnabled === false) { + return null; + } + + const uiDefinition = attachmentsService.getAttachmentUiDefinition(attachment.type); + + if (!uiDefinition) { + return null; + } + + const actionButtons = uiDefinition.getActionButtons?.({ + attachment, + isSidebar, + updateOrigin: async (originId: string) => { + // TODO: Implement updateOrigin + // attachmentsService.updateOrigin(conversationId, attachment.id, originId); + }, + }); + + return ( + <> + {actionButtons?.map((button) => ( + + {button.label} + + ))} + {uiDefinition?.renderContent?.({ attachment, isSidebar })} + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/chat_message_text.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/chat_message_text.tsx index f7af6becc845b..a1fffcd62c501 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/chat_message_text.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/chat_message_text.tsx @@ -22,7 +22,14 @@ import { } from '@elastic/eui'; import { type PluggableList } from 'unified'; import type { ConversationRoundStep } from '@kbn/agent-builder-common'; -import { visualizationElement } from '@kbn/agent-builder-common/tools/custom_rendering'; +import type { + VersionedAttachment, + AttachmentVersionRef, +} from '@kbn/agent-builder-common/attachments'; +import { + visualizationElement, + renderAttachmentElement, +} from '@kbn/agent-builder-common/tools/custom_rendering'; import { useAgentBuilderServices } from '../../../../hooks/use_agent_builder_service'; import { Cursor, @@ -30,19 +37,31 @@ import { createVisualizationRenderer, loadingCursorPlugin, visualizationTagParser, + renderAttachmentTagParser, + createRenderAttachmentRenderer, } from './markdown_plugins'; import { useStepsFromPrevRounds } from '../../../../hooks/use_conversation'; +import { useConversationContext } from '../../../../context/conversation/conversation_context'; interface Props { content: string; steps: ConversationRoundStep[]; + conversationAttachments?: VersionedAttachment[]; + attachmentRefs?: AttachmentVersionRef[]; + conversationId?: string; } /** * Component handling markdown support to the assistant's responses. * Also handles "loading" state by appending the blinking cursor. */ -export function ChatMessageText({ content, steps: stepsFromCurrentRound }: Props) { +export function ChatMessageText({ + content, + steps: stepsFromCurrentRound, + conversationAttachments, + attachmentRefs, + conversationId, +}: Props) { const { euiTheme } = useEuiTheme(); const containerClassName = css` @@ -58,8 +77,9 @@ export function ChatMessageText({ content, steps: stepsFromCurrentRound }: Props } `; - const { startDependencies } = useAgentBuilderServices(); + const { attachmentsService, startDependencies } = useAgentBuilderServices(); const stepsFromPrevRounds = useStepsFromPrevRounds(); + const { isEmbeddedContext: isSidebar } = useConversationContext(); const { parsingPluginList, processingPluginList } = useMemo(() => { const parsingPlugins = getDefaultEuiMarkdownParsingPlugins(); @@ -125,6 +145,13 @@ export function ChatMessageText({ content, steps: stepsFromCurrentRound }: Props stepsFromCurrentRound, stepsFromPrevRounds, }), + [renderAttachmentElement.tagName]: createRenderAttachmentRenderer({ + conversationAttachments, + attachmentRefs, + conversationId, + isSidebar, + attachmentsService, + }), }; return { @@ -132,11 +159,21 @@ export function ChatMessageText({ content, steps: stepsFromCurrentRound }: Props loadingCursorPlugin, esqlLanguagePlugin, visualizationTagParser, + renderAttachmentTagParser, ...parsingPlugins, ], processingPluginList: processingPlugins, }; - }, [startDependencies, stepsFromCurrentRound, stepsFromPrevRounds]); + }, [ + startDependencies, + stepsFromCurrentRound, + stepsFromPrevRounds, + conversationAttachments, + attachmentRefs, + conversationId, + isSidebar, + attachmentsService, + ]); return ( diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/index.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/index.ts index 6af1102f2ec3b..c87c98c4ff7f5 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/index.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/index.ts @@ -7,5 +7,9 @@ export { findToolResult, type MutableNode } from './utils'; export { visualizationTagParser, createVisualizationRenderer } from './visualization_plugin'; +export { + renderAttachmentTagParser, + createRenderAttachmentRenderer, +} from './render_attachment_plugin'; export { loadingCursorPlugin, Cursor } from './cursor_plugin'; export { esqlLanguagePlugin } from './esql_plugin'; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/render_attachment_plugin.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/render_attachment_plugin.tsx new file mode 100644 index 0000000000000..bff467bf2d180 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/render_attachment_plugin.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { + VersionedAttachment, + AttachmentVersionRef, +} from '@kbn/agent-builder-common/attachments'; +import { + renderAttachmentElement, + type RenderAttachmentElementAttributes, +} from '@kbn/agent-builder-common/tools/custom_rendering'; +import type { AttachmentsService } from '../../../../../../services'; +import { createTagParser } from './utils'; +import { AttachmentWithActions } from '../attachment_with_actions'; + +/** + * Parser for tags in markdown. + * Converts HTML/text nodes containing render_attachment tags into structured AST nodes. + */ +export const renderAttachmentTagParser = createTagParser({ + tagName: renderAttachmentElement.tagName, + getAttributes: (value, extractAttr) => ({ + attachmentId: extractAttr(value, renderAttachmentElement.attributes.attachmentId), + version: extractAttr(value, renderAttachmentElement.attributes.version), + }), + assignAttributes: (node, attributes) => { + node.type = renderAttachmentElement.tagName; + node.attachmentId = attributes.attachmentId; + node.attachmentVersion = attributes.version; + delete node.value; + }, + createNode: (attributes, position) => ({ + type: renderAttachmentElement.tagName, + attachmentId: attributes.attachmentId, + attachmentVersion: attributes.version, + position, + }), +}); + +interface RenderAttachmentRendererProps { + attachmentsService: AttachmentsService; + conversationAttachments?: VersionedAttachment[]; + attachmentRefs?: AttachmentVersionRef[]; + conversationId?: string; + isSidebar: boolean; +} +/** + * Creates a renderer for tags. + */ +export const createRenderAttachmentRenderer = ({ + attachmentsService, + conversationAttachments, + attachmentRefs, + conversationId, + isSidebar, +}: RenderAttachmentRendererProps) => { + return (props: RenderAttachmentElementAttributes) => { + const { attachmentId, version: explicitVersion } = props; + + if (!attachmentId || !conversationId) { + return null; + } + + const attachment = conversationAttachments?.find((att) => att.id === attachmentId); + + if (!attachment) { + return null; + } + + // Resolve version: explicit > from refs > current_version + let versionToUse: number; + if (explicitVersion !== undefined) { + versionToUse = + typeof explicitVersion === 'string' ? parseInt(explicitVersion, 10) : explicitVersion; + } else { + const refVersion = attachmentRefs?.find((r) => r.attachment_id === attachmentId)?.version; + versionToUse = refVersion ?? attachment.current_version; + } + + const versionData = attachment.versions.find((v) => v.version === versionToUse); + + if (!versionData) { + return null; + } + + return ( + + ); + }; +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/utils.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/utils.ts index 052cd1f6f1aea..1200cb09f6677 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/utils.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/markdown_plugins/utils.ts @@ -13,6 +13,8 @@ export type MutableNode = Node & { value?: string; toolResultId?: string; chartType?: string; + attachmentId?: string; + attachmentVersion?: string; }; export const createTagParser = >(config: { @@ -40,8 +42,8 @@ export const createTagParser = >(co visitParent(child as Parent); } - if (child.type !== 'html') { - continue; // terminate iteration if not html node + if (child.type !== 'html' && child.type !== 'text') { + continue; // terminate iteration if not html/text node } const rawValue = child.value; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/round_response.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/round_response.tsx index d23378a537df2..cb452e467dd4e 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/round_response.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/round_response.tsx @@ -9,6 +9,10 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import type { AssistantResponse, ConversationRoundStep } from '@kbn/agent-builder-common'; +import type { + VersionedAttachment, + AttachmentVersionRef, +} from '@kbn/agent-builder-common/attachments'; import React from 'react'; import { StreamingText } from './streaming_text'; import { ChatMessageText } from './chat_message_text'; @@ -20,6 +24,9 @@ export interface RoundResponseProps { isLoading: boolean; hasError: boolean; isLastRound: boolean; + conversationAttachments?: VersionedAttachment[]; + attachmentRefs?: AttachmentVersionRef[]; + conversationId?: string; } export const RoundResponse: React.FC = ({ @@ -28,6 +35,9 @@ export const RoundResponse: React.FC = ({ steps, isLoading, isLastRound, + conversationAttachments, + attachmentRefs, + conversationId, }) => ( = ({ > {isLoading ? ( - + ) : ( - + )} {!isLoading && !hasError && ( diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/streaming_text.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/streaming_text.tsx index 5272fd1bc170c..b0199b217e185 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/streaming_text.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_rounds/round_response/streaming_text.tsx @@ -7,6 +7,10 @@ import React, { useEffect, useRef, useState } from 'react'; import type { ConversationRoundStep } from '@kbn/agent-builder-common'; +import type { + VersionedAttachment, + AttachmentVersionRef, +} from '@kbn/agent-builder-common/attachments'; import { ChatMessageText } from './chat_message_text'; const TOKEN_DELAY = 17; @@ -14,9 +18,19 @@ interface StreamingTextProps { content: string; steps: ConversationRoundStep[]; tokenDelay?: number; // ms between tokens. Defaults to 17ms to ensure 60fps. + conversationAttachments?: VersionedAttachment[]; + attachmentRefs?: AttachmentVersionRef[]; + conversationId?: string; } -export const StreamingText = ({ content, steps, tokenDelay = TOKEN_DELAY }: StreamingTextProps) => { +export const StreamingText = ({ + content, + steps, + tokenDelay = TOKEN_DELAY, + conversationAttachments, + attachmentRefs, + conversationId, +}: StreamingTextProps) => { const [displayedText, setDisplayedText] = useState(''); const tokenQueueRef = useRef([]); const intervalRef = useRef(null); @@ -56,5 +70,13 @@ export const StreamingText = ({ content, steps, tokenDelay = TOKEN_DELAY }: Stre }; }, [content, tokenDelay]); - return ; + return ( + + ); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/answer_agent.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/answer_agent.ts index d4503d013cf4e..54f47cf0bfed7 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/answer_agent.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/answer_agent.ts @@ -13,6 +13,7 @@ import { formatDate } from './utils/helpers'; import { customInstructionsBlock } from './utils/custom_instructions'; import { formatResearcherActionHistory, formatAnswerActionHistory } from './utils/actions'; import { renderVisualizationPrompt } from './utils/visualizations'; +import { renderAttachmentPrompt } from './utils/attachment_rendering'; import { attachmentTypeInstructions } from './utils/attachments'; import type { PromptFactoryParams, AnswerAgentPromptRuntimeParams } from './types'; @@ -79,7 +80,9 @@ ${attachmentTypeInstructions(attachmentTypes)} ## CUSTOM RENDERING -${visEnabled ? renderVisualizationPrompt() : 'No custom renderers available'} +${visEnabled ? renderVisualizationPrompt() : ''} + +${renderAttachmentPrompt()} ## ADDITIONAL INFO - Current date: ${formatDate(conversationTimestamp)} @@ -149,7 +152,9 @@ ${attachmentTypeInstructions(attachmentTypes)} ## CUSTOM RENDERING -${visEnabled ? renderVisualizationPrompt() : 'No custom renderers available'} +${visEnabled ? renderVisualizationPrompt() : ''} + +${renderAttachmentPrompt()} ## ADDITIONAL INFO - Current date: ${formatDate(conversationTimestamp)} diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/utils/attachment_rendering.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/utils/attachment_rendering.ts new file mode 100644 index 0000000000000..ba66f11ec9b92 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/agents/modes/default/prompts/utils/attachment_rendering.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderAttachmentElement } from '@kbn/agent-builder-common/tools/custom_rendering'; + +export const renderAttachmentPrompt = () => { + const { tagName, attributes } = renderAttachmentElement; + + return `### RENDERING ATTACHMENTS (from conversation storage) +When you want to render a stored attachment in the UI, emit a custom XML element: + +<${tagName} ${attributes.attachmentId}="ATTACHMENT_ID" ${attributes.version}="VERSION" /> + +**When to use this:** +Use \`<${tagName}>\` to render attachments that are stored in the conversation. These appear in the \`\` block or were created by tools like \`attachment_add\`. + +**When NOT to use this:** +Do NOT use \`<${tagName}>\` for tool results that just returned tabular data. Use \`\` instead for those. + +**Where to find attachment IDs:** +Attachments in the conversation are listed in the \`\` block. Each attachment has an \`id\` and \`version\` attribute that you can reference. + +**Example:** +If the conversation contains: +\`\`\`xml + + + Query content... + + +\`\`\` + +And the user asks to see the ESQL query, your response should include: +<${tagName} ${attributes.attachmentId}="my-esql-query" ${attributes.version}="1" /> + +**Rules** +* Only use to render existing attachments by their id +* Copy the attachment id and version verbatim from the \`\` block or from tool results that created attachments +* The ${attributes.version} attribute is optional - if omitted, the system will use the version from when the round was created +* Do not invent, alter, or guess attachment ids or versions +* Never wrap in backticks or code blocks`; +};