feat: Add dual language selector for AI Transport guides#3176
feat: Add dual language selector for AI Transport guides#3176GregHolmes wants to merge 9 commits intomainfrom
Conversation
WalkthroughThis pull request implements dual-language support for Client and Agent code blocks across the documentation platform. Changes introduce language selection infrastructure, context providers, and conditional rendering logic, along with updated language data and comprehensive guide documentation with per-language implementations. Changes
Sequence DiagramsequenceDiagram
participant User
participant LanguageSelector
participant LayoutContext
participant LeftSidebar
participant MDXWrapper
participant CodeBlock as If/CodeBlock
User->>LanguageSelector: Select client language
LanguageSelector->>LanguageSelector: Check activePage.isDualLanguage
alt isDualLanguage true
LanguageSelector->>LanguageSelector: Render DualLanguageSelector
LanguageSelector->>LanguageSelector: Update client_lang/agent_lang params
else isDualLanguage false
LanguageSelector->>LanguageSelector: Render SingleLanguageSelector
LanguageSelector->>LanguageSelector: Update lang param
end
LanguageSelector->>LayoutContext: Update URL query params
LayoutContext->>LayoutContext: determineClientLanguage/determineAgentLanguage
LayoutContext->>LayoutContext: Set clientLanguage & agentLanguage in activePage
LeftSidebar->>LayoutContext: Read isDualLanguagePath
LeftSidebar->>LeftSidebar: buildLinkWithParams preserves lang params
MDXWrapper->>MDXWrapper: Detect client_/agent_ prefixed languages
MDXWrapper->>MDXWrapper: Filter code blocks based on clientLanguage/agentLanguage
MDXWrapper->>CodeBlock: Pass language selection context
CodeBlock->>CodeBlock: Evaluate If conditions (client_lang, agent_lang, etc.)
CodeBlock->>User: Render filtered content for selected language
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
a5fe2ec to
c5eb002
Compare
|
I haven't reviewed the code in detail, I will leave that to someone in the docs/deved team. But from a UX perspective, I really like the approach and I am happy with this :) |
c5eb002 to
71caaea
Compare
|
This branch is heavily dependant on ably/ably-ui#1060. Which I need the devex web team to review. I'm very sorry but you're not going to be able to view the deployment as it stands as I've made changes to the ably-ui. Note: I've asked April to make me a early release candidate version for ably-ui that I can use in this branch. I'll update with a new comment when it happens. I've added quite a few people to the reviews. I think I'd like to recommend what each person reviews! @m-hulbert Could I please have you review this from the docs perspective? Let me know if you have any questions. |
|
I've now moved the ably-ui code over to the new package in website repo: https://github.com/ably/website/pull/7827 |
|
I'm gonna see if the skill from #3192 can be helpful here |
5911152 to
de2bf59
Compare
|
I've put updated Swift translations on #3205 if you're interested |
|
Nice! A few things that I noticed when viewing the Swift versions of the guides:
|
- Add Client/Agent dropdowns for separate frontend/backend language selection - Support fe_/be_ prefixed code blocks in MDX files - Preserve language params when navigating between guide pages - Add aiTransport languages (javascript, python, swift, java) - Add Guides section to AI Transport nav - Update anthropic-message-per-response guide with dual language code blocks - Update @ably/ui to 17.13.1-dev.c839343a URL params: ?fe_lang=xxx&be_lang=yyy
The test mock for layout-context was missing the isDualLanguagePath function that LeftSidebar imports, causing TypeError during test runs.
Extends the If component to support dual-language page conditionals: - fe_lang: show content when frontend language matches - be_lang: show content when backend language matches - fe_or_be_lang: show content when either matches (with deduplication) Used in AI Transport guides for language-specific prerequisites and SDK links.
- Add directory references to file creation instructions (e.g., "In your ably-anthropic-agent directory, create publisher.mjs") - Add cd commands to run instructions for publisher and subscriber - Add directory context to Step 6 rewind instructions - Fix Swift Package.swift code block to use fe_swift prefix - Wrap URL code block in Code component with text language
Move "Message per response" before "Message per token" in navigation for all AI providers (Anthropic, OpenAI, LangGraph, Vercel AI SDK).
…age-per-token guide - Add multi-language support (Python, Java, Swift) to message-per-token guide, matching the structure of message-per-response - Fix Python code in both guides: use AsyncAnthropic with async/await, use Message objects for publish with extras and append_message, use transport_params for echo suppression - Scope "without await" asides to JavaScript only
de2bf59 to
57f0404
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (6)
src/contexts/layout-context.tsx (1)
68-92: Minor: redundantURLSearchParamsinstantiation.
determineClientLanguageanddetermineAgentLanguageeach create a newURLSearchParamsfrom the samelocation.searchstring. Consider parsing once and passing the params object, similar to howbuildLinkWithParamsworks inLeftSidebar.tsx.♻️ Suggested refactor
-const determineClientLanguage = (location: string, _product: ProductKey | null): LanguageKey => { - const params = new URLSearchParams(location); - const clientLangParam = params.get('client_lang') as LanguageKey; +const determineClientLanguage = (params: URLSearchParams): LanguageKey => { + const clientLangParam = params.get('client_lang') as LanguageKey; if (clientLangParam && CLIENT_LANGUAGES.includes(clientLangParam)) { return clientLangParam; } return DEFAULT_LANGUAGE; }; -const determineAgentLanguage = (location: string, _product: ProductKey | null): LanguageKey => { - const params = new URLSearchParams(location); - const agentLangParam = params.get('agent_lang') as LanguageKey; +const determineAgentLanguage = (params: URLSearchParams): LanguageKey => { + const agentLangParam = params.get('agent_lang') as LanguageKey; if (agentLangParam && AGENT_LANGUAGES.includes(agentLangParam)) { return agentLangParam; } return DEFAULT_LANGUAGE; };Then in the provider:
+ const searchParams = new URLSearchParams(location.search); const clientLanguage = isDualLanguage - ? determineClientLanguage(location.search, activePageData?.product ?? null) + ? determineClientLanguage(searchParams) : undefined; const agentLanguage = isDualLanguage - ? determineAgentLanguage(location.search, activePageData?.product ?? null) + ? determineAgentLanguage(searchParams) : undefined;This also removes the unused
_productparameters.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/contexts/layout-context.tsx` around lines 68 - 92, Both determineClientLanguage and determineAgentLanguage redundantly instantiate URLSearchParams from the same location string and keep an unused _product parameter; refactor them to accept a parsed URLSearchParams (or a shared params object) instead of a location string and remove the unused _product argument, then update the provider call site to parse new URLSearchParams once (following the buildLinkWithParams pattern used in LeftSidebar.tsx) and pass that params object into determineClientLanguage and determineAgentLanguage so each function only reads params.get('client_lang') / params.get('agent_lang').src/components/Layout/mdx/If.tsx (2)
42-48:client_or_agent_langOR-check can produce a falsy non-booleanshouldShow.When both
clientLanguageandagentLanguageareundefined,clientMatchesandagentMatchesare bothundefined. The expressionshouldShow && (undefined || undefined)yieldsundefinedrather thanfalse. This works in practice (falsy → renders null), but it weakens theshouldShowinvariant frombooleantoboolean | undefined.Consider explicit boolean coercion for clarity:
Suggested fix
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); + const clientMatches = !!clientLanguage && splitLang.includes(clientLanguage); + const agentMatches = !!agentLanguage && splitLang.includes(agentLanguage); + shouldShow = shouldShow && (clientMatches || agentMatches); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Layout/mdx/If.tsx` around lines 42 - 48, The OR-check in If.tsx can set shouldShow to undefined when both clientLanguage and agentLanguage are undefined; update the client_or_agent_lang block to coerce matches to booleans so shouldShow remains a boolean. Specifically, in the client_or_agent_lang handling (variables: splitLang, clientMatches, agentMatches, shouldShow), compute clientMatches and agentMatches as explicit booleans (e.g., use !!clientLanguage && splitLang.includes(clientLanguage) or Boolean(splitLang.includes(clientLanguage) && clientLanguage)) and then set shouldShow = shouldShow && (clientMatches || agentMatches) so the right-hand side is always true/false.
6-15:classNameandasprops are declared inIfPropsbut never destructured or used.
className(Line 12) andas(Line 14) are part of the interface but are not destructured in the component signature on Line 17 and have no effect. If these are intended for future use, they silently accept and discard values. If unneeded, removing them tightens the public API.Also applies to: 17-17
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Layout/mdx/If.tsx` around lines 6 - 15, IfProps declares className and as but the If component does not destructure or use them, so they are silently ignored; either remove className and as from the IfProps interface to tighten the API or update the If component signature (the component that consumes IfProps) to destructure className and as and apply them (e.g., pass className to the rendered wrapper element and use as to render a custom element type) so those props take effect; reference IfProps, className, as and the If component when making the change.src/components/Layout/LanguageSelector.tsx (2)
300-310:aria-hiddenis alwaysfalsein the else branch (pre-existing).On Line 309,
aria-hidden={option.value === value}is inside the else branch ofoption.value === value ? ... : ...(Line 300), so the condition is alwaysfalse. This is a no-op. The same pattern exists inSingleLanguageSelectorat Line 154. Consider removing the attribute or replacing the spacer<div>with a proper approach.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Layout/LanguageSelector.tsx` around lines 300 - 310, In LanguageSelector (and the similar SingleLanguageSelector) the else-branch spacer div renders aria-hidden={option.value === value} which will always be false there; update the spacer to either omit the aria-hidden prop entirely or set aria-hidden="true" so it correctly hides the decorative element from assistive tech, and ensure the replacement element keeps the same sizing/classNames (the spacer div that replaces Select.ItemIndicator should remain a non-interactive, decorative element). Target the else branch next to Select.ItemIndicator and the corresponding code in SingleLanguageSelector to apply the same change.
167-320: Substantial code duplication betweenSingleLanguageSelectorandDualLanguageDropdown.The dropdown rendering logic (trigger, portal, viewport, option items with icon/badge/check indicator, skeleton fallback) is nearly identical between the two components. Consider extracting a shared base dropdown component that both
SingleLanguageSelectorandDualLanguageDropdowncompose, passing in label, options, value, andonValueChange.This would reduce ~150 lines of duplicated JSX and make styling/behavior changes easier to maintain consistently.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Layout/LanguageSelector.tsx` around lines 167 - 320, The DualLanguageDropdown duplicates most of the JSX and behavior from SingleLanguageSelector; extract a shared BaseLanguageDropdown component that encapsulates the Select.Root/Trigger/Portal/Content/Viewport rendering, the skeleton fallback, option mapping (Icon, Badge, ItemIndicator), and sizing logic, and have both SingleLanguageSelector and DualLanguageDropdown call it with props like label, options (LanguageSelectorOptionData[]), value, onValueChange (handleValueChange), disabled flag, and a selectedLang lookup; ensure BaseLanguageDropdown uses the same constants and utilities (LANGUAGE_SELECTOR_HEIGHT, componentMaxHeight, Icon, Badge, Select.*) so both components become thin wrappers that prepare options/selectedLanguage and delegate rendering to the new BaseLanguageDropdown.src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx (1)
470-514: Java publisher:channeland class-level fields referenced without full class context.The Java
processEventsnippet at Lines 470-514 referenceschannel(Line 489) and declaresresponseIdas aprivate static String(Line 475), but these are meant to be added to thePublisherclass defined earlier. Since guides instruct users to progressively build the file, this is understandable — but a brief note like "Add these fields and update the method in yourPublisherclass" would reduce ambiguity.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx` around lines 470 - 514, The snippet references class-level state and a channel that belong in your Publisher class; move the private static String responseId field into the Publisher class and add a channel field (the same type used elsewhere in Publisher) there, then update the processEvent method to be a member of Publisher (or make it static if you prefer) so it uses Publisher's responseId and channel fields; ensure the method signature and access modifiers match the existing Publisher class conventions (e.g., processEvent(...) inside Publisher) and that any imports/types used (MessageExtras, JsonObject, RawMessageStreamEvent, ContentBlockDeltaEvent) are available in that class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 43: package.json currently pins the `@ably/ui` dependency to a dev
prerelease ("@ably/ui": "17.13.2-dev.aa83caf92f"); replace this prerelease
version with a stable release (e.g., the corresponding released semver like
"17.13.2" or a caret range such as "^17.13.2") before merging. Update the
"@ably/ui" entry in package.json to a published stable version and run your
package manager install and a quick build/test to ensure compatibility.
In `@src/components/Layout/MDXWrapper.tsx`:
- Around line 170-181: The handler handleLanguageChange references
location.pathname but location isn't in scope; import useLocation from
"@reach/router" and call const location = useLocation() in the component where
handleLanguageChange/WrappedCodeSnippet is defined (or in MDXWrapper and pass it
down) so the function uses the router-provided location instead of the global
object; update the code to remove reliance on the global location and use the
location from useLocation() when calling
navigate(`${location.pathname}?lang=${lang}`).
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-response.mdx`:
- Around line 945-990: The Java snippet for AblyRealtime/Channel setup and the
subscribe lambda (references: ClientOptions, AblyRealtime, ChannelOptions,
Channel, channel.subscribe, responses map) is shown at top-level and will not
compile; wrap these statements inside a proper class and method (e.g., add them
into the existing Subscriber class or a new method like subscribeToChannel()) or
add a clear comment indicating they belong inside Subscriber.java’s
main/mainSubscribe method so the field declarations (responses) and the
subscribe call are within a valid class/method scope.
- Around line 631-670: The Swift example uses the wrong ARTMessageAction enum
case `.messageAppend`; update the switch in the channel.subscribe closure to use
the correct ably-cocoa enum case `.append` (the switch on message.action where
`.messageAppend` appears should be renamed to `.append`) so the handler for
appending tokens matches the ARTMessageAction API; leave the other cases
(`.create`, `.update`) and the responses dictionary handling unchanged.
- Around line 485-527: The code in processEvent reads message.serial
synchronously after calling channel.publish(message, listener), causing a race
because the server-assigned serial is provided asynchronously; move the
assignment of msgSerial into the PublishResult callback by implementing
CompletionListener.onSuccess to extract the serial (PublishResult.serials[0])
and set msgSerial there, and keep channel.appendMessage using msgSerial as
before; update the CompletionListener implementation passed to channel.publish
inside the message start branch to capture the serial instead of reading
message.serial directly.
- Around line 449-483: The code calls channel.append_message (not available in
ably-python); replace the append_message branch in process_event to publish
incremental token chunks via channel.publish instead of append_message (e.g.,
use channel.publish('response', event.delta.text)), remove or stop relying on
Message and msg_serial for appending, and add a brief comment noting that true
appendMessage behavior is a future SDK feature if you want to preserve intent;
update imports/usages (Message, msg_serial) accordingly.
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx`:
- Around line 340-349: The Python example incorrectly passes
transport_params={'echo': 'false'} to AblyRealtime (AblyRealtime) which the
Python SDK does not support for disabling echo; remove the transport_params
argument from the AblyRealtime(...) call in this snippet and either (a) document
that echo control via echoMessages is not available in the Python SDK (mention
echoMessages and ClientOptions.echoMessages for JS/Java) or (b) add a short
application-level workaround suggestion in the doc (e.g., filter incoming
messages by client_id on subscribe) so the example and Aside reflect accurate
Python behavior.
---
Nitpick comments:
In `@src/components/Layout/LanguageSelector.tsx`:
- Around line 300-310: In LanguageSelector (and the similar
SingleLanguageSelector) the else-branch spacer div renders
aria-hidden={option.value === value} which will always be false there; update
the spacer to either omit the aria-hidden prop entirely or set
aria-hidden="true" so it correctly hides the decorative element from assistive
tech, and ensure the replacement element keeps the same sizing/classNames (the
spacer div that replaces Select.ItemIndicator should remain a non-interactive,
decorative element). Target the else branch next to Select.ItemIndicator and the
corresponding code in SingleLanguageSelector to apply the same change.
- Around line 167-320: The DualLanguageDropdown duplicates most of the JSX and
behavior from SingleLanguageSelector; extract a shared BaseLanguageDropdown
component that encapsulates the Select.Root/Trigger/Portal/Content/Viewport
rendering, the skeleton fallback, option mapping (Icon, Badge, ItemIndicator),
and sizing logic, and have both SingleLanguageSelector and DualLanguageDropdown
call it with props like label, options (LanguageSelectorOptionData[]), value,
onValueChange (handleValueChange), disabled flag, and a selectedLang lookup;
ensure BaseLanguageDropdown uses the same constants and utilities
(LANGUAGE_SELECTOR_HEIGHT, componentMaxHeight, Icon, Badge, Select.*) so both
components become thin wrappers that prepare options/selectedLanguage and
delegate rendering to the new BaseLanguageDropdown.
In `@src/components/Layout/mdx/If.tsx`:
- Around line 42-48: The OR-check in If.tsx can set shouldShow to undefined when
both clientLanguage and agentLanguage are undefined; update the
client_or_agent_lang block to coerce matches to booleans so shouldShow remains a
boolean. Specifically, in the client_or_agent_lang handling (variables:
splitLang, clientMatches, agentMatches, shouldShow), compute clientMatches and
agentMatches as explicit booleans (e.g., use !!clientLanguage &&
splitLang.includes(clientLanguage) or Boolean(splitLang.includes(clientLanguage)
&& clientLanguage)) and then set shouldShow = shouldShow && (clientMatches ||
agentMatches) so the right-hand side is always true/false.
- Around line 6-15: IfProps declares className and as but the If component does
not destructure or use them, so they are silently ignored; either remove
className and as from the IfProps interface to tighten the API or update the If
component signature (the component that consumes IfProps) to destructure
className and as and apply them (e.g., pass className to the rendered wrapper
element and use as to render a custom element type) so those props take effect;
reference IfProps, className, as and the If component when making the change.
In `@src/contexts/layout-context.tsx`:
- Around line 68-92: Both determineClientLanguage and determineAgentLanguage
redundantly instantiate URLSearchParams from the same location string and keep
an unused _product parameter; refactor them to accept a parsed URLSearchParams
(or a shared params object) instead of a location string and remove the unused
_product argument, then update the provider call site to parse new
URLSearchParams once (following the buildLinkWithParams pattern used in
LeftSidebar.tsx) and pass that params object into determineClientLanguage and
determineAgentLanguage so each function only reads params.get('client_lang') /
params.get('agent_lang').
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx`:
- Around line 470-514: The snippet references class-level state and a channel
that belong in your Publisher class; move the private static String responseId
field into the Publisher class and add a channel field (the same type used
elsewhere in Publisher) there, then update the processEvent method to be a
member of Publisher (or make it static if you prefer) so it uses Publisher's
responseId and channel fields; ensure the method signature and access modifiers
match the existing Publisher class conventions (e.g., processEvent(...) inside
Publisher) and that any imports/types used (MessageExtras, JsonObject,
RawMessageStreamEvent, ContentBlockDeltaEvent) are available in that class.
| // 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}`); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l src/components/Layout/MDXWrapper.tsxRepository: ably/docs
Length of output: 93
🏁 Script executed:
cat -n src/components/Layout/MDXWrapper.tsx | head -250Repository: ably/docs
Length of output: 10803
🏁 Script executed:
# Check the exact context around lines 170-181 and where handleLanguageChange is called
cat -n src/components/Layout/MDXWrapper.tsx | sed -n '150,220p'Repository: ably/docs
Length of output: 3727
🏁 Script executed:
# Search for all references to handleLanguageChange to understand its scope
rg -n 'handleLanguageChange' src/components/Layout/MDXWrapper.tsxRepository: ably/docs
Length of output: 178
🏁 Script executed:
# Check how `@reach/router` or useLocation is used in the codebase
rg -n 'useLocation|@reach/router' src/Repository: ably/docs
Length of output: 5625
handleLanguageChange uses location without it being in scope.
Line 180 references location.pathname inside WrappedCodeSnippet, but the component doesn't receive location as a prop (line 67 component signature). The parent MDXWrapper receives location from Gatsby (line 208) but doesn't pass it down. This relies on the global location object, which is unavailable during SSR or in test environments.
Use useLocation() from @reach/router (already a pattern in this codebase—see HiddenLanguageLinks, LanguageSelector, Header, etc.) inside handleLanguageChange:
+import { useLocation } from '@reach/router';
const WrappedCodeSnippet: React.FC<{ activePage: ActivePage } & CodeSnippetProps> = ({
activePage,
apiKeys,
children,
...props
}) => {
const { sdk, setSdk } = useSDK();
+ const location = useLocation();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/Layout/MDXWrapper.tsx` around lines 170 - 181, The handler
handleLanguageChange references location.pathname but location isn't in scope;
import useLocation from "@reach/router" and call const location = useLocation()
in the component where handleLanguageChange/WrappedCodeSnippet is defined (or in
MDXWrapper and pass it down) so the function uses the router-provided location
instead of the global object; update the code to remove reliance on the global
location and use the location from useLocation() when calling
navigate(`${location.pathname}?lang=${lang}`).
| ```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!') | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
ably-python SDK channel.publish return type serials
💡 Result:
In the current ably-python SDK, Channel.publish(...) is an async method, and when you await it, it returns a PublishResult object (not None). (raw.githubusercontent.com)
- Return type:
PublishResult(raw.githubusercontent.com) - Serials:
PublishResult.serials→ alistof message serial strings (1:1 with the messages you published). (raw.githubusercontent.com)
Example:
result = await channel.publish("event", {"x": 1})
serials = result.serials
``` ([raw.githubusercontent.com](https://raw.githubusercontent.com/ably/ably-python/main/ably/rest/channel.py))
Citations:
- 1: https://raw.githubusercontent.com/ably/ably-python/main/ably/rest/channel.py
- 2: https://raw.githubusercontent.com/ably/ably-python/main/ably/rest/channel.py
- 3: https://raw.githubusercontent.com/ably/ably-python/main/ably/types/operations.py
- 4: https://raw.githubusercontent.com/ably/ably-python/main/ably/rest/channel.py
---
🌐 Web query:
`ably-python append_message API message history`
💡 Result:
In Ably, **`appendMessage()` (a.k.a. “append message”)** appends new `data` onto an *existing* message identified by its Ably-assigned **`serial`** (you get the serial from the `publish()` result, from a subscribed message, or from `history`). When Ably processes an append, it builds the latest full message and uses that full (non-incremental) form for **History** results—so **history queries return the complete, latest message content**, not a list of individual appended chunks. [1]
For **message history retention**:
- By default, Ably retains messages **in memory for ~2 minutes** (useful for short disconnect recovery). [2]
- For retention beyond that, you must **enable message persistence** for the channel/namespace using a dashboard **channel rule** (history is disabled by default). [3]
- If you enable the **“Message annotations, updates, deletes and appends”** channel rule (required for appends), Ably notes that messages are **persisted regardless of whether persistence is otherwise enabled**, which can affect usage/billing. [1]
Notes on “history vs versions”:
- **Updates/deletes** replace what you see in history (history shows the latest version in the original position) and you can retrieve a message’s version history. [1]
- **Appends are designed for high-frequency incremental building**; subscribers may receive `message.append` events (possibly conflated), but history/rewind is intended to show the fully aggregated message. [1]
**Sources**
[1] Ably docs — Updates, deletes and appends (incl. append behavior + history semantics + channel rule requirement) https://ably.com/docs/messages/updates-deletes
[2] Ably docs — Getting started in Python (history section; 2-minute default retention) https://ably.com/docs/getting-started/python
[3] Ably FAQ — History is disabled by default; enable persistence via channel rules https://faqs.ably.com/how-do-i-enable-history-my-messages-are-not-being-stored-for-longer-than-a-few-minutes
---
🌐 Web query:
`ably-python Channel append_message method documentation`
💡 Result:
The Ably Pub/Sub **Python SDK (`ably-python`) does not currently document (or generally expose) a `Channel.append_message` method**. The SDK README notes it supports **Ably REST and basic realtime message subscriptions**, not the full realtime feature set where message update/append APIs typically live. [1]
Ably’s platform feature for appending to an existing message is documented as **`appendMessage()`** (camelCase) on a REST or realtime channel, as part of **“Updates, deletes and appends”** (Public Preview). That page also notes the feature **may not yet be implemented in all SDKs**. [2]
If you’re looking for the *conceptual/HTTP-level* behavior and requirements (serial, required capabilities, etc.), use Ably’s “Updates, deletes and appends” documentation. [2]
**Sources:**
[1] Ably `ably-python` GitHub repository README (feature/support notes) ([github.com](https://github.com/ably/ably-python))
[2] Ably docs: “Updates, deletes and appends” (includes `appendMessage()` and SDK-availability note) ([ably.com](https://ably.com/docs/messages/updates-deletes?utm_source=openai))
Citations:
- 1: https://github.com/ably/ably-python
- 2: https://ably.com/docs/messages/updates-deletes?utm_source=openai
---
</details>
**Remove or update `channel.append_message()` call—API does not exist in ably-python SDK.**
Lines 462–465 correctly use `channel.publish()` and access `.serials[0]` (verified against ably-python SDK). However, line 477 calls `channel.append_message()`, which is **not yet implemented** in the ably-python SDK. The `appendMessage()` feature exists in Ably as a Public Preview feature in some SDKs, but ably-python does not currently expose this method. Either replace with an alternative approach, document the limitation, or indicate this requires a future SDK version.
<details>
<summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In @src/pages/docs/guides/ai-transport/anthropic-message-per-response.mdx around
lines 449 - 483, The code calls channel.append_message (not available in
ably-python); replace the append_message branch in process_event to publish
incremental token chunks via channel.publish instead of append_message (e.g.,
use channel.publish('response', event.delta.text)), remove or stop relying on
Message and msg_serial for appending, and add a brief comment noting that true
appendMessage behavior is a future SDK feature if you want to preserve intent;
update imports/usages (Message, msg_serial) accordingly.
</details>
<!-- fingerprinting:phantom:medusa:phoenix -->
<!-- This is an auto-generated comment by CodeRabbit -->
| ```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!"); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ably Java SDK channel.publish() message serial synchronous asynchronous
💡 Result:
Ably Java SDK channel.publish(...): sync/async + message serial
Realtime (io.ably.lib.realtime.Channel / ChannelBase)
channel.publish(...)is non-blocking (asynchronous) in the Java Realtime SDK (annotated@NonBlocking) and returnsvoid. If you need to know when it’s actually been accepted/sent (or failed), use the overload that takes aCallback<PublishResult>. The callback is invoked on a background thread. [1]- To get the message serial for what you published, you must use the callback overloads, because on success the callback receives a
PublishResultthat includes the message serial(s). [1]
REST (io.ably.lib.rest.Channel)
channel.publish(name, data)is blocking (synchronous) in the Java REST SDK (annotated@Blocking). [2]- If you want a synchronous call and you need the serial(s) returned, use
publishWithResult(...), which returnsPublishResultcontaining the serial(s). [2] - REST also provides
publishAsync(..., Callback<PublishResult>)for an explicit async REST publish. [2]
What is “message serial” and how does it relate to ordering?
- Ably’s realtime system uses a unique incrementing message serial on messages sent over a realtime connection, enabling reliable ordering for persistently-connected realtime subscribers on a channel (they receive messages in the order published on that channel). [3]
Sources:
[1] Realtime ChannelBase.publish(...) Java API docs (callbacks return PublishResult with serial(s), non-blocking) https://ably.com/docs/sdk/java/v1.2/io/ably/lib/realtime/ChannelBase.html
[2] REST ChannelBase.publish(...) / publishWithResult(...) Java API docs (blocking vs async, PublishResult contains serial(s)) https://ably.com/docs/sdk/java/v1.2/io/ably/lib/rest/ChannelBase.html
[3] Ably FAQ on message ordering + incrementing message serial https://faqs.ably.com/reliable-message-ordering-for-connected-clients
🌐 Web query:
Ably Java SDK message.serial when is it set Realtime PublishResult callback
💡 Result:
In the Ably Java Realtime SDK, the Message.serial you’re thinking of is server-assigned and is primarily used to reference a message later (for annotations / update / delete / append). It is not something you can rely on being set on the Message instance you pass to channel.publish(...).
What you do get back in the Realtime publish callback is a PublishResult, whose serials field contains the server-assigned serial(s), 1:1 with the messages you published (and a serial can be null if the message was discarded due to conflation rules). That array is the point at which the serial is available to the publisher. [1]
Also note: Ably documents Message.serial as being set only when message annotations/updates/deletes/appends are enabled via channel rules (because that serial is what those features use to target a specific message). [2][3]
So:
- The serial is set by Ably when the publish is accepted/processed server-side, and is surfaced to the publisher via
PublishResult.serialsin the publish callback. [1] - Subscribers may see
message.serialon received messages when those message-mutation/annotation features are enabled. [2][3]
Sources:
[1] Ably Java SDK Javadoc: PublishResult.serials (and null if conflated) — https://ably.com/docs/sdk/java/v1.2/io/ably/lib/types/PublishResult.html
[2] Ably “Messages” docs: meaning/availability of Message.serial — https://ably.com/docs/api/realtime-sdk/messages
[3] Ably “Message annotations” docs: annotations target message.serial — https://ably.com/docs/messages/annotations
Java publisher: race condition reading message.serial before server assigns it.
Line 501 calls channel.publish(message, listener) which is asynchronous in the Realtime SDK, but Line 505 reads msgSerial = message.serial; immediately — before the server has processed the message. The serial is server-assigned and only available in the PublishResult passed to the callback's onSuccess() method, not on the Message object. Reading it synchronously will return null, causing subsequent appendMessage calls on Line 521 to fail.
The comment on Line 504 correctly identifies the solution: capture the serial inside the callback. Extract serial from PublishResult.serials[0] in the onSuccess() method instead of reading it from the message object.
Correct fix: extract serial from PublishResult in callback
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() {}
+ public void onSuccess() {
+ // Capture the message serial from the server response
+ if (listener.result != null && listener.result.serials != null && listener.result.serials.length > 0) {
+ msgSerial = listener.result.serials[0];
+ }
+ }
`@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;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-response.mdx` around
lines 485 - 527, The code in processEvent reads message.serial synchronously
after calling channel.publish(message, listener), causing a race because the
server-assigned serial is provided asynchronously; move the assignment of
msgSerial into the PublishResult callback by implementing
CompletionListener.onSuccess to extract the serial (PublishResult.serials[0])
and set msgSerial there, and keep channel.appendMessage using msgSerial as
before; update the CompletionListener implementation passed to channel.publish
inside the message start branch to capture the serial instead of reading
message.serial directly.
| ```client_swift | ||
| import Ably | ||
|
|
||
| // Initialize Ably Realtime client | ||
| let realtime = ARTRealtime(key: "{{API_KEY}}") | ||
|
|
||
| // Get the same channel used by the publisher | ||
| let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}") | ||
|
|
||
| // Track responses by message serial | ||
| var responses: [String: String] = [:] | ||
|
|
||
| // Subscribe to receive messages | ||
| channel.subscribe { message in | ||
| guard let serial = message.serial else { return } | ||
|
|
||
| switch message.action { | ||
| case .create: | ||
| // New response started | ||
| print("\n[Response started] \(serial)") | ||
| responses[serial] = message.data as? String ?? "" | ||
|
|
||
| case .messageAppend: | ||
| // Append token to existing response | ||
| let current = responses[serial] ?? "" | ||
| let token = message.data as? String ?? "" | ||
| responses[serial] = current + token | ||
|
|
||
| // Display token as it arrives | ||
| print(token, terminator: "") | ||
|
|
||
| case .update: | ||
| // Replace entire response content | ||
| responses[serial] = message.data as? String ?? "" | ||
| print("\n[Response updated with full content]") | ||
|
|
||
| default: | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
ably-cocoa ARTMessageAction enum cases Swift SDK documentation
💡 Result:
In the Ably Cocoa (ably-cocoa) Swift SDK, ARTMessageAction has these enum cases (with raw values): create = 0, update = 1, delete = 2, meta = 3, messageSummary = 4, append = 5. [1]
Sources
[1] Ably Cocoa SDK API Reference — ARTMessageAction Enumeration Reference (ably.com)
Fix incorrect enum case name in Swift code example.
Line 653 uses .messageAppend, but the correct ARTMessageAction enum case in the ably-cocoa SDK is .append. Update this case name to match the actual SDK API.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-response.mdx` around
lines 631 - 670, The Swift example uses the wrong ARTMessageAction enum case
`.messageAppend`; update the switch in the channel.subscribe closure to use the
correct ably-cocoa enum case `.append` (the switch on message.action where
`.messageAppend` appears should be renamed to `.append`) so the handler for
appending tokens matches the ARTMessageAction API; leave the other cases
(`.create`, `.update`) and the responses dictionary handling unchanged.
| ```client_java | ||
| import io.ably.lib.realtime.AblyRealtime; | ||
| import io.ably.lib.realtime.Channel; | ||
| import io.ably.lib.types.ClientOptions; | ||
| import io.ably.lib.types.ChannelOptions; | ||
| import io.ably.lib.types.Param; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| // Use rewind to receive recent historical messages | ||
| ClientOptions clientOptions = new ClientOptions("{{API_KEY}}"); | ||
| AblyRealtime realtime = new AblyRealtime(clientOptions); | ||
|
|
||
| ChannelOptions channelOptions = new ChannelOptions(); | ||
| channelOptions.params = new Param[] { | ||
| new Param("rewind", "2m") // Retrieve messages from the last 2 minutes | ||
| }; | ||
|
|
||
| Channel channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}", channelOptions); | ||
|
|
||
| Map<String, String> responses = new HashMap<>(); | ||
|
|
||
| channel.subscribe(message -> { | ||
| String serial = message.serial; | ||
| if (serial == null) return; | ||
|
|
||
| switch (message.action) { | ||
| case MESSAGE_CREATE: | ||
| responses.put(serial, message.data != null ? message.data.toString() : ""); | ||
| break; | ||
|
|
||
| case MESSAGE_APPEND: | ||
| String current = responses.getOrDefault(serial, ""); | ||
| String token = message.data != null ? message.data.toString() : ""; | ||
| responses.put(serial, current + token); | ||
| System.out.print(token); | ||
| break; | ||
|
|
||
| case MESSAGE_UPDATE: | ||
| // Historical messages contain full concatenated response | ||
| responses.put(serial, message.data != null ? message.data.toString() : ""); | ||
| System.out.println("\n[Historical response]: " + responses.get(serial)); | ||
| break; | ||
| } | ||
| }); | ||
| ``` |
There was a problem hiding this comment.
Java rewind subscriber snippet appears outside a class context.
Lines 945-990 show Java code (variable declarations, channel setup, subscribe call) that is not wrapped in a class or method. Unlike the earlier Subscriber.java that has a proper class structure, this snippet would not compile as shown. Consider wrapping it in a method or adding a comment indicating where in the Subscriber class it should be placed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-response.mdx` around
lines 945 - 990, The Java snippet for AblyRealtime/Channel setup and the
subscribe lambda (references: ClientOptions, AblyRealtime, ChannelOptions,
Channel, channel.subscribe, responses map) is shown at top-level and will not
compile; wrap these statements inside a proper class and method (e.g., add them
into the existing Subscriber class or a new method like subscribeToChannel()) or
add a clear comment indicating they belong inside Subscriber.java’s
main/mainSubscribe method so the field declarations (responses) and the
subscribe call are within a valid class/method scope.
|
|
||
| ```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('{{RANDOM_CHANNEL_NAME}}') | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Ably Python SDK transport_params echo messages documentation
💡 Result:
echoMessages (echo / self-receive published messages)
- Ably Realtime clients echo messages back to the publisher by default; you can disable that by setting the
echoMessagesclient option tofalse. [1][2]
transportParams (connection / transport tuning)
- Ably Realtime clients support a
transportParams(key/value) client option used to pass transport-level parameters such as:heartbeatInterval(milliseconds) to request a different heartbeat interval. [3]remainPresentFor(milliseconds) to control how long a client stays in presence after an abrupt disconnect. [4]
Python SDK status / where to look
- The official
ably(ably-python) SDK exists, but its README notes full Realtime support is unavailable (it supports REST + basic realtime subscriptions; for full Realtime features they suggest using MQTT). That means some advanced Realtime client options you may see in other SDKs’ docs (liketransportParams,echoMessages) may not be available/exposed in the Python SDK in the same way. [5]
Sources:
[1] https://ably.com/docs/pub-sub/advanced
[2] https://faqs.ably.com/is-it-possible-to-prevent-messages-published-being-echoed-back-to-the-publishing-client
[3] https://ably.com/docs/connect
[4] https://ably.com/docs/ai-transport/sessions-identity/online-status
[5] https://github.com/ably/ably-python
🌐 Web query:
Ably Python SDK AblyRealtime echoMessages parameter
💡 Result:
echoMessages is an Ably client option (default true) that, when set to false, prevents messages published on a connection from being echoed back to the same connection. [1][2]
In the Ably Python SDK (ably / AblyRealtime), there is no echoMessages / echo_messages option exposed in the client Options used by AblyRealtime, so you can’t set it (passing it will error because Python Options/AuthOptions don’t accept that keyword). [3][4][5]
Workaround (Python): do application-level filtering (e.g., set a client_id and ignore received messages where message.client_id == my_client_id).
🏁 Script executed:
# Search for Python SDK documentation or examples in the codebase
fd -t f "\.md" "\.mdx" | xargs rg -l "transport_params|python.*echo" | head -20Repository: ably/docs
Length of output: 646
🏁 Script executed:
# Search for other Python examples in the Anthropic guide
rg -A 5 "agent_python" src/pages/docs/guides/ai-transport/ --type mdxRepository: ably/docs
Length of output: 80
🏁 Script executed:
# Check if there are any other Python + Realtime examples
rg -B 2 -A 5 "AblyRealtime.*python|agent_python" src/pages/docs/ --type mdx | head -50Repository: ably/docs
Length of output: 80
🏁 Script executed:
# Read the specific file to see the context around lines 340-349
cat -n src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx | sed -n '330,360p'Repository: ably/docs
Length of output: 1111
🏁 Script executed:
# Check the surrounding context to understand what this code is trying to do
cat -n src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx | sed -n '320,370p'Repository: ably/docs
Length of output: 2167
Python SDK does not support disabling echo via echoMessages or transport_params.
The JavaScript example (line 334) correctly uses echoMessages: false, and the Java example (line 358) uses ClientOptions.echoMessages = false. However, the Python SDK does not expose an echoMessages option, and transport_params is reserved for transport-level tuning (heartbeat intervals, presence timeouts) — not echo control. The current Python code (transport_params={'echo': 'false'}) will not achieve the documented behavior in the Aside note (line 369).
For the Python SDK, remove the transport_params parameter since echo disabling is not available, or provide an application-level workaround (e.g., filter messages by client_id). Alternatively, clarify in the documentation that this feature is only available in JavaScript, Java, and other SDKs with full Realtime support.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/docs/guides/ai-transport/anthropic-message-per-token.mdx` around
lines 340 - 349, The Python example incorrectly passes transport_params={'echo':
'false'} to AblyRealtime (AblyRealtime) which the Python SDK does not support
for disabling echo; remove the transport_params argument from the
AblyRealtime(...) call in this snippet and either (a) document that echo control
via echoMessages is not available in the Python SDK (mention echoMessages and
ClientOptions.echoMessages for JS/Java) or (b) add a short application-level
workaround suggestion in the doc (e.g., filter incoming messages by client_id on
subscribe) so the example and Aside reflect accurate Python behavior.
Summary
Adds dual language selection for AI Transport guide pages, allowing users to independently choose Client (frontend) and Agent (backend) languages for code examples.
Changes
?fe_lang=xxx&be_lang=yyyfor bookmarking and sharing specific language combinationsfe_andbe_prefixes (e.g.,fe_javascript,be_python) to distinguish frontend vs backend codeSupported languages
Dependencies
@ably/ui@17.13.1-dev.c839343a(or later) withfe_/be_prefix support in CodeSnippetSummary by CodeRabbit
New Features
Documentation