From 5a500b1b25e95bd70de89f577279f69268f3f454 Mon Sep 17 00:00:00 2001 From: jmansdorfer Date: Tue, 24 Feb 2026 14:07:07 -0500 Subject: [PATCH 1/5] adding responses and mcp components --- examples/responses.ipynb | 129 ++++++++++++ predictionguard/client.py | 22 +- predictionguard/src/chat.py | 2 +- predictionguard/src/completions.py | 2 +- predictionguard/src/mcp_servers.py | 85 ++++++++ predictionguard/src/mcp_tools.py | 85 ++++++++ predictionguard/src/models.py | 2 +- predictionguard/src/responses.py | 316 +++++++++++++++++++++++++++++ predictionguard/version.py | 2 +- pyproject.toml | 4 +- tests/test_mcp_servers.py | 10 + tests/test_mcp_tools.py | 11 + tests/test_responses.py | 188 +++++++++++++++++ uv.lock | 52 ++--- 14 files changed, 873 insertions(+), 37 deletions(-) create mode 100644 examples/responses.ipynb create mode 100644 predictionguard/src/mcp_servers.py create mode 100644 predictionguard/src/mcp_tools.py create mode 100644 predictionguard/src/responses.py create mode 100644 tests/test_mcp_servers.py create mode 100644 tests/test_mcp_tools.py create mode 100644 tests/test_responses.py diff --git a/examples/responses.ipynb b/examples/responses.ipynb new file mode 100644 index 0000000..ef53cc4 --- /dev/null +++ b/examples/responses.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Using Responses with Prediction Guard" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import necessary packages\n", + "import os\n", + "import json\n", + "\n", + "from predictionguard import PredictionGuard\n", + "\n", + "\n", + "# Set your Prediction Guard token and url as an environmental variable.\n", + "os.environ[\"PREDICTIONGUARD_API_KEY\"] = \"\"\n", + "os.environ[\"PREDICTIONGUARD_URL\"] = \"\"\n", + "\n", + "# Or set your Prediction Guard token and url when initializing the PredictionGuard class.\n", + "client = PredictionGuard(\n", + " api_key=\"\",\n", + " url=\"\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Basic Responses" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response = client.responses.create(\n", + " model=\"Hermes-3-Llama-3.1-8B\",\n", + " input=\"Tell me a funny joke about pirates.\"\n", + ")\n", + "\n", + "print(json.dumps(\n", + " response,\n", + " sort_keys=True,\n", + " indent=4,\n", + " separators=(',', ': ')\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Response with Images" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " # Text to use for inference.\n", + " \"type\": \"input_text\",\n", + " \"text\": \"What's in this image?\"\n", + " },\n", + " {\n", + " # Image to use for inference. Accepts image urls, files, and base64 encoded images, all under the \"image_url\" param.\n", + " \"type\": \"input_image\",\n", + " \"image_url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"\n", + " }\n", + " ]\n", + " },\n", + "]\n", + "\n", + "image_response = client.responses.create(\n", + " model=\"Qwen2.5-VL-7B-Instruct\",\n", + " input=input\n", + ")\n", + "\n", + "print(json.dumps(\n", + " image_response,\n", + " sort_keys=True,\n", + " indent=4,\n", + " separators=(',', ': ')\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### List Responses Models" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_list = client.responses.list_models()\n", + "\n", + "print(model_list)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/predictionguard/client.py b/predictionguard/client.py index 6697a71..c0a84fc 100644 --- a/predictionguard/client.py +++ b/predictionguard/client.py @@ -4,6 +4,7 @@ from typing import Optional, Union from .src.audio import Audio +from .src.responses import Responses from .src.chat import Chat from .src.completions import Completions from .src.detokenize import Detokenize @@ -16,13 +17,15 @@ from .src.toxicity import Toxicity from .src.pii import Pii from .src.injection import Injection +from .src.mcp_servers import MCPServers +from .src.mcp_tools import MCPTools from .src.models import Models from .version import __version__ __all__ = [ - "PredictionGuard", "Chat", "Completions", "Embeddings", - "Audio", "Documents", "Rerank", "Tokenize", "Translate", - "Detokenize", "Factuality", "Toxicity", "Pii", "Injection", + "PredictionGuard", "Responses", "Chat", "Completions", "Embeddings", + "Audio", "Documents", "Rerank", "Tokenize", "Translate", "Detokenize", + "Factuality", "Toxicity", "Pii", "Injection", "MCPServers", "MCPTools", "Models" ] @@ -81,11 +84,14 @@ def __init__( self._connect_client() # Pass Prediction Guard class variables to inner classes + self.responses: Responses = Responses(self.api_key, self.url, self.timeout) + """Responses allows for the usage of LLMs intended for agentic usages.""" + self.chat: Chat = Chat(self.api_key, self.url, self.timeout) - """Chat generates chat completions based on a conversation history""" + """Chat generates chat completions based on a conversation history.""" self.completions: Completions = Completions(self.api_key, self.url, self.timeout) - """Completions generates text completions based on the provided input""" + """Completions generates text completions based on the provided input.""" self.embeddings: Embeddings = Embeddings(self.api_key, self.url, self.timeout) """Embedding generates chat completions based on a conversation history.""" @@ -120,6 +126,12 @@ def __init__( self.detokenize: Detokenize = Detokenize(self.api_key, self.url, self.timeout) """Detokenizes generates text for input tokens.""" + self.mcp_servers: MCPServers = MCPServers(self.api_key, self.url, self.timeout) + """MCPServers lists all the MCP servers available in the Prediction Guard API.""" + + self.mcp_tools: MCPTools = MCPTools(self.api_key, self.url, self.timeout) + """MCPTools lists all the MCP tools available in the Prediction Guard API.""" + self.models: Models = Models(self.api_key, self.url, self.timeout) """Models lists all of the models available in the Prediction Guard API.""" diff --git a/predictionguard/src/chat.py b/predictionguard/src/chat.py index dcc8f7f..e6087d3 100644 --- a/predictionguard/src/chat.py +++ b/predictionguard/src/chat.py @@ -122,7 +122,7 @@ def create( :param model: The ID(s) of the model to use. :param messages: The content of the call, an array of dictionaries containing a role and content. :param input: A dictionary containing the PII and injection arguments. - :param output: A dictionary containing the consistency, factuality, and toxicity arguments. + :param output: A dictionary containing the factuality, and toxicity arguments. :param frequency_penalty: The frequency penalty to use. :param logit_bias: The logit bias to use. :param max_completion_tokens: The maximum amount of tokens the model should return. diff --git a/predictionguard/src/completions.py b/predictionguard/src/completions.py index 5531f1b..454587c 100644 --- a/predictionguard/src/completions.py +++ b/predictionguard/src/completions.py @@ -71,7 +71,7 @@ def create( :param model: The ID(s) of the model to use. :param prompt: The prompt(s) to generate completions for. :param input: A dictionary containing the PII and injection arguments. - :param output: A dictionary containing the consistency, factuality, and toxicity arguments. + :param output: A dictionary containing the factuality, and toxicity arguments. :param echo: A boolean indicating whether to echo the prompt(s) to the output. :param frequency_penalty: The frequency penalty to use. :param logit_bias: The logit bias to use. diff --git a/predictionguard/src/mcp_servers.py b/predictionguard/src/mcp_servers.py new file mode 100644 index 0000000..1663df3 --- /dev/null +++ b/predictionguard/src/mcp_servers.py @@ -0,0 +1,85 @@ +import requests +from typing import Any, Dict, Optional + +from ..version import __version__ + + +class MCPServers: + """ + MCPServers lists all the MCP servers available in the Prediction Guard API. + + Usage:: + + import os + import json + + from predictionguard import PredictionGuard + + # Set your Prediction Guard token and url as an environmental variable. + os.environ["PREDICTIONGUARD_API_KEY"] = "" + os.environ["PREDICTIONGUARD_URL"] = "" + + # Or set your Prediction Guard token and url when initializing the PredictionGuard class. + client = PredictionGuard( + api_key="", + url="" + ) + + response = client.mcp_servers.list() + + print(json.dumps( + response, + sort_keys=True, + indent=4, + separators=(",", ": ") + )) + """ + + def __init__(self, api_key, url, timeout): + self.api_key = api_key + self.url = url + self.timeout = timeout + + def list(self) -> Dict[str, Any]: + """ + Creates a mcp_servers list request in the Prediction Guard REST API. + + :return: A dictionary containing the metadata of all the MCP servers. + """ + + # Run _list_mcp_servers + choices = self._list_mcp_servers() + return choices + + def _list_mcp_servers(self): + """ + Function to list available MCP servers. + """ + + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + self.api_key, + "User-Agent": "Prediction Guard Python Client: " + __version__, + } + + response = requests.request( + "GET", self.url + "/mcp_servers", headers=headers, timeout=self.timeout + ) + + if response.status_code == 200: + ret = response.json() + return ret + elif response.status_code == 429: + raise ValueError( + "Could not connect to Prediction Guard API. " + "Too many requests, rate limit or quota exceeded." + ) + else: + # Check if there is a JSON body in the response. Read that in, + # print out the error field in the JSON body, and raise an exception. + err = "" + try: + err = response.json()["error"] + except Exception: + pass + raise ValueError("Could not check for injection. " + err) diff --git a/predictionguard/src/mcp_tools.py b/predictionguard/src/mcp_tools.py new file mode 100644 index 0000000..0e1bd57 --- /dev/null +++ b/predictionguard/src/mcp_tools.py @@ -0,0 +1,85 @@ +import requests +from typing import Any, Dict, Optional + +from ..version import __version__ + + +class MCPTools: + """ + MCPTools lists all the MCP tools available in the Prediction Guard API. + + Usage:: + + import os + import json + + from predictionguard import PredictionGuard + + # Set your Prediction Guard token and url as an environmental variable. + os.environ["PREDICTIONGUARD_API_KEY"] = "" + os.environ["PREDICTIONGUARD_URL"] = "" + + # Or set your Prediction Guard token and url when initializing the PredictionGuard class. + client = PredictionGuard( + api_key="", + url="" + ) + + response = client.mcp_tools.list() + + print(json.dumps( + response, + sort_keys=True, + indent=4, + separators=(",", ": ") + )) + """ + + def __init__(self, api_key, url, timeout): + self.api_key = api_key + self.url = url + self.timeout = timeout + + def list(self) -> Dict[str, Any]: + """ + Creates a mcp_tools list request in the Prediction Guard REST API. + + :return: A dictionary containing the metadata of all the MCP tools. + """ + + # Run _list_mcp_tools + choices = self._list_mcp_tools() + return choices + + def _list_mcp_tools(self): + """ + Function to list available MCP tools. + """ + + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + self.api_key, + "User-Agent": "Prediction Guard Python Client: " + __version__, + } + + response = requests.request( + "GET", self.url + "/mcp_tools", headers=headers, timeout=self.timeout + ) + + if response.status_code == 200: + ret = response.json() + return ret + elif response.status_code == 429: + raise ValueError( + "Could not connect to Prediction Guard API. " + "Too many requests, rate limit or quota exceeded." + ) + else: + # Check if there is a JSON body in the response. Read that in, + # print out the error field in the JSON body, and raise an exception. + err = "" + try: + err = response.json()["error"] + except Exception: + pass + raise ValueError("Could not check for injection. " + err) diff --git a/predictionguard/src/models.py b/predictionguard/src/models.py index 7793a52..575bb55 100644 --- a/predictionguard/src/models.py +++ b/predictionguard/src/models.py @@ -48,7 +48,7 @@ def list(self, capability: Optional[str] = "") -> Dict[str, Any]: :return: A dictionary containing the metadata of all the models. """ - # Run _check_injection + # Run _list_models choices = self._list_models(capability) return choices diff --git a/predictionguard/src/responses.py b/predictionguard/src/responses.py new file mode 100644 index 0000000..c42f78f --- /dev/null +++ b/predictionguard/src/responses.py @@ -0,0 +1,316 @@ +import re +import json +import os +import base64 + +import requests +from typing import Any, Dict, List, Optional, Union +import urllib.request +import urllib.parse +import uuid + +from ..version import __version__ + + +class Responses: + """ + Responses allows for the usage of LLMs intended for agentic usages. + + Usage:: + + import os + import json + + from predictionguard import PredictionGuard + + # Set your Prediction Guard token and url as an environmental variable. + os.environ["PREDICTIONGUARD_API_KEY"] = "" + os.environ["PREDICTIONGUARD_URL"] = "" + + # Or set your Prediction Guard token and url when initializing the PredictionGuard class. + client = PredictionGuard( + api_key="", + url="" + ) + + input = [ + { + "role": "system", + "content": "You are a helpful assistant that provide clever and sometimes funny responses.", + }, + { + "role": "user", + "content": "What's up!" + }, + { + "role": "assistant", + "content": "Well, technically vertically out from the center of the earth." + }, + { + "role": "user", + "content": "Haha. Good one." + } + ] + + result = client.responses.create( + model="gpt-oss-120b", + input=input + ) + + print(json.dumps( + response, + sort_keys=True, + indent=4, + separators=(",", ": ") + )) + """ + + def __init__(self, api_key, url, timeout): + self.api_key = api_key + self.url = url + self.timeout = timeout + + def create( + self, + model: str, + input: Union[ + str, List[ + Dict[str, Any] + ] + ], + max_output_tokens: Optional[int] = None, + max_tool_calls: Optional[int] = None, + parallel_tool_calls: Optional[bool] = None, + reasoning: Optional[Dict[str, str]] = None, + safeguards: Optional[Dict[str, Any]] = None, + stream: Optional[bool] = False, + temperature: Optional[float] = None, + tool_choice: Optional[Union[ + str, Dict[ + str, Dict[str, str] + ] + ]] = None, + tools: Optional[List[Dict[str, Union[str, Dict[str, str]]]]] = None, + top_p: Optional[float] = None, + ) -> Dict[str, Any]: + """ + Creates a chat request for the Prediction Guard /chat API. + + :param model: The ID(s) of the model to use. + :param input: The content of the call, an array of dictionaries containing a role and content. + :param max_output_tokens: The maximum amount of tokens the model should return. + :param max_tool_calls: The maximum amount of tool calls the model can perform. + :param parallel_tool_calls: The parallel tool calls to use. + :param reasoning: How much effort for model to use for reasoning. Only supported by reasoning models. + :param safeguards: A dictionary containing the PII, injection, factuality, and toxicity arguments. + :param stream: Option to stream the API response + :param temperature: The consistency of the model responses to the same prompt. The higher it is set, the more consistent. + :param tool_choice: The tool choice to use. + :param tools: Options to pass to the tool choice. + :param top_p: The sampling for the model to use. + :return: A dictionary containing the responses response. + """ + + # Create a list of tuples, each containing all the parameters for + # a call to _generate_response + args = ( + model, + input, + max_output_tokens, + max_tool_calls, + parallel_tool_calls, + reasoning, + safeguards, + stream, + temperature, + tool_choice, + tools, + top_p, + ) + + # Run _generate_response + output = self._generate_response(*args) + + return output + + def _generate_response( + self, + model, + input, + max_output_tokens, + max_tool_calls, + parallel_tool_calls, + reasoning, + safeguards, + stream, + temperature, + tool_choice, + tools, + top_p, + ): + """ + Function to generate a single responses response. + """ + + def return_dict(url, headers, payload, timeout): + response = requests.request( + "POST", url + "/responses", headers=headers, data=payload, timeout=timeout + ) + # If the request was successful, print the proxies. + if response.status_code == 200: + ret = response.json() + return ret + elif response.status_code == 429: + raise ValueError( + "Could not connect to Prediction Guard API. " + "Too many requests, rate limit or quota exceeded." + ) + else: + # Check if there is a JSON body in the response. Read that in, + # then print out the error field in the JSON body, and raise an exception. + err = "" + try: + err = response.json()["error"] + except Exception: + pass + raise ValueError("Could not make prediction. " + err) + + def stream_generator(url, headers, payload, stream, timeout): + with requests.post( + url + "/responses", + headers=headers, + data=payload, + stream=stream, + timeout=timeout, + ) as response: + response.raise_for_status() + + for line in response.iter_lines(): + if line: + decoded_line = line.decode("utf-8") + formatted_return = ( + "{" + (decoded_line.replace("data", '"data"', 1)) + "}" + ) + try: + dict_return = json.loads(formatted_return) + except json.decoder.JSONDecodeError: + pass + else: + try: + dict_return["data"]["choices"][0]["delta"]["content"] + except KeyError: + pass + else: + yield dict_return + + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + self.api_key, + "User-Agent": "Prediction Guard Python Client: " + __version__, + } + + if type(input) is list: + for inpt in input: + if type(inpt["content"]) is list: + for entry in inpt["content"]: + if entry["type"] == "input_image": + image_data = entry["image_url"] + if stream: + raise ValueError( + "Streaming is not currently supported when using vision." + ) + else: + image_url_check = urllib.parse.urlparse(image_data) + data_uri_pattern = re.compile( + r'^data:([a-zA-Z0-9!#$&-^_]+/[a-zA-Z0-9!#$&-^_]+)?(;base64)?,.*$' + ) + + if os.path.exists(image_data): + with open(image_data, "rb") as image_file: + image_input = base64.b64encode( + image_file.read() + ).decode("utf-8") + + image_data_uri = "data:image/jpeg;base64," + image_input + + elif re.fullmatch(r"[A-Za-z0-9+/]*={0,2}", image_data): + if ( + base64.b64encode( + base64.b64decode(image_data) + ).decode("utf-8") + == image_data + ): + image_input = image_data + image_data_uri = "data:image/jpeg;base64," + image_input + + elif image_url_check.scheme in ( + "http", + "https", + "ftp", + ): + temp_image = uuid.uuid4().hex + ".jpg" + urllib.request.urlretrieve(image_data, temp_image) + with open(temp_image, "rb") as image_file: + image_input = base64.b64encode( + image_file.read() + ).decode("utf-8") + os.remove(temp_image) + image_data_uri = "data:image/jpeg;base64," + image_input + + elif data_uri_pattern.match(image_data): + image_data_uri = image_data + + else: + raise ValueError( + "Please enter a valid base64 encoded image, image file, image URL, or data URI." + ) + + entry["image_url"] = image_data_uri + elif entry["type"] == "input_text": + continue + + payload_dict = { + "model": model, + "input": input, + "max_output_tokens": max_output_tokens, + "max_tool_calls": max_tool_calls, + "parallel_tool_calls": parallel_tool_calls, + "reasoning": reasoning, + "safeguards": safeguards, + "stream": stream, + "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, + "top_p": top_p, + } + + payload = json.dumps(payload_dict) + + if stream: + return stream_generator(self.url, headers, payload, stream, self.timeout) + + else: + return return_dict(self.url, headers, payload, self.timeout) + + def list_models(self, capability: Optional[str] = "responses") -> List[str]: + # Get the list of current models. + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + self.api_key, + "User-Agent": "Prediction Guard Python Client: " + __version__ + } + + if capability != "responses" and capability != "responses-with-image": + raise ValueError( + "Please enter a valid model type (responses or responses-with-image)." + ) + else: + model_path = "/models/" + capability + + response = requests.request("GET", self.url + model_path, headers=headers, timeout=self.timeout) + + response_list = [] + for model in response.json()["data"]: + response_list.append(model["id"]) + + return response_list diff --git a/predictionguard/version.py b/predictionguard/version.py index ab01394..45b05c5 100644 --- a/predictionguard/version.py +++ b/predictionguard/version.py @@ -1,2 +1,2 @@ # Setting the package version -__version__ = "2.10.1" +__version__ = "2.11.0" diff --git a/pyproject.toml b/pyproject.toml index 1e08170..14f9b12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ Issues = "https://github.com/predictionguard/python-client/issues" [project.optional-dependencies] dev = [ "pytest>=9.0.2", - "ruff>=0.15.1", + "ruff==0.15.2", "sphinx==9.1", - "sphinx-autodoc-typehints==3.6.2", + "sphinx-autodoc-typehints==3.7.0", "sphinx_rtd_theme>=3.1.0", "black>=26.1.0", ] diff --git a/tests/test_mcp_servers.py b/tests/test_mcp_servers.py new file mode 100644 index 0000000..bde1294 --- /dev/null +++ b/tests/test_mcp_servers.py @@ -0,0 +1,10 @@ +from predictionguard import PredictionGuard + + +def test_mcp_servers_list(): + test_client = PredictionGuard() + + response = test_client.mcp_servers.list() + + assert len(response["data"]) > 0 + assert type(response["data"][0]["server_label"]) is str diff --git a/tests/test_mcp_tools.py b/tests/test_mcp_tools.py new file mode 100644 index 0000000..2f31e1a --- /dev/null +++ b/tests/test_mcp_tools.py @@ -0,0 +1,11 @@ +from predictionguard import PredictionGuard + + +def test_mcp_tools_list(): + test_client = PredictionGuard() + + response = test_client.mcp_tools.list() + + assert len(response["data"]) > 0 + first_key = list(response["data"].keys())[0] + assert type(response["data"][first_key][0]["id"]) is str diff --git a/tests/test_responses.py b/tests/test_responses.py new file mode 100644 index 0000000..218ee63 --- /dev/null +++ b/tests/test_responses.py @@ -0,0 +1,188 @@ +import os +import base64 + +import pytest + +from predictionguard import PredictionGuard + + +def test_responses_create(): + test_client = PredictionGuard() + + response = test_client.responses.create( + model=os.environ["TEST_RESPONSES_MODEL"], + input=[ + {"role": "system", "content": "You are a helpful chatbot."}, + {"role": "user", "content": "Tell me a joke."}, + ], + ) + + assert len(response["output"][0]["content"]) > 0 + + +def test_responses_create_string(): + test_client = PredictionGuard() + + response = test_client.responses.create( + model=os.environ["TEST_RESPONSES_MODEL"], + input="Tell me a joke" + ) + + assert len(response["output"][0]["content"]) > 0 + + +# def test_responses_create_stream(): +# test_client = PredictionGuard() +# +# response_list = [] +# for res in test_client.responses.create( +# model=os.environ["TEST_RESPONSES_MODEL"], +# input=[ +# {"role": "system", "content": "You are a helpful chatbot."}, +# {"role": "user", "content": "Tell me a joke."}, +# ], +# stream=True, +# ): +# response_list.append(res) +# +# assert len(response_list) > 1 + + +def test_responses_create_vision_image_file(): + test_client = PredictionGuard() + + response = test_client.responses.create( + model=os.environ["TEST_VISION_MODEL"], + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + { + "type": "input_image", + "image_url": "fixtures/test_image1.jpeg", + }, + ], + } + ], + ) + + assert len(response["output"][0]["content"]) > 0 + + +def test_responses_create_vision_image_url(): + test_client = PredictionGuard() + + response = test_client.responses.create( + model=os.environ["TEST_VISION_MODEL"], + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + { + "type": "input_image", + "image_url": "https://farm4.staticflickr.com/3300/3497460990_11dfb95dd1_z.jpg" + }, + ], + } + ], + ) + + assert len(response["output"][0]["content"]) > 0 + + +def test_responses_create_vision_image_b64(): + test_client = PredictionGuard() + + with open("fixtures/test_image1.jpeg", "rb") as image_file: + b64_image = base64.b64encode(image_file.read()).decode("utf-8") + + response = test_client.responses.create( + model=os.environ["TEST_VISION_MODEL"], + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + {"type": "input_image", "image_url": b64_image}, + ], + } + ], + ) + + assert len(response["output"][0]["content"]) > 0 + + +def test_responses_create_vision_data_uri(): + test_client = PredictionGuard() + + with open("fixtures/test_image1.jpeg", "rb") as image_file: + b64_image = base64.b64encode(image_file.read()).decode("utf-8") + + data_uri = "data:image/jpeg;base64," + b64_image + + response = test_client.responses.create( + model=os.environ["TEST_VISION_MODEL"], + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + {"type": "input_image", "image_url": data_uri}, + ], + } + ], + ) + + assert len(response["output"][0]["content"]) > 0 + + +def test_responses_create_vision_stream_fail(): + test_client = PredictionGuard() + + streaming_error = "Streaming is not currently supported when using vision." + + response_list = [] + with pytest.raises(ValueError, match=streaming_error): + for res in test_client.responses.create( + model=os.environ["TEST_VISION_MODEL"], + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + { + "type": "input_image", + "image_url": "fixtures/test_image1.jpeg", + }, + ], + } + ], + stream=True, + ): + response_list.append(res) + + +def test_responses_create_tool_call(): + test_client = PredictionGuard() + + response = test_client.responses.create( + model=os.environ["TEST_RESPONSES_MODEL"], + input=[ + {"role": "system", "content": "You are a helpful chatbot."}, + {"role": "user", "content": "Tell me a joke."}, + ], + + ) + + assert len(response["output"][0]["content"]) > 0 + + +def test_responses_list_models(): + test_client = PredictionGuard() + + response = test_client.responses.list_models() + + assert len(response) > 0 + assert type(response[0]) is str diff --git a/uv.lock b/uv.lock index 7c278bf..fe27843 100644 --- a/uv.lock +++ b/uv.lock @@ -318,9 +318,9 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=26.1.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "requests", specifier = ">=2.32.5" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.2" }, { name = "sphinx", marker = "extra == 'dev'", specifier = "==9.1" }, - { name = "sphinx-autodoc-typehints", marker = "extra == 'dev'", specifier = "==3.6.2" }, + { name = "sphinx-autodoc-typehints", marker = "extra == 'dev'", specifier = "==3.7.0" }, { name = "sphinx-rtd-theme", marker = "extra == 'dev'", specifier = ">=3.1.0" }, { name = "tabulate", specifier = ">=0.9.0" }, ] @@ -406,27 +406,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, - { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, - { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, - { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, - { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, - { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] [[package]] @@ -468,14 +468,14 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.6.2" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/51/6603ed3786a2d52366c66f49bc8afb31ae5c0e33d4a156afcb38d2bac62c/sphinx_autodoc_typehints-3.6.2.tar.gz", hash = "sha256:3d37709a21b7b765ad6e20a04ecefcb229b9eb0007cb24f6ebaa8a4576ea7f06", size = 37574, upload-time = "2026-01-02T21:25:28.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/54/319628e24e98102e6f73cf6e5f345324a94d9adc18d456193b84f2ac1608/sphinx_autodoc_typehints-3.7.0.tar.gz", hash = "sha256:f7c536f4c0a729324cfebfaa3787c80ca14d08817952153e6da4e971c5306c20", size = 41344, upload-time = "2026-02-23T20:54:58.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6a/877e8a6ea52fc86d88ce110ebcfe4f8474ff590d8a8d322909673af3da7b/sphinx_autodoc_typehints-3.6.2-py3-none-any.whl", hash = "sha256:9e70bee1f487b087c83ba0f4949604a4630bee396e263a324aae1dc4268d2c0f", size = 20853, upload-time = "2026-01-02T21:25:26.853Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2c/ded433effc5943195a64e18404fd46c195aca559cd9bede1ae73f8bbf67f/sphinx_autodoc_typehints-3.7.0-py3-none-any.whl", hash = "sha256:ad0c9759bac0c7462768003bb57e7bb853c75b74d603e818511c56c10931e72f", size = 21558, upload-time = "2026-02-23T20:54:57.015Z" }, ] [[package]] From 616ae9d2bf2aa96d092868183ab1d127e7a3fffa Mon Sep 17 00:00:00 2001 From: jmansdorfer Date: Tue, 24 Feb 2026 14:29:26 -0500 Subject: [PATCH 2/5] adding responses model var to actions --- .github/workflows/main.yml | 1 + .github/workflows/pr.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b9065a..138fddc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,7 @@ jobs: PREDICTIONGUARD_API_KEY: ${{ secrets.PREDICTIONGUARD_API_KEY }} PREDICTIONGUARD_URL: ${{ vars.PREDICTIONGUARD_URL }} TEST_CHAT_MODEL: ${{ vars.TEST_CHAT_MODEL }} + TEST_RESPONSES_MODEL: ${{ vars.TEST_RESPONSES_MODEL }} TEST_TEXT_EMBEDDINGS_MODEL: ${{ vars.TEST_TEXT_EMBEDDINGS_MODEL }} TEST_MULTIMODAL_EMBEDDINGS_MODEL: ${{ vars.TEST_MULTIMODAL_EMBEDDINGS_MODEL }} TEST_VISION_MODEL: ${{ vars.TEST_VISION_MODEL }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a742261..64efd9f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -31,6 +31,7 @@ jobs: PREDICTIONGUARD_API_KEY: ${{ secrets.PREDICTIONGUARD_API_KEY }} PREDICTIONGUARD_URL: ${{ vars.PREDICTIONGUARD_URL }} TEST_CHAT_MODEL: ${{ vars.TEST_CHAT_MODEL }} + TEST_RESPONSES_MODEL: ${{ vars.TEST_RESPONSES_MODEL }} TEST_TEXT_EMBEDDINGS_MODEL: ${{ vars.TEST_TEXT_EMBEDDINGS_MODEL }} TEST_MULTIMODAL_EMBEDDINGS_MODEL: ${{ vars.TEST_MULTIMODAL_EMBEDDINGS_MODEL }} TEST_VISION_MODEL: ${{ vars.TEST_VISION_MODEL }} From 6b559f0308ac6eae22520f19c05ee9dbee96fb4a Mon Sep 17 00:00:00 2001 From: jmansdorfer Date: Mon, 2 Mar 2026 10:01:28 -0500 Subject: [PATCH 3/5] non-docs review changes --- predictionguard/src/responses.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/predictionguard/src/responses.py b/predictionguard/src/responses.py index c42f78f..9ed747a 100644 --- a/predictionguard/src/responses.py +++ b/predictionguard/src/responses.py @@ -4,7 +4,7 @@ import base64 import requests -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union import urllib.request import urllib.parse import uuid @@ -86,9 +86,8 @@ def create( stream: Optional[bool] = False, temperature: Optional[float] = None, tool_choice: Optional[Union[ - str, Dict[ - str, Dict[str, str] - ] + Literal["auto", "required", "none"], + Dict[str, Union[str, Dict[str, str]]] ]] = None, tools: Optional[List[Dict[str, Union[str, Dict[str, str]]]]] = None, top_p: Optional[float] = None, @@ -103,7 +102,7 @@ def create( :param parallel_tool_calls: The parallel tool calls to use. :param reasoning: How much effort for model to use for reasoning. Only supported by reasoning models. :param safeguards: A dictionary containing the PII, injection, factuality, and toxicity arguments. - :param stream: Option to stream the API response + :param stream: Option to stream the API response. Not currently supported. :param temperature: The consistency of the model responses to the same prompt. The higher it is set, the more consistent. :param tool_choice: The tool choice to use. :param tools: Options to pass to the tool choice. From e60e9e536c46566e12f3cacc6e9a5ab665fd607e Mon Sep 17 00:00:00 2001 From: jmansdorfer Date: Mon, 2 Mar 2026 12:14:27 -0500 Subject: [PATCH 4/5] fixing test to account for empty mcp response when no mcp is available --- tests/test_mcp_servers.py | 11 ++++++++++- tests/test_mcp_tools.py | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_mcp_servers.py b/tests/test_mcp_servers.py index bde1294..59eda27 100644 --- a/tests/test_mcp_servers.py +++ b/tests/test_mcp_servers.py @@ -1,3 +1,7 @@ +import warnings + +import pytest + from predictionguard import PredictionGuard @@ -6,5 +10,10 @@ def test_mcp_servers_list(): response = test_client.mcp_servers.list() - assert len(response["data"]) > 0 + if len(response["data"]) == 0: + assert response["object"] == "list" + assert response["data"] == [] + warnings.warn(pytest.PytestWarning("No servers available — data is empty, skipping content checks")) + return + assert type(response["data"][0]["server_label"]) is str diff --git a/tests/test_mcp_tools.py b/tests/test_mcp_tools.py index 2f31e1a..14e16cf 100644 --- a/tests/test_mcp_tools.py +++ b/tests/test_mcp_tools.py @@ -1,3 +1,7 @@ +import warnings + +import pytest + from predictionguard import PredictionGuard @@ -6,6 +10,12 @@ def test_mcp_tools_list(): response = test_client.mcp_tools.list() - assert len(response["data"]) > 0 + if len(response["data"]) == 0: + # Verify it's a valid empty response, then warn instead of fail + assert response["object"] == "list" + assert response["data"] == {} + warnings.warn(pytest.PytestWarning("No tools available — data is empty, skipping content checks")) + return + first_key = list(response["data"].keys())[0] assert type(response["data"][first_key][0]["id"]) is str From 21baa5d20d575925ccec7f2864661667e584255f Mon Sep 17 00:00:00 2001 From: jmansdorfer Date: Mon, 2 Mar 2026 13:53:23 -0500 Subject: [PATCH 5/5] upgrading deps --- pyproject.toml | 4 ++-- uv.lock | 52 +++++++++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 14f9b12..208c157 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ Issues = "https://github.com/predictionguard/python-client/issues" [project.optional-dependencies] dev = [ "pytest>=9.0.2", - "ruff==0.15.2", + "ruff==0.15.4", "sphinx==9.1", - "sphinx-autodoc-typehints==3.7.0", + "sphinx-autodoc-typehints==3.9.4", "sphinx_rtd_theme>=3.1.0", "black>=26.1.0", ] diff --git a/uv.lock b/uv.lock index fe27843..0afc2c2 100644 --- a/uv.lock +++ b/uv.lock @@ -318,9 +318,9 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=26.1.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "requests", specifier = ">=2.32.5" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.4" }, { name = "sphinx", marker = "extra == 'dev'", specifier = "==9.1" }, - { name = "sphinx-autodoc-typehints", marker = "extra == 'dev'", specifier = "==3.7.0" }, + { name = "sphinx-autodoc-typehints", marker = "extra == 'dev'", specifier = "==3.9.4" }, { name = "sphinx-rtd-theme", marker = "extra == 'dev'", specifier = ">=3.1.0" }, { name = "tabulate", specifier = ">=0.9.0" }, ] @@ -406,27 +406,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, - { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, - { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, - { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, - { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, - { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, ] [[package]] @@ -468,14 +468,14 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.7.0" +version = "3.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/54/319628e24e98102e6f73cf6e5f345324a94d9adc18d456193b84f2ac1608/sphinx_autodoc_typehints-3.7.0.tar.gz", hash = "sha256:f7c536f4c0a729324cfebfaa3787c80ca14d08817952153e6da4e971c5306c20", size = 41344, upload-time = "2026-02-23T20:54:58.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/58/377d2044f2fb539f8368404948da96651500492f510042750208ae49aecc/sphinx_autodoc_typehints-3.9.4.tar.gz", hash = "sha256:ffc2ce3d2c62cd7873e199162db395eccfff32c9ea14ce9c152c0a384bd0d29d", size = 62468, upload-time = "2026-03-02T16:16:32.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/2c/ded433effc5943195a64e18404fd46c195aca559cd9bede1ae73f8bbf67f/sphinx_autodoc_typehints-3.7.0-py3-none-any.whl", hash = "sha256:ad0c9759bac0c7462768003bb57e7bb853c75b74d603e818511c56c10931e72f", size = 21558, upload-time = "2026-02-23T20:54:57.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/e9/fea859e08be8093ef686b46e8bce494d694154c7180124e8f105bcd9817c/sphinx_autodoc_typehints-3.9.4-py3-none-any.whl", hash = "sha256:bd34fd41f12334c994ad53aac9deff2327a7ef736115ea61d0fc2398e8a3e127", size = 34474, upload-time = "2026-03-02T16:16:29.804Z" }, ] [[package]]