diff --git a/generated/.openapi-generator/FILES b/generated/.openapi-generator/FILES index 27394696..fc763407 100644 --- a/generated/.openapi-generator/FILES +++ b/generated/.openapi-generator/FILES @@ -76,6 +76,11 @@ docs/TextModeConfiguration.md docs/TextRecognitionResult.md docs/UserApi.md docs/VerbEnum.md +docs/VerdictEnum.md +docs/VlmVerification.md +docs/VlmVerificationCost.md +docs/VlmVerificationResult.md +docs/VlmVerificationsApi.md docs/WebhookAction.md docs/WebhookActionRequest.md git_push.sh @@ -92,6 +97,7 @@ groundlight_openapi_client/api/month_to_date_account_info_api.py groundlight_openapi_client/api/notes_api.py groundlight_openapi_client/api/priming_groups_api.py groundlight_openapi_client/api/user_api.py +groundlight_openapi_client/api/vlm_verifications_api.py groundlight_openapi_client/api_client.py groundlight_openapi_client/apis/__init__.py groundlight_openapi_client/configuration.py @@ -162,6 +168,10 @@ groundlight_openapi_client/model/status_enum.py groundlight_openapi_client/model/text_mode_configuration.py groundlight_openapi_client/model/text_recognition_result.py groundlight_openapi_client/model/verb_enum.py +groundlight_openapi_client/model/verdict_enum.py +groundlight_openapi_client/model/vlm_verification.py +groundlight_openapi_client/model/vlm_verification_cost.py +groundlight_openapi_client/model/vlm_verification_result.py groundlight_openapi_client/model/webhook_action.py groundlight_openapi_client/model/webhook_action_request.py groundlight_openapi_client/model_utils.py diff --git a/generated/README.md b/generated/README.md index 40630eba..b353ad26 100644 --- a/generated/README.md +++ b/generated/README.md @@ -146,6 +146,7 @@ Class | Method | HTTP request | Description *PrimingGroupsApi* | [**get_priming_group**](docs/PrimingGroupsApi.md#get_priming_group) | **GET** /v1/priming-groups/{id} | *PrimingGroupsApi* | [**list_priming_groups**](docs/PrimingGroupsApi.md#list_priming_groups) | **GET** /v1/priming-groups | *UserApi* | [**who_am_i**](docs/UserApi.md#who_am_i) | **GET** /v1/me | +*VlmVerificationsApi* | [**submit_vlm_verification**](docs/VlmVerificationsApi.md#submit_vlm_verification) | **POST** /v1/vlm-verifications | ## Documentation For Models @@ -215,6 +216,10 @@ Class | Method | HTTP request | Description - [TextModeConfiguration](docs/TextModeConfiguration.md) - [TextRecognitionResult](docs/TextRecognitionResult.md) - [VerbEnum](docs/VerbEnum.md) + - [VerdictEnum](docs/VerdictEnum.md) + - [VlmVerification](docs/VlmVerification.md) + - [VlmVerificationCost](docs/VlmVerificationCost.md) + - [VlmVerificationResult](docs/VlmVerificationResult.md) - [WebhookAction](docs/WebhookAction.md) - [WebhookActionRequest](docs/WebhookActionRequest.md) diff --git a/generated/docs/VerdictEnum.md b/generated/docs/VerdictEnum.md new file mode 100644 index 00000000..50008dfa --- /dev/null +++ b/generated/docs/VerdictEnum.md @@ -0,0 +1,12 @@ +# VerdictEnum + +* `YES` - YES * `NO` - NO * `UNSURE` - UNSURE + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**value** | **str** | * `YES` - YES * `NO` - NO * `UNSURE` - UNSURE | must be one of ["YES", "NO", "UNSURE", ] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/generated/docs/VlmVerification.md b/generated/docs/VlmVerification.md new file mode 100644 index 00000000..019d59d7 --- /dev/null +++ b/generated/docs/VlmVerification.md @@ -0,0 +1,19 @@ +# VlmVerification + +Response shape for POST /v1/vlm-verifications. + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **str** | | [readonly] +**type** | **str** | | [readonly] +**created_at** | **datetime** | | [readonly] +**query** | **str** | | +**model_id** | **str** | | +**result** | [**VlmVerificationResult**](VlmVerificationResult.md) | | +**cost** | [**VlmVerificationCost**](VlmVerificationCost.md) | | +**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/generated/docs/VlmVerificationCost.md b/generated/docs/VlmVerificationCost.md new file mode 100644 index 00000000..f432dc34 --- /dev/null +++ b/generated/docs/VlmVerificationCost.md @@ -0,0 +1,14 @@ +# VlmVerificationCost + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**input_tokens** | **int, none_type** | | +**output_tokens** | **int, none_type** | | +**total_cost_usd** | **float, none_type** | | +**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/generated/docs/VlmVerificationResult.md b/generated/docs/VlmVerificationResult.md new file mode 100644 index 00000000..a06b3879 --- /dev/null +++ b/generated/docs/VlmVerificationResult.md @@ -0,0 +1,14 @@ +# VlmVerificationResult + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**verdict** | [**VerdictEnum**](VerdictEnum.md) | | +**confidence** | **float** | | +**reasoning** | **str** | | +**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/generated/docs/VlmVerificationsApi.md b/generated/docs/VlmVerificationsApi.md new file mode 100644 index 00000000..6e35dc5f --- /dev/null +++ b/generated/docs/VlmVerificationsApi.md @@ -0,0 +1,100 @@ +# groundlight_openapi_client.VlmVerificationsApi + +All URIs are relative to *https://api.groundlight.ai/device-api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**submit_vlm_verification**](VlmVerificationsApi.md#submit_vlm_verification) | **POST** /v1/vlm-verifications | + + +# **submit_vlm_verification** +> VlmVerification submit_vlm_verification(media, query) + + + + Submit one or more images for VLM-based alert verification. Send everything as `multipart/form-data`: one to eight `media` parts, plus a `query` field and an optional `model_id` field. The `query` describes what each image is and what to look for — the server makes no assumptions about the images' meaning. Images are presented to the model labeled `Image 1`, `Image 2`, ... in upload order, so the query can reference them (e.g. \"Image 1 is the full frame; image 2 is the cropped ROI ...\"). (Video parts are planned but not yet supported and are rejected.) Requires `ENABLE_BEDROCK_VLM_ACCESS` (enabled for Standard_Internal and SciDuck accounts) and accepted terms of service. ```bash curl https://api.groundlight.ai/device-api/v1/vlm-verifications \\ -F \"media=@full_frame.jpg;type=image/jpeg\" \\ -F \"media=@roi.jpg;type=image/jpeg\" \\ -F \"query=Image 1 is the full camera frame; image 2 is the cropped region a detector flagged. Is there really a fire?\" \\ -F \"model_id=gpt-5.4\" ``` + +### Example + +* Api Key Authentication (ApiToken): + +```python +import time +import groundlight_openapi_client +from groundlight_openapi_client.api import vlm_verifications_api +from groundlight_openapi_client.model.vlm_verification import VlmVerification +from pprint import pprint +# Defining the host is optional and defaults to https://api.groundlight.ai/device-api +# See configuration.py for a list of all supported configuration parameters. +configuration = groundlight_openapi_client.Configuration( + host = "https://api.groundlight.ai/device-api" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +# Configure API key authorization: ApiToken +configuration.api_key['ApiToken'] = 'YOUR_API_KEY' + +# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed +# configuration.api_key_prefix['ApiToken'] = 'Bearer' + +# Enter a context with an instance of the API client +with groundlight_openapi_client.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = vlm_verifications_api.VlmVerificationsApi(api_client) + media = [ + open('/path/to/file', 'rb'), + ] # [file_type] | One or more images (common formats: JPEG, PNG, WEBP). Video is not yet supported. + query = "query_example" # str | Natural-language prompt describing the media and what to verify. + model_id = "model_id_example" # str | Friendly model alias (e.g. 'gpt-5.4', 'claude-sonnet-4.5'). Defaults to the server default. (optional) + + # example passing only required values which don't have defaults set + try: + api_response = api_instance.submit_vlm_verification(media, query) + pprint(api_response) + except groundlight_openapi_client.ApiException as e: + print("Exception when calling VlmVerificationsApi->submit_vlm_verification: %s\n" % e) + + # example passing only required values which don't have defaults set + # and optional values + try: + api_response = api_instance.submit_vlm_verification(media, query, model_id=model_id) + pprint(api_response) + except groundlight_openapi_client.ApiException as e: + print("Exception when calling VlmVerificationsApi->submit_vlm_verification: %s\n" % e) +``` + + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **media** | **[file_type]**| One or more images (common formats: JPEG, PNG, WEBP). Video is not yet supported. | + **query** | **str**| Natural-language prompt describing the media and what to verify. | + **model_id** | **str**| Friendly model alias (e.g. 'gpt-5.4', 'claude-sonnet-4.5'). Defaults to the server default. | [optional] + +### Return type + +[**VlmVerification**](VlmVerification.md) + +### Authorization + +[ApiToken](../README.md#ApiToken) + +### HTTP request headers + + - **Content-Type**: multipart/form-data + - **Accept**: application/json + + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/generated/groundlight_openapi_client/api/vlm_verifications_api.py b/generated/groundlight_openapi_client/api/vlm_verifications_api.py new file mode 100644 index 00000000..b1e9fb9c --- /dev/null +++ b/generated/groundlight_openapi_client/api/vlm_verifications_api.py @@ -0,0 +1,153 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 + +from groundlight_openapi_client.api_client import ApiClient, Endpoint as _Endpoint +from groundlight_openapi_client.model_utils import ( # noqa: F401 + check_allowed_values, + check_validations, + date, + datetime, + file_type, + none_type, + validate_and_convert_types, +) +from groundlight_openapi_client.model.vlm_verification import VlmVerification + + +class VlmVerificationsApi(object): + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None): + if api_client is None: + api_client = ApiClient() + self.api_client = api_client + self.submit_vlm_verification_endpoint = _Endpoint( + settings={ + "response_type": (VlmVerification,), + "auth": ["ApiToken"], + "endpoint_path": "/v1/vlm-verifications", + "operation_id": "submit_vlm_verification", + "http_method": "POST", + "servers": None, + }, + params_map={ + "all": [ + "media", + "query", + "model_id", + ], + "required": [ + "media", + "query", + ], + "nullable": [], + "enum": [], + "validation": [ + "media", + ], + }, + root_map={ + "validations": { + ("media",): { + "max_items": 8, + "min_items": 1, + }, + }, + "allowed_values": {}, + "openapi_types": { + "media": ([file_type],), + "query": (str,), + "model_id": (str,), + }, + "attribute_map": { + "media": "media", + "query": "query", + "model_id": "model_id", + }, + "location_map": { + "media": "form", + "query": "form", + "model_id": "form", + }, + "collection_format_map": { + "media": "csv", + }, + }, + headers_map={"accept": ["application/json"], "content_type": ["multipart/form-data"]}, + api_client=api_client, + ) + + def submit_vlm_verification(self, media, query, **kwargs): + """submit_vlm_verification # noqa: E501 + + Submit one or more images for VLM-based alert verification. Send everything as `multipart/form-data`: one to eight `media` parts, plus a `query` field and an optional `model_id` field. The `query` describes what each image is and what to look for — the server makes no assumptions about the images' meaning. Images are presented to the model labeled `Image 1`, `Image 2`, ... in upload order, so the query can reference them (e.g. \"Image 1 is the full frame; image 2 is the cropped ROI ...\"). (Video parts are planned but not yet supported and are rejected.) Requires `ENABLE_BEDROCK_VLM_ACCESS` (enabled for Standard_Internal and SciDuck accounts) and accepted terms of service. ```bash curl https://api.groundlight.ai/device-api/v1/vlm-verifications \\ -F \"media=@full_frame.jpg;type=image/jpeg\" \\ -F \"media=@roi.jpg;type=image/jpeg\" \\ -F \"query=Image 1 is the full camera frame; image 2 is the cropped region a detector flagged. Is there really a fire?\" \\ -F \"model_id=gpt-5.4\" ``` # noqa: E501 + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + + >>> thread = api.submit_vlm_verification(media, query, async_req=True) + >>> result = thread.get() + + Args: + media ([file_type]): One or more images (common formats: JPEG, PNG, WEBP). Video is not yet supported. + query (str): Natural-language prompt describing the media and what to verify. + + Keyword Args: + model_id (str): Friendly model alias (e.g. 'gpt-5.4', 'claude-sonnet-4.5'). Defaults to the server default.. [optional] + _return_http_data_only (bool): response data without head status + code and headers. Default is True. + _preload_content (bool): if False, the urllib3.HTTPResponse object + will be returned without reading/decoding response data. + Default is True. + _request_timeout (int/float/tuple): timeout setting for this request. If + one number provided, it will be total request timeout. It can also + be a pair (tuple) of (connection, read) timeouts. + Default is None. + _check_input_type (bool): specifies if type checking + should be done one the data sent to the server. + Default is True. + _check_return_type (bool): specifies if type checking + should be done one the data received from the server. + Default is True. + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _content_type (str/None): force body content-type. + Default is None and content-type will be predicted by allowed + content-types and body. + _host_index (int/None): specifies the index of the server + that we want to use. + Default is read from the configuration. + async_req (bool): execute request asynchronously + + Returns: + VlmVerification + If the method is called asynchronously, returns the request + thread. + """ + kwargs["async_req"] = kwargs.get("async_req", False) + kwargs["_return_http_data_only"] = kwargs.get("_return_http_data_only", True) + kwargs["_preload_content"] = kwargs.get("_preload_content", True) + kwargs["_request_timeout"] = kwargs.get("_request_timeout", None) + kwargs["_check_input_type"] = kwargs.get("_check_input_type", True) + kwargs["_check_return_type"] = kwargs.get("_check_return_type", True) + kwargs["_spec_property_naming"] = kwargs.get("_spec_property_naming", False) + kwargs["_content_type"] = kwargs.get("_content_type") + kwargs["_host_index"] = kwargs.get("_host_index") + kwargs["media"] = media + kwargs["query"] = query + return self.submit_vlm_verification_endpoint.call_with_http_info(**kwargs) diff --git a/generated/groundlight_openapi_client/apis/__init__.py b/generated/groundlight_openapi_client/apis/__init__.py index 4d24cda6..63b7497a 100644 --- a/generated/groundlight_openapi_client/apis/__init__.py +++ b/generated/groundlight_openapi_client/apis/__init__.py @@ -24,3 +24,4 @@ from groundlight_openapi_client.api.notes_api import NotesApi from groundlight_openapi_client.api.priming_groups_api import PrimingGroupsApi from groundlight_openapi_client.api.user_api import UserApi +from groundlight_openapi_client.api.vlm_verifications_api import VlmVerificationsApi diff --git a/generated/groundlight_openapi_client/model/verdict_enum.py b/generated/groundlight_openapi_client/model/verdict_enum.py new file mode 100644 index 00000000..e41359b8 --- /dev/null +++ b/generated/groundlight_openapi_client/model/verdict_enum.py @@ -0,0 +1,284 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 + +from groundlight_openapi_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from groundlight_openapi_client.exceptions import ApiAttributeError + + +class VerdictEnum(ModelSimple): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + ("value",): { + "YES": "YES", + "NO": "NO", + "UNSURE": "UNSURE", + }, + } + + validations = {} + + additional_properties_type = None + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + "value": (str,), + } + + @cached_property + def discriminator(): + return None + + attribute_map = {} + + read_only_vars = set() + + _composed_schemas = None + + required_properties = set([ + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + ]) + + @convert_js_args_to_python_args + def __init__(self, *args, **kwargs): + """VerdictEnum - a model defined in OpenAPI + + Note that value can be passed either in args or in kwargs, but not in both. + + Args: + args[0] (str): * `YES` - YES * `NO` - NO * `UNSURE` - UNSURE., must be one of ["YES", "NO", "UNSURE", ] # noqa: E501 + + Keyword Args: + value (str): * `YES` - YES * `NO` - NO * `UNSURE` - UNSURE., must be one of ["YES", "NO", "UNSURE", ] # noqa: E501 + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + # required up here when default value is not given + _path_to_item = kwargs.pop("_path_to_item", ()) + + if "value" in kwargs: + value = kwargs.pop("value") + elif args: + args = list(args) + value = args.pop(0) + else: + raise ApiTypeError( + "value is required, but not passed in args or kwargs and doesn't have default", + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self.value = value + if kwargs: + raise ApiTypeError( + "Invalid named arguments=%s passed to %s. Remove those invalid named arguments." + % ( + kwargs, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, *args, **kwargs): + """VerdictEnum - a model defined in OpenAPI + + Note that value can be passed either in args or in kwargs, but not in both. + + Args: + args[0] (str): * `YES` - YES * `NO` - NO * `UNSURE` - UNSURE., must be one of ["YES", "NO", "UNSURE", ] # noqa: E501 + + Keyword Args: + value (str): * `YES` - YES * `NO` - NO * `UNSURE` - UNSURE., must be one of ["YES", "NO", "UNSURE", ] # noqa: E501 + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + # required up here when default value is not given + _path_to_item = kwargs.pop("_path_to_item", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if "value" in kwargs: + value = kwargs.pop("value") + elif args: + args = list(args) + value = args.pop(0) + else: + raise ApiTypeError( + "value is required, but not passed in args or kwargs and doesn't have default", + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self.value = value + if kwargs: + raise ApiTypeError( + "Invalid named arguments=%s passed to %s. Remove those invalid named arguments." + % ( + kwargs, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + return self diff --git a/generated/groundlight_openapi_client/model/vlm_verification.py b/generated/groundlight_openapi_client/model/vlm_verification.py new file mode 100644 index 00000000..b28a2d7f --- /dev/null +++ b/generated/groundlight_openapi_client/model/vlm_verification.py @@ -0,0 +1,317 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 + +from groundlight_openapi_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from groundlight_openapi_client.exceptions import ApiAttributeError + + +def lazy_import(): + from groundlight_openapi_client.model.vlm_verification_cost import VlmVerificationCost + from groundlight_openapi_client.model.vlm_verification_result import VlmVerificationResult + + globals()["VlmVerificationCost"] = VlmVerificationCost + globals()["VlmVerificationResult"] = VlmVerificationResult + + +class VlmVerification(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = {} + + validations = {} + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + lazy_import() + return ( + bool, + date, + datetime, + dict, + float, + int, + list, + str, + none_type, + ) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + lazy_import() + return { + "id": (str,), # noqa: E501 + "type": (str,), # noqa: E501 + "created_at": (datetime,), # noqa: E501 + "query": (str,), # noqa: E501 + "model_id": (str,), # noqa: E501 + "result": (VlmVerificationResult,), # noqa: E501 + "cost": (VlmVerificationCost,), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + "id": "id", # noqa: E501 + "type": "type", # noqa: E501 + "created_at": "created_at", # noqa: E501 + "query": "query", # noqa: E501 + "model_id": "model_id", # noqa: E501 + "result": "result", # noqa: E501 + "cost": "cost", # noqa: E501 + } + + read_only_vars = { + "id", # noqa: E501 + "type", # noqa: E501 + "created_at", # noqa: E501 + } + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, id, type, created_at, query, model_id, result, cost, *args, **kwargs): # noqa: E501 + """VlmVerification - a model defined in OpenAPI + + Args: + id (str): + type (str): + created_at (datetime): + query (str): + model_id (str): + result (VlmVerificationResult): + cost (VlmVerificationCost): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.id = id + self.type = type + self.created_at = created_at + self.query = query + self.model_id = model_id + self.result = result + self.cost = cost + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = set([ + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + ]) + + @convert_js_args_to_python_args + def __init__(self, query, model_id, result, cost, *args, **kwargs): # noqa: E501 + """VlmVerification - a model defined in OpenAPI + + query (str): + model_id (str): + result (VlmVerificationResult): + cost (VlmVerificationCost): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.query = query + self.model_id = model_id + self.result = result + self.cost = cost + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError( + f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + "class with read only attributes." + ) diff --git a/generated/groundlight_openapi_client/model/vlm_verification_cost.py b/generated/groundlight_openapi_client/model/vlm_verification_cost.py new file mode 100644 index 00000000..0acc190a --- /dev/null +++ b/generated/groundlight_openapi_client/model/vlm_verification_cost.py @@ -0,0 +1,295 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 + +from groundlight_openapi_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from groundlight_openapi_client.exceptions import ApiAttributeError + + +class VlmVerificationCost(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = {} + + validations = {} + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + return ( + bool, + date, + datetime, + dict, + float, + int, + list, + str, + none_type, + ) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + "input_tokens": ( + int, + none_type, + ), # noqa: E501 + "output_tokens": ( + int, + none_type, + ), # noqa: E501 + "total_cost_usd": ( + float, + none_type, + ), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + "input_tokens": "input_tokens", # noqa: E501 + "output_tokens": "output_tokens", # noqa: E501 + "total_cost_usd": "total_cost_usd", # noqa: E501 + } + + read_only_vars = {} + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, input_tokens, output_tokens, total_cost_usd, *args, **kwargs): # noqa: E501 + """VlmVerificationCost - a model defined in OpenAPI + + Args: + input_tokens (int, none_type): + output_tokens (int, none_type): + total_cost_usd (float, none_type): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.input_tokens = input_tokens + self.output_tokens = output_tokens + self.total_cost_usd = total_cost_usd + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = set([ + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + ]) + + @convert_js_args_to_python_args + def __init__(self, input_tokens, output_tokens, total_cost_usd, *args, **kwargs): # noqa: E501 + """VlmVerificationCost - a model defined in OpenAPI + + Args: + input_tokens (int, none_type): + output_tokens (int, none_type): + total_cost_usd (float, none_type): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.input_tokens = input_tokens + self.output_tokens = output_tokens + self.total_cost_usd = total_cost_usd + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError( + f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + "class with read only attributes." + ) diff --git a/generated/groundlight_openapi_client/model/vlm_verification_result.py b/generated/groundlight_openapi_client/model/vlm_verification_result.py new file mode 100644 index 00000000..acec3aac --- /dev/null +++ b/generated/groundlight_openapi_client/model/vlm_verification_result.py @@ -0,0 +1,299 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 + +from groundlight_openapi_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from groundlight_openapi_client.exceptions import ApiAttributeError + + +def lazy_import(): + from groundlight_openapi_client.model.verdict_enum import VerdictEnum + + globals()["VerdictEnum"] = VerdictEnum + + +class VlmVerificationResult(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = {} + + validations = { + ("confidence",): { + "inclusive_maximum": 1.0, + "inclusive_minimum": 0.0, + }, + } + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + lazy_import() + return ( + bool, + date, + datetime, + dict, + float, + int, + list, + str, + none_type, + ) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + lazy_import() + return { + "verdict": (VerdictEnum,), # noqa: E501 + "confidence": (float,), # noqa: E501 + "reasoning": (str,), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + "verdict": "verdict", # noqa: E501 + "confidence": "confidence", # noqa: E501 + "reasoning": "reasoning", # noqa: E501 + } + + read_only_vars = {} + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, verdict, confidence, reasoning, *args, **kwargs): # noqa: E501 + """VlmVerificationResult - a model defined in OpenAPI + + Args: + verdict (VerdictEnum): + confidence (float): + reasoning (str): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.verdict = verdict + self.confidence = confidence + self.reasoning = reasoning + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = set([ + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + ]) + + @convert_js_args_to_python_args + def __init__(self, verdict, confidence, reasoning, *args, **kwargs): # noqa: E501 + """VlmVerificationResult - a model defined in OpenAPI + + Args: + verdict (VerdictEnum): + confidence (float): + reasoning (str): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.verdict = verdict + self.confidence = confidence + self.reasoning = reasoning + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError( + f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + "class with read only attributes." + ) diff --git a/generated/groundlight_openapi_client/models/__init__.py b/generated/groundlight_openapi_client/models/__init__.py index f35cb11e..9f8727d8 100644 --- a/generated/groundlight_openapi_client/models/__init__.py +++ b/generated/groundlight_openapi_client/models/__init__.py @@ -74,5 +74,9 @@ from groundlight_openapi_client.model.text_mode_configuration import TextModeConfiguration from groundlight_openapi_client.model.text_recognition_result import TextRecognitionResult from groundlight_openapi_client.model.verb_enum import VerbEnum +from groundlight_openapi_client.model.verdict_enum import VerdictEnum +from groundlight_openapi_client.model.vlm_verification import VlmVerification +from groundlight_openapi_client.model.vlm_verification_cost import VlmVerificationCost +from groundlight_openapi_client.model.vlm_verification_result import VlmVerificationResult from groundlight_openapi_client.model.webhook_action import WebhookAction from groundlight_openapi_client.model.webhook_action_request import WebhookActionRequest diff --git a/generated/model.py b/generated/model.py index a9c0f7f3..0c82b2bc 100644 --- a/generated/model.py +++ b/generated/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: public-api.yaml -# timestamp: 2026-06-16T00:34:34+00:00 +# timestamp: 2026-06-29T20:54:06+00:00 from __future__ import annotations @@ -505,6 +505,30 @@ class Label(str, Enum): UNCLEAR = "UNCLEAR" +class VerdictEnum(str, Enum): + """ + * `YES` - YES + * `NO` - NO + * `UNSURE` - UNSURE + """ + + YES = "YES" + NO = "NO" + UNSURE = "UNSURE" + + +class VlmVerificationCost(BaseModel): + input_tokens: Optional[int] = Field(...) + output_tokens: Optional[int] = Field(...) + total_cost_usd: Optional[float] = Field(...) + + +class VlmVerificationResult(BaseModel): + verdict: VerdictEnum + confidence: confloat(ge=0.0, le=1.0) + reasoning: str + + class AllNotes(BaseModel): """ Serializes all notes for a given detector, grouped by type as listed in UserProfile.NoteCategoryChoices @@ -724,6 +748,20 @@ class RuleRequest(BaseModel): webhook_action: Optional[List[WebhookActionRequest]] = None +class VlmVerification(BaseModel): + """ + Response shape for POST /v1/vlm-verifications. + """ + + id: str + type: str + created_at: datetime + query: str + model_id: str + result: VlmVerificationResult + cost: VlmVerificationCost + + class PaginatedRuleList(BaseModel): count: int = Field(..., examples=[123]) next: Optional[AnyUrl] = Field(None, examples=["http://api.example.org/accounts/?page=4"]) diff --git a/generated/test/test_verdict_enum.py b/generated/test/test_verdict_enum.py new file mode 100644 index 00000000..f847ec71 --- /dev/null +++ b/generated/test/test_verdict_enum.py @@ -0,0 +1,35 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import sys +import unittest + +import groundlight_openapi_client +from groundlight_openapi_client.model.verdict_enum import VerdictEnum + + +class TestVerdictEnum(unittest.TestCase): + """VerdictEnum unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testVerdictEnum(self): + """Test VerdictEnum""" + # FIXME: construct object with mandatory attributes with example values + # model = VerdictEnum() # noqa: E501 + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/generated/test/test_vlm_verification.py b/generated/test/test_vlm_verification.py new file mode 100644 index 00000000..7e0d85aa --- /dev/null +++ b/generated/test/test_vlm_verification.py @@ -0,0 +1,40 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import sys +import unittest + +import groundlight_openapi_client +from groundlight_openapi_client.model.vlm_verification_cost import VlmVerificationCost +from groundlight_openapi_client.model.vlm_verification_result import VlmVerificationResult + +globals()["VlmVerificationCost"] = VlmVerificationCost +globals()["VlmVerificationResult"] = VlmVerificationResult +from groundlight_openapi_client.model.vlm_verification import VlmVerification + + +class TestVlmVerification(unittest.TestCase): + """VlmVerification unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testVlmVerification(self): + """Test VlmVerification""" + # FIXME: construct object with mandatory attributes with example values + # model = VlmVerification() # noqa: E501 + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/generated/test/test_vlm_verification_cost.py b/generated/test/test_vlm_verification_cost.py new file mode 100644 index 00000000..f2e02d7b --- /dev/null +++ b/generated/test/test_vlm_verification_cost.py @@ -0,0 +1,35 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import sys +import unittest + +import groundlight_openapi_client +from groundlight_openapi_client.model.vlm_verification_cost import VlmVerificationCost + + +class TestVlmVerificationCost(unittest.TestCase): + """VlmVerificationCost unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testVlmVerificationCost(self): + """Test VlmVerificationCost""" + # FIXME: construct object with mandatory attributes with example values + # model = VlmVerificationCost() # noqa: E501 + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/generated/test/test_vlm_verification_result.py b/generated/test/test_vlm_verification_result.py new file mode 100644 index 00000000..396179e6 --- /dev/null +++ b/generated/test/test_vlm_verification_result.py @@ -0,0 +1,38 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import sys +import unittest + +import groundlight_openapi_client +from groundlight_openapi_client.model.verdict_enum import VerdictEnum + +globals()["VerdictEnum"] = VerdictEnum +from groundlight_openapi_client.model.vlm_verification_result import VlmVerificationResult + + +class TestVlmVerificationResult(unittest.TestCase): + """VlmVerificationResult unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testVlmVerificationResult(self): + """Test VlmVerificationResult""" + # FIXME: construct object with mandatory attributes with example values + # model = VlmVerificationResult() # noqa: E501 + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/generated/test/test_vlm_verifications_api.py b/generated/test/test_vlm_verifications_api.py new file mode 100644 index 00000000..b0789205 --- /dev/null +++ b/generated/test/test_vlm_verifications_api.py @@ -0,0 +1,32 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.18.2 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import unittest + +import groundlight_openapi_client +from groundlight_openapi_client.api.vlm_verifications_api import VlmVerificationsApi # noqa: E501 + + +class TestVlmVerificationsApi(unittest.TestCase): + """VlmVerificationsApi unit test stubs""" + + def setUp(self): + self.api = VlmVerificationsApi() # noqa: E501 + + def tearDown(self): + pass + + def test_submit_vlm_verification(self): + """Test case for submit_vlm_verification""" + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/spec/public-api.yaml b/spec/public-api.yaml index 494c1f2f..ad960bff 100644 --- a/spec/public-api.yaml +++ b/spec/public-api.yaml @@ -962,6 +962,69 @@ paths: responses: '204': description: No response body + /v1/vlm-verifications: + post: + operationId: Submit VLM verification + description: |2 + + Submit one or more images for VLM-based alert verification. + + Send everything as `multipart/form-data`: one to eight `media` parts, plus a + `query` field and an optional `model_id` field. + + The `query` describes what each image is and what to look for — the server makes + no assumptions about the images' meaning. Images are presented to the model + labeled `Image 1`, `Image 2`, ... in upload order, so the query can reference + them (e.g. "Image 1 is the full frame; image 2 is the cropped ROI ..."). + + (Video parts are planned but not yet supported and are rejected.) + + Requires `ENABLE_BEDROCK_VLM_ACCESS` (enabled for Standard_Internal and SciDuck accounts) and accepted terms of service. + + ```bash + curl https://api.groundlight.ai/device-api/v1/vlm-verifications \ + -F "media=@full_frame.jpg;type=image/jpeg" \ + -F "media=@roi.jpg;type=image/jpeg" \ + -F "query=Image 1 is the full camera frame; image 2 is the cropped region a detector flagged. Is there really a fire?" \ + -F "model_id=gpt-5.4" + ``` + tags: + - vlm-verifications + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + media: + type: array + items: + type: string + format: binary + minItems: 1 + maxItems: 8 + description: 'One or more images (common formats: JPEG, PNG, WEBP). + Video is not yet supported.' + query: + type: string + description: Natural-language prompt describing the media and what + to verify. + model_id: + type: string + description: Friendly model alias (e.g. 'gpt-5.4', 'claude-sonnet-4.5'). + Defaults to the server default. + required: + - media + - query + security: + - ApiToken: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/VlmVerification' + description: '' components: schemas: AccountMonthToDateInfo: @@ -2338,6 +2401,79 @@ components: - 'YES' - 'NO' - UNCLEAR + VerdictEnum: + enum: + - 'YES' + - 'NO' + - UNSURE + type: string + description: |- + * `YES` - YES + * `NO` - NO + * `UNSURE` - UNSURE + VlmVerification: + type: object + description: Response shape for POST /v1/vlm-verifications. + properties: + id: + type: string + readOnly: true + type: + type: string + readOnly: true + created_at: + type: string + format: date-time + readOnly: true + query: + type: string + model_id: + type: string + result: + $ref: '#/components/schemas/VlmVerificationResult' + cost: + $ref: '#/components/schemas/VlmVerificationCost' + required: + - cost + - created_at + - id + - model_id + - query + - result + - type + VlmVerificationCost: + type: object + properties: + input_tokens: + type: integer + nullable: true + output_tokens: + type: integer + nullable: true + total_cost_usd: + type: number + format: double + nullable: true + required: + - input_tokens + - output_tokens + - total_cost_usd + VlmVerificationResult: + type: object + properties: + verdict: + $ref: '#/components/schemas/VerdictEnum' + confidence: + type: number + format: double + maximum: 1.0 + minimum: 0.0 + reasoning: + type: string + required: + - confidence + - reasoning + - verdict securitySchemes: ApiToken: name: x-api-token diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 82fa5fa9..5a4a0cec 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -180,6 +180,7 @@ def wrapper(*args, **kwargs): "Image Queries", "ML Pipelines & Priming", "Notes", + "VLM Verification", "Utilities", ] @@ -232,6 +233,10 @@ def wrapper(*args, **kwargs): "create_priming_group": "ML Pipelines & Priming", "get_priming_group": "ML Pipelines & Priming", "delete_priming_group": "ML Pipelines & Priming", + # VLM Verification + # NOTE: via the CLI only a single image (a filepath) is supported for now — the `media` + # Union collapses to `str`. The Python API (ExperimentalApi.ask_vlm_verify) accepts up to 8. + "ask_vlm_verify": "VLM Verification", # Utilities "edge_base_url": "Utilities", "get_raw_headers": "Utilities", diff --git a/src/groundlight/experimental_api.py b/src/groundlight/experimental_api.py index 547b23d2..8deb93e0 100644 --- a/src/groundlight/experimental_api.py +++ b/src/groundlight/experimental_api.py @@ -10,7 +10,7 @@ from http import HTTPStatus from io import BufferedReader, BytesIO from pathlib import Path -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union from urllib.parse import urlparse, urlunparse import requests @@ -19,6 +19,7 @@ from groundlight_openapi_client.api.edge_api import EdgeApi from groundlight_openapi_client.api.notes_api import NotesApi from groundlight_openapi_client.api.priming_groups_api import PrimingGroupsApi +from groundlight_openapi_client.api.vlm_verifications_api import VlmVerificationsApi from groundlight_openapi_client.exceptions import ApiException, NotFoundException from groundlight_openapi_client.model.patched_detector_request import PatchedDetectorRequest from groundlight_openapi_client.model.priming_group_creation_input_request import PrimingGroupCreationInputRequest @@ -30,6 +31,7 @@ PaginatedMLPipelineList, PaginatedPrimingGroupList, PrimingGroup, + VlmVerification, ) from urllib3.response import HTTPResponse @@ -40,6 +42,9 @@ from .client import DEFAULT_REQUEST_TIMEOUT, Groundlight, GroundlightClientError +# Maximum number of media items accepted by ask_vlm_verify (mirrors the server's limit). +MAX_VLM_MEDIA_ITEMS = 8 + class ExperimentalApi(Groundlight): # pylint: disable=too-many-public-methods,too-many-instance-attributes def __init__( @@ -84,6 +89,7 @@ def __init__( self.detector_group_api = DetectorGroupsApi(self.api_client) self.detector_reset_api = DetectorResetApi(self.api_client) self.priming_groups_api = PrimingGroupsApi(self.api_client) + self.vlm_verifications_api = VlmVerificationsApi(self.api_client) # API client for fetching Edge models self._edge_model_download_api = EdgeApi(self.api_client) @@ -164,6 +170,101 @@ def create_note( response = requests.post(url, headers=headers, data=data, files=files, params=params) # type: ignore response.raise_for_status() # Raise an exception for error status codes + def ask_vlm_verify( # pylint: disable=too-many-locals + self, + media: Union[ + np.ndarray, + str, + bytes, + Image.Image, + BytesIO, + BufferedReader, + List[Union[np.ndarray, str, bytes, Image.Image, BytesIO, BufferedReader]], + ], + query: str, + model_id: Optional[str] = None, + timeout: float = DEFAULT_REQUEST_TIMEOUT, + ) -> VlmVerification: + """Verify one or more images against a natural-language query using a cloud VLM. + + Calls the Groundlight ``POST /v1/vlm-verifications`` endpoint. The VLM runs in the + Groundlight cloud (AWS Bedrock) — no local inference. This is intended for alert + verification (a structured ``YES`` / ``NO`` / ``UNSURE`` verdict), and each call is + relatively expensive, so use it deliberately rather than as a general-purpose VLM. + + The server makes no assumptions about what the images are — your ``query`` should + describe them. Images are presented to the model labeled ``Image 1``, ``Image 2``, + ... in the order given, so the query can refer to them. + + **Example usage**:: + + gl = ExperimentalApi() + + # Single image + result = gl.ask_vlm_verify(frame, query="Is there a fire in this image?") + if result.result.verdict == "YES": + emit_alert() + + # Full frame + cropped ROI — describe each in the query + result = gl.ask_vlm_verify( + media=[full_frame, roi_crop], + query="Image 1 is the full camera frame; image 2 is the cropped region " + "a detector flagged. Is there really a fire?", + ) + print(result.result.confidence, result.result.reasoning, result.cost.total_cost_usd) + + :param media: One image or a list of up to 8 images. Accepted formats per image: + + - filename (string) of a JPEG or PNG file (``".jpg"``, ``".jpeg"``, ``".png"``) + - raw bytes, BytesIO, or BufferedReader — sent as-is; the server decodes and + normalises to JPEG regardless of the declared content type, so PNG/WEBP bytes + all work + - numpy array (H, W, 3) in BGR order (OpenCV convention) — converted to JPEG + before sending + - PIL Image — converted to JPEG before sending + + :param query: Natural-language prompt describing the media and what to verify, + e.g. ``"Is there a fire visible in the image? Reason step by step."`` + :param model_id: Friendly alias of the VLM to use (e.g. ``"gpt-5.4"``, + ``"claude-sonnet-4.5"``). The server is the source of truth for the supported + aliases and the default; passing an unrecognised alias returns HTTP 400. Omit + to use the server-configured default. + :param timeout: Request timeout in seconds. + + :return: A :class:`VlmVerification` with nested ``result`` (``verdict`` — + ``"YES"`` / ``"NO"`` / ``"UNSURE"`` — ``confidence``, ``reasoning``) and ``cost`` + (``input_tokens``, ``output_tokens``, ``total_cost_usd``). + :raises ValueError: If zero or more than ``MAX_VLM_MEDIA_ITEMS`` (8) images are supplied. + :raises groundlight_openapi_client.exceptions.ApiException: On non-2xx response (400 for + an invalid model alias or undecodable image bytes; 502 if the upstream VLM is unavailable). + """ + # Normalise: single image → list + if not isinstance(media, list): + media = [media] + if not media: + raise ValueError("ask_vlm_verify requires at least one media item.") + if len(media) > MAX_VLM_MEDIA_ITEMS: + raise ValueError(f"ask_vlm_verify supports at most {MAX_VLM_MEDIA_ITEMS} media items.") + + # Encode each item to a byte stream. numpy/PIL → JPEG; bytes/BytesIO/BufferedReader → + # passed through (the server calls ensure_jpeg_format and validates by decoding, so any + # common image format works). The `.name` gives the generated multipart client a filename + # and content-type for each `media` part. + media_files = [] + for i, img in enumerate(media): + stream = parse_supported_image_types(img) + stream.name = f"image_{i+1}.jpg" # one-indexed naming for VLM Verifications API + media_files.append(stream) + + kwargs: Dict[str, Any] = {"_request_timeout": timeout} + if model_id: + kwargs["model_id"] = model_id + + # Use the generated client (same pattern as submit_image_query): it handles the + # multipart upload, auth, and base URL, then we validate into the pydantic model. + raw = self.vlm_verifications_api.submit_vlm_verification(media_files, query, **kwargs) + return VlmVerification.model_validate(raw.to_dict()) + def reset_detector(self, detector: Union[str, Detector]) -> None: """ Removes all image queries and training data for the given detector. This effectively resets diff --git a/src/groundlight/images.py b/src/groundlight/images.py index d618144c..83e5f24d 100644 --- a/src/groundlight/images.py +++ b/src/groundlight/images.py @@ -1,7 +1,7 @@ # pylint: disable=deprecated-module from io import BufferedReader, BytesIO, IOBase from pathlib import Path -from typing import Union +from typing import Optional, Union from groundlight.optional_imports import Image, np @@ -23,12 +23,15 @@ class ByteStreamWrapper(IOBase): when we want to retry accessing the file without having to re-open it. """ - def __init__(self, data: Union[BufferedReader, BytesIO, bytes]) -> None: + def __init__(self, data: Union[BufferedReader, BytesIO, bytes], name: Optional[str] = None) -> None: super().__init__() if isinstance(data, (BufferedReader, BytesIO)): self._data = data.read() else: self._data = data + # An optional filename. Multipart file uploads via the generated client read `.name` + # to derive each part's filename and content-type. + self.name = name def read(self) -> bytes: return self._data diff --git a/test/unit/test_ask_vlm_verify.py b/test/unit/test_ask_vlm_verify.py new file mode 100644 index 00000000..0ce0555d --- /dev/null +++ b/test/unit/test_ask_vlm_verify.py @@ -0,0 +1,118 @@ +"""Unit tests for ExperimentalApi.ask_vlm_verify. + +These mock the generated client's HTTP transport (RESTClientObject.request) so they exercise +the real request assembly (multipart parts, URL, auth) and response parsing without a live +server — the same layer the rest of the SDK's request plumbing runs through. +""" + +from unittest.mock import MagicMock, patch + +import pytest +from groundlight import ExperimentalApi +from groundlight.experimental_api import MAX_VLM_MEDIA_ITEMS +from groundlight.optional_imports import MISSING_NUMPY, np +from groundlight_openapi_client.rest import RESTClientObject +from model import VlmVerification + +# Minimal valid-looking JPEG bytes for tests that don't exercise image encoding. +_FAKE_JPEG = b"\xff\xd8\xff\xe0" + b"\x00" * 16 + +_RESPONSE_JSON = ( + b'{"id":"vlmv_test123","type":"vlm_verification","created_at":"2025-06-17T00:00:00Z",' + b'"query":"Is there a fire?","model_id":"gpt-5.4",' + b'"result":{"verdict":"YES","confidence":0.92,"reasoning":"Flames visible."},' + b'"cost":{"input_tokens":400,"output_tokens":80,"total_cost_usd":0.0015}}' +) + + +@pytest.fixture(name="gl") +def experimental_fixture(monkeypatch) -> ExperimentalApi: + monkeypatch.setenv("GROUNDLIGHT_API_TOKEN", "api_fake_test_token") + with patch.object(ExperimentalApi, "_verify_connectivity", return_value=None): + return ExperimentalApi(endpoint="http://test-server/device-api/") + + +def _capturing_transport(captured: dict, data: bytes = _RESPONSE_JSON): + """Return a fake RESTClientObject.request that records its args and returns a 201.""" + + def fake_request(self, method, url, **kwargs): # noqa: ANN001 + captured["method"] = method + captured["url"] = url + captured["post_params"] = kwargs.get("post_params") + captured["headers"] = kwargs.get("headers") + resp = MagicMock() + resp.status = 201 + resp.data = data + resp.getheader = lambda name, default=None: ("application/json" if name.lower() == "content-type" else default) + resp.getheaders = lambda: {"Content-Type": "application/json"} + return resp + + return fake_request + + +def test_returns_vlm_verification(gl: ExperimentalApi): + """Server JSON is parsed into the generated VlmVerification model (nested result/cost).""" + captured: dict = {} + with patch.object(RESTClientObject, "request", _capturing_transport(captured)): + result = gl.ask_vlm_verify(media=_FAKE_JPEG, query="Is there a fire?") + + assert isinstance(result, VlmVerification) + assert result.id == "vlmv_test123" + assert result.result.verdict == "YES" + assert result.result.confidence == pytest.approx(0.92) + assert result.result.reasoning == "Flames visible." + assert result.cost.total_cost_usd == pytest.approx(0.0015) + assert result.cost.input_tokens is not None + # Sanity: it went through the generated client to the right endpoint. + assert captured["method"] == "POST" + assert captured["url"].endswith("/device-api/v1/vlm-verifications") + + +@pytest.mark.skipif(MISSING_NUMPY, reason="Needs numpy") +def test_numpy_image_encoded_as_jpeg_multipart(gl: ExperimentalApi): + """A numpy array is converted to JPEG and sent as a multipart 'media' part.""" + captured: dict = {} + with patch.object(RESTClientObject, "request", _capturing_transport(captured)): + gl.ask_vlm_verify(media=np.zeros((480, 640, 3), dtype=np.uint8), query="Is there a fire?") + + media_parts = [p for p in captured["post_params"] if p[0] == "media"] + assert len(media_parts) == 1 + filename, data, ctype = media_parts[0][1] + assert ctype == "image/jpeg" + assert len(data) > 0 + + +def test_query_and_model_id_sent_as_form_fields(gl: ExperimentalApi): + """query and model_id go in the multipart body, not the URL, so the prompt can't leak into logs.""" + captured: dict = {} + with patch.object(RESTClientObject, "request", _capturing_transport(captured)): + gl.ask_vlm_verify(media=_FAKE_JPEG, query="Is there a fire?", model_id="nova-pro") + + fields = {p[0]: p[1] for p in captured["post_params"]} + assert fields["query"] == "Is there a fire?" + assert fields["model_id"] == "nova-pro" + assert "query" not in captured["url"] + assert "nova-pro" not in captured["url"] + + +def test_multiple_images_sent_as_separate_media_parts(gl: ExperimentalApi): + """A list of images produces one 'media' part each.""" + num_images = 3 + captured: dict = {} + with patch.object(RESTClientObject, "request", _capturing_transport(captured)): + gl.ask_vlm_verify(media=[_FAKE_JPEG] * num_images, query="test") + + media_parts = [p for p in captured["post_params"] if p[0] == "media"] + assert len(media_parts) == num_images + + +def test_empty_media_raises(gl: ExperimentalApi): + """An empty media list raises ValueError before any network call.""" + with pytest.raises(ValueError, match="at least one media item"): + gl.ask_vlm_verify(media=[], query="test") + + +def test_more_than_max_media_raises(gl: ExperimentalApi): + """Supplying more than MAX_VLM_MEDIA_ITEMS raises ValueError before any network call.""" + with pytest.raises(ValueError, match=f"at most {MAX_VLM_MEDIA_ITEMS}"): + gl.ask_vlm_verify(media=[_FAKE_JPEG] * (MAX_VLM_MEDIA_ITEMS + 1), query="test") diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index 3659a6d2..a728df76 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -131,6 +131,19 @@ def test_experimental_subcommand(): assert "list-priming-groups" in completed_process.stdout +def test_ask_vlm_verify_registered(): + """ask_vlm_verify is exposed as an experimental CLI command under its own group.""" + completed_process = subprocess.run( + ["groundlight", "exp", "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + assert completed_process.returncode == 0 + assert "ask-vlm-verify" in completed_process.stdout + + def test_bad_commands(): completed_process = subprocess.run( ["groundlight", "wat"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False