Skip to content

Add support for streaming through entities #5517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7d7adb8
delete `text` from typing activities which are not streaming
Jul 10, 2025
5860d06
add support for reading streaming data in `entities`
Jul 10, 2025
b09797d
Merge branch 'feature/entities-streaming-support' into main
kylerohnmsft Jul 11, 2025
64bdfa8
Merge pull request #1 from kylerohnmsft/main
kylerohnmsft Jul 11, 2025
48b0ea1
Merge pull request #2 from kylerohnmsft/feature/entities-streaming-su…
kylerohnmsft Jul 11, 2025
e680dd4
refactoring to pass existing html2 test cases
Jul 15, 2025
684a78b
minimal implementation of entities livestreaming
Jul 16, 2025
1d569a1
Rewrite code flow to check for streamingData first -- draft version (…
Jul 17, 2025
d5cd926
Change conditionals to be more readable and actually check channelData
Jul 17, 2025
87442e2
handle case of entities being undefined
Jul 17, 2025
c72ad0f
Make channelData optional in entitiesStreamingActivitySchema
Jul 17, 2025
e34df98
make channelData not optional for entitiesStreamingActivitySchema
Jul 17, 2025
211a8a7
change looseObject back to object
Jul 17, 2025
f6596cf
handle entities being undefined again
Jul 17, 2025
3b0da48
clean up logical flow and include more typing
Jul 17, 2025
fcc5512
Removed common data from schemas for reusability
Jul 17, 2025
f881517
read channelData instead of entities
Jul 17, 2025
5f5fe92
remove unnecessary field from StreamingData
Jul 17, 2025
8f01f68
typing+final+text activity returns undefined
Jul 17, 2025
07797eb
further abstraction of schemas
Jul 17, 2025
53980b5
update unit tests
Jul 21, 2025
07e0418
edit changelog
Jul 21, 2025
3d1bfa3
Merge branch 'microsoft:main' into main
kylerohn Jul 21, 2025
0778a90
finish changelog
kylerohn Jul 21, 2025
ed212de
edit docs
kylerohn Jul 21, 2025
1e89aeb
allow for optional channelData in entities streaming schema
Jul 21, 2025
abea4be
Merge branch 'temp'
Aug 4, 2025
0c29c8f
added jsdoc to StreamingData interface
Aug 4, 2025
d335e2e
fix naming issues
Aug 4, 2025
b8a886d
add sample
Aug 4, 2025
5515771
trigger CI
Aug 4, 2025
3f905ad
fix sample
Aug 5, 2025
ef54b41
require type: streaminfo field for streaming data in entities
Aug 5, 2025
5abefa2
fix unit tests to reflect added `entities` field
Aug 5, 2025
145373f
Merge branch 'main' into main
kylerohnmsft Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Added `disableFileUpload` flag to completelly disable file upload feature, in PR [#5508](https://github.com/microsoft/BotFramework-WebChat/pull/5508), by [@JamesNewbyAtMicrosoft](https://github.com/JamesNewbyAtMicrosoft)
- Deprecated `hideUploadButton` in favor of `disableFileUpload`.
- Updated `BasicSendBoxToolbar` to rely solely on `disableFileUpload`.
- Added livestreaming support for livestreaming data in `entities` in PR [#5517](https://github.com/microsoft/BotFramework-WebChat/pull/5517) by [@kylerohn](https://github.com/kylerohn)

### Changed

Expand Down
17 changes: 17 additions & 0 deletions docs/LIVESTREAMING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ To simplify this documentation, we are using the term "bot" instead of "copilot"

Bot developers would need to implement the livestreaming as outlined in this section. The implementation below will enable livestreaming to both Azure Bot Services and Teams.

> [!NOTE]
> In the scenarios below, the livestream metadata is inside the `channelData` field. BotFramework-WebChat checks both `channelData` and the first element of the `entities` field for livestreaming metadata. It will appear in different places depending on the platform used to communicate with BotFramework-WebChat:
>
> ```json
> {
> "entities": [
> {
> "streamSequence": 1,
> "streamType": "streaming",
> "type": "streaminfo"
> }
> ],
> "text": "A quick",
> "type": "typing"
> }
> ```

### Scenario 1: Livestream from start to end

> In this example, we assume the bot is livestreaming the following sentence to the user:
Expand Down
179 changes: 172 additions & 7 deletions packages/core/src/utils/getActivityLivestreamingMetadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { WebChatActivity } from '../types/WebChatActivity';
import getActivityLivestreamingMetadata from './getActivityLivestreamingMetadata';

describe.each([['with "streamId"' as const], ['without "streamId"' as const]])('activity %s', variant => {
describe('activity with "streamType" of "streaming"', () => {
describe('activity with "streamType" of "streaming" (channelData)', () => {
let activity: WebChatActivity;

beforeEach(() => {
Expand Down Expand Up @@ -32,7 +32,7 @@ describe.each([['with "streamId"' as const], ['without "streamId"' as const]])('
}
});

describe('activity with "streamType" of "informative message"', () => {
describe('activity with "streamType" of "informative message" (channelData)', () => {
let activity: WebChatActivity;

beforeEach(() => {
Expand Down Expand Up @@ -62,7 +62,7 @@ describe.each([['with "streamId"' as const], ['without "streamId"' as const]])('
}
});

describe('activity with "streamType" of "final"', () => {
describe('activity with "streamType" of "final" (channelData)', () => {
let activity: WebChatActivity;

beforeEach(() => {
Expand Down Expand Up @@ -91,22 +91,133 @@ describe.each([['with "streamId"' as const], ['without "streamId"' as const]])('
});
});

describe.each([['with "streamId"' as const], ['without "streamId"' as const]])('activity %s', variant => {
describe('activity with "streamType" of "streaming" (entities)', () => {
let activity: WebChatActivity;

beforeEach(() => {
activity = {
entities: [
{
...(variant === 'with "streamId"' ? { streamId: 'a-00001' } : {}),
streamSequence: 1,
streamType: 'streaming',
type: 'streaminfo'
}
],
channelData: {},
id: 'a-00002',
text: 'Hello, World!',
type: 'typing'
} as any;
});

test('should return type of "interim activity"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('type', 'interim activity'));
test('should return sequence number', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sequenceNumber', 1));

if (variant === 'with "streamId"') {
test('should return session ID with value from "entities.streamId"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sessionId', 'a-00001'));
} else {
test('should return session ID with value of "activity.id"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sessionId', 'a-00002'));
}
});

describe('activity with "streamType" of "informative message" (entities)', () => {
let activity: WebChatActivity;

beforeEach(() => {
activity = {
entities: [
{
...(variant === 'with "streamId"' ? { streamId: 'a-00001' } : {}),
streamSequence: 1,
streamType: 'informative',
type: 'streaminfo'
}
],
channelData: {},
id: 'a-00002',
text: 'Hello, World!',
type: 'typing'
} as any;
});

test('should return type of "informative message"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('type', 'informative message'));
test('should return sequence number', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sequenceNumber', 1));

if (variant === 'with "streamId"') {
test('should return session ID with value from "entities.streamId"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sessionId', 'a-00001'));
} else {
test('should return session ID with value of "activity.id"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sessionId', 'a-00002'));
}
});

describe('activity with "streamType" of "final" (entities)', () => {
let activity: WebChatActivity;

beforeEach(() => {
activity = {
entities: [
{
...(variant === 'with "streamId"' ? { streamId: 'a-00001' } : {}),
streamType: 'final',
type: 'streaminfo'
}
],
channelData: {},
id: 'a-00002',
text: 'Hello, World!',
type: 'message'
} as any;
});

if (variant === 'with "streamId"') {
test('should return type of "final activity"', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('type', 'final activity'));
test('should return sequence number of Infinity', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sequenceNumber', Infinity));
test('should return session ID', () =>
expect(getActivityLivestreamingMetadata(activity)).toHaveProperty('sessionId', 'a-00001'));
} else {
// Final activity must have "streamId". Final activity without "streamId" is not a valid livestream activity.
test('should return undefined', () => expect(getActivityLivestreamingMetadata(activity)).toBeUndefined());
}
});
});

test('invalid activity should return undefined', () =>
expect(getActivityLivestreamingMetadata('invalid' as any)).toBeUndefined());

test('activity with "streamType" of "streaming" without critical fields should return undefined', () =>
test('activity with "streamType" of "streaming" without critical fields should return undefined (channelData)', () =>
expect(
getActivityLivestreamingMetadata({
channelData: { streamType: 'streaming' },
type: 'typing'
} as any)
).toBeUndefined());

test('activity with "streamType" of "streaming" without critical fields should return undefined (entities)', () =>
expect(
getActivityLivestreamingMetadata({
entities: [{ streamType: 'streaming', type: 'streaminfo' }],
channelData: {},
type: 'typing'
} as any)
).toBeUndefined());

describe.each([
['integer', 1, true],
['zero', 0, false],
['decimal', 1.234, false]
])('activity with %s "streamSequence" should return undefined', (_, streamSequence, isValid) => {
])('activity with %s "streamSequence" should return undefined (channelData)', (_, streamSequence, isValid) => {
const activity = {
channelData: { streamSequence, streamType: 'streaming' },
id: 'a-00001',
Expand All @@ -121,7 +232,27 @@ describe.each([
}
});

describe('"typing" activity with "streamType" of "final"', () => {
describe.each([
['integer', 1, true],
['zero', 0, false],
['decimal', 1.234, false]
])('activity with %s "streamSequence" should return undefined (entities)', (_, streamSequence, isValid) => {
const activity = {
entities: [{ streamSequence, streamType: 'streaming', type: 'streaminfo' }],
channelData: {},
id: 'a-00001',
text: '',
type: 'typing'
} as any;

if (isValid) {
expect(getActivityLivestreamingMetadata(activity)).toBeTruthy();
} else {
expect(getActivityLivestreamingMetadata(activity)).toBeUndefined();
}
});

describe('"typing" activity with "streamType" of "final" (channelData)', () => {
test('should return undefined if "text" field is defined', () =>
expect(
getActivityLivestreamingMetadata({
Expand All @@ -143,11 +274,45 @@ describe('"typing" activity with "streamType" of "final"', () => {
).toHaveProperty('type', 'final activity'));
});

test('activity with "streamType" of "streaming" without "content" should return type of "contentless"', () =>
describe('"typing" activity with "streamType" of "final" (entities)', () => {
test('should return undefined if "text" field is defined', () =>
expect(
getActivityLivestreamingMetadata({
entities: [{ streamId: 'a-00001', streamType: 'final', type: 'streaminfo' }],
channelData: {},
id: 'a-00002',
text: 'Final "typing" activity, must not have "text".',
type: 'typing'
} as any)
).toBeUndefined());

test('should return truthy if "text" field is not defined', () =>
expect(
getActivityLivestreamingMetadata({
entities: [{ streamId: 'a-00001', streamType: 'final', type: 'streaminfo' }],
channelData: {},
id: 'a-00002',
// Final activity can be "typing" if it does not have "text".
type: 'typing'
} as any)
).toHaveProperty('type', 'final activity'));
});

test('activity with "streamType" of "streaming" without "content" should return type of "contentless" (channelData)', () =>
expect(
getActivityLivestreamingMetadata({
channelData: { streamSequence: 1, streamType: 'streaming' },
id: 'a-00001',
type: 'typing'
} as any)
).toHaveProperty('type', 'contentless'));

test('activity with "streamType" of "streaming" without "content" should return type of "contentless" (entities)', () =>
expect(
getActivityLivestreamingMetadata({
entities: [{ streamSequence: 1, streamType: 'streaming', type: 'streaminfo' }],
channelData: {},
id: 'a-00001',
type: 'typing'
} as any)
).toHaveProperty('type', 'contentless'));
Loading
Loading