amesa-api 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.
- amesa_api-0.1.0/PKG-INFO +45 -0
- amesa_api-0.1.0/README.md +27 -0
- amesa_api-0.1.0/amesa_api/__init__.py +10 -0
- amesa_api-0.1.0/amesa_api/common.py +116 -0
- amesa_api-0.1.0/amesa_api/controller/__init__.py +5 -0
- amesa_api-0.1.0/amesa_api/controller/job_status_enum.py +14 -0
- amesa_api-0.1.0/amesa_api/controller/main.py +60 -0
- amesa_api-0.1.0/amesa_api/controller/service.py +87 -0
- amesa_api-0.1.0/amesa_api/nocode/__init__.py +5 -0
- amesa_api-0.1.0/amesa_api/nocode/main.py +583 -0
- amesa_api-0.1.0/amesa_api/util/__init__.py +3 -0
- amesa_api-0.1.0/amesa_api/util/logger.py +64 -0
- amesa_api-0.1.0/amesa_api/util/singleton.py +55 -0
- amesa_api-0.1.0/amesa_api/util/token.py +32 -0
- amesa_api-0.1.0/amesa_api.egg-info/PKG-INFO +45 -0
- amesa_api-0.1.0/amesa_api.egg-info/SOURCES.txt +32 -0
- amesa_api-0.1.0/amesa_api.egg-info/dependency_links.txt +1 -0
- amesa_api-0.1.0/amesa_api.egg-info/requires.txt +7 -0
- amesa_api-0.1.0/amesa_api.egg-info/top_level.txt +1 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/__init__.c +4915 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/common.c +18995 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/controller/__init__.c +4889 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/controller/job_status_enum.c +5272 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/controller/main.c +14696 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/controller/service.c +14464 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/nocode/__init__.c +4889 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/nocode/main.c +36866 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/util/__init__.c +4591 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/util/logger.c +9504 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/util/singleton.c +9444 -0
- amesa_api-0.1.0/dist/build_cython/amesa_api/util/token.c +7840 -0
- amesa_api-0.1.0/pyproject.toml +112 -0
- amesa_api-0.1.0/setup.cfg +4 -0
- amesa_api-0.1.0/setup.py +168 -0
amesa_api-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amesa-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: the Amesa API
|
|
5
|
+
Author-email: Hunter Park <hunter@amesa.com>
|
|
6
|
+
Requires-Python: >=3.10, <3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: cython
|
|
9
|
+
Requires-Dist: ruff
|
|
10
|
+
Requires-Dist: simple-term-menu
|
|
11
|
+
Requires-Dist: aiohttp
|
|
12
|
+
Requires-Dist: httpretty==1.0.5
|
|
13
|
+
Requires-Dist: pytest-asyncio
|
|
14
|
+
Requires-Dist: pre-commit
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
|
|
19
|
+
# Amesa
|
|
20
|
+
|
|
21
|
+
Amesa helps you build Autonomous Agents! Through an easy SDK you get access to outscaled simulator training tools.
|
|
22
|
+
|
|
23
|
+
## Licenses / Seats
|
|
24
|
+
|
|
25
|
+
Amesa is an enterprise platform and requires an API key to use.
|
|
26
|
+
|
|
27
|
+
We offer 3 types of licenses:
|
|
28
|
+
|
|
29
|
+
- **Enterprise:** for enterprise projects and agent creations
|
|
30
|
+
- **Personal:** for personal usage and testing (typically offered to System Integrators)
|
|
31
|
+
- **Trial:** validate the Amesa platform (requires sales contact person)
|
|
32
|
+
|
|
33
|
+
One key is needed per user (seat-based licensing)
|
|
34
|
+
|
|
35
|
+
You can request an API Key through the following methods:
|
|
36
|
+
|
|
37
|
+
- [Discord](https://discord.gg/EQ3BgJt9NC)
|
|
38
|
+
- [Mail](mailto:sales@amesa.com?subject=REQUEST%20API%20KEY%20-%20COMPANY_NAME%20-%20NAME&body=Hi%2C%0D%0A%0D%0AI%20would%20like%20to%20request%20an%20API%20key%20for%20my%20company%20to%20get%20started%20with%20Amesa.%0D%0A%0D%0A*%20Company%20Name%3A%20COMPANY_NAME%0D%0A*%20Seats%3A%20NO_OF_SEATS%0D%0A*%20License%20Type%3A%20Enterprise%20%7C%20Personal%20%7C%20Trial%20%28keep%20what%20is%20required%29%0D%0A%0D%0AKind%20Regards%2C%0D%0ANAME%0D%0AFUNCTION)
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
1. Download the Amesa SDK: `pip install amesa`
|
|
43
|
+
2. Request an API Key
|
|
44
|
+
3. Get a simulator or use one of the [prebuilt ones](https://hub.docker.com/u/composabl)
|
|
45
|
+
4. Create an Agent or see our [examples](https://github.com/Amesa/examples.amesa.com)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Amesa
|
|
2
|
+
|
|
3
|
+
Amesa helps you build Autonomous Agents! Through an easy SDK you get access to outscaled simulator training tools.
|
|
4
|
+
|
|
5
|
+
## Licenses / Seats
|
|
6
|
+
|
|
7
|
+
Amesa is an enterprise platform and requires an API key to use.
|
|
8
|
+
|
|
9
|
+
We offer 3 types of licenses:
|
|
10
|
+
|
|
11
|
+
- **Enterprise:** for enterprise projects and agent creations
|
|
12
|
+
- **Personal:** for personal usage and testing (typically offered to System Integrators)
|
|
13
|
+
- **Trial:** validate the Amesa platform (requires sales contact person)
|
|
14
|
+
|
|
15
|
+
One key is needed per user (seat-based licensing)
|
|
16
|
+
|
|
17
|
+
You can request an API Key through the following methods:
|
|
18
|
+
|
|
19
|
+
- [Discord](https://discord.gg/EQ3BgJt9NC)
|
|
20
|
+
- [Mail](mailto:sales@amesa.com?subject=REQUEST%20API%20KEY%20-%20COMPANY_NAME%20-%20NAME&body=Hi%2C%0D%0A%0D%0AI%20would%20like%20to%20request%20an%20API%20key%20for%20my%20company%20to%20get%20started%20with%20Amesa.%0D%0A%0D%0A*%20Company%20Name%3A%20COMPANY_NAME%0D%0A*%20Seats%3A%20NO_OF_SEATS%0D%0A*%20License%20Type%3A%20Enterprise%20%7C%20Personal%20%7C%20Trial%20%28keep%20what%20is%20required%29%0D%0A%0D%0AKind%20Regards%2C%0D%0ANAME%0D%0AFUNCTION)
|
|
21
|
+
|
|
22
|
+
## Getting Started
|
|
23
|
+
|
|
24
|
+
1. Download the Amesa SDK: `pip install amesa`
|
|
25
|
+
2. Request an API Key
|
|
26
|
+
3. Get a simulator or use one of the [prebuilt ones](https://hub.docker.com/u/composabl)
|
|
27
|
+
4. Create an Agent or see our [examples](https://github.com/Amesa/examples.amesa.com)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (C) Amesa, Inc - All Rights Reserved
|
|
2
|
+
# Unauthorized copying of this file, via any medium is strictly prohibited
|
|
3
|
+
# Proprietary and confidential
|
|
4
|
+
|
|
5
|
+
# We want to expose these classes to the user at the base of core
|
|
6
|
+
# ex: `from amesa.core import Agent`
|
|
7
|
+
|
|
8
|
+
# Continue with the imports
|
|
9
|
+
from .nocode import APINoCode
|
|
10
|
+
from .common import get_ssl_connector
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Copyright (C) Amesa, Inc - All Rights Reserved
|
|
2
|
+
# Unauthorized copying of this file, via any medium is strictly prohibited
|
|
3
|
+
# Proprietary and confidential
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import ssl
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_ssl_connector():
|
|
13
|
+
ssl_cert_file = os.getenv("SSL_CERT_FILE")
|
|
14
|
+
ssl_cert_path = os.getenv("SSL_CERT_PATH")
|
|
15
|
+
ssl_ctx = ssl.create_default_context(
|
|
16
|
+
ssl.Purpose.SERVER_AUTH, cafile=ssl_cert_file, capath=ssl_cert_path
|
|
17
|
+
)
|
|
18
|
+
return aiohttp.TCPConnector(ssl_context=ssl_ctx)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class APIBase:
|
|
22
|
+
def __init__(self, base_url):
|
|
23
|
+
self.base_url = base_url
|
|
24
|
+
self.token = None
|
|
25
|
+
|
|
26
|
+
def _get_headers(self):
|
|
27
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
28
|
+
|
|
29
|
+
if self.token is not None:
|
|
30
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
31
|
+
|
|
32
|
+
return headers
|
|
33
|
+
|
|
34
|
+
async def get(self, path: str, headers={}):
|
|
35
|
+
"""
|
|
36
|
+
Make a GET request to the API
|
|
37
|
+
"""
|
|
38
|
+
headers_base = self._get_headers()
|
|
39
|
+
headers_base.update(headers)
|
|
40
|
+
connector = get_ssl_connector()
|
|
41
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
|
42
|
+
async with session.get(
|
|
43
|
+
self.base_url + path, headers=headers_base
|
|
44
|
+
) as response:
|
|
45
|
+
return await self._process_response(response)
|
|
46
|
+
|
|
47
|
+
async def post(self, path: str, data: Union[dict, str], headers={}):
|
|
48
|
+
"""
|
|
49
|
+
Make a POST request to the API
|
|
50
|
+
"""
|
|
51
|
+
headers_base = self._get_headers()
|
|
52
|
+
headers_base.update(headers)
|
|
53
|
+
|
|
54
|
+
if isinstance(data, dict):
|
|
55
|
+
data = json.dumps(data)
|
|
56
|
+
else:
|
|
57
|
+
del headers_base["Content-Type"]
|
|
58
|
+
|
|
59
|
+
connector = get_ssl_connector()
|
|
60
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
|
61
|
+
async with session.post(
|
|
62
|
+
self.base_url + path, headers=headers_base, data=data
|
|
63
|
+
) as response:
|
|
64
|
+
return await self._process_response(response)
|
|
65
|
+
|
|
66
|
+
async def put(self, path: str, data: Union[dict, str], headers={}):
|
|
67
|
+
"""
|
|
68
|
+
Make a PUT request to the API
|
|
69
|
+
"""
|
|
70
|
+
headers_base = self._get_headers()
|
|
71
|
+
headers_base.update(headers)
|
|
72
|
+
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
data = json.dumps(data)
|
|
75
|
+
else:
|
|
76
|
+
del headers_base["Content-Type"]
|
|
77
|
+
|
|
78
|
+
connector = get_ssl_connector()
|
|
79
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
|
80
|
+
async with session.put(
|
|
81
|
+
self.base_url + path, headers=headers_base, data=data
|
|
82
|
+
) as response:
|
|
83
|
+
return await self._process_response(response)
|
|
84
|
+
|
|
85
|
+
async def delete(self, path: str, headers={}):
|
|
86
|
+
"""
|
|
87
|
+
Make a DELETE request to the API
|
|
88
|
+
"""
|
|
89
|
+
headers_base = self._get_headers()
|
|
90
|
+
headers_base.update(headers)
|
|
91
|
+
|
|
92
|
+
connector = get_ssl_connector()
|
|
93
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
|
94
|
+
async with session.delete(
|
|
95
|
+
self.base_url + path, headers=headers_base
|
|
96
|
+
) as response:
|
|
97
|
+
return await self._process_response(response)
|
|
98
|
+
|
|
99
|
+
async def _process_response(self, response):
|
|
100
|
+
"""
|
|
101
|
+
Process the response from the API
|
|
102
|
+
"""
|
|
103
|
+
if response.status >= 200 and response.status < 202:
|
|
104
|
+
try:
|
|
105
|
+
return await response.json()
|
|
106
|
+
except Exception:
|
|
107
|
+
return await response.text()
|
|
108
|
+
elif response.status >= 202 and response.status < 300:
|
|
109
|
+
return None
|
|
110
|
+
else:
|
|
111
|
+
try:
|
|
112
|
+
res_json = await response.json()
|
|
113
|
+
raise Exception(f"Error: {response.status} - {res_json['message']}")
|
|
114
|
+
except Exception:
|
|
115
|
+
txt = await response.text()
|
|
116
|
+
raise Exception(f"Error: {response.status} - {txt}")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (C) Amesa, Inc - All Rights Reserved
|
|
2
|
+
# Unauthorized copying of this file, via any medium is strictly prohibited
|
|
3
|
+
# Proprietary and confidential
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JobStatusEnum(str, Enum):
|
|
9
|
+
STARTING = "starting"
|
|
10
|
+
STARTED = "started"
|
|
11
|
+
STOPPING = "stopping"
|
|
12
|
+
STOPPED = "stopped"
|
|
13
|
+
FAILED = "failed"
|
|
14
|
+
DONE = "done"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright (C) Amesa, Inc - All Rights Reserved
|
|
2
|
+
# Unauthorized copying of this file, via any medium is strictly prohibited
|
|
3
|
+
# Proprietary and confidential
|
|
4
|
+
|
|
5
|
+
from amesa_api.common import APIBase
|
|
6
|
+
from amesa_api.controller.job_status_enum import JobStatusEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class APIController(APIBase):
|
|
10
|
+
def __init__(self, base_url):
|
|
11
|
+
super().__init__(base_url)
|
|
12
|
+
|
|
13
|
+
self.admin = APIControllerAdmin(self)
|
|
14
|
+
self.sdk = APIControllerSdk(self)
|
|
15
|
+
self.direct_mode = APIControllerDirectMode(self)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APIControllerDirectMode:
|
|
19
|
+
def __init__(self, api):
|
|
20
|
+
self.api: APIController = api
|
|
21
|
+
|
|
22
|
+
async def submit(self, job_id: str, serialized_trainer: str):
|
|
23
|
+
body = {"id": job_id, "json": serialized_trainer}
|
|
24
|
+
|
|
25
|
+
await self.api.post("/jobs", body)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class APIControllerAdmin:
|
|
29
|
+
def __init__(self, api):
|
|
30
|
+
self.api: APIController = api
|
|
31
|
+
|
|
32
|
+
async def list_jobs(self):
|
|
33
|
+
return await self.api.get("/api/admin/v1/jobs")
|
|
34
|
+
|
|
35
|
+
async def get_job(self, job_id: str):
|
|
36
|
+
return await self.api.get(f"/api/admin/v1/jobs/{job_id}")
|
|
37
|
+
|
|
38
|
+
async def cancel_job(self, job_id: str):
|
|
39
|
+
return await self.api.delete(f"/api/admin/v1/jobs/{job_id}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class APIControllerSdk:
|
|
43
|
+
def __init__(self, api):
|
|
44
|
+
self.api: APIController = api
|
|
45
|
+
|
|
46
|
+
async def heartbeat(self, job_id: str):
|
|
47
|
+
body = {
|
|
48
|
+
"health": "ok",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
res = await self.api.post(f"/api/sdk/v1/jobs/{job_id}/heartbeat", body)
|
|
52
|
+
|
|
53
|
+
return res
|
|
54
|
+
|
|
55
|
+
async def status(self, status: JobStatusEnum, job_id: str):
|
|
56
|
+
status = {"status": status}
|
|
57
|
+
|
|
58
|
+
res = await self.api.post(f"/api/sdk/v1/jobs/{job_id}/status", status)
|
|
59
|
+
|
|
60
|
+
return res
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright (C) Amesa, Inc - All Rights Reserved
|
|
2
|
+
# Unauthorized copying of this file, via any medium is strictly prohibited
|
|
3
|
+
# Proprietary and confidential
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from os import getenv
|
|
9
|
+
|
|
10
|
+
import amesa_api.util.logger as logger_util
|
|
11
|
+
from amesa_api.controller.job_status_enum import JobStatusEnum
|
|
12
|
+
from amesa_api.controller.main import APIController
|
|
13
|
+
|
|
14
|
+
logger = logger_util.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ControllerService:
|
|
18
|
+
_instance = None
|
|
19
|
+
heartbeat_thread = None
|
|
20
|
+
healthy = True
|
|
21
|
+
job_id: str = None
|
|
22
|
+
|
|
23
|
+
def __new__(cls, *args, **kwargs):
|
|
24
|
+
if cls._instance is None:
|
|
25
|
+
cls._instance = super().__new__(cls)
|
|
26
|
+
|
|
27
|
+
return cls._instance
|
|
28
|
+
|
|
29
|
+
def __init__(self, url=None):
|
|
30
|
+
if url is None:
|
|
31
|
+
controller_url = getenv("AMESA_CONTROLLER_URL", None)
|
|
32
|
+
else:
|
|
33
|
+
controller_url = url
|
|
34
|
+
|
|
35
|
+
if controller_url is None:
|
|
36
|
+
raise Exception("AMESA_CONTROLLER_URL could not be read!")
|
|
37
|
+
|
|
38
|
+
self.client = APIController(controller_url)
|
|
39
|
+
|
|
40
|
+
def set_job_id(self, job_id):
|
|
41
|
+
self.job_id = job_id
|
|
42
|
+
|
|
43
|
+
async def start_heartbeat_thread(self):
|
|
44
|
+
try:
|
|
45
|
+
await self.client.sdk.heartbeat(self.job_id)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Failed to connect to controller: {e}")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
self.heartbeat_thread = run_async_in_daemon_thread(self.send_heartbeat)
|
|
51
|
+
|
|
52
|
+
def mark_unhealthy(self):
|
|
53
|
+
self.healthy = False
|
|
54
|
+
|
|
55
|
+
async def send_heartbeat(self):
|
|
56
|
+
while self.healthy:
|
|
57
|
+
try:
|
|
58
|
+
await self.client.sdk.heartbeat(self.job_id)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Failed to send heartbeat to controller: {e}")
|
|
61
|
+
|
|
62
|
+
time.sleep(10)
|
|
63
|
+
|
|
64
|
+
logger.info(
|
|
65
|
+
"ControllerService heartbeat task stopped because the job is marked as unhealthy"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def send_status(self, status: JobStatusEnum):
|
|
69
|
+
await self.client.sdk.status(status, self.job_id)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_async_in_daemon_thread(fn, *args, **kwargs):
|
|
73
|
+
"""
|
|
74
|
+
Runs an asynchronous function in a separate daemon thread, and waits for it to complete.
|
|
75
|
+
Returns the result of the asynchronous function.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def run(loop):
|
|
79
|
+
asyncio.set_event_loop(loop)
|
|
80
|
+
|
|
81
|
+
loop.run_until_complete(fn(*args, **kwargs))
|
|
82
|
+
|
|
83
|
+
loop = asyncio.new_event_loop()
|
|
84
|
+
t = threading.Thread(target=run, args=(loop,), daemon=True)
|
|
85
|
+
t.start()
|
|
86
|
+
|
|
87
|
+
return t
|