dotnet / dotnet/aspnetcore

[Blazor][Components.AI] Allow custom handlers to replace structured RichContentBlock content

Open Beginner friendly
#69,266 1 comment 0 reactions 0 assignees View on GitHub
api-proposal api-suggestion area-blazor
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

## Background and Motivation

`ContentBlockHandler` allows applications to consume model updates before the built-in handlers. This appears to be the natural extension point for converting streamed `TextContent` into application-defined structured rich text, such as Markdown parsed into `RichTextNode` values.

A custom handler can accumulate text by calling `RichContentBlock.AppendText()`, but it cannot update the block's structured representation:

- `RichContentBlock.Content` has an internal setter.
- `RichContentBlock.ReplaceContent()` is internal.
- The structured-node renderer used by `MessageList` is not exposed as a reusable component.

As a result, an application cannot use a custom handler to populate a standard `RichContentBlock`. It must either:

1. Transform the `IChatClient` response stream into `RichTextContent` snapshots before `UIAgent` processes it, as the Components AI Dojo currently does.
2. Define a custom block and duplicate the package's structured-node renderer.

Transforming the chat-client stream couples presentation formatting to model updates. `UIAgent` builds its internal conversation history from those transformed updates, so cumulative presentation snapshots can be retained alongside the original provider text.

Related work:

- #68324 introduced rich-text rendering and demonstrates the `FormattedChatClient` approach.
- #68418 tracks related rich-node renderer extensibility, but not custom-handler access to `RichContentBlock` content.

## Proposed API

Make the existing atomic replacement method public:

```diff
namespace Microsoft.AspNetCore.Components.AI;

public class RichContentBlock : ContentBlock
{
public string RawText { get; }
public IReadOnlyList Content { get; internal set; }

public void AppendText(string text);

- internal void ReplaceContent(
+ public void ReplaceContent(
string text,
IReadOnlyList content);
}
```

Keeping the `Content` setter internal while exposing `ReplaceContent` ensures that raw text and its structured representation are replaced atomically.

## Usage Examples

```csharp
internal sealed class MarkdownContentHandler
: ContentBlockHandler
{
public override BlockMappingResult Handle(
BlockMappingContext context,
RichContentBlock state)
{
TextContent? textContent = null;
foreach (var content in context.UnhandledContents)
{
if (content is TextContent text)
{
textContent = text;
break;
}
}

if (textContent is null)
{
return state.Id.Length > 0
? BlockMappingResult.Complete()
: BlockMappingResult.Pass();
}

context.MarkHandled(textContent);
state.AppendText(textContent.Text ?? string.Empty);
state.ReplaceContent(
state.RawText,
MarkdownParser.Parse(state.RawText));

if (state.Id.Length == 0)
{
state.Id = context.Update.MessageId ?? Guid.NewGuid().ToString("N");
return BlockMappingResult.Emit(state, state);
}

return BlockMappingResult.Update(state);
}
}
```

This keeps provider updates and `UIAgent` history as ordinary text while applying formatting only in the presentation pipeline.

## Alternative Designs

- Add a parser callback to `UIAgentOptions` or the built-in text handler.
- Expose the structured-node renderer as a reusable component for custom blocks.
- Introduce a separate public mutable structured-content block type.

A public `ReplaceContent` method is the smallest change and uses the atomic snapshot model already implemented by `RichTextContentHandler`.

## Risks

Applications could provide nodes that do not correspond to the supplied raw text. This is already possible through `RichTextContent`, and `ReplaceContent` preserves the same snapshot contract.

Contributor guide

Open the contributing guide

Research direction

Start with RichContentBlock and the existing RichTextContentHandler, which already uses the atomic replacement model described here. Expose ReplaceContent with the shown signature while keeping Content's setter internal, then verify a custom ContentBlockHandler can replace raw text and structured nodes atomically.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
frontend
Issue type
Feature
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.