marona 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.
- marona-0.1.0/LICENSE +21 -0
- marona-0.1.0/PKG-INFO +214 -0
- marona-0.1.0/README.md +185 -0
- marona-0.1.0/marona/__init__.py +39 -0
- marona-0.1.0/marona/business.py +184 -0
- marona-0.1.0/marona/client.py +427 -0
- marona-0.1.0/marona/models.py +386 -0
- marona-0.1.0/marona/py.typed +1 -0
- marona-0.1.0/marona.egg-info/PKG-INFO +214 -0
- marona-0.1.0/marona.egg-info/SOURCES.txt +14 -0
- marona-0.1.0/marona.egg-info/dependency_links.txt +1 -0
- marona-0.1.0/marona.egg-info/requires.txt +6 -0
- marona-0.1.0/marona.egg-info/top_level.txt +1 -0
- marona-0.1.0/pyproject.toml +52 -0
- marona-0.1.0/setup.cfg +4 -0
- marona-0.1.0/tests/test_marona_client.py +124 -0
marona-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blessing Nyuwani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
marona-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marona
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client SDK for Marona-compatible AI runtimes.
|
|
5
|
+
Author: Blessing Nyuwani
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.marona.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/BlessingNyuwani/edge-node-service
|
|
9
|
+
Project-URL: Documentation, https://hub.marona.ai
|
|
10
|
+
Keywords: marona,sdk,mcp,agents,ai,runtime
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx<1.0,>=0.28
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: build<2.0,>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: twine<7.0,>=6.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# marona
|
|
31
|
+
|
|
32
|
+
Official Python client SDK for Marona-compatible AI runtimes.
|
|
33
|
+
|
|
34
|
+
Marona provides a simple interface for messaging, discovering apps, managing
|
|
35
|
+
permissions, connecting service accounts, receiving inbound events, and using
|
|
36
|
+
tools from any compatible runtime.
|
|
37
|
+
|
|
38
|
+
You can use it with Marona Cloud or with your own self-hosted Marona-compatible
|
|
39
|
+
runtime.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install marona
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Send a message
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
|
|
52
|
+
from marona import Marona
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def main() -> None:
|
|
56
|
+
client = Marona(
|
|
57
|
+
base_url="https://edge.marona.ai",
|
|
58
|
+
api_key="mrn_live_...",
|
|
59
|
+
)
|
|
60
|
+
response = await client.message(
|
|
61
|
+
"Create a PowerPoint about AI introduction",
|
|
62
|
+
interface="api",
|
|
63
|
+
conversation_id="demo",
|
|
64
|
+
)
|
|
65
|
+
print(response.reply)
|
|
66
|
+
for artifact in response.artifacts:
|
|
67
|
+
print(artifact.get("filename"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Runtime targets
|
|
74
|
+
|
|
75
|
+
Marona Cloud:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from marona import Marona
|
|
79
|
+
|
|
80
|
+
client = Marona(
|
|
81
|
+
base_url="https://edge.marona.ai",
|
|
82
|
+
api_key="mrn_live_...",
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Self-hosted:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from marona import Marona
|
|
90
|
+
|
|
91
|
+
client = Marona(
|
|
92
|
+
base_url="https://my-company-edge.example.com",
|
|
93
|
+
api_key="mrn_live_...",
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## List available apps
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
|
|
102
|
+
from marona import Marona
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def main() -> None:
|
|
106
|
+
client = Marona("https://edge.marona.ai")
|
|
107
|
+
apps = await client.list_apps(limit=20, search="slides")
|
|
108
|
+
|
|
109
|
+
for app in apps.apps:
|
|
110
|
+
print(app["slug"], app["name"])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Pair an interface with WhatsApp
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import asyncio
|
|
120
|
+
|
|
121
|
+
from marona import Marona
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def main() -> None:
|
|
125
|
+
client = Marona("https://edge.marona.ai")
|
|
126
|
+
pairing = await client.start_pairing(interface="web", device_name="My web app")
|
|
127
|
+
|
|
128
|
+
print(pairing.display_code)
|
|
129
|
+
print(pairing.whatsapp_url)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
After the user sends the pairing code from WhatsApp, poll for completion:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
status = await client.pairing_status(pairing.pairing_id)
|
|
139
|
+
|
|
140
|
+
if status.identity:
|
|
141
|
+
identity_token = status.identity.access_token
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use the identity token on future requests:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
response = await client.message(
|
|
148
|
+
"What is on my calendar today?",
|
|
149
|
+
identity_token=identity_token,
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Connect an app account
|
|
154
|
+
|
|
155
|
+
Some apps need a user service connection, such as Gmail, Google Calendar, or
|
|
156
|
+
Google Slides.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import asyncio
|
|
160
|
+
|
|
161
|
+
from marona import Marona
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def main() -> None:
|
|
165
|
+
client = Marona("https://edge.marona.ai")
|
|
166
|
+
identity_token = "mrn_identity_token_from_pairing"
|
|
167
|
+
connection = await client.connect_app(
|
|
168
|
+
"slides",
|
|
169
|
+
provider="google",
|
|
170
|
+
identity_token=identity_token,
|
|
171
|
+
return_url="https://example.com/connected",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
print(connection.authorization_url)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
asyncio.run(main())
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Open `authorization_url` in a browser so the user can authorize the provider.
|
|
181
|
+
|
|
182
|
+
## Attach files
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import asyncio
|
|
186
|
+
|
|
187
|
+
from marona import Marona, RuntimeAttachment
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def main() -> None:
|
|
191
|
+
identity_token = "mrn_identity_token_from_pairing"
|
|
192
|
+
attachment = RuntimeAttachment.from_bytes(
|
|
193
|
+
b"hello",
|
|
194
|
+
filename="note.txt",
|
|
195
|
+
mime_type="text/plain",
|
|
196
|
+
kind="document",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
client = Marona("https://edge.marona.ai")
|
|
200
|
+
response = await client.message(
|
|
201
|
+
"Summarize this file",
|
|
202
|
+
attachments=[attachment],
|
|
203
|
+
identity_token=identity_token,
|
|
204
|
+
)
|
|
205
|
+
print(response.reply)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
asyncio.run(main())
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Advanced
|
|
212
|
+
|
|
213
|
+
`EdgeRuntimeClient` is kept as a compatibility class for existing integrations.
|
|
214
|
+
New integrations should import `Marona`.
|
marona-0.1.0/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# marona
|
|
2
|
+
|
|
3
|
+
Official Python client SDK for Marona-compatible AI runtimes.
|
|
4
|
+
|
|
5
|
+
Marona provides a simple interface for messaging, discovering apps, managing
|
|
6
|
+
permissions, connecting service accounts, receiving inbound events, and using
|
|
7
|
+
tools from any compatible runtime.
|
|
8
|
+
|
|
9
|
+
You can use it with Marona Cloud or with your own self-hosted Marona-compatible
|
|
10
|
+
runtime.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install marona
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Send a message
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import asyncio
|
|
22
|
+
|
|
23
|
+
from marona import Marona
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def main() -> None:
|
|
27
|
+
client = Marona(
|
|
28
|
+
base_url="https://edge.marona.ai",
|
|
29
|
+
api_key="mrn_live_...",
|
|
30
|
+
)
|
|
31
|
+
response = await client.message(
|
|
32
|
+
"Create a PowerPoint about AI introduction",
|
|
33
|
+
interface="api",
|
|
34
|
+
conversation_id="demo",
|
|
35
|
+
)
|
|
36
|
+
print(response.reply)
|
|
37
|
+
for artifact in response.artifacts:
|
|
38
|
+
print(artifact.get("filename"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
asyncio.run(main())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Runtime targets
|
|
45
|
+
|
|
46
|
+
Marona Cloud:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from marona import Marona
|
|
50
|
+
|
|
51
|
+
client = Marona(
|
|
52
|
+
base_url="https://edge.marona.ai",
|
|
53
|
+
api_key="mrn_live_...",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Self-hosted:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from marona import Marona
|
|
61
|
+
|
|
62
|
+
client = Marona(
|
|
63
|
+
base_url="https://my-company-edge.example.com",
|
|
64
|
+
api_key="mrn_live_...",
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## List available apps
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
|
|
73
|
+
from marona import Marona
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def main() -> None:
|
|
77
|
+
client = Marona("https://edge.marona.ai")
|
|
78
|
+
apps = await client.list_apps(limit=20, search="slides")
|
|
79
|
+
|
|
80
|
+
for app in apps.apps:
|
|
81
|
+
print(app["slug"], app["name"])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Pair an interface with WhatsApp
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import asyncio
|
|
91
|
+
|
|
92
|
+
from marona import Marona
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main() -> None:
|
|
96
|
+
client = Marona("https://edge.marona.ai")
|
|
97
|
+
pairing = await client.start_pairing(interface="web", device_name="My web app")
|
|
98
|
+
|
|
99
|
+
print(pairing.display_code)
|
|
100
|
+
print(pairing.whatsapp_url)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
After the user sends the pairing code from WhatsApp, poll for completion:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
status = await client.pairing_status(pairing.pairing_id)
|
|
110
|
+
|
|
111
|
+
if status.identity:
|
|
112
|
+
identity_token = status.identity.access_token
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use the identity token on future requests:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
response = await client.message(
|
|
119
|
+
"What is on my calendar today?",
|
|
120
|
+
identity_token=identity_token,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Connect an app account
|
|
125
|
+
|
|
126
|
+
Some apps need a user service connection, such as Gmail, Google Calendar, or
|
|
127
|
+
Google Slides.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import asyncio
|
|
131
|
+
|
|
132
|
+
from marona import Marona
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def main() -> None:
|
|
136
|
+
client = Marona("https://edge.marona.ai")
|
|
137
|
+
identity_token = "mrn_identity_token_from_pairing"
|
|
138
|
+
connection = await client.connect_app(
|
|
139
|
+
"slides",
|
|
140
|
+
provider="google",
|
|
141
|
+
identity_token=identity_token,
|
|
142
|
+
return_url="https://example.com/connected",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
print(connection.authorization_url)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
asyncio.run(main())
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Open `authorization_url` in a browser so the user can authorize the provider.
|
|
152
|
+
|
|
153
|
+
## Attach files
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import asyncio
|
|
157
|
+
|
|
158
|
+
from marona import Marona, RuntimeAttachment
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def main() -> None:
|
|
162
|
+
identity_token = "mrn_identity_token_from_pairing"
|
|
163
|
+
attachment = RuntimeAttachment.from_bytes(
|
|
164
|
+
b"hello",
|
|
165
|
+
filename="note.txt",
|
|
166
|
+
mime_type="text/plain",
|
|
167
|
+
kind="document",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
client = Marona("https://edge.marona.ai")
|
|
171
|
+
response = await client.message(
|
|
172
|
+
"Summarize this file",
|
|
173
|
+
attachments=[attachment],
|
|
174
|
+
identity_token=identity_token,
|
|
175
|
+
)
|
|
176
|
+
print(response.reply)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
asyncio.run(main())
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Advanced
|
|
183
|
+
|
|
184
|
+
`EdgeRuntimeClient` is kept as a compatibility class for existing integrations.
|
|
185
|
+
New integrations should import `Marona`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .business import MaronaBusinessApiError, MaronaBusinessClient
|
|
2
|
+
from .client import EdgeRuntimeClient, EdgeRuntimeError, Marona
|
|
3
|
+
from .models import (
|
|
4
|
+
IdentityAuthenticated,
|
|
5
|
+
IdentityPairingStarted,
|
|
6
|
+
IdentityPairingStatus,
|
|
7
|
+
RealtimeSessionCreated,
|
|
8
|
+
RealtimeSessionRequest,
|
|
9
|
+
RuntimeApp,
|
|
10
|
+
RuntimeAppsPage,
|
|
11
|
+
RuntimeAttachment,
|
|
12
|
+
RuntimeInboundEvent,
|
|
13
|
+
RuntimeMessageRequest,
|
|
14
|
+
RuntimeMessageResponse,
|
|
15
|
+
ServiceConnectionAction,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"EdgeRuntimeClient",
|
|
22
|
+
"EdgeRuntimeError",
|
|
23
|
+
"Marona",
|
|
24
|
+
"__version__",
|
|
25
|
+
"MaronaBusinessApiError",
|
|
26
|
+
"MaronaBusinessClient",
|
|
27
|
+
"IdentityAuthenticated",
|
|
28
|
+
"IdentityPairingStarted",
|
|
29
|
+
"IdentityPairingStatus",
|
|
30
|
+
"RealtimeSessionCreated",
|
|
31
|
+
"RealtimeSessionRequest",
|
|
32
|
+
"RuntimeApp",
|
|
33
|
+
"RuntimeAppsPage",
|
|
34
|
+
"RuntimeAttachment",
|
|
35
|
+
"RuntimeInboundEvent",
|
|
36
|
+
"RuntimeMessageRequest",
|
|
37
|
+
"RuntimeMessageResponse",
|
|
38
|
+
"ServiceConnectionAction",
|
|
39
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MaronaBusinessApiError(RuntimeError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MaronaBusinessClient:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_url: str,
|
|
16
|
+
*,
|
|
17
|
+
token: str = "",
|
|
18
|
+
app_id: str = "",
|
|
19
|
+
timeout: float = 30,
|
|
20
|
+
client: httpx.AsyncClient | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.base_url = base_url.rstrip("/")
|
|
23
|
+
self.token = token
|
|
24
|
+
self.app_id = app_id
|
|
25
|
+
self.timeout = timeout
|
|
26
|
+
self._client = client
|
|
27
|
+
|
|
28
|
+
async def upload_media(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
media_type: str,
|
|
32
|
+
file_name: str,
|
|
33
|
+
mime_type: str,
|
|
34
|
+
data: bytes,
|
|
35
|
+
purpose: str = "generated_reply",
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
self._require_token()
|
|
38
|
+
files = {"file": (file_name, data, mime_type)}
|
|
39
|
+
form = {
|
|
40
|
+
"type": media_type,
|
|
41
|
+
"purpose": purpose,
|
|
42
|
+
**({"app_id": self.app_id} if self.app_id else {}),
|
|
43
|
+
}
|
|
44
|
+
response = await self._request(
|
|
45
|
+
"POST",
|
|
46
|
+
"/media",
|
|
47
|
+
timeout=60,
|
|
48
|
+
data=form,
|
|
49
|
+
files=files,
|
|
50
|
+
)
|
|
51
|
+
payload = response.json()
|
|
52
|
+
media = payload.get("media") if isinstance(payload, dict) else None
|
|
53
|
+
if not isinstance(media, dict) or not media.get("media_id"):
|
|
54
|
+
raise MaronaBusinessApiError("Marona Business API media upload did not return media_id.")
|
|
55
|
+
return self._normalize_media_response_url(media)
|
|
56
|
+
|
|
57
|
+
async def send_reply(
|
|
58
|
+
self,
|
|
59
|
+
conversation_id: str,
|
|
60
|
+
*,
|
|
61
|
+
message_type: str = "text",
|
|
62
|
+
body: str = "",
|
|
63
|
+
media_id: str | None = None,
|
|
64
|
+
caption: str | None = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
self._require_token()
|
|
67
|
+
clean_type = (message_type or "text").strip().lower()
|
|
68
|
+
if clean_type == "text":
|
|
69
|
+
payload: dict[str, Any] = {
|
|
70
|
+
"conversation_id": conversation_id,
|
|
71
|
+
"type": "text",
|
|
72
|
+
"text": {"body": body},
|
|
73
|
+
}
|
|
74
|
+
elif clean_type in {"image", "audio", "video", "document"}:
|
|
75
|
+
if not media_id:
|
|
76
|
+
raise MaronaBusinessApiError("media_id is required for media replies.")
|
|
77
|
+
payload = {
|
|
78
|
+
"conversation_id": conversation_id,
|
|
79
|
+
"type": clean_type,
|
|
80
|
+
clean_type: {
|
|
81
|
+
"media_id": media_id,
|
|
82
|
+
"caption": caption if caption is not None else body,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
else:
|
|
86
|
+
raise MaronaBusinessApiError(f"Unsupported reply type: {clean_type}")
|
|
87
|
+
if self.app_id:
|
|
88
|
+
payload["app_id"] = self.app_id
|
|
89
|
+
response = await self._request("POST", "/messages", json=payload, timeout=20)
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
async def sync_external_conversation_message(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
93
|
+
self._require_token()
|
|
94
|
+
if self.app_id:
|
|
95
|
+
payload = {**payload, "app_id": self.app_id}
|
|
96
|
+
response = await self._request(
|
|
97
|
+
"POST",
|
|
98
|
+
"/external/conversations/sync",
|
|
99
|
+
json=payload,
|
|
100
|
+
timeout=30,
|
|
101
|
+
)
|
|
102
|
+
return response.json()
|
|
103
|
+
|
|
104
|
+
async def get_memory_context(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
conversation_id: str | None = None,
|
|
108
|
+
external_conversation_id: str | None = None,
|
|
109
|
+
external_conversation_key: str | None = None,
|
|
110
|
+
limit: int = 12,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
self._require_token()
|
|
113
|
+
payload = {
|
|
114
|
+
"app_id": self.app_id or None,
|
|
115
|
+
"conversation_id": conversation_id,
|
|
116
|
+
"external_conversation_id": external_conversation_id,
|
|
117
|
+
"external_conversation_key": external_conversation_key,
|
|
118
|
+
"limit": max(4, min(int(limit or 12), 24)),
|
|
119
|
+
}
|
|
120
|
+
response = await self._request("POST", "/memory/context", json=payload, timeout=20)
|
|
121
|
+
return response.json()
|
|
122
|
+
|
|
123
|
+
async def update_external_message_status(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
124
|
+
self._require_token()
|
|
125
|
+
if self.app_id:
|
|
126
|
+
payload = {**payload, "app_id": self.app_id}
|
|
127
|
+
response = await self._request(
|
|
128
|
+
"PATCH",
|
|
129
|
+
"/external/messages/status",
|
|
130
|
+
json=payload,
|
|
131
|
+
timeout=20,
|
|
132
|
+
)
|
|
133
|
+
return response.json()
|
|
134
|
+
|
|
135
|
+
async def accept_call(self, call_id: str) -> dict[str, Any]:
|
|
136
|
+
self._require_token()
|
|
137
|
+
clean_call_id = (call_id or "").strip()
|
|
138
|
+
if not clean_call_id:
|
|
139
|
+
raise MaronaBusinessApiError("call_id is required.")
|
|
140
|
+
response = await self._request("POST", f"/calls/{clean_call_id}/accept", timeout=30)
|
|
141
|
+
return response.json()
|
|
142
|
+
|
|
143
|
+
async def health(self) -> dict[str, Any]:
|
|
144
|
+
response = await self._request("GET", "/health", auth=False, timeout=10)
|
|
145
|
+
return response.json()
|
|
146
|
+
|
|
147
|
+
async def _request(self, method: str, path: str, *, timeout: float | None = None, auth: bool = True, **kwargs: Any) -> httpx.Response:
|
|
148
|
+
headers = {**(self._headers() if auth else {}), **kwargs.pop("headers", {})}
|
|
149
|
+
client = self._client
|
|
150
|
+
close_after = False
|
|
151
|
+
if client is None:
|
|
152
|
+
client = httpx.AsyncClient(timeout=timeout or self.timeout)
|
|
153
|
+
close_after = True
|
|
154
|
+
try:
|
|
155
|
+
response = await client.request(
|
|
156
|
+
method,
|
|
157
|
+
f"{self.base_url}{path}",
|
|
158
|
+
headers=headers or None,
|
|
159
|
+
**kwargs,
|
|
160
|
+
)
|
|
161
|
+
if response.status_code >= 400:
|
|
162
|
+
raise MaronaBusinessApiError(
|
|
163
|
+
f"Marona Business API request failed with {response.status_code}: {response.text[:500]}"
|
|
164
|
+
)
|
|
165
|
+
return response
|
|
166
|
+
finally:
|
|
167
|
+
if close_after:
|
|
168
|
+
await client.aclose()
|
|
169
|
+
|
|
170
|
+
def _headers(self) -> dict[str, str]:
|
|
171
|
+
headers = {"Authorization": f"Bearer {self.token}"}
|
|
172
|
+
if self.app_id:
|
|
173
|
+
headers["X-Marona-App-Id"] = self.app_id
|
|
174
|
+
return headers
|
|
175
|
+
|
|
176
|
+
def _require_token(self) -> None:
|
|
177
|
+
if not self.token:
|
|
178
|
+
raise MaronaBusinessApiError("MARONA_BUSINESS_API_TOKEN is not configured.")
|
|
179
|
+
|
|
180
|
+
def _normalize_media_response_url(self, media: dict[str, Any]) -> dict[str, Any]:
|
|
181
|
+
url = str(media.get("url") or "").strip()
|
|
182
|
+
if url.startswith("http://") and self.base_url.startswith("https://"):
|
|
183
|
+
return {**media, "url": f"https://{url.removeprefix('http://')}"}
|
|
184
|
+
return media
|