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.
@@ -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
+ ![PyPI](https://img.shields.io/pypi/v/chatatp-studio)
44
+ ![Python](https://img.shields.io/pypi/pyversions/chatatp-studio)
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
+ ![PyPI](https://img.shields.io/pypi/v/chatatp-studio)
11
+ ![Python](https://img.shields.io/pypi/pyversions/chatatp-studio)
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()