From 7755968aecb722ef6beec1fa8988852e0f2a2f91 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 14 Jul 2020 11:08:37 -0400 Subject: [PATCH 01/21] have ta client hold onto its api version in a private property --- .../azure/ai/textanalytics/_base_client.py | 6 ++--- .../textanalytics/aio/_base_client_async.py | 6 ++--- .../tests/test_multiapi.py | 25 ++++++++----------- .../tests/test_multiapi_async.py | 23 ++++++++--------- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_base_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_base_client.py index 3fe9927fbd0c..98d93e3785e2 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_base_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_base_client.py @@ -8,7 +8,7 @@ from azure.core.credentials import AzureKeyCredential from ._policies import TextAnalyticsResponseHookPolicy from ._user_agent import USER_AGENT -from ._multiapi import load_generated_api +from ._multiapi import load_generated_api, ApiVersion def _authentication_policy(credential): authentication_policy = None @@ -26,8 +26,8 @@ def _authentication_policy(credential): class TextAnalyticsClientBase(object): def __init__(self, endpoint, credential, **kwargs): - api_version = kwargs.pop("api_version", None) - _TextAnalyticsClient = load_generated_api(api_version) + self._api_version = kwargs.pop("api_version", ApiVersion.V3_0) + _TextAnalyticsClient = load_generated_api(self._api_version) self._client = _TextAnalyticsClient( endpoint=endpoint, credential=credential, diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_base_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_base_client_async.py index 2d97f49c3ee6..6e05b39645e5 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_base_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_base_client_async.py @@ -8,7 +8,7 @@ from azure.core.pipeline.policies import AzureKeyCredentialPolicy from ._policies_async import AsyncTextAnalyticsResponseHookPolicy from .._user_agent import USER_AGENT -from .._multiapi import load_generated_api +from .._multiapi import load_generated_api, ApiVersion def _authentication_policy(credential): @@ -27,8 +27,8 @@ def _authentication_policy(credential): class AsyncTextAnalyticsClientBase(object): def __init__(self, endpoint, credential, **kwargs): - api_version = kwargs.pop("api_version", None) - _TextAnalyticsClient = load_generated_api(api_version, aio=True) + self._api_version = kwargs.pop("api_version", ApiVersion.V3_0) + _TextAnalyticsClient = load_generated_api(self._api_version, aio=True) self._client = _TextAnalyticsClient( endpoint=endpoint, credential=credential, diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py index fd9d267d8849..9737e821c252 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py @@ -3,29 +3,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from azure.core.credentials import AzureKeyCredential -from azure.ai.textanalytics import TextAnalyticsClient, ApiVersion +import functools +from azure.ai.textanalytics import ApiVersion, TextAnalyticsClient from testcase import TextAnalyticsTest, GlobalTextAnalyticsAccountPreparer +from testcase import TextAnalyticsClientPreparer as _TextAnalyticsClientPreparer + +TextAnalyticsClientPreparer = functools.partial(_TextAnalyticsClientPreparer, TextAnalyticsClient) class TestRecognizeEntities(TextAnalyticsTest): @GlobalTextAnalyticsAccountPreparer() - def test_default_api_version(self, resource_group, location, text_analytics_account, text_analytics_account_key): - credential = AzureKeyCredential(text_analytics_account_key) - client = TextAnalyticsClient(text_analytics_account, credential) - + @TextAnalyticsClientPreparer() + def test_default_api_version(self, client): assert "v3.0" in client._client._client._base_url @GlobalTextAnalyticsAccountPreparer() - def test_v3_0_api_version(self, resource_group, location, text_analytics_account, text_analytics_account_key): - credential = AzureKeyCredential(text_analytics_account_key) - client = TextAnalyticsClient(text_analytics_account, credential, api_version=ApiVersion.V3_0) - + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) + def test_v3_0_api_version(self, client): assert "v3.0" in client._client._client._base_url @GlobalTextAnalyticsAccountPreparer() - def test_v3_1_preview_1_api_version(self, resource_group, location, text_analytics_account, text_analytics_account_key): - credential = AzureKeyCredential(text_analytics_account_key) - client = TextAnalyticsClient(text_analytics_account, credential, api_version=ApiVersion.V3_1_preview_1) - + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + def test_v3_1_preview_1_api_version(self, client): assert "v3.1-preview.1" in client._client._client._base_url \ No newline at end of file diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py index 5493fdc9087b..f23a9a88f87c 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py @@ -3,30 +3,27 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from azure.core.credentials import AzureKeyCredential +import functools from azure.ai.textanalytics import ApiVersion from azure.ai.textanalytics.aio import TextAnalyticsClient from testcase import TextAnalyticsTest, GlobalTextAnalyticsAccountPreparer +from testcase import TextAnalyticsClientPreparer as _TextAnalyticsClientPreparer + +TextAnalyticsClientPreparer = functools.partial(_TextAnalyticsClientPreparer, TextAnalyticsClient) class TestRecognizeEntities(TextAnalyticsTest): @GlobalTextAnalyticsAccountPreparer() - def test_default_api_version(self, resource_group, location, text_analytics_account, text_analytics_account_key): - credential = AzureKeyCredential(text_analytics_account_key) - client = TextAnalyticsClient(text_analytics_account, credential) - + @TextAnalyticsClientPreparer() + def test_default_api_version(self, client): assert "v3.0" in client._client._client._base_url @GlobalTextAnalyticsAccountPreparer() - def test_v3_0_api_version(self, resource_group, location, text_analytics_account, text_analytics_account_key): - credential = AzureKeyCredential(text_analytics_account_key) - client = TextAnalyticsClient(text_analytics_account, credential, api_version=ApiVersion.V3_0) - + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) + def test_v3_0_api_version(self, client): assert "v3.0" in client._client._client._base_url @GlobalTextAnalyticsAccountPreparer() - def test_v3_1_preview_1_api_version(self, resource_group, location, text_analytics_account, text_analytics_account_key): - credential = AzureKeyCredential(text_analytics_account_key) - client = TextAnalyticsClient(text_analytics_account, credential, api_version=ApiVersion.V3_1_preview_1) - + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + def test_v3_1_preview_1_api_version(self, client): assert "v3.1-preview.1" in client._client._client._base_url \ No newline at end of file From 43e72dc3045bc946309a4ca97d6c6235cba14a0e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 14 Jul 2020 14:59:42 -0400 Subject: [PATCH 02/21] implement design for opinion mining --- .../azure/ai/textanalytics/_models.py | 148 +++++++++++++++++- .../azure/ai/textanalytics/_multiapi.py | 4 - .../ai/textanalytics/_response_handlers.py | 14 +- .../textanalytics/_text_analytics_client.py | 20 +++ .../aio/_text_analytics_client_async.py | 20 +++ 5 files changed, 187 insertions(+), 19 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index a6bdf3e80a14..9c69904c9ee2 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -3,9 +3,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ - -from ._generated.v3_0.models._models import LanguageInput -from ._generated.v3_0.models._models import MultiLanguageInput +import re +from ._generated.v3_0.models._models import ( + LanguageInput, + MultiLanguageInput +) class DictMixin(object): @@ -635,19 +637,30 @@ class SentenceSentiment(DictMixin): and 1 for the sentence for all labels. :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores + :ivar aspects: The list of aspects of the sentence. An aspect is a + key attribute of a sentence, for example the attributes of a product + or a service. Only returned if `show_aspects` is set to True in + call to `analyze_sentiment` + :vartype aspects: + list[~azure.ai.textanalytics.SentenceAspect] """ def __init__(self, **kwargs): self.text = kwargs.get("text", None) self.sentiment = kwargs.get("sentiment", None) self.confidence_scores = kwargs.get("confidence_scores", None) + self.aspects = kwargs.get("aspects", None) @classmethod - def _from_generated(cls, sentence): + def _from_generated(cls, sentence, results): return cls( text=sentence.text, sentiment=sentence.sentiment, confidence_scores=SentimentConfidenceScores._from_generated(sentence.confidence_scores), # pylint: disable=protected-access + aspects=( + [SentenceAspect._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access + if hasattr(sentence, "aspects") else None + ) ) def __repr__(self): @@ -658,6 +671,125 @@ def __repr__(self): )[:1024] +class SentenceAspect(DictMixin): + """SentenceAspect contains the related opinions, predicted sentiment, + confidence scores and other information about an aspect of a sentence. + An aspect of a sentence is a key component of a sentence, for example + in the sentence "The food is good", "food" is the aspect. + + :ivar str text: The aspect text. + :ivar str sentiment: The predicted Sentiment for the aspect. Possible values + include 'positive', 'mixed', and 'negative'. + :ivar confidence_scores: The sentiment confidence score between 0 + and 1 for the aspect for 'positive' and 'negative' labels. It's score + for 'neutral' will always be 0 + :vartype confidence_scores: + ~azure.ai.textanalytics.SentimentConfidenceScores + :ivar opinions: All of the opinions in the sentence related to this aspect. + :vartype opinions: list[~azure.ai.textanalytics.AspectOpinion] + :ivar int offset: The aspect offset from the start of the sentence. + :ivar int length: The lenght of the aspect. + """ + + def __init__(self, **kwargs): + self.text = kwargs.get("text", None) + self.sentiment = kwargs.get("sentiment", None) + self.confidence_scores = kwargs.get("confidence_scores", None) + self.opinions = kwargs.get("opinions", None) + self.offset = kwargs.get("offset", None) + self.length = kwargs.get("length", None) + + @staticmethod + def _get_opinions(relations, results): + if not relations: + return [] + opinion_relations = [r.ref for r in relations if r.relation_type == "opinion"] + opinions = [] + for opinion_relation in opinion_relations: + nums = [int(s) for s in re.findall(r"\d+", opinion_relation)] + document_index = nums[0] + sentence_index = nums[1] + opinion_index = nums[2] + opinions.append( + results[document_index].sentences[sentence_index].opinions[opinion_index] + ) + return opinions + + + @classmethod + def _from_generated(cls, aspect, results): + return cls( + text=aspect.text, + sentiment=aspect.sentiment, + confidence_scores=SentimentConfidenceScores._from_generated(aspect.confidence_scores), # pylint: disable=protected-access + opinions=[ + AspectOpinion._from_generated(opinion) for opinion in cls._get_opinions(aspect.relations, results) # pylint: disable=protected-access + ], + offset=aspect.offset, + length=aspect.length + ) + + def __repr__(self): + return "SentenceAspect(text={}, sentiment={}, confidence_scores={}, opinions={}, offset={}, length={})".format( + self.text, + self.sentiment, + repr(self.confidence_scores), + repr(self.opinions), + self.offset, + self.length + )[:1024] + + +class AspectOpinion(DictMixin): + """AspectOpinion contains the predicted sentiment, + confidence scores and other information about an opinion of an aspect. + For example, in the sentence "The food is good", the opinion of the + aspect 'food' is 'good'. + + :ivar str text: The opinion text. + :ivar str sentiment: The predicted Sentiment for the opinion. Possible values + include 'positive', 'mixed', and 'negative'. + :ivar confidence_scores: The sentiment confidence score between 0 + and 1 for the opinion for 'positive' and 'negative' labels. It's score + for 'neutral' will always be 0 + :vartype confidence_scores: + ~azure.ai.textanalytics.SentimentConfidenceScores + :ivar int offset: The opinion offset from the start of the sentence. + :ivar int length: The lenght of the opinion. + :ivar bool is_negated: Whether the opinion is negated. For example, in + "The food is not good", the opinion "good" is negated. + """ + + def __init__(self, **kwargs): + self.text = kwargs.get("text", None) + self.sentiment = kwargs.get("sentiment", None) + self.confidence_scores = kwargs.get("confidence_scores", None) + self.offset = kwargs.get("offset", None) + self.length = kwargs.get("length", None) + self.is_negated = kwargs.get("is_negated", None) + + @classmethod + def _from_generated(cls, opinion): + return cls( + text=opinion.text, + sentiment=opinion.sentiment, + confidence_scores=SentimentConfidenceScores._from_generated(opinion.confidence_scores), # pylint: disable=protected-access + offset=opinion.offset, + length=opinion.length, + is_negated=opinion.is_negated + ) + + def __repr__(self): + return "AspectOpinion(text={}, sentiment={}, confidence_scores={}, offset={}, length={}, is_negated={})".format( + self.text, + self.sentiment, + repr(self.confidence_scores), + self.offset, + self.length, + self.is_negated + )[:1024] + + class SentimentConfidenceScores(DictMixin): """The confidence scores (Softmax scores) between 0 and 1. Higher values indicate higher confidence. @@ -671,15 +803,15 @@ class SentimentConfidenceScores(DictMixin): """ def __init__(self, **kwargs): - self.positive = kwargs.get('positive', None) - self.neutral = kwargs.get('neutral', None) - self.negative = kwargs.get('negative', None) + self.positive = kwargs.get('positive', 0.0) + self.neutral = kwargs.get('neutral', 0.0) + self.negative = kwargs.get('negative', 0.0) @classmethod def _from_generated(cls, score): return cls( positive=score.positive, - neutral=score.neutral, + neutral=score.neutral if hasattr(score, "netural") else 0.0, negative=score.negative ) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py index c3d1f548a989..3acad28a6d65 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py @@ -17,11 +17,7 @@ class ApiVersion(str, Enum): V3_0 = "v3.0" -DEFAULT_VERSION = ApiVersion.V3_0 - - def load_generated_api(api_version, aio=False): - api_version = api_version or DEFAULT_VERSION try: # api_version could be a string; map it to an instance of ApiVersion # (this is a no-op if it's already an instance of ApiVersion) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_response_handlers.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_response_handlers.py index 0bef54fa0c1c..9ecef39554e6 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_response_handlers.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_response_handlers.py @@ -106,14 +106,14 @@ def wrapper(response, obj, response_headers): # pylint: disable=unused-argument if hasattr(item, "error"): results[idx] = DocumentError(id=item.id, error=TextAnalyticsError._from_generated(item.error)) # pylint: disable=protected-access else: - results[idx] = func(item) + results[idx] = func(item, results) return results return wrapper @prepare_result -def language_result(language): +def language_result(language, results): # pylint: disable=unused-argument return DetectLanguageResult( id=language.id, primary_language=DetectedLanguage._from_generated(language.detected_language), # pylint: disable=protected-access @@ -123,7 +123,7 @@ def language_result(language): @prepare_result -def entities_result(entity): +def entities_result(entity, results): # pylint: disable=unused-argument return RecognizeEntitiesResult( id=entity.id, entities=[CategorizedEntity._from_generated(e) for e in entity.entities], # pylint: disable=protected-access @@ -133,7 +133,7 @@ def entities_result(entity): @prepare_result -def linked_entities_result(entity): +def linked_entities_result(entity, results): # pylint: disable=unused-argument return RecognizeLinkedEntitiesResult( id=entity.id, entities=[LinkedEntity._from_generated(e) for e in entity.entities], # pylint: disable=protected-access @@ -143,7 +143,7 @@ def linked_entities_result(entity): @prepare_result -def key_phrases_result(phrases): +def key_phrases_result(phrases, results): # pylint: disable=unused-argument return ExtractKeyPhrasesResult( id=phrases.id, key_phrases=phrases.key_phrases, @@ -153,12 +153,12 @@ def key_phrases_result(phrases): @prepare_result -def sentiment_result(sentiment): +def sentiment_result(sentiment, results): return AnalyzeSentimentResult( id=sentiment.id, sentiment=sentiment.sentiment, warnings=[TextAnalyticsWarning._from_generated(w) for w in sentiment.warnings], # pylint: disable=protected-access statistics=TextDocumentStatistics._from_generated(sentiment.statistics), # pylint: disable=protected-access confidence_scores=SentimentConfidenceScores._from_generated(sentiment.confidence_scores), # pylint: disable=protected-access - sentences=[SentenceSentiment._from_generated(s) for s in sentiment.sentences], # pylint: disable=protected-access + sentences=[SentenceSentiment._from_generated(s, results) for s in sentiment.sentences], # pylint: disable=protected-access ) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 15cc197d740c..702c52a4e195 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -378,6 +378,11 @@ def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] + :keyword bool show_aspects: Whether to conduct aspect-based sentiment analysis. + Aspect-based sentiment analysis provides more granular + information about the opinions related to aspects (which are the attributes of products or services) + in text. If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects + will have property `aspects` containing the result of this analysis :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. If not set, uses "en" for English as default. Per-document language will @@ -408,11 +413,26 @@ def analyze_sentiment( # type: ignore docs = _validate_batch_input(documents, "language", language) model_version = kwargs.pop("model_version", None) show_stats = kwargs.pop("show_stats", False) + show_aspects = kwargs.pop("show_aspects", None) + try: + if self._api_version == "v3.0": + if show_aspects is not None: + raise TypeError( + "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" + ) + return self._client.sentiment( + documents=docs, + model_version=model_version, + show_stats=show_stats, + cls=kwargs.pop("cls", sentiment_result), + **kwargs + ) return self._client.sentiment( documents=docs, model_version=model_version, show_stats=show_stats, + opinion_mining=show_aspects, cls=kwargs.pop("cls", sentiment_result), **kwargs ) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index 90f12fb9837d..541143f7b71f 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -378,6 +378,11 @@ async def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] + :keyword bool show_aspects: Whether to conduct aspect-based sentiment analysis. + Aspect-based sentiment analysis provides more granular + information about the opinions related to aspects (which are the attributes of products or services) + in text. If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects + will have property `aspects` containing the result of this analysis :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. If not set, uses "en" for English as default. Per-document language will @@ -408,11 +413,26 @@ async def analyze_sentiment( # type: ignore docs = _validate_batch_input(documents, "language", language) model_version = kwargs.pop("model_version", None) show_stats = kwargs.pop("show_stats", False) + show_aspects = kwargs.pop("show_aspects", None) + try: + if self._api_version == "v3.0": + if show_aspects is not None: + raise TypeError( + "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" + ) + return await self._client.sentiment( + documents=docs, + model_version=model_version, + show_stats=show_stats, + cls=kwargs.pop("cls", sentiment_result), + **kwargs + ) return await self._client.sentiment( documents=docs, model_version=model_version, show_stats=show_stats, + opinion_mining=show_aspects, cls=kwargs.pop("cls", sentiment_result), **kwargs ) From a3b45100ffab64783dcc1a939ea8b0a4f57bfa4a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 14 Jul 2020 14:59:49 -0400 Subject: [PATCH 03/21] add tests --- ....test_aspect_based_sentiment_analysis.yaml | 44 +++++++++ ...ed_sentiment_analysis_negated_opinion.yaml | 44 +++++++++ ....test_aspect_based_sentiment_analysis.yaml | 33 +++++++ ...ed_sentiment_analysis_negated_opinion.yaml | 33 +++++++ .../tests/test_analyze_sentiment.py | 92 ++++++++++++++++++- .../tests/test_analyze_sentiment_async.py | 92 ++++++++++++++++++- .../tests/test_text_analytics.py | 32 +++++++ .../azure-ai-textanalytics/tests/testcase.py | 10 ++ 8 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml new file mode 100644 index 000000000000..d8b23aa54c34 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml @@ -0,0 +1,44 @@ +interactions: +- request: + body: '{"documents": [{"id": "0", "text": "It has a sleek premium aluminum design + that makes it beautiful to look at.", "language": "en"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '132' + Content-Type: + - application/json + User-Agent: + - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + method: POST + uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true + response: + body: + string: '{"documents":[{"id":"0","sentiment":"positive","confidenceScores":{"positive":0.98,"neutral":0.02,"negative":0.0},"sentences":[{"sentiment":"positive","confidenceScores":{"positive":0.98,"neutral":0.02,"negative":0.0},"offset":0,"length":74,"text":"It + has a sleek premium aluminum design that makes it beautiful to look at.","aspects":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":32,"length":6,"text":"design","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"},{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/1"}]}],"opinions":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":9,"length":5,"text":"sleek","isNegated":false},{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":15,"length":7,"text":"premium","isNegated":false}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' + headers: + apim-request-id: + - c0d3172f-8ac0-4cd0-b1d8-27116cb5b854 + content-type: + - application/json; charset=utf-8 + csp-billing-usage: + - CognitiveServices.TextAnalytics.BatchScoring=1 + date: + - Tue, 14 Jul 2020 17:43:15 GMT + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '98' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml new file mode 100644 index 000000000000..73eba57a49eb --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml @@ -0,0 +1,44 @@ +interactions: +- request: + body: '{"documents": [{"id": "0", "text": "The food and service is not good", + "language": "en"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '90' + Content-Type: + - application/json + User-Agent: + - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + method: POST + uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true + response: + body: + string: '{"documents":[{"id":"0","sentiment":"negative","confidenceScores":{"positive":0.0,"neutral":0.0,"negative":1.0},"sentences":[{"sentiment":"negative","confidenceScores":{"positive":0.0,"neutral":0.0,"negative":1.0},"offset":0,"length":32,"text":"The + food and service is not good","aspects":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":4,"length":4,"text":"food","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]},{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":13,"length":7,"text":"service","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]}],"opinions":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":28,"length":4,"text":"good","isNegated":true}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' + headers: + apim-request-id: + - 628974f5-51d3-4a84-958b-9e0a695dbf47 + content-type: + - application/json; charset=utf-8 + csp-billing-usage: + - CognitiveServices.TextAnalytics.BatchScoring=1 + date: + - Tue, 14 Jul 2020 17:43:16 GMT + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '102' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml new file mode 100644 index 000000000000..5ff518c4daa0 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml @@ -0,0 +1,33 @@ +interactions: +- request: + body: '{"documents": [{"id": "0", "text": "It has a sleek premium aluminum design + that makes it beautiful to look at.", "language": "en"}]}' + headers: + Accept: + - application/json + Content-Length: + - '132' + Content-Type: + - application/json + User-Agent: + - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + method: POST + uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true + response: + body: + string: '{"documents":[{"id":"0","sentiment":"positive","confidenceScores":{"positive":0.98,"neutral":0.02,"negative":0.0},"sentences":[{"sentiment":"positive","confidenceScores":{"positive":0.98,"neutral":0.02,"negative":0.0},"offset":0,"length":74,"text":"It + has a sleek premium aluminum design that makes it beautiful to look at.","aspects":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":32,"length":6,"text":"design","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"},{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/1"}]}],"opinions":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":9,"length":5,"text":"sleek","isNegated":false},{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":15,"length":7,"text":"premium","isNegated":false}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' + headers: + apim-request-id: 622b5442-6a46-4d23-8a95-c46265b09262 + content-type: application/json; charset=utf-8 + csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 + date: Tue, 14 Jul 2020 17:44:20 GMT + strict-transport-security: max-age=31536000; includeSubDomains; preload + transfer-encoding: chunked + x-content-type-options: nosniff + x-envoy-upstream-service-time: '93' + status: + code: 200 + message: OK + url: https://westus2.api.cognitive.microsoft.com//text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml new file mode 100644 index 000000000000..9b5f12af6341 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml @@ -0,0 +1,33 @@ +interactions: +- request: + body: '{"documents": [{"id": "0", "text": "The food and service is not good", + "language": "en"}]}' + headers: + Accept: + - application/json + Content-Length: + - '90' + Content-Type: + - application/json + User-Agent: + - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + method: POST + uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true + response: + body: + string: '{"documents":[{"id":"0","sentiment":"negative","confidenceScores":{"positive":0.0,"neutral":0.0,"negative":1.0},"sentences":[{"sentiment":"negative","confidenceScores":{"positive":0.0,"neutral":0.0,"negative":1.0},"offset":0,"length":32,"text":"The + food and service is not good","aspects":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":4,"length":4,"text":"food","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]},{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":13,"length":7,"text":"service","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]}],"opinions":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":28,"length":4,"text":"good","isNegated":true}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' + headers: + apim-request-id: b2622874-68b3-442e-8b03-aa9ab9b4ed63 + content-type: application/json; charset=utf-8 + csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 + date: Tue, 14 Jul 2020 17:44:19 GMT + strict-transport-security: max-age=31536000; includeSubDomains; preload + transfer-encoding: chunked + x-content-type-options: nosniff + x-envoy-upstream-service-time: '102' + status: + code: 200 + message: OK + url: https://westus2.api.cognitive.microsoft.com//text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index ff1b06f49e71..a203eea0b5d4 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -15,7 +15,8 @@ from azure.ai.textanalytics import ( TextAnalyticsClient, TextDocumentInput, - VERSION + VERSION, + ApiVersion ) # pre-apply the client_cls positional argument so it needn't be explicitly passed below @@ -573,3 +574,92 @@ def callback(pipeline_response, deserialized, _): cls=callback ) assert res == "cls result" + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + def test_aspect_based_sentiment_analysis(self, client): + documents = [ + "It has a sleek premium aluminum design that makes it beautiful to look at." + ] + + document = client.analyze_sentiment(documents=documents, show_aspects=True)[0] + + for sentence in document.sentences: + for aspect in sentence.aspects: + self.assertEqual('design', aspect.text) + self.assertEqual('positive', aspect.sentiment) + self.assertEqual(1.0, aspect.confidence_scores.positive) + self.assertEqual(0.0, aspect.confidence_scores.neutral) + self.assertEqual(0.0, aspect.confidence_scores.negative) + self.assertEqual(32, aspect.offset) + self.assertEqual(6, aspect.length) + + sleek_opinion = aspect.opinions[0] + self.assertEqual('sleek', sleek_opinion.text) + self.assertEqual('positive', sleek_opinion.sentiment) + self.assertEqual(1.0, sleek_opinion.confidence_scores.positive) + self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) + self.assertEqual(0.0, sleek_opinion.confidence_scores.negative) + self.assertEqual(9, sleek_opinion.offset) + self.assertEqual(5, sleek_opinion.length) + self.assertFalse(sleek_opinion.is_negated) + + premium_opinion = aspect.opinions[1] + self.assertEqual('premium', premium_opinion.text) + self.assertEqual('positive', premium_opinion.sentiment) + self.assertEqual(1.0, premium_opinion.confidence_scores.positive) + self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) + self.assertEqual(0.0, premium_opinion.confidence_scores.negative) + self.assertEqual(15, premium_opinion.offset) + self.assertEqual(7, premium_opinion.length) + self.assertFalse(premium_opinion.is_negated) + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + def test_aspect_based_sentiment_analysis_negated_opinion(self, client): + documents = [ + "The food and service is not good" + ] + + document = client.analyze_sentiment(documents=documents, show_aspects=True)[0] + + for sentence in document.sentences: + food_aspect = sentence.aspects[0] + service_aspect = sentence.aspects[1] + + self.assertEqual('food', food_aspect.text) + self.assertEqual('negative', food_aspect.sentiment) + self.assertEqual(0.01, food_aspect.confidence_scores.positive) + self.assertEqual(0.0, food_aspect.confidence_scores.neutral) + self.assertEqual(0.99, food_aspect.confidence_scores.negative) + self.assertEqual(4, food_aspect.offset) + self.assertEqual(4, food_aspect.length) + + self.assertEqual('service', service_aspect.text) + self.assertEqual('negative', service_aspect.sentiment) + self.assertEqual(0.01, service_aspect.confidence_scores.positive) + self.assertEqual(0.0, service_aspect.confidence_scores.neutral) + self.assertEqual(0.99, service_aspect.confidence_scores.negative) + self.assertEqual(13, service_aspect.offset) + self.assertEqual(7, service_aspect.length) + + food_opinion = food_aspect.opinions[0] + service_opinion = service_aspect.opinions[0] + self.assertOpinionsEqual(food_opinion, service_opinion) + + self.assertEqual('good', food_opinion.text) + self.assertEqual('negative', food_opinion.sentiment) + self.assertEqual(0.01, food_opinion.confidence_scores.positive) + self.assertEqual(0.0, food_opinion.confidence_scores.neutral) + self.assertEqual(0.99, food_opinion.confidence_scores.negative) + self.assertEqual(28, food_opinion.offset) + self.assertEqual(4, food_opinion.length) + self.assertTrue(food_opinion.is_negated) + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + def test_aspect_based_sentiment_analysis_v3(self, client): + with pytest.raises(TypeError) as excinfo: + client.analyze_sentiment(["will fail"], show_aspects=True) + + assert "'show_aspects' is only added for API version v3.1-preview.1 and up" in str(excinfo.value) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index 48b36ae9a055..51b397aaced4 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -16,7 +16,8 @@ from azure.ai.textanalytics import ( VERSION, DetectLanguageInput, - TextDocumentInput + TextDocumentInput, + ApiVersion ) from testcase import GlobalTextAnalyticsAccountPreparer @@ -589,3 +590,92 @@ def callback(pipeline_response, deserialized, _): cls=callback ) assert res == "cls result" + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + async def test_aspect_based_sentiment_analysis(self, client): + documents = [ + "It has a sleek premium aluminum design that makes it beautiful to look at." + ] + + document = (await client.analyze_sentiment(documents=documents, show_aspects=True))[0] + + for sentence in document.sentences: + for aspect in sentence.aspects: + self.assertEqual('design', aspect.text) + self.assertEqual('positive', aspect.sentiment) + self.assertEqual(1.0, aspect.confidence_scores.positive) + self.assertEqual(0.0, aspect.confidence_scores.neutral) + self.assertEqual(0.0, aspect.confidence_scores.negative) + self.assertEqual(32, aspect.offset) + self.assertEqual(6, aspect.length) + + sleek_opinion = aspect.opinions[0] + self.assertEqual('sleek', sleek_opinion.text) + self.assertEqual('positive', sleek_opinion.sentiment) + self.assertEqual(1.0, sleek_opinion.confidence_scores.positive) + self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) + self.assertEqual(0.0, sleek_opinion.confidence_scores.negative) + self.assertEqual(9, sleek_opinion.offset) + self.assertEqual(5, sleek_opinion.length) + self.assertFalse(sleek_opinion.is_negated) + + premium_opinion = aspect.opinions[1] + self.assertEqual('premium', premium_opinion.text) + self.assertEqual('positive', premium_opinion.sentiment) + self.assertEqual(1.0, premium_opinion.confidence_scores.positive) + self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) + self.assertEqual(0.0, premium_opinion.confidence_scores.negative) + self.assertEqual(15, premium_opinion.offset) + self.assertEqual(7, premium_opinion.length) + self.assertFalse(premium_opinion.is_negated) + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): + documents = [ + "The food and service is not good" + ] + + document = (await client.analyze_sentiment(documents=documents, show_aspects=True))[0] + + for sentence in document.sentences: + food_aspect = sentence.aspects[0] + service_aspect = sentence.aspects[1] + + self.assertEqual('food', food_aspect.text) + self.assertEqual('negative', food_aspect.sentiment) + self.assertEqual(0.01, food_aspect.confidence_scores.positive) + self.assertEqual(0.0, food_aspect.confidence_scores.neutral) + self.assertEqual(0.99, food_aspect.confidence_scores.negative) + self.assertEqual(4, food_aspect.offset) + self.assertEqual(4, food_aspect.length) + + self.assertEqual('service', service_aspect.text) + self.assertEqual('negative', service_aspect.sentiment) + self.assertEqual(0.01, service_aspect.confidence_scores.positive) + self.assertEqual(0.0, service_aspect.confidence_scores.neutral) + self.assertEqual(0.99, service_aspect.confidence_scores.negative) + self.assertEqual(13, service_aspect.offset) + self.assertEqual(7, service_aspect.length) + + food_opinion = food_aspect.opinions[0] + service_opinion = service_aspect.opinions[0] + self.assertOpinionsEqual(food_opinion, service_opinion) + + self.assertEqual('good', food_opinion.text) + self.assertEqual('negative', food_opinion.sentiment) + self.assertEqual(0.01, food_opinion.confidence_scores.positive) + self.assertEqual(0.0, food_opinion.confidence_scores.neutral) + self.assertEqual(0.99, food_opinion.confidence_scores.negative) + self.assertEqual(28, food_opinion.offset) + self.assertEqual(4, food_opinion.length) + self.assertTrue(food_opinion.is_negated) + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + async def test_aspect_based_sentiment_analysis_v3(self, client): + with pytest.raises(TypeError) as excinfo: + await client.analyze_sentiment(["will fail"], show_aspects=True) + + assert "'show_aspects' is only added for API version v3.1-preview.1 and up" in str(excinfo.value) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py index e424d8d67356..4c99b549fc07 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py @@ -110,6 +110,26 @@ def test_repr(self): transaction_count=4 ) + aspect_opinion_confidence_score = _models.SentimentConfidenceScores(positive=0.5, negative=0.5) + + aspect_opinion = _models.AspectOpinion( + text="opinion", + sentiment="positive", + confidence_scores=aspect_opinion_confidence_score, + offset=3, + length=7, + is_negated=False + ) + + sentence_aspect = _models.SentenceAspect( + text="aspect", + sentiment="positive", + confidence_scores=aspect_opinion_confidence_score, + offset=10, + length=6, + opinions=[aspect_opinion] + ) + self.assertEqual("DetectedLanguage(name=English, iso6391_name=en, confidence_score=1.0)", repr(detected_language)) self.assertEqual("CategorizedEntity(text=Bill Gates, category=Person, subcategory=Age, confidence_score=0.899)", repr(categorized_entity)) @@ -169,6 +189,18 @@ def test_repr(self): self.assertEqual("TextDocumentBatchStatistics(document_count=1, valid_document_count=2, " "erroneous_document_count=3, transaction_count=4)", repr(text_document_batch_statistics)) + aspect_opinion_repr = ( + "AspectOpinion(text=opinion, sentiment=positive, confidence_scores=SentimentConfidenceScores(" + "positive=0.5, neutral=0.0, negative=0.5), offset=3, length=7, is_negated=False)" + ) + self.assertEqual(aspect_opinion_repr, repr(aspect_opinion)) + self.assertEqual( + "SentenceAspect(text=aspect, sentiment=positive, confidence_scores=SentimentConfidenceScores(" + "positive=0.5, neutral=0.0, negative=0.5), opinions=[{}], offset=10, length=6)".format(aspect_opinion_repr), + repr(sentence_aspect) + ) + + def test_inner_error_takes_precedence(self): generated_innererror = _generated_models.InnerError( code="UnsupportedLanguageCode", diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/testcase.py b/sdk/textanalytics/azure-ai-textanalytics/tests/testcase.py index 483d63cabcd3..d8adc379b490 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/testcase.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/testcase.py @@ -51,6 +51,16 @@ def generate_oauth_token(self): def generate_fake_token(self): return FakeTokenCredential() + def assertOpinionsEqual(self, opinion_one, opinion_two): + self.assertEqual(opinion_one.sentiment, opinion_two.sentiment) + self.assertEqual(opinion_one.confidence_scores.positive, opinion_two.confidence_scores.positive) + self.assertEqual(opinion_one.confidence_scores.neutral, opinion_two.confidence_scores.neutral) + self.assertEqual(opinion_one.confidence_scores.negative, opinion_two.confidence_scores.negative) + self.assertEqual(opinion_one.offset, opinion_two.offset) + self.assertEqual(opinion_one.length, opinion_two.length) + self.assertEqual(opinion_one.text, opinion_two.text) + self.assertEqual(opinion_one.is_negated, opinion_two.is_negated) + class GlobalResourceGroupPreparer(AzureMgmtPreparer): def __init__(self): From 32ddecce8352cea25f34d70f347e03f194271997 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 14 Jul 2020 16:58:35 -0400 Subject: [PATCH 04/21] improve docstrings --- .../azure/ai/textanalytics/__init__.py | 8 ++++++-- .../azure/ai/textanalytics/_models.py | 8 ++++---- .../azure/ai/textanalytics/_text_analytics_client.py | 6 +++--- .../ai/textanalytics/aio/_text_analytics_client_async.py | 6 +++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py index 24df553f396d..7347cb826242 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py @@ -25,7 +25,9 @@ LinkedEntityMatch, TextDocumentBatchStatistics, SentenceSentiment, - SentimentConfidenceScores + SentimentConfidenceScores, + SentenceAspect, + AspectOpinion ) __all__ = [ @@ -48,7 +50,9 @@ 'LinkedEntityMatch', 'TextDocumentBatchStatistics', 'SentenceSentiment', - 'SentimentConfidenceScores' + 'SentimentConfidenceScores', + 'SentenceAspect', + 'AspectOpinion' ] __version__ = VERSION diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index 9c69904c9ee2..deac24feabb9 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -638,7 +638,7 @@ class SentenceSentiment(DictMixin): :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores :ivar aspects: The list of aspects of the sentence. An aspect is a - key attribute of a sentence, for example the attributes of a product + key phrase of a sentence, for example the attributes of a product or a service. Only returned if `show_aspects` is set to True in call to `analyze_sentiment` :vartype aspects: @@ -675,7 +675,7 @@ class SentenceAspect(DictMixin): """SentenceAspect contains the related opinions, predicted sentiment, confidence scores and other information about an aspect of a sentence. An aspect of a sentence is a key component of a sentence, for example - in the sentence "The food is good", "food" is the aspect. + in the sentence "The food is good", "food" is an aspect. :ivar str text: The aspect text. :ivar str sentiment: The predicted Sentiment for the aspect. Possible values @@ -688,7 +688,7 @@ class SentenceAspect(DictMixin): :ivar opinions: All of the opinions in the sentence related to this aspect. :vartype opinions: list[~azure.ai.textanalytics.AspectOpinion] :ivar int offset: The aspect offset from the start of the sentence. - :ivar int length: The lenght of the aspect. + :ivar int length: The length of the aspect. """ def __init__(self, **kwargs): @@ -755,7 +755,7 @@ class AspectOpinion(DictMixin): :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores :ivar int offset: The opinion offset from the start of the sentence. - :ivar int length: The lenght of the opinion. + :ivar int length: The length of the opinion. :ivar bool is_negated: Whether the opinion is negated. For example, in "The food is not good", the opinion "good" is negated. """ diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 702c52a4e195..add746166c26 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -379,9 +379,9 @@ def analyze_sentiment( # type: ignore list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] :keyword bool show_aspects: Whether to conduct aspect-based sentiment analysis. - Aspect-based sentiment analysis provides more granular - information about the opinions related to aspects (which are the attributes of products or services) - in text. If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects + Aspect-based sentiment analysis provides more granular analysis of sentiment and + opinions around specific aspects or attributes of a product or service. + If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects will have property `aspects` containing the result of this analysis :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index 541143f7b71f..da1b3bb7f1ba 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -379,9 +379,9 @@ async def analyze_sentiment( # type: ignore list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] :keyword bool show_aspects: Whether to conduct aspect-based sentiment analysis. - Aspect-based sentiment analysis provides more granular - information about the opinions related to aspects (which are the attributes of products or services) - in text. If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects + Aspect-based sentiment analysis provides more granular analysis of sentiment and + opinions around specific aspects or attributes of a product or service. + If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects will have property `aspects` containing the result of this analysis :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. From ea9fca332389d837a3419f4500c002f9378a0b40 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 14 Jul 2020 17:06:12 -0400 Subject: [PATCH 05/21] made ApiVersion enum all uppercase --- .../azure/ai/textanalytics/_multiapi.py | 4 ++-- .../azure-ai-textanalytics/tests/test_analyze_sentiment.py | 4 ++-- .../tests/test_analyze_sentiment_async.py | 4 ++-- .../azure-ai-textanalytics/tests/test_multiapi.py | 2 +- .../azure-ai-textanalytics/tests/test_multiapi_async.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py index 3acad28a6d65..3d29a0c06583 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py @@ -13,7 +13,7 @@ class ApiVersion(str, Enum): """Text Analytics API versions supported by this package""" #: this is the default version - V3_1_preview_1 = "v3.1-preview.1" + V3_1_PREVIEW_1 = "v3.1-preview.1" V3_0 = "v3.0" @@ -29,7 +29,7 @@ def load_generated_api(api_version, aio=False): + "Supported versions: {}".format(", ".join(v.value for v in ApiVersion)) ) - if api_version == ApiVersion.V3_1_preview_1: + if api_version == ApiVersion.V3_1_PREVIEW_1: if aio: from ._generated.v3_1_preview_1.aio import TextAnalyticsClient else: diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index a203eea0b5d4..2551c8147991 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -576,7 +576,7 @@ def callback(pipeline_response, deserialized, _): assert res == "cls result" @GlobalTextAnalyticsAccountPreparer() - @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_PREVIEW_1}) def test_aspect_based_sentiment_analysis(self, client): documents = [ "It has a sleek premium aluminum design that makes it beautiful to look at." @@ -615,7 +615,7 @@ def test_aspect_based_sentiment_analysis(self, client): self.assertFalse(premium_opinion.is_negated) @GlobalTextAnalyticsAccountPreparer() - @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_PREVIEW_1}) def test_aspect_based_sentiment_analysis_negated_opinion(self, client): documents = [ "The food and service is not good" diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index 51b397aaced4..0a956b220796 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -592,7 +592,7 @@ def callback(pipeline_response, deserialized, _): assert res == "cls result" @GlobalTextAnalyticsAccountPreparer() - @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_PREVIEW_1}) async def test_aspect_based_sentiment_analysis(self, client): documents = [ "It has a sleek premium aluminum design that makes it beautiful to look at." @@ -631,7 +631,7 @@ async def test_aspect_based_sentiment_analysis(self, client): self.assertFalse(premium_opinion.is_negated) @GlobalTextAnalyticsAccountPreparer() - @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_PREVIEW_1}) async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): documents = [ "The food and service is not good" diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py index 9737e821c252..366b0581a533 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi.py @@ -23,6 +23,6 @@ def test_v3_0_api_version(self, client): assert "v3.0" in client._client._client._base_url @GlobalTextAnalyticsAccountPreparer() - @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_PREVIEW_1}) def test_v3_1_preview_1_api_version(self, client): assert "v3.1-preview.1" in client._client._client._base_url \ No newline at end of file diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py index f23a9a88f87c..964f77edd7a8 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py @@ -24,6 +24,6 @@ def test_v3_0_api_version(self, client): assert "v3.0" in client._client._client._base_url @GlobalTextAnalyticsAccountPreparer() - @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_preview_1}) + @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_1_PREVIEW_1}) def test_v3_1_preview_1_api_version(self, client): assert "v3.1-preview.1" in client._client._client._base_url \ No newline at end of file From 590dd1134c71b61bf0011614eeadcf5b0dcdb87a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 14 Jul 2020 17:22:38 -0400 Subject: [PATCH 06/21] update changelog --- sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md index f08fefb11aa5..7304f3b7e6ee 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md +++ b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md @@ -2,6 +2,11 @@ ## 1.0.1 (Unreleased) +**New features** + +- Adding support for the service's v3.1-preview.1 API. The default API version is v3.0, but pass in "v3.1-preview.1" as the value for `api_version` when creating your client. +- We now have added support for aspect based sentiment analysis. To use this feature, you need to make sure you are using the service's +v3.1-preview.1 API. To get this support pass `show_aspects` as True when calling the `analyze_sentiment` endpoint ## 1.0.0 (2020-06-09) From 399571a4224ff7bc62b0a3d12792f03e46508c1f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 15 Jul 2020 10:41:16 -0400 Subject: [PATCH 07/21] check if positive and negative confidence scores are not None since service may change figures --- .../tests/test_analyze_sentiment.py | 24 +++++++++---------- .../tests/test_analyze_sentiment_async.py | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index 2551c8147991..9708f5dd13d1 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -588,18 +588,18 @@ def test_aspect_based_sentiment_analysis(self, client): for aspect in sentence.aspects: self.assertEqual('design', aspect.text) self.assertEqual('positive', aspect.sentiment) - self.assertEqual(1.0, aspect.confidence_scores.positive) + self.assertIsNotNone(1.0, aspect.confidence_scores.positive) self.assertEqual(0.0, aspect.confidence_scores.neutral) - self.assertEqual(0.0, aspect.confidence_scores.negative) + self.assertIsNotNone(0.0, aspect.confidence_scores.negative) self.assertEqual(32, aspect.offset) self.assertEqual(6, aspect.length) sleek_opinion = aspect.opinions[0] self.assertEqual('sleek', sleek_opinion.text) self.assertEqual('positive', sleek_opinion.sentiment) - self.assertEqual(1.0, sleek_opinion.confidence_scores.positive) + self.assertIsNotNone(1.0, sleek_opinion.confidence_scores.positive) self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) - self.assertEqual(0.0, sleek_opinion.confidence_scores.negative) + self.assertIsNotNone(0.0, sleek_opinion.confidence_scores.negative) self.assertEqual(9, sleek_opinion.offset) self.assertEqual(5, sleek_opinion.length) self.assertFalse(sleek_opinion.is_negated) @@ -607,9 +607,9 @@ def test_aspect_based_sentiment_analysis(self, client): premium_opinion = aspect.opinions[1] self.assertEqual('premium', premium_opinion.text) self.assertEqual('positive', premium_opinion.sentiment) - self.assertEqual(1.0, premium_opinion.confidence_scores.positive) + self.assertIsNotNone(1.0, premium_opinion.confidence_scores.positive) self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) - self.assertEqual(0.0, premium_opinion.confidence_scores.negative) + self.assertIsNotNone(0.0, premium_opinion.confidence_scores.negative) self.assertEqual(15, premium_opinion.offset) self.assertEqual(7, premium_opinion.length) self.assertFalse(premium_opinion.is_negated) @@ -629,17 +629,17 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('food', food_aspect.text) self.assertEqual('negative', food_aspect.sentiment) - self.assertEqual(0.01, food_aspect.confidence_scores.positive) + self.assertIsNotNone(0.01, food_aspect.confidence_scores.positive) self.assertEqual(0.0, food_aspect.confidence_scores.neutral) - self.assertEqual(0.99, food_aspect.confidence_scores.negative) + self.assertIsNotNone(0.99, food_aspect.confidence_scores.negative) self.assertEqual(4, food_aspect.offset) self.assertEqual(4, food_aspect.length) self.assertEqual('service', service_aspect.text) self.assertEqual('negative', service_aspect.sentiment) - self.assertEqual(0.01, service_aspect.confidence_scores.positive) + self.assertIsNotNone(0.01, service_aspect.confidence_scores.positive) self.assertEqual(0.0, service_aspect.confidence_scores.neutral) - self.assertEqual(0.99, service_aspect.confidence_scores.negative) + self.assertIsNotNone(0.99, service_aspect.confidence_scores.negative) self.assertEqual(13, service_aspect.offset) self.assertEqual(7, service_aspect.length) @@ -649,9 +649,9 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('good', food_opinion.text) self.assertEqual('negative', food_opinion.sentiment) - self.assertEqual(0.01, food_opinion.confidence_scores.positive) + self.assertIsNotNone(0.01, food_opinion.confidence_scores.positive) self.assertEqual(0.0, food_opinion.confidence_scores.neutral) - self.assertEqual(0.99, food_opinion.confidence_scores.negative) + self.assertIsNotNone(0.99, food_opinion.confidence_scores.negative) self.assertEqual(28, food_opinion.offset) self.assertEqual(4, food_opinion.length) self.assertTrue(food_opinion.is_negated) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index 0a956b220796..d6bacccc788d 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -604,18 +604,18 @@ async def test_aspect_based_sentiment_analysis(self, client): for aspect in sentence.aspects: self.assertEqual('design', aspect.text) self.assertEqual('positive', aspect.sentiment) - self.assertEqual(1.0, aspect.confidence_scores.positive) + self.assertIsNotNone(1.0, aspect.confidence_scores.positive) self.assertEqual(0.0, aspect.confidence_scores.neutral) - self.assertEqual(0.0, aspect.confidence_scores.negative) + self.assertIsNotNone(0.0, aspect.confidence_scores.negative) self.assertEqual(32, aspect.offset) self.assertEqual(6, aspect.length) sleek_opinion = aspect.opinions[0] self.assertEqual('sleek', sleek_opinion.text) self.assertEqual('positive', sleek_opinion.sentiment) - self.assertEqual(1.0, sleek_opinion.confidence_scores.positive) + self.assertIsNotNone(1.0, sleek_opinion.confidence_scores.positive) self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) - self.assertEqual(0.0, sleek_opinion.confidence_scores.negative) + self.assertIsNotNone(0.0, sleek_opinion.confidence_scores.negative) self.assertEqual(9, sleek_opinion.offset) self.assertEqual(5, sleek_opinion.length) self.assertFalse(sleek_opinion.is_negated) @@ -623,9 +623,9 @@ async def test_aspect_based_sentiment_analysis(self, client): premium_opinion = aspect.opinions[1] self.assertEqual('premium', premium_opinion.text) self.assertEqual('positive', premium_opinion.sentiment) - self.assertEqual(1.0, premium_opinion.confidence_scores.positive) + self.assertIsNotNone(1.0, premium_opinion.confidence_scores.positive) self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) - self.assertEqual(0.0, premium_opinion.confidence_scores.negative) + self.assertIsNotNone(0.0, premium_opinion.confidence_scores.negative) self.assertEqual(15, premium_opinion.offset) self.assertEqual(7, premium_opinion.length) self.assertFalse(premium_opinion.is_negated) @@ -645,17 +645,17 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('food', food_aspect.text) self.assertEqual('negative', food_aspect.sentiment) - self.assertEqual(0.01, food_aspect.confidence_scores.positive) + self.assertIsNotNone(0.01, food_aspect.confidence_scores.positive) self.assertEqual(0.0, food_aspect.confidence_scores.neutral) - self.assertEqual(0.99, food_aspect.confidence_scores.negative) + self.assertIsNotNone(0.99, food_aspect.confidence_scores.negative) self.assertEqual(4, food_aspect.offset) self.assertEqual(4, food_aspect.length) self.assertEqual('service', service_aspect.text) self.assertEqual('negative', service_aspect.sentiment) - self.assertEqual(0.01, service_aspect.confidence_scores.positive) + self.assertIsNotNone(0.01, service_aspect.confidence_scores.positive) self.assertEqual(0.0, service_aspect.confidence_scores.neutral) - self.assertEqual(0.99, service_aspect.confidence_scores.negative) + self.assertIsNotNone(0.99, service_aspect.confidence_scores.negative) self.assertEqual(13, service_aspect.offset) self.assertEqual(7, service_aspect.length) @@ -665,9 +665,9 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('good', food_opinion.text) self.assertEqual('negative', food_opinion.sentiment) - self.assertEqual(0.01, food_opinion.confidence_scores.positive) + self.assertIsNotNone(0.01, food_opinion.confidence_scores.positive) self.assertEqual(0.0, food_opinion.confidence_scores.neutral) - self.assertEqual(0.99, food_opinion.confidence_scores.negative) + self.assertIsNotNone(0.99, food_opinion.confidence_scores.negative) self.assertEqual(28, food_opinion.offset) self.assertEqual(4, food_opinion.length) self.assertTrue(food_opinion.is_negated) From 1ef46f934789afce32ba6c697a84d1660913ff14 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 15 Jul 2020 10:45:52 -0400 Subject: [PATCH 08/21] improve code flow in sentiment endpoinnt --- .../textanalytics/_text_analytics_client.py | 19 +++++++------------ .../aio/_text_analytics_client_async.py | 19 +++++++------------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index add746166c26..4dd4baf1808d 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -415,24 +415,19 @@ def analyze_sentiment( # type: ignore show_stats = kwargs.pop("show_stats", False) show_aspects = kwargs.pop("show_aspects", None) - try: + if show_aspects is not None: if self._api_version == "v3.0": - if show_aspects is not None: - raise TypeError( - "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" - ) - return self._client.sentiment( - documents=docs, - model_version=model_version, - show_stats=show_stats, - cls=kwargs.pop("cls", sentiment_result), - **kwargs + raise TypeError( + "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" ) + elif self._api_version == "v3.1-preview.1": + kwargs.update({"opinion_mining": show_aspects}) + + try: return self._client.sentiment( documents=docs, model_version=model_version, show_stats=show_stats, - opinion_mining=show_aspects, cls=kwargs.pop("cls", sentiment_result), **kwargs ) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index da1b3bb7f1ba..f3e8916001a1 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -415,24 +415,19 @@ async def analyze_sentiment( # type: ignore show_stats = kwargs.pop("show_stats", False) show_aspects = kwargs.pop("show_aspects", None) - try: + if show_aspects is not None: if self._api_version == "v3.0": - if show_aspects is not None: - raise TypeError( - "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" - ) - return await self._client.sentiment( - documents=docs, - model_version=model_version, - show_stats=show_stats, - cls=kwargs.pop("cls", sentiment_result), - **kwargs + raise TypeError( + "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" ) + elif self._api_version == "v3.1-preview.1": + kwargs.update({"opinion_mining": show_aspects}) + + try: return await self._client.sentiment( documents=docs, model_version=model_version, show_stats=show_stats, - opinion_mining=show_aspects, cls=kwargs.pop("cls", sentiment_result), **kwargs ) From 858e70d53adc1bcbfbbc4677ebadf48ffec534e4 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 15 Jul 2020 11:00:43 -0400 Subject: [PATCH 09/21] pylint --- .../azure/ai/textanalytics/_text_analytics_client.py | 2 +- .../azure/ai/textanalytics/aio/_text_analytics_client_async.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 4dd4baf1808d..2fc9f62b25fd 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -420,7 +420,7 @@ def analyze_sentiment( # type: ignore raise TypeError( "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" ) - elif self._api_version == "v3.1-preview.1": + if self._api_version == "v3.1-preview.1": kwargs.update({"opinion_mining": show_aspects}) try: diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index f3e8916001a1..5e9a6a3b2df3 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -420,7 +420,7 @@ async def analyze_sentiment( # type: ignore raise TypeError( "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" ) - elif self._api_version == "v3.1-preview.1": + if self._api_version == "v3.1-preview.1": kwargs.update({"opinion_mining": show_aspects}) try: From e83b145cf62cdec7a98417d1641603c74a2885ed Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 15 Jul 2020 11:48:24 -0400 Subject: [PATCH 10/21] whitespace to update PR --- .../azure/ai/textanalytics/_text_analytics_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 2fc9f62b25fd..7cfb5e226a85 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -422,7 +422,6 @@ def analyze_sentiment( # type: ignore ) if self._api_version == "v3.1-preview.1": kwargs.update({"opinion_mining": show_aspects}) - try: return self._client.sentiment( documents=docs, From ffe563df2f071306db686ad87236553191b52ac3 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 16 Jul 2020 11:31:49 -0400 Subject: [PATCH 11/21] unify structure of sentiment objects --- .../azure/ai/textanalytics/__init__.py | 8 ++-- .../azure/ai/textanalytics/_models.py | 38 ++++++++++--------- .../textanalytics/_text_analytics_client.py | 9 +++-- .../aio/_text_analytics_client_async.py | 9 +++-- .../tests/test_text_analytics.py | 8 ++-- 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py index 7347cb826242..5831a2526b0e 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py @@ -26,8 +26,8 @@ TextDocumentBatchStatistics, SentenceSentiment, SentimentConfidenceScores, - SentenceAspect, - AspectOpinion + AspectSentiment, + OpinionSentiment ) __all__ = [ @@ -51,8 +51,8 @@ 'TextDocumentBatchStatistics', 'SentenceSentiment', 'SentimentConfidenceScores', - 'SentenceAspect', - 'AspectOpinion' + 'AspectSentiment', + 'OpinionSentiment' ] __version__ = VERSION diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index deac24feabb9..c6fd9f9bc522 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -637,12 +637,13 @@ class SentenceSentiment(DictMixin): and 1 for the sentence for all labels. :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores - :ivar aspects: The list of aspects of the sentence. An aspect is a - key phrase of a sentence, for example the attributes of a product - or a service. Only returned if `show_aspects` is set to True in - call to `analyze_sentiment` + :ivar aspects: The list of aspects in this sentence. An aspect is a + key attribute of a product or a service. For example in + "The food at Hotel Foo is good", "food" is an aspect of + "Hotel Foo". This property is only returned if `show_aspects` is + set to True in the call to `analyze_sentiment` :vartype aspects: - list[~azure.ai.textanalytics.SentenceAspect] + list[~azure.ai.textanalytics.AspectSentiment] """ def __init__(self, **kwargs): @@ -658,7 +659,7 @@ def _from_generated(cls, sentence, results): sentiment=sentence.sentiment, confidence_scores=SentimentConfidenceScores._from_generated(sentence.confidence_scores), # pylint: disable=protected-access aspects=( - [SentenceAspect._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access + [AspectSentiment._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access if hasattr(sentence, "aspects") else None ) ) @@ -671,11 +672,12 @@ def __repr__(self): )[:1024] -class SentenceAspect(DictMixin): - """SentenceAspect contains the related opinions, predicted sentiment, - confidence scores and other information about an aspect of a sentence. - An aspect of a sentence is a key component of a sentence, for example - in the sentence "The food is good", "food" is an aspect. +class AspectSentiment(DictMixin): + """AspectSentiment contains the related opinions, predicted sentiment, + confidence scores and other information about an aspect of a product. + An aspect of a product/service is a key component of that product/service. + For example in "The food at Hotel Foo is good", "food" is an aspect of + "Hotel Foo". :ivar str text: The aspect text. :ivar str sentiment: The predicted Sentiment for the aspect. Possible values @@ -685,8 +687,8 @@ class SentenceAspect(DictMixin): for 'neutral' will always be 0 :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores - :ivar opinions: All of the opinions in the sentence related to this aspect. - :vartype opinions: list[~azure.ai.textanalytics.AspectOpinion] + :ivar opinions: All of the opinions related to this aspect. + :vartype opinions: list[~azure.ai.textanalytics.OpinionSentiment] :ivar int offset: The aspect offset from the start of the sentence. :ivar int length: The length of the aspect. """ @@ -723,14 +725,14 @@ def _from_generated(cls, aspect, results): sentiment=aspect.sentiment, confidence_scores=SentimentConfidenceScores._from_generated(aspect.confidence_scores), # pylint: disable=protected-access opinions=[ - AspectOpinion._from_generated(opinion) for opinion in cls._get_opinions(aspect.relations, results) # pylint: disable=protected-access + OpinionSentiment._from_generated(opinion) for opinion in cls._get_opinions(aspect.relations, results) # pylint: disable=protected-access ], offset=aspect.offset, length=aspect.length ) def __repr__(self): - return "SentenceAspect(text={}, sentiment={}, confidence_scores={}, opinions={}, offset={}, length={})".format( + return "AspectSentiment(text={}, sentiment={}, confidence_scores={}, opinions={}, offset={}, length={})".format( self.text, self.sentiment, repr(self.confidence_scores), @@ -740,8 +742,8 @@ def __repr__(self): )[:1024] -class AspectOpinion(DictMixin): - """AspectOpinion contains the predicted sentiment, +class OpinionSentiment(DictMixin): + """OpinionSentiment contains the predicted sentiment, confidence scores and other information about an opinion of an aspect. For example, in the sentence "The food is good", the opinion of the aspect 'food' is 'good'. @@ -780,7 +782,7 @@ def _from_generated(cls, opinion): ) def __repr__(self): - return "AspectOpinion(text={}, sentiment={}, confidence_scores={}, offset={}, length={}, is_negated={})".format( + return "OpinionSentiment(text={}, sentiment={}, confidence_scores={}, offset={}, length={}, is_negated={})".format( self.text, self.sentiment, repr(self.confidence_scores), diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 7cfb5e226a85..000200c20a6b 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -378,11 +378,12 @@ def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] - :keyword bool show_aspects: Whether to conduct aspect-based sentiment analysis. - Aspect-based sentiment analysis provides more granular analysis of sentiment and - opinions around specific aspects or attributes of a product or service. + :keyword bool show_aspects: Whether to conduct more granular analysis around the aspects of + a product or service (also known as aspect-based sentiment analysis). For example, + in the review "The food at Hotel Foo is good", "food" is an aspect of "Hotel Foo", and + setting `show_aspects` to True will go into the sentiment and opinions of "food". If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects - will have property `aspects` containing the result of this analysis + will have property `aspects` containing the result of this analysis. :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. If not set, uses "en" for English as default. Per-document language will diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index 5e9a6a3b2df3..c95a7eece893 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -378,11 +378,12 @@ async def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] - :keyword bool show_aspects: Whether to conduct aspect-based sentiment analysis. - Aspect-based sentiment analysis provides more granular analysis of sentiment and - opinions around specific aspects or attributes of a product or service. + :keyword bool show_aspects: Whether to conduct more granular analysis around the aspects of + a product or service (also known as aspect-based sentiment analysis). For example, + in the review "The food at Hotel Foo is good", "food" is an aspect of "Hotel Foo", and + setting `show_aspects` to True will go into the sentiment and opinions of "food". If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects - will have property `aspects` containing the result of this analysis + will have property `aspects` containing the result of this analysis. :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. If not set, uses "en" for English as default. Per-document language will diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py index 4c99b549fc07..f84ccd797343 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py @@ -112,7 +112,7 @@ def test_repr(self): aspect_opinion_confidence_score = _models.SentimentConfidenceScores(positive=0.5, negative=0.5) - aspect_opinion = _models.AspectOpinion( + aspect_opinion = _models.OpinionSentiment( text="opinion", sentiment="positive", confidence_scores=aspect_opinion_confidence_score, @@ -121,7 +121,7 @@ def test_repr(self): is_negated=False ) - sentence_aspect = _models.SentenceAspect( + sentence_aspect = _models.AspectSentiment( text="aspect", sentiment="positive", confidence_scores=aspect_opinion_confidence_score, @@ -190,12 +190,12 @@ def test_repr(self): "erroneous_document_count=3, transaction_count=4)", repr(text_document_batch_statistics)) aspect_opinion_repr = ( - "AspectOpinion(text=opinion, sentiment=positive, confidence_scores=SentimentConfidenceScores(" + "OpinionSentiment(text=opinion, sentiment=positive, confidence_scores=SentimentConfidenceScores(" "positive=0.5, neutral=0.0, negative=0.5), offset=3, length=7, is_negated=False)" ) self.assertEqual(aspect_opinion_repr, repr(aspect_opinion)) self.assertEqual( - "SentenceAspect(text=aspect, sentiment=positive, confidence_scores=SentimentConfidenceScores(" + "AspectSentiment(text=aspect, sentiment=positive, confidence_scores=SentimentConfidenceScores(" "positive=0.5, neutral=0.0, negative=0.5), opinions=[{}], offset=10, length=6)".format(aspect_opinion_repr), repr(sentence_aspect) ) From 09cf3fda37422c167a031090dc558cb330975b28 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 16 Jul 2020 11:54:41 -0400 Subject: [PATCH 12/21] pylint --- .../azure/ai/textanalytics/_models.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index c6fd9f9bc522..701cffef66f9 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -782,14 +782,16 @@ def _from_generated(cls, opinion): ) def __repr__(self): - return "OpinionSentiment(text={}, sentiment={}, confidence_scores={}, offset={}, length={}, is_negated={})".format( - self.text, - self.sentiment, - repr(self.confidence_scores), - self.offset, - self.length, - self.is_negated - )[:1024] + return ( + "OpinionSentiment(text={}, sentiment={}, confidence_scores={}, offset={}, length={}, is_negated={})".format( + self.text, + self.sentiment, + repr(self.confidence_scores), + self.offset, + self.length, + self.is_negated + )[:1024] + ) class SentimentConfidenceScores(DictMixin): From 7ee99e08c8d57ed133e5f35c19dc0827a0ab47ed Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 20 Jul 2020 14:50:09 -0400 Subject: [PATCH 13/21] fix null checks in aspect tests --- .../tests/test_analyze_sentiment.py | 24 +++++++++---------- .../tests/test_analyze_sentiment_async.py | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index 9708f5dd13d1..01d05f838109 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -588,18 +588,18 @@ def test_aspect_based_sentiment_analysis(self, client): for aspect in sentence.aspects: self.assertEqual('design', aspect.text) self.assertEqual('positive', aspect.sentiment) - self.assertIsNotNone(1.0, aspect.confidence_scores.positive) + self.assertIsNotNone(aspect.confidence_scores.positive) self.assertEqual(0.0, aspect.confidence_scores.neutral) - self.assertIsNotNone(0.0, aspect.confidence_scores.negative) + self.assertIsNotNone(aspect.confidence_scores.negative) self.assertEqual(32, aspect.offset) self.assertEqual(6, aspect.length) sleek_opinion = aspect.opinions[0] self.assertEqual('sleek', sleek_opinion.text) self.assertEqual('positive', sleek_opinion.sentiment) - self.assertIsNotNone(1.0, sleek_opinion.confidence_scores.positive) + self.assertIsNotNone(sleek_opinion.confidence_scores.positive) self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) - self.assertIsNotNone(0.0, sleek_opinion.confidence_scores.negative) + self.assertIsNotNone(sleek_opinion.confidence_scores.negative) self.assertEqual(9, sleek_opinion.offset) self.assertEqual(5, sleek_opinion.length) self.assertFalse(sleek_opinion.is_negated) @@ -607,9 +607,9 @@ def test_aspect_based_sentiment_analysis(self, client): premium_opinion = aspect.opinions[1] self.assertEqual('premium', premium_opinion.text) self.assertEqual('positive', premium_opinion.sentiment) - self.assertIsNotNone(1.0, premium_opinion.confidence_scores.positive) + self.assertIsNotNone(premium_opinion.confidence_scores.positive) self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) - self.assertIsNotNone(0.0, premium_opinion.confidence_scores.negative) + self.assertIsNotNone(premium_opinion.confidence_scores.negative) self.assertEqual(15, premium_opinion.offset) self.assertEqual(7, premium_opinion.length) self.assertFalse(premium_opinion.is_negated) @@ -629,17 +629,17 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('food', food_aspect.text) self.assertEqual('negative', food_aspect.sentiment) - self.assertIsNotNone(0.01, food_aspect.confidence_scores.positive) + self.assertIsNotNone(food_aspect.confidence_scores.positive) self.assertEqual(0.0, food_aspect.confidence_scores.neutral) - self.assertIsNotNone(0.99, food_aspect.confidence_scores.negative) + self.assertIsNotNone(food_aspect.confidence_scores.negative) self.assertEqual(4, food_aspect.offset) self.assertEqual(4, food_aspect.length) self.assertEqual('service', service_aspect.text) self.assertEqual('negative', service_aspect.sentiment) - self.assertIsNotNone(0.01, service_aspect.confidence_scores.positive) + self.assertIsNotNone(service_aspect.confidence_scores.positive) self.assertEqual(0.0, service_aspect.confidence_scores.neutral) - self.assertIsNotNone(0.99, service_aspect.confidence_scores.negative) + self.assertIsNotNone(service_aspect.confidence_scores.negative) self.assertEqual(13, service_aspect.offset) self.assertEqual(7, service_aspect.length) @@ -649,9 +649,9 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('good', food_opinion.text) self.assertEqual('negative', food_opinion.sentiment) - self.assertIsNotNone(0.01, food_opinion.confidence_scores.positive) + self.assertIsNotNone(food_opinion.confidence_scores.positive) self.assertEqual(0.0, food_opinion.confidence_scores.neutral) - self.assertIsNotNone(0.99, food_opinion.confidence_scores.negative) + self.assertIsNotNone(food_opinion.confidence_scores.negative) self.assertEqual(28, food_opinion.offset) self.assertEqual(4, food_opinion.length) self.assertTrue(food_opinion.is_negated) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index d6bacccc788d..ec20868d304d 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -604,18 +604,18 @@ async def test_aspect_based_sentiment_analysis(self, client): for aspect in sentence.aspects: self.assertEqual('design', aspect.text) self.assertEqual('positive', aspect.sentiment) - self.assertIsNotNone(1.0, aspect.confidence_scores.positive) + self.assertIsNotNone(aspect.confidence_scores.positive) self.assertEqual(0.0, aspect.confidence_scores.neutral) - self.assertIsNotNone(0.0, aspect.confidence_scores.negative) + self.assertIsNotNone(aspect.confidence_scores.negative) self.assertEqual(32, aspect.offset) self.assertEqual(6, aspect.length) sleek_opinion = aspect.opinions[0] self.assertEqual('sleek', sleek_opinion.text) self.assertEqual('positive', sleek_opinion.sentiment) - self.assertIsNotNone(1.0, sleek_opinion.confidence_scores.positive) + self.assertIsNotNone(sleek_opinion.confidence_scores.positive) self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) - self.assertIsNotNone(0.0, sleek_opinion.confidence_scores.negative) + self.assertIsNotNone(sleek_opinion.confidence_scores.negative) self.assertEqual(9, sleek_opinion.offset) self.assertEqual(5, sleek_opinion.length) self.assertFalse(sleek_opinion.is_negated) @@ -623,9 +623,9 @@ async def test_aspect_based_sentiment_analysis(self, client): premium_opinion = aspect.opinions[1] self.assertEqual('premium', premium_opinion.text) self.assertEqual('positive', premium_opinion.sentiment) - self.assertIsNotNone(1.0, premium_opinion.confidence_scores.positive) + self.assertIsNotNone(premium_opinion.confidence_scores.positive) self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) - self.assertIsNotNone(0.0, premium_opinion.confidence_scores.negative) + self.assertIsNotNone(premium_opinion.confidence_scores.negative) self.assertEqual(15, premium_opinion.offset) self.assertEqual(7, premium_opinion.length) self.assertFalse(premium_opinion.is_negated) @@ -645,17 +645,17 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('food', food_aspect.text) self.assertEqual('negative', food_aspect.sentiment) - self.assertIsNotNone(0.01, food_aspect.confidence_scores.positive) + self.assertIsNotNone(food_aspect.confidence_scores.positive) self.assertEqual(0.0, food_aspect.confidence_scores.neutral) - self.assertIsNotNone(0.99, food_aspect.confidence_scores.negative) + self.assertIsNotNone(food_aspect.confidence_scores.negative) self.assertEqual(4, food_aspect.offset) self.assertEqual(4, food_aspect.length) self.assertEqual('service', service_aspect.text) self.assertEqual('negative', service_aspect.sentiment) - self.assertIsNotNone(0.01, service_aspect.confidence_scores.positive) + self.assertIsNotNone(service_aspect.confidence_scores.positive) self.assertEqual(0.0, service_aspect.confidence_scores.neutral) - self.assertIsNotNone(0.99, service_aspect.confidence_scores.negative) + self.assertIsNotNone(service_aspect.confidence_scores.negative) self.assertEqual(13, service_aspect.offset) self.assertEqual(7, service_aspect.length) @@ -665,9 +665,9 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual('good', food_opinion.text) self.assertEqual('negative', food_opinion.sentiment) - self.assertIsNotNone(0.01, food_opinion.confidence_scores.positive) + self.assertIsNotNone(food_opinion.confidence_scores.positive) self.assertEqual(0.0, food_opinion.confidence_scores.neutral) - self.assertIsNotNone(0.99, food_opinion.confidence_scores.negative) + self.assertIsNotNone(food_opinion.confidence_scores.negative) self.assertEqual(28, food_opinion.offset) self.assertEqual(4, food_opinion.length) self.assertTrue(food_opinion.is_negated) From e9fee50fc753d7b86c1c4b3651d29da416e1eb19 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 21 Jul 2020 11:58:03 -0400 Subject: [PATCH 14/21] add trailing whitespace to init __all__ --- .../azure-ai-textanalytics/azure/ai/textanalytics/__init__.py | 2 +- .../azure/ai/textanalytics/aio/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py index 5831a2526b0e..022668c65e1d 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py @@ -52,7 +52,7 @@ 'SentenceSentiment', 'SentimentConfidenceScores', 'AspectSentiment', - 'OpinionSentiment' + 'OpinionSentiment', ] __version__ = VERSION diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/__init__.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/__init__.py index a3d4ff19e3d8..08ea8acd7812 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/__init__.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/__init__.py @@ -7,5 +7,5 @@ from ._text_analytics_client_async import TextAnalyticsClient __all__ = [ - 'TextAnalyticsClient' + 'TextAnalyticsClient', ] From 77c7fa8b6a19303ff9894045988b62bf086b6109 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 21 Jul 2020 15:21:41 -0400 Subject: [PATCH 15/21] separate out json pointer parsing and add unittest --- .../azure/ai/textanalytics/_models.py | 4 +++- .../azure-ai-textanalytics/tests/test_unittests.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/test_unittests.py diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index 701cffef66f9..dbf260b04542 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -9,6 +9,8 @@ MultiLanguageInput ) +def _get_indices(relation): + return [int(s) for s in re.findall(r"\d+", relation)] class DictMixin(object): @@ -708,7 +710,7 @@ def _get_opinions(relations, results): opinion_relations = [r.ref for r in relations if r.relation_type == "opinion"] opinions = [] for opinion_relation in opinion_relations: - nums = [int(s) for s in re.findall(r"\d+", opinion_relation)] + nums = _get_indices(opinion_relation) document_index = nums[0] sentence_index = nums[1] opinion_index = nums[2] diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_unittests.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_unittests.py new file mode 100644 index 000000000000..1ec9d72224eb --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_unittests.py @@ -0,0 +1,14 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.ai.textanalytics._models import _get_indices +from testcase import TextAnalyticsTest + + +class TestUnittests(TextAnalyticsTest): + + def test_json_pointer_parsing(self): + assert [1, 0, 15] == _get_indices("#/documents/1/sentences/0/opinions/15") From 87b4a800aa4f4ca5cb32cde9ffca4171450e5262 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 21 Jul 2020 15:57:13 -0400 Subject: [PATCH 16/21] add that show_aspects is only available in v3.1-preview.1 in sentiment docstrings --- .../azure/ai/textanalytics/_text_analytics_client.py | 5 ++++- .../ai/textanalytics/aio/_text_analytics_client_async.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 000200c20a6b..5288571d3b9c 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -383,7 +383,8 @@ def analyze_sentiment( # type: ignore in the review "The food at Hotel Foo is good", "food" is an aspect of "Hotel Foo", and setting `show_aspects` to True will go into the sentiment and opinions of "food". If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects - will have property `aspects` containing the result of this analysis. + will have property `aspects` containing the result of this analysis. Only available for + API version v3.1-preview.1. :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. If not set, uses "en" for English as default. Per-document language will @@ -393,6 +394,8 @@ def analyze_sentiment( # type: ignore be used for scoring, e.g. "latest", "2019-10-01". If a model-version is not specified, the API will default to the latest, non-preview version. :keyword bool show_stats: If set to true, response will contain document level statistics. + .. versionadded:: v3.1-preview.1 + The *show_aspects* parameter. :return: The combined list of :class:`~azure.ai.textanalytics.AnalyzeSentimentResult` and :class:`~azure.ai.textanalytics.DocumentError` in the order the original documents were passed in. diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index c95a7eece893..5195a638f13b 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -383,7 +383,8 @@ async def analyze_sentiment( # type: ignore in the review "The food at Hotel Foo is good", "food" is an aspect of "Hotel Foo", and setting `show_aspects` to True will go into the sentiment and opinions of "food". If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects - will have property `aspects` containing the result of this analysis. + will have property `aspects` containing the result of this analysis. Only available for + API version v3.1-preview.1. :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. If not set, uses "en" for English as default. Per-document language will @@ -393,6 +394,8 @@ async def analyze_sentiment( # type: ignore be used for scoring, e.g. "latest", "2019-10-01". If a model-version is not specified, the API will default to the latest, non-preview version. :keyword bool show_stats: If set to true, response will contain document level statistics. + .. versionadded:: v3.1-preview.1 + The *show_aspects* parameter. :return: The combined list of :class:`~azure.ai.textanalytics.AnalyzeSentimentResult` and :class:`~azure.ai.textanalytics.DocumentError` in the order the original documents were passed in. From 83426aab4d5fa9ce5ab4991f3f61fbdecea15343 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 29 Jul 2020 18:27:13 -0400 Subject: [PATCH 17/21] switch to kwarg mine_opinions and MinedOpinion body --- .../azure-ai-textanalytics/CHANGELOG.md | 4 +- .../azure/ai/textanalytics/__init__.py | 4 +- .../azure/ai/textanalytics/_models.py | 93 ++++++++++++------- .../textanalytics/_text_analytics_client.py | 21 ++--- .../aio/_text_analytics_client_async.py | 21 ++--- .../tests/test_analyze_sentiment.py | 23 ++--- .../tests/test_analyze_sentiment_async.py | 23 ++--- .../tests/test_text_analytics.py | 24 +++-- 8 files changed, 121 insertions(+), 92 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md index 7304f3b7e6ee..6bff5df4c231 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md +++ b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md @@ -5,8 +5,8 @@ **New features** - Adding support for the service's v3.1-preview.1 API. The default API version is v3.0, but pass in "v3.1-preview.1" as the value for `api_version` when creating your client. -- We now have added support for aspect based sentiment analysis. To use this feature, you need to make sure you are using the service's -v3.1-preview.1 API. To get this support pass `show_aspects` as True when calling the `analyze_sentiment` endpoint +- We now have added support for opinion mining. To use this feature, you need to make sure you are using the service's +v3.1-preview.1 API. To get this support pass `mine_opinions` as True when calling the `analyze_sentiment` endpoint ## 1.0.0 (2020-06-09) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py index 022668c65e1d..ae973a7f15d2 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py @@ -26,8 +26,9 @@ TextDocumentBatchStatistics, SentenceSentiment, SentimentConfidenceScores, + MinedOpinion, AspectSentiment, - OpinionSentiment + OpinionSentiment, ) __all__ = [ @@ -51,6 +52,7 @@ 'TextDocumentBatchStatistics', 'SentenceSentiment', 'SentimentConfidenceScores', + 'MinedOpinion', 'AspectSentiment', 'OpinionSentiment', ] diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index dbf260b04542..39f4f983c289 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -639,20 +639,19 @@ class SentenceSentiment(DictMixin): and 1 for the sentence for all labels. :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores - :ivar aspects: The list of aspects in this sentence. An aspect is a - key attribute of a product or a service. For example in - "The food at Hotel Foo is good", "food" is an aspect of - "Hotel Foo". This property is only returned if `show_aspects` is - set to True in the call to `analyze_sentiment` - :vartype aspects: - list[~azure.ai.textanalytics.AspectSentiment] + :ivar mined_opinions: The list of opinions mined from this sentence. + For example in "The food is good, but the service is bad", we would + mind these two opinions "food is good", "service is bad". Only returned + if `mine_opinions` is set to True in the call to `analyze_sentiment`. + :vartype mined_opinions: + list[~azure.ai.textanalytics.MinedOpinion] """ def __init__(self, **kwargs): self.text = kwargs.get("text", None) self.sentiment = kwargs.get("sentiment", None) self.confidence_scores = kwargs.get("confidence_scores", None) - self.aspects = kwargs.get("aspects", None) + self.mined_opinions = kwargs.get("mined_opinions", None) @classmethod def _from_generated(cls, sentence, results): @@ -660,8 +659,8 @@ def _from_generated(cls, sentence, results): text=sentence.text, sentiment=sentence.sentiment, confidence_scores=SentimentConfidenceScores._from_generated(sentence.confidence_scores), # pylint: disable=protected-access - aspects=( - [AspectSentiment._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access + mined_opinions=( + [MinedOpinion._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access if hasattr(sentence, "aspects") else None ) ) @@ -673,6 +672,52 @@ def __repr__(self): repr(self.confidence_scores) )[:1024] +class MinedOpinion(DictMixin): + """A mined opinion object represents an opinion we've extracted from a sentence. + It consists of both an aspect that these opinions are about, and the actual + opinions themselves. + + :ivar aspect: The aspect of a product/service that this opinion is about + :vartype aspect: ~azure.ai.textanalytics.AspectSentiment + :ivar opinions: The actual opinions of the aspect + :vartype opinions: list[~azure.ai.textanalytics.OpinionSentiment] + """ + + def __init__(self, **kwargs): + self.aspect = kwargs.get("aspect", None) + self.opinions = kwargs.get("opinions", None) + + @staticmethod + def _get_opinions(relations, results): + if not relations: + return [] + opinion_relations = [r.ref for r in relations if r.relation_type == "opinion"] + opinions = [] + for opinion_relation in opinion_relations: + nums = _get_indices(opinion_relation) + document_index = nums[0] + sentence_index = nums[1] + opinion_index = nums[2] + opinions.append( + results[document_index].sentences[sentence_index].opinions[opinion_index] + ) + return opinions + + @classmethod + def _from_generated(cls, aspect, results): + return cls( + aspect=AspectSentiment._from_generated(aspect), # pylint: disable=protected-access + opinions=[ + OpinionSentiment._from_generated(opinion) for opinion in cls._get_opinions(aspect.relations, results) # pylint: disable=protected-access + ], + ) + + def __repr__(self): + return "MinedOpinion(aspect={}, opinions={})".format( + repr(self.aspect), + repr(self.opinions) + )[:1024] + class AspectSentiment(DictMixin): """AspectSentiment contains the related opinions, predicted sentiment, @@ -689,8 +734,6 @@ class AspectSentiment(DictMixin): for 'neutral' will always be 0 :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores - :ivar opinions: All of the opinions related to this aspect. - :vartype opinions: list[~azure.ai.textanalytics.OpinionSentiment] :ivar int offset: The aspect offset from the start of the sentence. :ivar int length: The length of the aspect. """ @@ -699,46 +742,24 @@ def __init__(self, **kwargs): self.text = kwargs.get("text", None) self.sentiment = kwargs.get("sentiment", None) self.confidence_scores = kwargs.get("confidence_scores", None) - self.opinions = kwargs.get("opinions", None) self.offset = kwargs.get("offset", None) self.length = kwargs.get("length", None) - @staticmethod - def _get_opinions(relations, results): - if not relations: - return [] - opinion_relations = [r.ref for r in relations if r.relation_type == "opinion"] - opinions = [] - for opinion_relation in opinion_relations: - nums = _get_indices(opinion_relation) - document_index = nums[0] - sentence_index = nums[1] - opinion_index = nums[2] - opinions.append( - results[document_index].sentences[sentence_index].opinions[opinion_index] - ) - return opinions - - @classmethod - def _from_generated(cls, aspect, results): + def _from_generated(cls, aspect): return cls( text=aspect.text, sentiment=aspect.sentiment, confidence_scores=SentimentConfidenceScores._from_generated(aspect.confidence_scores), # pylint: disable=protected-access - opinions=[ - OpinionSentiment._from_generated(opinion) for opinion in cls._get_opinions(aspect.relations, results) # pylint: disable=protected-access - ], offset=aspect.offset, length=aspect.length ) def __repr__(self): - return "AspectSentiment(text={}, sentiment={}, confidence_scores={}, opinions={}, offset={}, length={})".format( + return "AspectSentiment(text={}, sentiment={}, confidence_scores={}, offset={}, length={})".format( self.text, self.sentiment, repr(self.confidence_scores), - repr(self.opinions), self.offset, self.length )[:1024] diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 5288571d3b9c..08ae69282b1f 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -378,12 +378,11 @@ def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] - :keyword bool show_aspects: Whether to conduct more granular analysis around the aspects of - a product or service (also known as aspect-based sentiment analysis). For example, - in the review "The food at Hotel Foo is good", "food" is an aspect of "Hotel Foo", and - setting `show_aspects` to True will go into the sentiment and opinions of "food". - If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects - will have property `aspects` containing the result of this analysis. Only available for + :keyword bool mine_opinions: Whether to mine the opinions of a sentence and conduct more + granular analysis around the aspects of a product or service (also known as + aspect-based sentiment analysis). If set to true, the returned + :class:`~azure.ai.textanalytics.SentenceSentiment` objects + will have property `mined_opinions` containing the result of this analysis. Only available for API version v3.1-preview.1. :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. @@ -395,7 +394,7 @@ def analyze_sentiment( # type: ignore is not specified, the API will default to the latest, non-preview version. :keyword bool show_stats: If set to true, response will contain document level statistics. .. versionadded:: v3.1-preview.1 - The *show_aspects* parameter. + The *mine_opinions* parameter. :return: The combined list of :class:`~azure.ai.textanalytics.AnalyzeSentimentResult` and :class:`~azure.ai.textanalytics.DocumentError` in the order the original documents were passed in. @@ -417,15 +416,15 @@ def analyze_sentiment( # type: ignore docs = _validate_batch_input(documents, "language", language) model_version = kwargs.pop("model_version", None) show_stats = kwargs.pop("show_stats", False) - show_aspects = kwargs.pop("show_aspects", None) + mine_opinions = kwargs.pop("mine_opinions", None) - if show_aspects is not None: + if mine_opinions is not None: if self._api_version == "v3.0": raise TypeError( - "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" + "Parameter 'mine_opinions' is only added for API version v3.1-preview.1 and up" ) if self._api_version == "v3.1-preview.1": - kwargs.update({"opinion_mining": show_aspects}) + kwargs.update({"opinion_mining": mine_opinions}) try: return self._client.sentiment( documents=docs, diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index 5195a638f13b..135929df4b61 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -378,12 +378,11 @@ async def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] - :keyword bool show_aspects: Whether to conduct more granular analysis around the aspects of - a product or service (also known as aspect-based sentiment analysis). For example, - in the review "The food at Hotel Foo is good", "food" is an aspect of "Hotel Foo", and - setting `show_aspects` to True will go into the sentiment and opinions of "food". - If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects - will have property `aspects` containing the result of this analysis. Only available for + :keyword bool mine_opinions: Whether to mine the opinions of a sentence and conduct more + granular analysis around the aspects of a product or service (also known as + aspect-based sentiment analysis). If set to true, the returned + :class:`~azure.ai.textanalytics.SentenceSentiment` objects + will have property `mined_opinions` containing the result of this analysis. Only available for API version v3.1-preview.1. :keyword str language: The 2 letter ISO 639-1 representation of language for the entire batch. For example, use "en" for English; "es" for Spanish etc. @@ -395,7 +394,7 @@ async def analyze_sentiment( # type: ignore is not specified, the API will default to the latest, non-preview version. :keyword bool show_stats: If set to true, response will contain document level statistics. .. versionadded:: v3.1-preview.1 - The *show_aspects* parameter. + The *mine_opinions* parameter. :return: The combined list of :class:`~azure.ai.textanalytics.AnalyzeSentimentResult` and :class:`~azure.ai.textanalytics.DocumentError` in the order the original documents were passed in. @@ -417,15 +416,15 @@ async def analyze_sentiment( # type: ignore docs = _validate_batch_input(documents, "language", language) model_version = kwargs.pop("model_version", None) show_stats = kwargs.pop("show_stats", False) - show_aspects = kwargs.pop("show_aspects", None) + mine_opinions = kwargs.pop("mine_opinions", None) - if show_aspects is not None: + if mine_opinions is not None: if self._api_version == "v3.0": raise TypeError( - "Parameter 'show_aspects' is only added for API version v3.1-preview.1 and up" + "Parameter 'mine_opinions' is only added for API version v3.1-preview.1 and up" ) if self._api_version == "v3.1-preview.1": - kwargs.update({"opinion_mining": show_aspects}) + kwargs.update({"opinion_mining": mine_opinions}) try: return await self._client.sentiment( diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index 01d05f838109..a0c647cce488 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -582,10 +582,11 @@ def test_aspect_based_sentiment_analysis(self, client): "It has a sleek premium aluminum design that makes it beautiful to look at." ] - document = client.analyze_sentiment(documents=documents, show_aspects=True)[0] + document = client.analyze_sentiment(documents=documents, mine_opinions=True)[0] for sentence in document.sentences: - for aspect in sentence.aspects: + for mined_opinion in sentence.mined_opinions: + aspect = mined_opinion.aspect self.assertEqual('design', aspect.text) self.assertEqual('positive', aspect.sentiment) self.assertIsNotNone(aspect.confidence_scores.positive) @@ -594,7 +595,7 @@ def test_aspect_based_sentiment_analysis(self, client): self.assertEqual(32, aspect.offset) self.assertEqual(6, aspect.length) - sleek_opinion = aspect.opinions[0] + sleek_opinion = mined_opinion.opinions[0] self.assertEqual('sleek', sleek_opinion.text) self.assertEqual('positive', sleek_opinion.sentiment) self.assertIsNotNone(sleek_opinion.confidence_scores.positive) @@ -604,7 +605,7 @@ def test_aspect_based_sentiment_analysis(self, client): self.assertEqual(5, sleek_opinion.length) self.assertFalse(sleek_opinion.is_negated) - premium_opinion = aspect.opinions[1] + premium_opinion = mined_opinion.opinions[1] self.assertEqual('premium', premium_opinion.text) self.assertEqual('positive', premium_opinion.sentiment) self.assertIsNotNone(premium_opinion.confidence_scores.positive) @@ -621,11 +622,11 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): "The food and service is not good" ] - document = client.analyze_sentiment(documents=documents, show_aspects=True)[0] + document = client.analyze_sentiment(documents=documents, mine_opinions=True)[0] for sentence in document.sentences: - food_aspect = sentence.aspects[0] - service_aspect = sentence.aspects[1] + food_aspect = sentence.mined_opinions[0].aspect + service_aspect = sentence.mined_opinions[1].aspect self.assertEqual('food', food_aspect.text) self.assertEqual('negative', food_aspect.sentiment) @@ -643,8 +644,8 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual(13, service_aspect.offset) self.assertEqual(7, service_aspect.length) - food_opinion = food_aspect.opinions[0] - service_opinion = service_aspect.opinions[0] + food_opinion = sentence.mined_opinions[0].opinions[0] + service_opinion = sentence.mined_opinions[1].opinions[0] self.assertOpinionsEqual(food_opinion, service_opinion) self.assertEqual('good', food_opinion.text) @@ -660,6 +661,6 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): @TextAnalyticsClientPreparer() def test_aspect_based_sentiment_analysis_v3(self, client): with pytest.raises(TypeError) as excinfo: - client.analyze_sentiment(["will fail"], show_aspects=True) + client.analyze_sentiment(["will fail"], mine_opinions=True) - assert "'show_aspects' is only added for API version v3.1-preview.1 and up" in str(excinfo.value) + assert "'mine_opinions' is only added for API version v3.1-preview.1 and up" in str(excinfo.value) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index ec20868d304d..48e2c9477a11 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -598,10 +598,11 @@ async def test_aspect_based_sentiment_analysis(self, client): "It has a sleek premium aluminum design that makes it beautiful to look at." ] - document = (await client.analyze_sentiment(documents=documents, show_aspects=True))[0] + document = (await client.analyze_sentiment(documents=documents, mine_opinions=True))[0] for sentence in document.sentences: - for aspect in sentence.aspects: + for mined_opinion in sentence.mined_opinions: + aspect = mined_opinion.aspect self.assertEqual('design', aspect.text) self.assertEqual('positive', aspect.sentiment) self.assertIsNotNone(aspect.confidence_scores.positive) @@ -610,7 +611,7 @@ async def test_aspect_based_sentiment_analysis(self, client): self.assertEqual(32, aspect.offset) self.assertEqual(6, aspect.length) - sleek_opinion = aspect.opinions[0] + sleek_opinion = mined_opinion.opinions[0] self.assertEqual('sleek', sleek_opinion.text) self.assertEqual('positive', sleek_opinion.sentiment) self.assertIsNotNone(sleek_opinion.confidence_scores.positive) @@ -620,7 +621,7 @@ async def test_aspect_based_sentiment_analysis(self, client): self.assertEqual(5, sleek_opinion.length) self.assertFalse(sleek_opinion.is_negated) - premium_opinion = aspect.opinions[1] + premium_opinion = mined_opinion.opinions[1] self.assertEqual('premium', premium_opinion.text) self.assertEqual('positive', premium_opinion.sentiment) self.assertIsNotNone(premium_opinion.confidence_scores.positive) @@ -637,11 +638,11 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): "The food and service is not good" ] - document = (await client.analyze_sentiment(documents=documents, show_aspects=True))[0] + document = (await client.analyze_sentiment(documents=documents, mine_opinions=True))[0] for sentence in document.sentences: - food_aspect = sentence.aspects[0] - service_aspect = sentence.aspects[1] + food_aspect = sentence.mined_opinions[0].aspect + service_aspect = sentence.mined_opinions[1].aspect self.assertEqual('food', food_aspect.text) self.assertEqual('negative', food_aspect.sentiment) @@ -659,8 +660,8 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): self.assertEqual(13, service_aspect.offset) self.assertEqual(7, service_aspect.length) - food_opinion = food_aspect.opinions[0] - service_opinion = service_aspect.opinions[0] + food_opinion = sentence.mined_opinions[0].opinions[0] + service_opinion = sentence.mined_opinions[1].opinions[0] self.assertOpinionsEqual(food_opinion, service_opinion) self.assertEqual('good', food_opinion.text) @@ -676,6 +677,6 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): @TextAnalyticsClientPreparer() async def test_aspect_based_sentiment_analysis_v3(self, client): with pytest.raises(TypeError) as excinfo: - await client.analyze_sentiment(["will fail"], show_aspects=True) + await client.analyze_sentiment(["will fail"], mine_opinions=True) - assert "'show_aspects' is only added for API version v3.1-preview.1 and up" in str(excinfo.value) + assert "'mine_opinions' is only added for API version v3.1-preview.1 and up" in str(excinfo.value) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py index f84ccd797343..799261306dda 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py @@ -112,7 +112,7 @@ def test_repr(self): aspect_opinion_confidence_score = _models.SentimentConfidenceScores(positive=0.5, negative=0.5) - aspect_opinion = _models.OpinionSentiment( + opinion_sentiment = _models.OpinionSentiment( text="opinion", sentiment="positive", confidence_scores=aspect_opinion_confidence_score, @@ -121,13 +121,17 @@ def test_repr(self): is_negated=False ) - sentence_aspect = _models.AspectSentiment( + aspect_sentiment = _models.AspectSentiment( text="aspect", sentiment="positive", confidence_scores=aspect_opinion_confidence_score, offset=10, - length=6, - opinions=[aspect_opinion] + length=6 + ) + + mined_opinion = _models.MinedOpinion( + aspect=aspect_sentiment, + opinions=[opinion_sentiment] ) self.assertEqual("DetectedLanguage(name=English, iso6391_name=en, confidence_score=1.0)", repr(detected_language)) @@ -189,16 +193,18 @@ def test_repr(self): self.assertEqual("TextDocumentBatchStatistics(document_count=1, valid_document_count=2, " "erroneous_document_count=3, transaction_count=4)", repr(text_document_batch_statistics)) - aspect_opinion_repr = ( + opinion_sentiment_repr = ( "OpinionSentiment(text=opinion, sentiment=positive, confidence_scores=SentimentConfidenceScores(" "positive=0.5, neutral=0.0, negative=0.5), offset=3, length=7, is_negated=False)" ) - self.assertEqual(aspect_opinion_repr, repr(aspect_opinion)) - self.assertEqual( + self.assertEqual(opinion_sentiment_repr, repr(opinion_sentiment)) + + aspect_sentiment_repr = ( "AspectSentiment(text=aspect, sentiment=positive, confidence_scores=SentimentConfidenceScores(" - "positive=0.5, neutral=0.0, negative=0.5), opinions=[{}], offset=10, length=6)".format(aspect_opinion_repr), - repr(sentence_aspect) + "positive=0.5, neutral=0.0, negative=0.5), offset=10, length=6)" ) + self.assertEqual(aspect_sentiment_repr, repr(aspect_sentiment)) + self.assertEqual("MinedOpinion(aspect={}, opinions=[{}])".format(aspect_sentiment_repr, opinion_sentiment_repr), repr(mined_opinion)) def test_inner_error_takes_precedence(self): From 273fb944c1a9015edbc1037f1d3410acc7e91c54 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Jul 2020 14:47:11 -0400 Subject: [PATCH 18/21] change kwarg from mine_opinions to show_opinion_mining --- .../azure-ai-textanalytics/CHANGELOG.md | 2 +- .../azure/ai/textanalytics/_models.py | 2 +- .../azure/ai/textanalytics/_text_analytics_client.py | 12 ++++++------ .../aio/_text_analytics_client_async.py | 12 ++++++------ .../tests/test_analyze_sentiment.py | 8 ++++---- .../tests/test_analyze_sentiment_async.py | 8 ++++---- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md index aba4ba35e2c9..be363e8685e5 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md +++ b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md @@ -7,7 +7,7 @@ pass in `v3.0` to the kwarg `api_version` when creating your TextAnalyticsClient - We have added an API `recognize_pii_entities` which returns entities containing personal information for a batch of documents. Only available for API version v3.1-preview.1 and up. - We now have added support for opinion mining. To use this feature, you need to make sure you are using the service's -v3.1-preview.1 API. To get this support pass `mine_opinions` as True when calling the `analyze_sentiment` endpoint +v3.1-preview.1 API. To get this support pass `show_opinion_mining` as True when calling the `analyze_sentiment` endpoint ## 5.0.0 (2020-07-27) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index 4790229c9266..bb50b97f9434 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -709,7 +709,7 @@ class SentenceSentiment(DictMixin): :ivar mined_opinions: The list of opinions mined from this sentence. For example in "The food is good, but the service is bad", we would mind these two opinions "food is good", "service is bad". Only returned - if `mine_opinions` is set to True in the call to `analyze_sentiment`. + if `show_opinion_mining` is set to True in the call to `analyze_sentiment`. :vartype mined_opinions: list[~azure.ai.textanalytics.MinedOpinion] """ diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py index 37b124440074..c6a89ba98e02 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_text_analytics_client.py @@ -451,7 +451,7 @@ def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] - :keyword bool mine_opinions: Whether to mine the opinions of a sentence and conduct more + :keyword bool show_opinion_mining: Whether to mine the opinions of a sentence and conduct more granular analysis around the aspects of a product or service (also known as aspect-based sentiment analysis). If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects @@ -467,7 +467,7 @@ def analyze_sentiment( # type: ignore is not specified, the API will default to the latest, non-preview version. :keyword bool show_stats: If set to true, response will contain document level statistics. .. versionadded:: v3.1-preview.1 - The *mine_opinions* parameter. + The *show_opinion_mining* parameter. :return: The combined list of :class:`~azure.ai.textanalytics.AnalyzeSentimentResult` and :class:`~azure.ai.textanalytics.DocumentError` in the order the original documents were passed in. @@ -489,10 +489,10 @@ def analyze_sentiment( # type: ignore docs = _validate_batch_input(documents, "language", language) model_version = kwargs.pop("model_version", None) show_stats = kwargs.pop("show_stats", False) - mine_opinions = kwargs.pop("mine_opinions", None) + show_opinion_mining = kwargs.pop("show_opinion_mining", None) - if mine_opinions is not None: - kwargs.update({"opinion_mining": mine_opinions}) + if show_opinion_mining is not None: + kwargs.update({"opinion_mining": show_opinion_mining}) try: return self._client.sentiment( documents=docs, @@ -504,7 +504,7 @@ def analyze_sentiment( # type: ignore except TypeError as error: if "opinion_mining" in str(error): raise NotImplementedError( - "'mine_opinions' is only available for API version v3.1-preview.1 and up" + "'show_opinion_mining' is only available for API version v3.1-preview.1 and up" ) raise error except HttpResponseError as error: diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py index 2f8e20998283..6d5ea0dd2b67 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_text_analytics_client_async.py @@ -450,7 +450,7 @@ async def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] - :keyword bool mine_opinions: Whether to mine the opinions of a sentence and conduct more + :keyword bool show_opinion_mining: Whether to mine the opinions of a sentence and conduct more granular analysis around the aspects of a product or service (also known as aspect-based sentiment analysis). If set to true, the returned :class:`~azure.ai.textanalytics.SentenceSentiment` objects @@ -466,7 +466,7 @@ async def analyze_sentiment( # type: ignore is not specified, the API will default to the latest, non-preview version. :keyword bool show_stats: If set to true, response will contain document level statistics. .. versionadded:: v3.1-preview.1 - The *mine_opinions* parameter. + The *show_opinion_mining* parameter. :return: The combined list of :class:`~azure.ai.textanalytics.AnalyzeSentimentResult` and :class:`~azure.ai.textanalytics.DocumentError` in the order the original documents were passed in. @@ -488,10 +488,10 @@ async def analyze_sentiment( # type: ignore docs = _validate_batch_input(documents, "language", language) model_version = kwargs.pop("model_version", None) show_stats = kwargs.pop("show_stats", False) - mine_opinions = kwargs.pop("mine_opinions", None) + show_opinion_mining = kwargs.pop("show_opinion_mining", None) - if mine_opinions is not None: - kwargs.update({"opinion_mining": mine_opinions}) + if show_opinion_mining is not None: + kwargs.update({"opinion_mining": show_opinion_mining}) try: return await self._client.sentiment( @@ -504,7 +504,7 @@ async def analyze_sentiment( # type: ignore except TypeError as error: if "opinion_mining" in str(error): raise NotImplementedError( - "'mine_opinions' is only available for API version v3.1-preview.1 and up" + "'show_opinion_mining' is only available for API version v3.1-preview.1 and up" ) raise error except HttpResponseError as error: diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index 6bb79177f12b..6e30c4b9d832 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -582,7 +582,7 @@ def test_aspect_based_sentiment_analysis(self, client): "It has a sleek premium aluminum design that makes it beautiful to look at." ] - document = client.analyze_sentiment(documents=documents, mine_opinions=True)[0] + document = client.analyze_sentiment(documents=documents, show_opinion_mining=True)[0] for sentence in document.sentences: for mined_opinion in sentence.mined_opinions: @@ -622,7 +622,7 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): "The food and service is not good" ] - document = client.analyze_sentiment(documents=documents, mine_opinions=True)[0] + document = client.analyze_sentiment(documents=documents, show_opinion_mining=True)[0] for sentence in document.sentences: food_aspect = sentence.mined_opinions[0].aspect @@ -661,6 +661,6 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) def test_aspect_based_sentiment_analysis_v3(self, client): with pytest.raises(NotImplementedError) as excinfo: - client.analyze_sentiment(["will fail"], mine_opinions=True) + client.analyze_sentiment(["will fail"], show_opinion_mining=True) - assert "'mine_opinions' is only available for API version v3.1-preview.1 and up" in str(excinfo.value) + assert "'show_opinion_mining' is only available for API version v3.1-preview.1 and up" in str(excinfo.value) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index f45de0509957..70c3ec2b6a1f 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -598,7 +598,7 @@ async def test_aspect_based_sentiment_analysis(self, client): "It has a sleek premium aluminum design that makes it beautiful to look at." ] - document = (await client.analyze_sentiment(documents=documents, mine_opinions=True))[0] + document = (await client.analyze_sentiment(documents=documents, show_opinion_mining=True))[0] for sentence in document.sentences: for mined_opinion in sentence.mined_opinions: @@ -638,7 +638,7 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): "The food and service is not good" ] - document = (await client.analyze_sentiment(documents=documents, mine_opinions=True))[0] + document = (await client.analyze_sentiment(documents=documents, show_opinion_mining=True))[0] for sentence in document.sentences: food_aspect = sentence.mined_opinions[0].aspect @@ -677,6 +677,6 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) async def test_aspect_based_sentiment_analysis_v3(self, client): with pytest.raises(NotImplementedError) as excinfo: - await client.analyze_sentiment(["will fail"], mine_opinions=True) + await client.analyze_sentiment(["will fail"], show_opinion_mining=True) - assert "'mine_opinions' is only available for API version v3.1-preview.1 and up" in str(excinfo.value) + assert "'show_opinion_mining' is only available for API version v3.1-preview.1 and up" in str(excinfo.value) From 4180ad09e9014be2b62efc47d7957e3405eba18b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Jul 2020 17:33:33 -0400 Subject: [PATCH 19/21] change test names from aspect_based_sentiment_analysis to opinion_mining --- ...ml => test_analyze_sentiment.test_opinion_mining.yaml} | 8 ++++---- ...ntiment.test_opinion_mining_with_negated_opinion.yaml} | 8 ++++---- ...test_analyze_sentiment_async.test_opinion_mining.yaml} | 8 ++++---- ...t_async.test_opinion_mining_with_negated_opinion.yaml} | 8 ++++---- .../tests/test_analyze_sentiment.py | 6 +++--- .../tests/test_analyze_sentiment_async.py | 6 +++--- 6 files changed, 22 insertions(+), 22 deletions(-) rename sdk/textanalytics/azure-ai-textanalytics/tests/recordings/{test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml => test_analyze_sentiment.test_opinion_mining.yaml} (92%) rename sdk/textanalytics/azure-ai-textanalytics/tests/recordings/{test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml => test_analyze_sentiment.test_opinion_mining_with_negated_opinion.yaml} (92%) rename sdk/textanalytics/azure-ai-textanalytics/tests/recordings/{test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml => test_analyze_sentiment_async.test_opinion_mining.yaml} (90%) rename sdk/textanalytics/azure-ai-textanalytics/tests/recordings/{test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml => test_analyze_sentiment_async.test_opinion_mining_with_negated_opinion.yaml} (89%) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining.yaml similarity index 92% rename from sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml rename to sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining.yaml index d8b23aa54c34..9daa244e8e3b 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis.yaml +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining.yaml @@ -14,7 +14,7 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + - azsdk-python-ai-textanalytics/5.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) method: POST uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true response: @@ -23,13 +23,13 @@ interactions: has a sleek premium aluminum design that makes it beautiful to look at.","aspects":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":32,"length":6,"text":"design","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"},{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/1"}]}],"opinions":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":9,"length":5,"text":"sleek","isNegated":false},{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":15,"length":7,"text":"premium","isNegated":false}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' headers: apim-request-id: - - c0d3172f-8ac0-4cd0-b1d8-27116cb5b854 + - 4c02e1ce-36d5-4e40-ab94-817af736669d content-type: - application/json; charset=utf-8 csp-billing-usage: - CognitiveServices.TextAnalytics.BatchScoring=1 date: - - Tue, 14 Jul 2020 17:43:15 GMT + - Thu, 30 Jul 2020 21:31:42 GMT strict-transport-security: - max-age=31536000; includeSubDomains; preload transfer-encoding: @@ -37,7 +37,7 @@ interactions: x-content-type-options: - nosniff x-envoy-upstream-service-time: - - '98' + - '114' status: code: 200 message: OK diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_with_negated_opinion.yaml similarity index 92% rename from sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml rename to sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_with_negated_opinion.yaml index 73eba57a49eb..a907e92069f7 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_aspect_based_sentiment_analysis_negated_opinion.yaml +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_with_negated_opinion.yaml @@ -14,7 +14,7 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + - azsdk-python-ai-textanalytics/5.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) method: POST uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true response: @@ -23,13 +23,13 @@ interactions: food and service is not good","aspects":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":4,"length":4,"text":"food","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]},{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":13,"length":7,"text":"service","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]}],"opinions":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":28,"length":4,"text":"good","isNegated":true}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' headers: apim-request-id: - - 628974f5-51d3-4a84-958b-9e0a695dbf47 + - 301fd828-cccc-417e-bd0d-83eeb9dfb542 content-type: - application/json; charset=utf-8 csp-billing-usage: - CognitiveServices.TextAnalytics.BatchScoring=1 date: - - Tue, 14 Jul 2020 17:43:16 GMT + - Thu, 30 Jul 2020 21:31:42 GMT strict-transport-security: - max-age=31536000; includeSubDomains; preload transfer-encoding: @@ -37,7 +37,7 @@ interactions: x-content-type-options: - nosniff x-envoy-upstream-service-time: - - '102' + - '97' status: code: 200 message: OK diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining.yaml similarity index 90% rename from sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml rename to sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining.yaml index 5ff518c4daa0..8bd21eaab0d4 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis.yaml +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining.yaml @@ -10,7 +10,7 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + - azsdk-python-ai-textanalytics/5.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) method: POST uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true response: @@ -18,14 +18,14 @@ interactions: string: '{"documents":[{"id":"0","sentiment":"positive","confidenceScores":{"positive":0.98,"neutral":0.02,"negative":0.0},"sentences":[{"sentiment":"positive","confidenceScores":{"positive":0.98,"neutral":0.02,"negative":0.0},"offset":0,"length":74,"text":"It has a sleek premium aluminum design that makes it beautiful to look at.","aspects":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":32,"length":6,"text":"design","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"},{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/1"}]}],"opinions":[{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":9,"length":5,"text":"sleek","isNegated":false},{"sentiment":"positive","confidenceScores":{"positive":1.0,"negative":0.0},"offset":15,"length":7,"text":"premium","isNegated":false}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' headers: - apim-request-id: 622b5442-6a46-4d23-8a95-c46265b09262 + apim-request-id: 6576433c-7362-4637-804c-a25189ff691d content-type: application/json; charset=utf-8 csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 - date: Tue, 14 Jul 2020 17:44:20 GMT + date: Thu, 30 Jul 2020 21:31:43 GMT strict-transport-security: max-age=31536000; includeSubDomains; preload transfer-encoding: chunked x-content-type-options: nosniff - x-envoy-upstream-service-time: '93' + x-envoy-upstream-service-time: '96' status: code: 200 message: OK diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_with_negated_opinion.yaml similarity index 89% rename from sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml rename to sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_with_negated_opinion.yaml index 9b5f12af6341..240f3545c987 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_aspect_based_sentiment_analysis_negated_opinion.yaml +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_with_negated_opinion.yaml @@ -10,7 +10,7 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-ai-textanalytics/1.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + - azsdk-python-ai-textanalytics/5.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) method: POST uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true response: @@ -18,14 +18,14 @@ interactions: string: '{"documents":[{"id":"0","sentiment":"negative","confidenceScores":{"positive":0.0,"neutral":0.0,"negative":1.0},"sentences":[{"sentiment":"negative","confidenceScores":{"positive":0.0,"neutral":0.0,"negative":1.0},"offset":0,"length":32,"text":"The food and service is not good","aspects":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":4,"length":4,"text":"food","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]},{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":13,"length":7,"text":"service","relations":[{"relationType":"opinion","ref":"#/documents/0/sentences/0/opinions/0"}]}],"opinions":[{"sentiment":"negative","confidenceScores":{"positive":0.01,"negative":0.99},"offset":28,"length":4,"text":"good","isNegated":true}]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' headers: - apim-request-id: b2622874-68b3-442e-8b03-aa9ab9b4ed63 + apim-request-id: c2bf1989-2526-45db-a201-02972303c315 content-type: application/json; charset=utf-8 csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 - date: Tue, 14 Jul 2020 17:44:19 GMT + date: Thu, 30 Jul 2020 21:31:43 GMT strict-transport-security: max-age=31536000; includeSubDomains; preload transfer-encoding: chunked x-content-type-options: nosniff - x-envoy-upstream-service-time: '102' + x-envoy-upstream-service-time: '80' status: code: 200 message: OK diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index 6e30c4b9d832..ba45e76f6b74 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -577,7 +577,7 @@ def callback(pipeline_response, deserialized, _): @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer() - def test_aspect_based_sentiment_analysis(self, client): + def test_opinion_mining(self, client): documents = [ "It has a sleek premium aluminum design that makes it beautiful to look at." ] @@ -617,7 +617,7 @@ def test_aspect_based_sentiment_analysis(self, client): @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer() - def test_aspect_based_sentiment_analysis_negated_opinion(self, client): + def test_opinion_mining_with_negated_opinion(self, client): documents = [ "The food and service is not good" ] @@ -659,7 +659,7 @@ def test_aspect_based_sentiment_analysis_negated_opinion(self, client): @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) - def test_aspect_based_sentiment_analysis_v3(self, client): + def test_opinion_mining_v3(self, client): with pytest.raises(NotImplementedError) as excinfo: client.analyze_sentiment(["will fail"], show_opinion_mining=True) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index 70c3ec2b6a1f..f2b20e60c3dd 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -593,7 +593,7 @@ def callback(pipeline_response, deserialized, _): @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer() - async def test_aspect_based_sentiment_analysis(self, client): + async def test_opinion_mining(self, client): documents = [ "It has a sleek premium aluminum design that makes it beautiful to look at." ] @@ -633,7 +633,7 @@ async def test_aspect_based_sentiment_analysis(self, client): @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer() - async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): + async def test_opinion_mining_with_negated_opinion(self, client): documents = [ "The food and service is not good" ] @@ -675,7 +675,7 @@ async def test_aspect_based_sentiment_analysis_negated_opinion(self, client): @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) - async def test_aspect_based_sentiment_analysis_v3(self, client): + async def test_opinion_mining_v3(self, client): with pytest.raises(NotImplementedError) as excinfo: await client.analyze_sentiment(["will fail"], show_opinion_mining=True) From 5b804d29322bcad40df7d83b060cf75d84f6e032 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Jul 2020 18:00:46 -0400 Subject: [PATCH 20/21] add test for opinion mining with no mined opinions --- ...test_opinion_mining_no_mined_opinions.yaml | 43 +++++++++++++++++++ ...test_opinion_mining_no_mined_opinions.yaml | 32 ++++++++++++++ .../tests/test_analyze_sentiment.py | 7 +++ .../tests/test_analyze_sentiment_async.py | 7 +++ 4 files changed, 89 insertions(+) create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_no_mined_opinions.yaml create mode 100644 sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_no_mined_opinions.yaml diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_no_mined_opinions.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_no_mined_opinions.yaml new file mode 100644 index 000000000000..699be403bc2a --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_no_mined_opinions.yaml @@ -0,0 +1,43 @@ +interactions: +- request: + body: '{"documents": [{"id": "0", "text": "today is a hot day", "language": "en"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '76' + Content-Type: + - application/json + User-Agent: + - azsdk-python-ai-textanalytics/5.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + method: POST + uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true + response: + body: + string: '{"documents":[{"id":"0","sentiment":"neutral","confidenceScores":{"positive":0.1,"neutral":0.88,"negative":0.02},"sentences":[{"sentiment":"neutral","confidenceScores":{"positive":0.1,"neutral":0.88,"negative":0.02},"offset":0,"length":18,"text":"today + is a hot day","aspects":[],"opinions":[]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' + headers: + apim-request-id: + - 8c5e391d-7ced-4b43-a89b-d87abc04c772 + content-type: + - application/json; charset=utf-8 + csp-billing-usage: + - CognitiveServices.TextAnalytics.BatchScoring=1 + date: + - Thu, 30 Jul 2020 21:38:55 GMT + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '90' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_no_mined_opinions.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_no_mined_opinions.yaml new file mode 100644 index 000000000000..882cb8156f35 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_no_mined_opinions.yaml @@ -0,0 +1,32 @@ +interactions: +- request: + body: '{"documents": [{"id": "0", "text": "today is a hot day", "language": "en"}]}' + headers: + Accept: + - application/json + Content-Length: + - '76' + Content-Type: + - application/json + User-Agent: + - azsdk-python-ai-textanalytics/5.0.1 Python/3.7.7 (Darwin-17.7.0-x86_64-i386-64bit) + method: POST + uri: https://westus2.api.cognitive.microsoft.com/text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true + response: + body: + string: '{"documents":[{"id":"0","sentiment":"neutral","confidenceScores":{"positive":0.1,"neutral":0.88,"negative":0.02},"sentences":[{"sentiment":"neutral","confidenceScores":{"positive":0.1,"neutral":0.88,"negative":0.02},"offset":0,"length":18,"text":"today + is a hot day","aspects":[],"opinions":[]}],"warnings":[]}],"errors":[],"modelVersion":"2020-04-01"}' + headers: + apim-request-id: feb1be87-502c-4e43-b981-c7fb7e6d2b30 + content-type: application/json; charset=utf-8 + csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 + date: Thu, 30 Jul 2020 21:38:56 GMT + strict-transport-security: max-age=31536000; includeSubDomains; preload + transfer-encoding: chunked + x-content-type-options: nosniff + x-envoy-upstream-service-time: '128' + status: + code: 200 + message: OK + url: https://westus2.api.cognitive.microsoft.com//text/analytics/v3.1-preview.1/sentiment?showStats=false&opinionMining=true +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py index ba45e76f6b74..86c0681c2966 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py @@ -657,6 +657,13 @@ def test_opinion_mining_with_negated_opinion(self, client): self.assertEqual(4, food_opinion.length) self.assertTrue(food_opinion.is_negated) + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + def test_opinion_mining_no_mined_opinions(self, client): + document = client.analyze_sentiment(documents=["today is a hot day"], show_opinion_mining=True)[0] + + assert not document.sentences[0].mined_opinions + @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) def test_opinion_mining_v3(self, client): diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py index f2b20e60c3dd..b6151ac14ff9 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py @@ -673,6 +673,13 @@ async def test_opinion_mining_with_negated_opinion(self, client): self.assertEqual(4, food_opinion.length) self.assertTrue(food_opinion.is_negated) + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + async def test_opinion_mining_no_mined_opinions(self, client): + document = (await client.analyze_sentiment(documents=["today is a hot day"], show_opinion_mining=True))[0] + + assert not document.sentences[0].mined_opinions + @GlobalTextAnalyticsAccountPreparer() @TextAnalyticsClientPreparer(client_kwargs={"api_version": ApiVersion.V3_0}) async def test_opinion_mining_v3(self, client): From f03b4779f5985fdb0cc011da5fdddcf7cd9de1f7 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Jul 2020 18:05:19 -0400 Subject: [PATCH 21/21] have no mined opinions return as [] for v3.1-preview.1, and None for v3.0 --- .../azure/ai/textanalytics/_models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py index bb50b97f9434..682ffc9e2a51 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -722,14 +722,18 @@ def __init__(self, **kwargs): @classmethod def _from_generated(cls, sentence, results): + if hasattr(sentence, "aspects"): + mined_opinions = ( + [MinedOpinion._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access + if sentence.aspects else [] + ) + else: + mined_opinions = None return cls( text=sentence.text, sentiment=sentence.sentiment, confidence_scores=SentimentConfidenceScores._from_generated(sentence.confidence_scores), # pylint: disable=protected-access - mined_opinions=( - [MinedOpinion._from_generated(aspect, results) for aspect in sentence.aspects] # pylint: disable=protected-access - if (hasattr(sentence, "aspects") and sentence.aspects) else None - ) + mined_opinions=mined_opinions ) def __repr__(self):