diff --git a/src/components/Layout/LanguageSelector.tsx b/src/components/Layout/LanguageSelector.tsx index 00646c0471..1c8c65b8a9 100644 --- a/src/components/Layout/LanguageSelector.tsx +++ b/src/components/Layout/LanguageSelector.tsx @@ -20,7 +20,7 @@ type LanguageSelectorOptionData = { version: string; }; -export const LanguageSelector = () => { +const SingleLanguageSelector = () => { const { activePage } = useLayoutContext(); const location = useLocation(); const languageVersions = languageData[activePage.product ?? 'pubsub']; @@ -56,7 +56,9 @@ export const LanguageSelector = () => { language: option.label, location: location.pathname, }); - navigate(`${location.pathname}?lang=${option.label}`); + const params = new URLSearchParams(location.search); + params.set('lang', option.label); + navigate(`${location.pathname}?${params.toString()}`); } }; @@ -163,3 +165,191 @@ export const LanguageSelector = () => { ); }; + +type DualLanguageDropdownProps = { + label: string; + paramName: 'client_lang' | 'agent_lang'; + languages: LanguageKey[]; + selectedLanguage: LanguageKey | undefined; +}; + +const DualLanguageDropdown = ({ label, paramName, languages, selectedLanguage }: DualLanguageDropdownProps) => { + const { activePage } = useLayoutContext(); + const location = useLocation(); + const languageVersions = languageData[activePage.product ?? 'aiTransport']; + + const options: LanguageSelectorOptionData[] = useMemo( + () => + Object.entries(languageVersions) + .map(([lang, version]) => ({ + label: lang as LanguageKey, + value: `${lang}-${version}`, + version, + })) + .filter((option) => languages.includes(option.label)), + [languages, languageVersions], + ); + + const [value, setValue] = useState(''); + + useEffect(() => { + const defaultOption = options.find((option) => option.label === selectedLanguage) || options[0]; + if (defaultOption) { + setValue(defaultOption.value); + } + }, [selectedLanguage, options]); + + const selectedOption = useMemo(() => options.find((option) => option.value === value), [options, value]); + + const handleValueChange = (newValue: string) => { + setValue(newValue); + + const option = options.find((opt) => opt.value === newValue); + if (option) { + track('language_selector_changed', { + language: option.label, + type: paramName, + location: location.pathname, + }); + + // Preserve existing URL params and update the relevant one + const params = new URLSearchParams(location.search); + params.set(paramName, option.label); + navigate(`${location.pathname}?${params.toString()}`); + } + }; + + if (!selectedOption) { + return ; + } + + const selectedLang = languageInfo[selectedOption.label]; + + return ( +
+ {label} + + 1 ? 'cursor-pointer' : 'cursor-auto', + )} + style={{ height: LANGUAGE_SELECTOR_HEIGHT }} + aria-label={`Select ${label.toLowerCase()} language`} + disabled={options.length === 1} + > +
+ + {selectedLang?.label} + + v{selectedOption.version} + +
+ {options.length > 1 && ( + + + + )} +
+ + + + +

{label}

+ {options.map((option) => { + const lang = languageInfo[option.label]; + return ( + +
+ + {lang?.label} +
+ + v{option.version} + + {option.value === value ? ( + + + + ) : ( +
+ )} + + ); + })} + + + + +
+ ); +}; + +const DualLanguageSelector = () => { + const { activePage } = useLayoutContext(); + + return ( +
+ + +
+ ); +}; + +// Main export - renders appropriate selector based on page type +export const LanguageSelector = () => { + const { activePage } = useLayoutContext(); + + if (activePage.isDualLanguage) { + return ; + } + + return ; +}; diff --git a/src/components/Layout/LeftSidebar.tsx b/src/components/Layout/LeftSidebar.tsx index 0574666f77..5c646e6dbe 100644 --- a/src/components/Layout/LeftSidebar.tsx +++ b/src/components/Layout/LeftSidebar.tsx @@ -11,6 +11,28 @@ import Link from '../Link'; import { useLayoutContext } from 'src/contexts/layout-context'; import { interactiveButtonClassName } from './utils/styles'; +// Build link preserving all language params across navigation +const buildLinkWithParams = (targetLink: string, searchParams: URLSearchParams): string => { + const params = new URLSearchParams(); + + const lang = searchParams.get('lang'); + const clientLang = searchParams.get('client_lang'); + const agentLang = searchParams.get('agent_lang'); + + if (lang) { + params.set('lang', lang); + } + if (clientLang) { + params.set('client_lang', clientLang); + } + if (agentLang) { + params.set('agent_lang', agentLang); + } + + const paramString = params.toString(); + return paramString ? `${targetLink}?${paramString}` : targetLink; +}; + type LeftSidebarProps = { className?: string; inHeader?: boolean; @@ -78,7 +100,7 @@ const ChildAccordion = ({ content, tree }: { content: (NavProductPage | NavProdu } }, [activePage.tree.length, subtreeIdentifier]); - const lang = new URLSearchParams(location.search).get('lang'); + const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]); return ( {page.name} {page.external && ( diff --git a/src/components/Layout/MDXWrapper.tsx b/src/components/Layout/MDXWrapper.tsx index caf5c5468d..8a1fddce6c 100644 --- a/src/components/Layout/MDXWrapper.tsx +++ b/src/components/Layout/MDXWrapper.tsx @@ -43,8 +43,8 @@ type MDXWrapperProps = PageProps; // Create SDK Context type SDKContextType = { - sdk: SDKType; - setSdk: (sdk: SDKType) => void; + sdk: SDKType | undefined; + setSdk: (sdk: SDKType | undefined) => void; }; type Replacement = { @@ -104,57 +104,109 @@ const WrappedCodeSnippet: React.FC<{ activePage: ActivePage } & CodeSnippetProps return processChild(children); }, [children, replacements]); - // Check if this code block contains only a single utility language - const utilityLanguageOverride = useMemo(() => { + // Detect code block type (client_, agent_, utility, or standard) + const { languageOverride, detectedSdkType } = useMemo(() => { // Utility languages that should be shown without warning (like JSON) - const UTILITY_LANGUAGES = ['html', 'xml', 'css', 'sql', 'json']; + const UTILITY_LANGUAGES = ['html', 'xml', 'css', 'sql', 'json', 'shell', 'text']; - const childrenArray = React.Children.toArray(processedChildren); + // Helper to extract language from className + const extractLangFromClassName = (className: string | undefined): string | null => { + if (!className) { + return null; + } + const langMatch = className.match(/language-(\S+)/); + return langMatch ? langMatch[1] : null; + }; + + // Recursively find all language classes in children + const findLanguages = (node: ReactNode): string[] => { + const languages: string[] = []; + + React.Children.forEach(node, (child) => { + if (!isValidElement(child)) { + return; + } + + const element = child as ReactElement; + const props = element.props || {}; + + // Check className on this element + const lang = extractLangFromClassName(props.className); + if (lang) { + languages.push(lang); + } + + // Recursively check children + if (props.children) { + languages.push(...findLanguages(props.children)); + } + }); - // Check if this is a single child with a utility language - if (childrenArray.length !== 1) { - return null; + return languages; + }; + + const languages = findLanguages(processedChildren); + + // Check for client_/agent_ prefixes + const hasClientPrefix = languages.some((lang) => lang.startsWith('client_')); + const hasAgentPrefix = languages.some((lang) => lang.startsWith('agent_')); + + if (hasClientPrefix && activePage.isDualLanguage) { + return { languageOverride: activePage.clientLanguage, detectedSdkType: 'client' as SDKType }; } - const child = childrenArray[0]; - if (!isValidElement(child)) { - return null; + if (hasAgentPrefix && activePage.isDualLanguage) { + return { languageOverride: activePage.agentLanguage, detectedSdkType: 'agent' as SDKType }; } - const preElement = child as ReactElement; - const codeElement = isValidElement(preElement.props?.children) - ? (preElement.props.children as ReactElement) - : null; + // Check for single utility language (existing logic) + if (languages.length === 1 && UTILITY_LANGUAGES.includes(languages[0])) { + return { languageOverride: languages[0], detectedSdkType: undefined }; + } - if (!codeElement || !codeElement.props.className) { - return null; + return { languageOverride: undefined, detectedSdkType: undefined }; + }, [processedChildren, activePage.isDualLanguage, activePage.clientLanguage, activePage.agentLanguage]); + + const handleLanguageChange = (lang: string, newSdk: SDKType | undefined) => { + if (detectedSdkType === 'client' || detectedSdkType === 'agent') { + // Update the corresponding URL param so the page-level selector stays in sync + const paramKey = detectedSdkType === 'client' ? 'client_lang' : 'agent_lang'; + const params = new URLSearchParams(location.search); + params.set(paramKey, lang); + navigate(`${location.pathname}?${params.toString()}`); + return; } - const className = codeElement.props.className as string; - const langMatch = className.match(/language-(\w+)/); - const lang = langMatch ? langMatch[1] : null; + if (!detectedSdkType) { + setSdk(newSdk ?? undefined); + } + navigate(`${location.pathname}?lang=${lang}`); + }; - // If it's a utility language, return the language to use as override - return lang && UTILITY_LANGUAGES.includes(lang) ? lang : null; - }, [processedChildren]); + const sdkLabel = detectedSdkType === 'client' ? 'Client' : detectedSdkType === 'agent' ? 'Agent' : null; return ( - { - setSdk(sdk ?? null); - navigate(`${location.pathname}?lang=${lang}`); - }} - className={cn(props.className, 'mb-5')} - languageOrdering={ - activePage.product && languageData[activePage.product] ? Object.keys(languageData[activePage.product]) : [] - } - apiKeys={apiKeys} - > - {processedChildren} - +
+ {sdkLabel && ( + + {sdkLabel} + + )} + + {processedChildren} + +
); }; @@ -168,11 +220,11 @@ const MDXWrapper: React.FC = ({ children, pageContext, location const { frontmatter } = pageContext; const { activePage } = useLayoutContext(); - const [sdk, setSdk] = useState( + const [sdk, setSdk] = useState( (pageContext.languages ?.filter((language) => language.startsWith('realtime') || language.startsWith('rest')) ?.find((language) => activePage.language && language.endsWith(activePage.language)) - ?.split('_')[0] as SDKType) ?? null, + ?.split('_')[0] as SDKType) ?? undefined, ); const userContext = useContext(UserContext); diff --git a/src/components/Layout/mdx/If.tsx b/src/components/Layout/mdx/If.tsx index ada93c3dee..8c78bca3e2 100644 --- a/src/components/Layout/mdx/If.tsx +++ b/src/components/Layout/mdx/If.tsx @@ -5,23 +5,44 @@ import UserContext from 'src/contexts/user-context'; interface IfProps { lang?: LanguageKey; + clientLang?: LanguageKey; + agentLang?: LanguageKey; loggedIn?: boolean; className?: string; children: React.ReactNode; as?: React.ElementType; } -const If: React.FC = ({ lang, loggedIn, children }) => { +const If: React.FC = ({ lang, clientLang, agentLang, loggedIn, children }) => { const { activePage } = useLayoutContext(); - const { language } = activePage; + const { language, isDualLanguage, clientLanguage, agentLanguage } = activePage; const userContext = useContext(UserContext); let shouldShow = true; // Check language condition if lang prop is provided - if (lang !== undefined && language) { + if (lang !== undefined) { const splitLang = lang.split(','); - shouldShow = shouldShow && splitLang.includes(language); + if (isDualLanguage) { + // On dual-language pages, check if either client or agent matches + const clientMatches = clientLanguage && splitLang.includes(clientLanguage); + const agentMatches = agentLanguage && splitLang.includes(agentLanguage); + shouldShow = shouldShow && !!(clientMatches || agentMatches); + } else if (language) { + shouldShow = shouldShow && splitLang.includes(language); + } + } + + // Check client language condition if clientLang prop is provided + if (clientLang !== undefined && clientLanguage) { + const splitLang = clientLang.split(','); + shouldShow = shouldShow && splitLang.includes(clientLanguage); + } + + // Check agent language condition if agentLang prop is provided + if (agentLang !== undefined && agentLanguage) { + const splitLang = agentLang.split(','); + shouldShow = shouldShow && splitLang.includes(agentLanguage); } // Check logged in condition if loggedIn prop is provided diff --git a/src/components/Layout/mdx/PageHeader.tsx b/src/components/Layout/mdx/PageHeader.tsx index 19b5eb92f6..9b6c083400 100644 --- a/src/components/Layout/mdx/PageHeader.tsx +++ b/src/components/Layout/mdx/PageHeader.tsx @@ -39,11 +39,14 @@ export const PageHeader: React.FC = ({ title, intro }) => { const showLanguageSelector = useMemo( () => - activePage.languages.length > 0 && - !activePage.languages.every( - (language) => !Object.keys(languageData[product as ProductKey] ?? {}).includes(language), - ), - [activePage.languages, product], + // Always show for dual language pages (AI Transport guides) + activePage.isDualLanguage || + // Standard logic: show if languages exist and at least one is in languageData + (activePage.languages.length > 0 && + !activePage.languages.every( + (language) => !Object.keys(languageData[product as ProductKey] ?? {}).includes(language), + )), + [activePage.languages, product, activePage.isDualLanguage], ); useEffect(() => { diff --git a/src/components/Layout/utils/nav.ts b/src/components/Layout/utils/nav.ts index 3441a1c267..5ecb242a25 100644 --- a/src/components/Layout/utils/nav.ts +++ b/src/components/Layout/utils/nav.ts @@ -14,6 +14,12 @@ export type ActivePage = { language: LanguageKey | null; product: ProductKey | null; template: PageTemplate; + // Dual language support for AI Transport guides + clientLanguage?: LanguageKey; + agentLanguage?: LanguageKey; + clientLanguages?: LanguageKey[]; + agentLanguages?: LanguageKey[]; + isDualLanguage?: boolean; }; /** diff --git a/src/components/Markdown/MarkdownProvider.tsx b/src/components/Markdown/MarkdownProvider.tsx index 6241e64346..82b0a9e8ae 100644 --- a/src/components/Markdown/MarkdownProvider.tsx +++ b/src/components/Markdown/MarkdownProvider.tsx @@ -69,14 +69,28 @@ export const Anchor: FC = ({ children, href, ...prop cleanHref = href.slice(brokenAssetPrefix.length); } - // Add lang param from current URL if available + // Add language params from current URL if available const urlParams = new URLSearchParams(location.search); - const langParam = urlParams.get('lang'); - if (langParam && cleanHref && checkLinkIsInternal(cleanHref)) { + if (cleanHref && checkLinkIsInternal(cleanHref)) { const url = new URL(cleanHref, 'https://ably.com'); - url.searchParams.set('lang', langParam); - cleanHref = url.pathname + url.search; + const langParam = urlParams.get('lang'); + const clientLang = urlParams.get('client_lang'); + const agentLang = urlParams.get('agent_lang'); + + if (langParam) { + url.searchParams.set('lang', langParam); + } + if (clientLang) { + url.searchParams.set('client_lang', clientLang); + } + if (agentLang) { + url.searchParams.set('agent_lang', agentLang); + } + + if (langParam || clientLang || agentLang) { + cleanHref = url.pathname + url.search; + } } return ( diff --git a/src/contexts/layout-context.tsx b/src/contexts/layout-context.tsx index a81e7a594a..115f50f5b7 100644 --- a/src/contexts/layout-context.tsx +++ b/src/contexts/layout-context.tsx @@ -16,6 +16,15 @@ import { ProductKey } from 'src/data/types'; export const DEFAULT_LANGUAGE = 'javascript'; +// Extract languages for a given prefix (e.g. 'client_' or 'agent_') from raw page languages +const extractPrefixedLanguages = (rawLanguages: string[], prefix: string): LanguageKey[] => + rawLanguages.filter((lang) => lang.startsWith(prefix)).map((lang) => lang.replace(prefix, '') as LanguageKey); + +// Check if page content has client_/agent_ prefixed languages +const hasDualLanguageContent = (languages: string[]): boolean => { + return languages.some((lang) => lang.startsWith('client_') || lang.startsWith('agent_')); +}; + const LayoutContext = createContext<{ activePage: ActivePage; }>({ @@ -26,6 +35,11 @@ const LayoutContext = createContext<{ language: DEFAULT_LANGUAGE, product: null, template: null, + clientLanguage: undefined, + agentLanguage: undefined, + clientLanguages: [], + agentLanguages: [], + isDualLanguage: false, }, }); @@ -47,6 +61,34 @@ const determineActiveLanguage = ( return DEFAULT_LANGUAGE; }; +// Determine client language for dual-language pages +const determineClientLanguage = (location: string, validLanguages: LanguageKey[]): LanguageKey => { + const params = new URLSearchParams(location); + const clientLangParam = params.get('client_lang') as LanguageKey; + + if (clientLangParam && validLanguages.includes(clientLangParam)) { + return clientLangParam; + } + + return validLanguages.includes(DEFAULT_LANGUAGE as LanguageKey) + ? DEFAULT_LANGUAGE + : (validLanguages[0] ?? DEFAULT_LANGUAGE); +}; + +// Determine agent language for dual-language pages +const determineAgentLanguage = (location: string, validLanguages: LanguageKey[]): LanguageKey => { + const params = new URLSearchParams(location); + const agentLangParam = params.get('agent_lang') as LanguageKey; + + if (agentLangParam && validLanguages.includes(agentLangParam)) { + return agentLangParam; + } + + return validLanguages.includes(DEFAULT_LANGUAGE as LanguageKey) + ? DEFAULT_LANGUAGE + : (validLanguages[0] ?? DEFAULT_LANGUAGE); +}; + export const LayoutProvider: React.FC> = ({ children, pageContext, @@ -65,6 +107,16 @@ export const LayoutProvider: React.FC -To follow this guide, you need: -- Node.js 20 or higher + +Node.js 20 or higher is required. + + +Python 3.8 or higher is required. + + +Java 8 or higher is required. + + +Xcode 15 or higher is required. + + +You also need: - An Anthropic API key - An Ably API key Useful links: - [Anthropic API documentation](https://docs.anthropic.com/en/api) -- [Ably JavaScript SDK getting started](/docs/getting-started/javascript) +- [Token streaming overview](/docs/ai-transport/token-streaming) +- [AI Transport overview](/docs/ai-transport) -Create a new NPM package, which will contain the publisher and subscriber code: +### Agent setup + + +Create a new npm package for the agent (publisher) code: ```shell -mkdir ably-anthropic-example && cd ably-anthropic-example +mkdir ably-anthropic-agent && cd ably-anthropic-agent npm init -y +npm install @anthropic-ai/sdk ably ``` + -Install the required packages using NPM: + +Create a new directory and install the required packages: ```shell -npm install @anthropic-ai/sdk@^0.71 ably@^2 +mkdir ably-anthropic-agent && cd ably-anthropic-agent +pip install anthropic ably ``` + - + +Create a new project and add the required dependencies. + +For Maven, add to your `pom.xml`: + + +```xml + + + com.anthropic + anthropic-java + 2.15.0 + + + io.ably + ably-java + 1.6.1 + + +``` + + +For Gradle, add to your `build.gradle`: + + +```text +dependencies { + implementation 'com.anthropic:anthropic-java:2.15.0' + implementation 'io.ably:ably-java:1.6.1' +} +``` + + -Export your Anthropic API key to the environment, which will be used later in the guide by the Anthropic SDK: +Export your Anthropic API key to the environment: ```shell @@ -54,6 +105,66 @@ export ANTHROPIC_API_KEY="your_api_key_here" ``` +### Client setup + + +Create a new npm package for the client (subscriber) code, or use the same project as the agent if both are JavaScript: + + +```shell +mkdir ably-anthropic-client && cd ably-anthropic-client +npm init -y +npm install ably +``` + + + + +Add the Ably SDK to your iOS or macOS project using Swift Package Manager. In Xcode, go to File > Add Package Dependencies and add: + + +```text +https://github.com/ably/ably-cocoa +``` + + +Or add it to your `Package.swift`: + + +```client_swift +dependencies: [ + .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0") +] +``` + + + + +Add the Ably Java SDK to your `pom.xml`: + + +```xml + + io.ably + ably-java + 1.6.1 + +``` + + +For Gradle, add to your `build.gradle`: + + +```text +implementation 'io.ably:ably-java:1.6.1' +``` + + + + + ## Step 1: Enable message appends Message append functionality requires "Message annotations, updates, deletes and appends" to be enabled in a [channel rule](/docs/channels#rules) associated with the channel. @@ -81,10 +192,15 @@ The `ai:` namespace is just a naming convention used in this guide. There's noth Initialize an Anthropic client and use the [Messages API](https://docs.anthropic.com/en/api/messages) to stream model output as a series of events. -Create a new file `publisher.mjs` with the following contents: + +In your `ably-anthropic-agent` directory, create a new file called `publisher.mjs``publisher.py` with the following contents: + + +In your agent project, create a new file called `Publisher.java` with the following contents: + -```javascript +```agent_javascript import Anthropic from '@anthropic-ai/sdk'; // Initialize Anthropic client @@ -114,6 +230,68 @@ async function streamAnthropicResponse(prompt) { // Usage example streamAnthropicResponse("Tell me a short joke"); ``` + +```agent_python +import asyncio +import anthropic + +# Initialize Anthropic client +client = anthropic.AsyncAnthropic() + +# Process each streaming event +async def process_event(event): + print(event) + # This function is updated in the next sections + +# Create streaming response from Anthropic +async def stream_anthropic_response(prompt: str): + async with client.messages.stream( + model="claude-sonnet-4-5", + max_tokens=1024, + messages=[{"role": "user", "content": prompt}], + ) as stream: + async for event in stream: + await process_event(event) + +# Usage example +asyncio.run(stream_anthropic_response("Tell me a short joke")) +``` + +```agent_java +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.okhttp.AnthropicOkHttpClient; +import com.anthropic.core.http.StreamResponse; +import com.anthropic.models.messages.*; + +public class Publisher { + // Initialize Anthropic client + private static final AnthropicClient client = AnthropicOkHttpClient.fromEnv(); + + // Process each streaming event + private static void processEvent(RawMessageStreamEvent event) { + System.out.println(event); + // This method is updated in the next sections + } + + // Create streaming response from Anthropic + public static void streamAnthropicResponse(String prompt) { + MessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_SONNET_4_5) + .maxTokens(1024) + .addUserMessage(prompt) + .build(); + + try (StreamResponse stream = + client.messages().createStreaming(params)) { + stream.stream().forEach(Publisher::processEvent); + } + } + + public static void main(String[] args) { + streamAnthropicResponse("Tell me a short joke"); + } +} +``` ### Understand Anthropic streaming events @@ -170,10 +348,10 @@ Each AI response is stored as a single Ably message that grows as tokens are app ### Initialize the Ably client -Add the Ably client initialization to your `publisher.mjs` file: +Add the Ably client initialization to your publisher file: -```javascript +```agent_javascript import Ably from 'ably'; // Initialize Ably Realtime client @@ -185,6 +363,30 @@ const realtime = new Ably.Realtime({ // Create a channel for publishing streamed AI responses const channel = realtime.channels.get('ai:{{RANDOM_CHANNEL_NAME}}'); ``` + +```agent_python +from ably import AblyRealtime + +# Initialize Ably Realtime client +realtime = AblyRealtime(key='{{API_KEY}}', transport_params={'echo': 'false'}) + +# Create a channel for publishing streamed AI responses +channel = realtime.channels.get('ai:{{RANDOM_CHANNEL_NAME}}') +``` + +```agent_java +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.realtime.Channel; +import io.ably.lib.types.ClientOptions; + +// Initialize Ably Realtime client +ClientOptions options = new ClientOptions("{{API_KEY}}"); +options.echoMessages = false; +AblyRealtime realtime = new AblyRealtime(options); + +// Create a channel for publishing streamed AI responses +Channel channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}"); +``` The Ably Realtime client maintains a persistent connection to the Ably service, which allows you to publish tokens at high message rates with low latency. @@ -201,10 +403,10 @@ When a new response begins, publish an initial message to create it. Ably assign This implementation assumes each response contains a single text content block. It filters out thinking tokens and other non-text content blocks. For production use cases with multiple content blocks or concurrent responses, consider tracking state per message ID and content block index. -Update your `publisher.mjs` file to publish the initial message and append tokens: +Update your publisher file to publish the initial message and append tokens: -```javascript +```agent_javascript // Track state across events let msgSerial = null; let textBlockIndex = null; @@ -246,6 +448,92 @@ async function processEvent(event) { } } ``` + +```agent_python +from ably.types.message import Message + +# Track state across events +msg_serial = None +text_block_index = None + +# Process each streaming event and publish to Ably +async def process_event(event): + global msg_serial, text_block_index + + if event.type == 'message_start': + # Publish initial empty message when response starts + result = await channel.publish('response', '') + + # Capture the message serial for appending tokens + msg_serial = result.serials[0] + + elif event.type == 'content_block_start': + # Capture text block index when a text content block is added + if event.content_block.type == 'text': + text_block_index = event.index + + elif event.type == 'content_block_delta': + # Append tokens from text deltas only + if (event.index == text_block_index and + hasattr(event.delta, 'text') and + msg_serial): + await channel.append_message( + Message(serial=msg_serial, data=event.delta.text) + ) + + elif event.type == 'message_stop': + print('Stream completed!') +``` + +```agent_java +import io.ably.lib.types.Message; + +// Track state across events +private static String msgSerial = null; +private static Long textBlockIndex = null; + +// Process each streaming event and publish to Ably +private static void processEvent(RawMessageStreamEvent event) throws AblyException { + if (event.isMessageStart()) { + // Publish initial empty message when response starts + Message message = new Message("response", ""); + channel.publish(message, new Callback() { + @Override + public void onSuccess(PublishResult result) { + // Capture the message serial for appending tokens + msgSerial = result.serials[0]; + } + @Override + public void onError(ErrorInfo reason) { + System.err.println("Publish failed: " + reason.message); + } + }); + + } else if (event.isContentBlockStart()) { + // Capture text block index when a text content block is added + RawContentBlockStartEvent blockStart = event.asContentBlockStart(); + if (blockStart.contentBlock().isText()) { + textBlockIndex = blockStart.index(); + } + + } else if (event.isContentBlockDelta()) { + // Append tokens from text deltas only + RawContentBlockDeltaEvent delta = event.asContentBlockDelta(); + if (delta.index() == textBlockIndex && + delta.delta().isText() && + msgSerial != null) { + String text = delta.delta().asText().text(); + Message message = new Message(); + message.data = text; + message.serial = msgSerial; + channel.appendMessage(message); + } + + } else if (event.isMessageStop()) { + System.out.println("Stream completed!"); + } +} +``` This implementation: @@ -254,9 +542,11 @@ This implementation: - Filters for `content_block_delta` events with `text_delta` type from text content blocks - Appends each token to the original message + +