From e16db17db073da3e8e9d1158351ecc67f8a595ea Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 26 Jun 2022 10:24:53 +0200 Subject: [PATCH] Add remote calls --- .gitignore | 1 + docs/remote-test/remote.ipynb | 90 +++++++++++++++++++ setup.cfg | 3 +- src/great_ai/great_ai/__init__.py | 6 ++ src/great_ai/great_ai/remote/__init__.py | 4 + .../great_ai/remote/call_remote_great_ai.py | 26 ++++++ .../remote/call_remote_great_ai_async.py | 25 ++++++ src/great_ai/great_ai/remote/http_client.py | 60 +++++++++++++ .../great_ai/remote/remote_call_error.py | 2 + src/great_ai/utilities/__init__.py | 2 +- 10 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 docs/remote-test/remote.ipynb create mode 100644 src/great_ai/great_ai/remote/__init__.py create mode 100644 src/great_ai/great_ai/remote/call_remote_great_ai.py create mode 100644 src/great_ai/great_ai/remote/call_remote_great_ai_async.py create mode 100644 src/great_ai/great_ai/remote/http_client.py create mode 100644 src/great_ai/great_ai/remote/remote_call_error.py diff --git a/.gitignore b/.gitignore index 139f214..5603876 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ .pytest_cache .ipynb_checkpoints *.egg-info +build diff --git a/docs/remote-test/remote.ipynb b/docs/remote-test/remote.ipynb new file mode 100644 index 0000000..90d509c --- /dev/null +++ b/docs/remote-test/remote.ipynb @@ -0,0 +1,90 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Trace(trace_id='07719c28-e248-43f8-a652-608c49ad5a17', created='2022-06-26T07:55:30.587753', original_execution_time_ms=178.268, logged_values={'arg:text:value': 'I love chemical compounds and nuclear fission.', 'arg:text:length': 46, 'arg:target_confidence:value': 50}, models=[Model(key='small-domain-prediction', version=0)], exception=None, output={'labels': [{'label': 'Physics', 'confidence': 28.0, 'explanation': ['nuclear', 'chemical', 'fission', 'compounds', 'love']}, {'label': 'Chemistry', 'confidence': 22.0, 'explanation': ['chemical', 'compounds', 'nuclear', 'fission', 'love']}]}, feedback=None, tags=['predict_domain', 'online', 'development'])" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from great_ai import call_remote_great_ai_async\n", + "\n", + "\n", + "await call_remote_great_ai_async(\n", + " \"http://localhost:6060\", {\"text\": \"I love chemical compounds and nuclear fission.\"}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "<_UnixSelectorEventLoop running=True closed=False debug=False>\n" + ] + }, + { + "ename": "Exception", + "evalue": "Already running in an event loop, you have to call call_remote_great_ai_async.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/data/projects/great_ai/docs/remote-test/remote.ipynb Cell 1'\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mgreat_ai\u001b[39;00m \u001b[39mimport\u001b[39;00m call_remote_great_ai\n\u001b[0;32m----> 4\u001b[0m call_remote_great_ai(\u001b[39m'\u001b[39;49m\u001b[39mhttp://localhost:6060\u001b[39;49m\u001b[39m'\u001b[39;49m, {\n\u001b[1;32m 5\u001b[0m \u001b[39m'\u001b[39;49m\u001b[39mtext\u001b[39;49m\u001b[39m'\u001b[39;49m: \u001b[39m'\u001b[39;49m\u001b[39mI love chemical compounds and nuclear fission.\u001b[39;49m\u001b[39m'\u001b[39;49m\n\u001b[1;32m 6\u001b[0m })\n", + "File \u001b[0;32m/data/projects/great_ai/src/great_ai/great_ai/remote/call_remote_great_ai.py:17\u001b[0m, in \u001b[0;36mcall_remote_great_ai\u001b[0;34m(base_uri, data, retry_count)\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[1;32m 16\u001b[0m \u001b[39mprint\u001b[39m(asyncio\u001b[39m.\u001b[39mget_running_loop())\n\u001b[0;32m---> 17\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mException\u001b[39;00m(\u001b[39mf\u001b[39m\u001b[39m'\u001b[39m\u001b[39mAlready running in an event loop, you have to call \u001b[39m\u001b[39m{\u001b[39;00mcall_remote_great_ai_async\u001b[39m.\u001b[39m\u001b[39m__name__\u001b[39m\u001b[39m}\u001b[39;00m\u001b[39m.\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m 18\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mRuntimeError\u001b[39;00m:\n\u001b[1;32m 19\u001b[0m \u001b[39mpass\u001b[39;00m\n", + "\u001b[0;31mException\u001b[0m: Already running in an event loop, you have to call call_remote_great_ai_async." + ] + } + ], + "source": [ + "from great_ai import call_remote_great_ai\n", + "\n", + "\n", + "call_remote_great_ai(\n", + " \"http://localhost:6060\", {\"text\": \"I love chemical compounds and nuclear fission.\"}\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.10.4 ('.env': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "d88b0dd276e3f918f7798b7e97af2e3c2f843817b9e5b55a9df0a682ffd80f44" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/setup.cfg b/setup.cfg index d5a29d2..2a14c10 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = great-ai -version = 0.0.1 +version = 0.0.1-rc1 author = AndrĂ¡s Schmelczer author_email = andras@scoutinscience.com description = @@ -44,6 +44,7 @@ install_requires = uvicorn[standard] >= 0.18.0 watchdog >= 2.1.0 pymongo >= 4.0.0 + aiohttp >= 3.8.0 [options.package_data] * = *.json, *.yaml, *.yml, *.css diff --git a/src/great_ai/great_ai/__init__.py b/src/great_ai/great_ai/__init__.py index edf03ff..176966f 100644 --- a/src/great_ai/great_ai/__init__.py +++ b/src/great_ai/great_ai/__init__.py @@ -8,4 +8,10 @@ from .output_models import ( ) from .parameters import log_metric, parameter from .persistence import MongodbDriver, ParallelTinyDbDriver, TracingDatabaseDriver +from .remote import ( + HttpClient, + RemoteCallError, + call_remote_great_ai, + call_remote_great_ai_async, +) from .tracing import add_ground_truth, delete_ground_truth, query_ground_truth diff --git a/src/great_ai/great_ai/remote/__init__.py b/src/great_ai/great_ai/remote/__init__.py new file mode 100644 index 0000000..9399b96 --- /dev/null +++ b/src/great_ai/great_ai/remote/__init__.py @@ -0,0 +1,4 @@ +from .call_remote_great_ai import call_remote_great_ai +from .call_remote_great_ai_async import call_remote_great_ai_async +from .http_client import HttpClient +from .remote_call_error import RemoteCallError diff --git a/src/great_ai/great_ai/remote/call_remote_great_ai.py b/src/great_ai/great_ai/remote/call_remote_great_ai.py new file mode 100644 index 0000000..449c64b --- /dev/null +++ b/src/great_ai/great_ai/remote/call_remote_great_ai.py @@ -0,0 +1,26 @@ +import asyncio +from typing import Any, Mapping + +from ...utilities import get_logger +from ..views import Trace +from .call_remote_great_ai_async import call_remote_great_ai_async + +logger = get_logger("call_remote_great_ai") + + +def call_remote_great_ai( + base_uri: str, data: Mapping[str, Any], retry_count: int = 4 +) -> Trace: + try: + asyncio.get_running_loop() + raise Exception( + f"Already running in an event loop, you have to call `{call_remote_great_ai_async.__name__}`" + ) + except RuntimeError: + pass + + future = call_remote_great_ai_async( + base_uri=base_uri, data=data, retry_count=retry_count + ) + + return asyncio.run(future) diff --git a/src/great_ai/great_ai/remote/call_remote_great_ai_async.py b/src/great_ai/great_ai/remote/call_remote_great_ai_async.py new file mode 100644 index 0000000..4e99d08 --- /dev/null +++ b/src/great_ai/great_ai/remote/call_remote_great_ai_async.py @@ -0,0 +1,25 @@ +from typing import Any, Mapping, Optional + +from ..views import Trace +from .http_client import HttpClient +from .remote_call_error import RemoteCallError + +http: Optional[HttpClient] = None + + +async def call_remote_great_ai_async( + base_uri: str, data: Mapping[str, Any], retry_count: int = 4 +) -> Trace: + global http + if http is None: + http = HttpClient() + + url = f"{base_uri}/predict/" + response = await http.post( + url=url, data=data, retry_count=retry_count, expected_status=200 + ) + + try: + return Trace.parse_obj(response) + except Exception: + raise RemoteCallError("Could not parse response") diff --git a/src/great_ai/great_ai/remote/http_client.py b/src/great_ai/great_ai/remote/http_client.py new file mode 100644 index 0000000..c6d8255 --- /dev/null +++ b/src/great_ai/great_ai/remote/http_client.py @@ -0,0 +1,60 @@ +import logging +from asyncio import sleep +from typing import Any, Mapping, Optional + +import aiohttp + +from .remote_call_error import RemoteCallError + +logger = logging.getLogger("http") + + +class HttpClient: + timeout_seconds: int = 600 + wait_between_retries_seconds: float = 5 + + def __init__( + self, + ) -> None: + timeout = aiohttp.ClientTimeout(total=self.timeout_seconds) + + self._session = aiohttp.ClientSession( + raise_for_status=False, + timeout=timeout, + ) + + async def post( + self, + url: str, + data: Mapping[str, Any], + retry_count: int = 0, + expected_status: Optional[int] = None, + **kwargs: Any, + ) -> Any: + for i in range(retry_count + 1): + try: + async with self._session.post(url, json=data, **kwargs) as r: + if ( + expected_status is not None and r.status != expected_status + ) or r.status >= 500: + response_text = await r.text() + raise ValueError( + f"Found not-expected status code: {r.status}, response is: {response_text}" + ) + try: + return await r.json() + except Exception: + raise RemoteCallError( + "JSON parsing failed", + ) + except Exception as e: + logger.warning( + f"Request failed ({e}), {retry_count - i - 1} retries left", + ) + if retry_count - i > 1: + await sleep(self.wait_between_retries_seconds) + + raise RemoteCallError("Request has failed too many times") + + async def close(self) -> None: + await self._session.close() diff --git a/src/great_ai/great_ai/remote/remote_call_error.py b/src/great_ai/great_ai/remote/remote_call_error.py new file mode 100644 index 0000000..f310535 --- /dev/null +++ b/src/great_ai/great_ai/remote/remote_call_error.py @@ -0,0 +1,2 @@ +class RemoteCallError(Exception): + pass diff --git a/src/great_ai/utilities/__init__.py b/src/great_ai/utilities/__init__.py index 7da0959..22ed3b1 100644 --- a/src/great_ai/utilities/__init__.py +++ b/src/great_ai/utilities/__init__.py @@ -1,5 +1,5 @@ from .clean import clean -from .config_file import ConfigFile +from .config_file import ConfigFile, ParseError from .evaluate_ranking import evaluate_ranking from .get_sentences import get_sentences from .language import english_name_of_language, is_english, predict_language