From b590308b79a230561aed776f55260a73668c8efc Mon Sep 17 00:00:00 2001 From: wuyuexin Date: Wed, 30 Sep 2020 13:34:08 -0700 Subject: [PATCH] docs(samples): add initial sample codes (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-dialogflow-cx/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #12 🦕 --- .github/CODEOWNERS | 2 +- .github/snippet-bot.yml | 0 samples/AUTHORING_GUIDE.md | 1 + samples/CONTRIBUTING.md | 1 + samples/snippets/README.rst | 221 +++++++++++++++++ samples/snippets/README.rst.in | 25 ++ samples/snippets/detect_intent_audio.py | 107 +++++++++ samples/snippets/detect_intent_audio_test.py | 37 +++ samples/snippets/detect_intent_stream.py | 129 ++++++++++ samples/snippets/detect_intent_stream_test.py | 38 +++ samples/snippets/detect_intent_texts.py | 101 ++++++++ samples/snippets/detect_intent_texts_test.py | 35 +++ samples/snippets/noxfile.py | 224 ++++++++++++++++++ samples/snippets/noxfile_config.py | 40 ++++ samples/snippets/requirements-test.txt | 1 + samples/snippets/requirements.txt | 1 + samples/snippets/resources/hello.wav | Bin 0 -> 29564 bytes 17 files changed, 962 insertions(+), 1 deletion(-) create mode 100644 .github/snippet-bot.yml create mode 100644 samples/AUTHORING_GUIDE.md create mode 100644 samples/CONTRIBUTING.md create mode 100644 samples/snippets/README.rst create mode 100644 samples/snippets/README.rst.in create mode 100644 samples/snippets/detect_intent_audio.py create mode 100644 samples/snippets/detect_intent_audio_test.py create mode 100644 samples/snippets/detect_intent_stream.py create mode 100644 samples/snippets/detect_intent_stream_test.py create mode 100644 samples/snippets/detect_intent_texts.py create mode 100644 samples/snippets/detect_intent_texts_test.py create mode 100644 samples/snippets/noxfile.py create mode 100644 samples/snippets/noxfile_config.py create mode 100644 samples/snippets/requirements-test.txt create mode 100644 samples/snippets/requirements.txt create mode 100644 samples/snippets/resources/hello.wav diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30c3973a..9cd0a337 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,4 @@ * @googleapis/yoshi-python # The python-samples-reviewers team is the default owner for samples changes -/samples/ @googleapis/python-samples-owners \ No newline at end of file +/samples/ @googleapis/python-samples-owners @wuyuexin \ No newline at end of file diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 00000000..e69de29b diff --git a/samples/AUTHORING_GUIDE.md b/samples/AUTHORING_GUIDE.md new file mode 100644 index 00000000..55c97b32 --- /dev/null +++ b/samples/AUTHORING_GUIDE.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/AUTHORING_GUIDE.md \ No newline at end of file diff --git a/samples/CONTRIBUTING.md b/samples/CONTRIBUTING.md new file mode 100644 index 00000000..34c882b6 --- /dev/null +++ b/samples/CONTRIBUTING.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/CONTRIBUTING.md \ No newline at end of file diff --git a/samples/snippets/README.rst b/samples/snippets/README.rst new file mode 100644 index 00000000..7c403a4d --- /dev/null +++ b/samples/snippets/README.rst @@ -0,0 +1,221 @@ + +.. This file is automatically generated. Do not edit this file directly. + +Dialogflow CX API Python Samples +=============================================================================== + +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=/README.rst + + +This directory contains samples for Dialogflow CX API. The `Dialogflow CX API`_ enables you to create conversational experiences across devices and platforms. + + + + +.. _Dialogflow CX API: https://cloud.google.com/dialogflow/cx/docs/ + + +Setup +------------------------------------------------------------------------------- + + + +Authentication +++++++++++++++ + +This sample requires you to have authentication setup. Refer to the +`Authentication Getting Started Guide`_ for instructions on setting up +credentials for applications. + +.. _Authentication Getting Started Guide: + https://cloud.google.com/docs/authentication/getting-started + + + + +Install Dependencies +++++++++++++++++++++ + +#. Clone python-docs-samples and change directory to the sample directory you want to use. + + .. code-block:: bash + + $ git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. You may want to refer to the `Python Development Environment Setup Guide`_ for Google Cloud Platform for instructions. + + .. _Python Development Environment Setup Guide: + https://cloud.google.com/python/setup + +#. Create a virtualenv. Samples are compatible with Python 3.6+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ + + + + + + +Samples +------------------------------------------------------------------------------- + + +Detect Intent Text ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=/detect_intent_texts.py,/README.rst + + + + +To run this sample: + +.. code-block:: bash + + $ python detect_intent_texts.py + + + usage: detect_intent_texts.py [-h] --agent AGENT [--session-id SESSION_ID] + [--language-code LANGUAGE_CODE] + texts [texts ...] + + DialogFlow API Detect Intent Python sample with text inputs. + + Examples: + python detect_intent_texts.py -h + python detect_intent_texts.py --agent AGENT --session-id SESSION_ID "hello" "book a meeting room" "Mountain View" + python detect_intent_texts.py --agent AGENT --session-id SESSION_ID "tomorrow" "10 AM" "2 hours" "10 people" "A" "yes" + + positional arguments: + texts Text inputs. + + optional arguments: + -h, --help show this help message and exit + --agent AGENT Agent resource name. Required. + --session-id SESSION_ID + Identifier of the DetectIntent session. Defaults to a + random UUID. + --language-code LANGUAGE_CODE + Language code of the query. Defaults to "en-US". + + + + + +Detect Intent Audio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=/detect_intent_audio.py,/README.rst + + + + +To run this sample: + +.. code-block:: bash + + $ python detect_intent_audio.py + + + usage: detect_intent_audio.py [-h] --agent AGENT [--session-id SESSION_ID] + [--language-code LANGUAGE_CODE] + --audio-file-path AUDIO_FILE_PATH + + DialogFlow API Detect Intent Python sample with audio file. + + Examples: + python detect_intent_audio.py -h + python detect_intent_audio.py --agent AGENT --session-id SESSION_ID --audio-file-path resources/hello.wav + + optional arguments: + -h, --help show this help message and exit + --agent AGENT Agent resource name. Required. + --session-id SESSION_ID + Identifier of the DetectIntent session. Defaults to a + random UUID. + --language-code LANGUAGE_CODE + Language code of the query. Defaults to "en-US". + --audio-file-path AUDIO_FILE_PATH + Path to the audio file. + + + + + +Detect Intent Stream ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=/detect_intent_stream.py,/README.rst + + + + +To run this sample: + +.. code-block:: bash + + $ python detect_intent_stream.py + + + usage: detect_intent_stream.py [-h] --agent AGENT [--session-id SESSION_ID] + [--language-code LANGUAGE_CODE] + --audio-file-path AUDIO_FILE_PATH + + DialogFlow API Detect Intent Python sample with audio files processed as an audio stream. + + Examples: + python detect_intent_stream.py -h + python detect_intent_stream.py --agent AGENT --session-id SESSION_ID --audio-file-path resources/hello.wav + + optional arguments: + -h, --help show this help message and exit + --agent AGENT Agent resource name. Required. + --session-id SESSION_ID + Identifier of the DetectIntent session. Defaults to a + random UUID. + --language-code LANGUAGE_CODE + Language code of the query. Defaults to "en-US". + --audio-file-path AUDIO_FILE_PATH + Path to the audio file. + + + + + + + + + +The client library +------------------------------------------------------------------------------- + +This sample uses the `Google Cloud Client Library for Python`_. +You can read the documentation for more details on API usage and use GitHub +to `browse the source`_ and `report issues`_. + +.. _Google Cloud Client Library for Python: + https://googlecloudplatform.github.io/google-cloud-python/ +.. _browse the source: + https://github.com/GoogleCloudPlatform/google-cloud-python +.. _report issues: + https://github.com/GoogleCloudPlatform/google-cloud-python/issues + + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ diff --git a/samples/snippets/README.rst.in b/samples/snippets/README.rst.in new file mode 100644 index 00000000..043c9b77 --- /dev/null +++ b/samples/snippets/README.rst.in @@ -0,0 +1,25 @@ +# This file is used to generate README.rst + +product: + name: Dialogflow CX API + short_name: Dialogflow CX API + url: https://cloud.google.com/dialogflow/cx/docs/ + description: > + The `Dialogflow CX API`_ enables you to create conversational experiences across devices and platforms. + +setup: +- auth +- install_deps + +samples: +- name: Detect Intent Text + file: detect_intent_texts.py + show_help: True +- name: Detect Intent Audio + file: detect_intent_audio.py + show_help: True +- name: Detect Intent Stream + file: detect_intent_stream.py + show_help: True + +cloud_client_library: true diff --git a/samples/snippets/detect_intent_audio.py b/samples/snippets/detect_intent_audio.py new file mode 100644 index 00000000..2bb1fa1e --- /dev/null +++ b/samples/snippets/detect_intent_audio.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DialogFlow API Detect Intent Python sample with audio file. + +Examples: + python detect_intent_audio.py -h + python detect_intent_audio.py --agent AGENT \ + --session-id SESSION_ID --audio-file-path resources/hello.wav +""" + +import argparse +import uuid + +from google.cloud.dialogflowcx_v3beta1.services.sessions import SessionsClient +from google.cloud.dialogflowcx_v3beta1.types import audio_config +from google.cloud.dialogflowcx_v3beta1.types import session + + +# [START dialogflow_detect_intent_audio] +def run_sample(): + # TODO(developer): Replace these values when running the function + project_id = "YOUR-PROJECT-ID" + location_id = "YOUR-LOCATION-ID" + # For more info on agents see https://cloud.google.com/dialogflow/cx/docs/concept/agent + agent_id = "YOUR-AGENT-ID" + agent = f"projects/{project_id}/locations/{location_id}/agents/{agent_id}" + # For more information on sessions see https://cloud.google.com/dialogflow/cx/docs/concept/session + session_id = str(uuid.uuid4()) + audio_file_path = "YOUR-AUDIO-FILE-PATH" + # For more supported languages see https://cloud.google.com/dialogflow/es/docs/reference/language + language_code = "en-us" + + detect_intent_audio(agent, session_id, audio_file_path, language_code) + + +def detect_intent_audio(agent, session_id, audio_file_path, language_code): + """Returns the result of detect intent with an audio file as input. + + Using the same `session_id` between requests allows continuation + of the conversation.""" + session_client = SessionsClient() + session_path = f"{agent}/sessions/{session_id}" + print(f"Session path: {session_path}\n") + + input_audio_config = audio_config.InputAudioConfig( + audio_encoding=audio_config.AudioEncoding.AUDIO_ENCODING_LINEAR_16, + sample_rate_hertz=24000, + ) + + with open(audio_file_path, "rb") as audio_file: + input_audio = audio_file.read() + + audio_input = session.AudioInput(config=input_audio_config, audio=input_audio) + query_input = session.QueryInput(audio=audio_input, language_code=language_code) + request = session.DetectIntentRequest(session=session_path, query_input=query_input) + response = session_client.detect_intent(request=request) + + print("=" * 20) + print(f"Query text: {response.query_result.transcript}") + response_messages = [ + " ".join(msg.text.text) for msg in response.query_result.response_messages + ] + print(f"Response text: {' '.join(response_messages)}\n") + + +# [END dialogflow_detect_intent_audio] + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--agent", help="Agent resource name. Required.", required=True + ) + parser.add_argument( + "--session-id", + help="Identifier of the DetectIntent session. " "Defaults to a random UUID.", + default=str(uuid.uuid4()), + ) + parser.add_argument( + "--language-code", + help='Language code of the query. Defaults to "en-US".', + default="en-US", + ) + parser.add_argument( + "--audio-file-path", help="Path to the audio file.", required=True + ) + + args = parser.parse_args() + + detect_intent_audio( + args.agent, args.session_id, args.audio_file_path, args.language_code + ) diff --git a/samples/snippets/detect_intent_audio_test.py b/samples/snippets/detect_intent_audio_test.py new file mode 100644 index 00000000..720bf68c --- /dev/null +++ b/samples/snippets/detect_intent_audio_test.py @@ -0,0 +1,37 @@ +# Copyright 2020, Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for detect_intent_texts.""" + +from __future__ import absolute_import + +import os +import uuid + + +from detect_intent_audio import detect_intent_audio + +DIRNAME = os.path.realpath(os.path.dirname(__file__)) +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +AGENT_ID = os.getenv("AGENT_ID") +AGENT = f"projects/{PROJECT_ID}/locations/global/agents/{AGENT_ID}" +SESSION_ID = uuid.uuid4() +AUDIO_PATH = os.getenv("AUDIO_PATH") +AUDIO = f"{DIRNAME}/{AUDIO_PATH}" + + +def test_detect_intent_texts(capsys): + detect_intent_audio(AGENT, SESSION_ID, AUDIO, "en-US") + out, _ = capsys.readouterr() + + assert "Response text: Hi! I'm the virtual flights agent." in out diff --git a/samples/snippets/detect_intent_stream.py b/samples/snippets/detect_intent_stream.py new file mode 100644 index 00000000..ecf1ea53 --- /dev/null +++ b/samples/snippets/detect_intent_stream.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DialogFlow API Detect Intent Python sample with audio files processed as an audio stream. + +Examples: + python detect_intent_stream.py -h + python detect_intent_stream.py --agent AGENT \ + --session-id SESSION_ID --audio-file-path resources/hello.wav +""" + +import argparse +import uuid + +from google.cloud.dialogflowcx_v3beta1.services.sessions import SessionsClient +from google.cloud.dialogflowcx_v3beta1.types import audio_config +from google.cloud.dialogflowcx_v3beta1.types import session + + +# [START dialogflow_detect_intent_stream] +def run_sample(): + # TODO(developer): Replace these values when running the function + project_id = "YOUR-PROJECT-ID" + location_id = "YOUR-LOCATION-ID" + # For more info on agents see https://cloud.google.com/dialogflow/cx/docs/concept/agent + agent_id = "YOUR-AGENT-ID" + agent = f"projects/{project_id}/locations/{location_id}/agents/{agent_id}" + # For more information on sessions see https://cloud.google.com/dialogflow/cx/docs/concept/session + session_id = uuid.uuid4() + audio_file_path = "YOUR-AUDIO-FILE-PATH" + # For more supported languages see https://cloud.google.com/dialogflow/es/docs/reference/language + language_code = "en-us" + + detect_intent_stream(agent, session_id, audio_file_path, language_code) + + +def detect_intent_stream(agent, session_id, audio_file_path, language_code): + """Returns the result of detect intent with streaming audio as input. + + Using the same `session_id` between requests allows continuation + of the conversation.""" + session_client = SessionsClient() + session_path = f"{agent}/sessions/{session_id}" + print(f"Session path: {session_path}\n") + + input_audio_config = audio_config.InputAudioConfig( + audio_encoding=audio_config.AudioEncoding.AUDIO_ENCODING_LINEAR_16, + sample_rate_hertz=24000, + ) + + def request_generator(): + audio_input = session.AudioInput(config=input_audio_config) + query_input = session.QueryInput(audio=audio_input, language_code=language_code) + + # The first request contains the configuration. + yield session.StreamingDetectIntentRequest( + session=session_path, query_input=query_input + ) + + # Here we are reading small chunks of audio data from a local + # audio file. In practice these chunks should come from + # an audio input device. + with open(audio_file_path, "rb") as audio_file: + while True: + chunk = audio_file.read(4096) + if not chunk: + break + # The later requests contains audio data. + audio_input = session.AudioInput(audio=chunk) + query_input = session.QueryInput(audio=audio_input) + yield session.StreamingDetectIntentRequest(query_input=query_input) + + responses = session_client.streaming_detect_intent(requests=request_generator()) + + print("=" * 20) + for response in responses: + print(f'Intermediate transcript: "{response.recognition_result.transcript}".') + + # Note: The result from the last response is the final transcript along + # with the detected content. + response = response.detect_intent_response + print(f"Query text: {response.query_result.transcript}") + response_messages = [ + " ".join(msg.text.text) for msg in response.query_result.response_messages + ] + print(f"Response text: {' '.join(response_messages)}\n") + + +# [END dialogflow_detect_intent_stream] + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--agent", help="Agent resource name. Required.", required=True + ) + parser.add_argument( + "--session-id", + help="Identifier of the DetectIntent session. " "Defaults to a random UUID.", + default=str(uuid.uuid4()), + ) + parser.add_argument( + "--language-code", + help='Language code of the query. Defaults to "en-US".', + default="en-US", + ) + parser.add_argument( + "--audio-file-path", help="Path to the audio file.", required=True + ) + + args = parser.parse_args() + + detect_intent_stream( + args.agent, args.session_id, args.audio_file_path, args.language_code + ) diff --git a/samples/snippets/detect_intent_stream_test.py b/samples/snippets/detect_intent_stream_test.py new file mode 100644 index 00000000..7501cfc1 --- /dev/null +++ b/samples/snippets/detect_intent_stream_test.py @@ -0,0 +1,38 @@ +# Copyright 2020, Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for detect_intent_texts.""" + +from __future__ import absolute_import + +import os +import uuid + + +from detect_intent_stream import detect_intent_stream + +DIRNAME = os.path.realpath(os.path.dirname(__file__)) +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +AGENT_ID = os.getenv("AGENT_ID") +AGENT = f"projects/{PROJECT_ID}/locations/global/agents/{AGENT_ID}" +SESSION_ID = uuid.uuid4() +AUDIO_PATH = os.getenv("AUDIO_PATH") +AUDIO = f"{DIRNAME}/{AUDIO_PATH}" + + +def test_detect_intent_texts(capsys): + detect_intent_stream(AGENT, SESSION_ID, AUDIO, "en-US") + out, _ = capsys.readouterr() + + assert "Intermediate transcript:" in out + assert "Response text: Hi! I'm the virtual flights agent." in out diff --git a/samples/snippets/detect_intent_texts.py b/samples/snippets/detect_intent_texts.py new file mode 100644 index 00000000..b55f4bf2 --- /dev/null +++ b/samples/snippets/detect_intent_texts.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DialogFlow API Detect Intent Python sample with text inputs. + +Examples: + python detect_intent_texts.py -h + python detect_intent_texts.py --agent AGENT \ + --session-id SESSION_ID \ + "hello" "book a meeting room" "Mountain View" + python detect_intent_texts.py --agent AGENT \ + --session-id SESSION_ID \ + "tomorrow" "10 AM" "2 hours" "10 people" "A" "yes" +""" + +import argparse +import uuid + +from google.cloud.dialogflowcx_v3beta1.services.sessions import SessionsClient +from google.cloud.dialogflowcx_v3beta1.types import session + + +# [START dialogflow_detect_intent_text] +def run_sample(): + # TODO(developer): Replace these values when running the function + project_id = "YOUR-PROJECT-ID" + location_id = "YOUR-LOCATION-ID" + # For more info on agents see https://cloud.google.com/dialogflow/cx/docs/concept/agent + agent_id = "YOUR-AGENT-ID" + agent = f"projects/{project_id}/locations/{location_id}/agents/{agent_id}" + # For more information on sessions see https://cloud.google.com/dialogflow/cx/docs/concept/session + session_id = uuid.uuid4() + texts = ["Hello"] + # For more supported languages see https://cloud.google.com/dialogflow/es/docs/reference/language + language_code = "en-us" + + detect_intent_texts(agent, session_id, texts, language_code) + + +def detect_intent_texts(agent, session_id, texts, language_code): + """Returns the result of detect intent with texts as inputs. + + Using the same `session_id` between requests allows continuation + of the conversation.""" + session_client = SessionsClient() + session_path = f"{agent}/sessions/{session_id}" + print(f"Session path: {session_path}\n") + + for text in texts: + text_input = session.TextInput(text=text) + query_input = session.QueryInput(text=text_input, language_code=language_code) + request = session.DetectIntentRequest( + session=session_path, query_input=query_input + ) + response = session_client.detect_intent(request=request) + + print("=" * 20) + print(f"Query text: {response.query_result.text}") + response_messages = [ + " ".join(msg.text.text) for msg in response.query_result.response_messages + ] + print(f"Response text: {' '.join(response_messages)}\n") + + +# [END dialogflow_detect_intent_text] + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--agent", help="Agent resource name. Required.", required=True + ) + parser.add_argument( + "--session-id", + help="Identifier of the DetectIntent session. " "Defaults to a random UUID.", + default=str(uuid.uuid4()), + ) + parser.add_argument( + "--language-code", + help='Language code of the query. Defaults to "en-US".', + default="en-US", + ) + parser.add_argument("texts", nargs="+", type=str, help="Text inputs.") + + args = parser.parse_args() + + detect_intent_texts(args.agent, args.session_id, args.texts, args.language_code) diff --git a/samples/snippets/detect_intent_texts_test.py b/samples/snippets/detect_intent_texts_test.py new file mode 100644 index 00000000..0cc263ae --- /dev/null +++ b/samples/snippets/detect_intent_texts_test.py @@ -0,0 +1,35 @@ +# Copyright 2020, Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for detect_intent_texts.""" + +from __future__ import absolute_import + +import os +import uuid + + +from detect_intent_texts import detect_intent_texts + +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +AGENT_ID = os.getenv("AGENT_ID") +AGENT = f"projects/{PROJECT_ID}/locations/global/agents/{AGENT_ID}" +SESSION_ID = uuid.uuid4() +TEXTS = ["hello", "book a flight"] + + +def test_detect_intent_texts(capsys): + detect_intent_texts(AGENT, SESSION_ID, TEXTS, "en-US") + out, _ = capsys.readouterr() + + assert "Response text: I can help you find a ticket" in out diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py new file mode 100644 index 00000000..ba55d7ce --- /dev/null +++ b/samples/snippets/noxfile.py @@ -0,0 +1,224 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import os +from pathlib import Path +import sys + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +# Copy `noxfile_config.py` to your directory and modify it instead. + + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + 'ignored_versions': ["2.7"], + + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + 'envs': {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append('.') + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars(): + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG['gcloud_project_env'] + # This should error out if not set. + ret['GOOGLE_CLOUD_PROJECT'] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG['envs']) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to tested samples. +ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG['ignored_versions'] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = bool(os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False)) +# +# Style Checks +# + + +def _determine_local_import_names(start_dir): + """Determines all import names that should be considered "local". + + This is used when running the linter to insure that import order is + properly checked. + """ + file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)] + return [ + basename + for basename, extension in file_ext_pairs + if extension == ".py" + or os.path.isdir(os.path.join(start_dir, basename)) + and basename not in ("__pycache__") + ] + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--import-order-style=google", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session): + session.install("flake8", "flake8-import-order") + + local_names = _determine_local_import_names(".") + args = FLAKE8_COMMON_ARGS + [ + "--application-import-names", + ",".join(local_names), + "." + ] + session.run("flake8", *args) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests(session, post_install=None): + """Runs py.test for a particular project.""" + if os.path.exists("requirements.txt"): + session.install("-r", "requirements.txt") + + if os.path.exists("requirements-test.txt"): + session.install("-r", "requirements-test.txt") + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars() + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session): + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip("SKIPPED: {} tests are disabled for this sample.".format( + session.python + )) + + +# +# Readmegen +# + + +def _get_repo_root(): + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session, path): + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/samples/snippets/noxfile_config.py b/samples/snippets/noxfile_config.py new file mode 100644 index 00000000..edded193 --- /dev/null +++ b/samples/snippets/noxfile_config.py @@ -0,0 +1,40 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be inported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7"], + + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": { + "AGENT_ID": "53516802-3e2a-4016-80b6-a3df0d240240", + "AUDIO_PATH": "resources/hello.wav", + }, +} \ No newline at end of file diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt new file mode 100644 index 00000000..3413ad1c --- /dev/null +++ b/samples/snippets/requirements-test.txt @@ -0,0 +1 @@ +pytest==6.0.1 \ No newline at end of file diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt new file mode 100644 index 00000000..9123af57 --- /dev/null +++ b/samples/snippets/requirements.txt @@ -0,0 +1 @@ +google-cloud-dialogflow-cx==0.1.0 diff --git a/samples/snippets/resources/hello.wav b/samples/snippets/resources/hello.wav new file mode 100644 index 0000000000000000000000000000000000000000..0aadf16c7116b218b7fcb6828286846661dcc301 GIT binary patch literal 29564 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
00p9oSeNHc0 z56N@a_r(RW@J!KdwJ5w=}qxH$I)4&D+SI!0XAL10Io++&64* zEw8O#9Rr9MeNI(ppqvHP+2&6swq?IzC>D7|y^aWhWpCo!LlPcOM zj>_JOrg47IAKlwc$-3!fHS|i`VQLX)v}A>{zjBUjw0MLdO*l&gWfpaC*z%anq@?uP z)oY}_jv6g(N17^{{ZM_k<}54hS2^3d*WJr^ow`juBpZ=A#0$?VZ*%`_925Vh&I;)e z789aT-IZXx`z#9xL2pgg!`iTmxW&TOqBerwEZVR1{BB=h*=BiT`{2qa=QB&OY|H_S zFq9eQ?~7vQB)5(TQ2XJ27R8Mb)Du1s{K%fn=@Zk@TrlBTJNRl1`Lfl;6_~3QLLgCG|<)l-?#$9_+>&*}9d~$(#D) zMBdtBL-}>{JLh=sdU6e=p=Oe!d@0^dEl*mPdmU2#m;TW- z4GZwO+;02^f))G%b~)3{7qCw;jW%pFopWeO0bp3|*(2~+xYR$__l3ZSCBz8dL4OAP zll?|8UOY)c2pe#rE47rfl?P_E9TY8(Xo3@z0+FH9?lKFI7 z^rmMb98OQ955m{1HhiDRD|#&IDmcY;<7#@8cc4AooM5l_Fnk z5nL3=MIlnYIw!PxOr6B(DR^30iXe7`>K#4Sa6NZp*23(41$!%2S>L#A7=Jtf@7tuG7~w-oAlsqFfosb52! za8y|9*|HsVye-i_t8M6w$Q5X>_nA? zoS}1=-RP~f2FxhG(RN3q-bi05{|tH*u{Ex3((IIlNe!boipoF@ zle#D;Pg$5=e%+XE7kKV^S0K+HK=g3$cWrY5_n+Ps^gRAL1sS3bKNtQk=(K_>Cb;Qr z6DGyeaUJ^<@3gR{I6=tgNx^AyiwkXwwcN9ewtsV_`-b_u(@mIuOdPHCn|*%~6FoDD zE2M}S%KFL+5{?(%;@9F#!G_b({sGi0s$pOnJq*2HJe?8HN2pHTgRYsjwdNSpG*dUL z%X!f^jNXr84#vb#_y0$G=iwza)i&^%w8`{7vwdbL3(}U}5kU}GSjq~D;L?!}g0eJ0 zWML6px*#G|!2+_&Dn&$O0a0lpy(4vZYBI?rlS!TL&bu$i_dVx3-}(Lkle0-SnYp=n za_8peDZhtvab5Y9@_uy?9fjwBK0wh_AsdsQ2)h^~848CZdlK|$FRf8v9$=*R#MJ#(b<=%pOC68AH4wB2+G;VJE9r^(| zidYamL4U+S(m=IMtFz8<-^wV?{L}rp9Wx!#EIrslK6h5BiRJp1g}& z8!L;xNOmQMQ#%+ZrxN>1OHEf%6@6TaFhqJJz`Qa z$&8dM)^JrZf=q@j;n#?Z(FIHqca85Q)`;Dt6=J6Nl&{D41U}3s`C_?7{S{iJX@^aP zew5B~vtzAePvY+>KFw{@6vrWF1NTDrsI(#OET`Vq)x6of)ppnYZq`$uzi!lbD_zi+ z$sdwgfoU~I{B&Su*h9=A&Qf=%LV7SgiXKlrp{ZC|A}u*lv%@^l5qGOx-#Ff~eq**6 z+ZZP3o%-SWtsnxZO1D}!3;zNe1L@`Oxsh}UnMQKtHL7zgz}#im0`1azFrHh;@@!c= zFL8oTllmo(LXEMN8WYZA3((E*3H74naJj#T1nEa5PhA9EQ_HIT(srRHZcI!Tr^Ag6P3>Q# z9RU`ouiSsR=edlor_S4s)$S4L_N=+tKYJ#6f6S`ys@4?q4Z=S?t9*L9rab(Z*b;Nb zcF<|jZQvJ}9oj)Wq586K3M16lvGL}Xjy0}7oTKdfEQN-*^*eMk4H@Pewl5rr!)M)V zc%aFLA0(?pPeNcmqtmGi(Pz;+)Ja;7tz@P%YNkhQR_sZvm~F|8;7i0oN;h>md>@^R zy^M`SX97*?d(emKbIFy8N1iOzh%vFVbXm+5D--2x0c9p)DcXC2NQkli5r?JOxB zb${#}>&$S&>3CLt&LB^-Y@4Gm-XgIpa<+Q^vo8Ll!E2GGWO+1=Y)Re<&yVCqek0~k z{bI)wcI6ecv*Dq2m&549?Wcjwq5!n1L6%C(df)}|k@b}2igBRs50p}WETjDE@iDP9 zx(78N8X#G+C>o`X#HyM7>|J&aYhcUS>Ub`{RO+Vc2sc5`q4m+9k(EdQ*@fJMe^48e z52T6W60xULqD)K{B(?GniSx1LWN~D57$YW=0cvNgBP+0DL0QLRmtv3T7r?kRL?2`u z3#G|TPz3o2cEL9QjUz94MYRTPVHj-Rlm0C0uWYwxRZd3Esf=;ydmT~7Bj+afzVx&V zQ^t3$MYa}r+vH^Wh46uzp*5xcZh@}h#o^B6YSI)v8hIMA5!cBrOqf5Pl;AqLHHI7J zm^ENKV((+0>P#TgXv5sGEaP(_&;Z@)%i&?nI9wt>6QyV&#%_6!-(bmHZCG z;5SRQ6TMs}Jrg`@mI2R>ozXt@Mf%6sZn`S=Zfsesl4hxSRA)-V+>NgncLIOQ-Dnfc zk6eTfK@9BD)-m;UoOdtJd@uX8ocW%#Y;z`@*4fp`@v(!l=R4Xs6~_rM&iw`*E-hsq z5fdUyfX1>QJRmZd>`Cs4CW(CVWV9t!$TW=q%(qv9&?L(AbvRiMjDj}-)YfufJ3bcAjd`duq$;{5 z>ZR;dMf5Hy65WYo#0s(@&mKbab>us6#|+Cr!&rD7--f;u+8Y%76GA=1Q;5rCGx846 zDSSC}FVu!$qodig{0^lfatGgLC^5COq+1SJI$0lDd)h>s6Zm%=ckFf)*?u**GNAY? zNL|%&$tk!Jx8fw*lo>&{jw-~Z$o7bxV2Br@Gbm$B!+gtTa$oVgM4M8gss}BC^O0U? zDO!rTG~+avu`XC9It=zfX8^AQPWDoMmYNEUxkSuPwFT|!Q=%z3l~j=zB5#IsL%V|y zf`L#~B(a=p|X#NfQi zx?}sIEb%!ZMtT#)1VJ1kcSjMLrH?Yh<7W~_g=~3BvQYgE+z742=4;-v zjChfH$k%Wq*aT&&mdQmzAFekun;J|$j!Xx-=iQ*)?g>euzkv7ZlJJ($(crYug@}j# zHGW^*r?iA_z;n^*8br(K!=@RIw(eG$N*3{Hu*<5=+%alUWNTBhT*nX?G{ICr{6gm*xdrQH?5aUosB(oUusq&S5@x=-=`^D4Fko#qE3Ncav1qN zF%~?7$B=WP9OaAMVcLR`)A#%W@rL}e$_rm= zUPO)wUjXHQ8txT-EAo1{XCyb$D$<73QhgYKtt(8CXq6UyNxQ_*&ve<`%2H$K3DT4HVllY)M~fF2o)R{A<#dOJ0d@Xx&{jaX8*35 z>(%F~@tRRJI{%A-=Amwc5%^6_6JC@9s_yVEY=d^MvBqL@j!vJF75BXAJ71@7U3?yhC%=X9m-@y8dwF+EkXYhW_{?sF6g)Cq+jSCxQ3d+ThbbdZ2}WfIn7qI3NTD zM=FRvsjsZ+(oBongM>m zMck-8kMF@%c$y{~D?zRTBkl;fyRe&U$3CSE)VG8qk`ekn&=JT%SJS@cLQRqXgTOn% zRpDdAcG?(kDOi+Dbsxl|$Ci0%5B70;+J@wSsEKn zucod?Cq`#QtD{S)+4PoJCQHZdyi1g%OUg3UNJxilLl0wR8Y5na*T>5>K`e&K$UAUU zJt-NNI)Y!=G?t}p)DOhv@b|&yfweWiSC6dTT)na8Wxq4X28Ts@lV$X;><)g7Osj4q zn>B0njZMY2InLSX1F}q>JKkl!#dR+EJasmDU-w?f_GXPspXXX_A7|NUe5~DzHGy7L zE+jl`XKDmdKe8Ca@_ZRw9qbew7hD)D4$X+r#B};sW?*87xKX*GUIFy*knW11*un;F<=SwgZ9EOYQR|RBTcpDf+j=L8cQH&;2da!YK9_lXT|Jhp5y!;qHY9Aym|wYnA9DyW0P@V(jBsRH7c(9q!8 zz_@@fFd|SCcpNwr>=1qnv<4qj86Pc-mjMY8egn(V9@TF!eQlm(t+bx8oweEQxZMm| z!>8scAOd8F_5juyxvHL?bjizv$%*Fi9QI($7287>&_(nT`WZbsHh}p8SV`LQJ;faP znzB}9hql4jkfZ3wST$zQ{DtLVJ<&!83(Z%5mRux16uKvNv!2-J(H?{^oFDwzKeJ{F z@M$Q1R{3mUb*Gvu{{F#(;eBM1Zoz#f98}st*U>k$ZsTxEfuqdr%p3xwFr&`Ux@f(Y z^*Ytd2Yz0wJpQah>FwPvN13&&>Ao(8t$;c!PZ9-eBHD*M5)Oub3BDJ2GjPqnFOU_a zLcJoZ$-$J1v4S^%q-aPML(|a-_&yzERGTWzT`ezL%dJaIRUsZSIfQShV67{(Ro6r8vc$rR27PE#)Ff}a9bxB;}TZq3&4VA-5i~0?yFD${E zk!8p}q&~6*J`4S!KBelGye!`rpYgl7rK~rG0nMEy@=+)~*fUV<&-JJKzw_4*ycawf zDvE3*A5ebgDYrnp2J9`D&;{CMhIQs6w%*S2G%DlcY>fx@4g|e|(Ocqap7Uwe^^DxK z0vBboS?(Er)GF9fc)042^n2n6Yl#J;Ps#a&k+=|91vJGr@RZ+6&Z5d<$Jnb}sn9^m zO|}Pdrp>TCya3omju|=|9~*Jg0aHKIYSUKZ8AGA|incMHi+zL?LWflQlx+C}(JZL= z%*0+!58_TY$LGY$fqnlQt~gP_{~-9J*8q`iJfKB}0GWwXYoRRY3w64BimGq&O@)+p zh!Q^~F)}_4*jWmq-x62DWN1Z@3wQ#z{D1oKK>MH}B!o8;J}NiXCVnsRRQv#F{kLK} zwIhrfmcjNGE;Rk^%p=)y4&tr!&hjqscJz$SS(o*CW=&d#JHye;`lgB1d2tANq}nbm z;b+8$Ga9-acuTJ&CK5LzPa<+;2eFTwNyX{zY%Kl`=+B+XX_X5uK<8+3v}L;4`u+yD zak0^7a+oqrEsZ6HSM zd>sK2d6AM%$tB7wfY8`Y^&#LL^##1NhRJu8sq!32CGO#`an0hrm|66(s6Z@;_(SJ{ zWx&g|i+`Sfw|`7vMQ}wZFLI6OLv4>ejbG$@$h%Y*;WEv5oyMfMws#b`^D=s5jm=r- z8SNeG>*o8~x6`}Cb0_=P%rDXn?zxUb*5f8ok82yFThtHb7laX9lDSLYj2!|C7+ zC5^t#?BISC;%KNZ)}QR7akkTcvVTpBKk3?h<1uQ@lSWtVU}_%0HotW=Lg_G6!G zPwC4{_bmDLSDnk( zRz3w5typ3^w>f^9ZN@%hK4(rdKQX1OHQs<5pD5rNAqlLD?&N$Gt?mX-2Ks>6m``&< zx=!wapK3~PH~sGT^uN;iOYeE-sW#7nj}VZJLBK8zcH1uTl9IVK4p)7OkN;*5eDK) zL;*3xIC+lDpj`C1n3Sl%AUl(G@*-g*v%%HB0!H5>=ucvGfIWP1B1@Pd^-|WT zzJiV*3Kqtj=$jeanjz~2o7XYcndds|>g1l|p5^wqXSu$2e&ASP?_xV|8E*c}*veo8 z-n}nj1e^^`R8h(_xj-5!4ig^oJNUZ%V17J5n{Ot(D0sy(@mq1r)TM0x2$iF2AN&00FTW@LKNx->i0U5MGLOK`)^1U_7=^Luv-% zJMkO1MVqVbrtPh5uWhVVa034ZpN+S}uWPn|dv^uvhdn`yQ3v`PG6QLVgyG|GG4Kp- z4trr7jzW*1bI@UEFSH#hffhqEpjV;pkPp&9PXK=ukPFl~fTp=0*hD;l)w~U`CoRb< z%5uQ^(H^igjXW|NRwm3>00@_0tv9;J-d_im~HWM3( zb;No=%i2I}CAJsy#n;3s;``z!pygZvJ!hKKLz)Fjx&^fIedQJMd0DRvRW>Ql0P}Y? zAQa<(mAgX~Q01u?0oEQ0^#>HJOOPAx50?VoApv`ke#kq>M&u}R6Nw-y)Qo1L4bjGE zGqefNcluBlilG$p6uE>P1A5QZ$P{D%l7~2u2z(JP2W3oxyTd-1fzCtUK#PD^Q6q>^ z|E}HyXoelsdi8C<0Dd1ZLF)sC@#*BM1xSV?m3ILF z_H*SJU`jp%OiwFdXm&}y3h1&60ikqP@*p4!Uj?+rNRkJ1b`v1$<^Y0rQ$Xu&4LFb; z09mmkU?8^v|Fr;w>&B`S(Y+41Z<)Z3?)(=cT@MJge-r*|iTx>dzXEvFsbz|9pUMaD zocNz2WTtrh5FlsP<{wDy`OhhpMWFUGscZnXC8bJxo)O@`Gd9#_C`gr;BIVZ*ak6U?ct+R`P!lNP{#wP#N42gV{Gb@^X=p66Rg zU1O@WzvcMv!ogXdXKP61AE>QUYTt87*4737cb#fw_@5=!O0Tw#sby+PN#gnRzip+C zrM{Pv%=6FvU1sgE=k@!0|9|fJ@9or`cs~8-cCFl>ANjx5&%gituBX0PJO91)e9!as l-*@Q$f1dxV4*wkcM*{yy;2#P6BY}S;@Q(!kk-$F^_#Z0t#B=}v literal 0 HcmV?d00001