osva-sdk 1.0.0__py3-none-any.whl
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.
- osva/__init__.py +6 -0
- osva/client.py +45 -0
- osva/errors.py +15 -0
- osva/http_client.py +140 -0
- osva/resources/__init__.py +1 -0
- osva/resources/agents.py +26 -0
- osva/resources/approvals.py +23 -0
- osva/resources/artifacts.py +56 -0
- osva/resources/office.py +120 -0
- osva/resources/runs.py +19 -0
- osva/resources/schedules.py +16 -0
- osva/resources/workflow_runs.py +16 -0
- osva/resources/workflows.py +26 -0
- osva/runtime/__init__.py +5 -0
- osva/runtime/context.py +322 -0
- osva/runtime/credential.py +16 -0
- osva/runtime/errors.py +17 -0
- osva/runtime/runtime.py +162 -0
- osva_sdk-1.0.0.dist-info/METADATA +23 -0
- osva_sdk-1.0.0.dist-info/RECORD +21 -0
- osva_sdk-1.0.0.dist-info/WHEEL +4 -0
osva/__init__.py
ADDED
osva/client.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from osva.http_client import HTTPClient
|
|
4
|
+
from osva.resources.agents import AgentsResource
|
|
5
|
+
from osva.resources.approvals import ApprovalsResource
|
|
6
|
+
from osva.resources.artifacts import ArtifactsResource
|
|
7
|
+
from osva.resources.office import (
|
|
8
|
+
AssignmentsResource,
|
|
9
|
+
GoalsResource,
|
|
10
|
+
OfficeWorkersResource,
|
|
11
|
+
RolesResource,
|
|
12
|
+
TeamsResource,
|
|
13
|
+
)
|
|
14
|
+
from osva.resources.runs import RunsResource
|
|
15
|
+
from osva.resources.schedules import SchedulesResource
|
|
16
|
+
from osva.resources.workflow_runs import WorkflowRunsResource
|
|
17
|
+
from osva.resources.workflows import WorkflowsResource
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OSVAClient:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
base_url: str,
|
|
25
|
+
api_key: str,
|
|
26
|
+
timeout_seconds: float = 30.0,
|
|
27
|
+
client: HTTPClient | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._http = client or HTTPClient(
|
|
30
|
+
base_url,
|
|
31
|
+
api_key=api_key,
|
|
32
|
+
timeout_seconds=timeout_seconds,
|
|
33
|
+
)
|
|
34
|
+
self.agents = AgentsResource(self._http)
|
|
35
|
+
self.runs = RunsResource(self._http)
|
|
36
|
+
self.workflows = WorkflowsResource(self._http)
|
|
37
|
+
self.workflow_runs = WorkflowRunsResource(self._http)
|
|
38
|
+
self.approvals = ApprovalsResource(self._http)
|
|
39
|
+
self.schedules = SchedulesResource(self._http)
|
|
40
|
+
self.artifacts = ArtifactsResource(self._http)
|
|
41
|
+
self.office_workers = OfficeWorkersResource(self._http)
|
|
42
|
+
self.roles = RolesResource(self._http)
|
|
43
|
+
self.teams = TeamsResource(self._http)
|
|
44
|
+
self.goals = GoalsResource(self._http)
|
|
45
|
+
self.assignments = AssignmentsResource(self._http)
|
osva/errors.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OSVAAPIError(Exception):
|
|
7
|
+
def __init__(self, status: int, body: dict[str, Any]) -> None:
|
|
8
|
+
domain = body.get("status", "error")
|
|
9
|
+
super().__init__(f"OSVA API request failed with HTTP {status} ({domain}).")
|
|
10
|
+
self.status = status
|
|
11
|
+
self.body = body
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OSVATransportError(Exception):
|
|
15
|
+
pass
|
osva/http_client.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from osva.errors import OSVAAPIError, OSVATransportError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HTTPClient:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
base_url: str,
|
|
15
|
+
*,
|
|
16
|
+
api_key: str,
|
|
17
|
+
timeout_seconds: float = 30.0,
|
|
18
|
+
client: httpx.Client | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
normalized = base_url.strip().rstrip("/")
|
|
21
|
+
if not normalized:
|
|
22
|
+
raise ValueError("base_url must not be empty.")
|
|
23
|
+
trimmed_key = api_key.strip()
|
|
24
|
+
if not trimmed_key:
|
|
25
|
+
raise ValueError("api_key must not be empty.")
|
|
26
|
+
self._base_url = normalized
|
|
27
|
+
self._timeout = timeout_seconds
|
|
28
|
+
self._api_key = trimmed_key
|
|
29
|
+
self._client = client or httpx.Client(timeout=timeout_seconds)
|
|
30
|
+
|
|
31
|
+
def _auth_headers(self, headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
32
|
+
merged = dict(headers or {})
|
|
33
|
+
merged.setdefault("authorization", f"Bearer {self._api_key}")
|
|
34
|
+
return merged
|
|
35
|
+
|
|
36
|
+
def request(
|
|
37
|
+
self,
|
|
38
|
+
method: str,
|
|
39
|
+
path: str,
|
|
40
|
+
*,
|
|
41
|
+
query: dict[str, str | None] | None = None,
|
|
42
|
+
body: Any | None = None,
|
|
43
|
+
headers: dict[str, str] | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
params = {key: value for key, value in (query or {}).items() if value is not None}
|
|
46
|
+
url = f"{self._base_url}{path if path.startswith('/') else f'/{path}'}"
|
|
47
|
+
request_headers = self._auth_headers(headers)
|
|
48
|
+
if body is not None:
|
|
49
|
+
request_headers = {**request_headers, "content-type": "application/json"}
|
|
50
|
+
try:
|
|
51
|
+
response = self._client.request(
|
|
52
|
+
method,
|
|
53
|
+
url,
|
|
54
|
+
params=params,
|
|
55
|
+
json=body,
|
|
56
|
+
headers=request_headers,
|
|
57
|
+
)
|
|
58
|
+
except httpx.HTTPError as error:
|
|
59
|
+
raise OSVATransportError("OSVA API transport failed.") from error
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
payload = response.json()
|
|
63
|
+
except json.JSONDecodeError as error:
|
|
64
|
+
raise OSVATransportError("OSVA API returned a non-JSON response.") from error
|
|
65
|
+
|
|
66
|
+
if response.status_code >= 400:
|
|
67
|
+
if isinstance(payload, dict) and isinstance(payload.get("status"), str):
|
|
68
|
+
body_dict = payload
|
|
69
|
+
else:
|
|
70
|
+
body_dict = {"status": "error"}
|
|
71
|
+
raise OSVAAPIError(response.status_code, body_dict)
|
|
72
|
+
|
|
73
|
+
if not isinstance(payload, dict):
|
|
74
|
+
raise OSVATransportError("OSVA API returned an unexpected response shape.")
|
|
75
|
+
return payload
|
|
76
|
+
|
|
77
|
+
def upload_multipart(
|
|
78
|
+
self,
|
|
79
|
+
path: str,
|
|
80
|
+
*,
|
|
81
|
+
data: dict[str, str],
|
|
82
|
+
files: dict[str, tuple[str, bytes, str | None]],
|
|
83
|
+
headers: dict[str, str] | None = None,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
url = f"{self._base_url}{path if path.startswith('/') else f'/{path}'}"
|
|
86
|
+
try:
|
|
87
|
+
response = self._client.post(
|
|
88
|
+
url,
|
|
89
|
+
data=data,
|
|
90
|
+
files=files,
|
|
91
|
+
headers=self._auth_headers(headers),
|
|
92
|
+
)
|
|
93
|
+
except httpx.HTTPError as error:
|
|
94
|
+
raise OSVATransportError("OSVA API transport failed.") from error
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
payload = response.json()
|
|
98
|
+
except json.JSONDecodeError as error:
|
|
99
|
+
raise OSVATransportError("OSVA API returned a non-JSON response.") from error
|
|
100
|
+
|
|
101
|
+
if response.status_code >= 400:
|
|
102
|
+
if isinstance(payload, dict) and isinstance(payload.get("status"), str):
|
|
103
|
+
body_dict = payload
|
|
104
|
+
else:
|
|
105
|
+
body_dict = {"status": "error"}
|
|
106
|
+
raise OSVAAPIError(response.status_code, body_dict)
|
|
107
|
+
|
|
108
|
+
if not isinstance(payload, dict):
|
|
109
|
+
raise OSVATransportError("OSVA API returned an unexpected response shape.")
|
|
110
|
+
return payload
|
|
111
|
+
|
|
112
|
+
def download(
|
|
113
|
+
self,
|
|
114
|
+
path: str,
|
|
115
|
+
*,
|
|
116
|
+
query: dict[str, str | None] | None = None,
|
|
117
|
+
) -> httpx.Response:
|
|
118
|
+
params = {key: value for key, value in (query or {}).items() if value is not None}
|
|
119
|
+
url = f"{self._base_url}{path if path.startswith('/') else f'/{path}'}"
|
|
120
|
+
try:
|
|
121
|
+
response = self._client.get(
|
|
122
|
+
url,
|
|
123
|
+
params=params,
|
|
124
|
+
headers=self._auth_headers(),
|
|
125
|
+
)
|
|
126
|
+
except httpx.HTTPError as error:
|
|
127
|
+
raise OSVATransportError("OSVA API transport failed.") from error
|
|
128
|
+
|
|
129
|
+
if response.status_code >= 400:
|
|
130
|
+
try:
|
|
131
|
+
payload = response.json()
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
payload = {"status": "error"}
|
|
134
|
+
if isinstance(payload, dict) and isinstance(payload.get("status"), str):
|
|
135
|
+
body_dict = payload
|
|
136
|
+
else:
|
|
137
|
+
body_dict = {"status": "error"}
|
|
138
|
+
raise OSVAAPIError(response.status_code, body_dict)
|
|
139
|
+
|
|
140
|
+
return response
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Control-plane resource clients."""
|
osva/resources/agents.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("GET", "/v1/agents")
|
|
14
|
+
|
|
15
|
+
def get(self, agent_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/agents/{agent_id}")
|
|
17
|
+
|
|
18
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request("POST", "/v1/agents", body=payload)
|
|
20
|
+
|
|
21
|
+
def create_version(self, agent_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
return self._client.request(
|
|
23
|
+
"POST",
|
|
24
|
+
f"/v1/agents/{agent_id}/versions",
|
|
25
|
+
body=payload,
|
|
26
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApprovalsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def get(self, approval_request_id: str) -> dict[str, Any]:
|
|
13
|
+
return self._client.request(
|
|
14
|
+
"GET",
|
|
15
|
+
f"/v1/approval-requests/{approval_request_id}",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def decide(self, approval_request_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request(
|
|
20
|
+
"POST",
|
|
21
|
+
f"/v1/approval-requests/{approval_request_id}/decision",
|
|
22
|
+
body=payload,
|
|
23
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from osva.http_client import HTTPClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArtifactsResource:
|
|
11
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
12
|
+
self._http = http
|
|
13
|
+
|
|
14
|
+
def list(self, **query: str | None) -> dict[str, Any]:
|
|
15
|
+
return self._http.request("GET", "/v1/artifacts", query=query)
|
|
16
|
+
|
|
17
|
+
def get(self, artifact_id: str) -> dict[str, Any]:
|
|
18
|
+
return self._http.request("GET", f"/v1/artifacts/{artifact_id}")
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
name: str,
|
|
24
|
+
content: bytes,
|
|
25
|
+
media_type: str | None = None,
|
|
26
|
+
metadata: dict[str, Any] | None = None,
|
|
27
|
+
expected_digest: str | None = None,
|
|
28
|
+
idempotency_key: str | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
data: dict[str, str] = {"name": name}
|
|
31
|
+
if media_type is not None:
|
|
32
|
+
data["mediaType"] = media_type
|
|
33
|
+
if metadata is not None:
|
|
34
|
+
data["metadata"] = __import__("json").dumps(metadata)
|
|
35
|
+
if expected_digest is not None:
|
|
36
|
+
data["expectedDigest"] = expected_digest
|
|
37
|
+
if idempotency_key is not None:
|
|
38
|
+
data["idempotencyKey"] = idempotency_key
|
|
39
|
+
|
|
40
|
+
files: dict[str, tuple[str, bytes, str | None]] = {
|
|
41
|
+
"file": (
|
|
42
|
+
name,
|
|
43
|
+
content,
|
|
44
|
+
media_type or "application/octet-stream",
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key is not None else None
|
|
48
|
+
return self._http.upload_multipart(
|
|
49
|
+
"/v1/artifacts",
|
|
50
|
+
data=data,
|
|
51
|
+
files=files,
|
|
52
|
+
headers=headers,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def download(self, artifact_id: str) -> httpx.Response:
|
|
56
|
+
return self._http.download(f"/v1/artifacts/{artifact_id}/content")
|
osva/resources/office.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OfficeWorkersResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("GET", "/v1/office/workers")
|
|
14
|
+
|
|
15
|
+
def get(self, office_worker_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/office-workers/{office_worker_id}")
|
|
17
|
+
|
|
18
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request("POST", "/v1/office-workers", body=payload)
|
|
20
|
+
|
|
21
|
+
def update(self, office_worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
return self._client.request(
|
|
23
|
+
"PATCH",
|
|
24
|
+
f"/v1/office-workers/{office_worker_id}",
|
|
25
|
+
body=payload,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RolesResource:
|
|
30
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
31
|
+
self._client = client
|
|
32
|
+
|
|
33
|
+
def list(self) -> dict[str, Any]:
|
|
34
|
+
return self._client.request("GET", "/v1/office/roles")
|
|
35
|
+
|
|
36
|
+
def get(self, role_id: str) -> dict[str, Any]:
|
|
37
|
+
return self._client.request("GET", f"/v1/roles/{role_id}")
|
|
38
|
+
|
|
39
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
40
|
+
return self._client.request("POST", "/v1/roles", body=payload)
|
|
41
|
+
|
|
42
|
+
def update(self, role_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
return self._client.request("PATCH", f"/v1/roles/{role_id}", body=payload)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TeamsResource:
|
|
47
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
48
|
+
self._client = client
|
|
49
|
+
|
|
50
|
+
def list(self) -> dict[str, Any]:
|
|
51
|
+
return self._client.request("GET", "/v1/office/teams")
|
|
52
|
+
|
|
53
|
+
def get(self, team_id: str) -> dict[str, Any]:
|
|
54
|
+
return self._client.request("GET", f"/v1/teams/{team_id}")
|
|
55
|
+
|
|
56
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
return self._client.request("POST", "/v1/teams", body=payload)
|
|
58
|
+
|
|
59
|
+
def update(self, team_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
return self._client.request("PATCH", f"/v1/teams/{team_id}", body=payload)
|
|
61
|
+
|
|
62
|
+
def list_memberships(self, team_id: str) -> dict[str, Any]:
|
|
63
|
+
return self._client.request("GET", f"/v1/teams/{team_id}/memberships")
|
|
64
|
+
|
|
65
|
+
def add_membership(self, team_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
66
|
+
return self._client.request(
|
|
67
|
+
"POST",
|
|
68
|
+
f"/v1/teams/{team_id}/memberships",
|
|
69
|
+
body=payload,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GoalsResource:
|
|
74
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
75
|
+
self._client = client
|
|
76
|
+
|
|
77
|
+
def list(self) -> dict[str, Any]:
|
|
78
|
+
return self._client.request("GET", "/v1/office/goals")
|
|
79
|
+
|
|
80
|
+
def get(self, goal_id: str) -> dict[str, Any]:
|
|
81
|
+
return self._client.request("GET", f"/v1/goals/{goal_id}")
|
|
82
|
+
|
|
83
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
84
|
+
return self._client.request("POST", "/v1/goals", body=payload)
|
|
85
|
+
|
|
86
|
+
def update(self, goal_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
87
|
+
return self._client.request("PATCH", f"/v1/goals/{goal_id}", body=payload)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AssignmentsResource:
|
|
91
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
92
|
+
self._client = client
|
|
93
|
+
|
|
94
|
+
def list(self) -> dict[str, Any]:
|
|
95
|
+
return self._client.request("GET", "/v1/office/assignments")
|
|
96
|
+
|
|
97
|
+
def get(self, assignment_id: str) -> dict[str, Any]:
|
|
98
|
+
return self._client.request("GET", f"/v1/assignments/{assignment_id}")
|
|
99
|
+
|
|
100
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
101
|
+
return self._client.request("POST", "/v1/assignments", body=payload)
|
|
102
|
+
|
|
103
|
+
def update(self, assignment_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
104
|
+
return self._client.request(
|
|
105
|
+
"PATCH",
|
|
106
|
+
f"/v1/assignments/{assignment_id}",
|
|
107
|
+
body=payload,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def launch(self, assignment_id: str) -> dict[str, Any]:
|
|
111
|
+
return self._client.request(
|
|
112
|
+
"POST",
|
|
113
|
+
f"/v1/assignments/{assignment_id}/launch",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def cancel(self, assignment_id: str) -> dict[str, Any]:
|
|
117
|
+
return self._client.request(
|
|
118
|
+
"POST",
|
|
119
|
+
f"/v1/assignments/{assignment_id}/cancel",
|
|
120
|
+
)
|
osva/resources/runs.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RunsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self, **query: str | None) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("GET", "/v1/runs", query=query)
|
|
14
|
+
|
|
15
|
+
def get(self, run_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/runs/{run_id}")
|
|
17
|
+
|
|
18
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request("POST", "/v1/runs", body=payload)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchedulesResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self, **query: str | None) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("GET", "/v1/schedules", query=query)
|
|
14
|
+
|
|
15
|
+
def get(self, schedule_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/schedules/{schedule_id}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkflowRunsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("POST", "/v1/workflow-runs", body=payload)
|
|
14
|
+
|
|
15
|
+
def get(self, workflow_run_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/workflow-runs/{workflow_run_id}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkflowsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("GET", "/v1/workflows")
|
|
14
|
+
|
|
15
|
+
def get(self, workflow_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/workflows/{workflow_id}")
|
|
17
|
+
|
|
18
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request("POST", "/v1/workflows", body=payload)
|
|
20
|
+
|
|
21
|
+
def create_version(self, workflow_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
return self._client.request(
|
|
23
|
+
"POST",
|
|
24
|
+
f"/v1/workflows/{workflow_id}/versions",
|
|
25
|
+
body=payload,
|
|
26
|
+
)
|
osva/runtime/__init__.py
ADDED
osva/runtime/context.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from osva.runtime.credential import CapabilityCredential
|
|
8
|
+
from osva.runtime.errors import RuntimeCapabilityError
|
|
9
|
+
|
|
10
|
+
PROTOCOL_VERSION = "1"
|
|
11
|
+
GENERATE_TEXT_PATH = "/v1/runtime/capabilities/models/generate-text"
|
|
12
|
+
INVOKE_TOOL_PATH = "/v1/runtime/capabilities/tools/invoke"
|
|
13
|
+
KNOWLEDGE_SEARCH_PATH = "/v1/runtime/capabilities/knowledge/search"
|
|
14
|
+
ARTIFACT_GET_PATH = "/v1/runtime/capabilities/artifacts/get"
|
|
15
|
+
ARTIFACT_CREATE_PATH = "/v1/runtime/capabilities/artifacts/create"
|
|
16
|
+
ARTIFACT_CONTENT_PATH = "/v1/runtime/capabilities/artifacts/content"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelsCapability:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
execution_id: str,
|
|
24
|
+
credential: CapabilityCredential,
|
|
25
|
+
client: httpx.AsyncClient,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._execution_id = execution_id
|
|
28
|
+
self._credential = credential
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
async def generate_text(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
binding: str,
|
|
35
|
+
messages: list[dict[str, str]],
|
|
36
|
+
max_output_tokens: int | None = None,
|
|
37
|
+
) -> dict[str, str]:
|
|
38
|
+
payload: dict[str, Any] = {
|
|
39
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
40
|
+
"executionId": self._execution_id,
|
|
41
|
+
"bindingName": binding,
|
|
42
|
+
"input": {"messages": messages},
|
|
43
|
+
}
|
|
44
|
+
if max_output_tokens is not None:
|
|
45
|
+
payload["input"]["maxOutputTokens"] = max_output_tokens
|
|
46
|
+
response = await self._post(GENERATE_TEXT_PATH, payload)
|
|
47
|
+
if response.get("outcome") != "SUCCEEDED":
|
|
48
|
+
error = response.get("error", {})
|
|
49
|
+
raise RuntimeCapabilityError(str(error.get("message", "Model capability failed.")))
|
|
50
|
+
result = response.get("result")
|
|
51
|
+
if not isinstance(result, dict) or not isinstance(result.get("text"), str):
|
|
52
|
+
raise RuntimeCapabilityError("Invalid model capability response.")
|
|
53
|
+
return {"text": result["text"]}
|
|
54
|
+
|
|
55
|
+
async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
56
|
+
try:
|
|
57
|
+
response = await self._client.post(
|
|
58
|
+
f"{self._credential.endpoint}{path}",
|
|
59
|
+
json=payload,
|
|
60
|
+
headers={
|
|
61
|
+
"authorization": self._credential.authorization_header(),
|
|
62
|
+
"content-type": "application/json",
|
|
63
|
+
"accept": "application/json",
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
except httpx.HTTPError as error:
|
|
67
|
+
raise RuntimeCapabilityError("Capability transport failed.") from error
|
|
68
|
+
data = response.json()
|
|
69
|
+
if not isinstance(data, dict):
|
|
70
|
+
raise RuntimeCapabilityError("Capability response was not an object.")
|
|
71
|
+
if data.get("executionId") != self._execution_id:
|
|
72
|
+
raise RuntimeCapabilityError("Capability response executionId mismatch.")
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ToolsCapability:
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
execution_id: str,
|
|
81
|
+
credential: CapabilityCredential,
|
|
82
|
+
client: httpx.AsyncClient,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._execution_id = execution_id
|
|
85
|
+
self._credential = credential
|
|
86
|
+
self._client = client
|
|
87
|
+
|
|
88
|
+
async def invoke(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
binding: str,
|
|
92
|
+
input: Any,
|
|
93
|
+
idempotency_key: str | None = None,
|
|
94
|
+
) -> Any:
|
|
95
|
+
payload: dict[str, Any] = {
|
|
96
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
97
|
+
"executionId": self._execution_id,
|
|
98
|
+
"bindingName": binding,
|
|
99
|
+
"input": input,
|
|
100
|
+
}
|
|
101
|
+
if idempotency_key is not None:
|
|
102
|
+
payload["idempotencyKey"] = idempotency_key
|
|
103
|
+
response = await self._post(INVOKE_TOOL_PATH, payload)
|
|
104
|
+
if response.get("outcome") != "SUCCEEDED":
|
|
105
|
+
error = response.get("error", {})
|
|
106
|
+
raise RuntimeCapabilityError(str(error.get("message", "Tool capability failed.")))
|
|
107
|
+
return response.get("output")
|
|
108
|
+
|
|
109
|
+
async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
110
|
+
try:
|
|
111
|
+
response = await self._client.post(
|
|
112
|
+
f"{self._credential.endpoint}{path}",
|
|
113
|
+
json=payload,
|
|
114
|
+
headers={
|
|
115
|
+
"authorization": self._credential.authorization_header(),
|
|
116
|
+
"content-type": "application/json",
|
|
117
|
+
"accept": "application/json",
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
except httpx.HTTPError as error:
|
|
121
|
+
raise RuntimeCapabilityError("Capability transport failed.") from error
|
|
122
|
+
data = response.json()
|
|
123
|
+
if not isinstance(data, dict):
|
|
124
|
+
raise RuntimeCapabilityError("Capability response was not an object.")
|
|
125
|
+
if data.get("executionId") != self._execution_id:
|
|
126
|
+
raise RuntimeCapabilityError("Capability response executionId mismatch.")
|
|
127
|
+
return data
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ArtifactsCapability:
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
execution_id: str,
|
|
135
|
+
credential: CapabilityCredential,
|
|
136
|
+
client: httpx.AsyncClient,
|
|
137
|
+
) -> None:
|
|
138
|
+
self._execution_id = execution_id
|
|
139
|
+
self._credential = credential
|
|
140
|
+
self._client = client
|
|
141
|
+
|
|
142
|
+
async def get(self, *, artifact_id: str) -> dict[str, Any]:
|
|
143
|
+
payload = {
|
|
144
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
145
|
+
"executionId": self._execution_id,
|
|
146
|
+
"artifactId": artifact_id,
|
|
147
|
+
}
|
|
148
|
+
response = await self._post_json(ARTIFACT_GET_PATH, payload)
|
|
149
|
+
if response.get("outcome") != "SUCCEEDED":
|
|
150
|
+
error = response.get("error", {})
|
|
151
|
+
raise RuntimeCapabilityError(str(error.get("message", "Artifact get failed.")))
|
|
152
|
+
artifact = response.get("artifact")
|
|
153
|
+
if not isinstance(artifact, dict):
|
|
154
|
+
raise RuntimeCapabilityError("Invalid artifact capability response.")
|
|
155
|
+
return artifact
|
|
156
|
+
|
|
157
|
+
async def create(
|
|
158
|
+
self,
|
|
159
|
+
*,
|
|
160
|
+
name: str,
|
|
161
|
+
content: bytes,
|
|
162
|
+
media_type: str | None = None,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
files = {"file": (name, content, media_type or "application/octet-stream")}
|
|
165
|
+
data = {
|
|
166
|
+
"executionId": self._execution_id,
|
|
167
|
+
"name": name,
|
|
168
|
+
}
|
|
169
|
+
if media_type is not None:
|
|
170
|
+
data["mediaType"] = media_type
|
|
171
|
+
try:
|
|
172
|
+
response = await self._client.post(
|
|
173
|
+
f"{self._credential.endpoint}{ARTIFACT_CREATE_PATH}",
|
|
174
|
+
data=data,
|
|
175
|
+
files=files,
|
|
176
|
+
headers={
|
|
177
|
+
"authorization": self._credential.authorization_header(),
|
|
178
|
+
"accept": "application/json",
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
except httpx.HTTPError as error:
|
|
182
|
+
raise RuntimeCapabilityError("Capability transport failed.") from error
|
|
183
|
+
body = response.json()
|
|
184
|
+
if not isinstance(body, dict):
|
|
185
|
+
raise RuntimeCapabilityError("Capability response was not an object.")
|
|
186
|
+
if body.get("outcome") != "SUCCEEDED":
|
|
187
|
+
capability_error = body.get("error", {})
|
|
188
|
+
if not isinstance(capability_error, dict):
|
|
189
|
+
capability_error = {}
|
|
190
|
+
raise RuntimeCapabilityError(
|
|
191
|
+
str(capability_error.get("message", "Artifact create failed."))
|
|
192
|
+
)
|
|
193
|
+
artifact = body.get("artifact")
|
|
194
|
+
if not isinstance(artifact, dict):
|
|
195
|
+
raise RuntimeCapabilityError("Invalid artifact capability response.")
|
|
196
|
+
return artifact
|
|
197
|
+
|
|
198
|
+
async def open(self, *, artifact_id: str) -> httpx.Response:
|
|
199
|
+
url = (
|
|
200
|
+
f"{self._credential.endpoint}{ARTIFACT_CONTENT_PATH}"
|
|
201
|
+
f"?executionId={self._execution_id}&artifactId={artifact_id}"
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
return await self._client.get(
|
|
205
|
+
url,
|
|
206
|
+
headers={"authorization": self._credential.authorization_header()},
|
|
207
|
+
)
|
|
208
|
+
except httpx.HTTPError as error:
|
|
209
|
+
raise RuntimeCapabilityError("Capability transport failed.") from error
|
|
210
|
+
|
|
211
|
+
async def _post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
212
|
+
try:
|
|
213
|
+
response = await self._client.post(
|
|
214
|
+
f"{self._credential.endpoint}{path}",
|
|
215
|
+
json=payload,
|
|
216
|
+
headers={
|
|
217
|
+
"authorization": self._credential.authorization_header(),
|
|
218
|
+
"content-type": "application/json",
|
|
219
|
+
"accept": "application/json",
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
except httpx.HTTPError as error:
|
|
223
|
+
raise RuntimeCapabilityError("Capability transport failed.") from error
|
|
224
|
+
data = response.json()
|
|
225
|
+
if not isinstance(data, dict):
|
|
226
|
+
raise RuntimeCapabilityError("Capability response was not an object.")
|
|
227
|
+
if data.get("executionId") != self._execution_id:
|
|
228
|
+
raise RuntimeCapabilityError("Capability response executionId mismatch.")
|
|
229
|
+
return data
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class KnowledgeCapability:
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
execution_id: str,
|
|
237
|
+
credential: CapabilityCredential,
|
|
238
|
+
client: httpx.AsyncClient,
|
|
239
|
+
) -> None:
|
|
240
|
+
self._execution_id = execution_id
|
|
241
|
+
self._credential = credential
|
|
242
|
+
self._client = client
|
|
243
|
+
|
|
244
|
+
async def search(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
binding: str,
|
|
248
|
+
query: str,
|
|
249
|
+
top_k: int | None = None,
|
|
250
|
+
filter: dict[str, object] | None = None,
|
|
251
|
+
) -> list[dict[str, object]]:
|
|
252
|
+
payload: dict[str, Any] = {
|
|
253
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
254
|
+
"executionId": self._execution_id,
|
|
255
|
+
"bindingName": binding,
|
|
256
|
+
"query": query,
|
|
257
|
+
}
|
|
258
|
+
if top_k is not None:
|
|
259
|
+
payload["topK"] = top_k
|
|
260
|
+
if filter is not None:
|
|
261
|
+
payload["filter"] = filter
|
|
262
|
+
response = await self._post(KNOWLEDGE_SEARCH_PATH, payload)
|
|
263
|
+
if response.get("outcome") != "SUCCEEDED":
|
|
264
|
+
error = response.get("error", {})
|
|
265
|
+
raise RuntimeCapabilityError(
|
|
266
|
+
str(error.get("message", "Knowledge search failed.")),
|
|
267
|
+
)
|
|
268
|
+
hits = response.get("hits")
|
|
269
|
+
if not isinstance(hits, list):
|
|
270
|
+
raise RuntimeCapabilityError("Invalid knowledge capability response.")
|
|
271
|
+
return hits
|
|
272
|
+
|
|
273
|
+
async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
274
|
+
try:
|
|
275
|
+
response = await self._client.post(
|
|
276
|
+
f"{self._credential.endpoint}{path}",
|
|
277
|
+
json=payload,
|
|
278
|
+
headers={
|
|
279
|
+
"authorization": self._credential.authorization_header(),
|
|
280
|
+
"content-type": "application/json",
|
|
281
|
+
"accept": "application/json",
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
except httpx.HTTPError as error:
|
|
285
|
+
raise RuntimeCapabilityError("Capability transport failed.") from error
|
|
286
|
+
data = response.json()
|
|
287
|
+
if not isinstance(data, dict):
|
|
288
|
+
raise RuntimeCapabilityError("Capability response was not an object.")
|
|
289
|
+
if data.get("executionId") != self._execution_id:
|
|
290
|
+
raise RuntimeCapabilityError("Capability response executionId mismatch.")
|
|
291
|
+
return data
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class RuntimeContext:
|
|
295
|
+
def __init__(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
execution_id: str,
|
|
299
|
+
credential: CapabilityCredential,
|
|
300
|
+
client: httpx.AsyncClient,
|
|
301
|
+
) -> None:
|
|
302
|
+
self.execution_id = execution_id
|
|
303
|
+
self.models = ModelsCapability(
|
|
304
|
+
execution_id=execution_id,
|
|
305
|
+
credential=credential,
|
|
306
|
+
client=client,
|
|
307
|
+
)
|
|
308
|
+
self.tools = ToolsCapability(
|
|
309
|
+
execution_id=execution_id,
|
|
310
|
+
credential=credential,
|
|
311
|
+
client=client,
|
|
312
|
+
)
|
|
313
|
+
self.artifacts = ArtifactsCapability(
|
|
314
|
+
execution_id=execution_id,
|
|
315
|
+
credential=credential,
|
|
316
|
+
client=client,
|
|
317
|
+
)
|
|
318
|
+
self.knowledge = KnowledgeCapability(
|
|
319
|
+
execution_id=execution_id,
|
|
320
|
+
credential=credential,
|
|
321
|
+
client=client,
|
|
322
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CapabilityCredential:
|
|
5
|
+
def __init__(self, endpoint: str, token: str) -> None:
|
|
6
|
+
self.endpoint = endpoint.rstrip("/")
|
|
7
|
+
self._token = token
|
|
8
|
+
|
|
9
|
+
def authorization_header(self) -> str:
|
|
10
|
+
return f"Bearer {self._token}"
|
|
11
|
+
|
|
12
|
+
def __repr__(self) -> str:
|
|
13
|
+
return f"CapabilityCredential({self.endpoint!r})"
|
|
14
|
+
|
|
15
|
+
def to_json(self) -> dict[str, str]:
|
|
16
|
+
return {"endpoint": self.endpoint, "token": "[REDACTED]"}
|
osva/runtime/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RuntimeExecutionError(Exception):
|
|
5
|
+
def __init__(self, message: str, code: str = "AGENT_EXECUTION_FAILED") -> None:
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.code = code
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuntimeCapabilityError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RuntimeProtocolError(Exception):
|
|
15
|
+
def __init__(self, message: str, code: str = "RUNTIME_PROTOCOL_FAILURE") -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.code = code
|
osva/runtime/runtime.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from osva.runtime.context import RuntimeContext
|
|
10
|
+
from osva.runtime.credential import CapabilityCredential
|
|
11
|
+
from osva.runtime.errors import RuntimeExecutionError, RuntimeProtocolError
|
|
12
|
+
|
|
13
|
+
PROTOCOL_VERSION = "1"
|
|
14
|
+
MAX_BODY_BYTES = 1_048_576
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ExecuteFn = Callable[[Any, RuntimeContext], Awaitable[Any]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Runtime:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._execute_fn: ExecuteFn | None = None
|
|
23
|
+
|
|
24
|
+
def execute(self, fn: ExecuteFn) -> ExecuteFn:
|
|
25
|
+
self._execute_fn = fn
|
|
26
|
+
return fn
|
|
27
|
+
|
|
28
|
+
async def handle_execute_request(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
29
|
+
if body.get("protocolVersion") != PROTOCOL_VERSION:
|
|
30
|
+
raise RuntimeProtocolError(
|
|
31
|
+
f"Unsupported protocol version: {body.get('protocolVersion')!r}."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
required = ("executionId", "input", "capabilities")
|
|
35
|
+
if any(key not in body for key in required):
|
|
36
|
+
raise RuntimeProtocolError("Invalid Runtime Protocol V1 execute request.")
|
|
37
|
+
|
|
38
|
+
execution_id = str(body["executionId"])
|
|
39
|
+
capabilities = body["capabilities"]
|
|
40
|
+
if not isinstance(capabilities, dict):
|
|
41
|
+
raise RuntimeProtocolError("Invalid capability credential in execute request.")
|
|
42
|
+
|
|
43
|
+
endpoint = capabilities.get("endpoint")
|
|
44
|
+
token = capabilities.get("token")
|
|
45
|
+
if not isinstance(endpoint, str) or not isinstance(token, str):
|
|
46
|
+
raise RuntimeProtocolError("Invalid capability credential in execute request.")
|
|
47
|
+
|
|
48
|
+
if self._execute_fn is None:
|
|
49
|
+
raise RuntimeProtocolError("Runtime execute handler is not configured.")
|
|
50
|
+
|
|
51
|
+
credential = CapabilityCredential(endpoint, token)
|
|
52
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
53
|
+
context = RuntimeContext(
|
|
54
|
+
execution_id=execution_id,
|
|
55
|
+
credential=credential,
|
|
56
|
+
client=client,
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
output = await self._execute_fn(body["input"], context)
|
|
60
|
+
return {
|
|
61
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
62
|
+
"executionId": execution_id,
|
|
63
|
+
"outcome": "SUCCEEDED",
|
|
64
|
+
"output": output,
|
|
65
|
+
}
|
|
66
|
+
except RuntimeExecutionError as error:
|
|
67
|
+
return {
|
|
68
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
69
|
+
"executionId": execution_id,
|
|
70
|
+
"outcome": "FAILED",
|
|
71
|
+
"error": {"code": error.code, "message": str(error)[:500]},
|
|
72
|
+
}
|
|
73
|
+
except Exception as error: # noqa: BLE001
|
|
74
|
+
return {
|
|
75
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
76
|
+
"executionId": execution_id,
|
|
77
|
+
"outcome": "FAILED",
|
|
78
|
+
"error": {
|
|
79
|
+
"code": "AGENT_EXECUTION_FAILED",
|
|
80
|
+
"message": str(error)[:500] or "Agent execution failed.",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def asgi_app(self, path: str = "/execute") -> Callable[..., Awaitable[None]]:
|
|
85
|
+
async def app(scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
86
|
+
if scope.get("type") != "http":
|
|
87
|
+
await send({"type": "http.response.start", "status": 400, "headers": []})
|
|
88
|
+
await send({"type": "http.response.body", "body": b""})
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
request_path = scope.get("path", "")
|
|
92
|
+
method = scope.get("method", "GET")
|
|
93
|
+
if method != "POST" or request_path != path:
|
|
94
|
+
await _send_json(send, 404, _protocol_failure("Not found."))
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
body_bytes = b""
|
|
98
|
+
while True:
|
|
99
|
+
message = await receive()
|
|
100
|
+
if message["type"] != "http.request":
|
|
101
|
+
continue
|
|
102
|
+
body_bytes += message.get("body", b"")
|
|
103
|
+
if len(body_bytes) > MAX_BODY_BYTES:
|
|
104
|
+
await _send_json(
|
|
105
|
+
send, 413, _protocol_failure("Request body exceeds maximum size.")
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
if not message.get("more_body", False):
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
parsed = json.loads(body_bytes.decode("utf-8"))
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
await _send_json(send, 400, _protocol_failure("Request body must be valid JSON."))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if not isinstance(parsed, dict):
|
|
118
|
+
await _send_json(
|
|
119
|
+
send, 400, _protocol_failure("Request body must be a JSON object.")
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
result = await self.handle_execute_request(parsed)
|
|
125
|
+
except RuntimeProtocolError as error:
|
|
126
|
+
await _send_json(
|
|
127
|
+
send,
|
|
128
|
+
400,
|
|
129
|
+
{
|
|
130
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
131
|
+
"outcome": "FAILED",
|
|
132
|
+
"error": {"code": error.code, "message": str(error)},
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
await _send_json(send, 200, result)
|
|
138
|
+
|
|
139
|
+
return app
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def _send_json(send: Any, status: int, payload: dict[str, Any]) -> None:
|
|
143
|
+
body = json.dumps(payload).encode("utf-8")
|
|
144
|
+
await send(
|
|
145
|
+
{
|
|
146
|
+
"type": "http.response.start",
|
|
147
|
+
"status": status,
|
|
148
|
+
"headers": [
|
|
149
|
+
(b"content-type", b"application/json; charset=utf-8"),
|
|
150
|
+
(b"content-length", str(len(body)).encode("ascii")),
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
await send({"type": "http.response.body", "body": body})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _protocol_failure(message: str) -> dict[str, Any]:
|
|
158
|
+
return {
|
|
159
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
160
|
+
"outcome": "FAILED",
|
|
161
|
+
"error": {"code": "RUNTIME_PROTOCOL_FAILURE", "message": message},
|
|
162
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: osva-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official OSVA SDK for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/Osva-AI/osva
|
|
6
|
+
Project-URL: Repository, https://github.com/Osva-AI/osva
|
|
7
|
+
Project-URL: Documentation, https://github.com/Osva-AI/osva/blob/main/docs/OSS-1.0-QUICKSTART.md
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# osva-sdk
|
|
20
|
+
|
|
21
|
+
Official OSVA SDK for Python (>=3.11).
|
|
22
|
+
|
|
23
|
+
The package is publish-ready but not yet published to PyPI.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
osva/__init__.py,sha256=Y--tLLPYcvM_on4Hd0CnE7QIJ8y8CfADsB4ZNAwHtnU,193
|
|
2
|
+
osva/client.py,sha256=1A19cAVGzbSwlfjR76fRRBmD1Vs3pwwIy5HAcTWUZdM,1592
|
|
3
|
+
osva/errors.py,sha256=Y307QGbAWJ-QWTFFprSC8KpzuV8IeXcs0Abz91EE9v0,390
|
|
4
|
+
osva/http_client.py,sha256=9EfeANBEbMolSRuim2iqxgpGmIiGC9NL73fcGSdqix0,4880
|
|
5
|
+
osva/resources/__init__.py,sha256=WLGHEK54aeJqjJToThn6fi-BJbdMShp9KcZGOpGTlNQ,38
|
|
6
|
+
osva/resources/agents.py,sha256=8IIU8wGmAXsJSxVVzejGy_mr49k390Z8tSznCI_EC_c,791
|
|
7
|
+
osva/resources/approvals.py,sha256=fOaCxCUkebX4GgmR6dALt_Fx91nwILstRS2bRErNv0o,653
|
|
8
|
+
osva/resources/artifacts.py,sha256=zroDZiMdKQeLFfe9smLJsvWjUBu9lbURw1iPqgQLbIY,1775
|
|
9
|
+
osva/resources/office.py,sha256=7bZ_phdZvrizwt54lRm5H7AEEG8Ox7Ke4witrdYcXqc,4190
|
|
10
|
+
osva/resources/runs.py,sha256=3wtpvTzVrJuNklCcUxSHxiv-iEVgxM-V1dQOLJsPAcE,584
|
|
11
|
+
osva/resources/schedules.py,sha256=sg9R4kVwvtjKSCssX7794LkUZiVcJ4fMOiCOLGcH_oQ,473
|
|
12
|
+
osva/resources/workflow_runs.py,sha256=a7okJ9pGSZEZoN4dQyIYKuvLPkiwvznWVg2V8Qc-Lek,500
|
|
13
|
+
osva/resources/workflows.py,sha256=-4BGIwCqwkvdoStWnq2Jv03Y3adNpHMhvZ5z7T8DqmI,818
|
|
14
|
+
osva/runtime/__init__.py,sha256=6qBNiytFGTGoIbxkS4p-kSZl1x7WXiSjzEw9bJrgbJk,123
|
|
15
|
+
osva/runtime/context.py,sha256=CTzLY7WhlX-UheZ-Q-psdUj7UItU3wYqy7FApjK7MIM,11878
|
|
16
|
+
osva/runtime/credential.py,sha256=sRHfFxtGqYQgLKI6NM7T5MqD5fNme3t6sEuKzlIR9jA,478
|
|
17
|
+
osva/runtime/errors.py,sha256=0W_k2PXZ4Ymb-pqEK0pe3C7XnwB2--hKoJ9zkkbaiBA,458
|
|
18
|
+
osva/runtime/runtime.py,sha256=Xg0rwpza-63WPkQyf2O56bMgdw6Yji2aLoLi9fRNI94,6001
|
|
19
|
+
osva_sdk-1.0.0.dist-info/METADATA,sha256=x8D-n-F7MC4E6_up5mqtSx8snnid5TVtQxpb3stWcY8,774
|
|
20
|
+
osva_sdk-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
21
|
+
osva_sdk-1.0.0.dist-info/RECORD,,
|