latitude-sdk 4.0.0b1__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 CHANGED
@@ -1 +1,7 @@
1
+ from .version import version
2
+
3
+ __version__ = version.pep440
4
+ __version_info__ = version.info
5
+ __version_semver__ = version.semver
6
+
1
7
  from .sdk import *
@@ -1,27 +1,19 @@
1
1
  import asyncio
2
2
  import json
3
3
  from contextlib import asynccontextmanager
4
- from typing import Any, AsyncGenerator, Awaitable, Callable, Optional
4
+ from typing import Any, AsyncGenerator, Optional
5
5
 
6
6
  import httpx
7
7
  import httpx_sse
8
8
 
9
- from latitude_sdk.client.payloads import (
10
- ErrorResponse,
11
- RequestBody,
12
- RequestHandler,
13
- RequestParams,
14
- )
9
+ from latitude_sdk.client.payloads import ErrorResponse, RequestBody, RequestHandler, RequestParams
15
10
  from latitude_sdk.client.router import Router, RouterOptions
16
- from latitude_sdk.sdk.errors import (
17
- ApiError,
18
- ApiErrorCodes,
19
- ApiErrorDbRef,
20
- )
11
+ from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes, ApiErrorDbRef
21
12
  from latitude_sdk.sdk.types import LogSources
22
13
  from latitude_sdk.util import Model
14
+ from latitude_sdk.version import version
23
15
 
24
- RETRIABLE_STATUSES = [408, 409, 429, 500, 502, 503, 504]
16
+ RETRIABLE_STATUSES = [408, 429, 500, 502, 503, 504]
25
17
 
26
18
  ClientEvent = httpx_sse.ServerSentEvent
27
19
 
@@ -60,165 +52,68 @@ class Client:
60
52
  handler: RequestHandler,
61
53
  params: Optional[RequestParams] = None,
62
54
  body: Optional[RequestBody] = None,
55
+ stream: Optional[bool] = None,
63
56
  ) -> 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,
57
+ client = httpx.AsyncClient(
58
+ headers={
59
+ "Authorization": f"Bearer {self.options.api_key}",
60
+ "X-Latitude-SDK-Version": version.semver,
61
+ "Content-Type": "application/json",
62
+ "Accept": "text/event-stream" if stream else "application/json",
63
+ },
118
64
  timeout=self.options.timeout,
119
65
  follow_redirects=False,
120
66
  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()
67
+ )
68
+ response = None
69
+ attempt = 1
184
70
 
185
- response.raise_for_status() # type: ignore
186
- return response # type: ignore
71
+ try:
72
+ method, url = self.router.resolve(handler, params)
73
+ content = None
74
+ if body:
75
+ content = json.dumps(
76
+ {
77
+ **json.loads(body.model_dump_json()),
78
+ "__internal": {"source": self.options.source},
79
+ }
80
+ )
81
+
82
+ while attempt <= self.options.retries:
83
+ try:
84
+ request = client.build_request(method=method, url=url, content=content)
85
+ response = await client.send(request=request, stream=stream or False)
86
+ response.raise_for_status()
87
+
88
+ yield response # pyright: ignore [reportReturnType]
89
+ break
187
90
 
188
- except Exception as exception:
189
- last_exception = exception
91
+ except Exception as exception:
92
+ if isinstance(exception, ApiError):
93
+ raise exception
190
94
 
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)
95
+ if attempt >= self.options.retries:
96
+ raise await self._exception(exception, response) from exception
196
97
 
197
- # Don't retry ApiErrors - they're business logic errors
198
- if isinstance(exception, ApiError):
199
- raise exception
98
+ if response and response.status_code in RETRIABLE_STATUSES:
99
+ await asyncio.sleep(self.options.delay * (2 ** (attempt - 1)))
100
+ else:
101
+ raise await self._exception(exception, response) from exception
200
102
 
201
- # If this is the last attempt, break to raise the exception
202
- if attempt >= self.options.retries:
203
- break
103
+ finally:
104
+ if response:
105
+ await response.aclose()
204
106
 
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
107
+ attempt += 1
212
108
 
213
- # For non-retriable errors, don't retry
214
- break
109
+ except Exception as exception:
110
+ if isinstance(exception, ApiError):
111
+ raise exception
215
112
 
216
- # If we get here, all retries failed
217
- raise await self._exception(last_exception, last_response) from last_exception # type: ignore
113
+ raise await self._exception(exception, response) from exception
218
114
 
219
- def _calculate_delay(self, attempt: int) -> float:
220
- """Calculate exponential backoff delay."""
221
- return self.options.delay * (2 ** (attempt - 1))
115
+ finally:
116
+ await client.aclose()
222
117
 
223
118
  async def _exception(self, exception: Exception, response: Optional[httpx.Response] = None) -> ApiError:
224
119
  if not response:
@@ -229,41 +124,17 @@ class Client:
229
124
  response=str(exception),
230
125
  )
231
126
 
232
- # Try to safely get response text and content, handling streaming responses
233
- response_text = ""
234
- response_content = b""
235
-
236
127
  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
128
+ if not response.is_stream_consumed:
129
+ await response.aread()
240
130
 
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)
131
+ error = ErrorResponse.model_validate_json(response.content)
261
132
 
262
133
  return ApiError(
263
134
  status=response.status_code,
264
135
  code=error.code,
265
136
  message=error.message,
266
- response=response_text,
137
+ response=response.text,
267
138
  db_ref=ApiErrorDbRef(**dict(error.db_ref)) if error.db_ref else None,
268
139
  )
269
140
 
@@ -272,5 +143,5 @@ class Client:
272
143
  status=response.status_code,
273
144
  code=ApiErrorCodes.InternalServerError,
274
145
  message=str(exception),
275
- response=response_text,
146
+ response=response.text,
276
147
  )
@@ -44,8 +44,8 @@ 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
- stream: Optional[bool] = None
48
47
  tools: Optional[List[str]] = None
48
+ stream: Optional[bool] = None
49
49
 
50
50
 
51
51
  class ChatPromptRequestParams(Model):
@@ -54,6 +54,7 @@ class ChatPromptRequestParams(Model):
54
54
 
55
55
  class ChatPromptRequestBody(Model):
56
56
  messages: List[Message]
57
+ tools: Optional[List[str]] = None
57
58
  stream: Optional[bool] = None
58
59
 
59
60
 
@@ -81,11 +82,10 @@ class AnnotateEvaluationRequestParams(EvaluationRequestParams, Model):
81
82
 
82
83
 
83
84
  class AnnotateEvaluationRequestBody(Model):
84
- score: int
85
-
86
85
  class Metadata(Model):
87
86
  reason: str
88
87
 
88
+ score: int
89
89
  metadata: Optional[Metadata] = None
90
90
 
91
91
 
@@ -2,5 +2,6 @@ from .errors import *
2
2
  from .evaluations import *
3
3
  from .latitude import *
4
4
  from .logs import *
5
+ from .projects import *
5
6
  from .prompts import *
6
7
  from .types import *
@@ -7,6 +7,7 @@ from latitude_sdk.util import Model, StrEnum
7
7
  class ApiErrorCodes(StrEnum):
8
8
  # LatitudeErrorCodes
9
9
  NotFoundError = "NotFoundError"
10
+ BadRequestError = "BadRequestError"
10
11
 
11
12
  # RunErrorCodes
12
13
  AIRunError = "ai_run_error"
@@ -1,5 +1,4 @@
1
- from datetime import datetime
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 Field, Model
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
- uuid: str
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:
@@ -78,6 +78,7 @@ class Latitude:
78
78
  )
79
79
 
80
80
  self.promptl = Promptl(self._options.promptl)
81
+
81
82
  self.projects = Projects(self._client, self._options)
82
83
  self.prompts = Prompts(self._client, self.promptl, self._options)
83
84
  self.logs = Logs(self._client, self._options)
@@ -1,41 +1,35 @@
1
- import json
2
1
  from typing import List
3
2
 
4
- from latitude_sdk.client import Client
5
- from latitude_sdk.client.payloads import CreateProjectRequestBody, RequestHandler
3
+ from latitude_sdk.client import Client, CreateProjectRequestBody, RequestHandler
6
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
7
 
8
+ _GetAllProjectResults = AdapterUtil[List[Project]](List[Project])
8
9
 
9
- class CreateProjectResponse:
10
- def __init__(self, project: Project, version: Version):
11
- self.project = project
12
- self.version = version
10
+
11
+ class CreateProjectResult(Model):
12
+ project: Project
13
+ version: Version
13
14
 
14
15
 
15
16
  class Projects:
16
- _client: Client
17
17
  _options: SdkOptions
18
+ _client: Client
18
19
 
19
20
  def __init__(self, client: Client, options: SdkOptions):
20
- self._client = client
21
21
  self._options = options
22
+ self._client = client
22
23
 
23
24
  async def get_all(self) -> List[Project]:
24
25
  async with self._client.request(
25
26
  handler=RequestHandler.GetAllProjects,
26
- params=None,
27
27
  ) as response:
28
- projects_data = json.loads(response.content)
29
- return [Project.model_validate_json(json.dumps(project)) for project in projects_data]
28
+ return _GetAllProjectResults.validate_json(response.content)
30
29
 
31
- async def create(self, name: str) -> CreateProjectResponse:
30
+ async def create(self, name: str) -> CreateProjectResult:
32
31
  async with self._client.request(
33
32
  handler=RequestHandler.CreateProject,
34
- params=None,
35
33
  body=CreateProjectRequestBody(name=name),
36
34
  ) 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
- )
35
+ return CreateProjectResult.model_validate_json(response.content)
@@ -1,11 +1,6 @@
1
- from typing import Any, AsyncGenerator, Callable, List, Optional, Sequence
1
+ from typing import Any, AsyncGenerator, List, Optional, Sequence
2
2
 
3
- from promptl_ai import (
4
- Adapter,
5
- Message,
6
- MessageLike,
7
- Promptl,
8
- )
3
+ from promptl_ai import Adapter, Message, MessageLike, Promptl
9
4
  from promptl_ai.bindings.types import _Message
10
5
 
11
6
  from latitude_sdk.client import (
@@ -30,6 +25,8 @@ from latitude_sdk.sdk.types import (
30
25
  OnToolCall,
31
26
  OnToolCallDetails,
32
27
  Prompt,
28
+ ProviderEvents,
29
+ ProviderEventToolCalled,
33
30
  Providers,
34
31
  SdkOptions,
35
32
  StreamCallbacks,
@@ -145,7 +142,7 @@ class Prompts:
145
142
  self,
146
143
  stream: AsyncGenerator[ClientEvent, Any],
147
144
  on_event: Optional[StreamCallbacks.OnEvent],
148
- on_tool_call: Optional[Callable[[dict[str, Any]], Any]] = None,
145
+ tools: Optional[dict[str, OnToolCall]],
149
146
  ) -> FinishedResult:
150
147
  uuid = None
151
148
  conversation: List[Message] = []
@@ -153,6 +150,7 @@ class Prompts:
153
150
 
154
151
  async for stream_event in stream:
155
152
  event = None
153
+ tool_call = None
156
154
 
157
155
  if stream_event.event == str(StreamEvents.Latitude):
158
156
  event = _LatitudeEvent.validate_json(stream_event.data)
@@ -174,9 +172,8 @@ class Prompts:
174
172
  event = stream_event.json()
175
173
  event["event"] = StreamEvents.Provider
176
174
 
177
- # Handle tool calls when received in the stream
178
- if on_tool_call and event.get("type") == "tool-call":
179
- await on_tool_call(event)
175
+ if event.get("type") == str(ProviderEvents.ToolCalled):
176
+ tool_call = ProviderEventToolCalled.model_validate_json(stream_event.data)
180
177
 
181
178
  else:
182
179
  raise ApiError(
@@ -189,6 +186,9 @@ class Prompts:
189
186
  if on_event:
190
187
  on_event(event)
191
188
 
189
+ if tool_call:
190
+ await self._handle_tool_call(tool_call, tools)
191
+
192
192
  if not uuid or not response:
193
193
  raise ApiError(
194
194
  status=500,
@@ -197,11 +197,7 @@ class Prompts:
197
197
  response="Stream ended without a chain-complete event. Missing uuid or response.",
198
198
  )
199
199
 
200
- return FinishedResult(
201
- uuid=uuid,
202
- conversation=conversation,
203
- response=response,
204
- )
200
+ return FinishedResult(uuid=uuid, conversation=conversation, response=response)
205
201
 
206
202
  @staticmethod
207
203
  async def _wrap_tool_handler(
@@ -212,66 +208,45 @@ class Prompts:
212
208
  try:
213
209
  result = await handler(arguments, details)
214
210
 
215
- return ToolResult(**tool_result, result=result)
211
+ return ToolResult(**tool_result, result=result, is_error=False)
216
212
  except Exception as exception:
217
213
  return ToolResult(**tool_result, result=str(exception), is_error=True)
218
214
 
219
215
  async def _handle_tool_call(
220
- self,
221
- event: dict[str, Any],
222
- tools: dict[str, OnToolCall],
216
+ self, tool_call: ProviderEventToolCalled, tools: Optional[dict[str, OnToolCall]]
223
217
  ) -> None:
224
- toolCallId: str = event["toolCallId"]
225
- toolName: str = event["toolName"]
226
- args: dict[str, Any] = event["args"]
227
-
228
- tool = tools.get(toolName)
229
- if not tool:
218
+ # NOTE: Do not handle tool calls if user specified no tools
219
+ if not tools:
230
220
  return
231
221
 
222
+ tool_handler = tools.get(tool_call.name)
223
+ if not tool_handler:
224
+ raise ApiError(
225
+ status=400,
226
+ code=ApiErrorCodes.AIRunError,
227
+ message=f"Tool {tool_call.name} not supplied",
228
+ response=f"Tool {tool_call.name} not supplied",
229
+ )
230
+
232
231
  tool_result = await self._wrap_tool_handler(
233
- tool,
234
- args,
232
+ tool_handler,
233
+ tool_call.arguments,
235
234
  OnToolCallDetails(
236
- id=toolCallId,
237
- name=toolName,
238
- arguments=args,
235
+ id=tool_call.id,
236
+ name=tool_call.name,
237
+ arguments=tool_call.arguments,
239
238
  ),
240
239
  )
241
240
 
242
- try:
243
- async with self._client.request(
244
- handler=RequestHandler.ToolResults,
245
- params=None,
246
- body=ToolResultsRequestBody(
247
- tool_call_id=toolCallId,
248
- result=tool_result.result,
249
- is_error=tool_result.is_error,
250
- ),
251
- ) as _:
252
- pass
253
- except Exception as exception:
254
- if not isinstance(exception, ApiError):
255
- exception = ApiError(
256
- status=500,
257
- code=ApiErrorCodes.InternalServerError,
258
- message=str(exception),
259
- response=str(exception),
260
- )
261
-
262
- # Add context about which tool failed
263
- message = f"Failed to execute tool {toolName}. \nLatitude API returned the following error:\
264
- \n\n{exception.message}"
265
-
266
- raise ApiError(
267
- status=exception.status,
268
- code=exception.code,
269
- message=message,
270
- response=exception.response,
271
- ) from exception
272
-
273
- def _on_tool_call(self, tools: dict[str, OnToolCall]) -> Callable[[dict[str, Any]], Any]:
274
- return lambda event: self._handle_tool_call(event, tools)
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
275
250
 
276
251
  async def get(self, path: str, options: Optional[GetPromptOptions] = None) -> GetPromptResult:
277
252
  options = GetPromptOptions(**{**dict(self._options), **dict(options or {})})
@@ -338,16 +313,13 @@ class Prompts:
338
313
  path=path,
339
314
  parameters=options.parameters,
340
315
  custom_identifier=options.custom_identifier,
341
- stream=options.stream,
342
316
  tools=list(options.tools.keys()) if options.tools and options.stream else None,
317
+ stream=options.stream,
343
318
  ),
319
+ stream=options.stream,
344
320
  ) as response:
345
321
  if options.stream:
346
- result = await self._handle_stream(
347
- response.sse(),
348
- options.on_event,
349
- self._on_tool_call(options.tools if options.tools else {}),
350
- )
322
+ result = await self._handle_stream(response.sse(), options.on_event, options.tools)
351
323
  else:
352
324
  result = RunPromptResult.model_validate_json(response.content)
353
325
 
@@ -373,10 +345,7 @@ class Prompts:
373
345
  return None
374
346
 
375
347
  async def chat(
376
- self,
377
- uuid: str,
378
- messages: Sequence[MessageLike],
379
- options: Optional[ChatPromptOptions] = None,
348
+ self, uuid: str, messages: Sequence[MessageLike], options: Optional[ChatPromptOptions] = None
380
349
  ) -> Optional[ChatPromptResult]:
381
350
  options = ChatPromptOptions(**{**dict(self._options), **dict(options or {})})
382
351
 
@@ -390,15 +359,13 @@ class Prompts:
390
359
  ),
391
360
  body=ChatPromptRequestBody(
392
361
  messages=messages,
362
+ tools=list(options.tools.keys()) if options.tools and options.stream else None,
393
363
  stream=options.stream,
394
364
  ),
365
+ stream=options.stream,
395
366
  ) as response:
396
367
  if options.stream:
397
- result = await self._handle_stream(
398
- response.sse(),
399
- options.on_event,
400
- self._on_tool_call(options.tools if options.tools else {}),
401
- )
368
+ result = await self._handle_stream(response.sse(), options.on_event, options.tools)
402
369
  else:
403
370
  result = ChatPromptResult.model_validate_json(response.content)
404
371
 
@@ -451,10 +418,7 @@ class Prompts:
451
418
  )
452
419
 
453
420
  async def render_chain(
454
- self,
455
- prompt: Prompt,
456
- on_step: OnStep,
457
- options: Optional[RenderChainOptions] = None,
421
+ self, prompt: Prompt, on_step: OnStep, options: Optional[RenderChainOptions] = None
458
422
  ) -> RenderChainResult:
459
423
  options = RenderChainOptions(**{**dict(self._options), **dict(options or {})})
460
424
  adapter = options.adapter or _PROVIDER_TO_ADAPTER.get(prompt.provider or Providers.OpenAI, Adapter.OpenAI)
latitude_sdk/sdk/types.py CHANGED
@@ -30,6 +30,10 @@ class Providers(StrEnum):
30
30
  Google = "google"
31
31
  GoogleVertex = "google_vertex"
32
32
  AnthropicVertex = "anthropic_vertex"
33
+ XAI = "xai"
34
+ AmazonBedrock = "amazon_bedrock"
35
+ DeepSeek = "deepseek"
36
+ Perplexity = "perplexity"
33
37
  Custom = "custom"
34
38
 
35
39
 
@@ -68,9 +72,6 @@ class FinishReason(StrEnum):
68
72
  Unknown = "unknown"
69
73
 
70
74
 
71
- AGENT_START_TOOL_NAME = "start_autonomous_chain"
72
-
73
-
74
75
  class ToolCall(Model):
75
76
  id: str
76
77
  name: str
@@ -116,6 +117,23 @@ class StreamEvents(StrEnum):
116
117
  Provider = "provider-event"
117
118
 
118
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
+
119
137
  ProviderEvent = dict[str, Any]
120
138
 
121
139
 
@@ -137,20 +155,20 @@ class GenericChainEvent(Model):
137
155
  uuid: str
138
156
 
139
157
 
140
- class ChainEventChainStarted(GenericChainEvent):
158
+ class ChainEventChainStarted(GenericChainEvent, Model):
141
159
  type: Literal[ChainEvents.ChainStarted] = ChainEvents.ChainStarted
142
160
 
143
161
 
144
- class ChainEventStepStarted(GenericChainEvent):
162
+ class ChainEventStepStarted(GenericChainEvent, Model):
145
163
  type: Literal[ChainEvents.StepStarted] = ChainEvents.StepStarted
146
164
 
147
165
 
148
- class ChainEventProviderStarted(GenericChainEvent):
166
+ class ChainEventProviderStarted(GenericChainEvent, Model):
149
167
  type: Literal[ChainEvents.ProviderStarted] = ChainEvents.ProviderStarted
150
168
  config: dict[str, Any]
151
169
 
152
170
 
153
- class ChainEventProviderCompleted(GenericChainEvent):
171
+ class ChainEventProviderCompleted(GenericChainEvent, Model):
154
172
  type: Literal[ChainEvents.ProviderCompleted] = ChainEvents.ProviderCompleted
155
173
  provider_log_uuid: str = Field(alias=str("providerLogUuid"))
156
174
  token_usage: ModelUsage = Field(alias=str("tokenUsage"))
@@ -158,26 +176,26 @@ class ChainEventProviderCompleted(GenericChainEvent):
158
176
  response: ChainResponse
159
177
 
160
178
 
161
- class ChainEventToolsStarted(GenericChainEvent):
179
+ class ChainEventToolsStarted(GenericChainEvent, Model):
162
180
  type: Literal[ChainEvents.ToolsStarted] = ChainEvents.ToolsStarted
163
181
  tools: List[ToolCall]
164
182
 
165
183
 
166
- class ChainEventToolCompleted(GenericChainEvent):
184
+ class ChainEventToolCompleted(GenericChainEvent, Model):
167
185
  type: Literal[ChainEvents.ToolCompleted] = ChainEvents.ToolCompleted
168
186
 
169
187
 
170
- class ChainEventStepCompleted(GenericChainEvent):
188
+ class ChainEventStepCompleted(GenericChainEvent, Model):
171
189
  type: Literal[ChainEvents.StepCompleted] = ChainEvents.StepCompleted
172
190
 
173
191
 
174
- class ChainEventChainCompleted(GenericChainEvent):
192
+ class ChainEventChainCompleted(GenericChainEvent, Model):
175
193
  type: Literal[ChainEvents.ChainCompleted] = ChainEvents.ChainCompleted
176
194
  token_usage: ModelUsage = Field(alias=str("tokenUsage"))
177
195
  finish_reason: FinishReason = Field(alias=str("finishReason"))
178
196
 
179
197
 
180
- class ChainEventChainError(GenericChainEvent):
198
+ class ChainEventChainError(GenericChainEvent, Model):
181
199
  type: Literal[ChainEvents.ChainError] = ChainEvents.ChainError
182
200
  error: ChainError
183
201
 
@@ -229,6 +247,18 @@ class Log(Model):
229
247
  updated_at: datetime = Field(alias=str("updatedAt"))
230
248
 
231
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
+
232
262
  class Project(Model):
233
263
  id: int
234
264
  uuid: Optional[str] = None
@@ -238,11 +268,14 @@ class Project(Model):
238
268
 
239
269
 
240
270
  class Version(Model):
271
+ id: int
241
272
  uuid: str
242
- name: str
273
+ title: str
274
+ description: Optional[str] = None
243
275
  project_id: int = Field(alias=str("projectId"))
244
276
  created_at: datetime = Field(alias=str("createdAt"))
245
277
  updated_at: datetime = Field(alias=str("updatedAt"))
278
+ merged_at: Optional[datetime] = Field(default=None, alias=str("mergedAt"))
246
279
 
247
280
 
248
281
  class StreamCallbacks(Model):
@@ -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: 4.0.0b1
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
@@ -62,7 +62,13 @@ Requires uv `0.5.10` or higher.
62
62
 
63
63
  ### Running only a specific test
64
64
 
65
- Mark the test with an `only` marker:
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:
66
72
 
67
73
  ```python
68
74
  import pytest
@@ -78,12 +84,6 @@ async def my_test(self):
78
84
  uv run scripts/test.py -m only
79
85
  ```
80
86
 
81
- Another way is to specify the test in line:
82
-
83
- ```python
84
- uv run scripts/test.py <test_path>::<test_case>::<test_name>
85
- ```
86
-
87
87
  ## License
88
88
 
89
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,22 +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=PR89Qy0hL0GQEEKs4eys7wc3BhIPF_P9N-AJB6ufrV8,9676
5
- latitude_sdk/client/payloads.py,sha256=z0tLYE9clybYaFWHgMVJgj9w-6exUQgfdB8egLw1-5E,2998
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=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=-569RfclTdfjuEIrbz0y6mH3o3LF7KAAWqphLPBN0dU,2702
13
- latitude_sdk/sdk/logs.py,sha256=CyHkRJvPl_p7wTSvR9bgxEI5akS0Tjc9FeQRb2C2vMg,1997
14
- latitude_sdk/sdk/projects.py,sha256=1l6Vvu01-Oi7tp2L1lAhWyZelWIqJVju6rJlR91MVCQ,1458
15
- latitude_sdk/sdk/prompts.py,sha256=pi2eAYssCcc6q6kwfzyYfV4z2gRoN_eAS1rXYEAobd8,15949
16
- latitude_sdk/sdk/types.py,sha256=lfSjZUrPed8tNHeJ4vi0HLvpWHFthN9MFCKWGcNY-vg,7481
17
- latitude_sdk/util/__init__.py,sha256=alIDGBnxWH4JvP-UW-7N99seBBi0r1GV1h8f1ERFBec,21
18
- latitude_sdk/util/utils.py,sha256=hMOmF-u1QaDgOwXN6ME6n4TaQ70yZKLvijDUqNCMwXI,2844
19
- latitude_sdk-4.0.0b1.dist-info/METADATA,sha256=o9eFEHQ7z5h7c-A5qpHwwbUDR6pLhO7Bhs-Gpo0u2tA,2372
20
- latitude_sdk-4.0.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- latitude_sdk-4.0.0b1.dist-info/licenses/LICENSE.md,sha256=yFReu_tr5pjxslWkQREfxA9yVm2r6gay2s6SFCh0XfQ,1073
22
- latitude_sdk-4.0.0b1.dist-info/RECORD,,