diff --git a/package.json b/package.json index 9f599e4b5e..8eb9cd9902 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "validate-llms-txt": "node bin/validate-llms.txt.ts" }, "dependencies": { - "@ably/ui": "17.13.2", + "@ably/ui": "17.13.2-dev.aa83caf92f", "@codesandbox/sandpack-react": "^2.20.0", "@codesandbox/sandpack-themes": "^2.0.21", "@gfx/zopfli": "^1.0.15", diff --git a/src/components/Layout/LanguageSelector.tsx b/src/components/Layout/LanguageSelector.tsx index 00646c0471..666263301d 100644 --- a/src/components/Layout/LanguageSelector.tsx +++ b/src/components/Layout/LanguageSelector.tsx @@ -8,7 +8,7 @@ import { componentMaxHeight, HEADER_BOTTOM_MARGIN, HEADER_HEIGHT } from '@ably/u import { track } from '@ably/ui/core/insights'; import { languageData, languageInfo } from 'src/data/languages'; import { LanguageKey } from 'src/data/languages/types'; -import { useLayoutContext } from 'src/contexts/layout-context'; +import { useLayoutContext, CLIENT_LANGUAGES, AGENT_LANGUAGES } from 'src/contexts/layout-context'; import { navigate } from '../Link'; import { LANGUAGE_SELECTOR_HEIGHT, INKEEP_ASK_BUTTON_HEIGHT } from './utils/heights'; import * as Select from '../ui/Select'; @@ -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']; @@ -163,3 +163,193 @@ 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( + () => + languages + .filter((lang) => languageVersions[lang]) + .map((lang) => ({ + label: lang, + value: `${lang}-${languageVersions[lang]}`, + version: languageVersions[lang], + })), + [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.test.tsx b/src/components/Layout/LeftSidebar.test.tsx index c7efeacfcb..2a2610a411 100644 --- a/src/components/Layout/LeftSidebar.test.tsx +++ b/src/components/Layout/LeftSidebar.test.tsx @@ -16,6 +16,7 @@ jest.mock('src/contexts/layout-context', () => ({ template: null, }, }), + isDualLanguagePath: jest.fn().mockReturnValue(false), })); jest.mock('@reach/router', () => ({ diff --git a/src/components/Layout/LeftSidebar.tsx b/src/components/Layout/LeftSidebar.tsx index 0574666f77..0ad5eedd9b 100644 --- a/src/components/Layout/LeftSidebar.tsx +++ b/src/components/Layout/LeftSidebar.tsx @@ -8,9 +8,36 @@ import Icon from '@ably/ui/core/Icon'; import { productData } from 'src/data'; import { NavProductContent, NavProductPage } from 'src/data/nav/types'; import Link from '../Link'; -import { useLayoutContext } from 'src/contexts/layout-context'; +import { useLayoutContext, isDualLanguagePath } from 'src/contexts/layout-context'; import { interactiveButtonClassName } from './utils/styles'; +// Build link with appropriate language params based on target page type +const buildLinkWithParams = (targetLink: string, searchParams: URLSearchParams): string => { + const clientLang = searchParams.get('client_lang'); + const agentLang = searchParams.get('agent_lang'); + const lang = searchParams.get('lang'); + + const params = new URLSearchParams(); + + if (isDualLanguagePath(targetLink)) { + // Target supports dual language - preserve client_lang/agent_lang + if (clientLang) { + params.set('client_lang', clientLang); + } + if (agentLang) { + params.set('agent_lang', agentLang); + } + } else { + // Target uses single language - preserve lang + if (lang) { + params.set('lang', lang); + } + } + + const paramString = params.toString(); + return paramString ? `${targetLink}?${paramString}` : targetLink; +}; + type LeftSidebarProps = { className?: string; inHeader?: boolean; @@ -78,7 +105,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..dfea0b8fa0 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,54 +104,95 @@ 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; + }; - // Check if this is a single child with a utility language - if (childrenArray.length !== 1) { - return 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)); + } + }); - const child = childrenArray[0]; - if (!isValidElement(child)) { - 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 preElement = child as ReactElement; - const codeElement = isValidElement(preElement.props?.children) - ? (preElement.props.children as ReactElement) - : null; + if (hasAgentPrefix && activePage.isDualLanguage) { + return { languageOverride: activePage.agentLanguage, detectedSdkType: 'agent' as SDKType }; + } - if (!codeElement || !codeElement.props.className) { - return null; + // Check for single utility language (existing logic) + if (languages.length === 1 && UTILITY_LANGUAGES.includes(languages[0])) { + return { languageOverride: languages[0], detectedSdkType: undefined }; } - const className = codeElement.props.className as string; - const langMatch = className.match(/language-(\w+)/); - const lang = langMatch ? langMatch[1] : null; + return { languageOverride: undefined, detectedSdkType: undefined }; + }, [processedChildren, activePage.isDualLanguage, activePage.clientLanguage, activePage.agentLanguage]); + + // For client/agent blocks, the page-level selector controls language, so disable internal onChange + const handleLanguageChange = (lang: string, newSdk: SDKType | undefined) => { + // Don't navigate for client/agent blocks - page-level selector handles this + if (detectedSdkType === 'client' || detectedSdkType === 'agent') { + return; + } - // If it's a utility language, return the language to use as override - return lang && UTILITY_LANGUAGES.includes(lang) ? lang : null; - }, [processedChildren]); + if (!detectedSdkType) { + setSdk(newSdk ?? undefined); + } + navigate(`${location.pathname}?lang=${lang}`); + }; return ( { - setSdk(sdk ?? null); - navigate(`${location.pathname}?lang=${lang}`); - }} + lang={languageOverride || activePage.language} + sdk={detectedSdkType || sdk} + onChange={handleLanguageChange} className={cn(props.className, 'mb-5')} languageOrdering={ activePage.product && languageData[activePage.product] ? Object.keys(languageData[activePage.product]) : [] } apiKeys={apiKeys} + // Hide internal language selector for client/agent blocks since page-level selector controls it + fixed={detectedSdkType === 'client' || detectedSdkType === 'agent'} > {processedChildren} @@ -168,11 +209,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..b1a7700c75 100644 --- a/src/components/Layout/mdx/If.tsx +++ b/src/components/Layout/mdx/If.tsx @@ -5,15 +5,18 @@ import UserContext from 'src/contexts/user-context'; interface IfProps { lang?: LanguageKey; + client_lang?: LanguageKey; + agent_lang?: LanguageKey; + client_or_agent_lang?: LanguageKey; loggedIn?: boolean; className?: string; children: React.ReactNode; as?: React.ElementType; } -const If: React.FC = ({ lang, loggedIn, children }) => { +const If: React.FC = ({ lang, client_lang, agent_lang, client_or_agent_lang, loggedIn, children }) => { const { activePage } = useLayoutContext(); - const { language } = activePage; + const { language, clientLanguage, agentLanguage } = activePage; const userContext = useContext(UserContext); let shouldShow = true; @@ -24,6 +27,26 @@ const If: React.FC = ({ lang, loggedIn, children }) => { shouldShow = shouldShow && splitLang.includes(language); } + // Check client language condition if client_lang prop is provided + if (client_lang !== undefined && clientLanguage) { + const splitLang = client_lang.split(','); + shouldShow = shouldShow && splitLang.includes(clientLanguage); + } + + // Check agent language condition if agent_lang prop is provided + if (agent_lang !== undefined && agentLanguage) { + const splitLang = agent_lang.split(','); + shouldShow = shouldShow && splitLang.includes(agentLanguage); + } + + // Check if either client or agent matches (OR logic) - useful for shared requirements + if (client_or_agent_lang !== undefined) { + const splitLang = client_or_agent_lang.split(','); + const clientMatches = clientLanguage && splitLang.includes(clientLanguage); + const agentMatches = agentLanguage && splitLang.includes(agentLanguage); + shouldShow = shouldShow && (clientMatches || agentMatches); + } + // Check logged in condition if loggedIn prop is provided if (loggedIn !== undefined && userContext.sessionState !== undefined) { const isSignedIn = userContext.sessionState.signedIn ?? false; 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..5096fd849b 100644 --- a/src/components/Layout/utils/nav.ts +++ b/src/components/Layout/utils/nav.ts @@ -14,6 +14,10 @@ export type ActivePage = { language: LanguageKey | null; product: ProductKey | null; template: PageTemplate; + // Dual language support for AI Transport guides + clientLanguage?: LanguageKey; + agentLanguage?: LanguageKey; + isDualLanguage?: boolean; }; /** diff --git a/src/contexts/layout-context.tsx b/src/contexts/layout-context.tsx index a81e7a594a..45e3264b00 100644 --- a/src/contexts/layout-context.tsx +++ b/src/contexts/layout-context.tsx @@ -16,6 +16,21 @@ import { ProductKey } from 'src/data/types'; export const DEFAULT_LANGUAGE = 'javascript'; +// Languages supported for dual-language selection in AI Transport guides +export const CLIENT_LANGUAGES: LanguageKey[] = ['javascript', 'swift', 'java']; +export const AGENT_LANGUAGES: LanguageKey[] = ['javascript', 'python', 'java']; + +// Check if a page supports dual language selection based on its path +// Used for navigation param preservation (we don't have access to page content at nav time) +export const isDualLanguagePath = (pathname: string): boolean => { + return pathname.includes('/docs/guides/ai-transport'); +}; + +// Check if page content has client_/agent_ prefixed languages (more accurate than path check) +const hasDualLanguageContent = (languages: string[]): boolean => { + return languages.some((lang) => lang.startsWith('client_') || lang.startsWith('agent_')); +}; + const LayoutContext = createContext<{ activePage: ActivePage; }>({ @@ -26,6 +41,9 @@ const LayoutContext = createContext<{ language: DEFAULT_LANGUAGE, product: null, template: null, + clientLanguage: undefined, + agentLanguage: undefined, + isDualLanguage: false, }, }); @@ -47,6 +65,32 @@ const determineActiveLanguage = ( return DEFAULT_LANGUAGE; }; +// Determine client language for dual-language pages +const determineClientLanguage = (location: string, _product: ProductKey | null): LanguageKey => { + const params = new URLSearchParams(location); + const clientLangParam = params.get('client_lang') as LanguageKey; + + if (clientLangParam && CLIENT_LANGUAGES.includes(clientLangParam)) { + return clientLangParam; + } + + // Default to javascript + return DEFAULT_LANGUAGE; +}; + +// Determine agent language for dual-language pages +const determineAgentLanguage = (location: string, _product: ProductKey | null): LanguageKey => { + const params = new URLSearchParams(location); + const agentLangParam = params.get('agent_lang') as LanguageKey; + + if (agentLangParam && AGENT_LANGUAGES.includes(agentLangParam)) { + return agentLangParam; + } + + // Default to javascript + return DEFAULT_LANGUAGE; +}; + export const LayoutProvider: React.FC> = ({ children, pageContext, @@ -65,6 +109,16 @@ export const LayoutProvider: React.FC -To follow this guide, you need: -- Node.js 20 or higher + +The client code requires Node.js 20 or higher. + + +The client code requires Xcode 15 or higher. + + +The client code requires Java 11 or higher. + + + +The agent code requires Node.js 20 or higher. + + +The agent code requires Python 3.8 or higher. + + +The agent code requires Java 11 or higher. + + +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) - -Create a new NPM package, which will contain the publisher and subscriber code: + + +- [Ably Swift SDK getting started](/docs/getting-started/swift) + + +- [Ably Python SDK getting started](/docs/getting-started/python) + + +- [Ably Java SDK getting started](/docs/getting-started/java) + + +### 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 Maven project and add the following dependencies to your `pom.xml`: + + +```xml + + + com.anthropic + anthropic-java + 1.0.0 + + + io.ably + ably-java + 1.2.46 + + +``` + + -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 @@ -52,6 +107,58 @@ 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.2.46 + +``` + + + + + ## 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. @@ -79,10 +186,18 @@ 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 `publisher.mjs` with the following contents: + + +In your `ably-anthropic-agent` directory, create a new file `publisher.py` with the following contents: + + +In your agent project, create a new file `Publisher.java` with the following contents: + -```javascript +```agent_javascript import Anthropic from '@anthropic-ai/sdk'; // Initialize Anthropic client @@ -112,6 +227,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 @@ -168,10 +345,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 @@ -183,6 +360,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. @@ -199,10 +400,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; @@ -244,6 +445,87 @@ 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 +// 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 + io.ably.lib.types.Message message = new io.ably.lib.types.Message("response", ""); + CompletionListener listener = new CompletionListener() { + @Override + public void onSuccess() {} + @Override + public void onError(ErrorInfo reason) {} + }; + channel.publish(message, listener); + + // Capture the message serial for appending tokens + // Note: In production, use the callback to get the serial + msgSerial = message.serial; + + } else if (event.isContentBlockStart()) { + // Capture text block index when a text content block is added + ContentBlockStartEvent blockStart = event.asContentBlockStart(); + if (blockStart.contentBlock().isText()) { + textBlockIndex = blockStart.index(); + } + + } else if (event.isContentBlockDelta()) { + // Append tokens from text deltas only + ContentBlockDeltaEvent delta = event.asContentBlockDelta(); + if (delta.index().equals(textBlockIndex) && + delta.delta().isTextDelta() && + msgSerial != null) { + String text = delta.delta().asTextDelta().text(); + channel.appendMessage(msgSerial, text); + } + + } else if (event.isMessageStop()) { + System.out.println("Stream completed!"); + } +} +``` This implementation: @@ -252,9 +534,11 @@ This implementation: - Filters for `content_block_delta` events with `text_delta` type from text content blocks - Appends each token to the original message + +