latitude-sdk 0.1.0b8__py3-none-any.whl → 1.0.0__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.
@@ -1,6 +1,8 @@
1
1
  from typing import Any, Dict, List, Optional, Union
2
2
 
3
- from latitude_sdk.sdk.types import DbErrorRef, Message
3
+ from promptl_ai import Message
4
+
5
+ from latitude_sdk.sdk.types import DbErrorRef
4
6
  from latitude_sdk.util import Field, Model, StrEnum
5
7
 
6
8
 
@@ -8,10 +8,7 @@ from latitude_sdk.client import (
8
8
  TriggerEvaluationRequestBody,
9
9
  TriggerEvaluationRequestParams,
10
10
  )
11
- from latitude_sdk.sdk.types import (
12
- EvaluationResult,
13
- SdkOptions,
14
- )
11
+ from latitude_sdk.sdk.types import EvaluationResult, SdkOptions
15
12
  from latitude_sdk.util import Model
16
13
 
17
14
 
@@ -40,7 +37,9 @@ class Evaluations:
40
37
  self._options = options
41
38
  self._client = client
42
39
 
43
- async def trigger(self, uuid: str, options: TriggerEvaluationOptions) -> TriggerEvaluationResult:
40
+ async def trigger(self, uuid: str, options: Optional[TriggerEvaluationOptions] = None) -> TriggerEvaluationResult:
41
+ options = TriggerEvaluationOptions(**{**dict(self._options), **dict(options or {})})
42
+
44
43
  async with self._client.request(
45
44
  handler=RequestHandler.TriggerEvaluation,
46
45
  params=TriggerEvaluationRequestParams(
@@ -55,6 +54,8 @@ class Evaluations:
55
54
  async def create_result(
56
55
  self, uuid: str, evaluation_uuid: str, options: CreateEvaluationResultOptions
57
56
  ) -> CreateEvaluationResultResult:
57
+ options = CreateEvaluationResultOptions(**{**dict(self._options), **dict(options)})
58
+
58
59
  async with self._client.request(
59
60
  handler=RequestHandler.CreateEvaluationResult,
60
61
  params=CreateEvaluationResultRequestParams(
@@ -1,6 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
3
  from latitude_telemetry import Telemetry, TelemetryOptions
4
+ from promptl_ai import Promptl, PromptlOptions
4
5
 
5
6
  from latitude_sdk.client import Client, ClientOptions, RouterOptions
6
7
  from latitude_sdk.env import env
@@ -20,6 +21,7 @@ class InternalOptions(Model):
20
21
 
21
22
 
22
23
  class LatitudeOptions(SdkOptions, Model):
24
+ promptl: Optional[PromptlOptions] = None
23
25
  telemetry: Optional[TelemetryOptions] = None
24
26
  internal: Optional[InternalOptions] = None
25
27
 
@@ -39,7 +41,7 @@ DEFAULT_INTERNAL_OPTIONS = InternalOptions(
39
41
 
40
42
 
41
43
  DEFAULT_LATITUDE_OPTIONS = LatitudeOptions(
42
- telemetry=None, # Note: Telemetry is opt-in
44
+ telemetry=None, # NOTE: Telemetry is opt-in
43
45
  internal=DEFAULT_INTERNAL_OPTIONS,
44
46
  )
45
47
 
@@ -48,15 +50,16 @@ class Latitude:
48
50
  _options: LatitudeOptions
49
51
  _client: Client
50
52
 
53
+ promptl: Promptl
51
54
  telemetry: Optional[Telemetry]
52
55
 
53
56
  prompts: Prompts
54
57
  logs: Logs
55
58
  evaluations: Evaluations
56
59
 
57
- def __init__(self, api_key: str, options: LatitudeOptions):
60
+ def __init__(self, api_key: str, options: Optional[LatitudeOptions] = None):
61
+ options = LatitudeOptions(**{**dict(DEFAULT_LATITUDE_OPTIONS), **dict(options or {})})
58
62
  options.internal = InternalOptions(**{**dict(DEFAULT_INTERNAL_OPTIONS), **dict(options.internal or {})})
59
- options = LatitudeOptions(**{**dict(DEFAULT_LATITUDE_OPTIONS), **dict(options)})
60
63
  self._options = options
61
64
 
62
65
  assert self._options.internal is not None
@@ -77,9 +80,10 @@ class Latitude:
77
80
  )
78
81
  )
79
82
 
83
+ self.promptl = Promptl(self._options.promptl)
80
84
  if self._options.telemetry:
81
85
  self.telemetry = Telemetry(api_key, self._options.telemetry)
82
86
 
83
- self.prompts = Prompts(self._client, self._options)
87
+ self.prompts = Prompts(self._client, self.promptl, self._options)
84
88
  self.logs = Logs(self._client, self._options)
85
89
  self.evaluations = Evaluations(self._client, self._options)
latitude_sdk/sdk/logs.py CHANGED
@@ -1,13 +1,11 @@
1
- from typing import Any, Dict, Optional, Sequence, Union
1
+ from typing import Optional, Sequence
2
+
3
+ from promptl_ai import MessageLike
4
+ from promptl_ai.bindings.types import _Message
2
5
 
3
6
  from latitude_sdk.client import Client, CreateLogRequestBody, CreateLogRequestParams, RequestHandler
4
7
  from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes
5
- from latitude_sdk.sdk.types import (
6
- Log,
7
- Message,
8
- SdkOptions,
9
- _Message,
10
- )
8
+ from latitude_sdk.sdk.types import Log, SdkOptions
11
9
  from latitude_sdk.util import Model
12
10
 
13
11
 
@@ -32,9 +30,8 @@ class Logs:
32
30
  self._options = options
33
31
  self._client = client
34
32
 
35
- def _ensure_options(self, options: LogOptions) -> LogOptions:
36
- project_id = options.project_id or self._options.project_id
37
- if not project_id:
33
+ def _ensure_log_options(self, options: LogOptions):
34
+ if not options.project_id:
38
35
  raise ApiError(
39
36
  status=404,
40
37
  code=ApiErrorCodes.NotFoundError,
@@ -42,16 +39,11 @@ class Logs:
42
39
  response="Project ID is required",
43
40
  )
44
41
 
45
- version_uuid = options.version_uuid or self._options.version_uuid
46
-
47
- return LogOptions(project_id=project_id, version_uuid=version_uuid)
48
-
49
42
  async def create(
50
- self, path: str, messages: Sequence[Union[Message, Dict[str, Any]]], options: CreateLogOptions
43
+ self, path: str, messages: Sequence[MessageLike], options: Optional[CreateLogOptions] = None
51
44
  ) -> CreateLogResult:
52
- log_options = self._ensure_options(options)
53
- options = CreateLogOptions(**{**dict(options), **dict(log_options)})
54
-
45
+ options = CreateLogOptions(**{**dict(self._options), **dict(options or {})})
46
+ self._ensure_log_options(options)
55
47
  assert options.project_id is not None
56
48
 
57
49
  messages = [_Message.validate_python(message) for message in messages]
@@ -1,5 +1,9 @@
1
+ import asyncio
1
2
  from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence, Union
2
3
 
4
+ from promptl_ai import Adapter, Message, MessageLike, Promptl, ToolMessage, ToolResultContent
5
+ from promptl_ai.bindings.types import _Message
6
+
3
7
  from latitude_sdk.client import (
4
8
  ChatPromptRequestBody,
5
9
  ChatPromptRequestParams,
@@ -20,15 +24,37 @@ from latitude_sdk.sdk.types import (
20
24
  ChainEventStep,
21
25
  ChainEventStepCompleted,
22
26
  FinishedEvent,
23
- Message,
27
+ OnStep,
28
+ OnToolCall,
29
+ OnToolCallDetails,
24
30
  Prompt,
31
+ Providers,
25
32
  SdkOptions,
26
33
  StreamCallbacks,
27
34
  StreamEvents,
28
- _Message,
35
+ StreamTypes,
36
+ ToolResult,
29
37
  )
30
38
  from latitude_sdk.util import Model
31
39
 
40
+ _PROVIDER_TO_ADAPTER = {
41
+ Providers.OpenAI: Adapter.OpenAI,
42
+ Providers.Anthropic: Adapter.Anthropic,
43
+ }
44
+
45
+ _PROMPT_ATTR_TO_ADAPTER_ATTR = {
46
+ "maxTokens": ("max_tokens", [Adapter.OpenAI, Adapter.Anthropic]),
47
+ "topP": ("top_p", [Adapter.OpenAI, Adapter.Anthropic]),
48
+ "topK": ("top_k", [Adapter.OpenAI, Adapter.Anthropic]),
49
+ "presencePenalty": ("presence_penalty", [Adapter.OpenAI, Adapter.Anthropic]),
50
+ "stopSequences": ("stop_sequences", [Adapter.OpenAI, Adapter.Anthropic]),
51
+ "toolChoice": ("tool_choice", [Adapter.OpenAI, Adapter.Anthropic]),
52
+ }
53
+
54
+
55
+ class OnToolCallPaused(Exception):
56
+ pass
57
+
32
58
 
33
59
  class PromptOptions(Model):
34
60
  project_id: Optional[int] = None
@@ -54,6 +80,7 @@ class GetOrCreatePromptResult(Prompt, Model):
54
80
  class RunPromptOptions(StreamCallbacks, PromptOptions, Model):
55
81
  custom_identifier: Optional[str] = None
56
82
  parameters: Optional[Dict[str, Any]] = None
83
+ tools: Optional[Dict[str, OnToolCall]] = None
57
84
  stream: Optional[bool] = None
58
85
 
59
86
 
@@ -62,6 +89,7 @@ class RunPromptResult(FinishedEvent, Model):
62
89
 
63
90
 
64
91
  class ChatPromptOptions(StreamCallbacks, Model):
92
+ tools: Optional[Dict[str, OnToolCall]] = None
65
93
  stream: Optional[bool] = None
66
94
 
67
95
 
@@ -69,17 +97,37 @@ class ChatPromptResult(FinishedEvent, Model):
69
97
  pass
70
98
 
71
99
 
100
+ class RenderPromptOptions(Model):
101
+ parameters: Optional[Dict[str, Any]] = None
102
+ adapter: Optional[Adapter] = None
103
+
104
+
105
+ class RenderPromptResult(Model):
106
+ messages: List[MessageLike]
107
+ config: Dict[str, Any]
108
+
109
+
110
+ class RenderChainOptions(Model):
111
+ parameters: Optional[Dict[str, Any]] = None
112
+ adapter: Optional[Adapter] = None
113
+
114
+
115
+ class RenderChainResult(RenderPromptResult, Model):
116
+ pass
117
+
118
+
72
119
  class Prompts:
73
120
  _options: SdkOptions
74
121
  _client: Client
122
+ _promptl: Promptl
75
123
 
76
- def __init__(self, client: Client, options: SdkOptions):
124
+ def __init__(self, client: Client, promptl: Promptl, options: SdkOptions):
77
125
  self._options = options
78
126
  self._client = client
127
+ self._promptl = promptl
79
128
 
80
- def _ensure_options(self, options: PromptOptions) -> PromptOptions:
81
- project_id = options.project_id or self._options.project_id
82
- if not project_id:
129
+ def _ensure_prompt_options(self, options: PromptOptions):
130
+ if not options.project_id:
83
131
  raise ApiError(
84
132
  status=404,
85
133
  code=ApiErrorCodes.NotFoundError,
@@ -87,12 +135,8 @@ class Prompts:
87
135
  response="Project ID is required",
88
136
  )
89
137
 
90
- version_uuid = options.version_uuid or self._options.version_uuid
91
-
92
- return PromptOptions(project_id=project_id, version_uuid=version_uuid)
93
-
94
138
  async def _handle_stream(
95
- self, stream: AsyncGenerator[ClientEvent, Any], callbacks: StreamCallbacks
139
+ self, stream: AsyncGenerator[ClientEvent, Any], on_event: Optional[StreamCallbacks.OnEvent]
96
140
  ) -> FinishedEvent:
97
141
  uuid = None
98
142
  conversation: List[Message] = []
@@ -146,8 +190,8 @@ class Prompts:
146
190
  response=stream_event.data,
147
191
  )
148
192
 
149
- if callbacks.on_event:
150
- callbacks.on_event(event)
193
+ if on_event:
194
+ on_event(event)
151
195
 
152
196
  if not uuid or not response:
153
197
  raise ApiError(
@@ -160,10 +204,65 @@ class Prompts:
160
204
  # NOTE: FinishedEvent not in on_event
161
205
  return FinishedEvent(uuid=uuid, conversation=conversation, response=response)
162
206
 
163
- async def get(self, path: str, options: GetPromptOptions) -> GetPromptResult:
164
- prompt_options = self._ensure_options(options)
165
- options = GetPromptOptions(**{**dict(options), **dict(prompt_options)})
207
+ def _pause_tool_execution(self) -> ToolResult:
208
+ raise OnToolCallPaused()
209
+
210
+ async def _handle_tool_calls(
211
+ self, result: FinishedEvent, options: Union[RunPromptOptions, ChatPromptOptions]
212
+ ) -> Optional[FinishedEvent]:
213
+ # Seems Python cannot infer the type
214
+ assert result.response.type == StreamTypes.Text and result.response.tool_calls is not None
215
+
216
+ if not options.tools:
217
+ raise ApiError(
218
+ status=400,
219
+ code=ApiErrorCodes.AIRunError,
220
+ message="Tools not supplied",
221
+ response="Tools not supplied",
222
+ )
166
223
 
224
+ for tool_call in result.response.tool_calls:
225
+ if tool_call.name not in options.tools:
226
+ raise ApiError(
227
+ status=400,
228
+ code=ApiErrorCodes.AIRunError,
229
+ message=f"Tool {tool_call.name} not supplied",
230
+ response=f"Tool {tool_call.name} not supplied",
231
+ )
232
+
233
+ details = OnToolCallDetails(
234
+ conversation_uuid=result.uuid,
235
+ messages=result.conversation,
236
+ pause_execution=self._pause_tool_execution,
237
+ requested_tool_calls=result.response.tool_calls,
238
+ )
239
+
240
+ tool_results = await asyncio.gather(
241
+ *[options.tools[tool_call.name](tool_call, details) for tool_call in result.response.tool_calls],
242
+ return_exceptions=False,
243
+ )
244
+
245
+ tool_messages = [
246
+ ToolMessage(
247
+ content=[
248
+ ToolResultContent(
249
+ id=tool_result.id,
250
+ name=tool_result.name,
251
+ result=tool_result.result,
252
+ is_error=tool_result.is_error,
253
+ )
254
+ ]
255
+ )
256
+ for tool_result in tool_results
257
+ ]
258
+
259
+ next_result = await self.chat(result.uuid, tool_messages, ChatPromptOptions(**dict(options)))
260
+
261
+ return FinishedEvent(**dict(next_result)) if next_result else None
262
+
263
+ async def get(self, path: str, options: Optional[GetPromptOptions] = None) -> GetPromptResult:
264
+ options = GetPromptOptions(**{**dict(self._options), **dict(options or {})})
265
+ self._ensure_prompt_options(options)
167
266
  assert options.project_id is not None
168
267
 
169
268
  async with self._client.request(
@@ -176,10 +275,11 @@ class Prompts:
176
275
  ) as response:
177
276
  return GetPromptResult.model_validate_json(response.content)
178
277
 
179
- async def get_or_create(self, path: str, options: GetOrCreatePromptOptions) -> GetOrCreatePromptResult:
180
- prompt_options = self._ensure_options(options)
181
- options = GetOrCreatePromptOptions(**{**dict(options), **dict(prompt_options)})
182
-
278
+ async def get_or_create(
279
+ self, path: str, options: Optional[GetOrCreatePromptOptions] = None
280
+ ) -> GetOrCreatePromptResult:
281
+ options = GetOrCreatePromptOptions(**{**dict(self._options), **dict(options or {})})
282
+ self._ensure_prompt_options(options)
183
283
  assert options.project_id is not None
184
284
 
185
285
  async with self._client.request(
@@ -195,13 +295,12 @@ class Prompts:
195
295
  ) as response:
196
296
  return GetOrCreatePromptResult.model_validate_json(response.content)
197
297
 
198
- async def run(self, path: str, options: RunPromptOptions) -> Optional[RunPromptResult]:
199
- try:
200
- prompt_options = self._ensure_options(options)
201
- options = RunPromptOptions(**{**dict(options), **dict(prompt_options)})
202
-
203
- assert options.project_id is not None
298
+ async def run(self, path: str, options: Optional[RunPromptOptions] = None) -> Optional[RunPromptResult]:
299
+ options = RunPromptOptions(**{**dict(self._options), **dict(options or {})})
300
+ self._ensure_prompt_options(options)
301
+ assert options.project_id is not None
204
302
 
303
+ try:
205
304
  async with self._client.request(
206
305
  handler=RequestHandler.RunPrompt,
207
306
  params=RunPromptRequestParams(
@@ -216,21 +315,22 @@ class Prompts:
216
315
  ),
217
316
  ) as response:
218
317
  if options.stream:
219
- result = await self._handle_stream(
220
- response.sse(),
221
- callbacks=StreamCallbacks(
222
- on_event=options.on_event,
223
- on_finished=options.on_finished,
224
- on_error=options.on_error,
225
- ),
226
- )
318
+ result = await self._handle_stream(response.sse(), options.on_event)
227
319
  else:
228
320
  result = RunPromptResult.model_validate_json(response.content)
229
321
 
230
- if options.on_finished:
231
- options.on_finished(FinishedEvent(**dict(result)))
322
+ if options.tools and result.response.type == StreamTypes.Text and result.response.tool_calls:
323
+ try:
324
+ # NOTE: The last sdk.chat called will already call on_finished
325
+ final_result = await self._handle_tool_calls(result, options)
326
+ return RunPromptResult(**dict(final_result)) if final_result else None
327
+ except OnToolCallPaused:
328
+ pass
329
+
330
+ if options.on_finished:
331
+ options.on_finished(FinishedEvent(**dict(result)))
232
332
 
233
- return RunPromptResult(**dict(result))
333
+ return RunPromptResult(**dict(result))
234
334
 
235
335
  except Exception as exception:
236
336
  if not isinstance(exception, ApiError):
@@ -249,11 +349,13 @@ class Prompts:
249
349
  return None
250
350
 
251
351
  async def chat(
252
- self, uuid: str, messages: Sequence[Union[Message, Dict[str, Any]]], options: ChatPromptOptions
352
+ self, uuid: str, messages: Sequence[MessageLike], options: Optional[ChatPromptOptions] = None
253
353
  ) -> Optional[ChatPromptResult]:
254
- try:
255
- messages = [_Message.validate_python(message) for message in messages]
354
+ options = ChatPromptOptions(**{**dict(self._options), **dict(options or {})})
256
355
 
356
+ messages = [_Message.validate_python(message) for message in messages]
357
+
358
+ try:
257
359
  async with self._client.request(
258
360
  handler=RequestHandler.ChatPrompt,
259
361
  params=ChatPromptRequestParams(
@@ -265,21 +367,22 @@ class Prompts:
265
367
  ),
266
368
  ) as response:
267
369
  if options.stream:
268
- result = await self._handle_stream(
269
- response.sse(),
270
- callbacks=StreamCallbacks(
271
- on_event=options.on_event,
272
- on_finished=options.on_finished,
273
- on_error=options.on_error,
274
- ),
275
- )
370
+ result = await self._handle_stream(response.sse(), options.on_event)
276
371
  else:
277
372
  result = ChatPromptResult.model_validate_json(response.content)
278
373
 
279
- if options.on_finished:
280
- options.on_finished(FinishedEvent(**dict(result)))
374
+ if options.tools and result.response.type == StreamTypes.Text and result.response.tool_calls:
375
+ try:
376
+ # NOTE: The last sdk.chat called will already call on_finished
377
+ final_result = await self._handle_tool_calls(result, options)
378
+ return ChatPromptResult(**dict(final_result)) if final_result else None
379
+ except OnToolCallPaused:
380
+ pass
281
381
 
282
- return ChatPromptResult(**dict(result))
382
+ if options.on_finished:
383
+ options.on_finished(FinishedEvent(**dict(result)))
384
+
385
+ return ChatPromptResult(**dict(result))
283
386
 
284
387
  except Exception as exception:
285
388
  if not isinstance(exception, ApiError):
@@ -297,6 +400,54 @@ class Prompts:
297
400
 
298
401
  return None
299
402
 
300
- # TODO: render - needs PromptL in Python
403
+ def _adapt_prompt_config(self, config: Dict[str, Any], adapter: Adapter) -> Dict[str, Any]:
404
+ adapted_config: Dict[str, Any] = {}
405
+
406
+ for attr, value in config.items():
407
+ if attr in _PROMPT_ATTR_TO_ADAPTER_ATTR and adapter in _PROMPT_ATTR_TO_ADAPTER_ATTR[attr][1]:
408
+ adapted_config[_PROMPT_ATTR_TO_ADAPTER_ATTR[attr][0]] = value
409
+ else:
410
+ adapted_config[attr] = value
411
+
412
+ return adapted_config
413
+
414
+ async def render(self, prompt: str, options: Optional[RenderPromptOptions] = None) -> RenderPromptResult:
415
+ options = RenderPromptOptions(**{**dict(self._options), **dict(options or {})})
416
+ adapter = options.adapter or Adapter.OpenAI
417
+
418
+ result = self._promptl.prompts.render(
419
+ prompt=prompt,
420
+ parameters=options.parameters,
421
+ adapter=adapter,
422
+ )
423
+
424
+ return RenderPromptResult(
425
+ messages=result.messages,
426
+ config=self._adapt_prompt_config(result.config, adapter),
427
+ )
428
+
429
+ async def render_chain(
430
+ self, prompt: Prompt, on_step: OnStep, options: Optional[RenderChainOptions] = None
431
+ ) -> RenderChainResult:
432
+ options = RenderChainOptions(**{**dict(self._options), **dict(options or {})})
433
+ adapter = options.adapter or _PROVIDER_TO_ADAPTER.get(prompt.provider or Providers.OpenAI, Adapter.OpenAI)
434
+
435
+ chain = self._promptl.chains.create(
436
+ prompt=prompt.content,
437
+ parameters=options.parameters,
438
+ adapter=adapter,
439
+ )
440
+
441
+ step = None
442
+ response = None
443
+ while not chain.completed:
444
+ step = chain.step(response)
445
+ if not step.completed:
446
+ response = await on_step(step.messages, self._adapt_prompt_config(step.config, adapter))
447
+
448
+ assert step is not None
301
449
 
302
- # TODO: render_chain - needs PromptL in Python
450
+ return RenderChainResult(
451
+ messages=step.messages,
452
+ config=self._adapt_prompt_config(step.config, adapter),
453
+ )
latitude_sdk/sdk/types.py CHANGED
@@ -1,8 +1,10 @@
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, Sequence, Union, runtime_checkable
3
+
4
+ from promptl_ai import Message, MessageLike
3
5
 
4
6
  from latitude_sdk.sdk.errors import ApiError
5
- from latitude_sdk.util import Adapter, Field, Model, StrEnum
7
+ from latitude_sdk.util import Field, Model, StrEnum
6
8
 
7
9
 
8
10
  class DbErrorRef(Model):
@@ -28,99 +30,35 @@ class Prompt(Model):
28
30
  provider: Optional[Providers] = None
29
31
 
30
32
 
31
- class ContentType(StrEnum):
32
- Text = "text"
33
- Image = "image"
34
- File = "file"
35
- ToolCall = "tool-call"
36
- ToolResult = "tool-result"
37
-
38
-
39
- class TextContent(Model):
40
- type: Literal[ContentType.Text] = ContentType.Text
41
- text: str
42
-
43
-
44
- class ImageContent(Model):
45
- type: Literal[ContentType.Image] = ContentType.Image
46
- image: str
47
-
48
-
49
- class FileContent(Model):
50
- type: Literal[ContentType.File] = ContentType.File
51
- file: str
52
- mime_type: str = Field(alias=str("mimeType"))
53
-
54
-
55
- class ToolCallContent(Model):
56
- type: Literal[ContentType.ToolCall] = ContentType.ToolCall
57
- id: str = Field(alias=str("toolCallId"))
58
- name: str = Field(alias=str("toolName"))
59
- arguments: Dict[str, Any] = Field(alias=str("args"))
60
-
61
-
62
- class ToolResultContent(Model):
63
- type: Literal[ContentType.ToolResult] = ContentType.ToolResult
64
- id: str = Field(alias=str("toolCallId"))
65
- name: str = Field(alias=str("toolName"))
66
- result: str
67
- is_error: Optional[bool] = Field(default=None, alias=str("isError"))
68
-
69
-
70
- MessageContent = Union[
71
- str,
72
- List[TextContent],
73
- List[ImageContent],
74
- List[FileContent],
75
- List[ToolCallContent],
76
- List[ToolResultContent],
77
- ]
78
-
79
-
80
- class MessageRole(StrEnum):
81
- System = "system"
82
- User = "user"
83
- Assistant = "assistant"
84
- Tool = "tool"
85
-
86
-
87
- class SystemMessage(Model):
88
- role: Literal[MessageRole.System] = MessageRole.System
89
- content: Union[str, List[TextContent]]
90
-
91
-
92
- class UserMessage(Model):
93
- role: Literal[MessageRole.User] = MessageRole.User
94
- content: Union[str, List[Union[TextContent, ImageContent, FileContent]]]
95
- name: Optional[str] = None
96
-
97
-
98
- class AssistantMessage(Model):
99
- role: Literal[MessageRole.Assistant] = MessageRole.Assistant
100
- content: Union[str, List[Union[TextContent, ToolCallContent]]]
101
-
102
-
103
- class ToolMessage(Model):
104
- role: Literal[MessageRole.Tool] = MessageRole.Tool
105
- content: List[ToolResultContent]
106
-
107
-
108
- Message = Union[SystemMessage, UserMessage, AssistantMessage, ToolMessage]
109
- _Message = Adapter(Message)
110
-
111
-
112
33
  class ModelUsage(Model):
113
34
  prompt_tokens: int = Field(alias=str("promptTokens"))
114
35
  completion_tokens: int = Field(alias=str("completionTokens"))
115
36
  total_tokens: int = Field(alias=str("totalTokens"))
116
37
 
117
38
 
39
+ class FinishReason(StrEnum):
40
+ Stop = "stop"
41
+ Length = "length"
42
+ ContentFilter = "content-filter"
43
+ ToolCalls = "tool-calls"
44
+ Error = "error"
45
+ Other = "other"
46
+ Unknown = "unknown"
47
+
48
+
118
49
  class ToolCall(Model):
119
50
  id: str
120
51
  name: str
121
52
  arguments: Dict[str, Any]
122
53
 
123
54
 
55
+ class ToolResult(Model):
56
+ id: str
57
+ name: str
58
+ result: Any
59
+ is_error: Optional[bool] = None
60
+
61
+
124
62
  class StreamTypes(StrEnum):
125
63
  Text = "text"
126
64
  Object = "object"
@@ -184,6 +122,7 @@ class ChainEventCompleted(Model):
184
122
  event: Literal[StreamEvents.Latitude] = StreamEvents.Latitude
185
123
  type: Literal[ChainEvents.Completed] = ChainEvents.Completed
186
124
  uuid: Optional[str] = None
125
+ finish_reason: FinishReason = Field(alias=str("finishReason"))
187
126
  config: Dict[str, Any]
188
127
  messages: Optional[List[Message]] = None
189
128
  object: Optional[Any] = None
@@ -276,9 +215,29 @@ class StreamCallbacks(Model):
276
215
  on_error: Optional[OnError] = None
277
216
 
278
217
 
218
+ class OnToolCallDetails(Model):
219
+ conversation_uuid: str
220
+ messages: List[Message]
221
+ pause_execution: Callable[[], ToolResult]
222
+ requested_tool_calls: List[ToolCall]
223
+
224
+
225
+ @runtime_checkable
226
+ class OnToolCall(Protocol):
227
+ async def __call__(self, call: ToolCall, details: OnToolCallDetails) -> ToolResult: ...
228
+
229
+
230
+ @runtime_checkable
231
+ class OnStep(Protocol):
232
+ async def __call__(
233
+ self, messages: List[MessageLike], config: Dict[str, Any]
234
+ ) -> Union[str, MessageLike, Sequence[MessageLike]]: ...
235
+
236
+
279
237
  class SdkOptions(Model):
280
238
  project_id: Optional[int] = None
281
239
  version_uuid: Optional[str] = None
240
+ tools: Optional[Dict[str, OnToolCall]] = None
282
241
 
283
242
 
284
243
  class GatewayOptions(Model):
@@ -64,6 +64,10 @@ class StrEnum(str, Enum):
64
64
  Field = pydantic.Field
65
65
  Config = pydantic.ConfigDict
66
66
  Adapter = pydantic.TypeAdapter
67
+ Aliases = pydantic.AliasChoices
68
+ Validator = pydantic.WrapValidator
69
+ ValidatorInfo = pydantic.ValidationInfo
70
+ ValidatorHandler = pydantic.ValidatorFunctionWrapHandler
67
71
 
68
72
 
69
73
  class Model(pydantic.BaseModel):
@@ -76,12 +80,12 @@ class Model(pydantic.BaseModel):
76
80
  @is_like(pydantic.BaseModel.model_dump)
77
81
  def model_dump(self, *args: Any, **kwargs: Any) -> Any:
78
82
  exclude_none = kwargs.pop("exclude_none", True)
79
- return super().model_dump(*args, exclude_none=exclude_none, **kwargs)
83
+ by_alias = kwargs.pop("by_alias", True)
84
+ return super().model_dump(*args, exclude_none=exclude_none, by_alias=by_alias, **kwargs)
80
85
 
81
86
  @is_like(pydantic.BaseModel.dict) # pyright: ignore [reportDeprecated]
82
87
  def dict(self, *args: Any, **kwargs: Any) -> Any:
83
- exclude_none = kwargs.pop("exclude_none", True)
84
- return super().dict(*args, exclude_none=exclude_none, **kwargs) # pyright: ignore [reportDeprecated]
88
+ raise NotImplementedError("deprecated")
85
89
 
86
90
  @is_like(pydantic.BaseModel.model_dump_json)
87
91
  def model_dump_json(self, *args: Any, **kwargs: Any) -> Any:
@@ -91,6 +95,4 @@ class Model(pydantic.BaseModel):
91
95
 
92
96
  @is_like(pydantic.BaseModel.json) # pyright: ignore [reportDeprecated]
93
97
  def json(self, *args: Any, **kwargs: Any) -> Any:
94
- exclude_none = kwargs.pop("exclude_none", True)
95
- by_alias = kwargs.pop("by_alias", True)
96
- return super().json(*args, exclude_none=exclude_none, by_alias=by_alias, **kwargs) # pyright: ignore [reportDeprecated]
98
+ raise NotImplementedError("deprecated")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: latitude-sdk
3
- Version: 0.1.0b8
3
+ Version: 1.0.0
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,8 @@ 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.0b6
14
+ Requires-Dist: latitude-telemetry>=1.0.0
15
+ Requires-Dist: promptl-ai>=1.0.1
15
16
  Requires-Dist: pydantic>=2.10.3
16
17
  Requires-Dist: typing-extensions>=4.12.2
17
18
  Description-Content-Type: text/markdown
@@ -2,19 +2,19 @@ latitude_sdk/__init__.py,sha256=-AbNXLmzDZeGbRdDIOpNjdCbacOvLBflSJwQtLlZfgk,19
2
2
  latitude_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  latitude_sdk/client/__init__.py,sha256=d8CnNB8UoGwcftiIeeC0twdg01qNvfpj-v7O40I7IiE,68
4
4
  latitude_sdk/client/client.py,sha256=Oc4COkVFR1vFewVKZzUIvztJi_yTxeSMoyeML-ivVsY,4389
5
- latitude_sdk/client/payloads.py,sha256=4fZS5jlTRRiA93QiDMUBh1utctI_9Xyh0fQV2bygiRU,2696
5
+ latitude_sdk/client/payloads.py,sha256=odYmsrK5F_Ym9wTCbwuBuiBRBKiScdSnnRn40ywGR3E,2719
6
6
  latitude_sdk/client/router.py,sha256=XYi24oUKL1wHDLvydpwLWCjy9tDGSVPN-zhetuvaj8I,4038
7
7
  latitude_sdk/env/__init__.py,sha256=66of5veJ-u1aNI025L65Rrj321AjrYevMqomTMYIrPQ,19
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=GdkwdxtTSmfldLo17pJme8MWpjuHQy7JFDRYLg0l4cg,2724
13
- latitude_sdk/sdk/logs.py,sha256=-_jYlxKW8Hgq1nZ7QtNbaEg1eWuMUbeeA32jtFsDvNk,2199
14
- latitude_sdk/sdk/prompts.py,sha256=vh2WYSEQbArnqadfJ9mBwtpmEBzEJObUkNB6O87S9QQ,10256
15
- latitude_sdk/sdk/types.py,sha256=RPJA3cM8tJ6udwS4gi0LLlJPPrS10mSb6q4OaKmNsU4,7903
11
+ latitude_sdk/sdk/evaluations.py,sha256=fDGtAWjdPG9OuKLit6u-jufVleC1EnshRplK6RN8iyg,2277
12
+ latitude_sdk/sdk/latitude.py,sha256=fdOWrAs0k3puLIhyU_zwvLsMyV_f1RxDWpV65_Euenc,2928
13
+ latitude_sdk/sdk/logs.py,sha256=CyHkRJvPl_p7wTSvR9bgxEI5akS0Tjc9FeQRb2C2vMg,1997
14
+ latitude_sdk/sdk/prompts.py,sha256=w3A-dhE_V1ueYjc8LBRXmQn5XZVCnsMdxs7Rau7P_E0,15818
15
+ latitude_sdk/sdk/types.py,sha256=D6wKkt5WSC0Ih6QT3QFSaU9YoxURKw8GKM7aH_uQ2-Q,6892
16
16
  latitude_sdk/util/__init__.py,sha256=alIDGBnxWH4JvP-UW-7N99seBBi0r1GV1h8f1ERFBec,21
17
- latitude_sdk/util/utils.py,sha256=06phYKGKnlO0WcU3coMXpqyrHOVDvE0mFzvqTRJGbD8,2916
18
- latitude_sdk-0.1.0b8.dist-info/METADATA,sha256=Lsolipk18n_fM7hvfIXaTDuJZ7rOQLXZIr_fWmq3eKw,2031
19
- latitude_sdk-0.1.0b8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- latitude_sdk-0.1.0b8.dist-info/RECORD,,
17
+ latitude_sdk/util/utils.py,sha256=hMOmF-u1QaDgOwXN6ME6n4TaQ70yZKLvijDUqNCMwXI,2844
18
+ latitude_sdk-1.0.0.dist-info/METADATA,sha256=nK2qNPBfe6nAZLSVYkMfaU-ub9Y4flAUXyKc6OMRr2k,2060
19
+ latitude_sdk-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ latitude_sdk-1.0.0.dist-info/RECORD,,