latitude-sdk 0.1.0b7__py3-none-any.whl → 0.1.0b9__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.
@@ -41,6 +41,8 @@ class Evaluations:
41
41
  self._client = client
42
42
 
43
43
  async def trigger(self, uuid: str, options: TriggerEvaluationOptions) -> TriggerEvaluationResult:
44
+ options = TriggerEvaluationOptions(**{**dict(self._options), **dict(options)})
45
+
44
46
  async with self._client.request(
45
47
  handler=RequestHandler.TriggerEvaluation,
46
48
  params=TriggerEvaluationRequestParams(
@@ -55,6 +57,8 @@ class Evaluations:
55
57
  async def create_result(
56
58
  self, uuid: str, evaluation_uuid: str, options: CreateEvaluationResultOptions
57
59
  ) -> CreateEvaluationResultResult:
60
+ options = CreateEvaluationResultOptions(**{**dict(self._options), **dict(options)})
61
+
58
62
  async with self._client.request(
59
63
  handler=RequestHandler.CreateEvaluationResult,
60
64
  params=CreateEvaluationResultRequestParams(
@@ -1,6 +1,5 @@
1
1
  from typing import Optional
2
2
 
3
- from latitude_telemetry import InternalOptions as TelemetryInternalOptions
4
3
  from latitude_telemetry import Telemetry, TelemetryOptions
5
4
 
6
5
  from latitude_sdk.client import Client, ClientOptions, RouterOptions
@@ -40,7 +39,7 @@ DEFAULT_INTERNAL_OPTIONS = InternalOptions(
40
39
 
41
40
 
42
41
  DEFAULT_LATITUDE_OPTIONS = LatitudeOptions(
43
- telemetry=None, # Note: Telemetry is opt-in
42
+ telemetry=None, # NOTE: Telemetry is opt-in
44
43
  internal=DEFAULT_INTERNAL_OPTIONS,
45
44
  )
46
45
 
@@ -79,9 +78,6 @@ class Latitude:
79
78
  )
80
79
 
81
80
  if self._options.telemetry:
82
- self._options.telemetry.internal = TelemetryInternalOptions(
83
- **{**dict(self._options.internal), **dict(self._options.telemetry.internal or {})}
84
- )
85
81
  self.telemetry = Telemetry(api_key, self._options.telemetry)
86
82
 
87
83
  self.prompts = Prompts(self._client, self._options)
latitude_sdk/sdk/logs.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Iterable, Optional
1
+ from typing import Any, Dict, Optional, Sequence, Union
2
2
 
3
3
  from latitude_sdk.client import Client, CreateLogRequestBody, CreateLogRequestParams, RequestHandler
4
4
  from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes
@@ -6,6 +6,7 @@ from latitude_sdk.sdk.types import (
6
6
  Log,
7
7
  Message,
8
8
  SdkOptions,
9
+ _Message,
9
10
  )
10
11
  from latitude_sdk.util import Model
11
12
 
@@ -31,9 +32,8 @@ class Logs:
31
32
  self._options = options
32
33
  self._client = client
33
34
 
34
- def _ensure_options(self, options: LogOptions) -> LogOptions:
35
- project_id = options.project_id or self._options.project_id
36
- if not project_id:
35
+ def _ensure_log_options(self, options: LogOptions):
36
+ if not options.project_id:
37
37
  raise ApiError(
38
38
  status=404,
39
39
  code=ApiErrorCodes.NotFoundError,
@@ -41,16 +41,15 @@ class Logs:
41
41
  response="Project ID is required",
42
42
  )
43
43
 
44
- version_uuid = options.version_uuid or self._options.version_uuid
45
-
46
- return LogOptions(project_id=project_id, version_uuid=version_uuid)
47
-
48
- async def create(self, path: str, messages: Iterable[Message], options: CreateLogOptions) -> CreateLogResult:
49
- log_options = self._ensure_options(options)
50
- options = CreateLogOptions(**{**dict(options), **dict(log_options)})
51
-
44
+ async def create(
45
+ self, path: str, messages: Sequence[Union[Message, Dict[str, Any]]], options: CreateLogOptions
46
+ ) -> CreateLogResult:
47
+ options = CreateLogOptions(**{**dict(self._options), **dict(options)})
48
+ self._ensure_log_options(options)
52
49
  assert options.project_id is not None
53
50
 
51
+ messages = [_Message.validate_python(message) for message in messages]
52
+
54
53
  async with self._client.request(
55
54
  handler=RequestHandler.CreateLog,
56
55
  params=CreateLogRequestParams(
@@ -59,7 +58,7 @@ class Logs:
59
58
  ),
60
59
  body=CreateLogRequestBody(
61
60
  path=path,
62
- messages=list(messages),
61
+ messages=messages,
63
62
  response=options.response,
64
63
  ),
65
64
  ) as response:
@@ -1,4 +1,5 @@
1
- from typing import Any, AsyncGenerator, Dict, Iterable, List, Optional
1
+ import asyncio
2
+ from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence, Union
2
3
 
3
4
  from latitude_sdk.client import (
4
5
  ChatPromptRequestBody,
@@ -21,14 +22,25 @@ from latitude_sdk.sdk.types import (
21
22
  ChainEventStepCompleted,
22
23
  FinishedEvent,
23
24
  Message,
25
+ OnToolCall,
26
+ OnToolCallDetails,
24
27
  Prompt,
25
28
  SdkOptions,
26
29
  StreamCallbacks,
27
30
  StreamEvents,
31
+ StreamTypes,
32
+ ToolMessage,
33
+ ToolResult,
34
+ ToolResultContent,
35
+ _Message,
28
36
  )
29
37
  from latitude_sdk.util import Model
30
38
 
31
39
 
40
+ class OnToolCallPaused(Exception):
41
+ pass
42
+
43
+
32
44
  class PromptOptions(Model):
33
45
  project_id: Optional[int] = None
34
46
  version_uuid: Optional[str] = None
@@ -53,6 +65,7 @@ class GetOrCreatePromptResult(Prompt, Model):
53
65
  class RunPromptOptions(StreamCallbacks, PromptOptions, Model):
54
66
  custom_identifier: Optional[str] = None
55
67
  parameters: Optional[Dict[str, Any]] = None
68
+ tools: Optional[Dict[str, OnToolCall]] = None
56
69
  stream: Optional[bool] = None
57
70
 
58
71
 
@@ -61,6 +74,7 @@ class RunPromptResult(FinishedEvent, Model):
61
74
 
62
75
 
63
76
  class ChatPromptOptions(StreamCallbacks, Model):
77
+ tools: Optional[Dict[str, OnToolCall]] = None
64
78
  stream: Optional[bool] = None
65
79
 
66
80
 
@@ -76,9 +90,8 @@ class Prompts:
76
90
  self._options = options
77
91
  self._client = client
78
92
 
79
- def _ensure_options(self, options: PromptOptions) -> PromptOptions:
80
- project_id = options.project_id or self._options.project_id
81
- if not project_id:
93
+ def _ensure_prompt_options(self, options: PromptOptions):
94
+ if not options.project_id:
82
95
  raise ApiError(
83
96
  status=404,
84
97
  code=ApiErrorCodes.NotFoundError,
@@ -86,12 +99,8 @@ class Prompts:
86
99
  response="Project ID is required",
87
100
  )
88
101
 
89
- version_uuid = options.version_uuid or self._options.version_uuid
90
-
91
- return PromptOptions(project_id=project_id, version_uuid=version_uuid)
92
-
93
102
  async def _handle_stream(
94
- self, stream: AsyncGenerator[ClientEvent, Any], callbacks: StreamCallbacks
103
+ self, stream: AsyncGenerator[ClientEvent, Any], on_event: Optional[StreamCallbacks.OnEvent]
95
104
  ) -> FinishedEvent:
96
105
  uuid = None
97
106
  conversation: List[Message] = []
@@ -145,8 +154,8 @@ class Prompts:
145
154
  response=stream_event.data,
146
155
  )
147
156
 
148
- if callbacks.on_event:
149
- callbacks.on_event(event)
157
+ if on_event:
158
+ on_event(event)
150
159
 
151
160
  if not uuid or not response:
152
161
  raise ApiError(
@@ -159,10 +168,65 @@ class Prompts:
159
168
  # NOTE: FinishedEvent not in on_event
160
169
  return FinishedEvent(uuid=uuid, conversation=conversation, response=response)
161
170
 
162
- async def get(self, path: str, options: GetPromptOptions) -> GetPromptResult:
163
- prompt_options = self._ensure_options(options)
164
- options = GetPromptOptions(**{**dict(options), **dict(prompt_options)})
171
+ def _pause_tool_execution(self) -> ToolResult:
172
+ raise OnToolCallPaused()
173
+
174
+ async def _handle_tool_calls(
175
+ self, result: FinishedEvent, options: Union[RunPromptOptions, ChatPromptOptions]
176
+ ) -> Optional[FinishedEvent]:
177
+ # Seems Python cannot infer the type
178
+ assert result.response.type == StreamTypes.Text and result.response.tool_calls is not None
165
179
 
180
+ if not options.tools:
181
+ raise ApiError(
182
+ status=400,
183
+ code=ApiErrorCodes.AIRunError,
184
+ message="Tools not supplied",
185
+ response="Tools not supplied",
186
+ )
187
+
188
+ for tool_call in result.response.tool_calls:
189
+ if tool_call.name not in options.tools:
190
+ raise ApiError(
191
+ status=400,
192
+ code=ApiErrorCodes.AIRunError,
193
+ message=f"Tool {tool_call.name} not supplied",
194
+ response=f"Tool {tool_call.name} not supplied",
195
+ )
196
+
197
+ details = OnToolCallDetails(
198
+ conversation_uuid=result.uuid,
199
+ messages=result.conversation,
200
+ pause_execution=self._pause_tool_execution,
201
+ requested_tool_calls=result.response.tool_calls,
202
+ )
203
+
204
+ tool_results = await asyncio.gather(
205
+ *[options.tools[tool_call.name](tool_call, details) for tool_call in result.response.tool_calls],
206
+ return_exceptions=False,
207
+ )
208
+
209
+ tool_messages = [
210
+ ToolMessage(
211
+ content=[
212
+ ToolResultContent(
213
+ id=tool_result.id,
214
+ name=tool_result.name,
215
+ result=tool_result.result,
216
+ is_error=tool_result.is_error,
217
+ )
218
+ ]
219
+ )
220
+ for tool_result in tool_results
221
+ ]
222
+
223
+ next_result = await self.chat(result.uuid, tool_messages, ChatPromptOptions(**dict(options)))
224
+
225
+ return FinishedEvent(**dict(next_result)) if next_result else None
226
+
227
+ async def get(self, path: str, options: GetPromptOptions) -> GetPromptResult:
228
+ options = GetPromptOptions(**{**dict(self._options), **dict(options)})
229
+ self._ensure_prompt_options(options)
166
230
  assert options.project_id is not None
167
231
 
168
232
  async with self._client.request(
@@ -176,9 +240,8 @@ class Prompts:
176
240
  return GetPromptResult.model_validate_json(response.content)
177
241
 
178
242
  async def get_or_create(self, path: str, options: GetOrCreatePromptOptions) -> GetOrCreatePromptResult:
179
- prompt_options = self._ensure_options(options)
180
- options = GetOrCreatePromptOptions(**{**dict(options), **dict(prompt_options)})
181
-
243
+ options = GetOrCreatePromptOptions(**{**dict(self._options), **dict(options)})
244
+ self._ensure_prompt_options(options)
182
245
  assert options.project_id is not None
183
246
 
184
247
  async with self._client.request(
@@ -196,9 +259,8 @@ class Prompts:
196
259
 
197
260
  async def run(self, path: str, options: RunPromptOptions) -> Optional[RunPromptResult]:
198
261
  try:
199
- prompt_options = self._ensure_options(options)
200
- options = RunPromptOptions(**{**dict(options), **dict(prompt_options)})
201
-
262
+ options = RunPromptOptions(**{**dict(self._options), **dict(options)})
263
+ self._ensure_prompt_options(options)
202
264
  assert options.project_id is not None
203
265
 
204
266
  async with self._client.request(
@@ -215,21 +277,22 @@ class Prompts:
215
277
  ),
216
278
  ) as response:
217
279
  if options.stream:
218
- result = await self._handle_stream(
219
- response.sse(),
220
- callbacks=StreamCallbacks(
221
- on_event=options.on_event,
222
- on_finished=options.on_finished,
223
- on_error=options.on_error,
224
- ),
225
- )
280
+ result = await self._handle_stream(response.sse(), options.on_event)
226
281
  else:
227
282
  result = RunPromptResult.model_validate_json(response.content)
228
283
 
229
- if options.on_finished:
230
- options.on_finished(FinishedEvent(**dict(result)))
284
+ if options.tools and result.response.type == StreamTypes.Text and result.response.tool_calls:
285
+ try:
286
+ # NOTE: The last sdk.chat called will already call on_finished
287
+ final_result = await self._handle_tool_calls(result, options)
288
+ return RunPromptResult(**dict(final_result)) if final_result else None
289
+ except OnToolCallPaused:
290
+ pass
291
+
292
+ if options.on_finished:
293
+ options.on_finished(FinishedEvent(**dict(result)))
231
294
 
232
- return RunPromptResult(**dict(result))
295
+ return RunPromptResult(**dict(result))
233
296
 
234
297
  except Exception as exception:
235
298
  if not isinstance(exception, ApiError):
@@ -248,35 +311,40 @@ class Prompts:
248
311
  return None
249
312
 
250
313
  async def chat(
251
- self, uuid: str, messages: Iterable[Message], options: ChatPromptOptions
314
+ self, uuid: str, messages: Sequence[Union[Message, Dict[str, Any]]], options: ChatPromptOptions
252
315
  ) -> Optional[ChatPromptResult]:
253
316
  try:
317
+ options = ChatPromptOptions(**{**dict(self._options), **dict(options)})
318
+
319
+ messages = [_Message.validate_python(message) for message in messages]
320
+
254
321
  async with self._client.request(
255
322
  handler=RequestHandler.ChatPrompt,
256
323
  params=ChatPromptRequestParams(
257
324
  conversation_uuid=uuid,
258
325
  ),
259
326
  body=ChatPromptRequestBody(
260
- messages=list(messages),
327
+ messages=messages,
261
328
  stream=options.stream,
262
329
  ),
263
330
  ) as response:
264
331
  if options.stream:
265
- result = await self._handle_stream(
266
- response.sse(),
267
- callbacks=StreamCallbacks(
268
- on_event=options.on_event,
269
- on_finished=options.on_finished,
270
- on_error=options.on_error,
271
- ),
272
- )
332
+ result = await self._handle_stream(response.sse(), options.on_event)
273
333
  else:
274
334
  result = ChatPromptResult.model_validate_json(response.content)
275
335
 
276
- if options.on_finished:
277
- options.on_finished(FinishedEvent(**dict(result)))
336
+ if options.tools and result.response.type == StreamTypes.Text and result.response.tool_calls:
337
+ try:
338
+ # NOTE: The last sdk.chat called will already call on_finished
339
+ final_result = await self._handle_tool_calls(result, options)
340
+ return ChatPromptResult(**dict(final_result)) if final_result else None
341
+ except OnToolCallPaused:
342
+ pass
343
+
344
+ if options.on_finished:
345
+ options.on_finished(FinishedEvent(**dict(result)))
278
346
 
279
- return ChatPromptResult(**dict(result))
347
+ return ChatPromptResult(**dict(result))
280
348
 
281
349
  except Exception as exception:
282
350
  if not isinstance(exception, ApiError):
latitude_sdk/sdk/types.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from datetime import datetime
2
- from typing import Any, Dict, List, Literal, Optional, Protocol, Union, runtime_checkable
2
+ from typing import Any, Callable, Dict, List, Literal, Optional, Protocol, Union, runtime_checkable
3
3
 
4
4
  from latitude_sdk.sdk.errors import ApiError
5
- from latitude_sdk.util import Field, Model, StrEnum
5
+ from latitude_sdk.util import Adapter, Field, Model, StrEnum
6
6
 
7
7
 
8
8
  class DbErrorRef(Model):
@@ -63,7 +63,7 @@ class ToolResultContent(Model):
63
63
  type: Literal[ContentType.ToolResult] = ContentType.ToolResult
64
64
  id: str = Field(alias=str("toolCallId"))
65
65
  name: str = Field(alias=str("toolName"))
66
- result: str
66
+ result: Any
67
67
  is_error: Optional[bool] = Field(default=None, alias=str("isError"))
68
68
 
69
69
 
@@ -106,6 +106,7 @@ class ToolMessage(Model):
106
106
 
107
107
 
108
108
  Message = Union[SystemMessage, UserMessage, AssistantMessage, ToolMessage]
109
+ _Message = Adapter(Message)
109
110
 
110
111
 
111
112
  class ModelUsage(Model):
@@ -114,12 +115,29 @@ class ModelUsage(Model):
114
115
  total_tokens: int = Field(alias=str("totalTokens"))
115
116
 
116
117
 
118
+ class FinishReason(StrEnum):
119
+ Stop = "stop"
120
+ Length = "length"
121
+ ContentFilter = "content-filter"
122
+ ToolCalls = "tool-calls"
123
+ Error = "error"
124
+ Other = "other"
125
+ Unknown = "unknown"
126
+
127
+
117
128
  class ToolCall(Model):
118
129
  id: str
119
130
  name: str
120
131
  arguments: Dict[str, Any]
121
132
 
122
133
 
134
+ class ToolResult(Model):
135
+ id: str
136
+ name: str
137
+ result: Any
138
+ is_error: Optional[bool] = None
139
+
140
+
123
141
  class StreamTypes(StrEnum):
124
142
  Text = "text"
125
143
  Object = "object"
@@ -183,6 +201,7 @@ class ChainEventCompleted(Model):
183
201
  event: Literal[StreamEvents.Latitude] = StreamEvents.Latitude
184
202
  type: Literal[ChainEvents.Completed] = ChainEvents.Completed
185
203
  uuid: Optional[str] = None
204
+ finish_reason: FinishReason = Field(alias=str("finishReason"))
186
205
  config: Dict[str, Any]
187
206
  messages: Optional[List[Message]] = None
188
207
  object: Optional[Any] = None
@@ -275,9 +294,22 @@ class StreamCallbacks(Model):
275
294
  on_error: Optional[OnError] = None
276
295
 
277
296
 
297
+ class OnToolCallDetails(Model):
298
+ conversation_uuid: str
299
+ messages: List[Message]
300
+ pause_execution: Callable[[], ToolResult]
301
+ requested_tool_calls: List[ToolCall]
302
+
303
+
304
+ @runtime_checkable
305
+ class OnToolCall(Protocol):
306
+ async def __call__(self, call: ToolCall, details: OnToolCallDetails) -> ToolResult: ...
307
+
308
+
278
309
  class SdkOptions(Model):
279
310
  project_id: Optional[int] = None
280
311
  version_uuid: Optional[str] = None
312
+ tools: Optional[Dict[str, OnToolCall]] = None
281
313
 
282
314
 
283
315
  class GatewayOptions(Model):
@@ -63,6 +63,7 @@ class StrEnum(str, Enum):
63
63
 
64
64
  Field = pydantic.Field
65
65
  Config = pydantic.ConfigDict
66
+ Adapter = pydantic.TypeAdapter
66
67
 
67
68
 
68
69
  class Model(pydantic.BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: latitude-sdk
3
- Version: 0.1.0b7
3
+ Version: 0.1.0b9
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
@@ -11,7 +11,7 @@ License-Expression: LGPL-3.0
11
11
  Requires-Python: <3.13,>=3.9
12
12
  Requires-Dist: httpx-sse>=0.4.0
13
13
  Requires-Dist: httpx>=0.28.1
14
- Requires-Dist: latitude-telemetry>=0.1.0b5
14
+ Requires-Dist: latitude-telemetry>=0.1.0b6
15
15
  Requires-Dist: pydantic>=2.10.3
16
16
  Requires-Dist: typing-extensions>=4.12.2
17
17
  Description-Content-Type: text/markdown
@@ -8,13 +8,13 @@ latitude_sdk/env/__init__.py,sha256=66of5veJ-u1aNI025L65Rrj321AjrYevMqomTMYIrPQ,
8
8
  latitude_sdk/env/env.py,sha256=MnXexPOHE6aXcAszrDCbW7hzACUv4YtU1bfxpYwvHNw,455
9
9
  latitude_sdk/sdk/__init__.py,sha256=C9LlIjfnrS7KOK3-ruXKmbT77nSQMm23nZ6-t8sO8ME,137
10
10
  latitude_sdk/sdk/errors.py,sha256=9GlGdDE8LGy3dE2Ry_BipBg-tDbQx7LWXJfSnTJSSBE,1747
11
- latitude_sdk/sdk/evaluations.py,sha256=ASWfNfH124qeahzhAn-gb2Ep4QIew5uDveY5NbNsNfk,2086
12
- latitude_sdk/sdk/latitude.py,sha256=tVXxQeq9WAMv3ndsCE8V2cbzmGy3roUCMldhJiXOXoE,2985
13
- latitude_sdk/sdk/logs.py,sha256=UBlQzNq8AtWVkeVztqUWTs78jGkfA8a2r6wOhasUpxs,2056
14
- latitude_sdk/sdk/prompts.py,sha256=VaqI3qbVpcSbiT5xtd63eEwbIGTdMusDSoZeQZO8z4g,10134
15
- latitude_sdk/sdk/types.py,sha256=1sBtKNppipuayPj3j8qoUzMekIO0DxSBBXYNCEVsZjU,7866
11
+ latitude_sdk/sdk/evaluations.py,sha256=xmlFtnFxDtTfO4cJnnh6ExFnCQHan_b25KrH-I9MW6I,2267
12
+ latitude_sdk/sdk/latitude.py,sha256=XoSsM2_v3s_ndaMIeIrbWQT9f-1W1fAu6P75Zrw1iMQ,2724
13
+ latitude_sdk/sdk/logs.py,sha256=cvLFW4xNaJ5XHS9W2YgK18NC6c7N1oCo61Qe6CqXxag,1968
14
+ latitude_sdk/sdk/prompts.py,sha256=Aez9Y9IP5rKyQ3zeTs1zmbXwRatQrvsyNxaTcoTndQ8,12726
15
+ latitude_sdk/sdk/types.py,sha256=Mvu4cUOhIa8v6j0t91m5M1mD9X9Qr_ZWEtvgv7IZGas,8653
16
16
  latitude_sdk/util/__init__.py,sha256=alIDGBnxWH4JvP-UW-7N99seBBi0r1GV1h8f1ERFBec,21
17
- latitude_sdk/util/utils.py,sha256=MzEEIhM7yQKJ0btcf1U9rcRz7geHkYXEjGCDnLpNCSw,2885
18
- latitude_sdk-0.1.0b7.dist-info/METADATA,sha256=mKO1GdbeXDSYwRmKXM58mLJUl_2SyRFcqqQ3qarEG4c,2031
19
- latitude_sdk-0.1.0b7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- latitude_sdk-0.1.0b7.dist-info/RECORD,,
17
+ latitude_sdk/util/utils.py,sha256=06phYKGKnlO0WcU3coMXpqyrHOVDvE0mFzvqTRJGbD8,2916
18
+ latitude_sdk-0.1.0b9.dist-info/METADATA,sha256=FnKMDcMpee49w2lB8MT3mOBJiEUz3lcQ2T16tb69hZU,2031
19
+ latitude_sdk-0.1.0b9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ latitude_sdk-0.1.0b9.dist-info/RECORD,,