diff --git a/README.md b/README.md
index 3b1c56c..17ed258 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,34 @@
# gassist_text
+
A Python library for interacting with Google Assistant API via text
+
+## Credits
+
+Uses . See instructions there how to get `credentials.json`.
+
+Code is essentially a copy of wrapped in a package.
+
+## Example
+
+```python
+import json
+import google.oauth2.credentials
+with open('/path/to/credentials.json', 'r') as f:
+ credentials = google.oauth2.credentials.Credentials(token=None, **json.load(f))
+
+from gassist_text import TextAssistant
+with TextAssistant(credentials) as assistant:
+ print(assistant.assist('tell me a joke')[0])
+ print(assistant.assist('another one')[0])
+```
+
+## How to run
+
+```sh
+$ python3 -m venv venv
+$ source venv/bin/activate
+$ pip3 install -r requirements.txt
+
+# Run command line interactive tool
+$ python gassist_text/textinput.py
+```
diff --git a/gassist_text/__init__.py b/gassist_text/__init__.py
new file mode 100644
index 0000000..23a8f3a
--- /dev/null
+++ b/gassist_text/__init__.py
@@ -0,0 +1,4 @@
+"""
+.. include:: ../README.md
+"""
+from .textinput import TextAssistant
diff --git a/gassist_text/assistant_helpers.py b/gassist_text/assistant_helpers.py
new file mode 100644
index 0000000..4e41a6b
--- /dev/null
+++ b/gassist_text/assistant_helpers.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2017 Google Inc.
+#
+# 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.
+
+"""Helper functions for the Google Assistant API."""
+
+import logging
+
+from google.assistant.embedded.v1alpha2 import embedded_assistant_pb2
+
+
+def log_assist_request_without_audio(assist_request):
+ """Log AssistRequest fields without audio data."""
+ if logging.getLogger().isEnabledFor(logging.DEBUG):
+ resp_copy = embedded_assistant_pb2.AssistRequest()
+ resp_copy.CopyFrom(assist_request)
+ if len(resp_copy.audio_in) > 0:
+ size = len(resp_copy.audio_in)
+ resp_copy.ClearField('audio_in')
+ logging.debug('AssistRequest: audio_in (%d bytes)',
+ size)
+ return
+ logging.debug('AssistRequest: %s', resp_copy)
+
+
+def log_assist_response_without_audio(assist_response):
+ """Log AssistResponse fields without audio data."""
+ if logging.getLogger().isEnabledFor(logging.DEBUG):
+ resp_copy = embedded_assistant_pb2.AssistResponse()
+ resp_copy.CopyFrom(assist_response)
+ has_audio_data = (resp_copy.HasField('audio_out') and
+ len(resp_copy.audio_out.audio_data) > 0)
+ if has_audio_data:
+ size = len(resp_copy.audio_out.audio_data)
+ resp_copy.audio_out.ClearField('audio_data')
+ if resp_copy.audio_out.ListFields():
+ logging.debug('AssistResponse: %s audio_data (%d bytes)',
+ resp_copy,
+ size)
+ else:
+ logging.debug('AssistResponse: audio_data (%d bytes)',
+ size)
+ return
+ logging.debug('AssistResponse: %s', resp_copy)
diff --git a/gassist_text/browser_helpers.py b/gassist_text/browser_helpers.py
new file mode 100644
index 0000000..cc66b64
--- /dev/null
+++ b/gassist_text/browser_helpers.py
@@ -0,0 +1,33 @@
+# Copyright (C) 2018 Google Inc.
+#
+# 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.
+
+import os.path
+import tempfile
+import webbrowser
+
+ASSISTANT_HTML_FILE = 'google-assistant-sdk-screen-out.html'
+
+
+class SystemBrowser(object):
+ def __init__(self):
+ self.tempdir = tempfile.mkdtemp()
+ self.filename = os.path.join(self.tempdir, ASSISTANT_HTML_FILE)
+
+ def display(self, html):
+ with open(self.filename, 'wb') as f:
+ f.write(html)
+ webbrowser.open(self.filename, new=0)
+
+
+system_browser = SystemBrowser()
diff --git a/gassist_text/textinput.py b/gassist_text/textinput.py
new file mode 100644
index 0000000..30e169b
--- /dev/null
+++ b/gassist_text/textinput.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2017 Google Inc.
+#
+# 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.
+
+"""Implements a text client for the Google Assistant Service."""
+# Copied from:
+# https://github.com/googlesamples/assistant-sdk-python/blob/master/google-assistant-sdk/googlesamples/assistant/grpc/textinput.py # noqa
+# Changes:
+# - Renamed class
+# - Simplified constructor:
+# - Added default values
+# - Moved creation of the authorized gRPC channel in the constructor
+
+import os
+import logging
+import json
+
+import click
+import google.auth.transport.grpc
+import google.auth.transport.requests
+import google.oauth2.credentials
+
+from google.assistant.embedded.v1alpha2 import (
+ embedded_assistant_pb2,
+ embedded_assistant_pb2_grpc
+)
+
+try:
+ from . import (
+ assistant_helpers,
+ browser_helpers,
+ )
+except (SystemError, ImportError):
+ import assistant_helpers
+ import browser_helpers
+
+
+ASSISTANT_API_ENDPOINT = 'embeddedassistant.googleapis.com'
+DEFAULT_GRPC_DEADLINE = 60 * 3 + 5
+PLAYING = embedded_assistant_pb2.ScreenOutConfig.PLAYING
+
+
+class TextAssistant(object):
+ """Assistant that supports text based conversations.
+
+ Args:
+ credentials: OAuth2 credentials.
+ language_code: language for the conversation.
+ device_model_id: identifier of the device model.
+ device_id: identifier of the registered device instance.
+ display: enable visual display of assistant response.
+ deadline_sec: gRPC deadline in seconds for Google Assistant API call.
+ api_endpoint: Address of Google Assistant API service.
+ """
+
+ def __init__(self, credentials, language_code='en-US', device_model_id='default', device_id='default',
+ display=False, deadline_sec=DEFAULT_GRPC_DEADLINE, api_endpoint=ASSISTANT_API_ENDPOINT):
+ self.language_code = language_code
+ self.device_model_id = device_model_id
+ self.device_id = device_id
+ self.conversation_state = None
+ # Force reset of first conversation.
+ self.is_new_conversation = True
+ self.display = display
+ # Create an authorized gRPC channel.
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, google.auth.transport.requests.Request(), api_endpoint)
+ self.assistant = embedded_assistant_pb2_grpc.EmbeddedAssistantStub(
+ channel
+ )
+ self.deadline = deadline_sec
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, etype, e, traceback):
+ if e:
+ return False
+
+ def assist(self, text_query):
+ """Send a text request to the Assistant and playback the response.
+ """
+ def iter_assist_requests():
+ config = embedded_assistant_pb2.AssistConfig(
+ audio_out_config=embedded_assistant_pb2.AudioOutConfig(
+ encoding='LINEAR16',
+ sample_rate_hertz=16000,
+ volume_percentage=0,
+ ),
+ dialog_state_in=embedded_assistant_pb2.DialogStateIn(
+ language_code=self.language_code,
+ conversation_state=self.conversation_state,
+ is_new_conversation=self.is_new_conversation,
+ ),
+ device_config=embedded_assistant_pb2.DeviceConfig(
+ device_id=self.device_id,
+ device_model_id=self.device_model_id,
+ ),
+ text_query=text_query,
+ )
+ # Continue current conversation with later requests.
+ self.is_new_conversation = False
+ if self.display:
+ config.screen_out_config.screen_mode = PLAYING
+ req = embedded_assistant_pb2.AssistRequest(config=config)
+ assistant_helpers.log_assist_request_without_audio(req)
+ yield req
+
+ text_response = None
+ html_response = None
+ for resp in self.assistant.Assist(iter_assist_requests(),
+ self.deadline):
+ assistant_helpers.log_assist_response_without_audio(resp)
+ if resp.screen_out.data:
+ html_response = resp.screen_out.data
+ if resp.dialog_state_out.conversation_state:
+ conversation_state = resp.dialog_state_out.conversation_state
+ self.conversation_state = conversation_state
+ if resp.dialog_state_out.supplemental_display_text:
+ text_response = resp.dialog_state_out.supplemental_display_text
+ return text_response, html_response
+
+
+@click.command()
+@click.option('--api-endpoint', default=ASSISTANT_API_ENDPOINT,
+ metavar='', show_default=True,
+ help='Address of Google Assistant API service.')
+@click.option('--credentials',
+ metavar='', show_default=True,
+ default=os.path.join(click.get_app_dir('google-oauthlib-tool'),
+ 'credentials.json'),
+ help='Path to read OAuth2 credentials.')
+@click.option('--device-model-id',
+ metavar='',
+ required=True,
+ default='default',
+ help=(('Unique device model identifier, '
+ 'if not specifed, it is read from --device-config')))
+@click.option('--device-id',
+ metavar='',
+ required=True,
+ default='default',
+ help=(('Unique registered device instance identifier, '
+ 'if not specified, it is read from --device-config, '
+ 'if no device_config found: a new device is registered '
+ 'using a unique id and a new device config is saved')))
+@click.option('--lang', show_default=True,
+ metavar='',
+ default='en-US',
+ help='Language code of the Assistant')
+@click.option('--display', is_flag=True, default=False,
+ help='Enable visual display of Assistant responses in HTML.')
+@click.option('--verbose', '-v', is_flag=True, default=False,
+ help='Verbose logging.')
+@click.option('--grpc-deadline', default=DEFAULT_GRPC_DEADLINE,
+ metavar='', show_default=True,
+ help='gRPC deadline in seconds')
+def main(api_endpoint, credentials,
+ device_model_id, device_id, lang, display, verbose,
+ grpc_deadline, *args, **kwargs):
+ # Setup logging.
+ logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO)
+
+ # Load OAuth 2.0 credentials.
+ try:
+ with open(credentials, 'r') as f:
+ credentials = google.oauth2.credentials.Credentials(token=None,
+ **json.load(f))
+ http_request = google.auth.transport.requests.Request()
+ credentials.refresh(http_request)
+ except Exception as e:
+ logging.error('Error loading credentials: %s', e)
+ logging.error('Run google-oauthlib-tool to initialize '
+ 'new OAuth 2.0 credentials.')
+ return
+
+ with TextAssistant(credentials, lang, device_model_id, device_id, display,
+ grpc_deadline, api_endpoint) as assistant:
+ while True:
+ query = click.prompt('')
+ click.echo(' %s' % query)
+ response_text, response_html = assistant.assist(text_query=query)
+ if display and response_html:
+ system_browser = browser_helpers.system_browser
+ system_browser.display(response_html)
+ if response_text:
+ click.echo('<@assistant> %s' % response_text)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..653fc16
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+click==8.1.3
+google-assistant-grpc==0.3.0
+google-auth==2.14.1
+protobuf==3.20.3
+requests==2.28.1