chatatp-studio 0.1.0__tar.gz
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.
- chatatp_studio-0.1.0/PKG-INFO +142 -0
- chatatp_studio-0.1.0/README.md +109 -0
- chatatp_studio-0.1.0/pyproject.toml +75 -0
- chatatp_studio-0.1.0/setup.cfg +4 -0
- chatatp_studio-0.1.0/src/chatatp_studio/__init__.py +50 -0
- chatatp_studio-0.1.0/src/chatatp_studio/_requester.py +180 -0
- chatatp_studio-0.1.0/src/chatatp_studio/client.py +135 -0
- chatatp_studio-0.1.0/src/chatatp_studio/errors/__init__.py +101 -0
- chatatp_studio-0.1.0/src/chatatp_studio/models/__init__.py +168 -0
- chatatp_studio-0.1.0/src/chatatp_studio/resources/__init__.py +6 -0
- chatatp_studio-0.1.0/src/chatatp_studio/resources/agents.py +21 -0
- chatatp_studio-0.1.0/src/chatatp_studio/resources/conversations.py +53 -0
- chatatp_studio-0.1.0/src/chatatp_studio/resources/messages.py +68 -0
- chatatp_studio-0.1.0/src/chatatp_studio/resources/usage.py +16 -0
- chatatp_studio-0.1.0/src/chatatp_studio.egg-info/PKG-INFO +142 -0
- chatatp_studio-0.1.0/src/chatatp_studio.egg-info/SOURCES.txt +18 -0
- chatatp_studio-0.1.0/src/chatatp_studio.egg-info/dependency_links.txt +1 -0
- chatatp_studio-0.1.0/src/chatatp_studio.egg-info/requires.txt +10 -0
- chatatp_studio-0.1.0/src/chatatp_studio.egg-info/top_level.txt +1 -0
- chatatp_studio-0.1.0/tests/test_sdk.py +251 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatatp-studio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for ChatATP Studio Developer API
|
|
5
|
+
Author-email: Samuel Obinna Chimdi <sammyfirst6@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://studio.chat-atp.com
|
|
8
|
+
Project-URL: Documentation, https://studio.chat-atp.com/docs
|
|
9
|
+
Project-URL: Source, https://github.com/sam-14uel/chatatp_studio_python
|
|
10
|
+
Project-URL: Issues, https://github.com/sam-14uel/chatatp_studio_python/issues
|
|
11
|
+
Keywords: chatatp,chatatp-studio,agent-sdk,ai-agents,llm-agents,chatbot-sdk,api-client,developer-tools,async-client,python-sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-httpx>=0.30; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# ChatATP Studio SDK
|
|
35
|
+
|
|
36
|
+
Python SDK for building and interacting with agents created in ChatATP Studio.
|
|
37
|
+
|
|
38
|
+
- Async-first API
|
|
39
|
+
- Conversation lifecycle management
|
|
40
|
+
- Streaming support
|
|
41
|
+
- Fully typed
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
# chatatp-studio
|
|
47
|
+
|
|
48
|
+
Official Python SDK for the [ChatATP Studio](https://studio.chat-atp.com) Developer API.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install chatatp-studio
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from chatatp_studio import ChatATPClient
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
client = ChatATPClient(api_key="chatatp_sk_...")
|
|
68
|
+
|
|
69
|
+
# Send a message — conversation lifecycle handled automatically
|
|
70
|
+
result = await client.chat(
|
|
71
|
+
agent_id=7,
|
|
72
|
+
external_user_id="user_12345",
|
|
73
|
+
message="Do you ship to Lagos?",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
print(result.agent_message.content)
|
|
77
|
+
# → "Yes, shipping is available."
|
|
78
|
+
|
|
79
|
+
await client.aclose()
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Context manager
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
async with ChatATPClient(api_key="chatatp_sk_...") as client:
|
|
88
|
+
result = await client.chat(
|
|
89
|
+
agent_id=7,
|
|
90
|
+
external_user_id="user_12345",
|
|
91
|
+
message="Hello!",
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Streaming
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
async for event in await client.chat_stream(
|
|
99
|
+
agent_id=7,
|
|
100
|
+
external_user_id="user_12345",
|
|
101
|
+
message="Give me a summary of your return policy.",
|
|
102
|
+
):
|
|
103
|
+
if event.type == "agent.response.completed":
|
|
104
|
+
print(event.data)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Resources
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Agents
|
|
111
|
+
page = await client.agents.list()
|
|
112
|
+
agent = await client.agents.retrieve(7)
|
|
113
|
+
|
|
114
|
+
# Conversations
|
|
115
|
+
conv = await client.conversations.create(7, "user_12345")
|
|
116
|
+
page = await client.conversations.list(agent_id=7)
|
|
117
|
+
await client.conversations.delete(conv.id)
|
|
118
|
+
|
|
119
|
+
# Messages
|
|
120
|
+
history = await client.messages.list(conv.id)
|
|
121
|
+
reply = await client.messages.send(conv.id, "Hello")
|
|
122
|
+
|
|
123
|
+
# Usage
|
|
124
|
+
usage = await client.usage.retrieve()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Error handling
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from chatatp_studio import NotFoundError, RateLimitError
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
await client.agents.retrieve(999)
|
|
134
|
+
except NotFoundError:
|
|
135
|
+
print("Not found")
|
|
136
|
+
except RateLimitError:
|
|
137
|
+
print("Rate limited")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# ChatATP Studio SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for building and interacting with agents created in ChatATP Studio.
|
|
4
|
+
|
|
5
|
+
- Async-first API
|
|
6
|
+
- Conversation lifecycle management
|
|
7
|
+
- Streaming support
|
|
8
|
+
- Fully typed
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
# chatatp-studio
|
|
14
|
+
|
|
15
|
+
Official Python SDK for the [ChatATP Studio](https://studio.chat-atp.com) Developer API.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Python 3.10+
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install chatatp-studio
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
from chatatp_studio import ChatATPClient
|
|
32
|
+
|
|
33
|
+
async def main():
|
|
34
|
+
client = ChatATPClient(api_key="chatatp_sk_...")
|
|
35
|
+
|
|
36
|
+
# Send a message — conversation lifecycle handled automatically
|
|
37
|
+
result = await client.chat(
|
|
38
|
+
agent_id=7,
|
|
39
|
+
external_user_id="user_12345",
|
|
40
|
+
message="Do you ship to Lagos?",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
print(result.agent_message.content)
|
|
44
|
+
# → "Yes, shipping is available."
|
|
45
|
+
|
|
46
|
+
await client.aclose()
|
|
47
|
+
|
|
48
|
+
asyncio.run(main())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Context manager
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
async with ChatATPClient(api_key="chatatp_sk_...") as client:
|
|
55
|
+
result = await client.chat(
|
|
56
|
+
agent_id=7,
|
|
57
|
+
external_user_id="user_12345",
|
|
58
|
+
message="Hello!",
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Streaming
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
async for event in await client.chat_stream(
|
|
66
|
+
agent_id=7,
|
|
67
|
+
external_user_id="user_12345",
|
|
68
|
+
message="Give me a summary of your return policy.",
|
|
69
|
+
):
|
|
70
|
+
if event.type == "agent.response.completed":
|
|
71
|
+
print(event.data)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Resources
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Agents
|
|
78
|
+
page = await client.agents.list()
|
|
79
|
+
agent = await client.agents.retrieve(7)
|
|
80
|
+
|
|
81
|
+
# Conversations
|
|
82
|
+
conv = await client.conversations.create(7, "user_12345")
|
|
83
|
+
page = await client.conversations.list(agent_id=7)
|
|
84
|
+
await client.conversations.delete(conv.id)
|
|
85
|
+
|
|
86
|
+
# Messages
|
|
87
|
+
history = await client.messages.list(conv.id)
|
|
88
|
+
reply = await client.messages.send(conv.id, "Hello")
|
|
89
|
+
|
|
90
|
+
# Usage
|
|
91
|
+
usage = await client.usage.retrieve()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Error handling
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from chatatp_studio import NotFoundError, RateLimitError
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
await client.agents.retrieve(999)
|
|
101
|
+
except NotFoundError:
|
|
102
|
+
print("Not found")
|
|
103
|
+
except RateLimitError:
|
|
104
|
+
print("Rate limited")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chatatp-studio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for ChatATP Studio Developer API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Samuel Obinna Chimdi", email = "sammyfirst6@gmail.com" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
keywords = [
|
|
18
|
+
"chatatp",
|
|
19
|
+
"chatatp-studio",
|
|
20
|
+
"agent-sdk",
|
|
21
|
+
"ai-agents",
|
|
22
|
+
"llm-agents",
|
|
23
|
+
"chatbot-sdk",
|
|
24
|
+
"api-client",
|
|
25
|
+
"developer-tools",
|
|
26
|
+
"async-client",
|
|
27
|
+
"python-sdk"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Development Status :: 4 - Beta",
|
|
32
|
+
"Intended Audience :: Developers",
|
|
33
|
+
"Topic :: Software Development :: Libraries",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
"Programming Language :: Python :: 3",
|
|
36
|
+
"Programming Language :: Python :: 3.10",
|
|
37
|
+
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
39
|
+
"Operating System :: OS Independent",
|
|
40
|
+
"License :: OSI Approved :: MIT License"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
dependencies = [
|
|
44
|
+
"httpx>=0.27"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8",
|
|
50
|
+
"pytest-asyncio>=0.23",
|
|
51
|
+
"pytest-httpx>=0.30",
|
|
52
|
+
"mypy>=1.10",
|
|
53
|
+
"build",
|
|
54
|
+
"twine",
|
|
55
|
+
"ruff"
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[project.urls]
|
|
59
|
+
Homepage = "https://studio.chat-atp.com"
|
|
60
|
+
Documentation = "https://studio.chat-atp.com/docs"
|
|
61
|
+
Source = "https://github.com/sam-14uel/chatatp_studio_python"
|
|
62
|
+
Issues = "https://github.com/sam-14uel/chatatp_studio_python/issues"
|
|
63
|
+
|
|
64
|
+
[tool.setuptools]
|
|
65
|
+
package-dir = {"" = "src"}
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.packages.find]
|
|
68
|
+
where = ["src"]
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
asyncio_mode = "auto"
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
strict = true
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""ChatATP Studio Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import ChatATPClient
|
|
4
|
+
from .errors import (
|
|
5
|
+
ChatATPError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
PermissionError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
ServerError,
|
|
12
|
+
NetworkError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
)
|
|
15
|
+
from .models import (
|
|
16
|
+
Agent,
|
|
17
|
+
AgentCapabilities,
|
|
18
|
+
Conversation,
|
|
19
|
+
ConversationSummary,
|
|
20
|
+
Message,
|
|
21
|
+
SendMessageResponse,
|
|
22
|
+
Usage,
|
|
23
|
+
StreamEvent,
|
|
24
|
+
Page,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ChatATPClient",
|
|
30
|
+
# Errors
|
|
31
|
+
"ChatATPError",
|
|
32
|
+
"AuthenticationError",
|
|
33
|
+
"PermissionError",
|
|
34
|
+
"ValidationError",
|
|
35
|
+
"RateLimitError",
|
|
36
|
+
"NotFoundError",
|
|
37
|
+
"ServerError",
|
|
38
|
+
"NetworkError",
|
|
39
|
+
"TimeoutError",
|
|
40
|
+
# Models
|
|
41
|
+
"Agent",
|
|
42
|
+
"AgentCapabilities",
|
|
43
|
+
"Conversation",
|
|
44
|
+
"ConversationSummary",
|
|
45
|
+
"Message",
|
|
46
|
+
"SendMessageResponse",
|
|
47
|
+
"Usage",
|
|
48
|
+
"StreamEvent",
|
|
49
|
+
"Page",
|
|
50
|
+
]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Low-level HTTP client wrapping httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .errors import NetworkError, TimeoutError, build_api_error
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("chatatp")
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://chatatp-agent-builder-backend.onrender.com"
|
|
17
|
+
DEFAULT_TIMEOUT = 30.0
|
|
18
|
+
DEFAULT_MAX_RETRIES = 2
|
|
19
|
+
RETRY_INITIAL_DELAY = 0.5
|
|
20
|
+
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Requester:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
api_key: str,
|
|
28
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
29
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
30
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
31
|
+
debug: bool = False,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._api_key = api_key
|
|
34
|
+
self.base_url = base_url.rstrip("/")
|
|
35
|
+
self._timeout = timeout
|
|
36
|
+
self._max_retries = max_retries
|
|
37
|
+
self._debug = debug
|
|
38
|
+
|
|
39
|
+
if debug:
|
|
40
|
+
logging.basicConfig()
|
|
41
|
+
logger.setLevel(logging.DEBUG)
|
|
42
|
+
|
|
43
|
+
self._client = httpx.AsyncClient(
|
|
44
|
+
base_url=self.base_url,
|
|
45
|
+
timeout=httpx.Timeout(timeout),
|
|
46
|
+
headers=self._auth_headers(),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
50
|
+
return {
|
|
51
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async def request(
|
|
56
|
+
self,
|
|
57
|
+
method: str,
|
|
58
|
+
path: str,
|
|
59
|
+
*,
|
|
60
|
+
body: Any = None,
|
|
61
|
+
params: dict[str, Any] | None = None,
|
|
62
|
+
) -> Any:
|
|
63
|
+
clean_params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
64
|
+
|
|
65
|
+
attempt = 0
|
|
66
|
+
delay = RETRY_INITIAL_DELAY
|
|
67
|
+
|
|
68
|
+
while True:
|
|
69
|
+
logger.debug("→ %s %s", method, path)
|
|
70
|
+
try:
|
|
71
|
+
response = await self._client.request(
|
|
72
|
+
method,
|
|
73
|
+
path,
|
|
74
|
+
json=body,
|
|
75
|
+
params=clean_params or None,
|
|
76
|
+
)
|
|
77
|
+
except httpx.TimeoutException as exc:
|
|
78
|
+
if attempt < self._max_retries:
|
|
79
|
+
logger.debug("timeout, retrying in %.1fs", delay)
|
|
80
|
+
await _sleep(delay)
|
|
81
|
+
delay *= 2
|
|
82
|
+
attempt += 1
|
|
83
|
+
continue
|
|
84
|
+
raise TimeoutError(f"Request to {path} timed out.") from exc
|
|
85
|
+
except httpx.RequestError as exc:
|
|
86
|
+
if attempt < self._max_retries:
|
|
87
|
+
logger.debug("network error, retrying in %.1fs", delay)
|
|
88
|
+
await _sleep(delay)
|
|
89
|
+
delay *= 2
|
|
90
|
+
attempt += 1
|
|
91
|
+
continue
|
|
92
|
+
raise NetworkError(f"Network request failed: {exc}") from exc
|
|
93
|
+
|
|
94
|
+
logger.debug("← %s", response.status_code)
|
|
95
|
+
|
|
96
|
+
if response.status_code == 204:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
body_json: dict[str, Any] = {}
|
|
100
|
+
try:
|
|
101
|
+
body_json = response.json()
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
if not response.is_success:
|
|
106
|
+
request_id = response.headers.get("x-request-id")
|
|
107
|
+
err = build_api_error(response.status_code, body_json, request_id)
|
|
108
|
+
if response.status_code in RETRYABLE_STATUSES and attempt < self._max_retries:
|
|
109
|
+
logger.debug("retrying in %.1fs (attempt %d)", delay, attempt + 1)
|
|
110
|
+
await _sleep(delay)
|
|
111
|
+
delay *= 2
|
|
112
|
+
attempt += 1
|
|
113
|
+
continue
|
|
114
|
+
raise err
|
|
115
|
+
|
|
116
|
+
return body_json
|
|
117
|
+
|
|
118
|
+
async def stream(
|
|
119
|
+
self,
|
|
120
|
+
path: str,
|
|
121
|
+
body: Any,
|
|
122
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
123
|
+
"""Yield parsed SSE events as dicts with ``type`` and ``data`` keys."""
|
|
124
|
+
headers = {**self._auth_headers(), "Accept": "text/event-stream"}
|
|
125
|
+
buffer = ""
|
|
126
|
+
|
|
127
|
+
async with self._client.stream(
|
|
128
|
+
"POST",
|
|
129
|
+
path,
|
|
130
|
+
json=body,
|
|
131
|
+
headers=headers,
|
|
132
|
+
timeout=httpx.Timeout(self._timeout * 4),
|
|
133
|
+
) as response:
|
|
134
|
+
if not response.is_success:
|
|
135
|
+
raw = await response.aread()
|
|
136
|
+
body_json: dict[str, Any] = {}
|
|
137
|
+
try:
|
|
138
|
+
body_json = json.loads(raw)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
raise build_api_error(response.status_code, body_json)
|
|
142
|
+
|
|
143
|
+
async for chunk in response.aiter_text():
|
|
144
|
+
buffer += chunk
|
|
145
|
+
*events, buffer = buffer.split("\n\n")
|
|
146
|
+
for block in events:
|
|
147
|
+
parsed = _parse_sse_block(block)
|
|
148
|
+
if parsed:
|
|
149
|
+
logger.debug("← event: %s", parsed["type"])
|
|
150
|
+
yield parsed
|
|
151
|
+
|
|
152
|
+
async def aclose(self) -> None:
|
|
153
|
+
await self._client.aclose()
|
|
154
|
+
|
|
155
|
+
async def __aenter__(self) -> "Requester":
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
159
|
+
await self.aclose()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _parse_sse_block(block: str) -> dict[str, Any] | None:
|
|
163
|
+
event_type = ""
|
|
164
|
+
data_str = ""
|
|
165
|
+
for line in block.splitlines():
|
|
166
|
+
if line.startswith("event:"):
|
|
167
|
+
event_type = line[6:].strip()
|
|
168
|
+
elif line.startswith("data:"):
|
|
169
|
+
data_str = line[5:].strip()
|
|
170
|
+
if not data_str:
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
return {"type": event_type, "data": json.loads(data_str)}
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
return {"type": event_type, "data": data_str}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _sleep(seconds: float) -> None:
|
|
179
|
+
import asyncio
|
|
180
|
+
await asyncio.sleep(seconds)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Main ChatATP Studio client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._requester import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, Requester
|
|
9
|
+
from .models import SendMessageResponse, StreamEvent
|
|
10
|
+
from .resources import AgentsResource, ConversationsResource, MessagesResource, UsageResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChatATPClient:
|
|
14
|
+
"""
|
|
15
|
+
Async client for the ChatATP Studio Developer API.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from chatatp_studio import ChatATPClient
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
client = ChatATPClient(api_key="chatatp_sk_...")
|
|
24
|
+
result = await client.chat(
|
|
25
|
+
agent_id=7,
|
|
26
|
+
external_user_id="user_12345",
|
|
27
|
+
message="Do you ship to Lagos?",
|
|
28
|
+
)
|
|
29
|
+
print(result.agent_message.content)
|
|
30
|
+
await client.aclose()
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
33
|
+
|
|
34
|
+
Or as a context manager::
|
|
35
|
+
|
|
36
|
+
async with ChatATPClient(api_key="chatatp_sk_...") as client:
|
|
37
|
+
...
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str,
|
|
43
|
+
*,
|
|
44
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
45
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
46
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
47
|
+
debug: bool = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
if not api_key:
|
|
50
|
+
raise ValueError("api_key is required")
|
|
51
|
+
|
|
52
|
+
self._requester = Requester(
|
|
53
|
+
api_key=api_key,
|
|
54
|
+
base_url=base_url,
|
|
55
|
+
timeout=timeout,
|
|
56
|
+
max_retries=max_retries,
|
|
57
|
+
debug=debug,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.agents = AgentsResource(self._requester)
|
|
61
|
+
self.conversations = ConversationsResource(self._requester)
|
|
62
|
+
self.messages = MessagesResource(self._requester)
|
|
63
|
+
self.usage = UsageResource(self._requester)
|
|
64
|
+
|
|
65
|
+
# ──────────────────────────────────────────────
|
|
66
|
+
# High-level chat interface
|
|
67
|
+
# ──────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
async def chat(
|
|
70
|
+
self,
|
|
71
|
+
agent_id: int,
|
|
72
|
+
external_user_id: str,
|
|
73
|
+
message: str,
|
|
74
|
+
*,
|
|
75
|
+
user_display_name: str | None = None,
|
|
76
|
+
metadata: dict[str, Any] | None = None,
|
|
77
|
+
) -> SendMessageResponse:
|
|
78
|
+
"""
|
|
79
|
+
Send a message to an agent on behalf of a user.
|
|
80
|
+
|
|
81
|
+
The SDK automatically creates or retrieves the underlying conversation,
|
|
82
|
+
so you only need the agent ID, user ID, and message content.
|
|
83
|
+
|
|
84
|
+
:param agent_id: The ID of the agent to send the message to.
|
|
85
|
+
:param external_user_id: Your unique identifier for the end user.
|
|
86
|
+
:param message: The message content to send.
|
|
87
|
+
:param user_display_name: Optional human-readable name for the user.
|
|
88
|
+
:param metadata: Optional key-value metadata attached to the conversation.
|
|
89
|
+
:returns: A SendMessageResponse with ``user_message`` and ``agent_message``.
|
|
90
|
+
"""
|
|
91
|
+
conversation = await self.conversations.create(
|
|
92
|
+
agent_id,
|
|
93
|
+
external_user_id,
|
|
94
|
+
user_display_name=user_display_name,
|
|
95
|
+
metadata=metadata,
|
|
96
|
+
)
|
|
97
|
+
return await self.messages.send(conversation.id, message)
|
|
98
|
+
|
|
99
|
+
async def chat_stream(
|
|
100
|
+
self,
|
|
101
|
+
agent_id: int,
|
|
102
|
+
external_user_id: str,
|
|
103
|
+
message: str,
|
|
104
|
+
*,
|
|
105
|
+
user_display_name: str | None = None,
|
|
106
|
+
metadata: dict[str, Any] | None = None,
|
|
107
|
+
) -> AsyncGenerator[StreamEvent, None]:
|
|
108
|
+
"""
|
|
109
|
+
Stream a message response from an agent.
|
|
110
|
+
|
|
111
|
+
Like :meth:`chat`, conversation lifecycle is handled automatically.
|
|
112
|
+
|
|
113
|
+
Usage::
|
|
114
|
+
|
|
115
|
+
async for event in client.chat_stream(7, "user_12345", "Hello"):
|
|
116
|
+
if event.type == "agent.response.completed":
|
|
117
|
+
print(event.data)
|
|
118
|
+
"""
|
|
119
|
+
conversation = await self.conversations.create(
|
|
120
|
+
agent_id,
|
|
121
|
+
external_user_id,
|
|
122
|
+
user_display_name=user_display_name,
|
|
123
|
+
metadata=metadata,
|
|
124
|
+
)
|
|
125
|
+
return await self.messages.stream(conversation.id, message)
|
|
126
|
+
|
|
127
|
+
async def aclose(self) -> None:
|
|
128
|
+
"""Close the underlying HTTP client."""
|
|
129
|
+
await self._requester.aclose()
|
|
130
|
+
|
|
131
|
+
async def __aenter__(self) -> "ChatATPClient":
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
135
|
+
await self.aclose()
|