latitude-sdk 3.0.2__tar.gz → 4.0.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/PKG-INFO +8 -6
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/README.md +7 -5
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/pyproject.toml +1 -1
- latitude_sdk-4.0.0b1/src/latitude_sdk/client/client.py +276 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/client/payloads.py +16 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/client/router.py +10 -1
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/latitude.py +3 -0
- latitude_sdk-4.0.0b1/src/latitude_sdk/sdk/projects.py +41 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/prompts.py +86 -113
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/types.py +17 -15
- latitude_sdk-4.0.0b1/tests/projects/create_test.py +101 -0
- latitude_sdk-4.0.0b1/tests/projects/get_all_test.py +137 -0
- latitude_sdk-4.0.0b1/tests/prompts/__init__.py +0 -0
- latitude_sdk-4.0.0b1/tests/prompts/chat_test.py +300 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/run_test.py +22 -354
- latitude_sdk-4.0.0b1/tests/test_acceptance.py +314 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/utils/fixtures.py +61 -188
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/uv.lock +176 -175
- latitude_sdk-3.0.2/src/latitude_sdk/client/client.py +0 -140
- latitude_sdk-3.0.2/tests/prompts/chat_test.py +0 -575
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/.gitignore +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/.python-version +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/LICENSE.md +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/scripts/format.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/scripts/lint.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/scripts/test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/client/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/env/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/env/env.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/py.typed +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/errors.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/evaluations.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/logs.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/util/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/util/utils.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/evaluations/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/evaluations/annotate_test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/logs/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/logs/create_test.py +0 -0
- {latitude_sdk-3.0.2/tests/prompts → latitude_sdk-4.0.0b1/tests/projects}/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/get_all_test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/get_or_create_test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/get_test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/render_chain_test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/render_test.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/utils/__init__.py +0 -0
- {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: latitude-sdk
|
3
|
-
Version:
|
3
|
+
Version: 4.0.0b1
|
4
4
|
Summary: Latitude SDK for Python
|
5
5
|
Project-URL: repository, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python
|
6
6
|
Project-URL: homepage, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python#readme
|
@@ -46,7 +46,7 @@ await sdk.prompts.run("joke-teller", RunPromptOptions(
|
|
46
46
|
))
|
47
47
|
```
|
48
48
|
|
49
|
-
Find more [examples](https://
|
49
|
+
Find more [examples](https://docs.latitude.so/examples/sdk).
|
50
50
|
|
51
51
|
## Development
|
52
52
|
|
@@ -60,7 +60,9 @@ Requires uv `0.5.10` or higher.
|
|
60
60
|
- Build package: `uv build`
|
61
61
|
- Publish package: `uv publish`
|
62
62
|
|
63
|
-
|
63
|
+
### Running only a specific test
|
64
|
+
|
65
|
+
Mark the test with an `only` marker:
|
64
66
|
|
65
67
|
```python
|
66
68
|
import pytest
|
@@ -70,13 +72,13 @@ async def my_test(self):
|
|
70
72
|
# ... your code
|
71
73
|
```
|
72
74
|
|
73
|
-
|
75
|
+
...and then run the tests with the marker `only`:
|
74
76
|
|
75
77
|
```sh
|
76
78
|
uv run scripts/test.py -m only
|
77
79
|
```
|
78
80
|
|
79
|
-
|
81
|
+
Another way is to specify the test in line:
|
80
82
|
|
81
83
|
```python
|
82
84
|
uv run scripts/test.py <test_path>::<test_case>::<test_name>
|
@@ -84,4 +86,4 @@ uv run scripts/test.py <test_path>::<test_case>::<test_name>
|
|
84
86
|
|
85
87
|
## License
|
86
88
|
|
87
|
-
The SDK is licensed under the [
|
89
|
+
The SDK is licensed under the [MIT License](https://opensource.org/licenses/MIT) - read the [LICENSE](/LICENSE) file for details.
|
@@ -27,7 +27,7 @@ await sdk.prompts.run("joke-teller", RunPromptOptions(
|
|
27
27
|
))
|
28
28
|
```
|
29
29
|
|
30
|
-
Find more [examples](https://
|
30
|
+
Find more [examples](https://docs.latitude.so/examples/sdk).
|
31
31
|
|
32
32
|
## Development
|
33
33
|
|
@@ -41,7 +41,9 @@ Requires uv `0.5.10` or higher.
|
|
41
41
|
- Build package: `uv build`
|
42
42
|
- Publish package: `uv publish`
|
43
43
|
|
44
|
-
|
44
|
+
### Running only a specific test
|
45
|
+
|
46
|
+
Mark the test with an `only` marker:
|
45
47
|
|
46
48
|
```python
|
47
49
|
import pytest
|
@@ -51,13 +53,13 @@ async def my_test(self):
|
|
51
53
|
# ... your code
|
52
54
|
```
|
53
55
|
|
54
|
-
|
56
|
+
...and then run the tests with the marker `only`:
|
55
57
|
|
56
58
|
```sh
|
57
59
|
uv run scripts/test.py -m only
|
58
60
|
```
|
59
61
|
|
60
|
-
|
62
|
+
Another way is to specify the test in line:
|
61
63
|
|
62
64
|
```python
|
63
65
|
uv run scripts/test.py <test_path>::<test_case>::<test_name>
|
@@ -65,4 +67,4 @@ uv run scripts/test.py <test_path>::<test_case>::<test_name>
|
|
65
67
|
|
66
68
|
## License
|
67
69
|
|
68
|
-
The SDK is licensed under the [
|
70
|
+
The SDK is licensed under the [MIT License](https://opensource.org/licenses/MIT) - read the [LICENSE](/LICENSE) file for details.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "latitude-sdk"
|
3
|
-
version = "
|
3
|
+
version = "4.0.0-beta.1"
|
4
4
|
description = "Latitude SDK for Python"
|
5
5
|
authors = [{ name = "Latitude Data SL", email = "hello@latitude.so" }]
|
6
6
|
maintainers = [{ name = "Latitude Data SL", email = "hello@latitude.so" }]
|
@@ -0,0 +1,276 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
from contextlib import asynccontextmanager
|
4
|
+
from typing import Any, AsyncGenerator, Awaitable, Callable, Optional
|
5
|
+
|
6
|
+
import httpx
|
7
|
+
import httpx_sse
|
8
|
+
|
9
|
+
from latitude_sdk.client.payloads import (
|
10
|
+
ErrorResponse,
|
11
|
+
RequestBody,
|
12
|
+
RequestHandler,
|
13
|
+
RequestParams,
|
14
|
+
)
|
15
|
+
from latitude_sdk.client.router import Router, RouterOptions
|
16
|
+
from latitude_sdk.sdk.errors import (
|
17
|
+
ApiError,
|
18
|
+
ApiErrorCodes,
|
19
|
+
ApiErrorDbRef,
|
20
|
+
)
|
21
|
+
from latitude_sdk.sdk.types import LogSources
|
22
|
+
from latitude_sdk.util import Model
|
23
|
+
|
24
|
+
RETRIABLE_STATUSES = [408, 409, 429, 500, 502, 503, 504]
|
25
|
+
|
26
|
+
ClientEvent = httpx_sse.ServerSentEvent
|
27
|
+
|
28
|
+
|
29
|
+
class ClientResponse(httpx.Response):
|
30
|
+
async def sse(self: httpx.Response) -> AsyncGenerator[ClientEvent, Any]:
|
31
|
+
source = httpx_sse.EventSource(self)
|
32
|
+
|
33
|
+
async for event in source.aiter_sse():
|
34
|
+
yield event
|
35
|
+
|
36
|
+
|
37
|
+
httpx.Response.sse = ClientResponse.sse # pyright: ignore [reportAttributeAccessIssue]
|
38
|
+
|
39
|
+
|
40
|
+
class ClientOptions(Model):
|
41
|
+
api_key: str
|
42
|
+
retries: int
|
43
|
+
delay: float
|
44
|
+
timeout: Optional[float]
|
45
|
+
source: LogSources
|
46
|
+
router: RouterOptions
|
47
|
+
|
48
|
+
|
49
|
+
class Client:
|
50
|
+
options: ClientOptions
|
51
|
+
router: Router
|
52
|
+
|
53
|
+
def __init__(self, options: ClientOptions):
|
54
|
+
self.options = options
|
55
|
+
self.router = Router(options.router)
|
56
|
+
|
57
|
+
@asynccontextmanager
|
58
|
+
async def request(
|
59
|
+
self,
|
60
|
+
handler: RequestHandler,
|
61
|
+
params: Optional[RequestParams] = None,
|
62
|
+
body: Optional[RequestBody] = None,
|
63
|
+
) -> AsyncGenerator[ClientResponse, Any]:
|
64
|
+
"""
|
65
|
+
Main request handler that delegates to streaming or non-streaming methods.
|
66
|
+
"""
|
67
|
+
is_streaming = self._is_streaming_request(body)
|
68
|
+
|
69
|
+
if is_streaming:
|
70
|
+
async with self._streaming_request(handler, params, body) as response:
|
71
|
+
yield response
|
72
|
+
else:
|
73
|
+
async with self._non_streaming_request(handler, params, body) as response:
|
74
|
+
yield response
|
75
|
+
|
76
|
+
def _is_streaming_request(self, body: Optional[RequestBody]) -> bool:
|
77
|
+
"""Check if this is a streaming request based on the body."""
|
78
|
+
return body is not None and hasattr(body, "stream") and body.stream # type: ignore
|
79
|
+
|
80
|
+
def _prepare_headers(self, is_streaming: bool) -> dict[str, str]:
|
81
|
+
"""Prepare headers for the request."""
|
82
|
+
headers = {
|
83
|
+
"Authorization": f"Bearer {self.options.api_key}",
|
84
|
+
"Content-Type": "application/json",
|
85
|
+
}
|
86
|
+
|
87
|
+
if is_streaming:
|
88
|
+
headers["Accept"] = "text/event-stream"
|
89
|
+
|
90
|
+
return headers
|
91
|
+
|
92
|
+
def _prepare_content(self, body: Optional[RequestBody]) -> Optional[str]:
|
93
|
+
"""Prepare request content from body."""
|
94
|
+
if not body:
|
95
|
+
return None
|
96
|
+
|
97
|
+
return json.dumps(
|
98
|
+
{
|
99
|
+
**json.loads(body.model_dump_json()),
|
100
|
+
"__internal": {"source": self.options.source},
|
101
|
+
}
|
102
|
+
)
|
103
|
+
|
104
|
+
@asynccontextmanager
|
105
|
+
async def _streaming_request(
|
106
|
+
self,
|
107
|
+
handler: RequestHandler,
|
108
|
+
params: Optional[RequestParams] = None,
|
109
|
+
body: Optional[RequestBody] = None,
|
110
|
+
) -> AsyncGenerator[ClientResponse, Any]:
|
111
|
+
"""Handle streaming requests with proper resource management."""
|
112
|
+
headers = self._prepare_headers(is_streaming=True)
|
113
|
+
content = self._prepare_content(body)
|
114
|
+
method, url = self.router.resolve(handler, params)
|
115
|
+
|
116
|
+
async with httpx.AsyncClient(
|
117
|
+
headers=headers,
|
118
|
+
timeout=self.options.timeout,
|
119
|
+
follow_redirects=False,
|
120
|
+
max_redirects=0,
|
121
|
+
) as client:
|
122
|
+
response = await self._execute_with_retry(
|
123
|
+
lambda: client.stream(method=method, url=url, content=content), # type: ignore
|
124
|
+
is_streaming=True,
|
125
|
+
)
|
126
|
+
|
127
|
+
# For streaming responses, yield the response directly
|
128
|
+
# The caller is responsible for consuming the stream
|
129
|
+
yield response
|
130
|
+
|
131
|
+
@asynccontextmanager
|
132
|
+
async def _non_streaming_request(
|
133
|
+
self,
|
134
|
+
handler: RequestHandler,
|
135
|
+
params: Optional[RequestParams] = None,
|
136
|
+
body: Optional[RequestBody] = None,
|
137
|
+
) -> AsyncGenerator[ClientResponse, Any]:
|
138
|
+
"""Handle non-streaming requests with proper resource management."""
|
139
|
+
headers = self._prepare_headers(is_streaming=False)
|
140
|
+
content = self._prepare_content(body)
|
141
|
+
method, url = self.router.resolve(handler, params)
|
142
|
+
|
143
|
+
async with httpx.AsyncClient(
|
144
|
+
headers=headers,
|
145
|
+
timeout=self.options.timeout,
|
146
|
+
follow_redirects=False,
|
147
|
+
max_redirects=0,
|
148
|
+
) as client:
|
149
|
+
response = await self._execute_with_retry(
|
150
|
+
lambda: client.request(method=method, url=url, content=content), # type: ignore
|
151
|
+
is_streaming=False,
|
152
|
+
)
|
153
|
+
|
154
|
+
try:
|
155
|
+
# Pre-read the response text for non-streaming responses
|
156
|
+
# This ensures the response is fully loaded and validates it
|
157
|
+
_ = response.text
|
158
|
+
yield response
|
159
|
+
finally:
|
160
|
+
await response.aclose()
|
161
|
+
|
162
|
+
async def _execute_with_retry(
|
163
|
+
self,
|
164
|
+
request_func: Callable[[], Awaitable[ClientResponse]],
|
165
|
+
is_streaming: bool = False,
|
166
|
+
) -> ClientResponse:
|
167
|
+
"""Execute a request with retry logic."""
|
168
|
+
last_exception = None
|
169
|
+
last_response = None
|
170
|
+
|
171
|
+
for attempt in range(1, self.options.retries + 1):
|
172
|
+
response_cm = None
|
173
|
+
response = None
|
174
|
+
|
175
|
+
try:
|
176
|
+
if is_streaming:
|
177
|
+
# For streaming, request_func returns a context manager
|
178
|
+
response_cm = request_func()
|
179
|
+
response = await response_cm.__aenter__() # type: ignore
|
180
|
+
# Store the context manager for proper cleanup
|
181
|
+
response._stream_cm = response_cm # pyright: ignore [reportAttributeAccessIssue]
|
182
|
+
else:
|
183
|
+
response = await request_func()
|
184
|
+
|
185
|
+
response.raise_for_status() # type: ignore
|
186
|
+
return response # type: ignore
|
187
|
+
|
188
|
+
except Exception as exception:
|
189
|
+
last_exception = exception
|
190
|
+
|
191
|
+
# For HTTP errors, get the response from the exception
|
192
|
+
if isinstance(exception, httpx.HTTPStatusError):
|
193
|
+
last_response = exception.response
|
194
|
+
else:
|
195
|
+
last_response = getattr(exception, "response", None)
|
196
|
+
|
197
|
+
# Don't retry ApiErrors - they're business logic errors
|
198
|
+
if isinstance(exception, ApiError):
|
199
|
+
raise exception
|
200
|
+
|
201
|
+
# If this is the last attempt, break to raise the exception
|
202
|
+
if attempt >= self.options.retries:
|
203
|
+
break
|
204
|
+
|
205
|
+
# Check if we should retry based on status code
|
206
|
+
if (
|
207
|
+
isinstance(exception, httpx.HTTPStatusError)
|
208
|
+
and exception.response.status_code in RETRIABLE_STATUSES
|
209
|
+
):
|
210
|
+
await asyncio.sleep(self._calculate_delay(attempt))
|
211
|
+
continue
|
212
|
+
|
213
|
+
# For non-retriable errors, don't retry
|
214
|
+
break
|
215
|
+
|
216
|
+
# If we get here, all retries failed
|
217
|
+
raise await self._exception(last_exception, last_response) from last_exception # type: ignore
|
218
|
+
|
219
|
+
def _calculate_delay(self, attempt: int) -> float:
|
220
|
+
"""Calculate exponential backoff delay."""
|
221
|
+
return self.options.delay * (2 ** (attempt - 1))
|
222
|
+
|
223
|
+
async def _exception(self, exception: Exception, response: Optional[httpx.Response] = None) -> ApiError:
|
224
|
+
if not response:
|
225
|
+
return ApiError(
|
226
|
+
status=500,
|
227
|
+
code=ApiErrorCodes.InternalServerError,
|
228
|
+
message=str(exception),
|
229
|
+
response=str(exception),
|
230
|
+
)
|
231
|
+
|
232
|
+
# Try to safely get response text and content, handling streaming responses
|
233
|
+
response_text = ""
|
234
|
+
response_content = b""
|
235
|
+
|
236
|
+
try:
|
237
|
+
# Check if this is a streaming response
|
238
|
+
content_type = response.headers.get("content-type", "")
|
239
|
+
is_streaming = "text/event-stream" in content_type
|
240
|
+
|
241
|
+
# For streaming responses, try to read content but don't access text
|
242
|
+
if is_streaming:
|
243
|
+
# For streaming responses, we can access content but not text
|
244
|
+
response_content = response.content
|
245
|
+
response_text = ""
|
246
|
+
else:
|
247
|
+
# For non-streaming responses, we can safely access both
|
248
|
+
response_content = response.content
|
249
|
+
response_text = response.text
|
250
|
+
except Exception:
|
251
|
+
# If we can't read the response (e.g., streaming response that hasn't been read),
|
252
|
+
# try to get what we can
|
253
|
+
try:
|
254
|
+
response_content = response.content
|
255
|
+
except Exception:
|
256
|
+
response_content = b""
|
257
|
+
response_text = ""
|
258
|
+
|
259
|
+
try:
|
260
|
+
error = ErrorResponse.model_validate_json(response_content)
|
261
|
+
|
262
|
+
return ApiError(
|
263
|
+
status=response.status_code,
|
264
|
+
code=error.code,
|
265
|
+
message=error.message,
|
266
|
+
response=response_text,
|
267
|
+
db_ref=ApiErrorDbRef(**dict(error.db_ref)) if error.db_ref else None,
|
268
|
+
)
|
269
|
+
|
270
|
+
except Exception:
|
271
|
+
return ApiError(
|
272
|
+
status=response.status_code,
|
273
|
+
code=ApiErrorCodes.InternalServerError,
|
274
|
+
message=str(exception),
|
275
|
+
response=response_text,
|
276
|
+
)
|
@@ -45,6 +45,7 @@ class RunPromptRequestBody(Model):
|
|
45
45
|
parameters: Optional[Dict[str, Any]] = None
|
46
46
|
custom_identifier: Optional[str] = Field(default=None, alias=str("customIdentifier"))
|
47
47
|
stream: Optional[bool] = None
|
48
|
+
tools: Optional[List[str]] = None
|
48
49
|
|
49
50
|
|
50
51
|
class ChatPromptRequestParams(Model):
|
@@ -88,6 +89,16 @@ class AnnotateEvaluationRequestBody(Model):
|
|
88
89
|
metadata: Optional[Metadata] = None
|
89
90
|
|
90
91
|
|
92
|
+
class ToolResultsRequestBody(Model):
|
93
|
+
tool_call_id: str = Field(alias=str("toolCallId"))
|
94
|
+
result: Any
|
95
|
+
is_error: Optional[bool] = Field(default=None, alias=str("isError"))
|
96
|
+
|
97
|
+
|
98
|
+
class CreateProjectRequestBody(Model):
|
99
|
+
name: str
|
100
|
+
|
101
|
+
|
91
102
|
RequestParams = Union[
|
92
103
|
GetPromptRequestParams,
|
93
104
|
GetAllPromptRequestParams,
|
@@ -105,6 +116,8 @@ RequestBody = Union[
|
|
105
116
|
ChatPromptRequestBody,
|
106
117
|
CreateLogRequestBody,
|
107
118
|
AnnotateEvaluationRequestBody,
|
119
|
+
ToolResultsRequestBody,
|
120
|
+
CreateProjectRequestBody,
|
108
121
|
]
|
109
122
|
|
110
123
|
|
@@ -116,3 +129,6 @@ class RequestHandler(StrEnum):
|
|
116
129
|
ChatPrompt = "CHAT_PROMPT"
|
117
130
|
CreateLog = "CREATE_LOG"
|
118
131
|
AnnotateEvaluation = "ANNOTATE_EVALUATION"
|
132
|
+
ToolResults = "TOOL_RESULTS"
|
133
|
+
GetAllProjects = "GET_ALL_PROJECTS"
|
134
|
+
CreateProject = "CREATE_PROJECT"
|
@@ -27,7 +27,7 @@ class Router:
|
|
27
27
|
def __init__(self, options: RouterOptions):
|
28
28
|
self.options = options
|
29
29
|
|
30
|
-
def resolve(self, handler: RequestHandler, params: RequestParams) -> Tuple[str, str]:
|
30
|
+
def resolve(self, handler: RequestHandler, params: Optional[RequestParams] = None) -> Tuple[str, str]:
|
31
31
|
if handler == RequestHandler.GetPrompt:
|
32
32
|
assert isinstance(params, GetPromptRequestParams)
|
33
33
|
|
@@ -90,6 +90,15 @@ class Router:
|
|
90
90
|
|
91
91
|
return "POST", self.conversations().annotate(params.conversation_uuid, params.evaluation_uuid)
|
92
92
|
|
93
|
+
elif handler == RequestHandler.ToolResults:
|
94
|
+
return "POST", f"{self.options.gateway.base_url}/tools/results"
|
95
|
+
|
96
|
+
elif handler == RequestHandler.GetAllProjects:
|
97
|
+
return "GET", f"{self.options.gateway.base_url}/projects"
|
98
|
+
|
99
|
+
elif handler == RequestHandler.CreateProject:
|
100
|
+
return "POST", f"{self.options.gateway.base_url}/projects"
|
101
|
+
|
93
102
|
raise TypeError(f"Unknown handler: {handler}")
|
94
103
|
|
95
104
|
class Conversations(Model):
|
@@ -6,6 +6,7 @@ from latitude_sdk.client import Client, ClientOptions, RouterOptions
|
|
6
6
|
from latitude_sdk.env import env
|
7
7
|
from latitude_sdk.sdk.evaluations import Evaluations
|
8
8
|
from latitude_sdk.sdk.logs import Logs
|
9
|
+
from latitude_sdk.sdk.projects import Projects
|
9
10
|
from latitude_sdk.sdk.prompts import Prompts
|
10
11
|
from latitude_sdk.sdk.types import GatewayOptions, LogSources, SdkOptions
|
11
12
|
from latitude_sdk.util import Model
|
@@ -49,6 +50,7 @@ class Latitude:
|
|
49
50
|
|
50
51
|
promptl: Promptl
|
51
52
|
|
53
|
+
projects: Projects
|
52
54
|
prompts: Prompts
|
53
55
|
logs: Logs
|
54
56
|
evaluations: Evaluations
|
@@ -76,6 +78,7 @@ class Latitude:
|
|
76
78
|
)
|
77
79
|
|
78
80
|
self.promptl = Promptl(self._options.promptl)
|
81
|
+
self.projects = Projects(self._client, self._options)
|
79
82
|
self.prompts = Prompts(self._client, self.promptl, self._options)
|
80
83
|
self.logs = Logs(self._client, self._options)
|
81
84
|
self.evaluations = Evaluations(self._client, self._options)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import json
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from latitude_sdk.client import Client
|
5
|
+
from latitude_sdk.client.payloads import CreateProjectRequestBody, RequestHandler
|
6
|
+
from latitude_sdk.sdk.types import Project, SdkOptions, Version
|
7
|
+
|
8
|
+
|
9
|
+
class CreateProjectResponse:
|
10
|
+
def __init__(self, project: Project, version: Version):
|
11
|
+
self.project = project
|
12
|
+
self.version = version
|
13
|
+
|
14
|
+
|
15
|
+
class Projects:
|
16
|
+
_client: Client
|
17
|
+
_options: SdkOptions
|
18
|
+
|
19
|
+
def __init__(self, client: Client, options: SdkOptions):
|
20
|
+
self._client = client
|
21
|
+
self._options = options
|
22
|
+
|
23
|
+
async def get_all(self) -> List[Project]:
|
24
|
+
async with self._client.request(
|
25
|
+
handler=RequestHandler.GetAllProjects,
|
26
|
+
params=None,
|
27
|
+
) as response:
|
28
|
+
projects_data = json.loads(response.content)
|
29
|
+
return [Project.model_validate_json(json.dumps(project)) for project in projects_data]
|
30
|
+
|
31
|
+
async def create(self, name: str) -> CreateProjectResponse:
|
32
|
+
async with self._client.request(
|
33
|
+
handler=RequestHandler.CreateProject,
|
34
|
+
params=None,
|
35
|
+
body=CreateProjectRequestBody(name=name),
|
36
|
+
) as response:
|
37
|
+
response_data = json.loads(response.content)
|
38
|
+
return CreateProjectResponse(
|
39
|
+
project=Project.model_validate_json(json.dumps(response_data["project"])),
|
40
|
+
version=Version.model_validate_json(json.dumps(response_data["version"])),
|
41
|
+
)
|