diff --git a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md index 8cbe2435b6d8..be363e8685e5 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md +++ b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md @@ -6,6 +6,8 @@ - We are now targeting the service's v3.1-preview.1 API as the default. If you would like to still use version v3.0 of the service, 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 `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/__init__.py b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/__init__.py index e0ac06a728c6..bde288eb0cf6 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,11 @@ TextDocumentBatchStatistics, SentenceSentiment, SentimentConfidenceScores, + MinedOpinion, + AspectSentiment, + OpinionSentiment, RecognizePiiEntitiesResult, - PiiEntity + PiiEntity, ) __all__ = [ @@ -51,6 +54,9 @@ 'TextDocumentBatchStatistics', 'SentenceSentiment', 'SentimentConfidenceScores', + 'MinedOpinion', + 'AspectSentiment', + 'OpinionSentiment', 'RecognizePiiEntitiesResult', 'PiiEntity', ] 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 109c60c5fb95..682ffc9e2a51 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_models.py @@ -3,10 +3,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import re +from ._generated.v3_0.models._models import ( + LanguageInput, + MultiLanguageInput +) -from ._generated.v3_0.models._models import LanguageInput -from ._generated.v3_0.models._models import MultiLanguageInput - +def _get_indices(relation): + return [int(s) for s in re.findall(r"\d+", relation)] class DictMixin(object): @@ -702,19 +706,34 @@ class SentenceSentiment(DictMixin): and 1 for the sentence for all labels. :vartype confidence_scores: ~azure.ai.textanalytics.SentimentConfidenceScores + :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 `show_opinion_mining` 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.mined_opinions = kwargs.get("mined_opinions", None) @classmethod - def _from_generated(cls, sentence): + 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=mined_opinions ) def __repr__(self): @@ -724,6 +743,150 @@ 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, + 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 + 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 int offset: The aspect offset from the start of the sentence. + :ivar int length: The length 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.offset = kwargs.get("offset", None) + self.length = kwargs.get("length", None) + + @classmethod + 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 + offset=aspect.offset, + length=aspect.length + ) + + def __repr__(self): + return "AspectSentiment(text={}, sentiment={}, confidence_scores={}, offset={}, length={})".format( + self.text, + self.sentiment, + repr(self.confidence_scores), + self.offset, + self.length + )[:1024] + + +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'. + + :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 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. + """ + + 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 ( + "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): """The confidence scores (Softmax scores) between 0 and 1. @@ -738,15 +901,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 new file mode 100644 index 000000000000..3d29a0c06583 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_multiapi.py @@ -0,0 +1,42 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + +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_0 = "v3.0" + + +def load_generated_api(api_version, aio=False): + 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) + api_version = ApiVersion(api_version) + except ValueError: + # api_version is unknown to ApiVersion + raise NotImplementedError( + "This package doesn't support API version '{}'. ".format(api_version) + + "Supported versions: {}".format(", ".join(v.value for v in ApiVersion)) + ) + + if api_version == ApiVersion.V3_1_PREVIEW_1: + if aio: + from ._generated.v3_1_preview_1.aio import TextAnalyticsClient + else: + from ._generated.v3_1_preview_1 import TextAnalyticsClient # type: ignore + elif api_version == ApiVersion.V3_0: + if aio: + from ._generated.v3_0.aio import TextAnalyticsClient # type: ignore + else: + from ._generated.v3_0 import TextAnalyticsClient # type: ignore + return TextAnalyticsClient 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 03bf101131fa..94b4593dd769 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 @@ -108,14 +108,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 @@ -125,7 +125,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 @@ -135,7 +135,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 @@ -145,7 +145,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, @@ -155,18 +155,18 @@ 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 ) @prepare_result -def pii_entities_result(entity): +def pii_entities_result(entity, results): # pylint: disable=unused-argument return RecognizePiiEntitiesResult( id=entity.id, entities=[PiiEntity._from_generated(e) for e in entity.entities], # 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 c3a52be62102..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,6 +451,12 @@ def analyze_sentiment( # type: ignore :type documents: list[str] or list[~azure.ai.textanalytics.TextDocumentInput] or list[dict[str, str]] + :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 + 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. If not set, uses "en" for English as default. Per-document language will @@ -460,6 +466,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_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. @@ -481,6 +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) + show_opinion_mining = kwargs.pop("show_opinion_mining", None) + + if show_opinion_mining is not None: + kwargs.update({"opinion_mining": show_opinion_mining}) try: return self._client.sentiment( documents=docs, @@ -489,5 +501,11 @@ def analyze_sentiment( # type: ignore cls=kwargs.pop("cls", sentiment_result), **kwargs ) + except TypeError as error: + if "opinion_mining" in str(error): + raise NotImplementedError( + "'show_opinion_mining' is only available for API version v3.1-preview.1 and up" + ) + raise error except HttpResponseError as error: process_batch_error(error) 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', ] 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 89ac9fc6c943..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,6 +450,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_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 + 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. If not set, uses "en" for English as default. Per-document language will @@ -459,6 +465,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_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. @@ -480,6 +488,11 @@ 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_opinion_mining = kwargs.pop("show_opinion_mining", None) + + if show_opinion_mining is not None: + kwargs.update({"opinion_mining": show_opinion_mining}) + try: return await self._client.sentiment( documents=docs, @@ -488,5 +501,11 @@ async def analyze_sentiment( # type: ignore cls=kwargs.pop("cls", sentiment_result), **kwargs ) + except TypeError as error: + if "opinion_mining" in str(error): + raise NotImplementedError( + "'show_opinion_mining' is only available for API version v3.1-preview.1 and up" + ) + raise error except HttpResponseError as error: process_batch_error(error) diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining.yaml new file mode 100644 index 000000000000..9daa244e8e3b --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining.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/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":"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: + - 4c02e1ce-36d5-4e40-ab94-817af736669d + content-type: + - application/json; charset=utf-8 + csp-billing-usage: + - CognitiveServices.TextAnalytics.BatchScoring=1 + date: + - Thu, 30 Jul 2020 21:31:42 GMT + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '114' + status: + code: 200 + message: OK +version: 1 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.test_opinion_mining_with_negated_opinion.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_with_negated_opinion.yaml new file mode 100644 index 000000000000..a907e92069f7 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment.test_opinion_mining_with_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/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":"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: + - 301fd828-cccc-417e-bd0d-83eeb9dfb542 + content-type: + - application/json; charset=utf-8 + csp-billing-usage: + - CognitiveServices.TextAnalytics.BatchScoring=1 + date: + - Thu, 30 Jul 2020 21:31:42 GMT + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '97' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining.yaml new file mode 100644 index 000000000000..8bd21eaab0d4 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining.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/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":"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: 6576433c-7362-4637-804c-a25189ff691d + content-type: application/json; charset=utf-8 + csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 + 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: '96' + 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_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/recordings/test_analyze_sentiment_async.test_opinion_mining_with_negated_opinion.yaml b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_with_negated_opinion.yaml new file mode 100644 index 000000000000..240f3545c987 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/recordings/test_analyze_sentiment_async.test_opinion_mining_with_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/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":"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: c2bf1989-2526-45db-a201-02972303c315 + content-type: application/json; charset=utf-8 + csp-billing-usage: CognitiveServices.TextAnalytics.BatchScoring=1 + 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: '80' + 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..86c0681c2966 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,100 @@ def callback(pipeline_response, deserialized, _): cls=callback ) assert res == "cls result" + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + def test_opinion_mining(self, client): + documents = [ + "It has a sleek premium aluminum design that makes it beautiful to look at." + ] + + document = client.analyze_sentiment(documents=documents, show_opinion_mining=True)[0] + + for sentence in document.sentences: + 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) + self.assertEqual(0.0, aspect.confidence_scores.neutral) + self.assertIsNotNone(aspect.confidence_scores.negative) + self.assertEqual(32, aspect.offset) + self.assertEqual(6, aspect.length) + + 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) + self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) + 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) + + 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) + self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) + 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) + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + def test_opinion_mining_with_negated_opinion(self, client): + documents = [ + "The food and service is not good" + ] + + document = client.analyze_sentiment(documents=documents, show_opinion_mining=True)[0] + + for sentence in document.sentences: + 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) + self.assertIsNotNone(food_aspect.confidence_scores.positive) + self.assertEqual(0.0, food_aspect.confidence_scores.neutral) + 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(service_aspect.confidence_scores.positive) + self.assertEqual(0.0, service_aspect.confidence_scores.neutral) + self.assertIsNotNone(service_aspect.confidence_scores.negative) + self.assertEqual(13, service_aspect.offset) + self.assertEqual(7, service_aspect.length) + + 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) + self.assertEqual('negative', food_opinion.sentiment) + self.assertIsNotNone(food_opinion.confidence_scores.positive) + self.assertEqual(0.0, food_opinion.confidence_scores.neutral) + 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) + + @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): + with pytest.raises(NotImplementedError) as excinfo: + client.analyze_sentiment(["will fail"], show_opinion_mining=True) + + 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 48b36ae9a055..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 @@ -16,7 +16,8 @@ from azure.ai.textanalytics import ( VERSION, DetectLanguageInput, - TextDocumentInput + TextDocumentInput, + ApiVersion ) from testcase import GlobalTextAnalyticsAccountPreparer @@ -589,3 +590,100 @@ def callback(pipeline_response, deserialized, _): cls=callback ) assert res == "cls result" + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + async def test_opinion_mining(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_opinion_mining=True))[0] + + for sentence in document.sentences: + 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) + self.assertEqual(0.0, aspect.confidence_scores.neutral) + self.assertIsNotNone(aspect.confidence_scores.negative) + self.assertEqual(32, aspect.offset) + self.assertEqual(6, aspect.length) + + 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) + self.assertEqual(0.0, sleek_opinion.confidence_scores.neutral) + 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) + + 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) + self.assertEqual(0.0, premium_opinion.confidence_scores.neutral) + 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) + + @GlobalTextAnalyticsAccountPreparer() + @TextAnalyticsClientPreparer() + async def test_opinion_mining_with_negated_opinion(self, client): + documents = [ + "The food and service is not good" + ] + + document = (await client.analyze_sentiment(documents=documents, show_opinion_mining=True))[0] + + for sentence in document.sentences: + 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) + self.assertIsNotNone(food_aspect.confidence_scores.positive) + self.assertEqual(0.0, food_aspect.confidence_scores.neutral) + 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(service_aspect.confidence_scores.positive) + self.assertEqual(0.0, service_aspect.confidence_scores.neutral) + self.assertIsNotNone(service_aspect.confidence_scores.negative) + self.assertEqual(13, service_aspect.offset) + self.assertEqual(7, service_aspect.length) + + 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) + self.assertEqual('negative', food_opinion.sentiment) + self.assertIsNotNone(food_opinion.confidence_scores.positive) + self.assertEqual(0.0, food_opinion.confidence_scores.neutral) + 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) + + @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): + with pytest.raises(NotImplementedError) as excinfo: + await client.analyze_sentiment(["will fail"], show_opinion_mining=True) + + 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_multiapi_async.py b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py index 830072716808..1e56ad93d590 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_multiapi_async.py @@ -4,7 +4,6 @@ # Licensed under the MIT License. # ------------------------------------ import functools -from azure.core.credentials import AzureKeyCredential from azure.ai.textanalytics import ApiVersion from azure.ai.textanalytics.aio import TextAnalyticsClient from testcase import TextAnalyticsTest, GlobalTextAnalyticsAccountPreparer 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 d49a4a0cc3e4..7d3ea071ff5c 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py +++ b/sdk/textanalytics/azure-ai-textanalytics/tests/test_text_analytics.py @@ -120,6 +120,30 @@ def test_repr(self): transaction_count=4 ) + aspect_opinion_confidence_score = _models.SentimentConfidenceScores(positive=0.5, negative=0.5) + + opinion_sentiment = _models.OpinionSentiment( + text="opinion", + sentiment="positive", + confidence_scores=aspect_opinion_confidence_score, + offset=3, + length=7, + is_negated=False + ) + + aspect_sentiment = _models.AspectSentiment( + text="aspect", + sentiment="positive", + confidence_scores=aspect_opinion_confidence_score, + offset=10, + 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)) self.assertEqual("CategorizedEntity(text=Bill Gates, category=Person, subcategory=Age, confidence_score=0.899)", repr(categorized_entity)) @@ -186,6 +210,20 @@ 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)) + 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(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), 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): generated_innererror = _generated_models.InnerError( code="UnsupportedLanguageCode", 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") 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):