latitude-sdk 3.0.2__py3-none-any.whl → 5.0.0b1__py3-none-any.whl
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/__init__.py +6 -0
- latitude_sdk/client/client.py +15 -8
- latitude_sdk/client/payloads.py +18 -2
- latitude_sdk/client/router.py +10 -1
- latitude_sdk/sdk/__init__.py +1 -0
- latitude_sdk/sdk/errors.py +1 -0
- latitude_sdk/sdk/evaluations.py +5 -14
- latitude_sdk/sdk/latitude.py +4 -0
- latitude_sdk/sdk/projects.py +35 -0
- latitude_sdk/sdk/prompts.py +52 -115
- latitude_sdk/sdk/types.py +62 -27
- latitude_sdk/util/utils.py +5 -0
- latitude_sdk/version/__init__.py +1 -0
- latitude_sdk/version/version.py +63 -0
- {latitude_sdk-3.0.2.dist-info → latitude_sdk-5.0.0b1.dist-info}/METADATA +13 -11
- latitude_sdk-5.0.0b1.dist-info/RECORD +24 -0
- latitude_sdk-3.0.2.dist-info/RECORD +0 -21
- {latitude_sdk-3.0.2.dist-info → latitude_sdk-5.0.0b1.dist-info}/WHEEL +0 -0
- {latitude_sdk-3.0.2.dist-info → latitude_sdk-5.0.0b1.dist-info}/licenses/LICENSE.md +0 -0
latitude_sdk/__init__.py
CHANGED
latitude_sdk/client/client.py
CHANGED
@@ -8,15 +8,12 @@ import httpx_sse
|
|
8
8
|
|
9
9
|
from latitude_sdk.client.payloads import ErrorResponse, RequestBody, RequestHandler, RequestParams
|
10
10
|
from latitude_sdk.client.router import Router, RouterOptions
|
11
|
-
from latitude_sdk.sdk.errors import
|
12
|
-
ApiError,
|
13
|
-
ApiErrorCodes,
|
14
|
-
ApiErrorDbRef,
|
15
|
-
)
|
11
|
+
from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes, ApiErrorDbRef
|
16
12
|
from latitude_sdk.sdk.types import LogSources
|
17
13
|
from latitude_sdk.util import Model
|
14
|
+
from latitude_sdk.version import version
|
18
15
|
|
19
|
-
RETRIABLE_STATUSES = [408,
|
16
|
+
RETRIABLE_STATUSES = [408, 429, 500, 502, 503, 504]
|
20
17
|
|
21
18
|
ClientEvent = httpx_sse.ServerSentEvent
|
22
19
|
|
@@ -51,12 +48,18 @@ class Client:
|
|
51
48
|
|
52
49
|
@asynccontextmanager
|
53
50
|
async def request(
|
54
|
-
self,
|
51
|
+
self,
|
52
|
+
handler: RequestHandler,
|
53
|
+
params: Optional[RequestParams] = None,
|
54
|
+
body: Optional[RequestBody] = None,
|
55
|
+
stream: Optional[bool] = None,
|
55
56
|
) -> AsyncGenerator[ClientResponse, Any]:
|
56
57
|
client = httpx.AsyncClient(
|
57
58
|
headers={
|
58
59
|
"Authorization": f"Bearer {self.options.api_key}",
|
60
|
+
"X-Latitude-SDK-Version": version.semver,
|
59
61
|
"Content-Type": "application/json",
|
62
|
+
"Accept": "text/event-stream" if stream else "application/json",
|
60
63
|
},
|
61
64
|
timeout=self.options.timeout,
|
62
65
|
follow_redirects=False,
|
@@ -78,7 +81,8 @@ class Client:
|
|
78
81
|
|
79
82
|
while attempt <= self.options.retries:
|
80
83
|
try:
|
81
|
-
|
84
|
+
request = client.build_request(method=method, url=url, content=content)
|
85
|
+
response = await client.send(request=request, stream=stream or False)
|
82
86
|
response.raise_for_status()
|
83
87
|
|
84
88
|
yield response # pyright: ignore [reportReturnType]
|
@@ -121,6 +125,9 @@ class Client:
|
|
121
125
|
)
|
122
126
|
|
123
127
|
try:
|
128
|
+
if not response.is_stream_consumed:
|
129
|
+
await response.aread()
|
130
|
+
|
124
131
|
error = ErrorResponse.model_validate_json(response.content)
|
125
132
|
|
126
133
|
return ApiError(
|
latitude_sdk/client/payloads.py
CHANGED
@@ -44,6 +44,7 @@ class RunPromptRequestBody(Model):
|
|
44
44
|
path: str
|
45
45
|
parameters: Optional[Dict[str, Any]] = None
|
46
46
|
custom_identifier: Optional[str] = Field(default=None, alias=str("customIdentifier"))
|
47
|
+
tools: Optional[List[str]] = None
|
47
48
|
stream: Optional[bool] = None
|
48
49
|
|
49
50
|
|
@@ -53,6 +54,7 @@ class ChatPromptRequestParams(Model):
|
|
53
54
|
|
54
55
|
class ChatPromptRequestBody(Model):
|
55
56
|
messages: List[Message]
|
57
|
+
tools: Optional[List[str]] = None
|
56
58
|
stream: Optional[bool] = None
|
57
59
|
|
58
60
|
|
@@ -80,14 +82,23 @@ class AnnotateEvaluationRequestParams(EvaluationRequestParams, Model):
|
|
80
82
|
|
81
83
|
|
82
84
|
class AnnotateEvaluationRequestBody(Model):
|
83
|
-
score: int
|
84
|
-
|
85
85
|
class Metadata(Model):
|
86
86
|
reason: str
|
87
87
|
|
88
|
+
score: int
|
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"
|
latitude_sdk/client/router.py
CHANGED
@@ -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):
|
latitude_sdk/sdk/__init__.py
CHANGED
latitude_sdk/sdk/errors.py
CHANGED
latitude_sdk/sdk/evaluations.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
from
|
2
|
-
from typing import Any, Optional, Union
|
1
|
+
from typing import Optional
|
3
2
|
|
4
3
|
from latitude_sdk.client import (
|
5
4
|
AnnotateEvaluationRequestBody,
|
@@ -7,24 +6,16 @@ from latitude_sdk.client import (
|
|
7
6
|
Client,
|
8
7
|
RequestHandler,
|
9
8
|
)
|
10
|
-
from latitude_sdk.sdk.types import SdkOptions
|
11
|
-
from latitude_sdk.util import
|
9
|
+
from latitude_sdk.sdk.types import EvaluationResult, SdkOptions
|
10
|
+
from latitude_sdk.util import Model
|
12
11
|
|
13
12
|
|
14
13
|
class AnnotateEvaluationOptions(Model):
|
15
14
|
reason: str
|
16
15
|
|
17
16
|
|
18
|
-
class AnnotateEvaluationResult(Model):
|
19
|
-
|
20
|
-
score: int
|
21
|
-
normalized_score: int = Field(alias=str("normalizedScore"))
|
22
|
-
metadata: dict[str, Any]
|
23
|
-
has_passed: bool = Field(alias=str("hasPassed"))
|
24
|
-
created_at: datetime = Field(alias=str("createdAt"))
|
25
|
-
updated_at: datetime = Field(alias=str("updatedAt"))
|
26
|
-
version_uuid: str = Field(alias=str("versionUuid"))
|
27
|
-
error: Optional[Union[str, None]] = None
|
17
|
+
class AnnotateEvaluationResult(EvaluationResult, Model):
|
18
|
+
pass
|
28
19
|
|
29
20
|
|
30
21
|
class Evaluations:
|
latitude_sdk/sdk/latitude.py
CHANGED
@@ -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,8 @@ class Latitude:
|
|
76
78
|
)
|
77
79
|
|
78
80
|
self.promptl = Promptl(self._options.promptl)
|
81
|
+
|
82
|
+
self.projects = Projects(self._client, self._options)
|
79
83
|
self.prompts = Prompts(self._client, self.promptl, self._options)
|
80
84
|
self.logs = Logs(self._client, self._options)
|
81
85
|
self.evaluations = Evaluations(self._client, self._options)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from latitude_sdk.client import Client, CreateProjectRequestBody, RequestHandler
|
4
|
+
from latitude_sdk.sdk.types import Project, SdkOptions, Version
|
5
|
+
from latitude_sdk.util import Adapter as AdapterUtil
|
6
|
+
from latitude_sdk.util import Model
|
7
|
+
|
8
|
+
_GetAllProjectResults = AdapterUtil[List[Project]](List[Project])
|
9
|
+
|
10
|
+
|
11
|
+
class CreateProjectResult(Model):
|
12
|
+
project: Project
|
13
|
+
version: Version
|
14
|
+
|
15
|
+
|
16
|
+
class Projects:
|
17
|
+
_options: SdkOptions
|
18
|
+
_client: Client
|
19
|
+
|
20
|
+
def __init__(self, client: Client, options: SdkOptions):
|
21
|
+
self._options = options
|
22
|
+
self._client = client
|
23
|
+
|
24
|
+
async def get_all(self) -> List[Project]:
|
25
|
+
async with self._client.request(
|
26
|
+
handler=RequestHandler.GetAllProjects,
|
27
|
+
) as response:
|
28
|
+
return _GetAllProjectResults.validate_json(response.content)
|
29
|
+
|
30
|
+
async def create(self, name: str) -> CreateProjectResult:
|
31
|
+
async with self._client.request(
|
32
|
+
handler=RequestHandler.CreateProject,
|
33
|
+
body=CreateProjectRequestBody(name=name),
|
34
|
+
) as response:
|
35
|
+
return CreateProjectResult.model_validate_json(response.content)
|
latitude_sdk/sdk/prompts.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
import
|
2
|
-
from typing import Any, AsyncGenerator, List, Optional, Sequence, Tuple, Union
|
1
|
+
from typing import Any, AsyncGenerator, List, Optional, Sequence
|
3
2
|
|
4
|
-
from promptl_ai import Adapter, Message, MessageLike, Promptl
|
3
|
+
from promptl_ai import Adapter, Message, MessageLike, Promptl
|
5
4
|
from promptl_ai.bindings.types import _Message
|
6
5
|
|
7
6
|
from latitude_sdk.client import (
|
@@ -16,21 +15,22 @@ from latitude_sdk.client import (
|
|
16
15
|
RequestHandler,
|
17
16
|
RunPromptRequestBody,
|
18
17
|
RunPromptRequestParams,
|
18
|
+
ToolResultsRequestBody,
|
19
19
|
)
|
20
20
|
from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes
|
21
21
|
from latitude_sdk.sdk.types import (
|
22
|
-
AGENT_END_TOOL_NAME,
|
23
22
|
ChainEvents,
|
24
23
|
FinishedResult,
|
25
24
|
OnStep,
|
26
25
|
OnToolCall,
|
27
26
|
OnToolCallDetails,
|
28
27
|
Prompt,
|
28
|
+
ProviderEvents,
|
29
|
+
ProviderEventToolCalled,
|
29
30
|
Providers,
|
30
31
|
SdkOptions,
|
31
32
|
StreamCallbacks,
|
32
33
|
StreamEvents,
|
33
|
-
ToolCall,
|
34
34
|
ToolResult,
|
35
35
|
_LatitudeEvent,
|
36
36
|
)
|
@@ -52,10 +52,6 @@ _PROMPT_ATTR_TO_ADAPTER_ATTR = {
|
|
52
52
|
}
|
53
53
|
|
54
54
|
|
55
|
-
class OnToolCallPaused(Exception):
|
56
|
-
pass
|
57
|
-
|
58
|
-
|
59
55
|
class PromptOptions(Model):
|
60
56
|
project_id: Optional[int] = None
|
61
57
|
version_uuid: Optional[str] = None
|
@@ -142,31 +138,19 @@ class Prompts:
|
|
142
138
|
response="Project ID is required",
|
143
139
|
)
|
144
140
|
|
145
|
-
async def _extract_agent_tool_requests(
|
146
|
-
self, tool_requests: List[ToolCall]
|
147
|
-
) -> Tuple[List[ToolCall], List[ToolCall]]:
|
148
|
-
agent: List[ToolCall] = []
|
149
|
-
other: List[ToolCall] = []
|
150
|
-
|
151
|
-
for tool in tool_requests:
|
152
|
-
if tool.name == AGENT_END_TOOL_NAME:
|
153
|
-
agent.append(tool)
|
154
|
-
else:
|
155
|
-
other.append(tool)
|
156
|
-
|
157
|
-
return agent, other
|
158
|
-
|
159
141
|
async def _handle_stream(
|
160
|
-
self,
|
142
|
+
self,
|
143
|
+
stream: AsyncGenerator[ClientEvent, Any],
|
144
|
+
on_event: Optional[StreamCallbacks.OnEvent],
|
145
|
+
tools: Optional[dict[str, OnToolCall]],
|
161
146
|
) -> FinishedResult:
|
162
147
|
uuid = None
|
163
148
|
conversation: List[Message] = []
|
164
149
|
response = None
|
165
|
-
agent_response = None
|
166
|
-
tool_requests: List[ToolCall] = []
|
167
150
|
|
168
151
|
async for stream_event in stream:
|
169
152
|
event = None
|
153
|
+
tool_call = None
|
170
154
|
|
171
155
|
if stream_event.event == str(StreamEvents.Latitude):
|
172
156
|
event = _LatitudeEvent.validate_json(stream_event.data)
|
@@ -176,9 +160,6 @@ class Prompts:
|
|
176
160
|
if event.type == ChainEvents.ProviderCompleted:
|
177
161
|
response = event.response
|
178
162
|
|
179
|
-
elif event.type == ChainEvents.ToolsRequested:
|
180
|
-
tool_requests = event.tools
|
181
|
-
|
182
163
|
elif event.type == ChainEvents.ChainError:
|
183
164
|
raise ApiError(
|
184
165
|
status=400,
|
@@ -191,6 +172,9 @@ class Prompts:
|
|
191
172
|
event = stream_event.json()
|
192
173
|
event["event"] = StreamEvents.Provider
|
193
174
|
|
175
|
+
if event.get("type") == str(ProviderEvents.ToolCalled):
|
176
|
+
tool_call = ProviderEventToolCalled.model_validate_json(stream_event.data)
|
177
|
+
|
194
178
|
else:
|
195
179
|
raise ApiError(
|
196
180
|
status=500,
|
@@ -202,6 +186,9 @@ class Prompts:
|
|
202
186
|
if on_event:
|
203
187
|
on_event(event)
|
204
188
|
|
189
|
+
if tool_call:
|
190
|
+
await self._handle_tool_call(tool_call, tools)
|
191
|
+
|
205
192
|
if not uuid or not response:
|
206
193
|
raise ApiError(
|
207
194
|
status=500,
|
@@ -210,21 +197,7 @@ class Prompts:
|
|
210
197
|
response="Stream ended without a chain-complete event. Missing uuid or response.",
|
211
198
|
)
|
212
199
|
|
213
|
-
|
214
|
-
if len(agent_requests) > 0:
|
215
|
-
agent_response = agent_requests[0].arguments
|
216
|
-
|
217
|
-
return FinishedResult(
|
218
|
-
uuid=uuid,
|
219
|
-
conversation=conversation,
|
220
|
-
response=response,
|
221
|
-
agent_response=agent_response,
|
222
|
-
tool_requests=tool_requests,
|
223
|
-
)
|
224
|
-
|
225
|
-
@staticmethod
|
226
|
-
def _pause_tool_execution() -> Any:
|
227
|
-
raise OnToolCallPaused()
|
200
|
+
return FinishedResult(uuid=uuid, conversation=conversation, response=response)
|
228
201
|
|
229
202
|
@staticmethod
|
230
203
|
async def _wrap_tool_handler(
|
@@ -235,69 +208,45 @@ class Prompts:
|
|
235
208
|
try:
|
236
209
|
result = await handler(arguments, details)
|
237
210
|
|
238
|
-
return ToolResult(**tool_result, result=result)
|
211
|
+
return ToolResult(**tool_result, result=result, is_error=False)
|
239
212
|
except Exception as exception:
|
240
|
-
if isinstance(exception, OnToolCallPaused):
|
241
|
-
raise exception
|
242
|
-
|
243
213
|
return ToolResult(**tool_result, result=str(exception), is_error=True)
|
244
214
|
|
245
|
-
async def
|
246
|
-
self,
|
247
|
-
) ->
|
248
|
-
|
215
|
+
async def _handle_tool_call(
|
216
|
+
self, tool_call: ProviderEventToolCalled, tools: Optional[dict[str, OnToolCall]]
|
217
|
+
) -> None:
|
218
|
+
# NOTE: Do not handle tool calls if user specified no tools
|
219
|
+
if not tools:
|
220
|
+
return
|
221
|
+
|
222
|
+
tool_handler = tools.get(tool_call.name)
|
223
|
+
if not tool_handler:
|
249
224
|
raise ApiError(
|
250
225
|
status=400,
|
251
226
|
code=ApiErrorCodes.AIRunError,
|
252
|
-
message="
|
253
|
-
response="
|
227
|
+
message=f"Tool {tool_call.name} not supplied",
|
228
|
+
response=f"Tool {tool_call.name} not supplied",
|
254
229
|
)
|
255
230
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
tool_results = await asyncio.gather(
|
266
|
-
*[
|
267
|
-
self._wrap_tool_handler(
|
268
|
-
options.tools[tool_call.name],
|
269
|
-
tool_call.arguments,
|
270
|
-
OnToolCallDetails(
|
271
|
-
id=tool_call.id,
|
272
|
-
name=tool_call.name,
|
273
|
-
conversation_uuid=result.uuid,
|
274
|
-
messages=result.conversation,
|
275
|
-
pause_execution=self._pause_tool_execution,
|
276
|
-
requested_tool_calls=result.tool_requests,
|
277
|
-
),
|
278
|
-
)
|
279
|
-
for tool_call in result.tool_requests
|
280
|
-
],
|
281
|
-
return_exceptions=False,
|
231
|
+
tool_result = await self._wrap_tool_handler(
|
232
|
+
tool_handler,
|
233
|
+
tool_call.arguments,
|
234
|
+
OnToolCallDetails(
|
235
|
+
id=tool_call.id,
|
236
|
+
name=tool_call.name,
|
237
|
+
arguments=tool_call.arguments,
|
238
|
+
),
|
282
239
|
)
|
283
240
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
]
|
294
|
-
)
|
295
|
-
for tool_result in tool_results
|
296
|
-
]
|
297
|
-
|
298
|
-
next_result = await self.chat(result.uuid, tool_messages, ChatPromptOptions(**dict(options)))
|
299
|
-
|
300
|
-
return FinishedResult(**dict(next_result)) if next_result else None
|
241
|
+
async with self._client.request(
|
242
|
+
handler=RequestHandler.ToolResults,
|
243
|
+
body=ToolResultsRequestBody(
|
244
|
+
tool_call_id=tool_call.id,
|
245
|
+
result=tool_result.result,
|
246
|
+
is_error=tool_result.is_error,
|
247
|
+
),
|
248
|
+
):
|
249
|
+
pass
|
301
250
|
|
302
251
|
async def get(self, path: str, options: Optional[GetPromptOptions] = None) -> GetPromptResult:
|
303
252
|
options = GetPromptOptions(**{**dict(self._options), **dict(options or {})})
|
@@ -364,22 +313,16 @@ class Prompts:
|
|
364
313
|
path=path,
|
365
314
|
parameters=options.parameters,
|
366
315
|
custom_identifier=options.custom_identifier,
|
316
|
+
tools=list(options.tools.keys()) if options.tools and options.stream else None,
|
367
317
|
stream=options.stream,
|
368
318
|
),
|
319
|
+
stream=options.stream,
|
369
320
|
) as response:
|
370
321
|
if options.stream:
|
371
|
-
result = await self._handle_stream(response.sse(), options.on_event)
|
322
|
+
result = await self._handle_stream(response.sse(), options.on_event, options.tools)
|
372
323
|
else:
|
373
324
|
result = RunPromptResult.model_validate_json(response.content)
|
374
325
|
|
375
|
-
if options.tools and result.tool_requests:
|
376
|
-
try:
|
377
|
-
# NOTE: The last sdk.chat called will already call on_finished
|
378
|
-
final_result = await self._handle_tool_calls(result, options)
|
379
|
-
return RunPromptResult(**dict(final_result)) if final_result else None
|
380
|
-
except OnToolCallPaused:
|
381
|
-
pass
|
382
|
-
|
383
326
|
if options.on_finished:
|
384
327
|
options.on_finished(FinishedResult(**dict(result)))
|
385
328
|
|
@@ -416,22 +359,16 @@ class Prompts:
|
|
416
359
|
),
|
417
360
|
body=ChatPromptRequestBody(
|
418
361
|
messages=messages,
|
362
|
+
tools=list(options.tools.keys()) if options.tools and options.stream else None,
|
419
363
|
stream=options.stream,
|
420
364
|
),
|
365
|
+
stream=options.stream,
|
421
366
|
) as response:
|
422
367
|
if options.stream:
|
423
|
-
result = await self._handle_stream(response.sse(), options.on_event)
|
368
|
+
result = await self._handle_stream(response.sse(), options.on_event, options.tools)
|
424
369
|
else:
|
425
370
|
result = ChatPromptResult.model_validate_json(response.content)
|
426
371
|
|
427
|
-
if options.tools and result.tool_requests:
|
428
|
-
try:
|
429
|
-
# NOTE: The last sdk.chat called will already call on_finished
|
430
|
-
final_result = await self._handle_tool_calls(result, options)
|
431
|
-
return ChatPromptResult(**dict(final_result)) if final_result else None
|
432
|
-
except OnToolCallPaused:
|
433
|
-
pass
|
434
|
-
|
435
372
|
if options.on_finished:
|
436
373
|
options.on_finished(FinishedResult(**dict(result)))
|
437
374
|
|
latitude_sdk/sdk/types.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import (
|
3
3
|
Any,
|
4
|
-
Callable,
|
5
4
|
List,
|
6
5
|
Literal,
|
7
6
|
Optional,
|
@@ -31,6 +30,10 @@ class Providers(StrEnum):
|
|
31
30
|
Google = "google"
|
32
31
|
GoogleVertex = "google_vertex"
|
33
32
|
AnthropicVertex = "anthropic_vertex"
|
33
|
+
XAI = "xai"
|
34
|
+
AmazonBedrock = "amazon_bedrock"
|
35
|
+
DeepSeek = "deepseek"
|
36
|
+
Perplexity = "perplexity"
|
34
37
|
Custom = "custom"
|
35
38
|
|
36
39
|
|
@@ -69,10 +72,6 @@ class FinishReason(StrEnum):
|
|
69
72
|
Unknown = "unknown"
|
70
73
|
|
71
74
|
|
72
|
-
AGENT_START_TOOL_NAME = "start_autonomous_chain"
|
73
|
-
AGENT_END_TOOL_NAME = "end_autonomous_chain"
|
74
|
-
|
75
|
-
|
76
75
|
class ToolCall(Model):
|
77
76
|
id: str
|
78
77
|
name: str
|
@@ -118,6 +117,23 @@ class StreamEvents(StrEnum):
|
|
118
117
|
Provider = "provider-event"
|
119
118
|
|
120
119
|
|
120
|
+
# NOTE: Incomplete list
|
121
|
+
class ProviderEvents(StrEnum):
|
122
|
+
ToolCalled = "tool-call"
|
123
|
+
|
124
|
+
|
125
|
+
# NOTE: Incomplete event
|
126
|
+
class GenericProviderEvent(Model):
|
127
|
+
event: Literal[StreamEvents.Provider] = StreamEvents.Provider
|
128
|
+
|
129
|
+
|
130
|
+
# NOTE: Incomplete event
|
131
|
+
class ProviderEventToolCalled(GenericProviderEvent, Model):
|
132
|
+
id: str = Field(alias=str("toolCallId"))
|
133
|
+
name: str = Field(alias=str("toolName"))
|
134
|
+
arguments: dict[str, Any] = Field(alias=str("args"))
|
135
|
+
|
136
|
+
|
121
137
|
ProviderEvent = dict[str, Any]
|
122
138
|
|
123
139
|
|
@@ -131,7 +147,6 @@ class ChainEvents(StrEnum):
|
|
131
147
|
StepCompleted = "step-completed"
|
132
148
|
ChainCompleted = "chain-completed"
|
133
149
|
ChainError = "chain-error"
|
134
|
-
ToolsRequested = "tools-requested"
|
135
150
|
|
136
151
|
|
137
152
|
class GenericChainEvent(Model):
|
@@ -140,20 +155,20 @@ class GenericChainEvent(Model):
|
|
140
155
|
uuid: str
|
141
156
|
|
142
157
|
|
143
|
-
class ChainEventChainStarted(GenericChainEvent):
|
158
|
+
class ChainEventChainStarted(GenericChainEvent, Model):
|
144
159
|
type: Literal[ChainEvents.ChainStarted] = ChainEvents.ChainStarted
|
145
160
|
|
146
161
|
|
147
|
-
class ChainEventStepStarted(GenericChainEvent):
|
162
|
+
class ChainEventStepStarted(GenericChainEvent, Model):
|
148
163
|
type: Literal[ChainEvents.StepStarted] = ChainEvents.StepStarted
|
149
164
|
|
150
165
|
|
151
|
-
class ChainEventProviderStarted(GenericChainEvent):
|
166
|
+
class ChainEventProviderStarted(GenericChainEvent, Model):
|
152
167
|
type: Literal[ChainEvents.ProviderStarted] = ChainEvents.ProviderStarted
|
153
168
|
config: dict[str, Any]
|
154
169
|
|
155
170
|
|
156
|
-
class ChainEventProviderCompleted(GenericChainEvent):
|
171
|
+
class ChainEventProviderCompleted(GenericChainEvent, Model):
|
157
172
|
type: Literal[ChainEvents.ProviderCompleted] = ChainEvents.ProviderCompleted
|
158
173
|
provider_log_uuid: str = Field(alias=str("providerLogUuid"))
|
159
174
|
token_usage: ModelUsage = Field(alias=str("tokenUsage"))
|
@@ -161,35 +176,30 @@ class ChainEventProviderCompleted(GenericChainEvent):
|
|
161
176
|
response: ChainResponse
|
162
177
|
|
163
178
|
|
164
|
-
class ChainEventToolsStarted(GenericChainEvent):
|
179
|
+
class ChainEventToolsStarted(GenericChainEvent, Model):
|
165
180
|
type: Literal[ChainEvents.ToolsStarted] = ChainEvents.ToolsStarted
|
166
181
|
tools: List[ToolCall]
|
167
182
|
|
168
183
|
|
169
|
-
class ChainEventToolCompleted(GenericChainEvent):
|
184
|
+
class ChainEventToolCompleted(GenericChainEvent, Model):
|
170
185
|
type: Literal[ChainEvents.ToolCompleted] = ChainEvents.ToolCompleted
|
171
186
|
|
172
187
|
|
173
|
-
class ChainEventStepCompleted(GenericChainEvent):
|
188
|
+
class ChainEventStepCompleted(GenericChainEvent, Model):
|
174
189
|
type: Literal[ChainEvents.StepCompleted] = ChainEvents.StepCompleted
|
175
190
|
|
176
191
|
|
177
|
-
class ChainEventChainCompleted(GenericChainEvent):
|
192
|
+
class ChainEventChainCompleted(GenericChainEvent, Model):
|
178
193
|
type: Literal[ChainEvents.ChainCompleted] = ChainEvents.ChainCompleted
|
179
194
|
token_usage: ModelUsage = Field(alias=str("tokenUsage"))
|
180
195
|
finish_reason: FinishReason = Field(alias=str("finishReason"))
|
181
196
|
|
182
197
|
|
183
|
-
class ChainEventChainError(GenericChainEvent):
|
198
|
+
class ChainEventChainError(GenericChainEvent, Model):
|
184
199
|
type: Literal[ChainEvents.ChainError] = ChainEvents.ChainError
|
185
200
|
error: ChainError
|
186
201
|
|
187
202
|
|
188
|
-
class ChainEventToolsRequested(GenericChainEvent):
|
189
|
-
type: Literal[ChainEvents.ToolsRequested] = ChainEvents.ToolsRequested
|
190
|
-
tools: List[ToolCall]
|
191
|
-
|
192
|
-
|
193
203
|
ChainEvent = Union[
|
194
204
|
ChainEventChainStarted,
|
195
205
|
ChainEventStepStarted,
|
@@ -200,7 +210,6 @@ ChainEvent = Union[
|
|
200
210
|
ChainEventStepCompleted,
|
201
211
|
ChainEventChainCompleted,
|
202
212
|
ChainEventChainError,
|
203
|
-
ChainEventToolsRequested,
|
204
213
|
]
|
205
214
|
|
206
215
|
LatitudeEvent = ChainEvent
|
@@ -211,8 +220,6 @@ class FinishedResult(Model):
|
|
211
220
|
uuid: str
|
212
221
|
conversation: List[Message]
|
213
222
|
response: ChainResponse
|
214
|
-
agent_response: Optional[dict[str, Any]] = Field(default=None, alias=str("agentResponse"))
|
215
|
-
tool_requests: List[ToolCall] = Field(alias=str("toolRequests"))
|
216
223
|
|
217
224
|
|
218
225
|
StreamEvent = Union[ProviderEvent, LatitudeEvent]
|
@@ -240,6 +247,37 @@ class Log(Model):
|
|
240
247
|
updated_at: datetime = Field(alias=str("updatedAt"))
|
241
248
|
|
242
249
|
|
250
|
+
class EvaluationResult(Model):
|
251
|
+
uuid: str
|
252
|
+
version_uuid: str = Field(alias=str("versionUuid"))
|
253
|
+
score: int
|
254
|
+
normalized_score: int = Field(alias=str("normalizedScore"))
|
255
|
+
metadata: dict[str, Any]
|
256
|
+
has_passed: bool = Field(alias=str("hasPassed"))
|
257
|
+
error: Optional[Union[str, None]] = None
|
258
|
+
created_at: datetime = Field(alias=str("createdAt"))
|
259
|
+
updated_at: datetime = Field(alias=str("updatedAt"))
|
260
|
+
|
261
|
+
|
262
|
+
class Project(Model):
|
263
|
+
id: int
|
264
|
+
uuid: Optional[str] = None
|
265
|
+
name: str
|
266
|
+
created_at: datetime = Field(alias=str("createdAt"))
|
267
|
+
updated_at: datetime = Field(alias=str("updatedAt"))
|
268
|
+
|
269
|
+
|
270
|
+
class Version(Model):
|
271
|
+
id: int
|
272
|
+
uuid: str
|
273
|
+
title: str
|
274
|
+
description: Optional[str] = None
|
275
|
+
project_id: int = Field(alias=str("projectId"))
|
276
|
+
created_at: datetime = Field(alias=str("createdAt"))
|
277
|
+
updated_at: datetime = Field(alias=str("updatedAt"))
|
278
|
+
merged_at: Optional[datetime] = Field(default=None, alias=str("mergedAt"))
|
279
|
+
|
280
|
+
|
243
281
|
class StreamCallbacks(Model):
|
244
282
|
@runtime_checkable
|
245
283
|
class OnEvent(Protocol):
|
@@ -263,10 +301,7 @@ class StreamCallbacks(Model):
|
|
263
301
|
class OnToolCallDetails(Model):
|
264
302
|
id: str
|
265
303
|
name: str
|
266
|
-
|
267
|
-
messages: List[Message]
|
268
|
-
pause_execution: Callable[[], ToolResult]
|
269
|
-
requested_tool_calls: List[ToolCall]
|
304
|
+
arguments: dict[str, Any]
|
270
305
|
|
271
306
|
|
272
307
|
@runtime_checkable
|
latitude_sdk/util/utils.py
CHANGED
@@ -5,6 +5,11 @@ from typing import Any, Callable, List, TypeVar
|
|
5
5
|
import pydantic
|
6
6
|
from typing_extensions import ParamSpec, Self
|
7
7
|
|
8
|
+
|
9
|
+
def get_package() -> str:
|
10
|
+
return (__package__ or __name__).split(".")[0].replace("_", "-")
|
11
|
+
|
12
|
+
|
8
13
|
T = TypeVar("T", str, bool, int, List[str])
|
9
14
|
|
10
15
|
|
@@ -0,0 +1 @@
|
|
1
|
+
from .version import *
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import re
|
2
|
+
from importlib.metadata import version as get_version
|
3
|
+
from typing import Union
|
4
|
+
|
5
|
+
from latitude_sdk.util import Model, get_package
|
6
|
+
|
7
|
+
_PEP440_PATTERN = re.compile(
|
8
|
+
r"^(\d+(?:\.\d+){0,2})" # major.minor.patch
|
9
|
+
r"(?:([abcu]|rc|u)(\d+))?" # prerelease+number
|
10
|
+
r"(?:\.dev(\d+))?$" # .dev+number
|
11
|
+
)
|
12
|
+
|
13
|
+
_PEP440_PRERELEASES = {
|
14
|
+
"a": "alpha",
|
15
|
+
"b": "beta",
|
16
|
+
"u": "unknown",
|
17
|
+
"rc": "rc",
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
def _to_semver(pepver: str) -> str:
|
22
|
+
version = _PEP440_PATTERN.match(pepver)
|
23
|
+
if not version:
|
24
|
+
return pepver
|
25
|
+
|
26
|
+
release, prerelease, prenumber, devnumber = version.groups()
|
27
|
+
|
28
|
+
parts = release.split(".")
|
29
|
+
major = parts[0] if len(parts) > 0 else "0"
|
30
|
+
minor = parts[1] if len(parts) > 1 else "0"
|
31
|
+
patch = parts[2] if len(parts) > 2 else "0"
|
32
|
+
|
33
|
+
semver = f"{major}.{minor}.{patch}"
|
34
|
+
if prerelease:
|
35
|
+
semver += f"-{_PEP440_PRERELEASES.get(prerelease, 'unknown')}"
|
36
|
+
if prenumber:
|
37
|
+
semver += f".{prenumber}"
|
38
|
+
if devnumber:
|
39
|
+
semver += f"+dev.{devnumber}"
|
40
|
+
|
41
|
+
return semver
|
42
|
+
|
43
|
+
|
44
|
+
def _to_info(pepver: str) -> tuple[Union[int, str], ...]:
|
45
|
+
return tuple(int(x) if x.isdigit() else x for x in pepver.split("."))
|
46
|
+
|
47
|
+
|
48
|
+
class Version(Model):
|
49
|
+
pep440: str
|
50
|
+
semver: str
|
51
|
+
info: tuple[Union[int, str], ...]
|
52
|
+
|
53
|
+
|
54
|
+
try:
|
55
|
+
_version = get_version(get_package())
|
56
|
+
except Exception:
|
57
|
+
_version = "0.0.0u0"
|
58
|
+
|
59
|
+
version = Version(
|
60
|
+
pep440=_version,
|
61
|
+
semver=_to_semver(_version),
|
62
|
+
info=_to_info(_version),
|
63
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: latitude-sdk
|
3
|
-
Version:
|
3
|
+
Version: 5.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,15 @@ 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
|
+
Specify the test inline:
|
66
|
+
|
67
|
+
```python
|
68
|
+
uv run scripts/test.py <test_path>::<test_case>::<test_name>
|
69
|
+
```
|
70
|
+
|
71
|
+
Or mark the test with an `only` marker:
|
64
72
|
|
65
73
|
```python
|
66
74
|
import pytest
|
@@ -70,18 +78,12 @@ async def my_test(self):
|
|
70
78
|
# ... your code
|
71
79
|
```
|
72
80
|
|
73
|
-
|
81
|
+
...and then run the tests with the marker `only`:
|
74
82
|
|
75
83
|
```sh
|
76
84
|
uv run scripts/test.py -m only
|
77
85
|
```
|
78
86
|
|
79
|
-
Other way is all in line:
|
80
|
-
|
81
|
-
```python
|
82
|
-
uv run scripts/test.py <test_path>::<test_case>::<test_name>
|
83
|
-
```
|
84
|
-
|
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.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
latitude_sdk/__init__.py,sha256=Xp6tiHlLtEQxfii5F_gTya5jZdbD63ySlnPI4pJU-58,147
|
2
|
+
latitude_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
latitude_sdk/client/__init__.py,sha256=d8CnNB8UoGwcftiIeeC0twdg01qNvfpj-v7O40I7IiE,68
|
4
|
+
latitude_sdk/client/client.py,sha256=lidk9T5aeocg_rpihl_FUnYTK2kZDtsCFa8IrCCM3tE,4815
|
5
|
+
latitude_sdk/client/payloads.py,sha256=fJftAEQpbNef8cknAZpt_07Ye8n_9EWRuhfoI-DrZvs,3035
|
6
|
+
latitude_sdk/client/router.py,sha256=k5OT42_3riJlTm5Q2BU3AplmpN1FdMxQeIj8GptcllQ,4616
|
7
|
+
latitude_sdk/env/__init__.py,sha256=66of5veJ-u1aNI025L65Rrj321AjrYevMqomTMYIrPQ,19
|
8
|
+
latitude_sdk/env/env.py,sha256=MnXexPOHE6aXcAszrDCbW7hzACUv4YtU1bfxpYwvHNw,455
|
9
|
+
latitude_sdk/sdk/__init__.py,sha256=UDgjaJ5vTIqv-DPYt4Dtav_7zWUYKcPn3T34oAP2Tps,161
|
10
|
+
latitude_sdk/sdk/errors.py,sha256=WNdieePfS6KRiOXUXlh3ryko_Uw_jRjZJg2XZamLYNg,1787
|
11
|
+
latitude_sdk/sdk/evaluations.py,sha256=mk_mvS8T4oymvMuKt_lAB1H6RCMfzNDcRT0mTKUYWu4,1451
|
12
|
+
latitude_sdk/sdk/latitude.py,sha256=0FjPKe3ZAl8YsOkTL77NKPFBoHrMFyg6SfQmKW-8h6E,2703
|
13
|
+
latitude_sdk/sdk/logs.py,sha256=CyHkRJvPl_p7wTSvR9bgxEI5akS0Tjc9FeQRb2C2vMg,1997
|
14
|
+
latitude_sdk/sdk/projects.py,sha256=C1f8eQ6Sz1plCvL-l0XDKorCTtz_r13eyG_rIYv29ew,1134
|
15
|
+
latitude_sdk/sdk/prompts.py,sha256=1tUNbuqRfZs963x5IOygGU36No-5nJiK3hIBzLEQ1sE,15156
|
16
|
+
latitude_sdk/sdk/types.py,sha256=MWJS5rcotbBQjIucvHBlreALMde6_NxJGP3iq5x9eK0,8603
|
17
|
+
latitude_sdk/util/__init__.py,sha256=alIDGBnxWH4JvP-UW-7N99seBBi0r1GV1h8f1ERFBec,21
|
18
|
+
latitude_sdk/util/utils.py,sha256=kbYWGMIMBsEV-u5woUgW77C-1mSUXyAo6VLuThQ_cj0,2942
|
19
|
+
latitude_sdk/version/__init__.py,sha256=av5cneIPX5Jet6040Rlaf9ODQFSRVirka1iagoQJYvA,23
|
20
|
+
latitude_sdk/version/version.py,sha256=J-Hw4S7ezvrqUHHbmTboBGNmo4l1LwGcbN6MiZX1Krc,1467
|
21
|
+
latitude_sdk-5.0.0b1.dist-info/METADATA,sha256=zCmM2wU49INrMNDRKe-9slQO9800TIxZCJMe3mMg4sI,2356
|
22
|
+
latitude_sdk-5.0.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
23
|
+
latitude_sdk-5.0.0b1.dist-info/licenses/LICENSE.md,sha256=yFReu_tr5pjxslWkQREfxA9yVm2r6gay2s6SFCh0XfQ,1073
|
24
|
+
latitude_sdk-5.0.0b1.dist-info/RECORD,,
|
@@ -1,21 +0,0 @@
|
|
1
|
-
latitude_sdk/__init__.py,sha256=-AbNXLmzDZeGbRdDIOpNjdCbacOvLBflSJwQtLlZfgk,19
|
2
|
-
latitude_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
latitude_sdk/client/__init__.py,sha256=d8CnNB8UoGwcftiIeeC0twdg01qNvfpj-v7O40I7IiE,68
|
4
|
-
latitude_sdk/client/client.py,sha256=Tlg0IU7_64O9yo0BfQEdVwVsjp-5_DUwJZqlX3uEb0w,4399
|
5
|
-
latitude_sdk/client/payloads.py,sha256=XiGadgaZhCMKa2auBPLln6RTEt8RwEqx_H8FoSv5h2c,2554
|
6
|
-
latitude_sdk/client/router.py,sha256=sdNvUNXqoM5TDmvN7toyJWuI8Lt4Z6MhKmWWKitEFes,4218
|
7
|
-
latitude_sdk/env/__init__.py,sha256=66of5veJ-u1aNI025L65Rrj321AjrYevMqomTMYIrPQ,19
|
8
|
-
latitude_sdk/env/env.py,sha256=MnXexPOHE6aXcAszrDCbW7hzACUv4YtU1bfxpYwvHNw,455
|
9
|
-
latitude_sdk/sdk/__init__.py,sha256=C9LlIjfnrS7KOK3-ruXKmbT77nSQMm23nZ6-t8sO8ME,137
|
10
|
-
latitude_sdk/sdk/errors.py,sha256=9GlGdDE8LGy3dE2Ry_BipBg-tDbQx7LWXJfSnTJSSBE,1747
|
11
|
-
latitude_sdk/sdk/evaluations.py,sha256=UP0DKMOLbqcCYrJcxiqgsqUM3anGs8pkAZu1DoOSwxI,1845
|
12
|
-
latitude_sdk/sdk/latitude.py,sha256=lUlGOiZXSFt0zm3sTHfBgjKzbcVJueMk6MXTGn9WCn8,2570
|
13
|
-
latitude_sdk/sdk/logs.py,sha256=CyHkRJvPl_p7wTSvR9bgxEI5akS0Tjc9FeQRb2C2vMg,1997
|
14
|
-
latitude_sdk/sdk/prompts.py,sha256=WztCpDSt0mxng0N2hyIrrzEw1TanqGRIl7Cpc_5ei4M,17369
|
15
|
-
latitude_sdk/sdk/types.py,sha256=e_AQ6YGRNgYvPJOxD5rndH5zPtslKMBhBtLocWlIHQE,7626
|
16
|
-
latitude_sdk/util/__init__.py,sha256=alIDGBnxWH4JvP-UW-7N99seBBi0r1GV1h8f1ERFBec,21
|
17
|
-
latitude_sdk/util/utils.py,sha256=hMOmF-u1QaDgOwXN6ME6n4TaQ70yZKLvijDUqNCMwXI,2844
|
18
|
-
latitude_sdk-3.0.2.dist-info/METADATA,sha256=g4QEGAQTK70uOBQ8Sp82rlqsvQ5IckTW2d1qEoAkmKw,2347
|
19
|
-
latitude_sdk-3.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
20
|
-
latitude_sdk-3.0.2.dist-info/licenses/LICENSE.md,sha256=yFReu_tr5pjxslWkQREfxA9yVm2r6gay2s6SFCh0XfQ,1073
|
21
|
-
latitude_sdk-3.0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|