From 3a11c8a20163235935af235b06ad2a5d9c6dddd8 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Tue, 18 Jun 2024 12:48:49 -0500 Subject: [PATCH 1/6] get history endpoint --- Makefile | 2 +- guardrails_api/app.py | 4 +++ guardrails_api/blueprints/guards.py | 15 +++++++++++ guardrails_api/clients/cache_client.py | 35 ++++++++++++++++++++++++++ pyproject.toml | 3 ++- 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 guardrails_api/clients/cache_client.py diff --git a/Makefile b/Makefile index c1629a8..24cd581 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ install: # Installs development dependencies install-dev: - pip install .[dev]; + pip install ".[dev]"; lock: pip freeze --exclude guardrails-api-client > requirements-lock.txt diff --git a/guardrails_api/app.py b/guardrails_api/app.py index 8732d44..366096c 100644 --- a/guardrails_api/app.py +++ b/guardrails_api/app.py @@ -9,6 +9,7 @@ from opentelemetry.instrumentation.flask import FlaskInstrumentor from guardrails_api.clients.postgres_client import postgres_is_enabled from guardrails_api.otel import otel_is_disabled, initialize +from guardrails_api.clients.cache_client import CacheClient # TODO: Move this to a separate file @@ -80,6 +81,9 @@ def create_app(env: Optional[str] = None, config: Optional[str] = None): pg_client = PostgresClient() pg_client.initialize(app) + + cache_client = CacheClient() + cache_client.initialize(app) from guardrails_api.blueprints.root import root_bp from guardrails_api.blueprints.guards import guards_bp diff --git a/guardrails_api/blueprints/guards.py b/guardrails_api/blueprints/guards.py index 777fff3..6ebc790 100644 --- a/guardrails_api/blueprints/guards.py +++ b/guardrails_api/blueprints/guards.py @@ -10,6 +10,7 @@ from opentelemetry.trace import Span from guardrails_api_client import Guard as GuardStruct from guardrails_api.classes.http_error import HttpError +from guardrails_api.clients.cache_client import CacheClient from guardrails_api.clients.memory_guard_client import MemoryGuardClient from guardrails_api.clients.pg_guard_client import PGGuardClient from guardrails_api.clients.postgres_client import postgres_is_enabled @@ -34,6 +35,8 @@ is_guard = isinstance(export, Guard) if is_guard: guard_client.create_guard(export) + +cache_client = CacheClient() @guards_bp.route("/", methods=["GET", "POST"]) @@ -308,4 +311,16 @@ def validate_streamer(guard_iter): # prompt_params=prompt_params, # result=result # ) + serialized_history = [ + call.to_dict() for call in guard.history + ] + cache_key = f"{guard.name}-{result.call_id}" + cache_client.set(cache_key, serialized_history, 300) return result.to_dict() + +@guards_bp.route("//history/", methods=["GET"]) +@handle_error +def guard_history(guard_name: str, call_id: str): + if request.method == "GET": + cache_key = f"{guard_name}-{call_id}" + return cache_client.get(cache_key) \ No newline at end of file diff --git a/guardrails_api/clients/cache_client.py b/guardrails_api/clients/cache_client.py new file mode 100644 index 0000000..f910834 --- /dev/null +++ b/guardrails_api/clients/cache_client.py @@ -0,0 +1,35 @@ +from flask_caching import Cache + + +# TODO: Add option to connect to Redis or MemCached backend with environment variables +class CacheClient: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(CacheClient, cls).__new__(cls) + return cls._instance + + + def initialize(self, app): + self.cache = Cache( + app, + config={ + "CACHE_TYPE": "simple", + "CACHE_DEFAULT_TIMEOUT": 300, + "CACHE_NO_NULL_WARNING": True, + "CACHE_THRESHOLD": 50 + } + ) + + def get(self, key): + return self.cache.get(key) + + def set(self, key, value, ttl): + self.cache.set(key, value, timeout=ttl) + + def delete(self, key): + self.cache.delete(key) + + def clear(self): + self.cache.clear() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3beca79..584e368 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,10 @@ readme = "README.md" keywords = ["Guardrails", "Guardrails AI", "Guardrails API", "Guardrails API"] requires-python = ">= 3.8.1" dependencies = [ - "guardrails-ai@git+https://github.com/guardrails-ai/guardrails.git@core-schema-impl", + "guardrails-ai@git+https://github.com/guardrails-ai/guardrails.git@0.5.0-dev", "flask>=3.0.3,<4", "Flask-SQLAlchemy>=3.1.1,<4", + "Flask-Caching>=2.3.0,<3", "Werkzeug>=3.0.3,<4", "jsonschema>=4.22.0,<5", "referencing>=0.35.1,<1", From 3b5e6520207844f219984ac3bb3a52ed54fbc5d6 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Fri, 21 Jun 2024 09:01:23 -0500 Subject: [PATCH 2/6] set version to alpha --- guardrails_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guardrails_api/__init__.py b/guardrails_api/__init__.py index 6c8e6b9..0beccff 100644 --- a/guardrails_api/__init__.py +++ b/guardrails_api/__init__.py @@ -1 +1 @@ -__version__ = "0.0.0" +__version__ = "0.0.0a0" From ca9ca82c81459a719e17b0c01191644ab805f642 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Fri, 21 Jun 2024 09:04:38 -0500 Subject: [PATCH 3/6] update guardrails --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 584e368..fd7d86a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" keywords = ["Guardrails", "Guardrails AI", "Guardrails API", "Guardrails API"] requires-python = ">= 3.8.1" dependencies = [ - "guardrails-ai@git+https://github.com/guardrails-ai/guardrails.git@0.5.0-dev", + "guardrails-ai>=0.5.0a0", "flask>=3.0.3,<4", "Flask-SQLAlchemy>=3.1.1,<4", "Flask-Caching>=2.3.0,<3", From ee4b59dd4edf687aa84ea0a2755cb3fd266be0e8 Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Mon, 24 Jun 2024 09:13:24 -0500 Subject: [PATCH 4/6] allow setting port, make all cli option flag only --- .github/workflows/publish.yml | 5 ----- guardrails_api/app.py | 7 ++++++- guardrails_api/cli/start.py | 22 ++++++++++++++++------ guardrails_api/clients/cache_client.py | 3 +-- guardrails_api/default.env | 3 ++- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b0daf62..14fb4ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,11 +25,6 @@ jobs: pip install build; continue-on-error: false - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Build the module env: PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/guardrails_api/app.py b/guardrails_api/app.py index 366096c..088d172 100644 --- a/guardrails_api/app.py +++ b/guardrails_api/app.py @@ -47,7 +47,7 @@ def register_config(config: Optional[str] = None): SourceFileLoader("config", config_file_path).load_module() -def create_app(env: Optional[str] = None, config: Optional[str] = None): +def create_app(env: Optional[str] = None, config: Optional[str] = None, port: Optional[int] = None): if os.environ.get("APP_ENVIRONMENT") != "production": from dotenv import load_dotenv @@ -56,6 +56,11 @@ def create_app(env: Optional[str] = None, config: Optional[str] = None): env_file_path = os.path.abspath(env_file) load_dotenv(env_file_path) + set_port = port or os.environ.get("PORT", 8000) + host = os.environ.get("HOST", "http://localhost") + self_endpoint = os.environ.get("SELF_ENDPOINT", f"{host}:{set_port}") + os.environ["SELF_ENDPOINT"] = self_endpoint + register_config(config) app = Flask(__name__) diff --git a/guardrails_api/cli/start.py b/guardrails_api/cli/start.py index d686621..0fe995c 100644 --- a/guardrails_api/cli/start.py +++ b/guardrails_api/cli/start.py @@ -29,12 +29,22 @@ def start( env: Optional[str] = typer.Option( default="", help="An env file to load environment variables from.", - prompt=".env file (optional)", ), config: Optional[str] = typer.Option( default="", help="A config file to load Guards from.", - prompt="config file (optional)", + ), + timeout: Optional[int] = typer.Option( + default=5, + help="Gunicorn worker timeout.", + ), + threads: Optional[int] = typer.Option( + default=10, + help="Number of Gunicorn worker threads.", + ), + port: Optional[int] = typer.Option( + default=8000, + help="The port to run the server on.", ), ): # TODO: If these are empty, @@ -43,8 +53,8 @@ def start( config = config or None options = { - "bind": "0.0.0.0:8000", - "timeout": 5, - "threads": 10, + "bind": f"0.0.0.0:{port}", + "timeout": timeout, + "threads": threads, } - StandaloneApplication(create_app(env, config), options).run() + StandaloneApplication(create_app(env, config, port), options).run() diff --git a/guardrails_api/clients/cache_client.py b/guardrails_api/clients/cache_client.py index f910834..e24bcb5 100644 --- a/guardrails_api/clients/cache_client.py +++ b/guardrails_api/clients/cache_client.py @@ -15,9 +15,8 @@ def initialize(self, app): self.cache = Cache( app, config={ - "CACHE_TYPE": "simple", + "CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300, - "CACHE_NO_NULL_WARNING": True, "CACHE_THRESHOLD": 50 } ) diff --git a/guardrails_api/default.env b/guardrails_api/default.env index 674906a..6e37d0b 100644 --- a/guardrails_api/default.env +++ b/guardrails_api/default.env @@ -3,5 +3,6 @@ PYTHONUNBUFFERED=1 LOGLEVEL="INFO" GUARDRAILS_LOG_LEVEL="INFO" GUARDRAILS_PROCESS_COUNT=1 -SELF_ENDPOINT=http://localhost:8000 +HOST=http://localhost +PORT=8000 OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES \ No newline at end of file From 3ade3773f98c7ca3436aca52f836263c8b1fecad Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Mon, 24 Jun 2024 17:54:52 -0500 Subject: [PATCH 5/6] test fixes --- tests/blueprints/test_guards.py | 22 +++++++++++++++++++--- tests/cli/test_start.py | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/blueprints/test_guards.py b/tests/blueprints/test_guards.py index 4c1c29f..b12a49d 100644 --- a/tests/blueprints/test_guards.py +++ b/tests/blueprints/test_guards.py @@ -44,8 +44,8 @@ def test_route_setup(mocker): from guardrails_api.blueprints.guards import guards_bp - assert guards_bp.route_call_count == 3 - assert guards_bp.routes == ["/", "/", "//validate"] + assert guards_bp.route_call_count == 4 + assert guards_bp.routes == ["/", "/", "//validate", "//history/"] def test_guards__get(mocker): @@ -362,6 +362,7 @@ def test_validate__raises_bad_request__num_reasks(mocker): def test_validate__parse(mocker): os.environ["PGHOST"] = "localhost" mock_outcome = ValidationOutcome( + call_id="mock-call-id", raw_llm_output="Hello world!", validated_output="Hello world!", validation_passed=True, @@ -386,6 +387,10 @@ def test_validate__parse(mocker): "guardrails_api.blueprints.guards.guard_client.get_guard", return_value=mock_guard, ) + + mocker.patch( + "guardrails_api.blueprints.guards.CacheClient.set" + ) # mocker.patch("guardrails_api.blueprints.guards.get_tracer", return_value=mock_tracer) @@ -430,6 +435,7 @@ def test_validate__parse(mocker): # set_attribute_spy.assert_has_calls(expected_calls) assert response == { + "callId": "mock-call-id", "validatedOutput": "Hello world!", "validationPassed": True, "rawLlmOutput": "Hello world!", @@ -442,7 +448,10 @@ def test_validate__call(mocker): os.environ["PGHOST"] = "localhost" mock_guard = MockGuardStruct() mock_outcome = ValidationOutcome( - raw_llm_output="Hello world!", validated_output=None, validation_passed=False + call_id="mock-call-id", + raw_llm_output="Hello world!", + validated_output=None, + validation_passed=False ) mock___call__ = mocker.patch.object(MockGuardStruct, "__call__") @@ -475,6 +484,12 @@ def test_validate__call(mocker): "guardrails_api.blueprints.guards.get_llm_callable", return_value="openai.Completion.create", ) + + mocker.patch( + "guardrails_api.blueprints.guards.CacheClient.set" + ) + + # mocker.patch("guardrails_api.blueprints.guards.get_tracer", return_value=mock_tracer) @@ -523,6 +538,7 @@ def test_validate__call(mocker): # set_attribute_spy.assert_has_calls(expected_calls) assert response == { + "callId": "mock-call-id", "validationPassed": False, "validatedOutput": None, "rawLlmOutput": "Hello world!", diff --git a/tests/cli/test_start.py b/tests/cli/test_start.py index 9d91ad9..071675f 100644 --- a/tests/cli/test_start.py +++ b/tests/cli/test_start.py @@ -9,7 +9,7 @@ def test_start(mocker): from guardrails_api.cli.start import start - start("env", "config") + start("env", "config", 5, 10, 8000) mock_gunicorn.assert_called_once_with( mock_flask_app, @@ -20,4 +20,4 @@ def test_start(mocker): }, ) - mock_create_app.assert_called_once_with("env", "config") + mock_create_app.assert_called_once_with("env", "config", 8000) From 278b9128cd722b90e5b1360de33e3b761e2b276e Mon Sep 17 00:00:00 2001 From: Caleb Courier Date: Mon, 24 Jun 2024 18:54:53 -0500 Subject: [PATCH 6/6] update oss and lock file --- pyproject.toml | 2 +- requirements-lock.txt | 113 ++++++++++++++++++++---------------------- 2 files changed, 54 insertions(+), 61 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd7d86a..f0bb850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" keywords = ["Guardrails", "Guardrails AI", "Guardrails API", "Guardrails API"] requires-python = ">= 3.8.1" dependencies = [ - "guardrails-ai>=0.5.0a0", + "guardrails-ai>=0.5.0a2", "flask>=3.0.3,<4", "Flask-SQLAlchemy>=3.1.1,<4", "Flask-Caching>=2.3.0,<3", diff --git a/requirements-lock.txt b/requirements-lock.txt index 5171469..9be17cc 100644 --- a/requirements-lock.txt +++ b/requirements-lock.txt @@ -2,96 +2,84 @@ aiohttp==3.9.5 aiosignal==1.3.1 annotated-types==0.7.0 anyio==4.4.0 -asgiref==3.8.1 +arrow==1.3.0 attrs==23.2.0 blinker==1.8.2 -boto3==1.34.115 -botocore==1.34.115 -certifi==2024.2.2 +boto3==1.34.132 +botocore==1.34.132 +cachelib==0.9.0 +certifi==2024.6.2 cffi==1.16.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 coloredlogs==15.0.1 -cryptography==42.0.7 +cryptography==42.0.8 Deprecated==1.2.14 distro==1.9.0 -faiss-cpu==1.8.0 -filelock==3.14.0 +Faker==25.9.1 +filelock==3.15.4 Flask==3.0.3 +Flask-Caching==2.3.0 Flask-Cors==4.0.1 Flask-SQLAlchemy==3.1.1 +fqdn==1.5.1 frozenlist==1.4.1 -fsspec==2024.5.0 -googleapis-common-protos==1.63.0 +fsspec==2024.6.0 +googleapis-common-protos==1.63.2 griffe==0.36.9 -grpcio==1.64.0 -guardrails-ai @ git+https://github.com/guardrails-ai/guardrails.git@fd77007dfe823f8cb32cd314b78e5f63aea71e9a +grpcio==1.64.1 +guardrails-ai==0.5.0a2 +guardrails-api @ file:///Users/calebcourier/Projects/gr-mono/guardrails-cdk/guardrails-api +guardrails-api-client==0.3.8 gunicorn==22.0.0 h11==0.14.0 httpcore==1.0.5 httpx==0.27.0 -huggingface-hub==0.23.2 +huggingface-hub==0.23.4 humanfriendly==10.0 idna==3.7 -importlib-metadata==7.0.0 +ijson==3.3.0 +importlib_metadata==7.1.0 +isoduration==20.11.0 itsdangerous==2.2.0 Jinja2==3.1.4 jmespath==1.0.1 joblib==1.4.2 jsonpatch==1.33 -jsonpointer==2.4 +jsonpointer==3.0.0 +jsonref==1.1.0 jsonschema==4.22.0 jsonschema-specifications==2023.12.1 jwt==1.3.1 -langchain-core==0.1.52 -langsmith==0.1.65 -litellm==1.39.3 +langchain-core==0.2.9 +langsmith==0.1.82 +litellm==1.40.25 lxml==4.9.4 markdown-it-py==3.0.0 MarkupSafe==2.1.5 mdurl==0.1.2 multidict==6.0.5 nltk==3.8.1 -numpy==1.26.4 -openai==1.30.5 -opentelemetry-api==1.24.0 -opentelemetry-distro==0.45b0 -opentelemetry-exporter-otlp-proto-common==1.24.0 -opentelemetry-exporter-otlp-proto-grpc==1.24.0 -opentelemetry-exporter-otlp-proto-http==1.24.0 -opentelemetry-instrumentation==0.45b0 -opentelemetry-instrumentation-asgi==0.45b0 -opentelemetry-instrumentation-asyncio==0.45b0 -opentelemetry-instrumentation-aws-lambda==0.45b0 -opentelemetry-instrumentation-boto3sqs==0.45b0 -opentelemetry-instrumentation-botocore==0.45b0 -opentelemetry-instrumentation-dbapi==0.45b0 -opentelemetry-instrumentation-flask==0.45b0 -opentelemetry-instrumentation-grpc==0.45b0 -opentelemetry-instrumentation-httpx==0.45b0 -opentelemetry-instrumentation-jinja2==0.45b0 -opentelemetry-instrumentation-logging==0.45b0 -opentelemetry-instrumentation-requests==0.45b0 -opentelemetry-instrumentation-sqlalchemy==0.45b0 -opentelemetry-instrumentation-sqlite3==0.45b0 -opentelemetry-instrumentation-tortoiseorm==0.45b0 -opentelemetry-instrumentation-urllib==0.45b0 -opentelemetry-instrumentation-urllib3==0.45b0 -opentelemetry-instrumentation-wsgi==0.45b0 -opentelemetry-propagator-aws-xray==1.0.1 -opentelemetry-proto==1.24.0 -opentelemetry-sdk==1.24.0 -opentelemetry-semantic-conventions==0.45b0 -opentelemetry-test-utils==0.45b0 -opentelemetry-util-http==0.45b0 -orjson==3.10.3 -packaging==23.2 +openai==1.35.3 +opentelemetry-api==1.25.0 +opentelemetry-exporter-otlp-proto-common==1.25.0 +opentelemetry-exporter-otlp-proto-grpc==1.25.0 +opentelemetry-exporter-otlp-proto-http==1.25.0 +opentelemetry-instrumentation==0.46b0 +opentelemetry-instrumentation-flask==0.46b0 +opentelemetry-instrumentation-wsgi==0.46b0 +opentelemetry-proto==1.25.0 +opentelemetry-sdk==1.25.0 +opentelemetry-semantic-conventions==0.46b0 +opentelemetry-util-http==0.46b0 +orjson==3.10.5 +packaging==24.1 protobuf==4.25.3 psycopg2-binary==2.9.9 pycparser==2.22 -pydantic==2.7.2 -pydantic_core==2.18.3 +pydantic==2.7.4 +pydantic_core==2.18.4 pydash==7.0.7 Pygments==2.18.0 python-dateutil==2.9.0.post0 @@ -100,23 +88,28 @@ PyYAML==6.0.1 referencing==0.35.1 regex==2023.12.25 requests==2.32.3 +rfc3339-validator==0.1.4 +rfc3987==1.3.8 rich==13.7.1 rpds-py==0.18.1 rstr==3.2.2 -s3transfer==0.10.1 -setuptools==70.0.0 +s3transfer==0.10.2 +setuptools==70.1.0 shellingham==1.5.4 six==1.16.0 sniffio==1.3.1 -SQLAlchemy==2.0.30 -tenacity==8.3.0 +SQLAlchemy==2.0.31 +tenacity==8.4.2 tiktoken==0.7.0 tokenizers==0.19.1 tqdm==4.66.4 typer==0.9.4 -typing_extensions==4.12.0 -urllib3==2.2.1 +types-python-dateutil==2.9.0.20240316 +typing_extensions==4.12.2 +uri-template==1.3.0 +urllib3==2.2.2 +webcolors==24.6.0 Werkzeug==3.0.3 wrapt==1.16.0 yarl==1.9.4 -zipp==3.19.0 +zipp==3.19.2