-
Notifications
You must be signed in to change notification settings - Fork 60
Improving streaming entity serialization #339
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
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
d3652ff
Consolidating StreamingResponse definitions and slight fixes
rodrigobr-msft e8aa9b2
Improving StreamingResponse tests and formatting
rodrigobr-msft a374ced
Merge branch 'main' into users/robrandao/streaming-response-consolida…
rodrigobr-msft c730a89
Adding tests for StreamingResponse
rodrigobr-msft ead6403
Adding basic StreamingResponse tests
rodrigobr-msft 03ed8f7
Adding integration tests for streaming
rodrigobr-msft 634b608
Finalized basic end-to-end streaming tests
rodrigobr-msft 13dda2a
Addressing Copilot PR review
rodrigobr-msft c121834
Validating channel_id before accessing parent channel
rodrigobr-msft a230b9a
Removing duplicate sample
rodrigobr-msft ee5016c
Another commit
rodrigobr-msft 86686ae
Adding '@type' aliases for AIEntity-related classes
rodrigobr-msft a6a58f8
Fixing further linting issues
rodrigobr-msft df23e5c
Removing exclude_unset=True usagei n reply_to_activity to enable prop…
rodrigobr-msft 904557f
Adding improved serialization for AIEntity and related classes
rodrigobr-msft bb2980c
Readded exclude_unset=True usage in ConversationsOperations
rodrigobr-msft b976eb9
Formatting
rodrigobr-msft e634a91
Removing unused imports
rodrigobr-msft eedfd6c
Small fixes
rodrigobr-msft 8d1b300
Removing unnecessary dummy constructors
rodrigobr-msft f8639c3
Fixing tests
rodrigobr-msft 478651d
Potential fix for pull request finding
rodrigobr-msft c9b25e9
Adding formatting and comment
rodrigobr-msft d77f479
Potential fix for pull request finding
rodrigobr-msft e07bed3
Merge branch 'main' into users/robrandao/streaming-response-consolida…
rodrigobr-msft ea765fd
Reformatting
rodrigobr-msft fe605f3
Merge branch 'users/robrandao/streaming-response-consolidation' of ht…
rodrigobr-msft a8a4a83
Redid serialization/validation logic to allow for extra property alia…
rodrigobr-msft 114b0d9
Another commit
rodrigobr-msft 4a6f770
Addressed fixes to reincorporate snake/camel case aliasing for extra …
rodrigobr-msft 08d7b12
Removed Self annotation unsupported in 3.10
rodrigobr-msft 69e84ec
Removed Self in entity.py
rodrigobr-msft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
404 changes: 0 additions & 404 deletions
404
dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/activity.py
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import pytest | ||
| import asyncio | ||
|
|
||
| from microsoft_agents.activity import ( | ||
| Activity, | ||
| ActivityTypes, | ||
| Channels, | ||
| Entity | ||
| ) | ||
|
|
||
| from microsoft_agents.hosting.core import ( | ||
| TurnContext, | ||
| TurnState, | ||
| ) | ||
|
|
||
| from microsoft_agents.testing import ( | ||
| AgentClient, | ||
| AgentEnvironment, | ||
| AiohttpScenario, | ||
| ) | ||
|
|
||
| FULL_TEXT = "This is a streaming response." | ||
| CHUNKS = FULL_TEXT.split() | ||
|
|
||
| def get_streaminfo(activity: Activity) -> Entity: | ||
| for entity in activity.entities: | ||
| if isinstance(entity, dict) and entity.get("type") == "streaminfo": | ||
| return Entity.model_validate(entity) | ||
| elif isinstance(entity, Entity) and entity.type == "streaminfo": | ||
| return entity | ||
| raise ValueError("No streaminfo entity found") | ||
|
|
||
| async def init_agent(env: AgentEnvironment): | ||
|
|
||
| app = env.agent_application | ||
|
|
||
| @app.message("/stream") | ||
| async def stream_handler(context: TurnContext, state: TurnState): | ||
|
|
||
| assert context.streaming_response is not None | ||
|
|
||
| context.streaming_response.queue_informative_update("Starting stream...") | ||
| await asyncio.sleep(1.0) # Simulate delay before starting stream | ||
|
|
||
| for chunk in CHUNKS[:-1]: | ||
| context.streaming_response.queue_text_chunk(chunk) | ||
| await asyncio.sleep(1.0) # Simulate delay between chunks | ||
|
|
||
| context.streaming_response.queue_text_chunk(CHUNKS[-1]) | ||
| await context.streaming_response.end_stream() | ||
|
|
||
| _SCENARIO = AiohttpScenario(init_agent=init_agent, use_jwt_middleware=False) | ||
|
|
||
| @pytest.mark.asyncio | ||
| @pytest.mark.agent_test(_SCENARIO) | ||
| async def test_basic_streaming_response_non_streaming_channel(agent_client: AgentClient): | ||
|
|
||
| expected_len = len(FULL_TEXT.split()) | ||
|
|
||
| agent_client.template = agent_client.template.with_updates(channel_id=Channels.emulator) | ||
|
|
||
| # give enough time for all the activities to send | ||
| await agent_client.send("/stream", wait=expected_len * 2.0) | ||
|
|
||
| stream_activities = agent_client.select().where( | ||
| entities=lambda x: any(e["type"] == "streaminfo" for e in x) | ||
| ).get() | ||
|
|
||
| assert len(stream_activities) == 1 | ||
|
|
||
| final_streaminfo = get_streaminfo(stream_activities[0]) | ||
|
|
||
| assert final_streaminfo.stream_sequence == 1 | ||
| assert final_streaminfo.stream_type == "final" | ||
| assert stream_activities[0].text == FULL_TEXT.replace(" ", "") | ||
|
|
||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| @pytest.mark.agent_test(_SCENARIO) | ||
| async def test_basic_streaming_response_streaming_channel(agent_client: AgentClient): | ||
|
|
||
| expected_len = len(FULL_TEXT.split()) | ||
|
|
||
| agent_client.template = agent_client.template.with_updates(channel_id=Channels.webchat) | ||
|
|
||
| # give enough time for all the activities to send | ||
| await agent_client.send("/stream", wait=expected_len * 2.0) | ||
|
|
||
| stream_activities = agent_client.select().where( | ||
| entities=lambda x: any(e["type"] == "streaminfo" for e in x) | ||
| ).get() | ||
|
|
||
| assert len(stream_activities) == len(CHUNKS) + 1 | ||
|
|
||
| informative = stream_activities[0] | ||
| informative_streaminfo = get_streaminfo(informative) | ||
|
|
||
| assert informative_streaminfo.stream_type == "informative" | ||
| assert informative_streaminfo.stream_sequence == 1 | ||
| assert informative.text == "Starting stream..." | ||
| assert informative.type == ActivityTypes.typing | ||
|
|
||
| t = "" | ||
| for i, chunk in enumerate(CHUNKS[:-1]): | ||
| t += chunk | ||
|
|
||
| j = i + 1 | ||
|
|
||
| streaminfo = get_streaminfo(stream_activities[j]) | ||
|
|
||
| assert stream_activities[j].text == t | ||
| assert stream_activities[j].type == ActivityTypes.typing | ||
| assert streaminfo.stream_type == "streaming" | ||
| assert streaminfo.stream_sequence == j + 1 | ||
|
|
||
| final_streaminfo = get_streaminfo(stream_activities[-1]) | ||
|
|
||
| assert final_streaminfo.stream_sequence == len(stream_activities) | ||
| assert final_streaminfo.stream_type == "final" | ||
| assert stream_activities[-1].text == FULL_TEXT.replace(" ", "") | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
libraries/microsoft-agents-activity/microsoft_agents/activity/entity/_schema_mixin.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| from typing import Any | ||
| from pydantic import ( | ||
| model_serializer, | ||
| model_validator, | ||
| ModelWrapValidatorHandler, | ||
| SerializerFunctionWrapHandler, | ||
| SerializationInfo, | ||
| BaseModel, | ||
| ) | ||
|
|
||
|
|
||
| def validate_schema_model(data: Any, handler: ModelWrapValidatorHandler): | ||
| """Custom validator to handle the aliases @type, @context, and @id if defined in the destination type.""" | ||
| model = handler(data) | ||
| if isinstance(data, dict): | ||
| if "@type" in data: | ||
| setattr(model, "at_type", data["@type"]) | ||
| if "@context" in data: | ||
| setattr(model, "at_context", data["@context"]) | ||
| if "@id" in data: | ||
rodrigobr-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| setattr(model, "at_id", data["@id"]) | ||
| return model | ||
|
|
||
|
|
||
| def serialize_schema_model( | ||
| self, handler: SerializerFunctionWrapHandler, info: SerializationInfo | ||
| ) -> dict[str, Any]: | ||
| """Custom serializer to convert keys to force inclusion of @type, @context, and @id if defined.""" | ||
| serialized = handler(self) | ||
| if info.by_alias: | ||
| if hasattr(self, "at_type"): | ||
| serialized["@type"] = getattr(self, "at_type") | ||
| if hasattr(self, "at_context"): | ||
| serialized["@context"] = getattr(self, "at_context") | ||
| if hasattr(self, "at_id"): | ||
| serialized["@id"] = getattr(self, "at_id") | ||
| return serialized | ||
rodrigobr-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| class _SchemaMixin(BaseModel): | ||
| """Mixin class to force inclusion of @property fields when serializing.""" | ||
|
|
||
| @model_validator(mode="wrap") | ||
| @classmethod | ||
| def _validate_model(cls, data: Any, handler: ModelWrapValidatorHandler): | ||
| return validate_schema_model(data, handler) | ||
|
|
||
| @model_serializer(mode="wrap") | ||
| def _serialize_model( | ||
| self, handler: SerializerFunctionWrapHandler, info: SerializationInfo | ||
| ) -> dict[str, Any]: | ||
| return serialize_schema_model(self, handler, info) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.