cominty-sdk 0.1.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.
- cominty_sdk/__init__.py +50 -0
- cominty_sdk/_http.py +169 -0
- cominty_sdk/_qa.py +74 -0
- cominty_sdk/_streaming.py +32 -0
- cominty_sdk/client.py +106 -0
- cominty_sdk/config.py +29 -0
- cominty_sdk/exceptions.py +89 -0
- cominty_sdk/models/__init__.py +38 -0
- cominty_sdk/models/files.py +30 -0
- cominty_sdk/models/messages.py +149 -0
- cominty_sdk/models/threads.py +47 -0
- cominty_sdk/models/usage.py +46 -0
- cominty_sdk/resources/chat.py +85 -0
- cominty_sdk/resources/files.py +84 -0
- cominty_sdk/resources/messages.py +136 -0
- cominty_sdk/resources/threads.py +50 -0
- cominty_sdk/resources/usage.py +14 -0
- cominty_sdk/retry.py +76 -0
- cominty_sdk-0.1.0.dist-info/METADATA +166 -0
- cominty_sdk-0.1.0.dist-info/RECORD +21 -0
- cominty_sdk-0.1.0.dist-info/WHEEL +4 -0
cominty_sdk/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Cominty SDK — async Python client for the managed agent chat API."""
|
|
2
|
+
|
|
3
|
+
from cominty_sdk.client import AsyncCominty
|
|
4
|
+
from cominty_sdk.config import ComintyEnvironment
|
|
5
|
+
from cominty_sdk.exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ComintyAPIError,
|
|
8
|
+
ComintyError,
|
|
9
|
+
ComintyServerShuttingDownError,
|
|
10
|
+
ComintyTimeoutError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ServerError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
from cominty_sdk.models.files import ConversationFileOut
|
|
17
|
+
from cominty_sdk.models.messages import (
|
|
18
|
+
DocumentCitation,
|
|
19
|
+
HumanMessage,
|
|
20
|
+
MessageOut,
|
|
21
|
+
Question,
|
|
22
|
+
WebCitation,
|
|
23
|
+
)
|
|
24
|
+
from cominty_sdk.models.threads import ThreadOut, ThreadSummaryOut
|
|
25
|
+
from cominty_sdk.models.usage import UsageReport
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AsyncCominty",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"ComintyAPIError",
|
|
31
|
+
"ComintyEnvironment",
|
|
32
|
+
"ComintyError",
|
|
33
|
+
"ComintyServerShuttingDownError",
|
|
34
|
+
"ComintyTimeoutError",
|
|
35
|
+
"ConversationFileOut",
|
|
36
|
+
"DocumentCitation",
|
|
37
|
+
"HumanMessage",
|
|
38
|
+
"MessageOut",
|
|
39
|
+
"NotFoundError",
|
|
40
|
+
"Question",
|
|
41
|
+
"RateLimitError",
|
|
42
|
+
"ServerError",
|
|
43
|
+
"ThreadOut",
|
|
44
|
+
"ThreadSummaryOut",
|
|
45
|
+
"UsageReport",
|
|
46
|
+
"ValidationError",
|
|
47
|
+
"WebCitation",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
__version__ = "0.1.0"
|
cominty_sdk/_http.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from cominty_sdk.config import AUTH_HEADER, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
|
|
10
|
+
from cominty_sdk.exceptions import ComintyTimeoutError, raise_for_status
|
|
11
|
+
from cominty_sdk.retry import compute_backoff, is_retryable_exception, maybe_raise_for_status
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound=BaseModel)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncHTTPClient:
|
|
17
|
+
"""Internal async HTTP client with auth, retries, and error mapping."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
base_url: str,
|
|
23
|
+
api_key: str,
|
|
24
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
25
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
26
|
+
stream_timeout: float | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.base_url = base_url.rstrip("/")
|
|
29
|
+
self.api_key = api_key
|
|
30
|
+
self.max_retries = max_retries
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
self.stream_timeout = stream_timeout or timeout
|
|
33
|
+
self._client = httpx.AsyncClient(
|
|
34
|
+
base_url=self.base_url,
|
|
35
|
+
timeout=httpx.Timeout(timeout),
|
|
36
|
+
headers={AUTH_HEADER: api_key},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def close(self) -> None:
|
|
40
|
+
await self._client.aclose()
|
|
41
|
+
|
|
42
|
+
async def request(
|
|
43
|
+
self,
|
|
44
|
+
method: str,
|
|
45
|
+
path: str,
|
|
46
|
+
*,
|
|
47
|
+
params: dict[str, Any] | None = None,
|
|
48
|
+
json: dict[str, Any] | None = None,
|
|
49
|
+
headers: dict[str, str] | None = None,
|
|
50
|
+
parse_json: bool = True,
|
|
51
|
+
) -> Any:
|
|
52
|
+
"""Send a request with retries and return parsed JSON or raw response."""
|
|
53
|
+
last_exc: BaseException | None = None
|
|
54
|
+
for attempt in range(self.max_retries + 1):
|
|
55
|
+
try:
|
|
56
|
+
response = await self._client.request(
|
|
57
|
+
method,
|
|
58
|
+
path,
|
|
59
|
+
params=params,
|
|
60
|
+
json=json,
|
|
61
|
+
headers=headers,
|
|
62
|
+
)
|
|
63
|
+
body: Any
|
|
64
|
+
if parse_json:
|
|
65
|
+
body = response.json() if response.content else None
|
|
66
|
+
else:
|
|
67
|
+
body = response.content
|
|
68
|
+
|
|
69
|
+
if response.status_code >= 400:
|
|
70
|
+
maybe_raise_for_status(
|
|
71
|
+
response.status_code,
|
|
72
|
+
body,
|
|
73
|
+
f"{method} {path} failed",
|
|
74
|
+
)
|
|
75
|
+
return body
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
if not is_retryable_exception(exc) or attempt >= self.max_retries:
|
|
78
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
79
|
+
raise ComintyTimeoutError(str(exc)) from exc
|
|
80
|
+
raise
|
|
81
|
+
last_exc = exc
|
|
82
|
+
await asyncio.sleep(compute_backoff(attempt))
|
|
83
|
+
assert last_exc is not None
|
|
84
|
+
raise last_exc
|
|
85
|
+
|
|
86
|
+
async def request_model(
|
|
87
|
+
self,
|
|
88
|
+
method: str,
|
|
89
|
+
path: str,
|
|
90
|
+
model: type[T],
|
|
91
|
+
*,
|
|
92
|
+
params: dict[str, Any] | None = None,
|
|
93
|
+
json: dict[str, Any] | None = None,
|
|
94
|
+
headers: dict[str, str] | None = None,
|
|
95
|
+
) -> T:
|
|
96
|
+
data = await self.request(method, path, params=params, json=json, headers=headers)
|
|
97
|
+
return model.model_validate(data)
|
|
98
|
+
|
|
99
|
+
async def request_bytes(
|
|
100
|
+
self,
|
|
101
|
+
method: str,
|
|
102
|
+
path: str,
|
|
103
|
+
*,
|
|
104
|
+
params: dict[str, Any] | None = None,
|
|
105
|
+
headers: dict[str, str] | None = None,
|
|
106
|
+
) -> bytes:
|
|
107
|
+
last_exc: BaseException | None = None
|
|
108
|
+
for attempt in range(self.max_retries + 1):
|
|
109
|
+
try:
|
|
110
|
+
response = await self._client.request(
|
|
111
|
+
method,
|
|
112
|
+
path,
|
|
113
|
+
params=params,
|
|
114
|
+
headers=headers,
|
|
115
|
+
)
|
|
116
|
+
if response.status_code >= 400:
|
|
117
|
+
body: Any = None
|
|
118
|
+
if response.content:
|
|
119
|
+
try:
|
|
120
|
+
body = response.json()
|
|
121
|
+
except Exception:
|
|
122
|
+
body = response.text
|
|
123
|
+
raise_for_status(
|
|
124
|
+
response.status_code,
|
|
125
|
+
body,
|
|
126
|
+
f"{method} {path} failed",
|
|
127
|
+
)
|
|
128
|
+
return response.content
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
if not is_retryable_exception(exc) or attempt >= self.max_retries:
|
|
131
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
132
|
+
raise ComintyTimeoutError(str(exc)) from exc
|
|
133
|
+
raise
|
|
134
|
+
last_exc = exc
|
|
135
|
+
await asyncio.sleep(compute_backoff(attempt))
|
|
136
|
+
assert last_exc is not None
|
|
137
|
+
raise last_exc
|
|
138
|
+
|
|
139
|
+
async def stream_request(
|
|
140
|
+
self,
|
|
141
|
+
method: str,
|
|
142
|
+
path: str,
|
|
143
|
+
*,
|
|
144
|
+
params: dict[str, Any] | None = None,
|
|
145
|
+
headers: dict[str, str] | None = None,
|
|
146
|
+
) -> httpx.Response:
|
|
147
|
+
"""Open a streaming HTTP response (caller must close context)."""
|
|
148
|
+
response = await self._client.stream(
|
|
149
|
+
method,
|
|
150
|
+
path,
|
|
151
|
+
params=params,
|
|
152
|
+
headers=headers,
|
|
153
|
+
timeout=httpx.Timeout(self.stream_timeout),
|
|
154
|
+
).__aenter__()
|
|
155
|
+
if response.status_code >= 400:
|
|
156
|
+
await response.aread()
|
|
157
|
+
body: Any = None
|
|
158
|
+
if response.content:
|
|
159
|
+
try:
|
|
160
|
+
body = response.json()
|
|
161
|
+
except Exception:
|
|
162
|
+
body = response.text
|
|
163
|
+
raise_for_status(response.status_code, body, f"{method} {path} failed")
|
|
164
|
+
return response
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def raw_client(self) -> httpx.AsyncClient:
|
|
168
|
+
"""Expose the underlying httpx client for non-API requests (e.g. S3 upload)."""
|
|
169
|
+
return self._client
|
cominty_sdk/_qa.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
StreamEvent = dict[str, Any]
|
|
11
|
+
|
|
12
|
+
DOCUMENT_CITE_PATTERN = re.compile(
|
|
13
|
+
r'<cite\s+document_id="([^"]+)"\s+pages="([^"]+)"\s+name="([^"]+)"\s*/>',
|
|
14
|
+
)
|
|
15
|
+
WEB_CITE_PATTERN = re.compile(r'<cite\s+url="(https?://[^"]+)"\s*/>')
|
|
16
|
+
CITE_TAG_PATTERN = re.compile(r"<cite[^>]*/>")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_tool_names(events: list[dict[str, Any]] | None) -> list[str]:
|
|
20
|
+
"""Extract tool names from message events, deduplicated preserving order."""
|
|
21
|
+
if not events:
|
|
22
|
+
return []
|
|
23
|
+
names: list[str] = []
|
|
24
|
+
seen: set[str] = set()
|
|
25
|
+
for event in events:
|
|
26
|
+
name = _extract_tool_name_from_event(event)
|
|
27
|
+
if name and name not in seen:
|
|
28
|
+
seen.add(name)
|
|
29
|
+
names.append(name)
|
|
30
|
+
return names
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_tool_name_from_event(event: dict[str, Any]) -> str | None:
|
|
34
|
+
for key in ("tool_name", "tool", "name"):
|
|
35
|
+
value = event.get(key)
|
|
36
|
+
if isinstance(value, str) and value:
|
|
37
|
+
return value
|
|
38
|
+
event_type = event.get("type") or event.get("event")
|
|
39
|
+
if event_type in ("tool_call", "tool_use", "tool"):
|
|
40
|
+
for key in ("tool_name", "tool", "name"):
|
|
41
|
+
value = event.get(key)
|
|
42
|
+
if isinstance(value, str) and value:
|
|
43
|
+
return value
|
|
44
|
+
data = event.get("data")
|
|
45
|
+
if isinstance(data, dict):
|
|
46
|
+
return _extract_tool_name_from_event(data)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_cite_tags(content: str) -> list[str]:
|
|
51
|
+
"""Return raw <cite .../> tags found in message content."""
|
|
52
|
+
return CITE_TAG_PATTERN.findall(content)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_document_citations(content: str) -> list[dict[str, str]]:
|
|
56
|
+
"""Parse document citation tags from content."""
|
|
57
|
+
return [
|
|
58
|
+
{"document_id": m.group(1), "pages": m.group(2), "name": m.group(3)}
|
|
59
|
+
for m in DOCUMENT_CITE_PATTERN.finditer(content)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_web_citations(content: str) -> list[dict[str, str]]:
|
|
64
|
+
"""Parse web citation tags from content."""
|
|
65
|
+
return [{"url": m.group(1)} for m in WEB_CITE_PATTERN.finditer(content)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def iter_jsonl_events(response: httpx.Response) -> AsyncIterator[StreamEvent]:
|
|
69
|
+
"""Parse a JSONL stream into async event dicts."""
|
|
70
|
+
async for line in response.aiter_lines():
|
|
71
|
+
stripped = line.strip()
|
|
72
|
+
if not stripped:
|
|
73
|
+
continue
|
|
74
|
+
yield json.loads(stripped)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from cominty_sdk._qa import StreamEvent, iter_jsonl_events
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from cominty_sdk._http import AsyncHTTPClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def stream_message_events(
|
|
13
|
+
http: AsyncHTTPClient,
|
|
14
|
+
message_id: str,
|
|
15
|
+
*,
|
|
16
|
+
last_event_id: str | None = None,
|
|
17
|
+
) -> AsyncIterator[StreamEvent]:
|
|
18
|
+
"""Stream JSONL events for a message."""
|
|
19
|
+
headers: dict[str, str] = {}
|
|
20
|
+
if last_event_id is not None:
|
|
21
|
+
headers["last-event-id"] = last_event_id
|
|
22
|
+
|
|
23
|
+
response = await http.stream_request(
|
|
24
|
+
"GET",
|
|
25
|
+
f"/chat/messages/{message_id}/stream",
|
|
26
|
+
headers=headers or None,
|
|
27
|
+
)
|
|
28
|
+
try:
|
|
29
|
+
async for event in iter_jsonl_events(response):
|
|
30
|
+
yield event
|
|
31
|
+
finally:
|
|
32
|
+
await response.aclose()
|
cominty_sdk/client.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
from cominty_sdk._http import AsyncHTTPClient
|
|
9
|
+
from cominty_sdk.config import (
|
|
10
|
+
DEFAULT_MAX_RETRIES,
|
|
11
|
+
DEFAULT_STREAM_TIMEOUT,
|
|
12
|
+
DEFAULT_TIMEOUT,
|
|
13
|
+
ComintyEnvironment,
|
|
14
|
+
)
|
|
15
|
+
from cominty_sdk.exceptions import resolve_base_url
|
|
16
|
+
from cominty_sdk.resources.chat import ChatResource
|
|
17
|
+
from cominty_sdk.resources.files import FilesResource
|
|
18
|
+
from cominty_sdk.resources.messages import MessagesResource
|
|
19
|
+
from cominty_sdk.resources.threads import ThreadsResource
|
|
20
|
+
from cominty_sdk.resources.usage import UsageResource
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClientSettings(BaseSettings):
|
|
24
|
+
model_config = SettingsConfigDict(
|
|
25
|
+
env_prefix="COMINTY_",
|
|
26
|
+
env_file=".env",
|
|
27
|
+
extra="ignore",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
api_key: str | None = None
|
|
31
|
+
api_url: str | None = None
|
|
32
|
+
environment: ComintyEnvironment = ComintyEnvironment.PRODUCTION
|
|
33
|
+
agent_id: str | None = None
|
|
34
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
35
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
36
|
+
stream_timeout: float = DEFAULT_STREAM_TIMEOUT
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncCominty:
|
|
40
|
+
"""Async client for the Cominty managed agent chat API."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
api_key: str | None = None,
|
|
46
|
+
base_url: str | None = None,
|
|
47
|
+
environment: ComintyEnvironment | str | None = None,
|
|
48
|
+
agent_id: str | None = None,
|
|
49
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
50
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
51
|
+
stream_timeout: float = DEFAULT_STREAM_TIMEOUT,
|
|
52
|
+
) -> None:
|
|
53
|
+
settings = ClientSettings()
|
|
54
|
+
resolved_api_key = api_key or settings.api_key or os.environ.get("COMINTY_API_KEY")
|
|
55
|
+
if not resolved_api_key:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"API key is required. Pass api_key= or set COMINTY_API_KEY."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
resolved_base_url = resolve_base_url(
|
|
61
|
+
base_url=base_url or settings.api_url,
|
|
62
|
+
environment=environment or settings.environment,
|
|
63
|
+
)
|
|
64
|
+
resolved_agent_id = agent_id or settings.agent_id
|
|
65
|
+
if max_retries != DEFAULT_MAX_RETRIES:
|
|
66
|
+
resolved_max_retries = max_retries
|
|
67
|
+
else:
|
|
68
|
+
resolved_max_retries = settings.max_retries
|
|
69
|
+
resolved_timeout = timeout if timeout != DEFAULT_TIMEOUT else settings.timeout
|
|
70
|
+
resolved_stream_timeout = (
|
|
71
|
+
stream_timeout
|
|
72
|
+
if stream_timeout != DEFAULT_STREAM_TIMEOUT
|
|
73
|
+
else settings.stream_timeout
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self._http = AsyncHTTPClient(
|
|
77
|
+
base_url=resolved_base_url,
|
|
78
|
+
api_key=resolved_api_key,
|
|
79
|
+
max_retries=resolved_max_retries,
|
|
80
|
+
timeout=resolved_timeout,
|
|
81
|
+
stream_timeout=resolved_stream_timeout,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.threads = ThreadsResource(self._http)
|
|
85
|
+
self.messages = MessagesResource(
|
|
86
|
+
self._http,
|
|
87
|
+
default_agent_id=resolved_agent_id,
|
|
88
|
+
threads=self.threads,
|
|
89
|
+
)
|
|
90
|
+
self.chat = ChatResource(
|
|
91
|
+
self._http,
|
|
92
|
+
default_agent_id=resolved_agent_id,
|
|
93
|
+
threads=self.threads,
|
|
94
|
+
messages=self.messages,
|
|
95
|
+
)
|
|
96
|
+
self.files = FilesResource(self._http)
|
|
97
|
+
self.usage = UsageResource(self._http)
|
|
98
|
+
|
|
99
|
+
async def close(self) -> None:
|
|
100
|
+
await self._http.close()
|
|
101
|
+
|
|
102
|
+
async def __aenter__(self) -> Self:
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
async def __aexit__(self, *args: object) -> None:
|
|
106
|
+
await self.close()
|
cominty_sdk/config.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
DEFAULT_BASE_URLS: dict[str, str] = {
|
|
5
|
+
"dev": "https://api.dev.cominty.com",
|
|
6
|
+
"staging": "https://api.staging.cominty.com",
|
|
7
|
+
"production": "https://api.cominty.com",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComintyEnvironment(StrEnum):
|
|
12
|
+
DEV = "dev"
|
|
13
|
+
STAGING = "staging"
|
|
14
|
+
PRODUCTION = "production"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EnvironmentName = Literal["dev", "staging", "production"]
|
|
18
|
+
|
|
19
|
+
DEFAULT_MAX_RETRIES = 3
|
|
20
|
+
DEFAULT_TIMEOUT = 60.0
|
|
21
|
+
DEFAULT_STREAM_TIMEOUT = 300.0
|
|
22
|
+
DEFAULT_POLL_INTERVAL = 1.0
|
|
23
|
+
DEFAULT_POLL_TIMEOUT = 120.0
|
|
24
|
+
|
|
25
|
+
# Message statuses that indicate the agent is still processing.
|
|
26
|
+
NON_TERMINAL_STATUSES = frozenset({"pending", "running", "in_progress", "processing"})
|
|
27
|
+
|
|
28
|
+
# Header used for API key authentication.
|
|
29
|
+
AUTH_HEADER = "x-cominty-token"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from cominty_sdk.config import DEFAULT_BASE_URLS, ComintyEnvironment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ComintyError(Exception):
|
|
9
|
+
"""Base exception for all Cominty SDK errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status_code: int | None = None,
|
|
16
|
+
body: Any | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.message = message
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.body = body
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ComintyAPIError(ComintyError):
|
|
25
|
+
"""Raised when the API returns an error response."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthenticationError(ComintyAPIError):
|
|
29
|
+
"""Raised on 401 Unauthorized."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NotFoundError(ComintyAPIError):
|
|
33
|
+
"""Raised on 404 Not Found."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ValidationError(ComintyAPIError):
|
|
37
|
+
"""Raised on 422 Unprocessable Entity."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RateLimitError(ComintyAPIError):
|
|
41
|
+
"""Raised on 429 Too Many Requests."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServerError(ComintyAPIError):
|
|
45
|
+
"""Raised on 5xx server errors."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ComintyTimeoutError(ComintyError):
|
|
49
|
+
"""Raised when a request or poll operation times out."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ComintyServerShuttingDownError(ComintyError):
|
|
53
|
+
"""Raised when the API returns a partial response due to server shutdown."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def raise_for_status(status_code: int, body: Any, message: str | None = None) -> None:
|
|
57
|
+
"""Map HTTP status codes to typed exceptions."""
|
|
58
|
+
msg = message or f"API request failed with status {status_code}"
|
|
59
|
+
if status_code == 401:
|
|
60
|
+
raise AuthenticationError(msg, status_code=status_code, body=body)
|
|
61
|
+
if status_code == 404:
|
|
62
|
+
raise NotFoundError(msg, status_code=status_code, body=body)
|
|
63
|
+
if status_code == 422:
|
|
64
|
+
raise ValidationError(msg, status_code=status_code, body=body)
|
|
65
|
+
if status_code == 429:
|
|
66
|
+
raise RateLimitError(msg, status_code=status_code, body=body)
|
|
67
|
+
if 500 <= status_code < 600:
|
|
68
|
+
raise ServerError(msg, status_code=status_code, body=body)
|
|
69
|
+
if 400 <= status_code < 500:
|
|
70
|
+
raise ComintyAPIError(msg, status_code=status_code, body=body)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_base_url(
|
|
74
|
+
*,
|
|
75
|
+
base_url: str | None = None,
|
|
76
|
+
environment: ComintyEnvironment | str | None = None,
|
|
77
|
+
) -> str:
|
|
78
|
+
"""Resolve the API base URL from explicit override or environment name."""
|
|
79
|
+
if base_url is not None:
|
|
80
|
+
return base_url.rstrip("/")
|
|
81
|
+
env = environment or ComintyEnvironment.PRODUCTION
|
|
82
|
+
env_name = env.value if isinstance(env, ComintyEnvironment) else env
|
|
83
|
+
try:
|
|
84
|
+
return DEFAULT_BASE_URLS[env_name].rstrip("/")
|
|
85
|
+
except KeyError as exc:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Unknown environment {env_name!r}. "
|
|
88
|
+
f"Expected one of: {', '.join(DEFAULT_BASE_URLS)}"
|
|
89
|
+
) from exc
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from cominty_sdk.models.files import (
|
|
2
|
+
ConversationFileOut,
|
|
3
|
+
FileUploadConfirmation,
|
|
4
|
+
FileUploadPermission,
|
|
5
|
+
)
|
|
6
|
+
from cominty_sdk.models.messages import (
|
|
7
|
+
ChatOptions,
|
|
8
|
+
ChatRequest,
|
|
9
|
+
DocumentCitation,
|
|
10
|
+
HumanMessage,
|
|
11
|
+
MessageOut,
|
|
12
|
+
Question,
|
|
13
|
+
StartChatOptions,
|
|
14
|
+
StartChatRequest,
|
|
15
|
+
WebCitation,
|
|
16
|
+
)
|
|
17
|
+
from cominty_sdk.models.threads import ThreadOut, ThreadSummaryOut, ThreadUpdate
|
|
18
|
+
from cominty_sdk.models.usage import AgentDetail, UsageReport
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AgentDetail",
|
|
22
|
+
"ChatOptions",
|
|
23
|
+
"ChatRequest",
|
|
24
|
+
"ConversationFileOut",
|
|
25
|
+
"DocumentCitation",
|
|
26
|
+
"FileUploadConfirmation",
|
|
27
|
+
"FileUploadPermission",
|
|
28
|
+
"HumanMessage",
|
|
29
|
+
"MessageOut",
|
|
30
|
+
"Question",
|
|
31
|
+
"StartChatOptions",
|
|
32
|
+
"StartChatRequest",
|
|
33
|
+
"ThreadOut",
|
|
34
|
+
"ThreadSummaryOut",
|
|
35
|
+
"ThreadUpdate",
|
|
36
|
+
"UsageReport",
|
|
37
|
+
"WebCitation",
|
|
38
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Pydantic models for chat file operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileUploadPermission(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="ignore")
|
|
10
|
+
|
|
11
|
+
url: str
|
|
12
|
+
fields: dict[str, str] = Field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileUploadConfirmation(BaseModel):
|
|
16
|
+
model_config = ConfigDict(extra="forbid")
|
|
17
|
+
|
|
18
|
+
etag: str
|
|
19
|
+
key: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConversationFileOut(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="ignore")
|
|
24
|
+
|
|
25
|
+
id: str
|
|
26
|
+
name: str
|
|
27
|
+
size: int
|
|
28
|
+
mimetype: str
|
|
29
|
+
origin: str
|
|
30
|
+
url: str | None = None
|