Skip to content

Commit f8533b4

Browse files
authored
Changing detector_id to detector. Userguide updates (#12)
BREAKING CHANGE. `detector_id` is no long accepted as a named arg. Must use `detector` instead, which can now be an id string or a detector object. * Adding section on edge to User Guide. * Adding a partial `get_or_create_detector` method to SDK. * Doesn't fail silently if pagination would be needed. * submit_image_query can accept a detector or a detector_id * Consolidating the code samples into a single snippet. * format udpates * Wordsmithing the user guide. * Improving interface and docs based on feedback. * Moving exception handling to bottom of userguide. * Ooops taking out jpeg_from_numpy which isn't supposed to be on this branch. Bad merge. * Changing tests to pass `detector` instead of `detector_id`
1 parent 1d33b96 commit f8533b4

File tree

6 files changed

+101
-60
lines changed

6 files changed

+101
-60
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,5 @@ cython_debug/
163163
poetry.lock
164164

165165
node_modules/
166+
167+
*.swp

README.md

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,13 @@ $ pip install groundlight
1212
$ poetry add groundlight
1313
```
1414

15-
### Basic Usage
15+
### Usage
1616

17-
To access the API, you need an API token. You can create one on the [groundlight website](https://app.groundlight.ai/reef/my-account/api-tokens). Then, you're ready to use the SDK!
18-
19-
```Python
20-
from groundlight import Groundlight
21-
22-
# Load the API client. This defaults to the prod endpoint,
23-
# but you can specify a different endpoint like so:
24-
# gl = Groundlight(endpoint="https://device.integ.groundlight.ai/device-api")
25-
gl = Groundlight(api_token="<YOUR_API_TOKEN>")
26-
27-
# Create a detector
28-
detector = gl.create_detector(name="Dog", query="Is it a dog?")
29-
30-
# (Or, create a detector with a specific named ML config from https://github.com/positronix-ai/zuuul/blob/main/pysrc/predictor_config/binary_classification_predictors.yaml)
31-
# detector = gl.create_detector(name="Dog", query="Is it a dog?", config_name="b4mu11-mlp")
32-
33-
# Call an API method (e.g., retrieve a list of detectors)
34-
detectors = gl.list_detectors()
35-
```
36-
37-
(Alternatively, you can use the token by setting the `GROUNDLIGHT_API_TOKEN` environment variable.)
38-
39-
### What API methods are available?
40-
41-
Check out the [User Guide](UserGuide.md)!
17+
For instructions on using the SDK see the public [User Guide](UserGuide.md).
4218

4319
For more details, see the [Groundlight](src/groundlight/client.py)
44-
class. This SDK closely follows the methods in our [API
45-
Docs](https://app.groundlight.ai/reef/admin/api-docs).
20+
class. This SDK closely follows the methods in our [API
21+
Docs](https://app.groundlight.ai/reef/admin/public-api-docs/).
4622

4723
## Development
4824

UserGuide.md

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
1-
# User Guide
1+
# Groundlight Python SDK
22

3-
`groundlight` is a python SDK for working with the Groundlight API. You can send image queries and receive predictions powered by a mixture of machine learning models and human labelers in-the-loop.
3+
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.
44

5-
*Note: The SDK is currently in "alpha" phase.*
5+
How does it work? Your images are first analyzed by machine learning (ML) models which are automatically trained on your data. If those models have high enough confidence, that's your answer. But if the models are unsure, then the images are progressively escalated to more resource-intensive analysis methods up to real-time human review. So what you get is a computer vision system that starts working right away without even needing to first gather and label a dataset. At first it will operate with high latency, because people need to review the image queries. But over time, the ML systems will learn and improve so queries come back faster with higher confidence.
66

7-
## Pre-reqs
7+
*Note: The SDK is currently in "beta" phase. Interfaces are subject to change in future versions.*
8+
9+
10+
## Simple Example
11+
12+
How to build a computer vision system in 5 lines of python code:
13+
14+
```Python
15+
from groundlight import Groundlight
16+
gl = Groundlight()
17+
18+
# Create a new detector: use natural language to describe what you want to understand
19+
detector = gl.create_detector(name="door", query="Is the door open?")
20+
21+
# Send an image to the detector
22+
image_query = gl.submit_image_query(detector=detector, image="path/to/filename.jpeg")
23+
24+
# Show the results
25+
print(f"The answer is {image_query.result}")
26+
```
27+
28+
29+
## Getting Started
830

931
1. Install the `groundlight` sdk.
1032

@@ -13,32 +35,42 @@
1335
```
1436

1537
1. To access the API, you need an API token. You can create one on the
16-
[groundlight website](https://app.groundlight.ai/reef/my-account/api-tokens).
38+
[groundlight web app](https://app.groundlight.ai/reef/my-account/api-tokens).
1739

18-
1. Use the `Groundlight` client!
40+
The API token should be stored securely. You can use it directly in your code to initialize the SDK like:
1941

20-
```Python
21-
from groundlight import Groundlight
22-
gl = Groundlight(api_token="<YOUR_API_TOKEN>")
23-
```
42+
```python
43+
gl = Groundlight(api_token="<YOUR_API_TOKEN>")
44+
```
2445

25-
The API token should be stored securely - do not commit it to version control! Alternatively, you can use the token by setting the `GROUNDLIGHT_API_TOKEN` environment variable.
46+
which is an easy way to get started, but is NOT a best practice. Please do not commit your API Token to version control! Instead we recommend setting the `GROUNDLIGHT_API_TOKEN` environment variable outside your code so that the SDK can find it automatically.
47+
48+
```bash
49+
$ export GROUNDLIGHT_API_TOKEN=api_2asdfkjEXAMPLE
50+
$ python glapp.py
51+
```
2652

27-
## Basics
2853

29-
#### Create a new detector
54+
## Using Groundlight on the edge
55+
56+
Starting your model evaluations at the edge reduces latency, cost, network bandwidth, and energy. Once you have downloaded and installed your Groundlight edge models, you can configure the Groundlight SDK to use your edge environment by configuring the 'endpoint' to point at your local environment as such:
3057

3158
```Python
32-
detector = gl.create_detector(name="Dog", query="Is it a dog?")
59+
from groundlight import Groundlight
60+
gl = Groundlight(endpoint="http://localhost:6717")
3361
```
3462

35-
#### Retrieve a detector
63+
(Edge model download is not yet generally available.)
64+
65+
## Advanced
66+
67+
### Retrieve an existing detector
3668

3769
```Python
3870
detector = gl.get_detector(id="YOUR_DETECTOR_ID")
3971
```
4072

41-
#### List your detectors
73+
### List your detectors
4274

4375
```Python
4476
# Defaults to 10 results per page
@@ -48,21 +80,15 @@ detectors = gl.list_detectors()
4880
detectors = gl.list_detectors(page=3, page_size=25)
4981
```
5082

51-
#### Submit an image query
52-
53-
```Python
54-
image_query = gl.submit_image_query(detector_id="YOUR_DETECTOR_ID", image="path/to/filename.jpeg")
55-
```
56-
57-
#### Retrieve an image query
83+
### Retrieve an image query
5884

5985
In practice, you may want to check for a new result on your query. For example, after a cloud reviewer labels your query. For example, you can use the `image_query.id` after the above `submit_image_query()` call.
6086
6187
```Python
6288
image_query = gl.get_image_query(id="YOUR_IMAGE_QUERY_ID")
6389
```
6490

65-
#### List your previous image queries
91+
### List your previous image queries
6692

6793
```Python
6894
# Defaults to 10 results per page
@@ -72,8 +98,6 @@ image_queries = gl.list_image_queries()
7298
image_queries = gl.list_image_queries(page=3, page_size=25)
7399
```
74100

75-
## Advanced
76-
77101
### Handling HTTP errors
78102

79103
If there is an HTTP error during an API call, it will raise an `ApiException`. You can access different metadata from that exception:
@@ -92,3 +116,4 @@ except ApiException as e:
92116
print(e.reason)
93117
print(e.status)
94118
```
119+

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[tool.poetry]
22
name = "groundlight"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
license = "MIT"
55
readme = "UserGuide.md"
66
homepage = "https://groundlight.ai"
7-
description = "Call the Groundlight API from python"
7+
description = "Build computer vision systems from natural language with Groundlight"
88
authors = ["Groundlight AI <[email protected]>"]
99
packages = [
1010
{ include = "**/*.py", from = "src" },

src/groundlight/client.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
from io import BufferedReader, BytesIO
3-
from typing import Union
3+
from typing import Optional, Union
44

55
from model import Detector, ImageQuery, PaginatedDetectorList, PaginatedImageQueryList
66
from openapi_client import ApiClient, Configuration
@@ -61,6 +61,17 @@ def get_detector(self, id: str) -> Detector:
6161
obj = self.detectors_api.get_detector(id=id)
6262
return Detector.parse_obj(obj.to_dict())
6363

64+
def get_detector_by_name(self, name: str) -> Optional[Detector]:
65+
#TODO: Do this on server.
66+
detector_list = self.list_detectors(page_size=100)
67+
for d in detector_list.results:
68+
if d.name == name:
69+
return d
70+
if detector_list.next:
71+
#TODO: paginate
72+
raise RuntimeError("You have too many detectors to use get_detector_by_name")
73+
return None
74+
6475
def list_detectors(self, page: int = 1, page_size: int = 10) -> PaginatedDetectorList:
6576
obj = self.detectors_api.list_detectors(page=page, page_size=page_size)
6677
return PaginatedDetectorList.parse_obj(obj.to_dict())
@@ -69,6 +80,19 @@ def create_detector(self, name: str, query: str, config_name: str = None) -> Det
6980
obj = self.detectors_api.create_detector(DetectorCreationInput(name=name, query=query, config_name=config_name))
7081
return Detector.parse_obj(obj.to_dict())
7182

83+
def get_or_create_detector(self, name: str, query: str, config_name: str = None) -> Detector:
84+
"""Tries to look up the detector by name. If a detector with that name and query exists, return it.
85+
Otherwise, create a detector with the specified query and config.
86+
"""
87+
existing_detector = self.get_detector_by_name(name)
88+
if existing_detector:
89+
if existing_detector.query == query:
90+
return existing_detector
91+
else:
92+
raise ValueError(f"Found existing detector with {name=} (id={existing_detector.id}) but the queries don't match")
93+
94+
return self.create_detector(name, query, config_name)
95+
7296
def get_image_query(self, id: str) -> ImageQuery:
7397
obj = self.image_queries_api.get_image_query(id=id)
7498
return ImageQuery.parse_obj(obj.to_dict())
@@ -77,7 +101,21 @@ def list_image_queries(self, page: int = 1, page_size: int = 10) -> PaginatedIma
77101
obj = self.image_queries_api.list_image_queries(page=page, page_size=page_size)
78102
return PaginatedImageQueryList.parse_obj(obj.to_dict())
79103

80-
def submit_image_query(self, detector_id: str, image: Union[str, bytes, BytesIO]) -> ImageQuery:
104+
def submit_image_query(self,
105+
image: Union[str, bytes, BytesIO, BufferedReader],
106+
detector: Union[Detector, str],
107+
) -> ImageQuery:
108+
"""Evaluates an image with Groundlight.
109+
:param image: The image, in several possible formats:
110+
- a filename (string) of a jpeg file
111+
- a byte array or BytesIO with jpeg bytes
112+
- a numpy array in the 0-255 range (gets converted to jpeg)
113+
:param detector: the Detector object, or string id of a detector like `det_12345`
114+
"""
115+
if isinstance(detector, Detector):
116+
detector_id = detector.id
117+
else:
118+
detector_id = detector
81119
image_bytesio: Union[BytesIO, BufferedReader]
82120
if isinstance(image, str):
83121
# Assume it is a filename

test/integration/test_groundlight.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def detector(gl: Groundlight) -> Detector:
2121

2222
@pytest.fixture
2323
def image_query(gl: Groundlight, detector: Detector) -> ImageQuery:
24-
return gl.submit_image_query(detector_id=detector.id, image="test/assets/dog.jpeg")
24+
return gl.submit_image_query(detector=detector.id, image="test/assets/dog.jpeg")
2525

2626

2727
# @pytest.mark.skip(reason="We don't want to create a million detectors")
@@ -58,7 +58,7 @@ def test_get_detector(gl: Groundlight, detector: Detector):
5858

5959
# @pytest.mark.skip(reason="We don't want to create a million detectors and image_queries")
6060
def test_submit_image_query(gl: Groundlight, detector: Detector):
61-
_image_query = gl.submit_image_query(detector_id=detector.id, image="test/assets/dog.jpeg")
61+
_image_query = gl.submit_image_query(detector=detector.id, image="test/assets/dog.jpeg")
6262
assert str(_image_query)
6363
assert isinstance(_image_query, ImageQuery)
6464

0 commit comments

Comments
 (0)