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 ADDED
@@ -0,0 +1,6 @@
1
+ """Official OSVA SDK for Python."""
2
+
3
+ from osva.client import OSVAClient
4
+ from osva.errors import OSVAAPIError, OSVATransportError
5
+
6
+ __all__ = ["OSVAClient", "OSVAAPIError", "OSVATransportError"]
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."""
@@ -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")
@@ -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
+ )
@@ -0,0 +1,5 @@
1
+ """Runtime Protocol V1 helpers for REMOTE_HTTP agents."""
2
+
3
+ from osva.runtime.runtime import Runtime
4
+
5
+ __all__ = ["Runtime"]
@@ -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
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any