ezop 0.0.2__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.
- ezop-0.0.2/PKG-INFO +106 -0
- ezop-0.0.2/README.md +92 -0
- ezop-0.0.2/ezop/__init__.py +7 -0
- ezop-0.0.2/ezop/agent.py +140 -0
- ezop-0.0.2/ezop/client.py +82 -0
- ezop-0.0.2/ezop/config.py +6 -0
- ezop-0.0.2/ezop/models.py +36 -0
- ezop-0.0.2/ezop.egg-info/PKG-INFO +106 -0
- ezop-0.0.2/ezop.egg-info/SOURCES.txt +14 -0
- ezop-0.0.2/ezop.egg-info/dependency_links.txt +1 -0
- ezop-0.0.2/ezop.egg-info/requires.txt +7 -0
- ezop-0.0.2/ezop.egg-info/top_level.txt +1 -0
- ezop-0.0.2/pyproject.toml +22 -0
- ezop-0.0.2/setup.cfg +4 -0
- ezop-0.0.2/tests/test_agent.py +166 -0
- ezop-0.0.2/tests/test_integration.py +495 -0
ezop-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ezop
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Ezop SDK - The story of every AI agent
|
|
5
|
+
Author-email: Ezop <support@ezop.ai>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: responses; extra == "dev"
|
|
12
|
+
Requires-Dist: twine; extra == "dev"
|
|
13
|
+
Requires-Dist: build; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# Ezop Python SDK
|
|
16
|
+
|
|
17
|
+
Ezop tracks the lifecycle of your AI agents — registrations, versions, and runs — so you have full observability across every deployment.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ezop
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Set your API key before using the SDK:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export EZOP_API_KEY=your-ezop-api-key-here
|
|
31
|
+
export EZOP_API_URL=https://api.ezop.ai # optional, this is the default
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ezop import Agent
|
|
38
|
+
|
|
39
|
+
# Register your agent at startup (safe to call on every deploy — idempotent)
|
|
40
|
+
agent = Agent.init(
|
|
41
|
+
name="customer-support-bot",
|
|
42
|
+
owner="growth-team",
|
|
43
|
+
version="v0.3",
|
|
44
|
+
runtime="langchain",
|
|
45
|
+
description="Handles tier-1 customer support tickets",
|
|
46
|
+
default_permissions=["read:tickets"],
|
|
47
|
+
permissions=["read:tickets", "write:replies"],
|
|
48
|
+
changelog="Switched to new retrieval pipeline",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Track a run
|
|
52
|
+
agent.start_run()
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
result = your_agent_logic(user_input)
|
|
56
|
+
agent.end_run(
|
|
57
|
+
status="completed",
|
|
58
|
+
total_tokens=result.usage.total_tokens,
|
|
59
|
+
total_cost=result.cost,
|
|
60
|
+
metadata={"user_id": user_id},
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
agent.end_run(status="failed", metadata={"error": str(e)})
|
|
64
|
+
raise
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API reference
|
|
68
|
+
|
|
69
|
+
### `Agent.init()`
|
|
70
|
+
|
|
71
|
+
Registers the agent and its current version with the Ezop platform. On subsequent calls with the same `name` + `owner`, the agent record is updated in place — safe to call on every startup.
|
|
72
|
+
|
|
73
|
+
| Parameter | Required | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `name` | yes | Agent name |
|
|
76
|
+
| `owner` | yes | Team or user that owns the agent |
|
|
77
|
+
| `version` | yes | Version string for this deployment (e.g. `"v0.3"`) |
|
|
78
|
+
| `runtime` | yes | Runtime framework (e.g. `"langchain"`, `"langchain"`, `"python"`) |
|
|
79
|
+
| `description` | no | Human-readable description of the agent |
|
|
80
|
+
| `default_permissions` | no | Default permission scopes for the agent |
|
|
81
|
+
| `permissions` | no | Permission scopes for this specific version |
|
|
82
|
+
| `changelog` | no | Release notes for this version |
|
|
83
|
+
|
|
84
|
+
### `agent.start_run()`
|
|
85
|
+
|
|
86
|
+
Creates a new run record with status `running`. Call this at the start of each agent invocation.
|
|
87
|
+
|
|
88
|
+
### `agent.end_run()`
|
|
89
|
+
|
|
90
|
+
Closes the current run. Only provided fields are written.
|
|
91
|
+
|
|
92
|
+
| Parameter | Default | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `status` | `"completed"` | Final run status (`"completed"`, `"failed"`, etc.) |
|
|
95
|
+
| `total_tokens` | `None` | Total tokens consumed |
|
|
96
|
+
| `total_cost` | `None` | Total cost in USD |
|
|
97
|
+
| `metadata` | `None` | Arbitrary JSON metadata |
|
|
98
|
+
|
|
99
|
+
## Logging
|
|
100
|
+
|
|
101
|
+
The SDK uses Python's standard `logging` module under the `ezop` namespace. To enable logs in your application:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import logging
|
|
105
|
+
logging.getLogger("ezop").setLevel(logging.DEBUG)
|
|
106
|
+
```
|
ezop-0.0.2/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Ezop Python SDK
|
|
2
|
+
|
|
3
|
+
Ezop tracks the lifecycle of your AI agents — registrations, versions, and runs — so you have full observability across every deployment.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ezop
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Set your API key before using the SDK:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export EZOP_API_KEY=your-ezop-api-key-here
|
|
17
|
+
export EZOP_API_URL=https://api.ezop.ai # optional, this is the default
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from ezop import Agent
|
|
24
|
+
|
|
25
|
+
# Register your agent at startup (safe to call on every deploy — idempotent)
|
|
26
|
+
agent = Agent.init(
|
|
27
|
+
name="customer-support-bot",
|
|
28
|
+
owner="growth-team",
|
|
29
|
+
version="v0.3",
|
|
30
|
+
runtime="langchain",
|
|
31
|
+
description="Handles tier-1 customer support tickets",
|
|
32
|
+
default_permissions=["read:tickets"],
|
|
33
|
+
permissions=["read:tickets", "write:replies"],
|
|
34
|
+
changelog="Switched to new retrieval pipeline",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Track a run
|
|
38
|
+
agent.start_run()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
result = your_agent_logic(user_input)
|
|
42
|
+
agent.end_run(
|
|
43
|
+
status="completed",
|
|
44
|
+
total_tokens=result.usage.total_tokens,
|
|
45
|
+
total_cost=result.cost,
|
|
46
|
+
metadata={"user_id": user_id},
|
|
47
|
+
)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
agent.end_run(status="failed", metadata={"error": str(e)})
|
|
50
|
+
raise
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API reference
|
|
54
|
+
|
|
55
|
+
### `Agent.init()`
|
|
56
|
+
|
|
57
|
+
Registers the agent and its current version with the Ezop platform. On subsequent calls with the same `name` + `owner`, the agent record is updated in place — safe to call on every startup.
|
|
58
|
+
|
|
59
|
+
| Parameter | Required | Description |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `name` | yes | Agent name |
|
|
62
|
+
| `owner` | yes | Team or user that owns the agent |
|
|
63
|
+
| `version` | yes | Version string for this deployment (e.g. `"v0.3"`) |
|
|
64
|
+
| `runtime` | yes | Runtime framework (e.g. `"langchain"`, `"langchain"`, `"python"`) |
|
|
65
|
+
| `description` | no | Human-readable description of the agent |
|
|
66
|
+
| `default_permissions` | no | Default permission scopes for the agent |
|
|
67
|
+
| `permissions` | no | Permission scopes for this specific version |
|
|
68
|
+
| `changelog` | no | Release notes for this version |
|
|
69
|
+
|
|
70
|
+
### `agent.start_run()`
|
|
71
|
+
|
|
72
|
+
Creates a new run record with status `running`. Call this at the start of each agent invocation.
|
|
73
|
+
|
|
74
|
+
### `agent.end_run()`
|
|
75
|
+
|
|
76
|
+
Closes the current run. Only provided fields are written.
|
|
77
|
+
|
|
78
|
+
| Parameter | Default | Description |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `status` | `"completed"` | Final run status (`"completed"`, `"failed"`, etc.) |
|
|
81
|
+
| `total_tokens` | `None` | Total tokens consumed |
|
|
82
|
+
| `total_cost` | `None` | Total cost in USD |
|
|
83
|
+
| `metadata` | `None` | Arbitrary JSON metadata |
|
|
84
|
+
|
|
85
|
+
## Logging
|
|
86
|
+
|
|
87
|
+
The SDK uses Python's standard `logging` module under the `ezop` namespace. To enable logs in your application:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import logging
|
|
91
|
+
logging.getLogger("ezop").setLevel(logging.DEBUG)
|
|
92
|
+
```
|
ezop-0.0.2/ezop/agent.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .models import AgentModel, AgentVersion, AgentRun
|
|
4
|
+
from .client import EzopClient
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Agent:
|
|
10
|
+
|
|
11
|
+
def __init__(self, model: AgentModel, version: AgentVersion):
|
|
12
|
+
self.model = model
|
|
13
|
+
self.version = version
|
|
14
|
+
self.client = EzopClient()
|
|
15
|
+
self.current_run: Optional[AgentRun] = None
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def init(
|
|
19
|
+
cls,
|
|
20
|
+
name: str,
|
|
21
|
+
owner: str,
|
|
22
|
+
version: str,
|
|
23
|
+
runtime: str,
|
|
24
|
+
*,
|
|
25
|
+
description: Optional[str] = None,
|
|
26
|
+
default_permissions: Optional[list[str]] = None,
|
|
27
|
+
permissions: Optional[list[str]] = None,
|
|
28
|
+
changelog: Optional[str] = None,
|
|
29
|
+
) -> "Agent":
|
|
30
|
+
"""
|
|
31
|
+
Initialize an Ezop agent and register it with the Ezop platform.
|
|
32
|
+
Creates the agent record and a corresponding version record.
|
|
33
|
+
"""
|
|
34
|
+
logger.info(
|
|
35
|
+
"Initializing agent name=%s owner=%s version=%s runtime=%s",
|
|
36
|
+
name,
|
|
37
|
+
owner,
|
|
38
|
+
version,
|
|
39
|
+
runtime,
|
|
40
|
+
)
|
|
41
|
+
client = EzopClient()
|
|
42
|
+
|
|
43
|
+
agent_data = client.register_agent(
|
|
44
|
+
{
|
|
45
|
+
"name": name,
|
|
46
|
+
"owner": owner,
|
|
47
|
+
"runtime": runtime,
|
|
48
|
+
"description": description,
|
|
49
|
+
"default_permissions": default_permissions,
|
|
50
|
+
}
|
|
51
|
+
)["data"]
|
|
52
|
+
|
|
53
|
+
model = AgentModel(
|
|
54
|
+
id=agent_data["id"],
|
|
55
|
+
name=agent_data["name"],
|
|
56
|
+
owner=agent_data["owner"],
|
|
57
|
+
runtime=agent_data.get("runtime"),
|
|
58
|
+
description=agent_data.get("description"),
|
|
59
|
+
default_permissions=agent_data.get("default_permissions") or [],
|
|
60
|
+
)
|
|
61
|
+
logger.debug("Agent model built id=%s", model.id)
|
|
62
|
+
|
|
63
|
+
version_data = client.create_version(
|
|
64
|
+
model.id,
|
|
65
|
+
{
|
|
66
|
+
"version": version,
|
|
67
|
+
"permissions": permissions,
|
|
68
|
+
"changelog": changelog,
|
|
69
|
+
},
|
|
70
|
+
)["data"]
|
|
71
|
+
|
|
72
|
+
agent_version = AgentVersion(
|
|
73
|
+
id=version_data["id"],
|
|
74
|
+
agent_id=model.id,
|
|
75
|
+
version=version_data["version"],
|
|
76
|
+
permissions=version_data.get("permissions") or [],
|
|
77
|
+
changelog=version_data.get("changelog"),
|
|
78
|
+
)
|
|
79
|
+
logger.debug("Agent version built id=%s", agent_version.id)
|
|
80
|
+
|
|
81
|
+
logger.info("Agent initialized id=%s version_id=%s", model.id, agent_version.id)
|
|
82
|
+
return cls(model, agent_version)
|
|
83
|
+
|
|
84
|
+
def start_run(self) -> AgentRun:
|
|
85
|
+
"""Start a new run for this agent version."""
|
|
86
|
+
logger.info(
|
|
87
|
+
"Starting run for agent id=%s version_id=%s", self.model.id, self.version.id
|
|
88
|
+
)
|
|
89
|
+
run_data = self.client.start_run(self.model.id, self.version.id)["data"]
|
|
90
|
+
self.current_run = AgentRun(
|
|
91
|
+
id=run_data["id"],
|
|
92
|
+
agent_id=self.model.id,
|
|
93
|
+
version_id=self.version.id,
|
|
94
|
+
status=run_data.get("status", "running"),
|
|
95
|
+
)
|
|
96
|
+
logger.debug(
|
|
97
|
+
"Run created id=%s status=%s", self.current_run.id, self.current_run.status
|
|
98
|
+
)
|
|
99
|
+
return self.current_run
|
|
100
|
+
|
|
101
|
+
def end_run(
|
|
102
|
+
self,
|
|
103
|
+
status: str = "running",
|
|
104
|
+
total_tokens: Optional[int] = None,
|
|
105
|
+
total_cost: Optional[float] = None,
|
|
106
|
+
metadata: Optional[dict] = None,
|
|
107
|
+
message: Optional[str] = None,
|
|
108
|
+
) -> AgentRun:
|
|
109
|
+
"""End the current run.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
status: Final status of the run (e.g. "completed", "failed").
|
|
113
|
+
message: Optional human-readable message, e.g. a failure reason.
|
|
114
|
+
"""
|
|
115
|
+
if self.current_run is None:
|
|
116
|
+
logger.error("end_run called with no active run")
|
|
117
|
+
raise RuntimeError("No active run. Call start_run() first.")
|
|
118
|
+
|
|
119
|
+
logger.info("Ending run id=%s status=%s", self.current_run.id, status)
|
|
120
|
+
run_data = self.client.end_run(
|
|
121
|
+
self.current_run.id,
|
|
122
|
+
status=status,
|
|
123
|
+
total_tokens=total_tokens,
|
|
124
|
+
total_cost=total_cost,
|
|
125
|
+
metadata=metadata,
|
|
126
|
+
message=message,
|
|
127
|
+
)["data"]
|
|
128
|
+
self.current_run.status = run_data.get("status", status)
|
|
129
|
+
self.current_run.total_tokens = run_data.get("total_tokens")
|
|
130
|
+
self.current_run.total_cost = run_data.get("total_cost")
|
|
131
|
+
self.current_run.metadata = run_data.get("metadata")
|
|
132
|
+
self.current_run.message = run_data.get("message")
|
|
133
|
+
logger.debug(
|
|
134
|
+
"Run ended id=%s status=%s total_tokens=%s total_cost=%s",
|
|
135
|
+
self.current_run.id,
|
|
136
|
+
self.current_run.status,
|
|
137
|
+
self.current_run.total_tokens,
|
|
138
|
+
self.current_run.total_cost,
|
|
139
|
+
)
|
|
140
|
+
return self.current_run
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
from .config import Config
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EzopClient:
|
|
9
|
+
|
|
10
|
+
def _headers(self):
|
|
11
|
+
return {
|
|
12
|
+
"Authorization": f"Bearer {Config.EZOP_API_KEY}",
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
17
|
+
url = f"{Config.EZOP_API_URL}{path}"
|
|
18
|
+
logger.debug("POST %s body=%s", url, body)
|
|
19
|
+
response = requests.post(url, json=body, headers=self._headers())
|
|
20
|
+
if response.status_code not in (200, 201):
|
|
21
|
+
logger.error("POST %s failed status=%s body=%s", url, response.status_code, response.text)
|
|
22
|
+
raise Exception(f"Ezop API error: {response.text}")
|
|
23
|
+
logger.debug("POST %s status=%s", url, response.status_code)
|
|
24
|
+
return response.json()
|
|
25
|
+
|
|
26
|
+
def _patch(self, path: str, body: dict) -> dict:
|
|
27
|
+
url = f"{Config.EZOP_API_URL}{path}"
|
|
28
|
+
logger.debug("PATCH %s body=%s", url, body)
|
|
29
|
+
response = requests.patch(url, json=body, headers=self._headers())
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
logger.error("PATCH %s failed status=%s body=%s", url, response.status_code, response.text)
|
|
32
|
+
raise Exception(f"Ezop API error: {response.text}")
|
|
33
|
+
logger.debug("PATCH %s status=%s", url, response.status_code)
|
|
34
|
+
return response.json()
|
|
35
|
+
|
|
36
|
+
def register_agent(self, agent_data: dict) -> dict:
|
|
37
|
+
logger.info("Registering agent name=%s owner=%s", agent_data.get("name"), agent_data.get("owner"))
|
|
38
|
+
result = self._post("/agents/register", agent_data)
|
|
39
|
+
logger.info("Agent registered id=%s", result.get("data", {}).get("id"))
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
def create_version(self, agent_id: str, version_data: dict) -> dict:
|
|
43
|
+
logger.info("Creating version agent_id=%s version=%s", agent_id, version_data.get("version"))
|
|
44
|
+
result = self._post(f"/agents/{agent_id}/versions", version_data)
|
|
45
|
+
logger.info("Version created id=%s", result.get("data", {}).get("id"))
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
def start_run(
|
|
49
|
+
self,
|
|
50
|
+
agent_id: str,
|
|
51
|
+
version_id: str | None = None,
|
|
52
|
+
user_id: str | None = None,
|
|
53
|
+
metadata: dict | None = None,
|
|
54
|
+
) -> dict:
|
|
55
|
+
logger.info("Starting run agent_id=%s version_id=%s", agent_id, version_id)
|
|
56
|
+
body: dict = {}
|
|
57
|
+
if version_id is not None:
|
|
58
|
+
body["version_id"] = version_id
|
|
59
|
+
if user_id is not None:
|
|
60
|
+
body["user_id"] = user_id
|
|
61
|
+
if metadata is not None:
|
|
62
|
+
body["metadata"] = metadata
|
|
63
|
+
result = self._post(f"/agents/{agent_id}/runs", body)
|
|
64
|
+
logger.info("Run started id=%s", result.get("data", {}).get("id"))
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def end_run(self, run_id: str, status: str, total_tokens: int | None = None,
|
|
68
|
+
total_cost: float | None = None, metadata: dict | None = None,
|
|
69
|
+
message: str | None = None) -> dict:
|
|
70
|
+
logger.info("Ending run id=%s status=%s", run_id, status)
|
|
71
|
+
body = {"status": status}
|
|
72
|
+
if total_tokens is not None:
|
|
73
|
+
body["total_tokens"] = total_tokens
|
|
74
|
+
if total_cost is not None:
|
|
75
|
+
body["total_cost"] = total_cost
|
|
76
|
+
if metadata is not None:
|
|
77
|
+
body["metadata"] = metadata
|
|
78
|
+
if message is not None:
|
|
79
|
+
body["message"] = message
|
|
80
|
+
result = self._patch(f"/runs/{run_id}", body)
|
|
81
|
+
logger.info("Run ended id=%s status=%s total_tokens=%s total_cost=%s", run_id, status, total_tokens, total_cost)
|
|
82
|
+
return result
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class AgentModel:
|
|
8
|
+
id: str
|
|
9
|
+
name: str
|
|
10
|
+
owner: str
|
|
11
|
+
runtime: Optional[str]
|
|
12
|
+
description: Optional[str]
|
|
13
|
+
default_permissions: list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AgentVersion:
|
|
18
|
+
id: str
|
|
19
|
+
agent_id: str
|
|
20
|
+
version: str
|
|
21
|
+
permissions: list[str]
|
|
22
|
+
changelog: Optional[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AgentRun:
|
|
27
|
+
id: str
|
|
28
|
+
agent_id: str
|
|
29
|
+
version_id: Optional[str]
|
|
30
|
+
status: str
|
|
31
|
+
start_time: Optional[datetime] = None
|
|
32
|
+
end_time: Optional[datetime] = None
|
|
33
|
+
total_tokens: Optional[int] = None
|
|
34
|
+
total_cost: Optional[float] = None
|
|
35
|
+
metadata: Optional[dict] = None
|
|
36
|
+
message: Optional[str] = None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ezop
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Ezop SDK - The story of every AI agent
|
|
5
|
+
Author-email: Ezop <support@ezop.ai>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: responses; extra == "dev"
|
|
12
|
+
Requires-Dist: twine; extra == "dev"
|
|
13
|
+
Requires-Dist: build; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# Ezop Python SDK
|
|
16
|
+
|
|
17
|
+
Ezop tracks the lifecycle of your AI agents — registrations, versions, and runs — so you have full observability across every deployment.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ezop
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Set your API key before using the SDK:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export EZOP_API_KEY=your-ezop-api-key-here
|
|
31
|
+
export EZOP_API_URL=https://api.ezop.ai # optional, this is the default
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ezop import Agent
|
|
38
|
+
|
|
39
|
+
# Register your agent at startup (safe to call on every deploy — idempotent)
|
|
40
|
+
agent = Agent.init(
|
|
41
|
+
name="customer-support-bot",
|
|
42
|
+
owner="growth-team",
|
|
43
|
+
version="v0.3",
|
|
44
|
+
runtime="langchain",
|
|
45
|
+
description="Handles tier-1 customer support tickets",
|
|
46
|
+
default_permissions=["read:tickets"],
|
|
47
|
+
permissions=["read:tickets", "write:replies"],
|
|
48
|
+
changelog="Switched to new retrieval pipeline",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Track a run
|
|
52
|
+
agent.start_run()
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
result = your_agent_logic(user_input)
|
|
56
|
+
agent.end_run(
|
|
57
|
+
status="completed",
|
|
58
|
+
total_tokens=result.usage.total_tokens,
|
|
59
|
+
total_cost=result.cost,
|
|
60
|
+
metadata={"user_id": user_id},
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
agent.end_run(status="failed", metadata={"error": str(e)})
|
|
64
|
+
raise
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API reference
|
|
68
|
+
|
|
69
|
+
### `Agent.init()`
|
|
70
|
+
|
|
71
|
+
Registers the agent and its current version with the Ezop platform. On subsequent calls with the same `name` + `owner`, the agent record is updated in place — safe to call on every startup.
|
|
72
|
+
|
|
73
|
+
| Parameter | Required | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `name` | yes | Agent name |
|
|
76
|
+
| `owner` | yes | Team or user that owns the agent |
|
|
77
|
+
| `version` | yes | Version string for this deployment (e.g. `"v0.3"`) |
|
|
78
|
+
| `runtime` | yes | Runtime framework (e.g. `"langchain"`, `"langchain"`, `"python"`) |
|
|
79
|
+
| `description` | no | Human-readable description of the agent |
|
|
80
|
+
| `default_permissions` | no | Default permission scopes for the agent |
|
|
81
|
+
| `permissions` | no | Permission scopes for this specific version |
|
|
82
|
+
| `changelog` | no | Release notes for this version |
|
|
83
|
+
|
|
84
|
+
### `agent.start_run()`
|
|
85
|
+
|
|
86
|
+
Creates a new run record with status `running`. Call this at the start of each agent invocation.
|
|
87
|
+
|
|
88
|
+
### `agent.end_run()`
|
|
89
|
+
|
|
90
|
+
Closes the current run. Only provided fields are written.
|
|
91
|
+
|
|
92
|
+
| Parameter | Default | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `status` | `"completed"` | Final run status (`"completed"`, `"failed"`, etc.) |
|
|
95
|
+
| `total_tokens` | `None` | Total tokens consumed |
|
|
96
|
+
| `total_cost` | `None` | Total cost in USD |
|
|
97
|
+
| `metadata` | `None` | Arbitrary JSON metadata |
|
|
98
|
+
|
|
99
|
+
## Logging
|
|
100
|
+
|
|
101
|
+
The SDK uses Python's standard `logging` module under the `ezop` namespace. To enable logs in your application:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import logging
|
|
105
|
+
logging.getLogger("ezop").setLevel(logging.DEBUG)
|
|
106
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ezop/__init__.py
|
|
4
|
+
ezop/agent.py
|
|
5
|
+
ezop/client.py
|
|
6
|
+
ezop/config.py
|
|
7
|
+
ezop/models.py
|
|
8
|
+
ezop.egg-info/PKG-INFO
|
|
9
|
+
ezop.egg-info/SOURCES.txt
|
|
10
|
+
ezop.egg-info/dependency_links.txt
|
|
11
|
+
ezop.egg-info/requires.txt
|
|
12
|
+
ezop.egg-info/top_level.txt
|
|
13
|
+
tests/test_agent.py
|
|
14
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ezop
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ezop"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Ezop SDK - The story of every AI agent"
|
|
5
|
+
authors = [{ name = "Ezop", email = "support@ezop.ai" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"requests"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest",
|
|
15
|
+
"responses",
|
|
16
|
+
"twine",
|
|
17
|
+
"build",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=42", "wheel"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
ezop-0.0.2/setup.cfg
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from unittest.mock import patch, MagicMock
|
|
2
|
+
import pytest
|
|
3
|
+
from ezop import Agent
|
|
4
|
+
from ezop.models import AgentModel, AgentVersion, AgentRun
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
AGENT_RESP = {"success": True, "error": None, "data": {
|
|
8
|
+
"id": "agent-uuid-123",
|
|
9
|
+
"name": "support-bot",
|
|
10
|
+
"owner": "growth-team",
|
|
11
|
+
"runtime": "langchain",
|
|
12
|
+
"description": "Handles support tickets",
|
|
13
|
+
"default_permissions": ["read:tickets"],
|
|
14
|
+
}}
|
|
15
|
+
|
|
16
|
+
VERSION_RESP = {"success": True, "error": None, "data": {
|
|
17
|
+
"id": "version-uuid-456",
|
|
18
|
+
"version": "v0.3",
|
|
19
|
+
"permissions": ["read:tickets", "write:replies"],
|
|
20
|
+
"changelog": "Initial release",
|
|
21
|
+
}}
|
|
22
|
+
|
|
23
|
+
RUN_RESP = {"success": True, "error": None, "data": {
|
|
24
|
+
"id": "run-uuid-789",
|
|
25
|
+
"status": "running",
|
|
26
|
+
}}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_agent():
|
|
30
|
+
with patch("ezop.client.EzopClient.register_agent", return_value=AGENT_RESP), \
|
|
31
|
+
patch("ezop.client.EzopClient.create_version", return_value=VERSION_RESP):
|
|
32
|
+
return Agent.init(
|
|
33
|
+
name="support-bot",
|
|
34
|
+
owner="growth-team",
|
|
35
|
+
version="v0.3",
|
|
36
|
+
runtime="langchain",
|
|
37
|
+
description="Handles support tickets",
|
|
38
|
+
default_permissions=["read:tickets"],
|
|
39
|
+
permissions=["read:tickets", "write:replies"],
|
|
40
|
+
changelog="Initial release",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestAgentInit:
|
|
45
|
+
def test_returns_agent_instance(self):
|
|
46
|
+
agent = make_agent()
|
|
47
|
+
assert isinstance(agent, Agent)
|
|
48
|
+
|
|
49
|
+
def test_model_populated(self):
|
|
50
|
+
agent = make_agent()
|
|
51
|
+
assert agent.model.id == "agent-uuid-123"
|
|
52
|
+
assert agent.model.name == "support-bot"
|
|
53
|
+
assert agent.model.owner == "growth-team"
|
|
54
|
+
assert agent.model.runtime == "langchain"
|
|
55
|
+
assert agent.model.description == "Handles support tickets"
|
|
56
|
+
assert agent.model.default_permissions == ["read:tickets"]
|
|
57
|
+
|
|
58
|
+
def test_version_populated(self):
|
|
59
|
+
agent = make_agent()
|
|
60
|
+
assert agent.version.id == "version-uuid-456"
|
|
61
|
+
assert agent.version.agent_id == "agent-uuid-123"
|
|
62
|
+
assert agent.version.version == "v0.3"
|
|
63
|
+
assert agent.version.permissions == ["read:tickets", "write:replies"]
|
|
64
|
+
assert agent.version.changelog == "Initial release"
|
|
65
|
+
|
|
66
|
+
def test_no_active_run_on_init(self):
|
|
67
|
+
agent = make_agent()
|
|
68
|
+
assert agent.current_run is None
|
|
69
|
+
|
|
70
|
+
def test_calls_register_agent_with_correct_payload(self):
|
|
71
|
+
with patch("ezop.client.EzopClient.register_agent", return_value=AGENT_RESP) as mock_register, \
|
|
72
|
+
patch("ezop.client.EzopClient.create_version", return_value=VERSION_RESP):
|
|
73
|
+
Agent.init(name="support-bot", owner="growth-team", version="v0.3", runtime="langchain",
|
|
74
|
+
description="Handles support tickets", default_permissions=["read:tickets"])
|
|
75
|
+
mock_register.assert_called_once_with({
|
|
76
|
+
"name": "support-bot",
|
|
77
|
+
"owner": "growth-team",
|
|
78
|
+
"runtime": "langchain",
|
|
79
|
+
"description": "Handles support tickets",
|
|
80
|
+
"default_permissions": ["read:tickets"],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
def test_calls_create_version_with_correct_payload(self):
|
|
84
|
+
with patch("ezop.client.EzopClient.register_agent", return_value=AGENT_RESP), \
|
|
85
|
+
patch("ezop.client.EzopClient.create_version", return_value=VERSION_RESP) as mock_version:
|
|
86
|
+
Agent.init(name="support-bot", owner="growth-team", version="v0.3", runtime="langchain",
|
|
87
|
+
permissions=["read:tickets"], changelog="Initial release")
|
|
88
|
+
mock_version.assert_called_once_with("agent-uuid-123", {
|
|
89
|
+
"version": "v0.3",
|
|
90
|
+
"permissions": ["read:tickets"],
|
|
91
|
+
"changelog": "Initial release",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
def test_optional_fields_default_to_empty(self):
|
|
95
|
+
with patch("ezop.client.EzopClient.register_agent", return_value={**AGENT_RESP, "data": {**AGENT_RESP["data"], "default_permissions": None}}), \
|
|
96
|
+
patch("ezop.client.EzopClient.create_version", return_value={**VERSION_RESP, "data": {**VERSION_RESP["data"], "permissions": None}}):
|
|
97
|
+
agent = Agent.init(name="support-bot", owner="growth-team", version="v0.3", runtime="langchain")
|
|
98
|
+
assert agent.model.default_permissions == []
|
|
99
|
+
assert agent.version.permissions == []
|
|
100
|
+
|
|
101
|
+
def test_raises_on_api_error(self):
|
|
102
|
+
with patch("ezop.client.EzopClient.register_agent", side_effect=Exception("Ezop API error: 401")):
|
|
103
|
+
with pytest.raises(Exception, match="Ezop API error"):
|
|
104
|
+
Agent.init(name="support-bot", owner="growth-team", version="v0.3", runtime="langchain")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestAgentRuns:
|
|
108
|
+
def test_start_run_returns_agent_run(self):
|
|
109
|
+
agent = make_agent()
|
|
110
|
+
with patch("ezop.client.EzopClient.start_run", return_value=RUN_RESP):
|
|
111
|
+
run = agent.start_run()
|
|
112
|
+
assert isinstance(run, AgentRun)
|
|
113
|
+
assert run.id == "run-uuid-789"
|
|
114
|
+
assert run.status == "running"
|
|
115
|
+
assert run.agent_id == "agent-uuid-123"
|
|
116
|
+
assert run.version_id == "version-uuid-456"
|
|
117
|
+
|
|
118
|
+
def test_start_run_sets_current_run(self):
|
|
119
|
+
agent = make_agent()
|
|
120
|
+
with patch("ezop.client.EzopClient.start_run", return_value=RUN_RESP):
|
|
121
|
+
agent.start_run()
|
|
122
|
+
assert agent.current_run is not None
|
|
123
|
+
assert agent.current_run.id == "run-uuid-789"
|
|
124
|
+
|
|
125
|
+
def test_end_run_updates_status(self):
|
|
126
|
+
agent = make_agent()
|
|
127
|
+
with patch("ezop.client.EzopClient.start_run", return_value=RUN_RESP):
|
|
128
|
+
agent.start_run()
|
|
129
|
+
end_resp = {"success": True, "error": None, "data": {"status": "completed", "total_tokens": 500, "total_cost": 0.01, "metadata": None}}
|
|
130
|
+
with patch("ezop.client.EzopClient.end_run", return_value=end_resp):
|
|
131
|
+
run = agent.end_run(status="completed", total_tokens=500, total_cost=0.01)
|
|
132
|
+
assert run.status == "completed"
|
|
133
|
+
assert run.total_tokens == 500
|
|
134
|
+
assert run.total_cost == 0.01
|
|
135
|
+
|
|
136
|
+
def test_end_run_failed_status(self):
|
|
137
|
+
agent = make_agent()
|
|
138
|
+
with patch("ezop.client.EzopClient.start_run", return_value=RUN_RESP):
|
|
139
|
+
agent.start_run()
|
|
140
|
+
end_resp = {"success": True, "error": None, "data": {"status": "failed", "total_tokens": None, "total_cost": None, "metadata": {"error": "timeout"}, "message": "Rate limit exceeded"}}
|
|
141
|
+
with patch("ezop.client.EzopClient.end_run", return_value=end_resp):
|
|
142
|
+
run = agent.end_run(status="failed", metadata={"error": "timeout"}, message="Rate limit exceeded")
|
|
143
|
+
assert run.status == "failed"
|
|
144
|
+
assert run.metadata == {"error": "timeout"}
|
|
145
|
+
assert run.message == "Rate limit exceeded"
|
|
146
|
+
|
|
147
|
+
def test_end_run_calls_client_with_correct_args(self):
|
|
148
|
+
agent = make_agent()
|
|
149
|
+
with patch("ezop.client.EzopClient.start_run", return_value=RUN_RESP):
|
|
150
|
+
agent.start_run()
|
|
151
|
+
end_resp = {"success": True, "error": None, "data": {"status": "completed", "total_tokens": 100, "total_cost": 0.005, "metadata": {"k": "v"}, "message": None}}
|
|
152
|
+
with patch("ezop.client.EzopClient.end_run", return_value=end_resp) as mock_end:
|
|
153
|
+
agent.end_run(status="completed", total_tokens=100, total_cost=0.005, metadata={"k": "v"})
|
|
154
|
+
mock_end.assert_called_once_with(
|
|
155
|
+
"run-uuid-789",
|
|
156
|
+
status="completed",
|
|
157
|
+
total_tokens=100,
|
|
158
|
+
total_cost=0.005,
|
|
159
|
+
metadata={"k": "v"},
|
|
160
|
+
message=None,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def test_end_run_without_start_raises(self):
|
|
164
|
+
agent = make_agent()
|
|
165
|
+
with pytest.raises(RuntimeError, match="No active run"):
|
|
166
|
+
agent.end_run()
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for the Ezop SDK.
|
|
3
|
+
|
|
4
|
+
These tests use the `responses` library to intercept HTTP calls at the network
|
|
5
|
+
level, exercising the full stack (Agent → EzopClient → requests → HTTP parsing)
|
|
6
|
+
without hitting a real server.
|
|
7
|
+
|
|
8
|
+
To run against the real Ezop API, set EZOP_API_KEY and EZOP_API_URL in your
|
|
9
|
+
environment and run:
|
|
10
|
+
|
|
11
|
+
pytest tests/test_integration.py -m live -v
|
|
12
|
+
|
|
13
|
+
Live tests are skipped by default unless EZOP_API_KEY is present.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
import responses as responses_lib
|
|
18
|
+
from responses import matchers
|
|
19
|
+
from ezop import Agent
|
|
20
|
+
from ezop.config import Config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BASE_URL = Config.EZOP_API_URL
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
_AGENT_DATA = {
|
|
31
|
+
"id": "integ-agent-001",
|
|
32
|
+
"name": "integ-bot",
|
|
33
|
+
"owner": "integ-team",
|
|
34
|
+
"runtime": "langchain",
|
|
35
|
+
"description": "Integration test agent",
|
|
36
|
+
"default_permissions": ["read:logs"],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_VERSION_DATA = {
|
|
40
|
+
"id": "integ-version-001",
|
|
41
|
+
"agent_id": "integ-agent-001",
|
|
42
|
+
"version": "v1.0",
|
|
43
|
+
"permissions": ["read:logs", "write:reports"],
|
|
44
|
+
"changelog": "Integration test version",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_RUN_DATA = {
|
|
48
|
+
"id": "integ-run-001",
|
|
49
|
+
"status": "running",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_END_RUN_DATA = {
|
|
53
|
+
"id": "integ-run-001",
|
|
54
|
+
"status": "completed",
|
|
55
|
+
"total_tokens": 1200,
|
|
56
|
+
"total_cost": 0.024,
|
|
57
|
+
"metadata": None,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _wrap(data):
|
|
62
|
+
return {"success": True, "data": data, "error": None}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
AGENT_PAYLOAD = _wrap(_AGENT_DATA)
|
|
66
|
+
VERSION_PAYLOAD = _wrap(_VERSION_DATA)
|
|
67
|
+
RUN_PAYLOAD = _wrap(_RUN_DATA)
|
|
68
|
+
END_RUN_PAYLOAD = _wrap(_END_RUN_DATA)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def register_mock_endpoints():
|
|
72
|
+
"""Register all mock HTTP endpoints for a complete agent lifecycle."""
|
|
73
|
+
responses_lib.add(
|
|
74
|
+
responses_lib.POST,
|
|
75
|
+
f"{BASE_URL}/agents/register",
|
|
76
|
+
json=AGENT_PAYLOAD,
|
|
77
|
+
status=201,
|
|
78
|
+
)
|
|
79
|
+
responses_lib.add(
|
|
80
|
+
responses_lib.POST,
|
|
81
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
82
|
+
json=VERSION_PAYLOAD,
|
|
83
|
+
status=201,
|
|
84
|
+
)
|
|
85
|
+
responses_lib.add(
|
|
86
|
+
responses_lib.POST,
|
|
87
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
88
|
+
json=RUN_PAYLOAD,
|
|
89
|
+
status=201,
|
|
90
|
+
)
|
|
91
|
+
responses_lib.add(
|
|
92
|
+
responses_lib.PATCH,
|
|
93
|
+
f"{BASE_URL}/runs/integ-run-001",
|
|
94
|
+
json=END_RUN_PAYLOAD,
|
|
95
|
+
status=200,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# HTTP-level integration tests (no real network — responses intercepts)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestAgentInitHTTP:
|
|
105
|
+
"""Verify Agent.init() sends correct HTTP requests and parses responses."""
|
|
106
|
+
|
|
107
|
+
@responses_lib.activate
|
|
108
|
+
def test_register_agent_url_and_method(self):
|
|
109
|
+
responses_lib.add(
|
|
110
|
+
responses_lib.POST,
|
|
111
|
+
f"{BASE_URL}/agents/register",
|
|
112
|
+
json=AGENT_PAYLOAD,
|
|
113
|
+
status=201,
|
|
114
|
+
)
|
|
115
|
+
responses_lib.add(
|
|
116
|
+
responses_lib.POST,
|
|
117
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
118
|
+
json=VERSION_PAYLOAD,
|
|
119
|
+
status=201,
|
|
120
|
+
)
|
|
121
|
+
Agent.init(
|
|
122
|
+
name="integ-bot", owner="integ-team", version="v1.0", runtime="langchain"
|
|
123
|
+
)
|
|
124
|
+
assert responses_lib.calls[0].request.method == "POST"
|
|
125
|
+
assert responses_lib.calls[0].request.url == f"{BASE_URL}/agents/register"
|
|
126
|
+
|
|
127
|
+
@responses_lib.activate
|
|
128
|
+
def test_register_agent_request_body(self):
|
|
129
|
+
import json
|
|
130
|
+
|
|
131
|
+
responses_lib.add(
|
|
132
|
+
responses_lib.POST,
|
|
133
|
+
f"{BASE_URL}/agents/register",
|
|
134
|
+
json=AGENT_PAYLOAD,
|
|
135
|
+
status=201,
|
|
136
|
+
)
|
|
137
|
+
responses_lib.add(
|
|
138
|
+
responses_lib.POST,
|
|
139
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
140
|
+
json=VERSION_PAYLOAD,
|
|
141
|
+
status=201,
|
|
142
|
+
)
|
|
143
|
+
Agent.init(
|
|
144
|
+
name="integ-bot",
|
|
145
|
+
owner="integ-team",
|
|
146
|
+
version="v1.0",
|
|
147
|
+
runtime="langchain",
|
|
148
|
+
description="Integration test agent",
|
|
149
|
+
default_permissions=["read:logs"],
|
|
150
|
+
)
|
|
151
|
+
body = json.loads(responses_lib.calls[0].request.body)
|
|
152
|
+
assert body["name"] == "integ-bot"
|
|
153
|
+
assert body["owner"] == "integ-team"
|
|
154
|
+
assert body["runtime"] == "langchain"
|
|
155
|
+
assert body["description"] == "Integration test agent"
|
|
156
|
+
assert body["default_permissions"] == ["read:logs"]
|
|
157
|
+
|
|
158
|
+
@responses_lib.activate
|
|
159
|
+
def test_register_agent_auth_header(self):
|
|
160
|
+
responses_lib.add(
|
|
161
|
+
responses_lib.POST,
|
|
162
|
+
f"{BASE_URL}/agents/register",
|
|
163
|
+
json=AGENT_PAYLOAD,
|
|
164
|
+
status=201,
|
|
165
|
+
)
|
|
166
|
+
responses_lib.add(
|
|
167
|
+
responses_lib.POST,
|
|
168
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
169
|
+
json=VERSION_PAYLOAD,
|
|
170
|
+
status=201,
|
|
171
|
+
)
|
|
172
|
+
Agent.init(
|
|
173
|
+
name="integ-bot", owner="integ-team", version="v1.0", runtime="langchain"
|
|
174
|
+
)
|
|
175
|
+
auth = responses_lib.calls[0].request.headers.get("Authorization", "")
|
|
176
|
+
assert auth.startswith("Bearer ")
|
|
177
|
+
|
|
178
|
+
@responses_lib.activate
|
|
179
|
+
def test_create_version_url_uses_agent_id(self):
|
|
180
|
+
responses_lib.add(
|
|
181
|
+
responses_lib.POST,
|
|
182
|
+
f"{BASE_URL}/agents/register",
|
|
183
|
+
json=AGENT_PAYLOAD,
|
|
184
|
+
status=201,
|
|
185
|
+
)
|
|
186
|
+
responses_lib.add(
|
|
187
|
+
responses_lib.POST,
|
|
188
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
189
|
+
json=VERSION_PAYLOAD,
|
|
190
|
+
status=201,
|
|
191
|
+
)
|
|
192
|
+
Agent.init(
|
|
193
|
+
name="integ-bot", owner="integ-team", version="v1.0", runtime="langchain"
|
|
194
|
+
)
|
|
195
|
+
assert (
|
|
196
|
+
responses_lib.calls[1].request.url
|
|
197
|
+
== f"{BASE_URL}/agents/integ-agent-001/versions"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@responses_lib.activate
|
|
201
|
+
def test_create_version_request_body(self):
|
|
202
|
+
import json
|
|
203
|
+
|
|
204
|
+
responses_lib.add(
|
|
205
|
+
responses_lib.POST,
|
|
206
|
+
f"{BASE_URL}/agents/register",
|
|
207
|
+
json=AGENT_PAYLOAD,
|
|
208
|
+
status=201,
|
|
209
|
+
)
|
|
210
|
+
responses_lib.add(
|
|
211
|
+
responses_lib.POST,
|
|
212
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
213
|
+
json=VERSION_PAYLOAD,
|
|
214
|
+
status=201,
|
|
215
|
+
)
|
|
216
|
+
Agent.init(
|
|
217
|
+
name="integ-bot",
|
|
218
|
+
owner="integ-team",
|
|
219
|
+
version="v1.0",
|
|
220
|
+
runtime="langchain",
|
|
221
|
+
permissions=["read:logs", "write:reports"],
|
|
222
|
+
changelog="Integration test version",
|
|
223
|
+
)
|
|
224
|
+
body = json.loads(responses_lib.calls[1].request.body)
|
|
225
|
+
assert body["version"] == "v1.0"
|
|
226
|
+
assert body["permissions"] == ["read:logs", "write:reports"]
|
|
227
|
+
assert body["changelog"] == "Integration test version"
|
|
228
|
+
|
|
229
|
+
@responses_lib.activate
|
|
230
|
+
def test_agent_built_correctly_from_responses(self):
|
|
231
|
+
responses_lib.add(
|
|
232
|
+
responses_lib.POST,
|
|
233
|
+
f"{BASE_URL}/agents/register",
|
|
234
|
+
json=AGENT_PAYLOAD,
|
|
235
|
+
status=201,
|
|
236
|
+
)
|
|
237
|
+
responses_lib.add(
|
|
238
|
+
responses_lib.POST,
|
|
239
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
240
|
+
json=VERSION_PAYLOAD,
|
|
241
|
+
status=201,
|
|
242
|
+
)
|
|
243
|
+
agent = Agent.init(
|
|
244
|
+
name="integ-bot",
|
|
245
|
+
owner="integ-team",
|
|
246
|
+
version="v1.0",
|
|
247
|
+
runtime="lanchain",
|
|
248
|
+
description="Integration test agent",
|
|
249
|
+
default_permissions=["read:logs"],
|
|
250
|
+
permissions=["read:logs", "write:reports"],
|
|
251
|
+
changelog="Integration test version",
|
|
252
|
+
)
|
|
253
|
+
assert agent.model.id == "integ-agent-001"
|
|
254
|
+
assert agent.model.name == "integ-bot"
|
|
255
|
+
assert agent.model.runtime == "langchain"
|
|
256
|
+
assert agent.version.id == "integ-version-001"
|
|
257
|
+
assert agent.version.version == "v1.0"
|
|
258
|
+
assert agent.version.permissions == ["read:logs", "write:reports"]
|
|
259
|
+
|
|
260
|
+
@responses_lib.activate
|
|
261
|
+
def test_api_400_raises_exception(self):
|
|
262
|
+
responses_lib.add(
|
|
263
|
+
responses_lib.POST,
|
|
264
|
+
f"{BASE_URL}/agents/register",
|
|
265
|
+
json={"error": "invalid payload"},
|
|
266
|
+
status=400,
|
|
267
|
+
)
|
|
268
|
+
with pytest.raises(Exception, match="Ezop API error"):
|
|
269
|
+
Agent.init(
|
|
270
|
+
name="integ-bot",
|
|
271
|
+
owner="integ-team",
|
|
272
|
+
version="v1.0",
|
|
273
|
+
runtime="langchain",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@responses_lib.activate
|
|
277
|
+
def test_api_401_raises_exception(self):
|
|
278
|
+
responses_lib.add(
|
|
279
|
+
responses_lib.POST,
|
|
280
|
+
f"{BASE_URL}/agents/register",
|
|
281
|
+
json={"error": "unauthorized"},
|
|
282
|
+
status=401,
|
|
283
|
+
)
|
|
284
|
+
with pytest.raises(Exception, match="Ezop API error"):
|
|
285
|
+
Agent.init(
|
|
286
|
+
name="integ-bot",
|
|
287
|
+
owner="integ-team",
|
|
288
|
+
version="v1.0",
|
|
289
|
+
runtime="langchain",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
@responses_lib.activate
|
|
293
|
+
def test_api_500_raises_exception(self):
|
|
294
|
+
responses_lib.add(
|
|
295
|
+
responses_lib.POST,
|
|
296
|
+
f"{BASE_URL}/agents/register",
|
|
297
|
+
json={"error": "server error"},
|
|
298
|
+
status=500,
|
|
299
|
+
)
|
|
300
|
+
with pytest.raises(Exception, match="Ezop API error"):
|
|
301
|
+
Agent.init(
|
|
302
|
+
name="integ-bot",
|
|
303
|
+
owner="integ-team",
|
|
304
|
+
version="v1.0",
|
|
305
|
+
runtime="langchain",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class TestRunLifecycleHTTP:
|
|
310
|
+
"""Verify start_run / end_run send correct HTTP requests."""
|
|
311
|
+
|
|
312
|
+
def _make_agent(self):
|
|
313
|
+
responses_lib.add(
|
|
314
|
+
responses_lib.POST,
|
|
315
|
+
f"{BASE_URL}/agents/register",
|
|
316
|
+
json=AGENT_PAYLOAD,
|
|
317
|
+
status=201,
|
|
318
|
+
)
|
|
319
|
+
responses_lib.add(
|
|
320
|
+
responses_lib.POST,
|
|
321
|
+
f"{BASE_URL}/agents/integ-agent-001/versions",
|
|
322
|
+
json=VERSION_PAYLOAD,
|
|
323
|
+
status=201,
|
|
324
|
+
)
|
|
325
|
+
return Agent.init(
|
|
326
|
+
name="integ-bot", owner="integ-team", version="v1.0", runtime="langchain"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
@responses_lib.activate
|
|
330
|
+
def test_start_run_url_and_method(self):
|
|
331
|
+
agent = self._make_agent()
|
|
332
|
+
responses_lib.add(
|
|
333
|
+
responses_lib.POST,
|
|
334
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
335
|
+
json=RUN_PAYLOAD,
|
|
336
|
+
status=201,
|
|
337
|
+
)
|
|
338
|
+
agent.start_run()
|
|
339
|
+
last_call = responses_lib.calls[-1]
|
|
340
|
+
assert last_call.request.method == "POST"
|
|
341
|
+
assert last_call.request.url == f"{BASE_URL}/agents/integ-agent-001/runs"
|
|
342
|
+
|
|
343
|
+
@responses_lib.activate
|
|
344
|
+
def test_start_run_request_body_contains_version_id(self):
|
|
345
|
+
import json
|
|
346
|
+
|
|
347
|
+
agent = self._make_agent()
|
|
348
|
+
responses_lib.add(
|
|
349
|
+
responses_lib.POST,
|
|
350
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
351
|
+
json=RUN_PAYLOAD,
|
|
352
|
+
status=201,
|
|
353
|
+
)
|
|
354
|
+
agent.start_run()
|
|
355
|
+
body = json.loads(responses_lib.calls[-1].request.body)
|
|
356
|
+
assert body["version_id"] == "integ-version-001"
|
|
357
|
+
|
|
358
|
+
@responses_lib.activate
|
|
359
|
+
def test_start_run_returns_correct_run(self):
|
|
360
|
+
agent = self._make_agent()
|
|
361
|
+
responses_lib.add(
|
|
362
|
+
responses_lib.POST,
|
|
363
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
364
|
+
json=RUN_PAYLOAD,
|
|
365
|
+
status=201,
|
|
366
|
+
)
|
|
367
|
+
run = agent.start_run()
|
|
368
|
+
assert run.id == "integ-run-001"
|
|
369
|
+
assert run.status == "running"
|
|
370
|
+
assert run.agent_id == "integ-agent-001"
|
|
371
|
+
assert run.version_id == "integ-version-001"
|
|
372
|
+
|
|
373
|
+
@responses_lib.activate
|
|
374
|
+
def test_end_run_url_and_method(self):
|
|
375
|
+
agent = self._make_agent()
|
|
376
|
+
responses_lib.add(
|
|
377
|
+
responses_lib.POST,
|
|
378
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
379
|
+
json=RUN_PAYLOAD,
|
|
380
|
+
status=201,
|
|
381
|
+
)
|
|
382
|
+
responses_lib.add(
|
|
383
|
+
responses_lib.PATCH,
|
|
384
|
+
f"{BASE_URL}/runs/integ-run-001",
|
|
385
|
+
json=END_RUN_PAYLOAD,
|
|
386
|
+
status=200,
|
|
387
|
+
)
|
|
388
|
+
agent.start_run()
|
|
389
|
+
agent.end_run(status="completed", total_tokens=1200, total_cost=0.024)
|
|
390
|
+
last_call = responses_lib.calls[-1]
|
|
391
|
+
assert last_call.request.method == "PATCH"
|
|
392
|
+
assert last_call.request.url == f"{BASE_URL}/runs/integ-run-001"
|
|
393
|
+
|
|
394
|
+
@responses_lib.activate
|
|
395
|
+
def test_end_run_request_body(self):
|
|
396
|
+
import json
|
|
397
|
+
|
|
398
|
+
agent = self._make_agent()
|
|
399
|
+
responses_lib.add(
|
|
400
|
+
responses_lib.POST,
|
|
401
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
402
|
+
json=RUN_PAYLOAD,
|
|
403
|
+
status=201,
|
|
404
|
+
)
|
|
405
|
+
responses_lib.add(
|
|
406
|
+
responses_lib.PATCH,
|
|
407
|
+
f"{BASE_URL}/runs/integ-run-001",
|
|
408
|
+
json=END_RUN_PAYLOAD,
|
|
409
|
+
status=200,
|
|
410
|
+
)
|
|
411
|
+
agent.start_run()
|
|
412
|
+
agent.end_run(
|
|
413
|
+
status="completed",
|
|
414
|
+
total_tokens=1200,
|
|
415
|
+
total_cost=0.024,
|
|
416
|
+
metadata={"user_id": "u-42"},
|
|
417
|
+
)
|
|
418
|
+
body = json.loads(responses_lib.calls[-1].request.body)
|
|
419
|
+
assert body["status"] == "completed"
|
|
420
|
+
assert body["total_tokens"] == 1200
|
|
421
|
+
assert body["total_cost"] == 0.024
|
|
422
|
+
assert body["metadata"] == {"user_id": "u-42"}
|
|
423
|
+
|
|
424
|
+
@responses_lib.activate
|
|
425
|
+
def test_end_run_body_omits_none_fields(self):
|
|
426
|
+
import json
|
|
427
|
+
|
|
428
|
+
agent = self._make_agent()
|
|
429
|
+
responses_lib.add(
|
|
430
|
+
responses_lib.POST,
|
|
431
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
432
|
+
json=RUN_PAYLOAD,
|
|
433
|
+
status=201,
|
|
434
|
+
)
|
|
435
|
+
responses_lib.add(
|
|
436
|
+
responses_lib.PATCH,
|
|
437
|
+
f"{BASE_URL}/runs/integ-run-001",
|
|
438
|
+
json=_wrap(
|
|
439
|
+
{
|
|
440
|
+
"id": "integ-run-001",
|
|
441
|
+
"status": "failed",
|
|
442
|
+
"total_tokens": None,
|
|
443
|
+
"total_cost": None,
|
|
444
|
+
"metadata": None,
|
|
445
|
+
}
|
|
446
|
+
),
|
|
447
|
+
status=200,
|
|
448
|
+
)
|
|
449
|
+
agent.start_run()
|
|
450
|
+
agent.end_run(status="failed")
|
|
451
|
+
body = json.loads(responses_lib.calls[-1].request.body)
|
|
452
|
+
assert "total_tokens" not in body
|
|
453
|
+
assert "total_cost" not in body
|
|
454
|
+
assert "metadata" not in body
|
|
455
|
+
|
|
456
|
+
@responses_lib.activate
|
|
457
|
+
def test_end_run_updates_agent_state(self):
|
|
458
|
+
agent = self._make_agent()
|
|
459
|
+
responses_lib.add(
|
|
460
|
+
responses_lib.POST,
|
|
461
|
+
f"{BASE_URL}/agents/integ-agent-001/runs",
|
|
462
|
+
json=RUN_PAYLOAD,
|
|
463
|
+
status=201,
|
|
464
|
+
)
|
|
465
|
+
responses_lib.add(
|
|
466
|
+
responses_lib.PATCH,
|
|
467
|
+
f"{BASE_URL}/runs/integ-run-001",
|
|
468
|
+
json=END_RUN_PAYLOAD,
|
|
469
|
+
status=200,
|
|
470
|
+
)
|
|
471
|
+
agent.start_run()
|
|
472
|
+
run = agent.end_run(status="completed", total_tokens=1200, total_cost=0.024)
|
|
473
|
+
assert run.status == "completed"
|
|
474
|
+
assert run.total_tokens == 1200
|
|
475
|
+
assert run.total_cost == 0.024
|
|
476
|
+
|
|
477
|
+
@responses_lib.activate
|
|
478
|
+
def test_full_lifecycle(self):
|
|
479
|
+
"""Agent init → start_run → end_run, exactly 4 HTTP calls."""
|
|
480
|
+
register_mock_endpoints()
|
|
481
|
+
agent = Agent.init(
|
|
482
|
+
name="integ-bot",
|
|
483
|
+
owner="integ-team",
|
|
484
|
+
version="v1.0",
|
|
485
|
+
runtime="langchain",
|
|
486
|
+
description="Integration test agent",
|
|
487
|
+
default_permissions=["read:logs"],
|
|
488
|
+
permissions=["read:logs", "write:reports"],
|
|
489
|
+
changelog="Integration test version",
|
|
490
|
+
)
|
|
491
|
+
run = agent.start_run()
|
|
492
|
+
assert run.status == "running"
|
|
493
|
+
result = agent.end_run(status="completed", total_tokens=1200, total_cost=0.024)
|
|
494
|
+
assert result.status == "completed"
|
|
495
|
+
assert len(responses_lib.calls) == 4
|