latitude-sdk 0.1.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.
@@ -0,0 +1,297 @@
1
+ from typing import Any, AsyncGenerator, Dict, List, Optional
2
+
3
+ from latitude_sdk.client import (
4
+ ChatPromptRequestBody,
5
+ ChatPromptRequestParams,
6
+ Client,
7
+ ClientEvent,
8
+ GetOrCreatePromptRequestBody,
9
+ GetOrCreatePromptRequestParams,
10
+ GetPromptRequestParams,
11
+ RequestHandler,
12
+ RunPromptRequestBody,
13
+ RunPromptRequestParams,
14
+ )
15
+ from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes
16
+ from latitude_sdk.sdk.types import (
17
+ ChainEventCompleted,
18
+ ChainEventError,
19
+ ChainEvents,
20
+ ChainEventStep,
21
+ ChainEventStepCompleted,
22
+ FinishedEvent,
23
+ Message,
24
+ Prompt,
25
+ SdkOptions,
26
+ StreamCallbacks,
27
+ StreamEvents,
28
+ )
29
+ from latitude_sdk.util import Model
30
+
31
+
32
+ class PromptOptions(Model):
33
+ project_id: Optional[int] = None
34
+ version_uuid: Optional[str] = None
35
+
36
+
37
+ class GetPromptOptions(PromptOptions, Model):
38
+ pass
39
+
40
+
41
+ class GetPromptResult(Prompt, Model):
42
+ pass
43
+
44
+
45
+ class GetOrCreatePromptOptions(PromptOptions, Model):
46
+ prompt: Optional[str] = None
47
+
48
+
49
+ class GetOrCreatePromptResult(Prompt, Model):
50
+ pass
51
+
52
+
53
+ class RunPromptOptions(StreamCallbacks, PromptOptions, Model):
54
+ custom_identifier: Optional[str] = None
55
+ parameters: Optional[Dict[str, Any]] = None
56
+ stream: Optional[bool] = None
57
+
58
+
59
+ class RunPromptResult(FinishedEvent, Model):
60
+ pass
61
+
62
+
63
+ class ChatPromptOptions(StreamCallbacks, Model):
64
+ stream: Optional[bool] = None
65
+
66
+
67
+ class ChatPromptResult(FinishedEvent, Model):
68
+ pass
69
+
70
+
71
+ class Prompts:
72
+ _options: SdkOptions
73
+ _client: Client
74
+
75
+ def __init__(self, client: Client, options: SdkOptions):
76
+ self._options = options
77
+ self._client = client
78
+
79
+ def _ensure_options(self, options: PromptOptions) -> PromptOptions:
80
+ project_id = options.project_id or self._options.project_id
81
+ if not project_id:
82
+ raise ApiError(
83
+ status=404,
84
+ code=ApiErrorCodes.NotFoundError,
85
+ message="Project ID is required",
86
+ response="Project ID is required",
87
+ )
88
+
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
+ async def _handle_stream(
94
+ self, stream: AsyncGenerator[ClientEvent, Any], callbacks: StreamCallbacks
95
+ ) -> FinishedEvent:
96
+ uuid = None
97
+ conversation: List[Message] = []
98
+ response = None
99
+
100
+ async for stream_event in stream:
101
+ event = None
102
+
103
+ if stream_event.event == str(StreamEvents.Latitude):
104
+ type = stream_event.json().get("type")
105
+
106
+ if type == str(ChainEvents.Step):
107
+ event = ChainEventStep.model_validate_json(stream_event.data)
108
+ conversation.extend(event.messages)
109
+
110
+ elif type == str(ChainEvents.StepCompleted):
111
+ event = ChainEventStepCompleted.model_validate_json(stream_event.data)
112
+
113
+ elif type == str(ChainEvents.Completed):
114
+ event = ChainEventCompleted.model_validate_json(stream_event.data)
115
+ uuid = event.uuid
116
+ conversation.extend(event.messages or [])
117
+ response = event.response
118
+
119
+ elif type == str(ChainEvents.Error):
120
+ event = ChainEventError.model_validate_json(stream_event.data)
121
+ raise ApiError(
122
+ status=500,
123
+ code=ApiErrorCodes.AIRunError,
124
+ message=event.error.message,
125
+ response=stream_event.data,
126
+ )
127
+
128
+ else:
129
+ raise ApiError(
130
+ status=500,
131
+ code=ApiErrorCodes.InternalServerError,
132
+ message=f"Unknown latitude event: {type}",
133
+ response=f"Unknown latitude event: {type}",
134
+ )
135
+
136
+ elif stream_event.event == str(StreamEvents.Provider):
137
+ event = stream_event.json()
138
+ event["event"] = StreamEvents.Provider
139
+
140
+ else:
141
+ raise ApiError(
142
+ status=500,
143
+ code=ApiErrorCodes.InternalServerError,
144
+ message=f"Unknown stream event: {stream_event.event}",
145
+ response=f"Unknown stream event: {stream_event.event}",
146
+ )
147
+
148
+ if callbacks.on_event:
149
+ callbacks.on_event(event)
150
+
151
+ if not uuid or not response:
152
+ raise ApiError(
153
+ status=500,
154
+ code=ApiErrorCodes.InternalServerError,
155
+ message="Stream ended without a chain-complete event. Missing uuid or response.",
156
+ response="Stream ended without a chain-complete event. Missing uuid or response.",
157
+ )
158
+
159
+ # NOTE: FinishedEvent not in on_event
160
+ return FinishedEvent(uuid=uuid, conversation=conversation, response=response)
161
+
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)})
165
+
166
+ assert options.project_id is not None
167
+
168
+ async with self._client.request(
169
+ handler=RequestHandler.GetPrompt,
170
+ params=GetPromptRequestParams(
171
+ project_id=options.project_id,
172
+ version_uuid=options.version_uuid,
173
+ path=path,
174
+ ),
175
+ ) as response:
176
+ return GetPromptResult.model_validate_json(response.content)
177
+
178
+ 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
+
182
+ assert options.project_id is not None
183
+
184
+ async with self._client.request(
185
+ handler=RequestHandler.GetOrCreatePrompt,
186
+ params=GetOrCreatePromptRequestParams(
187
+ project_id=options.project_id,
188
+ version_uuid=options.version_uuid,
189
+ ),
190
+ body=GetOrCreatePromptRequestBody(
191
+ path=path,
192
+ prompt=options.prompt,
193
+ ),
194
+ ) as response:
195
+ return GetOrCreatePromptResult.model_validate_json(response.content)
196
+
197
+ async def run(self, path: str, options: RunPromptOptions) -> Optional[RunPromptResult]:
198
+ try:
199
+ prompt_options = self._ensure_options(options)
200
+ options = RunPromptOptions(**{**dict(options), **dict(prompt_options)})
201
+
202
+ assert options.project_id is not None
203
+
204
+ async with self._client.request(
205
+ handler=RequestHandler.RunPrompt,
206
+ params=RunPromptRequestParams(
207
+ project_id=options.project_id,
208
+ version_uuid=options.version_uuid,
209
+ ),
210
+ body=RunPromptRequestBody(
211
+ path=path,
212
+ parameters=options.parameters,
213
+ custom_identifier=options.custom_identifier,
214
+ stream=options.stream,
215
+ ),
216
+ ) as response:
217
+ 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
+ )
226
+ else:
227
+ result = RunPromptResult.model_validate_json(response.content)
228
+
229
+ if options.on_finished:
230
+ options.on_finished(FinishedEvent(**dict(result)))
231
+
232
+ return RunPromptResult(**dict(result))
233
+
234
+ except Exception as exception:
235
+ if not isinstance(exception, ApiError):
236
+ exception = ApiError(
237
+ status=500,
238
+ code=ApiErrorCodes.InternalServerError,
239
+ message=str(exception),
240
+ response=str(exception),
241
+ )
242
+
243
+ if not options.on_error:
244
+ raise exception
245
+
246
+ options.on_error(exception)
247
+
248
+ return None
249
+
250
+ async def chat(self, uuid: str, messages: List[Message], options: ChatPromptOptions) -> Optional[ChatPromptResult]:
251
+ try:
252
+ async with self._client.request(
253
+ handler=RequestHandler.ChatPrompt,
254
+ params=ChatPromptRequestParams(
255
+ conversation_uuid=uuid,
256
+ ),
257
+ body=ChatPromptRequestBody(
258
+ messages=messages,
259
+ stream=options.stream,
260
+ ),
261
+ ) as response:
262
+ if options.stream:
263
+ result = await self._handle_stream(
264
+ response.sse(),
265
+ callbacks=StreamCallbacks(
266
+ on_event=options.on_event,
267
+ on_finished=options.on_finished,
268
+ on_error=options.on_error,
269
+ ),
270
+ )
271
+ else:
272
+ result = ChatPromptResult.model_validate_json(response.content)
273
+
274
+ if options.on_finished:
275
+ options.on_finished(FinishedEvent(**dict(result)))
276
+
277
+ return ChatPromptResult(**dict(result))
278
+
279
+ except Exception as exception:
280
+ if not isinstance(exception, ApiError):
281
+ exception = ApiError(
282
+ status=500,
283
+ code=ApiErrorCodes.InternalServerError,
284
+ message=str(exception),
285
+ response=str(exception),
286
+ )
287
+
288
+ if not options.on_error:
289
+ raise exception
290
+
291
+ options.on_error(exception)
292
+
293
+ return None
294
+
295
+ # TODO: render - needs PromptL in Python
296
+
297
+ # TODO: render_chain - needs PromptL in Python
@@ -0,0 +1,296 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
3
+
4
+ from latitude_sdk.sdk.errors import ApiError
5
+ from latitude_sdk.util import Field, Model, StrEnum
6
+
7
+
8
+ class DbErrorRef(Model):
9
+ entity_uuid: str = Field(alias=str("entityUuid"))
10
+ entity_type: str = Field(alias=str("entityType"))
11
+
12
+
13
+ class Providers(StrEnum):
14
+ OpenAI = "openai"
15
+ Anthropic = "anthropic"
16
+ Groq = "groq"
17
+ Mistral = "mistral"
18
+ Azure = "azure"
19
+ Google = "google"
20
+ Custom = "custom"
21
+
22
+
23
+ class Prompt(Model):
24
+ uuid: str
25
+ path: str
26
+ content: str
27
+ config: Dict[str, Any]
28
+ provider: Optional[Providers] = None
29
+
30
+
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: ContentType = ContentType.Text
41
+ text: str
42
+
43
+
44
+ class ImageContent(Model):
45
+ type: ContentType = ContentType.Image
46
+ image: str
47
+
48
+
49
+ class FileContent(Model):
50
+ type: ContentType = ContentType.File
51
+ file: str
52
+ mime_type: str = Field(alias=str("mimeType"))
53
+
54
+
55
+ class ToolCallContent(Model):
56
+ type: ContentType = ContentType.ToolCall
57
+ tool_call_id: str = Field(alias=str("toolCallId"))
58
+ tool_name: str = Field(alias=str("toolName"))
59
+ args: Dict[str, Any]
60
+
61
+
62
+ class ToolResultContent(Model):
63
+ type: ContentType = ContentType.ToolResult
64
+ tool_call_id: str = Field(alias=str("toolCallId"))
65
+ tool_name: str = Field(alias=str("toolName"))
66
+ result: str
67
+ is_error: Optional[bool] = Field(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: MessageRole = MessageRole.System
89
+ content: Union[str, List[TextContent]]
90
+
91
+
92
+ class UserMessage(Model):
93
+ role: MessageRole = MessageRole.User
94
+ content: Union[str, List[TextContent], List[ImageContent], List[FileContent]]
95
+ name: Optional[str] = None
96
+
97
+
98
+ class ToolCall(Model):
99
+ id: str
100
+ name: str
101
+ arguments: Dict[str, Any]
102
+
103
+
104
+ class AssistantMessage(Model):
105
+ role: MessageRole = MessageRole.Assistant
106
+ content: Union[str, List[TextContent], List[ToolCallContent]]
107
+ tool_calls: Optional[List[ToolCall]] = Field(None, alias=str("toolCalls"))
108
+
109
+
110
+ class ToolMessage(Model):
111
+ role: MessageRole = MessageRole.Tool
112
+ content: List[ToolResultContent]
113
+
114
+
115
+ Message = Union[SystemMessage, UserMessage, AssistantMessage, ToolMessage]
116
+
117
+
118
+ class ModelUsage(Model):
119
+ prompt_tokens: int = Field(alias=str("promptTokens"))
120
+ completion_tokens: int = Field(alias=str("completionTokens"))
121
+ total_tokens: int = Field(alias=str("totalTokens"))
122
+
123
+
124
+ class StreamTypes(StrEnum):
125
+ Text = "text"
126
+ Object = "object"
127
+
128
+
129
+ class ChainTextResponse(Model):
130
+ type: StreamTypes = StreamTypes.Text
131
+ text: str
132
+ tool_calls: Optional[List[ToolCall]] = Field(None, alias=str("toolCalls"))
133
+ usage: ModelUsage
134
+
135
+
136
+ class ChainObjectResponse(Model):
137
+ type: StreamTypes = StreamTypes.Object
138
+ object: Any
139
+ usage: ModelUsage
140
+
141
+
142
+ ChainResponse = Union[ChainTextResponse, ChainObjectResponse]
143
+
144
+
145
+ class ChainError(Model):
146
+ name: str
147
+ message: str
148
+ stack: Optional[str] = None
149
+
150
+
151
+ class StreamEvents(StrEnum):
152
+ Latitude = "latitude-event"
153
+ Provider = "provider-event"
154
+ Finished = "finished-event"
155
+
156
+
157
+ ProviderEvent = Dict[str, Any]
158
+
159
+
160
+ class ChainEvents(StrEnum):
161
+ Step = "chain-step"
162
+ StepCompleted = "chain-step-complete"
163
+ Completed = "chain-complete"
164
+ Error = "chain-error"
165
+
166
+
167
+ class ChainEventStep(Model):
168
+ event: StreamEvents = StreamEvents.Latitude
169
+ type: ChainEvents = ChainEvents.Step
170
+ config: Dict[str, Any]
171
+ is_last_step: bool = Field(alias=str("isLastStep"))
172
+ messages: List[Message]
173
+ uuid: Optional[str] = None
174
+
175
+
176
+ class ChainEventStepCompleted(Model):
177
+ event: StreamEvents = StreamEvents.Latitude
178
+ type: ChainEvents = ChainEvents.StepCompleted
179
+ response: ChainResponse
180
+ uuid: Optional[str] = None
181
+
182
+
183
+ class ChainEventCompleted(Model):
184
+ event: StreamEvents = StreamEvents.Latitude
185
+ type: ChainEvents = ChainEvents.Completed
186
+ config: Dict[str, Any]
187
+ messages: Optional[List[Message]] = None
188
+ object: Optional[Any] = None
189
+ response: ChainResponse
190
+ uuid: Optional[str] = None
191
+
192
+
193
+ class ChainEventError(Model):
194
+ event: StreamEvents = StreamEvents.Latitude
195
+ type: ChainEvents = ChainEvents.Error
196
+ error: ChainError
197
+
198
+
199
+ ChainEvent = Union[ChainEventStep, ChainEventStepCompleted, ChainEventCompleted, ChainEventError]
200
+
201
+
202
+ LatitudeEvent = ChainEvent
203
+
204
+
205
+ class FinishedEvent(Model):
206
+ event: StreamEvents = StreamEvents.Finished
207
+ uuid: str
208
+ conversation: List[Message]
209
+ response: ChainResponse
210
+
211
+
212
+ StreamEvent = Union[ProviderEvent, LatitudeEvent, FinishedEvent]
213
+
214
+
215
+ class LogSources(StrEnum):
216
+ Api = "api"
217
+ Playground = "playground"
218
+ Evaluation = "evaluation"
219
+ User = "user"
220
+ SharedPrompt = "shared_prompt"
221
+
222
+
223
+ class Log(Model):
224
+ id: int
225
+ uuid: str
226
+ source: Optional[LogSources] = None
227
+ commit_id: int = Field(alias=str("commitId"))
228
+ resolved_content: str = Field(alias=str("resolvedContent"))
229
+ content_hash: str = Field(alias=str("contentHash"))
230
+ parameters: Dict[str, Any]
231
+ custom_identifier: Optional[str] = Field(None, alias=str("customIdentifier"))
232
+ duration: Optional[int] = None
233
+ created_at: datetime = Field(alias=str("createdAt"))
234
+ updated_at: datetime = Field(alias=str("updatedAt"))
235
+
236
+
237
+ class EvaluationResultType(StrEnum):
238
+ Boolean = "evaluation_resultable_booleans"
239
+ Text = "evaluation_resultable_texts"
240
+ Number = "evaluation_resultable_numbers"
241
+
242
+
243
+ class EvaluationResult(Model):
244
+ id: int
245
+ uuid: str
246
+ evaluation_id: int = Field(alias=str("evaluationId"))
247
+ document_log_id: int = Field(alias=str("documentLogId"))
248
+ evaluated_provider_log_id: Optional[int] = Field(None, alias=str("evaluatedProviderLogId"))
249
+ evaluation_provider_log_id: Optional[int] = Field(None, alias=str("evaluationProviderLogId"))
250
+ resultable_type: Optional[EvaluationResultType] = Field(None, alias=str("resultableType"))
251
+ resultable_id: Optional[int] = Field(None, alias=str("resultableId"))
252
+ result: Optional[Union[str, bool, int]] = None
253
+ source: Optional[LogSources] = None
254
+ reason: Optional[str] = None
255
+ created_at: datetime = Field(alias=str("createdAt"))
256
+ updated_at: datetime = Field(alias=str("updatedAt"))
257
+
258
+
259
+ class StreamCallbacks(Model):
260
+ @runtime_checkable
261
+ class OnEvent(Protocol):
262
+ def __call__(self, event: StreamEvent): ...
263
+
264
+ on_event: Optional[OnEvent] = None
265
+
266
+ @runtime_checkable
267
+ class OnFinished(Protocol):
268
+ def __call__(self, event: FinishedEvent): ...
269
+
270
+ on_finished: Optional[OnFinished] = None
271
+
272
+ @runtime_checkable
273
+ class OnError(Protocol):
274
+ def __call__(self, error: ApiError): ...
275
+
276
+ on_error: Optional[OnError] = None
277
+
278
+
279
+ class SdkOptions(Model):
280
+ project_id: Optional[int] = None
281
+ version_uuid: Optional[str] = None
282
+
283
+
284
+ class GatewayOptions(Model):
285
+ host: str
286
+ port: int
287
+ ssl: bool
288
+ api_version: str
289
+
290
+ @property
291
+ def protocol(self) -> str:
292
+ return "https" if self.ssl else "http"
293
+
294
+ @property
295
+ def base_url(self) -> str:
296
+ return f"{self.protocol}://{self.host}:{self.port}/api/{self.api_version}"
@@ -0,0 +1 @@
1
+ from .utils import *
@@ -0,0 +1,87 @@
1
+ import os
2
+ from enum import Enum
3
+ from typing import Any, Callable, List, TypeVar
4
+
5
+ import pydantic
6
+ from typing_extensions import ParamSpec
7
+
8
+ T = TypeVar("T", str, bool, int, List[str])
9
+
10
+
11
+ def get_env(key: str, default: T) -> T:
12
+ value = os.getenv(key)
13
+ if not value:
14
+ return default
15
+
16
+ if isinstance(default, str):
17
+ return value
18
+
19
+ elif isinstance(default, bool):
20
+ return value.lower() in ["true", "1", "yes", "on"]
21
+
22
+ elif isinstance(default, int):
23
+ return int(value)
24
+
25
+ elif isinstance(default, list):
26
+ return value.split(",")
27
+
28
+ raise TypeError(f"Unknown type {type(default)}")
29
+
30
+
31
+ P = ParamSpec("P")
32
+ R = TypeVar("R")
33
+
34
+
35
+ def returns_identity(x: R) -> R:
36
+ return x
37
+
38
+
39
+ def is_like(func: Callable[P, R]) -> Callable[[Callable[..., Any]], Callable[P, R]]:
40
+ return returns_identity # type: ignore
41
+
42
+
43
+ def returns_like(func: Callable[..., R]) -> Callable[[Callable[P, Any]], Callable[P, R]]:
44
+ return returns_identity # type: ignore
45
+
46
+
47
+ class StrEnum(str, Enum):
48
+ def __str__(self) -> str:
49
+ return str(self.value)
50
+
51
+ @classmethod
52
+ def list(cls) -> List[str]:
53
+ return [v.value for v in cls]
54
+
55
+
56
+ Field = pydantic.Field
57
+ Config = pydantic.ConfigDict
58
+
59
+
60
+ class Model(pydantic.BaseModel):
61
+ model_config = Config(populate_by_name=True, arbitrary_types_allowed=True, strict=True)
62
+
63
+ @is_like(pydantic.BaseModel.__iter__)
64
+ def __iter__(self) -> Any:
65
+ yield from [(k, v) for k, v in super().__iter__() if v is not None]
66
+
67
+ @is_like(pydantic.BaseModel.model_dump)
68
+ def model_dump(self, *args: Any, **kwargs: Any) -> Any:
69
+ exclude_none = kwargs.pop("exclude_none", True)
70
+ return super().model_dump(*args, exclude_none=exclude_none, **kwargs)
71
+
72
+ @is_like(pydantic.BaseModel.dict) # pyright: ignore [reportDeprecated]
73
+ def dict(self, *args: Any, **kwargs: Any) -> Any:
74
+ exclude_none = kwargs.pop("exclude_none", True)
75
+ return super().dict(*args, exclude_none=exclude_none, **kwargs) # pyright: ignore [reportDeprecated]
76
+
77
+ @is_like(pydantic.BaseModel.model_dump_json)
78
+ def model_dump_json(self, *args: Any, **kwargs: Any) -> Any:
79
+ exclude_none = kwargs.pop("exclude_none", True)
80
+ by_alias = kwargs.pop("by_alias", True)
81
+ return super().model_dump_json(*args, exclude_none=exclude_none, by_alias=by_alias, **kwargs)
82
+
83
+ @is_like(pydantic.BaseModel.json) # pyright: ignore [reportDeprecated]
84
+ def json(self, *args: Any, **kwargs: Any) -> Any:
85
+ exclude_none = kwargs.pop("exclude_none", True)
86
+ by_alias = kwargs.pop("by_alias", True)
87
+ return super().json(*args, exclude_none=exclude_none, by_alias=by_alias, **kwargs) # pyright: ignore [reportDeprecated]