admeasy 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.
- admeasy-0.1.0/.gitignore +19 -0
- admeasy-0.1.0/PKG-INFO +59 -0
- admeasy-0.1.0/README.md +40 -0
- admeasy-0.1.0/pyproject.toml +41 -0
- admeasy-0.1.0/src/admeasy/__init__.py +64 -0
- admeasy-0.1.0/src/admeasy/agent.py +184 -0
- admeasy-0.1.0/src/admeasy/cli.py +324 -0
- admeasy-0.1.0/src/admeasy/client.py +429 -0
- admeasy-0.1.0/src/admeasy/config.py +204 -0
- admeasy-0.1.0/src/admeasy/connections.py +96 -0
- admeasy-0.1.0/src/admeasy/context.py +468 -0
- admeasy-0.1.0/src/admeasy/employees.py +173 -0
- admeasy-0.1.0/src/admeasy/errors.py +228 -0
- admeasy-0.1.0/tests/conftest.py +63 -0
- admeasy-0.1.0/tests/integration/__init__.py +0 -0
- admeasy-0.1.0/tests/integration/conftest.py +124 -0
- admeasy-0.1.0/tests/integration/test_live_api.py +96 -0
- admeasy-0.1.0/tests/test_client.py +376 -0
- admeasy-0.1.0/tests/test_context.py +156 -0
admeasy-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
|
|
10
|
+
# Node
|
|
11
|
+
node_modules/
|
|
12
|
+
typescript/dist/
|
|
13
|
+
typescript/build/
|
|
14
|
+
*.tgz
|
|
15
|
+
|
|
16
|
+
# Never commit credentials
|
|
17
|
+
.admeasy/credentials
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
admeasy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: admeasy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK and CLI for Admeasy AI, the AI employee infrastructure platform.
|
|
5
|
+
Author-email: Admeasy <aadesh.panwar@admeasy.in>
|
|
6
|
+
License: TBD
|
|
7
|
+
Keywords: admeasy,agents,ai-employees,cli,sdk
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: httpx<1,>=0.25
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.4
|
|
14
|
+
Requires-Dist: typer>=0.9
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
17
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# admeasy (Python)
|
|
21
|
+
|
|
22
|
+
Python SDK and CLI for **Admeasy AI**, the AI employee infrastructure platform.
|
|
23
|
+
|
|
24
|
+
The SDK is a client of the Admeasy control plane. Employees you create here
|
|
25
|
+
are the same records the Admeasy website shows. Nothing is stored locally
|
|
26
|
+
except your API key in `~/.admeasy/credentials`, written by `admeasy login`.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install admeasy
|
|
30
|
+
export ADMEASY_API_KEY="..." # the key from the Admeasy website Integrations page
|
|
31
|
+
admeasy whoami
|
|
32
|
+
admeasy employee list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from admeasy import Agent, Client, EmployeeLimitReachedError
|
|
37
|
+
|
|
38
|
+
client = Client() # reads ADMEASY_API_KEY / ADMEASY_API_URL
|
|
39
|
+
print(client.get_workspace(), client.get_limits())
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
agent = Agent.create("sales-agent", model="sarvam", description="Follow up on new leads")
|
|
43
|
+
except EmployeeLimitReachedError as exc:
|
|
44
|
+
print(f"Plan is full: {exc.current}/{exc.limit}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Sarvam is the preferred currently available model, and `sarvam` is the default.
|
|
48
|
+
|
|
49
|
+
Release 0.1 is synchronous (`httpx.Client`). See the repository README and
|
|
50
|
+
`docs/API_CONTRACT.md` for the full contract, and `docs/BACKEND_GAPS.md` for
|
|
51
|
+
the backend changes the SDK still depends on.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
57
|
+
pip install -e '.[dev]'
|
|
58
|
+
pytest
|
|
59
|
+
```
|
admeasy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# admeasy (Python)
|
|
2
|
+
|
|
3
|
+
Python SDK and CLI for **Admeasy AI**, the AI employee infrastructure platform.
|
|
4
|
+
|
|
5
|
+
The SDK is a client of the Admeasy control plane. Employees you create here
|
|
6
|
+
are the same records the Admeasy website shows. Nothing is stored locally
|
|
7
|
+
except your API key in `~/.admeasy/credentials`, written by `admeasy login`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install admeasy
|
|
11
|
+
export ADMEASY_API_KEY="..." # the key from the Admeasy website Integrations page
|
|
12
|
+
admeasy whoami
|
|
13
|
+
admeasy employee list
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from admeasy import Agent, Client, EmployeeLimitReachedError
|
|
18
|
+
|
|
19
|
+
client = Client() # reads ADMEASY_API_KEY / ADMEASY_API_URL
|
|
20
|
+
print(client.get_workspace(), client.get_limits())
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
agent = Agent.create("sales-agent", model="sarvam", description="Follow up on new leads")
|
|
24
|
+
except EmployeeLimitReachedError as exc:
|
|
25
|
+
print(f"Plan is full: {exc.current}/{exc.limit}")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Sarvam is the preferred currently available model, and `sarvam` is the default.
|
|
29
|
+
|
|
30
|
+
Release 0.1 is synchronous (`httpx.Client`). See the repository README and
|
|
31
|
+
`docs/API_CONTRACT.md` for the full contract, and `docs/BACKEND_GAPS.md` for
|
|
32
|
+
the backend changes the SDK still depends on.
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
38
|
+
pip install -e '.[dev]'
|
|
39
|
+
pytest
|
|
40
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "admeasy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK and CLI for Admeasy AI, the AI employee infrastructure platform."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "TBD" }
|
|
12
|
+
authors = [{ name = "Admeasy", email = "aadesh.panwar@admeasy.in" }]
|
|
13
|
+
keywords = ["admeasy", "ai-employees", "agents", "sdk", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.25,<1",
|
|
21
|
+
"pydantic>=2.4,<3",
|
|
22
|
+
"typer>=0.9",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=7.4",
|
|
28
|
+
"respx>=0.21",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
admeasy = "admeasy.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/admeasy"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
markers = [
|
|
40
|
+
"integration: opt-in tests against a real Admeasy test backend; skipped unless ADMEASY_TEST_API_URL and ADMEASY_TEST_API_KEY are set",
|
|
41
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Admeasy AI — Python SDK for the AI employee infrastructure platform.
|
|
2
|
+
|
|
3
|
+
One Admeasy account, one source of truth (the Admeasy control plane), many
|
|
4
|
+
interfaces. This package is a client of the same employee records the
|
|
5
|
+
Admeasy website uses; it never keeps a second database.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .agent import Agent # noqa: E402
|
|
11
|
+
from .client import ROUTES, Client, Limits, Model, Route, User, Workspace # noqa: E402
|
|
12
|
+
from .config import DEFAULT_API_URL, DEFAULT_MODEL # noqa: E402
|
|
13
|
+
from .connections import Connection # noqa: E402
|
|
14
|
+
from .employees import Employee # noqa: E402
|
|
15
|
+
from .errors import ( # noqa: E402
|
|
16
|
+
AdmeasyAuthenticationError,
|
|
17
|
+
AdmeasyError,
|
|
18
|
+
APIConnectionError,
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
ConfigurationError,
|
|
21
|
+
ConflictError,
|
|
22
|
+
ConnectionNotAvailableError,
|
|
23
|
+
EmployeeLimitReachedError,
|
|
24
|
+
MissingAPIKeyError,
|
|
25
|
+
NotFoundError,
|
|
26
|
+
PaymentRequiredError,
|
|
27
|
+
PermissionDeniedError,
|
|
28
|
+
PlanLimitError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
ServerError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"Agent",
|
|
37
|
+
"Client",
|
|
38
|
+
"Connection",
|
|
39
|
+
"Employee",
|
|
40
|
+
"Limits",
|
|
41
|
+
"Model",
|
|
42
|
+
"Route",
|
|
43
|
+
"ROUTES",
|
|
44
|
+
"User",
|
|
45
|
+
"Workspace",
|
|
46
|
+
"DEFAULT_API_URL",
|
|
47
|
+
"DEFAULT_MODEL",
|
|
48
|
+
"AdmeasyError",
|
|
49
|
+
"AdmeasyAuthenticationError",
|
|
50
|
+
"AuthenticationError",
|
|
51
|
+
"MissingAPIKeyError",
|
|
52
|
+
"ConfigurationError",
|
|
53
|
+
"PermissionDeniedError",
|
|
54
|
+
"NotFoundError",
|
|
55
|
+
"ValidationError",
|
|
56
|
+
"ConflictError",
|
|
57
|
+
"PaymentRequiredError",
|
|
58
|
+
"PlanLimitError",
|
|
59
|
+
"EmployeeLimitReachedError",
|
|
60
|
+
"RateLimitError",
|
|
61
|
+
"ServerError",
|
|
62
|
+
"APIConnectionError",
|
|
63
|
+
"ConnectionNotAvailableError",
|
|
64
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""``Agent`` — a handle to one persistent Admeasy AI employee.
|
|
2
|
+
|
|
3
|
+
An ``Agent`` is never a local-only object. ``Agent.create`` asks the control
|
|
4
|
+
plane to create the employee (the website sees it immediately), ``Agent.get``
|
|
5
|
+
loads an existing one, and ``update``/``delete``/``connect`` all call the
|
|
6
|
+
server. Quota is enforced server-side; the SDK only surfaces
|
|
7
|
+
``EmployeeLimitReachedError``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .client import Client
|
|
15
|
+
from .config import DEFAULT_MODEL
|
|
16
|
+
from .connections import Connection
|
|
17
|
+
from .employees import Employee
|
|
18
|
+
from .errors import ConfigurationError, ConnectionNotAvailableError, NotFoundError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Agent:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
name: Optional[str] = None,
|
|
25
|
+
*,
|
|
26
|
+
model: str = DEFAULT_MODEL,
|
|
27
|
+
description: Optional[str] = None,
|
|
28
|
+
client: Optional[Client] = None,
|
|
29
|
+
employee: Optional[Employee] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Build a handle. ``Agent(name=...)`` alone does NOT create anything;
|
|
32
|
+
call ``Agent.create(...)``, ``Agent.get(id)`` or ``agent.resolve()``."""
|
|
33
|
+
self._client = client or Client()
|
|
34
|
+
self._employee = employee
|
|
35
|
+
self._name = name if employee is None else employee.name
|
|
36
|
+
self._model = model if employee is None else (employee.model or model)
|
|
37
|
+
self._description = description if employee is None else employee.description
|
|
38
|
+
|
|
39
|
+
# -- constructors --------------------------------------------------------
|
|
40
|
+
@classmethod
|
|
41
|
+
def create(
|
|
42
|
+
cls,
|
|
43
|
+
name: str,
|
|
44
|
+
*,
|
|
45
|
+
model: str = DEFAULT_MODEL,
|
|
46
|
+
description: Optional[str] = None,
|
|
47
|
+
instructions: Optional[str] = None,
|
|
48
|
+
connections: Optional[List[str]] = None,
|
|
49
|
+
department: Optional[str] = None,
|
|
50
|
+
schedule: Optional[str] = None,
|
|
51
|
+
client: Optional[Client] = None,
|
|
52
|
+
workspace_id: Optional[str] = None,
|
|
53
|
+
) -> "Agent":
|
|
54
|
+
client = client or Client()
|
|
55
|
+
employee = client.create_employee(
|
|
56
|
+
name=name,
|
|
57
|
+
model=model,
|
|
58
|
+
description=description,
|
|
59
|
+
instructions=instructions,
|
|
60
|
+
connections=connections,
|
|
61
|
+
department=department,
|
|
62
|
+
schedule=schedule,
|
|
63
|
+
workspace_id=workspace_id,
|
|
64
|
+
)
|
|
65
|
+
return cls(client=client, employee=employee, model=model)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get(cls, employee_id: str, *, client: Optional[Client] = None, workspace_id: Optional[str] = None) -> "Agent":
|
|
69
|
+
client = client or Client()
|
|
70
|
+
return cls(client=client, employee=client.get_employee(employee_id, workspace_id=workspace_id))
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def list(cls, *, client: Optional[Client] = None, workspace_id: Optional[str] = None) -> List["Agent"]:
|
|
74
|
+
client = client or Client()
|
|
75
|
+
return [cls(client=client, employee=e) for e in client.list_employees(workspace_id=workspace_id)]
|
|
76
|
+
|
|
77
|
+
def resolve(self) -> "Agent":
|
|
78
|
+
"""Bind this handle to the existing employee with the same name.
|
|
79
|
+
Never creates a duplicate; raises NotFoundError if none exists."""
|
|
80
|
+
if self._employee is not None:
|
|
81
|
+
return self
|
|
82
|
+
if not self._name:
|
|
83
|
+
raise ConfigurationError("Agent has no name or id to resolve")
|
|
84
|
+
found = self._client.employees.find(self._name)
|
|
85
|
+
if not found:
|
|
86
|
+
raise NotFoundError(
|
|
87
|
+
f"No Admeasy employee named {self._name!r} in this workspace. "
|
|
88
|
+
f"Use Agent.create({self._name!r}, model={self._model!r}) to create it.",
|
|
89
|
+
code="EMPLOYEE_NOT_FOUND",
|
|
90
|
+
)
|
|
91
|
+
self._employee = found
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
# -- properties ----------------------------------------------------------
|
|
95
|
+
def _require(self) -> Employee:
|
|
96
|
+
if self._employee is None:
|
|
97
|
+
raise ConfigurationError(
|
|
98
|
+
"This Agent is not bound to an Admeasy employee yet. "
|
|
99
|
+
"Use Agent.create(...), Agent.get(id) or agent.resolve()."
|
|
100
|
+
)
|
|
101
|
+
return self._employee
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def employee(self) -> Optional[Employee]:
|
|
105
|
+
return self._employee
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def id(self) -> Optional[str]:
|
|
109
|
+
return self._employee.id if self._employee else None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def name(self) -> Optional[str]:
|
|
113
|
+
return self._employee.name if self._employee else self._name
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def status(self) -> Optional[str]:
|
|
117
|
+
return self._employee.status if self._employee else None
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def model(self) -> Optional[str]:
|
|
121
|
+
return (self._employee.model if self._employee else None) or self._model
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def workspace(self) -> Optional[str]:
|
|
125
|
+
if self._employee and self._employee.workspace_id:
|
|
126
|
+
return self._employee.workspace_id
|
|
127
|
+
return self._client.workspace_id
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def connections(self) -> List[str]:
|
|
131
|
+
return list(self._employee.connections) if self._employee else []
|
|
132
|
+
|
|
133
|
+
def __repr__(self) -> str:
|
|
134
|
+
return f"Agent(id={self.id!r}, name={self.name!r}, status={self.status!r}, model={self.model!r})"
|
|
135
|
+
|
|
136
|
+
# -- server operations ---------------------------------------------------
|
|
137
|
+
def refresh(self) -> "Agent":
|
|
138
|
+
emp = self._require()
|
|
139
|
+
self._employee = self._client.get_employee(emp.id, workspace_id=emp.workspace_id)
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def update(self, **fields: Any) -> "Agent":
|
|
143
|
+
emp = self._require()
|
|
144
|
+
self._employee = self._client.update_employee(emp.id, workspace_id=emp.workspace_id, **fields)
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def delete(self) -> Dict[str, Any]:
|
|
148
|
+
emp = self._require()
|
|
149
|
+
result = self._client.delete_employee(emp.id, workspace_id=emp.workspace_id)
|
|
150
|
+
self._employee = None
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
def connect(self, name: str) -> Connection:
|
|
154
|
+
"""Attach a workspace connection to this employee.
|
|
155
|
+
|
|
156
|
+
Generic: the SDK asks the control plane which connections exist for
|
|
157
|
+
the workspace. If ``name`` is not connected there, it raises
|
|
158
|
+
``ConnectionNotAvailableError`` with instructions instead of guessing.
|
|
159
|
+
"""
|
|
160
|
+
emp = self._require()
|
|
161
|
+
connections = self._client.list_connections(workspace_id=emp.workspace_id)
|
|
162
|
+
match = next((c for c in connections if c.matches(name)), None)
|
|
163
|
+
if match is None or not match.connected:
|
|
164
|
+
connected = sorted(c.id for c in connections if c.connected)
|
|
165
|
+
state = "is not connected" if match else "is not available"
|
|
166
|
+
raise ConnectionNotAvailableError(
|
|
167
|
+
f"Connection {name!r} {state} in workspace {emp.workspace_id}. "
|
|
168
|
+
"Authorize it in the Admeasy website (Integrations page) with an account that "
|
|
169
|
+
"can grant access, then retry. Currently connected: "
|
|
170
|
+
+ (", ".join(connected) if connected else "none")
|
|
171
|
+
+ ".",
|
|
172
|
+
code="CONNECTION_NOT_AVAILABLE",
|
|
173
|
+
)
|
|
174
|
+
if match.id not in emp.connections:
|
|
175
|
+
self._employee = self._client.update_employee(
|
|
176
|
+
emp.id, workspace_id=emp.workspace_id, connections=[*emp.connections, match.id]
|
|
177
|
+
)
|
|
178
|
+
return match
|
|
179
|
+
|
|
180
|
+
async def run(self, task: str) -> Any: # pragma: no cover - Release 0.2
|
|
181
|
+
raise NotImplementedError(
|
|
182
|
+
"agent.run() arrives with the Admeasy runtime (Release 0.2). "
|
|
183
|
+
"Release 0.1 covers auth, employee CRUD, quota and sync."
|
|
184
|
+
)
|