diff --git a/.github/workflows/docker/compose/guardrails-compose.yaml b/.github/workflows/docker/compose/guardrails-compose.yaml index 5c8ccf621f..a07e8f921c 100644 --- a/.github/workflows/docker/compose/guardrails-compose.yaml +++ b/.github/workflows/docker/compose/guardrails-compose.yaml @@ -19,6 +19,10 @@ services: build: dockerfile: comps/guardrails/src/pii_detection/Dockerfile image: ${REGISTRY:-opea}/guardrails-pii-predictionguard:${TAG:-latest} + guardrails-polite-guard: + build: + dockerfile: comps/guardrails/src/polite_guard/Dockerfile + image: ${REGISTRY:-opea}/guardrails-polite-guard:${TAG:-latest} guardrails-toxicity-predictionguard: build: dockerfile: comps/guardrails/src/toxicity_detection/Dockerfile diff --git a/comps/guardrails/deployment/docker_compose/compose.yaml b/comps/guardrails/deployment/docker_compose/compose.yaml index 80df4806b2..3262ac9ba1 100644 --- a/comps/guardrails/deployment/docker_compose/compose.yaml +++ b/comps/guardrails/deployment/docker_compose/compose.yaml @@ -133,6 +133,20 @@ services: PREDICTIONGUARD_API_KEY: ${PREDICTIONGUARD_API_KEY} restart: unless-stopped + # polite guard service + guardrails-polite-guard-server: + image: ${REGISTRY:-opea}/guardrails-polite-guard:${TAG:-latest} + container_name: guardrails-polite-guard-server + ports: + - "${POLITE_GUARD_PORT:-9092}:9092" + ipc: host + environment: + no_proxy: ${no_proxy} + http_proxy: ${http_proxy} + https_proxy: ${https_proxy} + HUGGINGFACEHUB_API_TOKEN: ${HF_TOKEN} + restart: unless-stopped + # predictionguard injection service injection-predictionguard-server: image: ${REGISTRY:-opea}/injection-predictionguard:${TAG:-latest} diff --git a/comps/guardrails/src/polite_guard/Dockerfile b/comps/guardrails/src/polite_guard/Dockerfile new file mode 100644 index 0000000000..f3d197a802 --- /dev/null +++ b/comps/guardrails/src/polite_guard/Dockerfile @@ -0,0 +1,31 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +FROM python:3.11-slim + +ENV LANG=C.UTF-8 + +ARG ARCH="cpu" + +RUN apt-get update -y && apt-get install -y --no-install-recommends --fix-missing \ + libgl1-mesa-glx \ + libjemalloc-dev + + +RUN useradd -m -s /bin/bash user && \ + mkdir -p /home/user && \ + chown -R user /home/user/ + +USER user + +COPY comps /home/user/comps + +RUN pip install --no-cache-dir --upgrade pip && \ + if [ ${ARCH} = "cpu" ]; then pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu; fi && \ + pip install --no-cache-dir -r /home/user/comps/guardrails/src/polite_guard/requirements.txt + +ENV PYTHONPATH=$PYTHONPATH:/home/user + +WORKDIR /home/user/comps/guardrails/src/polite_guard/ + +ENTRYPOINT ["python", "opea_polite_guard_microservice.py"] diff --git a/comps/guardrails/src/polite_guard/README.md b/comps/guardrails/src/polite_guard/README.md new file mode 100644 index 0000000000..db86722716 --- /dev/null +++ b/comps/guardrails/src/polite_guard/README.md @@ -0,0 +1,87 @@ +# Politeness Guard Microservice + +## Introduction + +The Polite Guard Microservice allows AI application developers to ensure that user input and Large Language Model (LLM) outputs remain polite and respectful. By leveraging, [Polite Guard](https://huggingface.co/Intel/polite-guard), a fine-tuned Transformer model for politeness classification, this lightweight guardrails microservice ensures courteous interactions without significantly sacrificing performance, making it suitable for deployment on both Intel Gaudi and Xeon. + +Politeness plays a crucial role in creating a positive and respectful environment. The service classifies text into four categories: _polite_, _somewhat polite_, _neutral_, and _impolite_. Any _impolite_ text is rejected, along with a score, ensuring that systems maintain a courteous tone. + +More details about the Polite Guard model can be found [here](https://github.com/intel/polite-guard). + +## 🚀1. Start Microservice with Python(Option 1) + +### 1.1 Install Requirements + +```bash +pip install -r requirements.txt +``` + +### 1.2 Start Politeness Detection Microservice with Python Script + +```bash +python opea_polite_guard_microservice.py +``` + +## 🚀2. Start Microservice with Docker (Option 2) + +### 2.1 Prepare bias detection model + +```bash +export HUGGINGFACEHUB_API_TOKEN=${YOUR_HF_TOKEN} +``` + +### 2.2 Build Docker Image + +```bash +cd ../../../ # back to GenAIComps/ folder +docker build -t opea/guardrails-politeness-detection:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/guardrails/src/polite_guard/Dockerfile . +``` + +### 2.3 Run Docker Container with Microservice + +```bash +docker run -d --rm --runtime=runc --name="guardrails-politeness-detection" -p 9092:9092 --ipc=host -e http_proxy=$http_proxy -e https_proxy=$https_proxy -e HUGGINGFACEHUB_API_TOKEN=${HUGGINGFACEHUB_API_TOKEN} -e HF_TOKEN=${HUGGINGFACEHUB_API_TOKEN} opea/guardrails-politeness-detection:latest +``` + +### 2.4 Get Status of Microservice + +```bash +docker container logs -f guardrails-politeness-detection +``` + +### 2.5 Consume Microservice Pre-LLM/Post-LLM + +Once microservice starts, users can use examples (bash or python) below to apply bias detection for both user's query (Pre-LLM) or LLM's response (Post-LLM) + +**Bash:** + +```bash +curl localhost:9092/v1/polite \ + -X POST \ + -d '{"text":"He is stupid"}' \ + -H 'Content-Type: application/json' +``` + +Example Output: + +```bash +"\nViolated policies: Impolite (score: 1.00), please check your input.\n" +``` + +**Python Script:** + +```python +import requests + +# Define the URL and payload +url = "http://localhost:9092/v1/polite" +payload = {"text": "He is stupid"} +headers = {"Content-Type": "application/json"} + +# Send a POST request +response = requests.post(url, json=payload, headers=headers) + +# Print the response +print("Status Code:", response.status_code) +print("Response Body:", response.json()) +``` diff --git a/comps/guardrails/src/polite_guard/__init__.py b/comps/guardrails/src/polite_guard/__init__.py new file mode 100644 index 0000000000..916f3a44b2 --- /dev/null +++ b/comps/guardrails/src/polite_guard/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/comps/guardrails/src/polite_guard/integrations/__init__.py b/comps/guardrails/src/polite_guard/integrations/__init__.py new file mode 100644 index 0000000000..4057dc0163 --- /dev/null +++ b/comps/guardrails/src/polite_guard/integrations/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/comps/guardrails/src/polite_guard/integrations/politeguard.py b/comps/guardrails/src/polite_guard/integrations/politeguard.py new file mode 100644 index 0000000000..dd96260552 --- /dev/null +++ b/comps/guardrails/src/polite_guard/integrations/politeguard.py @@ -0,0 +1,51 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import os + +from transformers import pipeline + +from comps import CustomLogger, OpeaComponent, OpeaComponentRegistry, ServiceType, TextDoc + +logger = CustomLogger("opea_polite_guard") +logflag = os.getenv("LOGFLAG", False) + + +@OpeaComponentRegistry.register("OPEA_POLITE_GUARD") +class OpeaPoliteGuard(OpeaComponent): + """A specialized politeness detection component derived from OpeaComponent.""" + + def __init__(self, name: str, description: str, config: dict = None): + super().__init__(name, ServiceType.GUARDRAIL.name.lower(), description, config) + self.model = os.getenv("POLITE_GUARD_MODEL", "Intel/polite-guard") + self.polite_pipeline = pipeline("text-classification", model=self.model, tokenizer=self.model) + health_status = self.check_health() + if not health_status: + logger.error("OpeaPoliteGuard health check failed.") + + async def invoke(self, input: str): + """Invokes the polite guard for the input. + + Args: + input (Input str) + """ + response = await asyncio.to_thread(self.polite_pipeline, input) + if response[0]["label"] == "impolite": + return TextDoc( + text=f"Violated policies: Impolite (score: {response[0]['score']:0.2f}), please check your input.", + downstream_black_list=[".*"], + ) + else: + return TextDoc(text=input) + + def check_health(self) -> bool: + """Checks the health of the animation service. + + Returns: + bool: True if the service is reachable and healthy, False otherwise. + """ + if self.polite_pipeline: + return True + else: + return False diff --git a/comps/guardrails/src/polite_guard/opea_polite_guard_microservice.py b/comps/guardrails/src/polite_guard/opea_polite_guard_microservice.py new file mode 100644 index 0000000000..f5fe1149ef --- /dev/null +++ b/comps/guardrails/src/polite_guard/opea_polite_guard_microservice.py @@ -0,0 +1,68 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os +import time + +from integrations.politeguard import OpeaPoliteGuard + +from comps import ( + CustomLogger, + OpeaComponentLoader, + ServiceType, + TextDoc, + opea_microservices, + register_microservice, + register_statistics, + statistics_dict, +) + +logger = CustomLogger("opea_polite_guard_microservice") +logflag = os.getenv("LOGFLAG", False) + +polite_guard_component_name = os.getenv("POLITE_GUARD_COMPONENT_NAME", "OPEA_POLITE_GUARD") +# Initialize OpeaComponentLoader +loader = OpeaComponentLoader( + polite_guard_component_name, + name=polite_guard_component_name, + description=f"OPEA Polite Guard Component: {polite_guard_component_name}", +) + + +@register_microservice( + name="opea_service@polite_guard", + service_type=ServiceType.GUARDRAIL, + endpoint="/v1/polite", + host="0.0.0.0", + port=9092, + input_datatype=TextDoc, + output_datatype=TextDoc, +) +@register_statistics(names=["opea_service@polite_guard"]) +async def llm_generate(input: TextDoc): + start = time.time() + + # Log the input if logging is enabled + if logflag: + logger.info(f"Input received: {input}") + + try: + # Use the loader to invoke the component + bias_response = await loader.invoke(input.text) + + # Log the result if logging is enabled + if logflag: + logger.info(f"Output received: {bias_response}") + + # Record statistics + statistics_dict["opea_service@polite_guard"].append_latency(time.time() - start, None) + return bias_response + + except Exception as e: + logger.error(f"Error during polite guard invocation: {e}") + raise + + +if __name__ == "__main__": + opea_microservices["opea_service@polite_guard"].start() + logger.info("OPEA Polite Guard Microservice is up and running successfully...") diff --git a/comps/guardrails/src/polite_guard/requirements.txt b/comps/guardrails/src/polite_guard/requirements.txt new file mode 100644 index 0000000000..abca61e88c --- /dev/null +++ b/comps/guardrails/src/polite_guard/requirements.txt @@ -0,0 +1,16 @@ +aiohttp +docarray[full] +fastapi +httpx +huggingface_hub +langchain-community +langchain-huggingface +opentelemetry-api +opentelemetry-exporter-otlp +opentelemetry-sdk +prometheus-fastapi-instrumentator +pyyaml +requests +shortuuid +transformers +uvicorn diff --git a/tests/guardrails/test_guardrails_polite_guard.sh b/tests/guardrails/test_guardrails_polite_guard.sh new file mode 100644 index 0000000000..53c4aff5f1 --- /dev/null +++ b/tests/guardrails/test_guardrails_polite_guard.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -x + +WORKPATH=$(dirname "$PWD") +ip_address=$(hostname -I | awk '{print $1}') + +function build_docker_images() { + echo "Start building docker images for microservice" + cd $WORKPATH + docker build --no-cache -t opea/guardrails-polite-guard:comps --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/guardrails/src/polite_guard/Dockerfile . + if [ $? -ne 0 ]; then + echo "opea/guardrails-polite-guard built fail" + exit 1 + else + echo "opea/guardrails-polite-guard built successful" + fi +} + +function start_service() { + echo "Starting microservice" + export POLITE_GUARD_PORT=11301 + export TAG=comps + service_name="guardrails-polite-guard-server" + cd $WORKPATH + cd comps/guardrails/deployment/docker_compose/ + docker compose up ${service_name} -d + sleep 15 + max_retries=3 + retries=0 + until docker logs ${service_name} 2>&1 | grep -q "Application startup complete"; do + if [ $retries -ge $max_retries ]; then + echo "Application failed to start after $max_retries attempts." + exit 1 + fi + echo "Waiting for application startup to complete... (Attempt $((retries + 1))/$max_retries)" + retries=$((retries + 1)) + sleep 2 # Wait for 2 seconds before checking again + done + echo "Microservice started" +} + +function validate_microservice() { + echo "Validate microservice started" + echo "test 1 - Impolite" + result=$(curl localhost:11301/v1/polite -X POST -d '{"text":"He is stupid"}' -H 'Content-Type: application/json') + if [[ $result == *"Violated"* ]]; then + echo "Result correct." + else + docker logs guardrails-polite-guard-server + exit 1 + fi + echo "test 2 - Polite" + result=$(curl localhost:11301/v1/polite -X POST -d '{"text":"He is kind"}' -H 'Content-Type: application/json') + if [[ $result == *"kind"* ]]; then + echo "Result correct." + else + echo "Result wrong." + docker logs guardrails-polite-guard-server + exit 1 + fi + echo "Validate microservice completed" +} + +function stop_docker() { + cid=$(docker ps -aq --filter "name=guardrails-polite-guard-server") + echo "Shutdown legacy containers "$cid + if [[ ! -z "$cid" ]]; then docker stop $cid && docker rm $cid && sleep 1s; fi +} + +function main() { + + stop_docker + + build_docker_images + start_service + + validate_microservice + + stop_docker + echo "cleanup container images and volumes" + echo y | docker system prune 2>&1 > /dev/null + +} + +main