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 @@
1
+ from .sdk import *
@@ -0,0 +1,3 @@
1
+ from .client import *
2
+ from .payloads import *
3
+ from .router import *
@@ -0,0 +1,140 @@
1
+ import asyncio
2
+ import json
3
+ from contextlib import asynccontextmanager
4
+ from typing import Any, AsyncGenerator, Optional
5
+
6
+ import httpx
7
+ import httpx_sse
8
+
9
+ from latitude_sdk.client.payloads import ErrorResponse, RequestBody, RequestHandler, RequestParams
10
+ from latitude_sdk.client.router import Router, RouterOptions
11
+ from latitude_sdk.sdk.errors import (
12
+ ApiError,
13
+ ApiErrorCodes,
14
+ ApiErrorDbRef,
15
+ )
16
+ from latitude_sdk.sdk.types import LogSources
17
+ from latitude_sdk.util import Model
18
+
19
+ RETRIABLE_STATUSES = [429, 500, 502, 503, 504]
20
+
21
+ ClientEvent = httpx_sse.ServerSentEvent
22
+
23
+
24
+ class ClientResponse(httpx.Response):
25
+ async def sse(self: httpx.Response) -> AsyncGenerator[ClientEvent, Any]:
26
+ source = httpx_sse.EventSource(self)
27
+
28
+ async for event in source.aiter_sse():
29
+ yield event
30
+
31
+
32
+ httpx.Response.sse = ClientResponse.sse # pyright: ignore [reportAttributeAccessIssue]
33
+
34
+
35
+ class ClientOptions(Model):
36
+ api_key: str
37
+ retries: int
38
+ delay: float
39
+ timeout: float
40
+ source: LogSources
41
+ router: RouterOptions
42
+
43
+
44
+ class Client:
45
+ options: ClientOptions
46
+ router: Router
47
+
48
+ def __init__(self, options: ClientOptions):
49
+ self.options = options
50
+ self.router = Router(options.router)
51
+
52
+ @asynccontextmanager
53
+ async def request(
54
+ self, handler: RequestHandler, params: RequestParams, body: Optional[RequestBody] = None
55
+ ) -> AsyncGenerator[ClientResponse, Any]:
56
+ client = httpx.AsyncClient(
57
+ headers={
58
+ "Authorization": f"Bearer {self.options.api_key}",
59
+ "Content-Type": "application/json",
60
+ },
61
+ timeout=self.options.timeout,
62
+ follow_redirects=False,
63
+ max_redirects=0,
64
+ )
65
+ response = None
66
+ attempt = 1
67
+
68
+ try:
69
+ method, url = self.router.resolve(handler, params)
70
+ content = None
71
+ if body:
72
+ content = json.dumps(
73
+ {
74
+ **json.loads(body.model_dump_json()),
75
+ "__internal": {"source": self.options.source},
76
+ }
77
+ )
78
+
79
+ while attempt <= self.options.retries:
80
+ try:
81
+ response = await client.request(method=method, url=url, content=content)
82
+ response.raise_for_status()
83
+
84
+ yield response # pyright: ignore [reportReturnType]
85
+ break
86
+
87
+ except Exception as exception:
88
+ if isinstance(exception, ApiError):
89
+ raise exception
90
+
91
+ if attempt >= self.options.retries:
92
+ raise await self._exception(exception, response) from exception
93
+
94
+ if response and response.status_code in RETRIABLE_STATUSES:
95
+ await asyncio.sleep(self.options.delay * (2 ** (attempt - 1)))
96
+ else:
97
+ raise await self._exception(exception, response) from exception
98
+
99
+ finally:
100
+ if response:
101
+ await response.aclose()
102
+
103
+ attempt += 1
104
+
105
+ except Exception as exception:
106
+ if isinstance(exception, ApiError):
107
+ raise exception
108
+
109
+ raise await self._exception(exception, response) from exception
110
+
111
+ finally:
112
+ await client.aclose()
113
+
114
+ async def _exception(self, exception: Exception, response: Optional[httpx.Response] = None) -> ApiError:
115
+ if not response:
116
+ return ApiError(
117
+ status=500,
118
+ code=ApiErrorCodes.InternalServerError,
119
+ message=str(exception),
120
+ response=str(exception),
121
+ )
122
+
123
+ try:
124
+ error = ErrorResponse.model_validate_json(response.content)
125
+
126
+ return ApiError(
127
+ status=response.status_code,
128
+ code=error.code,
129
+ message=error.message,
130
+ response=response.text,
131
+ db_ref=ApiErrorDbRef(**dict(error.db_ref)) if error.db_ref else None,
132
+ )
133
+
134
+ except Exception:
135
+ return ApiError(
136
+ status=response.status_code,
137
+ code=ApiErrorCodes.InternalServerError,
138
+ message=str(exception),
139
+ response=response.text,
140
+ )
@@ -0,0 +1,117 @@
1
+ from typing import Any, Dict, List, Optional, Union
2
+
3
+ from latitude_sdk.sdk.types import DbErrorRef, Message
4
+ from latitude_sdk.util import Field, Model, StrEnum
5
+
6
+
7
+ class ErrorResponse(Model):
8
+ name: str
9
+ code: str = Field(alias=str("errorCode"))
10
+ message: str
11
+ details: Dict[str, Any]
12
+ db_ref: Optional[DbErrorRef] = Field(None, alias=str("dbErrorRef"))
13
+
14
+
15
+ class PromptRequestParams(Model):
16
+ project_id: int
17
+ version_uuid: Optional[str] = None
18
+
19
+
20
+ class GetPromptRequestParams(PromptRequestParams, Model):
21
+ path: str
22
+
23
+
24
+ class GetOrCreatePromptRequestParams(PromptRequestParams, Model):
25
+ pass
26
+
27
+
28
+ class GetOrCreatePromptRequestBody(Model):
29
+ path: str
30
+ prompt: Optional[str] = None
31
+
32
+
33
+ class RunPromptRequestParams(PromptRequestParams, Model):
34
+ pass
35
+
36
+
37
+ class RunPromptRequestBody(Model):
38
+ path: str
39
+ parameters: Optional[Dict[str, Any]] = None
40
+ custom_identifier: Optional[str] = Field(None, alias=str("customIdentifier"))
41
+ stream: Optional[bool] = None
42
+
43
+
44
+ class ChatPromptRequestParams(Model):
45
+ conversation_uuid: str
46
+
47
+
48
+ class ChatPromptRequestBody(Model):
49
+ messages: List[Message]
50
+ stream: Optional[bool] = None
51
+
52
+
53
+ class LogRequestParams(Model):
54
+ project_id: int
55
+ version_uuid: Optional[str] = None
56
+
57
+
58
+ class CreateLogRequestParams(LogRequestParams, Model):
59
+ pass
60
+
61
+
62
+ class CreateLogRequestBody(Model):
63
+ path: str
64
+ messages: List[Message]
65
+ response: Optional[str] = None
66
+
67
+
68
+ class EvaluationRequestParams(Model):
69
+ conversation_uuid: str
70
+
71
+
72
+ class TriggerEvaluationRequestParams(EvaluationRequestParams, Model):
73
+ pass
74
+
75
+
76
+ class TriggerEvaluationRequestBody(Model):
77
+ evaluation_uuids: Optional[List[str]] = Field(None, alias=str("evaluationUuids"))
78
+
79
+
80
+ class CreateEvaluationResultRequestParams(EvaluationRequestParams, Model):
81
+ evaluation_uuid: str
82
+
83
+
84
+ class CreateEvaluationResultRequestBody(Model):
85
+ result: Union[str, bool, int]
86
+ reason: str
87
+
88
+
89
+ RequestParams = Union[
90
+ GetPromptRequestParams,
91
+ GetOrCreatePromptRequestParams,
92
+ RunPromptRequestParams,
93
+ ChatPromptRequestParams,
94
+ CreateLogRequestParams,
95
+ TriggerEvaluationRequestParams,
96
+ CreateEvaluationResultRequestParams,
97
+ ]
98
+
99
+
100
+ RequestBody = Union[
101
+ GetOrCreatePromptRequestBody,
102
+ RunPromptRequestBody,
103
+ ChatPromptRequestBody,
104
+ CreateLogRequestBody,
105
+ TriggerEvaluationRequestBody,
106
+ CreateEvaluationResultRequestBody,
107
+ ]
108
+
109
+
110
+ class RequestHandler(StrEnum):
111
+ GetPrompt = "GET_PROMPT"
112
+ GetOrCreatePrompt = "GET_OR_CREATE_PROMPT"
113
+ RunPrompt = "RUN_PROMPT"
114
+ ChatPrompt = "CHAT_PROMPT"
115
+ CreateLog = "CREATE_LOG"
116
+ TriggerEvaluation = "TRIGGER_EVALUATION"
117
+ CreateEvaluationResult = "CREATE_EVALUATION_RESULT"
@@ -0,0 +1,117 @@
1
+ from typing import Callable, Optional, Tuple
2
+
3
+ from latitude_sdk.client.payloads import (
4
+ ChatPromptRequestParams,
5
+ CreateEvaluationResultRequestParams,
6
+ CreateLogRequestParams,
7
+ GetOrCreatePromptRequestParams,
8
+ GetPromptRequestParams,
9
+ RequestHandler,
10
+ RequestParams,
11
+ RunPromptRequestParams,
12
+ TriggerEvaluationRequestParams,
13
+ )
14
+ from latitude_sdk.sdk.types import GatewayOptions
15
+ from latitude_sdk.util import Model
16
+
17
+ HEAD_COMMIT = "live"
18
+
19
+
20
+ class RouterOptions(Model):
21
+ gateway: GatewayOptions
22
+
23
+
24
+ class Router:
25
+ options: RouterOptions
26
+
27
+ def __init__(self, options: RouterOptions):
28
+ self.options = options
29
+
30
+ def resolve(self, handler: RequestHandler, params: RequestParams) -> Tuple[str, str]:
31
+ if handler == RequestHandler.GetPrompt:
32
+ assert isinstance(params, GetPromptRequestParams)
33
+
34
+ return "GET", self.prompts(
35
+ project_id=params.project_id,
36
+ version_uuid=params.version_uuid,
37
+ ).prompt(params.path)
38
+
39
+ elif handler == RequestHandler.GetOrCreatePrompt:
40
+ assert isinstance(params, GetOrCreatePromptRequestParams)
41
+
42
+ return "POST", self.prompts(
43
+ project_id=params.project_id,
44
+ version_uuid=params.version_uuid,
45
+ ).get_or_create
46
+
47
+ elif handler == RequestHandler.RunPrompt:
48
+ assert isinstance(params, RunPromptRequestParams)
49
+
50
+ return "POST", self.prompts(
51
+ project_id=params.project_id,
52
+ version_uuid=params.version_uuid,
53
+ ).run
54
+
55
+ elif handler == RequestHandler.ChatPrompt:
56
+ assert isinstance(params, ChatPromptRequestParams)
57
+
58
+ return "POST", self.conversations().chat(params.conversation_uuid)
59
+
60
+ elif handler == RequestHandler.CreateLog:
61
+ assert isinstance(params, CreateLogRequestParams)
62
+
63
+ return "POST", self.prompts(
64
+ project_id=params.project_id,
65
+ version_uuid=params.version_uuid,
66
+ ).logs
67
+
68
+ elif handler == RequestHandler.TriggerEvaluation:
69
+ assert isinstance(params, TriggerEvaluationRequestParams)
70
+
71
+ return "POST", self.conversations().evaluate(params.conversation_uuid)
72
+
73
+ elif handler == RequestHandler.CreateEvaluationResult:
74
+ assert isinstance(params, CreateEvaluationResultRequestParams)
75
+
76
+ return "POST", self.conversations().evaluation_result(params.conversation_uuid, params.evaluation_uuid)
77
+
78
+ raise TypeError(f"Unknown handler: {handler}")
79
+
80
+ class Conversations(Model):
81
+ chat: Callable[[str], str]
82
+ evaluate: Callable[[str], str]
83
+ evaluation_result: Callable[[str, str], str]
84
+
85
+ def conversations(self) -> Conversations:
86
+ base_url = f"{self.options.gateway.base_url}/conversations"
87
+
88
+ return self.Conversations(
89
+ chat=lambda uuid: f"{base_url}/{uuid}/chat",
90
+ evaluate=lambda uuid: f"{base_url}/{uuid}/evaluate",
91
+ evaluation_result=lambda conversation_uuid,
92
+ evaluation_uuid: f"{base_url}/{conversation_uuid}/evaluations/{evaluation_uuid}/evaluation-results",
93
+ )
94
+
95
+ class Prompts(Model):
96
+ prompt: Callable[[str], str]
97
+ get_or_create: str
98
+ run: str
99
+ logs: str
100
+
101
+ def prompts(self, project_id: int, version_uuid: Optional[str]) -> Prompts:
102
+ base_url = f"{self.commits_url(project_id, version_uuid)}/documents"
103
+
104
+ return self.Prompts(
105
+ prompt=lambda path: f"{base_url}/{path}",
106
+ get_or_create=f"{base_url}/get-or-create",
107
+ run=f"{base_url}/run",
108
+ logs=f"{base_url}/logs",
109
+ )
110
+
111
+ def commits_url(self, project_id: int, version_uuid: Optional[str]) -> str:
112
+ version_uuid = version_uuid if version_uuid else HEAD_COMMIT
113
+
114
+ return f"{self.projects_url(project_id)}/versions/{version_uuid}"
115
+
116
+ def projects_url(self, project_id: int) -> str:
117
+ return f"{self.options.gateway.base_url}/projects/{project_id}"
@@ -0,0 +1 @@
1
+ from .env import *
@@ -0,0 +1,18 @@
1
+ from latitude_sdk.util import Model, get_env
2
+
3
+ DEFAULT_GATEWAY_HOSTNAME = "gateway.latitude.so"
4
+ DEFAULT_GATEWAY_PORT = 8787
5
+ DEFAULT_GATEWAY_SSL = True
6
+
7
+
8
+ class Env(Model):
9
+ GATEWAY_HOSTNAME: str
10
+ GATEWAY_PORT: int
11
+ GATEWAY_SSL: bool
12
+
13
+
14
+ env = Env(
15
+ GATEWAY_HOSTNAME=get_env("GATEWAY_HOSTNAME", DEFAULT_GATEWAY_HOSTNAME),
16
+ GATEWAY_PORT=get_env("GATEWAY_PORT", DEFAULT_GATEWAY_PORT),
17
+ GATEWAY_SSL=get_env("GATEWAY_SSL", DEFAULT_GATEWAY_SSL),
18
+ )
latitude_sdk/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ from .errors import *
2
+ from .evaluations import *
3
+ from .latitude import *
4
+ from .logs import *
5
+ from .prompts import *
6
+ from .types import *
@@ -0,0 +1,59 @@
1
+ from typing import Optional, Union
2
+
3
+ from latitude_sdk.util import Model, StrEnum
4
+
5
+
6
+ # NOTE: Incomplete list
7
+ class ApiErrorCodes(StrEnum):
8
+ # LatitudeErrorCodes
9
+ NotFoundError = "NotFoundError"
10
+
11
+ # RunErrorCodes
12
+ AIRunError = "ai_run_error"
13
+
14
+ # ApiErrorCodes
15
+ HTTPException = "http_exception"
16
+ InternalServerError = "internal_server_error"
17
+
18
+
19
+ UNEXPECTED_ERROR_CODES = [
20
+ str(ApiErrorCodes.HTTPException),
21
+ str(ApiErrorCodes.InternalServerError),
22
+ ]
23
+
24
+
25
+ class ApiErrorDbRef(Model):
26
+ entity_uuid: str
27
+ entity_type: str
28
+
29
+
30
+ class ApiError(Exception):
31
+ status: int
32
+ code: str # NOTE: Cannot be ApiErrorCodes because the list is incomplete
33
+ message: str
34
+ response: str
35
+ db_ref: Optional[ApiErrorDbRef]
36
+
37
+ def __init__(
38
+ self,
39
+ status: int,
40
+ code: Union[str, ApiErrorCodes],
41
+ message: str,
42
+ response: str,
43
+ db_ref: Optional[ApiErrorDbRef] = None,
44
+ ):
45
+ message = self._exception_message(status, str(code), message)
46
+ super().__init__(message)
47
+
48
+ self.status = status
49
+ self.code = str(code)
50
+ self.message = message
51
+ self.response = response
52
+ self.db_ref = db_ref
53
+
54
+ @staticmethod
55
+ def _exception_message(status: int, code: str, message: str) -> str:
56
+ if code in UNEXPECTED_ERROR_CODES:
57
+ return f"Unexpected API Error: {status} {message}"
58
+
59
+ return message
@@ -0,0 +1,69 @@
1
+ from typing import List, Optional, Union
2
+
3
+ from latitude_sdk.client import (
4
+ Client,
5
+ CreateEvaluationResultRequestBody,
6
+ CreateEvaluationResultRequestParams,
7
+ RequestHandler,
8
+ TriggerEvaluationRequestBody,
9
+ TriggerEvaluationRequestParams,
10
+ )
11
+ from latitude_sdk.sdk.types import (
12
+ EvaluationResult,
13
+ SdkOptions,
14
+ )
15
+ from latitude_sdk.util import Model
16
+
17
+
18
+ class TriggerEvaluationOptions(Model):
19
+ evaluation_uuids: Optional[List[str]] = None
20
+
21
+
22
+ class TriggerEvaluationResult(Model):
23
+ evaluations: List[str]
24
+
25
+
26
+ class CreateEvaluationResultOptions(Model):
27
+ result: Union[str, bool, int]
28
+ reason: str
29
+
30
+
31
+ class CreateEvaluationResultResult(EvaluationResult, Model):
32
+ pass
33
+
34
+
35
+ class Evaluations:
36
+ _options: SdkOptions
37
+ _client: Client
38
+
39
+ def __init__(self, client: Client, options: SdkOptions):
40
+ self._options = options
41
+ self._client = client
42
+
43
+ async def trigger(self, uuid: str, options: TriggerEvaluationOptions) -> TriggerEvaluationResult:
44
+ async with self._client.request(
45
+ handler=RequestHandler.TriggerEvaluation,
46
+ params=TriggerEvaluationRequestParams(
47
+ conversation_uuid=uuid,
48
+ ),
49
+ body=TriggerEvaluationRequestBody(
50
+ evaluation_uuids=options.evaluation_uuids,
51
+ ),
52
+ ) as response:
53
+ return TriggerEvaluationResult.model_validate_json(response.content)
54
+
55
+ async def create_result(
56
+ self, uuid: str, evaluation_uuid: str, options: CreateEvaluationResultOptions
57
+ ) -> CreateEvaluationResultResult:
58
+ async with self._client.request(
59
+ handler=RequestHandler.CreateEvaluationResult,
60
+ params=CreateEvaluationResultRequestParams(
61
+ conversation_uuid=uuid,
62
+ evaluation_uuid=evaluation_uuid,
63
+ ),
64
+ body=CreateEvaluationResultRequestBody(
65
+ result=options.result,
66
+ reason=options.reason,
67
+ ),
68
+ ) as response:
69
+ return CreateEvaluationResultResult.model_validate_json(response.content)
@@ -0,0 +1,76 @@
1
+ from typing import Optional
2
+
3
+ from latitude_sdk.client import Client, ClientOptions, RouterOptions
4
+ from latitude_sdk.env import env
5
+ from latitude_sdk.sdk.evaluations import Evaluations
6
+ from latitude_sdk.sdk.logs import Logs
7
+ from latitude_sdk.sdk.prompts import Prompts
8
+ from latitude_sdk.sdk.types import GatewayOptions, LogSources, SdkOptions
9
+ from latitude_sdk.util import Model
10
+
11
+
12
+ class InternalOptions(Model):
13
+ gateway: Optional[GatewayOptions] = None
14
+ source: Optional[LogSources] = None
15
+ retries: Optional[int] = None
16
+ delay: Optional[float] = None
17
+ timeout: Optional[float] = None
18
+
19
+
20
+ class LatitudeOptions(SdkOptions, Model):
21
+ internal: Optional[InternalOptions] = None
22
+
23
+
24
+ DEFAULT_INTERNAL_OPTIONS = InternalOptions(
25
+ gateway=GatewayOptions(
26
+ host=env.GATEWAY_HOSTNAME,
27
+ port=env.GATEWAY_PORT,
28
+ ssl=env.GATEWAY_SSL,
29
+ api_version="v2",
30
+ ),
31
+ source=LogSources.Api,
32
+ retries=3,
33
+ delay=0.5,
34
+ timeout=30,
35
+ )
36
+
37
+
38
+ DEFAULT_LATITUDE_OPTIONS = LatitudeOptions(internal=DEFAULT_INTERNAL_OPTIONS)
39
+
40
+
41
+ class Latitude:
42
+ _options: LatitudeOptions
43
+ _client: Client
44
+
45
+ prompts: Prompts
46
+ logs: Logs
47
+ evaluations: Evaluations
48
+
49
+ def __init__(self, api_key: str, options: LatitudeOptions = DEFAULT_LATITUDE_OPTIONS):
50
+ options.internal = options.internal or DEFAULT_INTERNAL_OPTIONS
51
+ options.internal = InternalOptions(**{**dict(DEFAULT_INTERNAL_OPTIONS), **dict(options.internal)})
52
+ options = LatitudeOptions(**{**dict(DEFAULT_LATITUDE_OPTIONS), **dict(options)})
53
+
54
+ assert options.internal is not None
55
+ assert options.internal.gateway is not None
56
+ assert options.internal.source is not None
57
+ assert options.internal.retries is not None
58
+ assert options.internal.delay is not None
59
+ assert options.internal.timeout is not None
60
+
61
+ self._options = options
62
+ self._client = Client(
63
+ ClientOptions(
64
+ api_key=api_key,
65
+ retries=options.internal.retries,
66
+ delay=options.internal.delay,
67
+ timeout=options.internal.timeout,
68
+ source=options.internal.source,
69
+ router=RouterOptions(gateway=options.internal.gateway),
70
+ )
71
+ )
72
+
73
+ self.prompts = Prompts(self._client, self._options)
74
+ self.logs = Logs(self._client, self._options)
75
+ self.evaluations = Evaluations(self._client, self._options)
76
+ # TODO: Telemetry - needs Telemetry SDK in Python
@@ -0,0 +1,66 @@
1
+ from typing import List, Optional
2
+
3
+ from latitude_sdk.client import Client, CreateLogRequestBody, CreateLogRequestParams, RequestHandler
4
+ from latitude_sdk.sdk.errors import ApiError, ApiErrorCodes
5
+ from latitude_sdk.sdk.types import (
6
+ Log,
7
+ Message,
8
+ SdkOptions,
9
+ )
10
+ from latitude_sdk.util import Model
11
+
12
+
13
+ class LogOptions(Model):
14
+ project_id: Optional[int] = None
15
+ version_uuid: Optional[str] = None
16
+
17
+
18
+ class CreateLogOptions(LogOptions, Model):
19
+ response: Optional[str] = None
20
+
21
+
22
+ class CreateLogResult(Log, Model):
23
+ pass
24
+
25
+
26
+ class Logs:
27
+ _options: SdkOptions
28
+ _client: Client
29
+
30
+ def __init__(self, client: Client, options: SdkOptions):
31
+ self._options = options
32
+ self._client = client
33
+
34
+ def _ensure_options(self, options: LogOptions) -> LogOptions:
35
+ project_id = options.project_id or self._options.project_id
36
+ if not project_id:
37
+ raise ApiError(
38
+ status=404,
39
+ code=ApiErrorCodes.NotFoundError,
40
+ message="Project ID is required",
41
+ response="Project ID is required",
42
+ )
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: List[Message], options: CreateLogOptions) -> CreateLogResult:
49
+ log_options = self._ensure_options(options)
50
+ options = CreateLogOptions(**{**dict(options), **dict(log_options)})
51
+
52
+ assert options.project_id is not None
53
+
54
+ async with self._client.request(
55
+ handler=RequestHandler.CreateLog,
56
+ params=CreateLogRequestParams(
57
+ project_id=options.project_id,
58
+ version_uuid=options.version_uuid,
59
+ ),
60
+ body=CreateLogRequestBody(
61
+ path=path,
62
+ messages=messages,
63
+ response=options.response,
64
+ ),
65
+ ) as response:
66
+ return CreateLogResult.model_validate_json(response.content)