amigo_sdk 0.8.0__py3-none-any.whl → 0.9.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.
- amigo_sdk/__init__.py +8 -1
- amigo_sdk/_retry_utils.py +70 -0
- amigo_sdk/auth.py +34 -16
- amigo_sdk/generated/model.py +2 -6
- amigo_sdk/http_client.py +222 -71
- amigo_sdk/resources/conversation.py +156 -4
- amigo_sdk/resources/organization.py +15 -3
- amigo_sdk/resources/service.py +21 -5
- amigo_sdk/resources/user.py +43 -3
- amigo_sdk/sdk_client.py +100 -18
- {amigo_sdk-0.8.0.dist-info → amigo_sdk-0.9.0.dist-info}/METADATA +35 -47
- amigo_sdk-0.9.0.dist-info/RECORD +17 -0
- amigo_sdk-0.8.0.dist-info/RECORD +0 -16
- {amigo_sdk-0.8.0.dist-info → amigo_sdk-0.9.0.dist-info}/WHEEL +0 -0
- {amigo_sdk-0.8.0.dist-info → amigo_sdk-0.9.0.dist-info}/entry_points.txt +0 -0
- {amigo_sdk-0.8.0.dist-info → amigo_sdk-0.9.0.dist-info}/licenses/LICENSE +0 -0
amigo_sdk/__init__.py
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import random
|
|
3
|
+
from email.utils import parsedate_to_datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
DEFAULT_RETRYABLE_STATUS: set[int] = {429, 500, 502, 503, 504}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_retry_after_seconds(retry_after: Optional[str]) -> float | None:
|
|
10
|
+
"""Parse Retry-After header into seconds.
|
|
11
|
+
|
|
12
|
+
Supports both numeric seconds and HTTP-date formats. Returns None when
|
|
13
|
+
header is missing or invalid.
|
|
14
|
+
"""
|
|
15
|
+
if not retry_after:
|
|
16
|
+
return None
|
|
17
|
+
# Numeric seconds
|
|
18
|
+
try:
|
|
19
|
+
seconds = float(retry_after)
|
|
20
|
+
return max(0.0, seconds)
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
# HTTP-date format
|
|
24
|
+
try:
|
|
25
|
+
target_dt = parsedate_to_datetime(retry_after)
|
|
26
|
+
if target_dt is None:
|
|
27
|
+
return None
|
|
28
|
+
if target_dt.tzinfo is None:
|
|
29
|
+
target_dt = target_dt.replace(tzinfo=dt.UTC)
|
|
30
|
+
now = dt.datetime.now(dt.UTC)
|
|
31
|
+
delta_seconds = (target_dt - now).total_seconds()
|
|
32
|
+
return max(0.0, delta_seconds)
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_retryable_response(
|
|
38
|
+
method: str,
|
|
39
|
+
status_code: int,
|
|
40
|
+
headers: dict,
|
|
41
|
+
retry_on_methods: set[str],
|
|
42
|
+
retry_on_status: set[int],
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Determine if the response is retryable under our policy.
|
|
45
|
+
|
|
46
|
+
Special case: allow POST retry only on 429 when Retry-After is present.
|
|
47
|
+
"""
|
|
48
|
+
method_upper = method.upper()
|
|
49
|
+
if method_upper == "POST" and status_code == 429 and headers.get("Retry-After"):
|
|
50
|
+
return True
|
|
51
|
+
return method_upper in retry_on_methods and status_code in retry_on_status
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def compute_retry_delay_seconds(
|
|
55
|
+
attempt: int,
|
|
56
|
+
backoff_base: float,
|
|
57
|
+
max_delay_seconds: float,
|
|
58
|
+
retry_after_header: Optional[str],
|
|
59
|
+
) -> float:
|
|
60
|
+
"""Compute delay for a given retry attempt.
|
|
61
|
+
|
|
62
|
+
If Retry-After is present, honor it (clamped by max). Otherwise, use
|
|
63
|
+
exponential backoff with full jitter.
|
|
64
|
+
"""
|
|
65
|
+
ra_seconds = parse_retry_after_seconds(retry_after_header)
|
|
66
|
+
if ra_seconds is not None:
|
|
67
|
+
return min(max_delay_seconds, ra_seconds)
|
|
68
|
+
window = backoff_base * (2 ** (attempt - 1))
|
|
69
|
+
window = min(window, max_delay_seconds)
|
|
70
|
+
return random.uniform(0.0, window)
|
amigo_sdk/auth.py
CHANGED
|
@@ -5,26 +5,44 @@ from amigo_sdk.errors import AuthenticationError
|
|
|
5
5
|
from amigo_sdk.generated.model import UserSignInWithApiKeyResponse
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
cfg
|
|
8
|
+
def _signin_url_headers(cfg: AmigoConfig) -> tuple[str, dict[str, str]]:
|
|
9
|
+
url = f"{cfg.base_url}/v1/{cfg.organization_id}/user/signin_with_api_key"
|
|
10
|
+
headers = {
|
|
11
|
+
"x-api-key": cfg.api_key,
|
|
12
|
+
"x-api-key-id": cfg.api_key_id,
|
|
13
|
+
"x-user-id": cfg.user_id,
|
|
14
|
+
}
|
|
15
|
+
return url, headers
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_signin_response_text(
|
|
19
|
+
response: httpx.Response,
|
|
10
20
|
) -> UserSignInWithApiKeyResponse:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
try:
|
|
22
|
+
return UserSignInWithApiKeyResponse.model_validate_json(response.text)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise AuthenticationError(f"Invalid response format: {e}") from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sign_in_with_api_key(cfg: AmigoConfig) -> UserSignInWithApiKeyResponse:
|
|
28
|
+
"""Sign in with API key (sync)."""
|
|
29
|
+
url, headers = _signin_url_headers(cfg)
|
|
30
|
+
with httpx.Client() as client:
|
|
21
31
|
try:
|
|
22
|
-
response =
|
|
32
|
+
response = client.post(url, headers=headers)
|
|
23
33
|
response.raise_for_status()
|
|
24
34
|
except httpx.HTTPStatusError as e:
|
|
25
35
|
raise AuthenticationError(f"Sign in with API key failed: {e}") from e
|
|
36
|
+
return _parse_signin_response_text(response)
|
|
26
37
|
|
|
38
|
+
|
|
39
|
+
async def sign_in_with_api_key_async(cfg: AmigoConfig) -> UserSignInWithApiKeyResponse:
|
|
40
|
+
"""Sign in with API key (async)."""
|
|
41
|
+
url, headers = _signin_url_headers(cfg)
|
|
42
|
+
async with httpx.AsyncClient() as client:
|
|
27
43
|
try:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
response = await client.post(url, headers=headers)
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
except httpx.HTTPStatusError as e:
|
|
47
|
+
raise AuthenticationError(f"Sign in with API key failed: {e}") from e
|
|
48
|
+
return _parse_signin_response_text(response)
|
amigo_sdk/generated/model.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: <stdin>
|
|
3
|
-
# timestamp: 2025-08-
|
|
3
|
+
# timestamp: 2025-08-25T20:27:06+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -8330,8 +8330,6 @@ class LLMConfigOutput(BaseModel):
|
|
|
8330
8330
|
|
|
8331
8331
|
class LLMLoadBalancingSetType(Enum):
|
|
8332
8332
|
o4_mini_2025_04_16 = 'o4-mini-2025-04-16'
|
|
8333
|
-
gpt_4_1_2025_04_14 = 'gpt-4.1-2025-04-14'
|
|
8334
|
-
gpt_4_1_mini_2025_04_14 = 'gpt-4.1-mini-2025-04-14'
|
|
8335
8333
|
gpt_5_2025_08_07 = 'gpt-5-2025-08-07'
|
|
8336
8334
|
gpt_5_mini_2025_08_07 = 'gpt-5-mini-2025-08-07'
|
|
8337
8335
|
gpt_5_nano_2025_08_07 = 'gpt-5-nano-2025-08-07'
|
|
@@ -8341,8 +8339,6 @@ class LLMLoadBalancingSetType(Enum):
|
|
|
8341
8339
|
|
|
8342
8340
|
class LLMType(Enum):
|
|
8343
8341
|
openai_o4_mini_2025_04_16 = 'openai_o4-mini-2025-04-16'
|
|
8344
|
-
openai_gpt_4_1_2025_04_14 = 'openai_gpt-4.1-2025-04-14'
|
|
8345
|
-
openai_gpt_4_1_mini_2025_04_14 = 'openai_gpt-4.1-mini-2025-04-14'
|
|
8346
8342
|
openai_gpt_5_2025_08_07 = 'openai_gpt-5-2025-08-07'
|
|
8347
8343
|
openai_gpt_5_mini_2025_08_07 = 'openai_gpt-5-mini-2025-08-07'
|
|
8348
8344
|
openai_gpt_5_nano_2025_08_07 = 'openai_gpt-5-nano-2025-08-07'
|
|
@@ -11646,7 +11642,7 @@ class GetConversationMessagesParametersQuery(BaseModel):
|
|
|
11646
11642
|
[], description='The IDs of the messages to retrieve.', title='Id'
|
|
11647
11643
|
)
|
|
11648
11644
|
message_type: Optional[List[MessageType]] = Field(
|
|
11649
|
-
['agent-message', '
|
|
11645
|
+
['agent-message', 'external-event', 'user-message'],
|
|
11650
11646
|
description='The type of messages to retrieve.',
|
|
11651
11647
|
title='Message Type',
|
|
11652
11648
|
)
|
amigo_sdk/http_client.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime as dt
|
|
3
3
|
import random
|
|
4
|
-
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import AsyncIterator, Iterator
|
|
6
|
+
from dataclasses import dataclass
|
|
5
7
|
from email.utils import parsedate_to_datetime
|
|
6
8
|
from typing import Any, Optional
|
|
7
9
|
|
|
8
10
|
import httpx
|
|
9
11
|
|
|
10
|
-
from amigo_sdk.auth import sign_in_with_api_key
|
|
12
|
+
from amigo_sdk.auth import sign_in_with_api_key, sign_in_with_api_key_async
|
|
11
13
|
from amigo_sdk.config import AmigoConfig
|
|
12
14
|
from amigo_sdk.errors import (
|
|
13
15
|
AuthenticationError,
|
|
@@ -16,48 +18,33 @@ from amigo_sdk.errors import (
|
|
|
16
18
|
)
|
|
17
19
|
from amigo_sdk.generated.model import UserSignInWithApiKeyResponse
|
|
18
20
|
|
|
21
|
+
# -----------------------------
|
|
22
|
+
# Shared helpers and structures
|
|
23
|
+
# -----------------------------
|
|
19
24
|
|
|
20
|
-
class AmigoHttpClient:
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
cfg: AmigoConfig,
|
|
24
|
-
*,
|
|
25
|
-
retry_max_attempts: int = 3,
|
|
26
|
-
retry_backoff_base: float = 0.25,
|
|
27
|
-
retry_max_delay_seconds: float = 30.0,
|
|
28
|
-
retry_on_status: set[int] | None = None,
|
|
29
|
-
retry_on_methods: set[str] | None = None,
|
|
30
|
-
**httpx_kwargs: Any,
|
|
31
|
-
) -> None:
|
|
32
|
-
self._cfg = cfg
|
|
33
|
-
self._token: Optional[UserSignInWithApiKeyResponse] = None
|
|
34
|
-
self._client = httpx.AsyncClient(
|
|
35
|
-
base_url=cfg.base_url,
|
|
36
|
-
**httpx_kwargs,
|
|
37
|
-
)
|
|
38
|
-
# Retry configuration
|
|
39
|
-
self._retry_max_attempts = max(1, retry_max_attempts)
|
|
40
|
-
self._retry_backoff_base = retry_backoff_base
|
|
41
|
-
self._retry_max_delay_seconds = max(0.0, retry_max_delay_seconds)
|
|
42
|
-
self._retry_on_status = retry_on_status or {408, 429, 500, 502, 503, 504}
|
|
43
|
-
# Default to GET only; POST is handled specially for 429 + Retry-After
|
|
44
|
-
self._retry_on_methods = {m.upper() for m in (retry_on_methods or {"GET"})}
|
|
45
25
|
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
@dataclass
|
|
27
|
+
class _RetryConfig:
|
|
28
|
+
max_attempts: int
|
|
29
|
+
backoff_base: float
|
|
30
|
+
max_delay_seconds: float
|
|
31
|
+
on_status: set[int]
|
|
32
|
+
on_methods: set[str]
|
|
33
|
+
|
|
34
|
+
def is_retryable_method(self, method: str) -> bool:
|
|
35
|
+
return method.upper() in self.on_methods
|
|
48
36
|
|
|
49
|
-
def
|
|
37
|
+
def is_retryable_response(self, method: str, resp: httpx.Response) -> bool:
|
|
50
38
|
status = resp.status_code
|
|
51
|
-
# Allow POST retry only for 429 when Retry-After header is present
|
|
52
39
|
if (
|
|
53
40
|
method.upper() == "POST"
|
|
54
41
|
and status == 429
|
|
55
42
|
and resp.headers.get("Retry-After")
|
|
56
43
|
):
|
|
57
44
|
return True
|
|
58
|
-
return self.
|
|
45
|
+
return self.is_retryable_method(method) and status in self.on_status
|
|
59
46
|
|
|
60
|
-
def
|
|
47
|
+
def parse_retry_after_seconds(self, resp: httpx.Response) -> float | None:
|
|
61
48
|
retry_after = resp.headers.get("Retry-After")
|
|
62
49
|
if not retry_after:
|
|
63
50
|
return None
|
|
@@ -76,28 +63,94 @@ class AmigoHttpClient:
|
|
|
76
63
|
target_dt = target_dt.replace(tzinfo=dt.UTC)
|
|
77
64
|
now = dt.datetime.now(dt.UTC)
|
|
78
65
|
delta_seconds = (target_dt - now).total_seconds()
|
|
66
|
+
# Round to milliseconds to avoid borderline off-by-epsilon in tests
|
|
67
|
+
delta_seconds = round(delta_seconds, 3)
|
|
79
68
|
return max(0.0, delta_seconds)
|
|
80
69
|
except Exception:
|
|
81
70
|
return None
|
|
82
71
|
|
|
83
|
-
def
|
|
72
|
+
def retry_delay_seconds(self, attempt: int, resp: httpx.Response | None) -> float:
|
|
84
73
|
# Honor Retry-After when present (numeric or HTTP-date), clamped by max delay
|
|
85
74
|
if resp is not None:
|
|
86
|
-
ra_seconds = self.
|
|
75
|
+
ra_seconds = self.parse_retry_after_seconds(resp)
|
|
87
76
|
if ra_seconds is not None:
|
|
88
|
-
return min(self.
|
|
77
|
+
return min(self.max_delay_seconds, ra_seconds)
|
|
89
78
|
# Exponential backoff with full jitter: U(0, min(cap, base * 2^(attempt-1)))
|
|
90
|
-
window = self.
|
|
91
|
-
window = min(window, self.
|
|
79
|
+
window = self.backoff_base * (2 ** (attempt - 1))
|
|
80
|
+
window = min(window, self.max_delay_seconds)
|
|
92
81
|
return random.uniform(0.0, window)
|
|
93
82
|
|
|
83
|
+
|
|
84
|
+
def _should_refresh_token(token: Optional[UserSignInWithApiKeyResponse]) -> bool:
|
|
85
|
+
if not token:
|
|
86
|
+
return True
|
|
87
|
+
return dt.datetime.now(dt.UTC) > token.expires_at - dt.timedelta(minutes=5)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _raise_status_with_body_async(resp: httpx.Response) -> None:
|
|
91
|
+
if 200 <= resp.status_code < 300:
|
|
92
|
+
return
|
|
93
|
+
try:
|
|
94
|
+
await resp.aread()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
if hasattr(resp, "is_success"):
|
|
98
|
+
raise_for_status(resp)
|
|
99
|
+
error_class = get_error_class_for_status_code(getattr(resp, "status_code", 0))
|
|
100
|
+
raise error_class(
|
|
101
|
+
f"HTTP {getattr(resp, 'status_code', 'unknown')} error",
|
|
102
|
+
status_code=getattr(resp, "status_code", None),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _raise_status_with_body_sync(resp: httpx.Response) -> None:
|
|
107
|
+
if 200 <= resp.status_code < 300:
|
|
108
|
+
return
|
|
109
|
+
try:
|
|
110
|
+
_ = resp.text
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
if hasattr(resp, "is_success"):
|
|
114
|
+
raise_for_status(resp)
|
|
115
|
+
error_class = get_error_class_for_status_code(getattr(resp, "status_code", 0))
|
|
116
|
+
raise error_class(
|
|
117
|
+
f"HTTP {getattr(resp, 'status_code', 'unknown')} error",
|
|
118
|
+
status_code=getattr(resp, "status_code", None),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AmigoAsyncHttpClient:
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
cfg: AmigoConfig,
|
|
126
|
+
*,
|
|
127
|
+
retry_max_attempts: int = 3,
|
|
128
|
+
retry_backoff_base: float = 0.25,
|
|
129
|
+
retry_max_delay_seconds: float = 30.0,
|
|
130
|
+
retry_on_status: set[int] | None = None,
|
|
131
|
+
retry_on_methods: set[str] | None = None,
|
|
132
|
+
**httpx_kwargs: Any,
|
|
133
|
+
) -> None:
|
|
134
|
+
self._cfg = cfg
|
|
135
|
+
self._token: Optional[UserSignInWithApiKeyResponse] = None
|
|
136
|
+
self._client = httpx.AsyncClient(
|
|
137
|
+
base_url=cfg.base_url,
|
|
138
|
+
**httpx_kwargs,
|
|
139
|
+
)
|
|
140
|
+
# Retry configuration
|
|
141
|
+
self._retry_cfg = _RetryConfig(
|
|
142
|
+
max(1, retry_max_attempts),
|
|
143
|
+
retry_backoff_base,
|
|
144
|
+
max(0.0, retry_max_delay_seconds),
|
|
145
|
+
retry_on_status or {408, 429, 500, 502, 503, 504},
|
|
146
|
+
{m.upper() for m in (retry_on_methods or {"GET"})},
|
|
147
|
+
)
|
|
148
|
+
|
|
94
149
|
async def _ensure_token(self) -> str:
|
|
95
150
|
"""Fetch or refresh bearer token ~5 min before expiry."""
|
|
96
|
-
if
|
|
97
|
-
dt.UTC
|
|
98
|
-
) > self._token.expires_at - dt.timedelta(minutes=5):
|
|
151
|
+
if _should_refresh_token(self._token):
|
|
99
152
|
try:
|
|
100
|
-
self._token = await
|
|
153
|
+
self._token = await sign_in_with_api_key_async(self._cfg)
|
|
101
154
|
except Exception as e:
|
|
102
155
|
raise AuthenticationError(
|
|
103
156
|
"API-key exchange failed",
|
|
@@ -127,20 +180,20 @@ class AmigoHttpClient:
|
|
|
127
180
|
except (httpx.TimeoutException, httpx.TransportError):
|
|
128
181
|
# Retry only if method is allowed (e.g., GET); POST not retried for transport/timeouts
|
|
129
182
|
if (
|
|
130
|
-
not self.
|
|
131
|
-
or attempt >= self.
|
|
183
|
+
not self._retry_cfg.is_retryable_method(method)
|
|
184
|
+
or attempt >= self._retry_cfg.max_attempts
|
|
132
185
|
):
|
|
133
186
|
raise
|
|
134
|
-
await asyncio.sleep(self.
|
|
187
|
+
await asyncio.sleep(self._retry_cfg.retry_delay_seconds(attempt, None))
|
|
135
188
|
attempt += 1
|
|
136
189
|
continue
|
|
137
190
|
|
|
138
191
|
# Retry on configured HTTP status codes
|
|
139
192
|
if (
|
|
140
|
-
self.
|
|
141
|
-
and attempt < self.
|
|
193
|
+
self._retry_cfg.is_retryable_response(method, resp)
|
|
194
|
+
and attempt < self._retry_cfg.max_attempts
|
|
142
195
|
):
|
|
143
|
-
await asyncio.sleep(self.
|
|
196
|
+
await asyncio.sleep(self._retry_cfg.retry_delay_seconds(attempt, resp))
|
|
144
197
|
attempt += 1
|
|
145
198
|
continue
|
|
146
199
|
|
|
@@ -166,29 +219,8 @@ class AmigoHttpClient:
|
|
|
166
219
|
headers["Authorization"] = f"Bearer {await self._ensure_token()}"
|
|
167
220
|
headers.setdefault("Accept", "application/x-ndjson")
|
|
168
221
|
|
|
169
|
-
async def _raise_status_with_body(resp: httpx.Response) -> None:
|
|
170
|
-
"""Ensure response body is buffered, then raise mapped error with details."""
|
|
171
|
-
if 200 <= resp.status_code < 300:
|
|
172
|
-
return
|
|
173
|
-
# Fully buffer the body so raise_for_status() can extract JSON/text safely
|
|
174
|
-
try:
|
|
175
|
-
await resp.aread()
|
|
176
|
-
except Exception:
|
|
177
|
-
pass
|
|
178
|
-
# If this is a real httpx.Response, use our rich raise_for_status
|
|
179
|
-
if hasattr(resp, "is_success"):
|
|
180
|
-
raise_for_status(resp)
|
|
181
|
-
# Otherwise, fall back to lightweight error mapping used in tests' mock responses
|
|
182
|
-
error_class = get_error_class_for_status_code(
|
|
183
|
-
getattr(resp, "status_code", 0)
|
|
184
|
-
)
|
|
185
|
-
raise error_class(
|
|
186
|
-
f"HTTP {getattr(resp, 'status_code', 'unknown')} error",
|
|
187
|
-
status_code=getattr(resp, "status_code", None),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
222
|
async def _yield_from_response(resp: httpx.Response) -> AsyncIterator[str]:
|
|
191
|
-
await
|
|
223
|
+
await _raise_status_with_body_async(resp)
|
|
192
224
|
if abort_event and abort_event.is_set():
|
|
193
225
|
return
|
|
194
226
|
async for line in resp.aiter_lines():
|
|
@@ -221,8 +253,127 @@ class AmigoHttpClient:
|
|
|
221
253
|
await self._client.aclose()
|
|
222
254
|
|
|
223
255
|
# async-context-manager sugar
|
|
224
|
-
async def __aenter__(self): # → async with
|
|
256
|
+
async def __aenter__(self): # → async with AmigoAsyncHttpClient(...) as http:
|
|
225
257
|
return self
|
|
226
258
|
|
|
227
259
|
async def __aexit__(self, *_):
|
|
228
260
|
await self.aclose()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class AmigoHttpClient:
|
|
264
|
+
def __init__(
|
|
265
|
+
self,
|
|
266
|
+
cfg: AmigoConfig,
|
|
267
|
+
*,
|
|
268
|
+
retry_max_attempts: int = 3,
|
|
269
|
+
retry_backoff_base: float = 0.25,
|
|
270
|
+
retry_max_delay_seconds: float = 30.0,
|
|
271
|
+
retry_on_status: set[int] | None = None,
|
|
272
|
+
retry_on_methods: set[str] | None = None,
|
|
273
|
+
**httpx_kwargs: Any,
|
|
274
|
+
) -> None:
|
|
275
|
+
self._cfg = cfg
|
|
276
|
+
self._token: Optional[UserSignInWithApiKeyResponse] = None
|
|
277
|
+
self._client = httpx.Client(base_url=cfg.base_url, **httpx_kwargs)
|
|
278
|
+
# Retry configuration
|
|
279
|
+
self._retry_cfg = _RetryConfig(
|
|
280
|
+
max(1, retry_max_attempts),
|
|
281
|
+
retry_backoff_base,
|
|
282
|
+
max(0.0, retry_max_delay_seconds),
|
|
283
|
+
retry_on_status or {408, 429, 500, 502, 503, 504},
|
|
284
|
+
{m.upper() for m in (retry_on_methods or {"GET"})},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def _ensure_token(self) -> str:
|
|
288
|
+
if _should_refresh_token(self._token):
|
|
289
|
+
try:
|
|
290
|
+
self._token = sign_in_with_api_key(self._cfg)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise AuthenticationError("API-key exchange failed") from e
|
|
293
|
+
return self._token.id_token
|
|
294
|
+
|
|
295
|
+
def request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
296
|
+
kwargs.setdefault("headers", {})
|
|
297
|
+
attempt = 1
|
|
298
|
+
|
|
299
|
+
while True:
|
|
300
|
+
kwargs["headers"]["Authorization"] = f"Bearer {self._ensure_token()}"
|
|
301
|
+
|
|
302
|
+
resp: httpx.Response | None = None
|
|
303
|
+
try:
|
|
304
|
+
resp = self._client.request(method, path, **kwargs)
|
|
305
|
+
if resp.status_code == 401:
|
|
306
|
+
self._token = None
|
|
307
|
+
kwargs["headers"]["Authorization"] = (
|
|
308
|
+
f"Bearer {self._ensure_token()}"
|
|
309
|
+
)
|
|
310
|
+
resp = self._client.request(method, path, **kwargs)
|
|
311
|
+
|
|
312
|
+
except (httpx.TimeoutException, httpx.TransportError):
|
|
313
|
+
if (
|
|
314
|
+
not self._retry_cfg.is_retryable_method(method)
|
|
315
|
+
) or attempt >= self._retry_cfg.max_attempts:
|
|
316
|
+
raise
|
|
317
|
+
time.sleep(self._retry_cfg.retry_delay_seconds(attempt, None))
|
|
318
|
+
attempt += 1
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
self._retry_cfg.is_retryable_response(method, resp)
|
|
323
|
+
and attempt < self._retry_cfg.max_attempts
|
|
324
|
+
):
|
|
325
|
+
time.sleep(self._retry_cfg.retry_delay_seconds(attempt, resp))
|
|
326
|
+
attempt += 1
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
raise_for_status(resp)
|
|
330
|
+
return resp
|
|
331
|
+
|
|
332
|
+
def stream_lines(
|
|
333
|
+
self,
|
|
334
|
+
method: str,
|
|
335
|
+
path: str,
|
|
336
|
+
abort_flag: list[bool] | None = None,
|
|
337
|
+
**kwargs,
|
|
338
|
+
) -> Iterator[str]:
|
|
339
|
+
kwargs.setdefault("headers", {})
|
|
340
|
+
headers = kwargs["headers"]
|
|
341
|
+
headers["Authorization"] = f"Bearer {self._ensure_token()}"
|
|
342
|
+
headers.setdefault("Accept", "application/x-ndjson")
|
|
343
|
+
|
|
344
|
+
def _yield_from_response(resp: httpx.Response) -> Iterator[str]:
|
|
345
|
+
_raise_status_with_body_sync(resp)
|
|
346
|
+
if abort_flag and abort_flag[0]:
|
|
347
|
+
return
|
|
348
|
+
for line in resp.iter_lines():
|
|
349
|
+
if abort_flag and abort_flag[0]:
|
|
350
|
+
return
|
|
351
|
+
line_stripped = (line or "").strip()
|
|
352
|
+
if not line_stripped:
|
|
353
|
+
continue
|
|
354
|
+
yield line_stripped
|
|
355
|
+
|
|
356
|
+
if abort_flag and abort_flag[0]:
|
|
357
|
+
return iter(())
|
|
358
|
+
with self._client.stream(method, path, **kwargs) as resp:
|
|
359
|
+
if resp.status_code == 401:
|
|
360
|
+
self._token = None
|
|
361
|
+
headers["Authorization"] = f"Bearer {self._ensure_token()}"
|
|
362
|
+
if abort_flag and abort_flag[0]:
|
|
363
|
+
return iter(())
|
|
364
|
+
with self._client.stream(method, path, **kwargs) as retry_resp:
|
|
365
|
+
for ln in _yield_from_response(retry_resp):
|
|
366
|
+
yield ln
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
for ln in _yield_from_response(resp):
|
|
370
|
+
yield ln
|
|
371
|
+
|
|
372
|
+
def aclose(self) -> None:
|
|
373
|
+
self._client.close()
|
|
374
|
+
|
|
375
|
+
def __enter__(self):
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
def __exit__(self, *_):
|
|
379
|
+
self.aclose()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from collections.abc import AsyncGenerator
|
|
2
|
+
from collections.abc import AsyncGenerator, Iterator
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Any, Literal
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ from amigo_sdk.generated.model import (
|
|
|
21
21
|
GetConversationsParametersQuery,
|
|
22
22
|
InteractWithConversationParametersQuery,
|
|
23
23
|
)
|
|
24
|
-
from amigo_sdk.http_client import AmigoHttpClient
|
|
24
|
+
from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class GetMessageSourceResponse(BaseModel):
|
|
@@ -35,10 +35,10 @@ class GetMessageSourceResponse(BaseModel):
|
|
|
35
35
|
content_type: Literal["audio/mpeg", "audio/wav"]
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
class
|
|
38
|
+
class AsyncConversationResource:
|
|
39
39
|
"""Conversation resource for Amigo API operations."""
|
|
40
40
|
|
|
41
|
-
def __init__(self, http_client:
|
|
41
|
+
def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
|
|
42
42
|
self._http = http_client
|
|
43
43
|
self._organization_id = organization_id
|
|
44
44
|
|
|
@@ -206,3 +206,155 @@ class ConversationResource:
|
|
|
206
206
|
return ConversationGenerateConversationStarterResponse.model_validate_json(
|
|
207
207
|
response.text
|
|
208
208
|
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ConversationResource:
|
|
212
|
+
"""Conversation resource for synchronous operations."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
|
|
215
|
+
self._http = http_client
|
|
216
|
+
self._organization_id = organization_id
|
|
217
|
+
|
|
218
|
+
def create_conversation(
|
|
219
|
+
self,
|
|
220
|
+
body: ConversationCreateConversationRequest,
|
|
221
|
+
params: CreateConversationParametersQuery,
|
|
222
|
+
abort_flag: list[bool] | None = None,
|
|
223
|
+
) -> Iterator[ConversationCreateConversationResponse]:
|
|
224
|
+
def _iter():
|
|
225
|
+
for line in self._http.stream_lines(
|
|
226
|
+
"POST",
|
|
227
|
+
f"/v1/{self._organization_id}/conversation/",
|
|
228
|
+
params=params.model_dump(mode="json", exclude_none=True),
|
|
229
|
+
json=body.model_dump(mode="json", exclude_none=True),
|
|
230
|
+
headers={"Accept": "application/x-ndjson"},
|
|
231
|
+
abort_flag=abort_flag,
|
|
232
|
+
):
|
|
233
|
+
yield ConversationCreateConversationResponse.model_validate_json(line)
|
|
234
|
+
|
|
235
|
+
return _iter()
|
|
236
|
+
|
|
237
|
+
def interact_with_conversation(
|
|
238
|
+
self,
|
|
239
|
+
conversation_id: str,
|
|
240
|
+
params: InteractWithConversationParametersQuery,
|
|
241
|
+
abort_flag: list[bool] | None = None,
|
|
242
|
+
*,
|
|
243
|
+
text_message: str | None = None,
|
|
244
|
+
audio_bytes: bytes | None = None,
|
|
245
|
+
audio_content_type: Literal["audio/mpeg", "audio/wav"] | None = None,
|
|
246
|
+
) -> Iterator[ConversationInteractWithConversationResponse]:
|
|
247
|
+
def _iter():
|
|
248
|
+
request_kwargs: dict[str, Any] = {
|
|
249
|
+
"params": params.model_dump(mode="json", exclude_none=True),
|
|
250
|
+
"headers": {"Accept": "application/x-ndjson"},
|
|
251
|
+
"abort_flag": abort_flag,
|
|
252
|
+
}
|
|
253
|
+
req_format = getattr(params, "request_format", None)
|
|
254
|
+
if req_format == Format.text:
|
|
255
|
+
if text_message is None:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"text_message is required when request_format is 'text'"
|
|
258
|
+
)
|
|
259
|
+
text_bytes = text_message.encode("utf-8")
|
|
260
|
+
request_kwargs["files"] = {
|
|
261
|
+
"recorded_message": (
|
|
262
|
+
"message.txt",
|
|
263
|
+
text_bytes,
|
|
264
|
+
"text/plain; charset=utf-8",
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
elif req_format == Format.voice:
|
|
268
|
+
if audio_bytes is None or audio_content_type is None:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
"audio_bytes and audio_content_type are required when request_format is 'voice'"
|
|
271
|
+
)
|
|
272
|
+
request_kwargs["content"] = audio_bytes
|
|
273
|
+
request_kwargs.setdefault("headers", {})
|
|
274
|
+
request_kwargs["headers"]["Content-Type"] = audio_content_type
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError("Unsupported or missing request_format in params")
|
|
277
|
+
|
|
278
|
+
for line in self._http.stream_lines(
|
|
279
|
+
"POST",
|
|
280
|
+
f"/v1/{self._organization_id}/conversation/{conversation_id}/interact",
|
|
281
|
+
**request_kwargs,
|
|
282
|
+
):
|
|
283
|
+
yield ConversationInteractWithConversationResponse.model_validate_json(
|
|
284
|
+
line
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return _iter()
|
|
288
|
+
|
|
289
|
+
def finish_conversation(self, conversation_id: str) -> None:
|
|
290
|
+
self._http.request(
|
|
291
|
+
"POST",
|
|
292
|
+
f"/v1/{self._organization_id}/conversation/{conversation_id}/finish/",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def get_conversations(
|
|
296
|
+
self, params: GetConversationsParametersQuery
|
|
297
|
+
) -> ConversationGetConversationsResponse:
|
|
298
|
+
response = self._http.request(
|
|
299
|
+
"GET",
|
|
300
|
+
f"/v1/{self._organization_id}/conversation/",
|
|
301
|
+
params=params.model_dump(mode="json", exclude_none=True),
|
|
302
|
+
)
|
|
303
|
+
return ConversationGetConversationsResponse.model_validate_json(response.text)
|
|
304
|
+
|
|
305
|
+
def get_conversation_messages(
|
|
306
|
+
self, conversation_id: str, params: GetConversationMessagesParametersQuery
|
|
307
|
+
) -> ConversationGetConversationMessagesResponse:
|
|
308
|
+
response = self._http.request(
|
|
309
|
+
"GET",
|
|
310
|
+
f"/v1/{self._organization_id}/conversation/{conversation_id}/messages/",
|
|
311
|
+
params=params.model_dump(
|
|
312
|
+
mode="json", exclude_none=True, exclude_defaults=True
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
return ConversationGetConversationMessagesResponse.model_validate_json(
|
|
316
|
+
response.text
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def recommend_responses_for_interaction(
|
|
320
|
+
self, conversation_id: str, interaction_id: str
|
|
321
|
+
) -> ConversationRecommendResponsesForInteractionResponse:
|
|
322
|
+
response = self._http.request(
|
|
323
|
+
"GET",
|
|
324
|
+
f"/v1/{self._organization_id}/conversation/{conversation_id}/interaction/{interaction_id}/recommend_responses",
|
|
325
|
+
)
|
|
326
|
+
return ConversationRecommendResponsesForInteractionResponse.model_validate_json(
|
|
327
|
+
response.text
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def get_interaction_insights(
|
|
331
|
+
self, conversation_id: str, interaction_id: str
|
|
332
|
+
) -> ConversationGetInteractionInsightsResponse:
|
|
333
|
+
response = self._http.request(
|
|
334
|
+
"GET",
|
|
335
|
+
f"/v1/{self._organization_id}/conversation/{conversation_id}/interaction/{interaction_id}/insights",
|
|
336
|
+
)
|
|
337
|
+
return ConversationGetInteractionInsightsResponse.model_validate_json(
|
|
338
|
+
response.text
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def get_message_source(
|
|
342
|
+
self, conversation_id: str, message_id: str
|
|
343
|
+
) -> GetMessageSourceResponse:
|
|
344
|
+
response = self._http.request(
|
|
345
|
+
"GET",
|
|
346
|
+
f"/v1/{self._organization_id}/conversation/{conversation_id}/messages/{message_id}/source",
|
|
347
|
+
)
|
|
348
|
+
return GetMessageSourceResponse.model_validate_json(response.text)
|
|
349
|
+
|
|
350
|
+
def generate_conversation_starters(
|
|
351
|
+
self, body: ConversationGenerateConversationStarterRequest
|
|
352
|
+
) -> ConversationGenerateConversationStarterResponse:
|
|
353
|
+
response = self._http.request(
|
|
354
|
+
"POST",
|
|
355
|
+
f"/v1/{self._organization_id}/conversation/conversation_starter",
|
|
356
|
+
json=body.model_dump(mode="json", exclude_none=True),
|
|
357
|
+
)
|
|
358
|
+
return ConversationGenerateConversationStarterResponse.model_validate_json(
|
|
359
|
+
response.text
|
|
360
|
+
)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from amigo_sdk.generated.model import (
|
|
2
2
|
OrganizationGetOrganizationResponse,
|
|
3
3
|
)
|
|
4
|
-
from amigo_sdk.http_client import AmigoHttpClient
|
|
4
|
+
from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class AsyncOrganizationResource:
|
|
8
8
|
"""Organization resource for Amigo API operations."""
|
|
9
9
|
|
|
10
|
-
def __init__(self, http_client:
|
|
10
|
+
def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
|
|
11
11
|
self._http = http_client
|
|
12
12
|
self._organization_id = organization_id
|
|
13
13
|
|
|
@@ -20,3 +20,15 @@ class OrganizationResource:
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
return OrganizationGetOrganizationResponse.model_validate_json(response.text)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OrganizationResource:
|
|
26
|
+
def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
|
|
27
|
+
self._http = http_client
|
|
28
|
+
self._organization_id = organization_id
|
|
29
|
+
|
|
30
|
+
def get(self) -> OrganizationGetOrganizationResponse:
|
|
31
|
+
response = self._http.request(
|
|
32
|
+
"GET", f"/v1/{self._organization_id}/organization/"
|
|
33
|
+
)
|
|
34
|
+
return OrganizationGetOrganizationResponse.model_validate_json(response.text)
|
amigo_sdk/resources/service.py
CHANGED
|
@@ -4,15 +4,13 @@ from amigo_sdk.generated.model import (
|
|
|
4
4
|
GetServicesParametersQuery,
|
|
5
5
|
ServiceGetServicesResponse,
|
|
6
6
|
)
|
|
7
|
-
from amigo_sdk.http_client import AmigoHttpClient
|
|
7
|
+
from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class AsyncServiceResource:
|
|
11
11
|
"""Service resource for Amigo API operations."""
|
|
12
12
|
|
|
13
|
-
def __init__(
|
|
14
|
-
self, http_client: AmigoHttpClient, organization_id: str
|
|
15
|
-
) -> ServiceGetServicesResponse:
|
|
13
|
+
def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
|
|
16
14
|
self._http = http_client
|
|
17
15
|
self._organization_id = organization_id
|
|
18
16
|
|
|
@@ -28,3 +26,21 @@ class ServiceResource:
|
|
|
28
26
|
else None,
|
|
29
27
|
)
|
|
30
28
|
return ServiceGetServicesResponse.model_validate_json(response.text)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ServiceResource:
|
|
32
|
+
def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
|
|
33
|
+
self._http = http_client
|
|
34
|
+
self._organization_id = organization_id
|
|
35
|
+
|
|
36
|
+
def get_services(
|
|
37
|
+
self, params: Optional[GetServicesParametersQuery] = None
|
|
38
|
+
) -> ServiceGetServicesResponse:
|
|
39
|
+
response = self._http.request(
|
|
40
|
+
"GET",
|
|
41
|
+
f"/v1/{self._organization_id}/service/",
|
|
42
|
+
params=params.model_dump(mode="json", exclude_none=True)
|
|
43
|
+
if params
|
|
44
|
+
else None,
|
|
45
|
+
)
|
|
46
|
+
return ServiceGetServicesResponse.model_validate_json(response.text)
|
amigo_sdk/resources/user.py
CHANGED
|
@@ -7,13 +7,13 @@ from amigo_sdk.generated.model import (
|
|
|
7
7
|
UserGetUsersResponse,
|
|
8
8
|
UserUpdateUserInfoRequest,
|
|
9
9
|
)
|
|
10
|
-
from amigo_sdk.http_client import AmigoHttpClient
|
|
10
|
+
from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class AsyncUserResource:
|
|
14
14
|
"""User resource for Amigo API operations."""
|
|
15
15
|
|
|
16
|
-
def __init__(self, http_client:
|
|
16
|
+
def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
|
|
17
17
|
self._http = http_client
|
|
18
18
|
self._organization_id = organization_id
|
|
19
19
|
|
|
@@ -55,3 +55,43 @@ class UserResource:
|
|
|
55
55
|
f"/v1/{self._organization_id}/user/{user_id}/user",
|
|
56
56
|
json=body.model_dump(mode="json", exclude_none=True),
|
|
57
57
|
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UserResource:
|
|
61
|
+
"""User resource (synchronous)."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
|
|
64
|
+
self._http = http_client
|
|
65
|
+
self._organization_id = organization_id
|
|
66
|
+
|
|
67
|
+
def get_users(
|
|
68
|
+
self, params: Optional[GetUsersParametersQuery] = None
|
|
69
|
+
) -> UserGetUsersResponse:
|
|
70
|
+
response = self._http.request(
|
|
71
|
+
"GET",
|
|
72
|
+
f"/v1/{self._organization_id}/user/",
|
|
73
|
+
params=params.model_dump(mode="json", exclude_none=True)
|
|
74
|
+
if params
|
|
75
|
+
else None,
|
|
76
|
+
)
|
|
77
|
+
return UserGetUsersResponse.model_validate_json(response.text)
|
|
78
|
+
|
|
79
|
+
def create_user(
|
|
80
|
+
self, body: UserCreateInvitedUserRequest
|
|
81
|
+
) -> UserCreateInvitedUserResponse:
|
|
82
|
+
response = self._http.request(
|
|
83
|
+
"POST",
|
|
84
|
+
f"/v1/{self._organization_id}/user/invite",
|
|
85
|
+
json=body.model_dump(mode="json", exclude_none=True),
|
|
86
|
+
)
|
|
87
|
+
return UserCreateInvitedUserResponse.model_validate_json(response.text)
|
|
88
|
+
|
|
89
|
+
def delete_user(self, user_id: str) -> None:
|
|
90
|
+
self._http.request("DELETE", f"/v1/{self._organization_id}/user/{user_id}")
|
|
91
|
+
|
|
92
|
+
def update_user(self, user_id: str, body: UserUpdateUserInfoRequest) -> None:
|
|
93
|
+
self._http.request(
|
|
94
|
+
"POST",
|
|
95
|
+
f"/v1/{self._organization_id}/user/{user_id}/user",
|
|
96
|
+
json=body.model_dump(mode="json", exclude_none=True),
|
|
97
|
+
)
|
amigo_sdk/sdk_client.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
from amigo_sdk.config import AmigoConfig
|
|
4
|
-
from amigo_sdk.http_client import AmigoHttpClient
|
|
5
|
-
from amigo_sdk.resources.conversation import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
|
|
5
|
+
from amigo_sdk.resources.conversation import (
|
|
6
|
+
AsyncConversationResource,
|
|
7
|
+
ConversationResource,
|
|
8
|
+
)
|
|
9
|
+
from amigo_sdk.resources.organization import (
|
|
10
|
+
AsyncOrganizationResource,
|
|
11
|
+
OrganizationResource,
|
|
12
|
+
)
|
|
13
|
+
from amigo_sdk.resources.service import AsyncServiceResource, ServiceResource
|
|
14
|
+
from amigo_sdk.resources.user import AsyncUserResource, UserResource
|
|
9
15
|
|
|
10
16
|
|
|
11
|
-
class
|
|
12
|
-
"""
|
|
13
|
-
Amigo API client
|
|
14
|
-
"""
|
|
17
|
+
class AsyncAmigoClient:
|
|
18
|
+
"""Amigo API client (asynchronous)."""
|
|
15
19
|
|
|
16
20
|
def __init__(
|
|
17
21
|
self,
|
|
@@ -62,11 +66,15 @@ class AmigoClient:
|
|
|
62
66
|
) from e
|
|
63
67
|
|
|
64
68
|
# Initialize HTTP client and resources
|
|
65
|
-
self._http =
|
|
66
|
-
self._organization =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self.
|
|
69
|
+
self._http = AmigoAsyncHttpClient(self._cfg, **httpx_kwargs)
|
|
70
|
+
self._organization = AsyncOrganizationResource(
|
|
71
|
+
self._http, self._cfg.organization_id
|
|
72
|
+
)
|
|
73
|
+
self._service = AsyncServiceResource(self._http, self._cfg.organization_id)
|
|
74
|
+
self._conversation = AsyncConversationResource(
|
|
75
|
+
self._http, self._cfg.organization_id
|
|
76
|
+
)
|
|
77
|
+
self._users = AsyncUserResource(self._http, self._cfg.organization_id)
|
|
70
78
|
|
|
71
79
|
@property
|
|
72
80
|
def config(self) -> AmigoConfig:
|
|
@@ -74,22 +82,22 @@ class AmigoClient:
|
|
|
74
82
|
return self._cfg
|
|
75
83
|
|
|
76
84
|
@property
|
|
77
|
-
def organization(self) ->
|
|
85
|
+
def organization(self) -> AsyncOrganizationResource:
|
|
78
86
|
"""Access organization resource."""
|
|
79
87
|
return self._organization
|
|
80
88
|
|
|
81
89
|
@property
|
|
82
|
-
def service(self) ->
|
|
90
|
+
def service(self) -> AsyncServiceResource:
|
|
83
91
|
"""Access service resource."""
|
|
84
92
|
return self._service
|
|
85
93
|
|
|
86
94
|
@property
|
|
87
|
-
def conversation(self) ->
|
|
95
|
+
def conversation(self) -> AsyncConversationResource:
|
|
88
96
|
"""Access conversation resource."""
|
|
89
97
|
return self._conversation
|
|
90
98
|
|
|
91
99
|
@property
|
|
92
|
-
def users(self) ->
|
|
100
|
+
def users(self) -> AsyncUserResource:
|
|
93
101
|
"""Access user resource."""
|
|
94
102
|
return self._users
|
|
95
103
|
|
|
@@ -103,3 +111,77 @@ class AmigoClient:
|
|
|
103
111
|
|
|
104
112
|
async def __aexit__(self, *_):
|
|
105
113
|
await self.aclose()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AmigoClient:
|
|
117
|
+
"""Amigo API client (synchronous)."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
api_key: Optional[str] = None,
|
|
123
|
+
api_key_id: Optional[str] = None,
|
|
124
|
+
user_id: Optional[str] = None,
|
|
125
|
+
organization_id: Optional[str] = None,
|
|
126
|
+
base_url: Optional[str] = None,
|
|
127
|
+
config: Optional[AmigoConfig] = None,
|
|
128
|
+
**httpx_kwargs: Any,
|
|
129
|
+
):
|
|
130
|
+
if config:
|
|
131
|
+
self._cfg = config
|
|
132
|
+
else:
|
|
133
|
+
cfg_dict: dict[str, Any] = {
|
|
134
|
+
k: v
|
|
135
|
+
for k, v in [
|
|
136
|
+
("api_key", api_key),
|
|
137
|
+
("api_key_id", api_key_id),
|
|
138
|
+
("user_id", user_id),
|
|
139
|
+
("organization_id", organization_id),
|
|
140
|
+
("base_url", base_url),
|
|
141
|
+
]
|
|
142
|
+
if v is not None
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
self._cfg = AmigoConfig(**cfg_dict)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"AmigoClient configuration incomplete. "
|
|
150
|
+
"Provide api_key, api_key_id, user_id, organization_id, base_url "
|
|
151
|
+
"either as kwargs or environment variables."
|
|
152
|
+
) from e
|
|
153
|
+
|
|
154
|
+
self._http = AmigoHttpClient(self._cfg, **httpx_kwargs)
|
|
155
|
+
self._organization = OrganizationResource(self._http, self._cfg.organization_id)
|
|
156
|
+
self._service = ServiceResource(self._http, self._cfg.organization_id)
|
|
157
|
+
self._conversation = ConversationResource(self._http, self._cfg.organization_id)
|
|
158
|
+
self._users = UserResource(self._http, self._cfg.organization_id)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def config(self) -> AmigoConfig:
|
|
162
|
+
return self._cfg
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def organization(self) -> OrganizationResource:
|
|
166
|
+
return self._organization
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def service(self) -> ServiceResource:
|
|
170
|
+
return self._service
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def conversation(self) -> ConversationResource:
|
|
174
|
+
return self._conversation
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def users(self) -> UserResource:
|
|
178
|
+
return self._users
|
|
179
|
+
|
|
180
|
+
def aclose(self) -> None:
|
|
181
|
+
self._http.aclose()
|
|
182
|
+
|
|
183
|
+
def __enter__(self):
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def __exit__(self, *_):
|
|
187
|
+
self.aclose()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amigo_sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Amigo AI Python SDK
|
|
5
5
|
Author: Amigo AI
|
|
6
6
|
License-File: LICENSE
|
|
@@ -46,34 +46,23 @@ amigo_sdk
|
|
|
46
46
|
|
|
47
47
|
This SDK auto-generates its types from the latest [Amigo OpenAPI schema](https://api.amigo.ai/v1/openapi.json). As a result, only the latest published SDK version is guaranteed to match the current API. If you pin to an older version, it may not include the newest endpoints or fields.
|
|
48
48
|
|
|
49
|
-
## Quick Start
|
|
49
|
+
## Quick Start (sync)
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
|
-
import asyncio
|
|
53
52
|
from amigo_sdk import AmigoClient
|
|
54
53
|
from amigo_sdk.generated.model import GetConversationsParametersQuery
|
|
55
54
|
|
|
56
|
-
# Initialize the client
|
|
57
|
-
|
|
55
|
+
# Initialize and use the client synchronously
|
|
56
|
+
with AmigoClient(
|
|
58
57
|
api_key="your-api-key",
|
|
59
58
|
api_key_id="your-api-key-id",
|
|
60
59
|
user_id="user-id",
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
async with client:
|
|
68
|
-
conversations = await client.conversation.get_conversations(
|
|
69
|
-
GetConversationsParametersQuery(limit=10, sort_by=["-created_at"])
|
|
70
|
-
)
|
|
71
|
-
print("Conversations:", conversations)
|
|
72
|
-
except Exception as error:
|
|
73
|
-
print(error)
|
|
74
|
-
|
|
75
|
-
# Run the example
|
|
76
|
-
asyncio.run(example())
|
|
60
|
+
organization_id="your-organization-id",
|
|
61
|
+
) as client:
|
|
62
|
+
conversations = client.conversation.get_conversations(
|
|
63
|
+
GetConversationsParametersQuery(limit=10, sort_by=["-created_at"])
|
|
64
|
+
)
|
|
65
|
+
print("Conversations:", conversations)
|
|
77
66
|
```
|
|
78
67
|
|
|
79
68
|
## Examples
|
|
@@ -84,13 +73,13 @@ For more SDK usage examples see checkout the [examples/](examples/README.md) fol
|
|
|
84
73
|
|
|
85
74
|
The SDK requires the following configuration parameters:
|
|
86
75
|
|
|
87
|
-
| Parameter
|
|
88
|
-
|
|
|
89
|
-
| `api_key`
|
|
90
|
-
| `api_key_id`
|
|
91
|
-
| `user_id`
|
|
92
|
-
| `
|
|
93
|
-
| `base_url`
|
|
76
|
+
| Parameter | Type | Required | Description |
|
|
77
|
+
| ----------------- | ---- | -------- | -------------------------------------------------------------- |
|
|
78
|
+
| `api_key` | str | ✅ | API key from Amigo dashboard |
|
|
79
|
+
| `api_key_id` | str | ✅ | API key ID from Amigo dashboard |
|
|
80
|
+
| `user_id` | str | ✅ | User ID on whose behalf the request is made |
|
|
81
|
+
| `organization_id` | str | ✅ | Your organization ID |
|
|
82
|
+
| `base_url` | str | ❌ | Base URL of the Amigo API (defaults to `https://api.amigo.ai`) |
|
|
94
83
|
|
|
95
84
|
### Environment Variables
|
|
96
85
|
|
|
@@ -100,7 +89,7 @@ You can also configure the SDK using environment variables:
|
|
|
100
89
|
export AMIGO_API_KEY="your-api-key"
|
|
101
90
|
export AMIGO_API_KEY_ID="your-api-key-id"
|
|
102
91
|
export AMIGO_USER_ID="user-id"
|
|
103
|
-
export
|
|
92
|
+
export AMIGO_ORGANIZATION_ID="your-organization-id"
|
|
104
93
|
export AMIGO_BASE_URL="https://api.amigo.ai" # optional
|
|
105
94
|
```
|
|
106
95
|
|
|
@@ -110,7 +99,8 @@ Then initialize the client without parameters:
|
|
|
110
99
|
from amigo_sdk import AmigoClient
|
|
111
100
|
|
|
112
101
|
# Automatically loads from environment variables
|
|
113
|
-
|
|
102
|
+
with AmigoClient() as client:
|
|
103
|
+
...
|
|
114
104
|
```
|
|
115
105
|
|
|
116
106
|
### Using .env Files
|
|
@@ -153,25 +143,23 @@ from amigo_sdk.errors import (
|
|
|
153
143
|
AuthenticationError,
|
|
154
144
|
NotFoundError,
|
|
155
145
|
BadRequestError,
|
|
156
|
-
ValidationError
|
|
146
|
+
ValidationError,
|
|
157
147
|
)
|
|
158
148
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
except Exception as error:
|
|
174
|
-
print("Unexpected error:", error)
|
|
149
|
+
try:
|
|
150
|
+
with AmigoClient() as client:
|
|
151
|
+
org = client.organization.get()
|
|
152
|
+
print("Organization:", org)
|
|
153
|
+
except AuthenticationError as error:
|
|
154
|
+
print("Authentication failed:", error)
|
|
155
|
+
except NotFoundError as error:
|
|
156
|
+
print("Resource not found:", error)
|
|
157
|
+
except BadRequestError as error:
|
|
158
|
+
print("Bad request:", error)
|
|
159
|
+
except ValidationError as error:
|
|
160
|
+
print("Validation error:", error)
|
|
161
|
+
except Exception as error:
|
|
162
|
+
print("Unexpected error:", error)
|
|
175
163
|
```
|
|
176
164
|
|
|
177
165
|
## Development
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
amigo_sdk/__init__.py,sha256=kCOODDDBTcYl4OwHYOLi80txgJizsTwkNb49c88xU0M,153
|
|
2
|
+
amigo_sdk/_retry_utils.py,sha256=kFjw9Wqye6MB5-B4rjLxsbSNcfYBIztcollIoncd1hY,2142
|
|
3
|
+
amigo_sdk/auth.py,sha256=WaM9PcEcnaC6CzNsgRKueHkdSAxNbRylzpR_3Q6guQ0,1765
|
|
4
|
+
amigo_sdk/config.py,sha256=0eZIo-hcJ8ODftKAr-mwB-FGJxGO5PT5T4dRpyWPqAg,1491
|
|
5
|
+
amigo_sdk/errors.py,sha256=RkRyF5eAASd8fIOS6YvL9rLDvLAYWqHfpHSCR7jqvl4,4840
|
|
6
|
+
amigo_sdk/http_client.py,sha256=z8h8FKHRxGzULRz_C60mL5PfYSAv8e_RHUndVo0vHrM,13452
|
|
7
|
+
amigo_sdk/sdk_client.py,sha256=Kr9M9o66pOLu0T2VDvqdYMmPZzgKJyTELu7BSPgGrYQ,6152
|
|
8
|
+
amigo_sdk/generated/model.py,sha256=jYhncFPqXUiwOSmV6OJT82TpkmjG5LZH_gFRFr7mTEs,427161
|
|
9
|
+
amigo_sdk/resources/conversation.py,sha256=pGM2vtUsem8ClVfzZne1qqKZdM4o7aWaRdAYXXKEMFw,14784
|
|
10
|
+
amigo_sdk/resources/organization.py,sha256=yX4UlOHNegRzFW4gCJrCxjiLCAGnGegasjviR1yad_Q,1211
|
|
11
|
+
amigo_sdk/resources/service.py,sha256=SiwEHXCQk4r1b_tGv47M08VuB7RALDHJQzWlpuD937g,1571
|
|
12
|
+
amigo_sdk/resources/user.py,sha256=i4t5aVzBI37KwAtLKSDWTMwf4D4KQdSDoUiblFe1u7o,3529
|
|
13
|
+
amigo_sdk-0.9.0.dist-info/METADATA,sha256=KOQJZyfLcA-ZbG8w22B24yP4XUvUDiUbeUct_4gWMyE,5982
|
|
14
|
+
amigo_sdk-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
amigo_sdk-0.9.0.dist-info/entry_points.txt,sha256=ivKZ8S9W6SH796zUDHeM-qHodrwmkmUItophi-jJWK0,82
|
|
16
|
+
amigo_sdk-0.9.0.dist-info/licenses/LICENSE,sha256=tx3FiTVbGxwBUOxQbNh05AAQlC2jd5hGvNpIkSfVbCo,1062
|
|
17
|
+
amigo_sdk-0.9.0.dist-info/RECORD,,
|
amigo_sdk-0.8.0.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
amigo_sdk/__init__.py,sha256=iPlYCcIzuzW7T2HKDkmYlMkRI51dBLfNRxPPiWrfw9U,22
|
|
2
|
-
amigo_sdk/auth.py,sha256=Kvwk4P2HJaVUIn2Lwj7l6_xTm0IOVxMlthiXthm-V2Y,1029
|
|
3
|
-
amigo_sdk/config.py,sha256=0eZIo-hcJ8ODftKAr-mwB-FGJxGO5PT5T4dRpyWPqAg,1491
|
|
4
|
-
amigo_sdk/errors.py,sha256=RkRyF5eAASd8fIOS6YvL9rLDvLAYWqHfpHSCR7jqvl4,4840
|
|
5
|
-
amigo_sdk/http_client.py,sha256=XDtcQnAXf4E46Kjl6tKVVFgrMgozjCbOcDf21aK_c4s,8843
|
|
6
|
-
amigo_sdk/sdk_client.py,sha256=yLK8f0ARxLOlJX_cM-Y1GW-zCgJWsL2LIrWMxQGaMhY,3685
|
|
7
|
-
amigo_sdk/generated/model.py,sha256=JSpAiUZ_1vrAp7pG3EcOdlXpFAG8w0hd0qKDo_orw9E,427393
|
|
8
|
-
amigo_sdk/resources/conversation.py,sha256=gRkZwqLGOscqoCFcagwU-NfTeMuvLsuEqN_uLyNu1NI,8514
|
|
9
|
-
amigo_sdk/resources/organization.py,sha256=HNwUgeggeEklvcwFS7Of6nGDawN-_Uvd9NsXtcYg65o,726
|
|
10
|
-
amigo_sdk/resources/service.py,sha256=DrKaLnKAglcCeZJQEw50hAOLWtW_InnOu551TxgwF60,947
|
|
11
|
-
amigo_sdk/resources/user.py,sha256=Y66Eb5kH3sYWpdx1E9PP8slban8jpUHmig3YEXHaD_Y,2066
|
|
12
|
-
amigo_sdk-0.8.0.dist-info/METADATA,sha256=UIgZqBJj4VYy8lbcqRIg_CtEaGCzafi0s2KlAXGHz88,6215
|
|
13
|
-
amigo_sdk-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
amigo_sdk-0.8.0.dist-info/entry_points.txt,sha256=ivKZ8S9W6SH796zUDHeM-qHodrwmkmUItophi-jJWK0,82
|
|
15
|
-
amigo_sdk-0.8.0.dist-info/licenses/LICENSE,sha256=tx3FiTVbGxwBUOxQbNh05AAQlC2jd5hGvNpIkSfVbCo,1062
|
|
16
|
-
amigo_sdk-0.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|