tryvira-sdk 0.1.1__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.
- tryvira_sdk-0.1.1/PKG-INFO +66 -0
- tryvira_sdk-0.1.1/README.md +52 -0
- tryvira_sdk-0.1.1/agent_native.py +63 -0
- tryvira_sdk-0.1.1/digest.py +10 -0
- tryvira_sdk-0.1.1/execution_boundary_v2.py +48 -0
- tryvira_sdk-0.1.1/integration.py +51 -0
- tryvira_sdk-0.1.1/observation_v2.py +26 -0
- tryvira_sdk-0.1.1/pyproject.toml +27 -0
- tryvira_sdk-0.1.1/runtime.py +60 -0
- tryvira_sdk-0.1.1/runtime_v2.py +60 -0
- tryvira_sdk-0.1.1/setup.cfg +4 -0
- tryvira_sdk-0.1.1/tryvira_sdk.egg-info/PKG-INFO +66 -0
- tryvira_sdk-0.1.1/tryvira_sdk.egg-info/SOURCES.txt +16 -0
- tryvira_sdk-0.1.1/tryvira_sdk.egg-info/dependency_links.txt +1 -0
- tryvira_sdk-0.1.1/tryvira_sdk.egg-info/top_level.txt +9 -0
- tryvira_sdk-0.1.1/vira/__init__.py +32 -0
- tryvira_sdk-0.1.1/vira/py.typed +0 -0
- tryvira_sdk-0.1.1/vira_client.py +386 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tryvira-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Typed Python client for the Vira authority boundary
|
|
5
|
+
Author: Vira
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/hrpering/vira_beh_redesign
|
|
8
|
+
Project-URL: Documentation, https://github.com/hrpering/vira_beh_redesign/tree/main/sdks/python
|
|
9
|
+
Keywords: ai-governance,observability,agent-runtime
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Vira Python SDK
|
|
16
|
+
|
|
17
|
+
Package name: `tryvira-sdk`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m pip install tryvira-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For a repository checkout use `python -m pip install ./sdks/python`. The client is a thin
|
|
26
|
+
HTTP client of Vira's authority boundary; it does not execute tools locally.
|
|
27
|
+
|
|
28
|
+
For maintainers, build with `python -m build sdks/python`. Public CI uses PyPI Trusted
|
|
29
|
+
Publishing; do not put a token in this repository or in an example.
|
|
30
|
+
|
|
31
|
+
The SDK is a thin client of the Vira authority boundary. It does not execute
|
|
32
|
+
tools or actions locally, invent stable semantic keys, or create a second
|
|
33
|
+
telemetry store. Runtime requests are fail-closed; observation telemetry can be
|
|
34
|
+
configured fail-open with `failure_mode="open"`.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import os
|
|
38
|
+
from vira import ViraClient
|
|
39
|
+
|
|
40
|
+
with ViraClient(
|
|
41
|
+
"http://127.0.0.1:4185",
|
|
42
|
+
api_key=os.environ.get("VIRA_API_KEY"),
|
|
43
|
+
) as vira:
|
|
44
|
+
connected = vira.connect_once(
|
|
45
|
+
"my-agent", "custom-agent",
|
|
46
|
+
{"organizationId": "org", "projectId": "project", "environmentId": "local"},
|
|
47
|
+
"custom-python", "1",
|
|
48
|
+
idempotency_key="connect-001",
|
|
49
|
+
)
|
|
50
|
+
system = connected.body["system"]
|
|
51
|
+
connection = connected.body["connection"]
|
|
52
|
+
vira.observe({
|
|
53
|
+
"eventId": "evt-1", "eventType": "model.response",
|
|
54
|
+
"aiSystemId": system["id"], "connectionId": connection["id"],
|
|
55
|
+
"payload": {"message": "A model response was produced."},
|
|
56
|
+
}, idempotency_key="evt-1")
|
|
57
|
+
semantic = vira.resolve_semantic({"input": "answer a customer billing question"})
|
|
58
|
+
print(semantic.body)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Runtime execution requires a released revision, a valid scope and an idempotency key;
|
|
62
|
+
semantic resolution is not authorization. See [`docs/SDK_GUIDE.md`](../../docs/SDK_GUIDE.md)
|
|
63
|
+
for the shared JS/Python lifecycle.
|
|
64
|
+
|
|
65
|
+
The package metadata is intentionally dependency-free.
|
|
66
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Vira Python SDK
|
|
2
|
+
|
|
3
|
+
Package name: `tryvira-sdk`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install tryvira-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For a repository checkout use `python -m pip install ./sdks/python`. The client is a thin
|
|
12
|
+
HTTP client of Vira's authority boundary; it does not execute tools locally.
|
|
13
|
+
|
|
14
|
+
For maintainers, build with `python -m build sdks/python`. Public CI uses PyPI Trusted
|
|
15
|
+
Publishing; do not put a token in this repository or in an example.
|
|
16
|
+
|
|
17
|
+
The SDK is a thin client of the Vira authority boundary. It does not execute
|
|
18
|
+
tools or actions locally, invent stable semantic keys, or create a second
|
|
19
|
+
telemetry store. Runtime requests are fail-closed; observation telemetry can be
|
|
20
|
+
configured fail-open with `failure_mode="open"`.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import os
|
|
24
|
+
from vira import ViraClient
|
|
25
|
+
|
|
26
|
+
with ViraClient(
|
|
27
|
+
"http://127.0.0.1:4185",
|
|
28
|
+
api_key=os.environ.get("VIRA_API_KEY"),
|
|
29
|
+
) as vira:
|
|
30
|
+
connected = vira.connect_once(
|
|
31
|
+
"my-agent", "custom-agent",
|
|
32
|
+
{"organizationId": "org", "projectId": "project", "environmentId": "local"},
|
|
33
|
+
"custom-python", "1",
|
|
34
|
+
idempotency_key="connect-001",
|
|
35
|
+
)
|
|
36
|
+
system = connected.body["system"]
|
|
37
|
+
connection = connected.body["connection"]
|
|
38
|
+
vira.observe({
|
|
39
|
+
"eventId": "evt-1", "eventType": "model.response",
|
|
40
|
+
"aiSystemId": system["id"], "connectionId": connection["id"],
|
|
41
|
+
"payload": {"message": "A model response was produced."},
|
|
42
|
+
}, idempotency_key="evt-1")
|
|
43
|
+
semantic = vira.resolve_semantic({"input": "answer a customer billing question"})
|
|
44
|
+
print(semantic.body)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Runtime execution requires a released revision, a valid scope and an idempotency key;
|
|
48
|
+
semantic resolution is not authorization. See [`docs/SDK_GUIDE.md`](../../docs/SDK_GUIDE.md)
|
|
49
|
+
for the shared JS/Python lifecycle.
|
|
50
|
+
|
|
51
|
+
The package metadata is intentionally dependency-free.
|
|
52
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Authority-free agent-native observation intents for the Python SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib, json
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
_FORBIDDEN = {'projectId','applicationId','integrationId','userRef','subjectRef','privacyClass','retentionClass','residencyClass','authorityLane','scopeKeys','integrityAttestation'}
|
|
7
|
+
_LIFECYCLE = {'workflow.started','workflow.step','workflow.completed','action.proposed','action.completed','checkpoint.created','compensation.completed'}
|
|
8
|
+
def _canonical(value: Any) -> bytes: return json.dumps(value, sort_keys=True, separators=(',',':'), ensure_ascii=False).encode()
|
|
9
|
+
def _digest(value: Any) -> str: return hashlib.sha256(_canonical(value)).hexdigest()
|
|
10
|
+
def _verify_payload(payload: Mapping[str, Any]) -> None:
|
|
11
|
+
injected=sorted(_FORBIDDEN.intersection(payload.keys()))
|
|
12
|
+
if injected: raise ValueError(f"AGENT_NATIVE_AUTHORITY_FIELD_FORBIDDEN:{','.join(injected)}")
|
|
13
|
+
def build_observation_intent(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
14
|
+
_verify_payload(payload)
|
|
15
|
+
value={'schemaVersion':2,**dict(payload)}
|
|
16
|
+
for key in ('eventId','kind','occurredAt'):
|
|
17
|
+
if not str(value.get(key,'')).strip(): raise ValueError(f"AGENT_NATIVE_{key.upper()}_REQUIRED")
|
|
18
|
+
if not isinstance(value.get('payload',{}),dict): raise ValueError('AGENT_NATIVE_PAYLOAD_OBJECT_REQUIRED')
|
|
19
|
+
return {**value,'intentDigest':_digest(value)}
|
|
20
|
+
def build_agent_native_observation_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_observation_intent(payload)
|
|
21
|
+
def verify_observation_intent(intent: Mapping[str, Any]) -> dict[str, Any]:
|
|
22
|
+
value=dict(intent); _verify_payload(value); received=value.pop('intentDigest',None)
|
|
23
|
+
if not received or _digest(value) != received: raise ValueError('AGENT_NATIVE_OBSERVATION_INTENT_DIGEST_MISMATCH')
|
|
24
|
+
return {**value,'intentDigest':received}
|
|
25
|
+
def verify_agent_native_observation_intent(intent: Mapping[str, Any]) -> dict[str, Any]: return verify_observation_intent(intent)
|
|
26
|
+
def build_lifecycle_intent(kind: str, payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
27
|
+
if kind not in _LIFECYCLE: raise ValueError('BEHAVIOR_LIFECYCLE_KIND_INVALID')
|
|
28
|
+
return build_observation_intent({**dict(payload),'kind':kind})
|
|
29
|
+
def build_behavior_lifecycle_observation_intent(kind: str, payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent(kind, payload)
|
|
30
|
+
def build_workflow_start_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('workflow.started', payload)
|
|
31
|
+
def build_workflow_step_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('workflow.step', payload)
|
|
32
|
+
def build_workflow_complete_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('workflow.completed', payload)
|
|
33
|
+
def build_action_proposed_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('action.proposed', payload)
|
|
34
|
+
def build_action_completed_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('action.completed', payload)
|
|
35
|
+
def build_checkpoint_created_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('checkpoint.created', payload)
|
|
36
|
+
def build_compensation_completed_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('compensation.completed', payload)
|
|
37
|
+
def build_preference_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_observation_intent({**dict(payload),'kind':'human.preference'})
|
|
38
|
+
def build_handoff_intent(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
39
|
+
value=dict(payload); parent=value.pop('parentEventId',None); source=value.pop('fromAgentRef',None); target=value.pop('toAgentRef',None)
|
|
40
|
+
if not parent: raise ValueError('AGENT_NATIVE_HANDOFF_PARENT_EVENT_REQUIRED')
|
|
41
|
+
if not source or not target or source == target: raise ValueError('AGENT_NATIVE_HANDOFF_AGENT_IDENTITY_INVALID')
|
|
42
|
+
nested=value.pop('payload',{});
|
|
43
|
+
if not isinstance(nested,dict): raise ValueError('AGENT_NATIVE_PAYLOAD_OBJECT_REQUIRED')
|
|
44
|
+
handoff={'fromAgentRef':source,'toAgentRef':target}
|
|
45
|
+
for key in ('delegationRef','decisionOwnerAgentRef','toolOwnerAgentRef','toolRef'):
|
|
46
|
+
if key in value: handoff[key]=value.pop(key)
|
|
47
|
+
return build_observation_intent({**value,'kind':'agent.handoff','parentEventId':parent,'agentRef':source,'payload':{**nested,'viraTrajectoryV2':{'schemaVersion':2,'handoff':handoff}}})
|
|
48
|
+
def create_online_delivery(*, client_instance_id: str, client_sequence: int, intent: Mapping[str, Any]) -> dict[str, Any]:
|
|
49
|
+
if client_sequence < 0: raise ValueError('AGENT_NATIVE_CLIENT_SEQUENCE_INVALID')
|
|
50
|
+
return {'schemaVersion':2,'clientInstanceId':client_instance_id,'clientSequence':client_sequence,'mode':'online','intent':verify_observation_intent(intent)}
|
|
51
|
+
def offline_signing_digest(*, client_instance_id: str, client_sequence: int, queued_at: str, intent: Mapping[str, Any], key_ref: str, signed_at: str) -> str:
|
|
52
|
+
if client_sequence < 0: raise ValueError('AGENT_NATIVE_CLIENT_SEQUENCE_INVALID')
|
|
53
|
+
verified=verify_observation_intent(intent)
|
|
54
|
+
return _digest({'schemaVersion':2,'clientInstanceId':client_instance_id,'clientSequence':client_sequence,'queuedAt':queued_at,'intentDigest':verified['intentDigest'],'keyRef':key_ref,'signedAt':signed_at})
|
|
55
|
+
def create_offline_delivery(*, client_instance_id: str, client_sequence: int, queued_at: str, intent: Mapping[str, Any], key_ref: str, signed_at: str, signature: str) -> dict[str, Any]:
|
|
56
|
+
if client_sequence < 0: raise ValueError('AGENT_NATIVE_CLIENT_SEQUENCE_INVALID')
|
|
57
|
+
if not key_ref.strip() or not signature.strip(): raise ValueError('AGENT_NATIVE_OFFLINE_PROOF_REQUIRED')
|
|
58
|
+
return {'schemaVersion':2,'clientInstanceId':client_instance_id,'clientSequence':client_sequence,'mode':'offline','queuedAt':queued_at,'intent':verify_observation_intent(intent),'offlineProof':{'scheme':'ed25519','keyRef':key_ref,'signature':signature,'signedAt':signed_at}}
|
|
59
|
+
def create_online_observation_delivery(**kwargs: Any) -> dict[str, Any]: return create_online_delivery(**kwargs)
|
|
60
|
+
def create_offline_observation_delivery(**kwargs: Any) -> dict[str, Any]: return create_offline_delivery(**kwargs)
|
|
61
|
+
def utc_now_iso() -> str:
|
|
62
|
+
from datetime import datetime, timezone
|
|
63
|
+
return datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00','Z')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
def canonical_json(value: Any) -> str:
|
|
7
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
|
|
8
|
+
|
|
9
|
+
def sha256_canonical(value: Any) -> str:
|
|
10
|
+
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
from digest import sha256_canonical
|
|
5
|
+
|
|
6
|
+
BoundaryFamily = Literal['agent', 'model-step', 'tool', 'action', 'workflow', 'retrieval']
|
|
7
|
+
IdentityStrength = Literal['server-derived', 'adapter-attested', 'registered', 'host-asserted', 'observed-only']
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ExecutionBoundaryIdentityV2:
|
|
11
|
+
project_id: str
|
|
12
|
+
application_id: str
|
|
13
|
+
environment: Literal['dev', 'staging', 'production']
|
|
14
|
+
integration_id: str
|
|
15
|
+
framework: str
|
|
16
|
+
adapter_id: str
|
|
17
|
+
adapter_version: str
|
|
18
|
+
family: BoundaryFamily
|
|
19
|
+
operation: str
|
|
20
|
+
execution_id: str
|
|
21
|
+
stable_ref: str
|
|
22
|
+
component_type: str
|
|
23
|
+
identity_strength: IdentityStrength
|
|
24
|
+
registration_version: int = 1
|
|
25
|
+
resource: dict[str, str] | None = None
|
|
26
|
+
trajectory_id: str | None = None
|
|
27
|
+
parent_execution_id: str | None = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
for field in ('project_id', 'application_id', 'integration_id', 'framework', 'adapter_id', 'adapter_version', 'operation', 'execution_id', 'stable_ref', 'component_type'):
|
|
31
|
+
if not getattr(self, field).strip():
|
|
32
|
+
raise ValueError(f'{field.upper()}_REQUIRED')
|
|
33
|
+
if self.family == 'action' and self.identity_strength == 'observed-only':
|
|
34
|
+
raise ValueError('RUNTIME_IDENTITY_TOO_WEAK_FOR_EFFECT')
|
|
35
|
+
if self.registration_version < 1:
|
|
36
|
+
raise ValueError('REGISTRATION_VERSION_INVALID')
|
|
37
|
+
|
|
38
|
+
def to_dict_without_digest(self) -> dict[str, Any]:
|
|
39
|
+
value: dict[str, Any] = {'projectId': self.project_id, 'applicationId': self.application_id, 'environment': self.environment, 'integration': {'integrationId': self.integration_id, 'framework': self.framework, 'adapterId': self.adapter_id, 'adapterVersion': self.adapter_version}, 'execution': {'family': self.family, 'operation': self.operation, 'executionId': self.execution_id}, 'component': {'stableRef': self.stable_ref, 'componentType': self.component_type, 'identityStrength': self.identity_strength, 'registrationVersion': self.registration_version}}
|
|
40
|
+
if self.resource is not None: value['resource'] = dict(self.resource)
|
|
41
|
+
if self.trajectory_id is not None: value['execution']['trajectoryId'] = self.trajectory_id
|
|
42
|
+
if self.parent_execution_id is not None: value['execution']['parentExecutionId'] = self.parent_execution_id
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, Any]:
|
|
46
|
+
value = self.to_dict_without_digest()
|
|
47
|
+
value['boundaryDigest'] = sha256_canonical(value)
|
|
48
|
+
return value
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Callable, Literal, Mapping
|
|
4
|
+
from runtime import BehaviorExecutionContext, LifecycleCallback, RuntimeLifecycle
|
|
5
|
+
|
|
6
|
+
PythonFramework = Literal['crewai','pydantic-ai','autogen','semantic-kernel','custom']
|
|
7
|
+
IntegrationMode = Literal['observe','guide','rerank','repair','escalate','strict']
|
|
8
|
+
IntegrationOperation = Literal['generate','stream','tool','action']
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class RuntimeIntegrationManifest:
|
|
11
|
+
framework: PythonFramework; adapter_version: str='2'; generate: bool=False; stream: bool=False; tool: bool=False; action: bool=False; stream_enforcement: Literal['unsupported','pass-through','buffer-until-verified']='unsupported'; post_model_finalization: bool=False; tool_interception: bool=False; action_interception: bool=False; signal_attestation: Literal['none','host-asserted','external-verified','server-derived']='none'; trace_provenance: bool=True; model_target: bool=True; projection_artifact: bool=False; unsupported: tuple[str,...]=field(default_factory=tuple)
|
|
12
|
+
def to_dict(self) -> dict[str,object]: return {'schemaVersion':2,'integrationId':f'vira.{self.framework}','framework':self.framework,'adapterVersion':self.adapter_version,'operations':{'generate':self.generate,'stream':self.stream,'tool':self.tool,'action':self.action},'streamEnforcement':self.stream_enforcement,'postModelFinalization':self.post_model_finalization,'toolInterception':self.tool_interception,'actionInterception':self.action_interception,'signalAttestation':self.signal_attestation,'traceProvenance':self.trace_provenance,'modelTarget':self.model_target,'projectionArtifact':self.projection_artifact,'knownUnsupportedGuarantees':list(self.unsupported)}
|
|
13
|
+
def operation_supported(self, operation: IntegrationOperation) -> bool: return {'generate': self.generate, 'stream': self.stream, 'tool': self.tool, 'action': self.action}[operation]
|
|
14
|
+
def assess_runtime_integration_manifest(manifest: Mapping[str,object], *, mode: IntegrationMode, operation: IntegrationOperation, requires_trusted_signals: bool=False) -> dict[str,object]:
|
|
15
|
+
value=dict(manifest); operations=value.get('operations') if isinstance(value.get('operations'),Mapping) else {}; reasons=[]
|
|
16
|
+
if operations.get(operation) is not True: reasons.append(f'operation:{operation}:unsupported')
|
|
17
|
+
if value.get('traceProvenance') is not True: reasons.append('trace-provenance:missing')
|
|
18
|
+
if value.get('modelTarget') is not True: reasons.append('model-target:missing')
|
|
19
|
+
if requires_trusted_signals and value.get('signalAttestation') == 'none': reasons.append('signal-attestation:missing')
|
|
20
|
+
enforcing=mode != 'observe'
|
|
21
|
+
if enforcing and operation not in ('tool','action') and value.get('postModelFinalization') is not True: reasons.append('post-model-finalization:missing')
|
|
22
|
+
if operation == 'stream' and enforcing and value.get('streamEnforcement') != 'buffer-until-verified': reasons.append('stream-buffer-until-verified:missing')
|
|
23
|
+
if operation == 'tool' and enforcing and value.get('toolInterception') is not True: reasons.append('tool-interception:missing')
|
|
24
|
+
if operation == 'action' and enforcing and value.get('actionInterception') is not True: reasons.append('action-interception:missing')
|
|
25
|
+
if mode != 'observe' and value.get('projectionArtifact') is not True: reasons.append('projection-artifact:missing')
|
|
26
|
+
reasons.extend(f'declared-unsupported:{item}' for item in value.get('knownUnsupportedGuarantees',[]) if isinstance(item,str))
|
|
27
|
+
hard=any(reason.startswith(('operation:','trace-provenance:','model-target:','signal-attestation:','post-model-finalization:','stream-buffer-until-verified:','tool-interception:','action-interception:','projection-artifact:')) for reason in reasons)
|
|
28
|
+
status='unsupported' if not operations.get(operation) else 'partial' if hard or (mode != 'observe' and reasons) else 'supported'
|
|
29
|
+
return {'status':status,'enforcementGuaranteed':status == 'supported' and enforcing,'reasons':reasons}
|
|
30
|
+
def assess_runtime_integration(manifest: RuntimeIntegrationManifest|Mapping[str,object], *, mode: IntegrationMode, operation: IntegrationOperation, requires_trusted_signals: bool=False) -> dict[str,object]:
|
|
31
|
+
value = manifest.to_dict() if hasattr(manifest, 'to_dict') else manifest
|
|
32
|
+
return assess_runtime_integration_manifest(value, mode=mode, operation=operation, requires_trusted_signals=requires_trusted_signals)
|
|
33
|
+
|
|
34
|
+
class PythonFrameworkBridge:
|
|
35
|
+
def __init__(self, context: BehaviorExecutionContext, manifest: RuntimeIntegrationManifest, callback: LifecycleCallback | None = None):
|
|
36
|
+
self.context, self.manifest, self.lifecycle = context, manifest, RuntimeLifecycle(context, callback)
|
|
37
|
+
def before_execution(self, payload: dict[str, object]) -> dict[str, object]: self.lifecycle.emit('execution:start', eventType='model.response', framework=self.manifest.framework); return self.lifecycle.resolve_payload(payload)
|
|
38
|
+
def after_execution(self, output: object) -> dict[str, object]: return self.lifecycle.emit('execution:complete', eventType='model.response', framework=self.manifest.framework, output=output)
|
|
39
|
+
def before_tool(self, tool_name: str) -> dict[str, object]: return self.lifecycle.emit('tool:start', eventType='tool.started', framework=self.manifest.framework, toolName=tool_name)
|
|
40
|
+
def after_tool(self, tool_name: str) -> dict[str, object]: return self.lifecycle.emit('tool:complete', eventType='tool.completed', framework=self.manifest.framework, toolName=tool_name)
|
|
41
|
+
def before_action(self, action_name: str) -> dict[str, object]: return self.lifecycle.emit('action:start', eventType='action.proposed', framework=self.manifest.framework, actionName=action_name)
|
|
42
|
+
def after_action(self, action_name: str) -> dict[str, object]: return self.lifecycle.emit('action:complete', eventType='action.executed', framework=self.manifest.framework, actionName=action_name)
|
|
43
|
+
|
|
44
|
+
def create_framework_bridge(framework: PythonFramework, context: BehaviorExecutionContext, *, callback: LifecycleCallback | None = None, manifest: RuntimeIntegrationManifest | None = None) -> PythonFrameworkBridge:
|
|
45
|
+
declared = manifest or RuntimeIntegrationManifest(framework=framework, generate=True, unsupported=('no effect-boundary enforcement is implied until native framework hooks are wired',))
|
|
46
|
+
if declared.framework != framework: raise ValueError('RUNTIME_INTEGRATION_FRAMEWORK_MISMATCH')
|
|
47
|
+
return PythonFrameworkBridge(context, declared, callback)
|
|
48
|
+
def create_crewai_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('crewai', context, **kwargs)
|
|
49
|
+
def create_pydantic_ai_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('pydantic-ai', context, **kwargs)
|
|
50
|
+
def create_autogen_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('autogen', context, **kwargs)
|
|
51
|
+
def create_semantic_kernel_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('semantic-kernel', context, **kwargs)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from agent_native import (
|
|
2
|
+
build_action_completed_intent, build_action_proposed_intent,
|
|
3
|
+
build_agent_native_observation_intent, build_behavior_lifecycle_observation_intent,
|
|
4
|
+
build_checkpoint_created_intent, build_compensation_completed_intent,
|
|
5
|
+
build_handoff_intent, build_preference_intent, build_workflow_complete_intent,
|
|
6
|
+
build_workflow_start_intent, build_workflow_step_intent,
|
|
7
|
+
create_offline_observation_delivery, create_online_observation_delivery,
|
|
8
|
+
offline_signing_digest, utc_now_iso, verify_agent_native_observation_intent,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def build_action_completed_intent(payload): return __import__('agent_native').build_action_completed_intent(payload)
|
|
12
|
+
def build_action_proposed_intent(payload): return __import__('agent_native').build_action_proposed_intent(payload)
|
|
13
|
+
def build_agent_native_observation_intent(payload): return __import__('agent_native').build_agent_native_observation_intent(payload)
|
|
14
|
+
def build_behavior_lifecycle_observation_intent(kind, payload): return __import__('agent_native').build_behavior_lifecycle_observation_intent(kind, payload)
|
|
15
|
+
def build_checkpoint_created_intent(payload): return __import__('agent_native').build_checkpoint_created_intent(payload)
|
|
16
|
+
def build_compensation_completed_intent(payload): return __import__('agent_native').build_compensation_completed_intent(payload)
|
|
17
|
+
def build_handoff_intent(payload): return __import__('agent_native').build_handoff_intent(payload)
|
|
18
|
+
def build_preference_intent(payload): return __import__('agent_native').build_preference_intent(payload)
|
|
19
|
+
def build_workflow_complete_intent(payload): return __import__('agent_native').build_workflow_complete_intent(payload)
|
|
20
|
+
def build_workflow_start_intent(payload): return __import__('agent_native').build_workflow_start_intent(payload)
|
|
21
|
+
def build_workflow_step_intent(payload): return __import__('agent_native').build_workflow_step_intent(payload)
|
|
22
|
+
def create_offline_observation_delivery(**kwargs): return __import__('agent_native').create_offline_observation_delivery(**kwargs)
|
|
23
|
+
def create_online_observation_delivery(**kwargs): return __import__('agent_native').create_online_observation_delivery(**kwargs)
|
|
24
|
+
def offline_signing_digest(**kwargs): return __import__('agent_native').offline_signing_digest(**kwargs)
|
|
25
|
+
def utc_now_iso(): return __import__('agent_native').utc_now_iso()
|
|
26
|
+
def verify_agent_native_observation_intent(intent): return __import__('agent_native').verify_agent_native_observation_intent(intent)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tryvira-sdk"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Typed Python client for the Vira authority boundary"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Vira" }]
|
|
13
|
+
keywords = ["ai-governance", "observability", "agent-runtime"]
|
|
14
|
+
classifiers = ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/hrpering/vira_beh_redesign"
|
|
18
|
+
Documentation = "https://github.com/hrpering/vira_beh_redesign/tree/main/sdks/python"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["vira"]
|
|
22
|
+
include-package-data = true
|
|
23
|
+
py-modules = ["vira_client", "agent_native", "digest", "observation_v2", "execution_boundary_v2", "runtime", "runtime_v2", "integration"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
vira = ["py.typed"]
|
|
27
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Callable, Literal
|
|
5
|
+
|
|
6
|
+
BehaviorExecutionKind = Literal['chat', 'agent', 'copilot', 'workflow']
|
|
7
|
+
BehaviorExecutionOperation = Literal['generate', 'stream', 'tool', 'action']
|
|
8
|
+
ModelProtocol = Literal['openai-chat', 'anthropic-messages', 'generic-chat', 'custom']
|
|
9
|
+
|
|
10
|
+
def _required(value: str, field: str) -> str:
|
|
11
|
+
if not isinstance(value, str) or not value.strip(): raise ValueError(f'{field.upper()}_REQUIRED')
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ModelTarget:
|
|
16
|
+
provider: str
|
|
17
|
+
model: str | None = None
|
|
18
|
+
model_id: str | None = None
|
|
19
|
+
model_family: str | None = None
|
|
20
|
+
protocol: ModelProtocol | None = None
|
|
21
|
+
capabilities: dict[str, bool] | None = None
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
_required(self.provider, 'model_target_provider')
|
|
24
|
+
identities = [value for value in (self.model, self.model_id, self.model_family) if value is not None]
|
|
25
|
+
if not identities or any(not isinstance(value, str) or not value.strip() for value in identities): raise ValueError('MODEL_TARGET_IDENTITY_REQUIRED')
|
|
26
|
+
if self.model is not None and self.model_id is not None and self.model != self.model_id: raise ValueError('MODEL_TARGET_IDENTITY_CONFLICT')
|
|
27
|
+
if self.protocol is not None and self.protocol not in ('openai-chat', 'anthropic-messages', 'generic-chat', 'custom'): raise ValueError('MODEL_TARGET_PROTOCOL_INVALID')
|
|
28
|
+
if self.capabilities is not None and (set(self.capabilities) - {'tools', 'structuredOutput', 'reasoning', 'streaming'} or any(not isinstance(value, bool) for value in self.capabilities.values())): raise ValueError('MODEL_TARGET_CAPABILITY_INVALID')
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
return {key: value for key, value in {'provider': self.provider, 'model': self.model, 'modelId': self.model_id, 'modelFamily': self.model_family, 'protocol': self.protocol, 'capabilities': dict(self.capabilities) if self.capabilities is not None else None}.items() if value is not None}
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class RuntimeHost:
|
|
34
|
+
name: str
|
|
35
|
+
version: str | None = None
|
|
36
|
+
runtime: str | None = None
|
|
37
|
+
def __post_init__(self) -> None: _required(self.name, 'runtime_host_name')
|
|
38
|
+
def to_dict(self) -> dict[str, str]: return {key: value for key, value in {'name': self.name, 'version': self.version, 'runtime': self.runtime}.items() if value is not None}
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class BehaviorExecutionContext:
|
|
42
|
+
kind: BehaviorExecutionKind
|
|
43
|
+
operation: BehaviorExecutionOperation
|
|
44
|
+
host: RuntimeHost
|
|
45
|
+
model_target: ModelTarget
|
|
46
|
+
execution_id: str | None = None
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if self.kind not in ('chat', 'agent', 'copilot', 'workflow'): raise ValueError('BEHAVIOR_EXECUTION_KIND_INVALID')
|
|
49
|
+
if self.operation not in ('generate', 'stream', 'tool', 'action'): raise ValueError('BEHAVIOR_EXECUTION_OPERATION_INVALID')
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
return {**{'kind': self.kind, 'operation': self.operation, 'host': self.host.to_dict(), 'modelTarget': self.model_target.to_dict()}, **({'executionId': self.execution_id} if self.execution_id is not None else {})}
|
|
52
|
+
|
|
53
|
+
LifecycleCallback = Callable[[dict[str, Any]], None]
|
|
54
|
+
class RuntimeLifecycle:
|
|
55
|
+
def __init__(self, context: BehaviorExecutionContext, callback: LifecycleCallback | None = None): self.context, self.callback = context, callback
|
|
56
|
+
def emit(self, stage: str, **provenance: Any) -> dict[str, Any]:
|
|
57
|
+
event = {'stage': stage, 'context': self.context.to_dict(), **provenance, 'at': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')}
|
|
58
|
+
if self.callback: self.callback(event)
|
|
59
|
+
return event
|
|
60
|
+
def resolve_payload(self, payload: dict[str, Any]) -> dict[str, Any]: return {**payload, 'executionContext': self.context.to_dict()}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
_DIGEST = re.compile(r'^[0-9a-f]{64}$')
|
|
6
|
+
_REQUIRED_PLAN = {'schemaVersion', 'traceId', 'revisions', 'effectiveProgramDigest', 'artifactDigests', 'capsuleDigest', 'matchedModuleKeys', 'preModel', 'toolPolicy', 'postModel', 'execution', 'provenance'}
|
|
7
|
+
_REQUIRED_ENVELOPE = {'schemaVersion', 'legacyProjection', 'executionPlanV2', 'modelTarget', 'projectionArtifact', 'integration'}
|
|
8
|
+
|
|
9
|
+
class ViraRuntimeCompatibilityError(ValueError):
|
|
10
|
+
def __init__(self, code: str, message: str, details: Mapping[str, Any] | None = None):
|
|
11
|
+
super().__init__(message); self.code, self.details = code, dict(details or {})
|
|
12
|
+
|
|
13
|
+
def _invalid(message: str, field: str) -> ViraRuntimeCompatibilityError:
|
|
14
|
+
return ViraRuntimeCompatibilityError('VIRA_RUNTIME_V2_ENVELOPE_INVALID', message, {'field': field})
|
|
15
|
+
def _object(value: Any, field: str) -> Mapping[str, Any]:
|
|
16
|
+
if not isinstance(value, Mapping): raise _invalid(f'{field} must be an object', field)
|
|
17
|
+
return value
|
|
18
|
+
def _required_string(value: Any, field: str) -> str:
|
|
19
|
+
if not isinstance(value, str) or not value.strip(): raise _invalid(f'{field} is required', field)
|
|
20
|
+
return value
|
|
21
|
+
def _digest(value: Any, field: str) -> str:
|
|
22
|
+
result = _required_string(value, field)
|
|
23
|
+
if not _DIGEST.fullmatch(result): raise _invalid(f'{field} must be a lowercase sha256 digest', field)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
def parse_runtime_v2_compatibility_envelope(input: Any) -> dict[str, Any]:
|
|
27
|
+
envelope = _object(input, 'envelope')
|
|
28
|
+
missing = sorted(_REQUIRED_ENVELOPE - set(envelope))
|
|
29
|
+
if missing: raise _invalid(f'envelope is missing required fields: {", ".join(missing)}', 'envelope')
|
|
30
|
+
if envelope['schemaVersion'] != 2: raise _invalid('schemaVersion must be 2', 'schemaVersion')
|
|
31
|
+
_required_string(envelope['legacyProjection'], 'legacyProjection')
|
|
32
|
+
plan = _object(envelope['executionPlanV2'], 'executionPlanV2')
|
|
33
|
+
plan_missing = sorted(_REQUIRED_PLAN - set(plan))
|
|
34
|
+
if plan_missing: raise _invalid(f'executionPlanV2 is missing required fields: {", ".join(plan_missing)}', 'executionPlanV2')
|
|
35
|
+
if plan['schemaVersion'] != 2: raise _invalid('executionPlanV2.schemaVersion must be 2', 'executionPlanV2.schemaVersion')
|
|
36
|
+
_required_string(plan['traceId'], 'executionPlanV2.traceId')
|
|
37
|
+
_digest(plan['effectiveProgramDigest'], 'executionPlanV2.effectiveProgramDigest')
|
|
38
|
+
_digest(plan['capsuleDigest'], 'executionPlanV2.capsuleDigest')
|
|
39
|
+
for key in ('artifactDigests', 'matchedModuleKeys'):
|
|
40
|
+
if not isinstance(plan[key], list): raise _invalid(f'executionPlanV2.{key} must be a list', f'executionPlanV2.{key}')
|
|
41
|
+
_object(plan['preModel'], 'executionPlanV2.preModel'); _object(plan['toolPolicy'], 'executionPlanV2.toolPolicy'); _object(plan['postModel'], 'executionPlanV2.postModel'); _object(plan['execution'], 'executionPlanV2.execution'); _object(plan['provenance'], 'executionPlanV2.provenance')
|
|
42
|
+
_object(envelope['modelTarget'], 'modelTarget'); integration = _object(envelope['integration'], 'integration')
|
|
43
|
+
if 'manifest' in integration:
|
|
44
|
+
manifest = _object(integration['manifest'], 'integration.manifest')
|
|
45
|
+
for key in ('operations', 'traceProvenance', 'modelTarget', 'postModelFinalization', 'knownUnsupportedGuarantees'):
|
|
46
|
+
if key not in manifest: raise _invalid(f'integration.manifest is missing {key}', f'integration.manifest.{key}')
|
|
47
|
+
if not isinstance(manifest['operations'], Mapping) or not isinstance(manifest['knownUnsupportedGuarantees'], list): raise _invalid('integration.manifest capability shape is invalid', 'integration.manifest')
|
|
48
|
+
assessment = _object(integration.get('assessment'), 'integration.assessment')
|
|
49
|
+
if assessment.get('enforcementGuaranteed') is True and manifest.get('postModelFinalization') is not True: raise _invalid('integration assessment is forged', 'integration.assessment')
|
|
50
|
+
interoperability = integration.get('interoperability') or manifest.get('interoperability')
|
|
51
|
+
if isinstance(interoperability, Mapping) and interoperability.get('mode') == 'lossy' and not any(isinstance(item, str) and 'lossy' in item for item in manifest['knownUnsupportedGuarantees']): raise _invalid('lossy interoperability requires an explicit unsupported guarantee', 'integration.manifest.knownUnsupportedGuarantees')
|
|
52
|
+
if envelope['projectionArtifact'] is not None: _object(envelope['projectionArtifact'], 'projectionArtifact')
|
|
53
|
+
return dict(envelope)
|
|
54
|
+
|
|
55
|
+
def require_runtime_v2_enforcement(input: Any) -> dict[str, Any]:
|
|
56
|
+
envelope = parse_runtime_v2_compatibility_envelope(input)
|
|
57
|
+
integration = envelope['integration']; assessment = integration.get('assessment') if isinstance(integration, Mapping) else None
|
|
58
|
+
guaranteed = assessment.get('enforcementGuaranteed') if isinstance(assessment, Mapping) else integration.get('enforcementGuaranteed')
|
|
59
|
+
if guaranteed is not True: raise ViraRuntimeCompatibilityError('VIRA_RUNTIME_V2_ENFORCEMENT_UNAVAILABLE', 'Runtime V2 enforcement was not negotiated', {'status': assessment.get('status') if isinstance(assessment, Mapping) else 'partial'})
|
|
60
|
+
return envelope
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tryvira-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Typed Python client for the Vira authority boundary
|
|
5
|
+
Author: Vira
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/hrpering/vira_beh_redesign
|
|
8
|
+
Project-URL: Documentation, https://github.com/hrpering/vira_beh_redesign/tree/main/sdks/python
|
|
9
|
+
Keywords: ai-governance,observability,agent-runtime
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Vira Python SDK
|
|
16
|
+
|
|
17
|
+
Package name: `tryvira-sdk`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m pip install tryvira-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For a repository checkout use `python -m pip install ./sdks/python`. The client is a thin
|
|
26
|
+
HTTP client of Vira's authority boundary; it does not execute tools locally.
|
|
27
|
+
|
|
28
|
+
For maintainers, build with `python -m build sdks/python`. Public CI uses PyPI Trusted
|
|
29
|
+
Publishing; do not put a token in this repository or in an example.
|
|
30
|
+
|
|
31
|
+
The SDK is a thin client of the Vira authority boundary. It does not execute
|
|
32
|
+
tools or actions locally, invent stable semantic keys, or create a second
|
|
33
|
+
telemetry store. Runtime requests are fail-closed; observation telemetry can be
|
|
34
|
+
configured fail-open with `failure_mode="open"`.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import os
|
|
38
|
+
from vira import ViraClient
|
|
39
|
+
|
|
40
|
+
with ViraClient(
|
|
41
|
+
"http://127.0.0.1:4185",
|
|
42
|
+
api_key=os.environ.get("VIRA_API_KEY"),
|
|
43
|
+
) as vira:
|
|
44
|
+
connected = vira.connect_once(
|
|
45
|
+
"my-agent", "custom-agent",
|
|
46
|
+
{"organizationId": "org", "projectId": "project", "environmentId": "local"},
|
|
47
|
+
"custom-python", "1",
|
|
48
|
+
idempotency_key="connect-001",
|
|
49
|
+
)
|
|
50
|
+
system = connected.body["system"]
|
|
51
|
+
connection = connected.body["connection"]
|
|
52
|
+
vira.observe({
|
|
53
|
+
"eventId": "evt-1", "eventType": "model.response",
|
|
54
|
+
"aiSystemId": system["id"], "connectionId": connection["id"],
|
|
55
|
+
"payload": {"message": "A model response was produced."},
|
|
56
|
+
}, idempotency_key="evt-1")
|
|
57
|
+
semantic = vira.resolve_semantic({"input": "answer a customer billing question"})
|
|
58
|
+
print(semantic.body)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Runtime execution requires a released revision, a valid scope and an idempotency key;
|
|
62
|
+
semantic resolution is not authorization. See [`docs/SDK_GUIDE.md`](../../docs/SDK_GUIDE.md)
|
|
63
|
+
for the shared JS/Python lifecycle.
|
|
64
|
+
|
|
65
|
+
The package metadata is intentionally dependency-free.
|
|
66
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
agent_native.py
|
|
3
|
+
digest.py
|
|
4
|
+
execution_boundary_v2.py
|
|
5
|
+
integration.py
|
|
6
|
+
observation_v2.py
|
|
7
|
+
pyproject.toml
|
|
8
|
+
runtime.py
|
|
9
|
+
runtime_v2.py
|
|
10
|
+
vira_client.py
|
|
11
|
+
tryvira_sdk.egg-info/PKG-INFO
|
|
12
|
+
tryvira_sdk.egg-info/SOURCES.txt
|
|
13
|
+
tryvira_sdk.egg-info/dependency_links.txt
|
|
14
|
+
tryvira_sdk.egg-info/top_level.txt
|
|
15
|
+
vira/__init__.py
|
|
16
|
+
vira/py.typed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Packaged Python SDK facade for Vira."""
|
|
2
|
+
|
|
3
|
+
from vira_client import ViraApiError, ViraClient, ViraResponse
|
|
4
|
+
from agent_native import (
|
|
5
|
+
build_observation_intent,
|
|
6
|
+
build_lifecycle_intent,
|
|
7
|
+
build_handoff_intent,
|
|
8
|
+
create_online_delivery,
|
|
9
|
+
create_offline_delivery,
|
|
10
|
+
verify_observation_intent,
|
|
11
|
+
build_agent_native_observation_intent, build_behavior_lifecycle_observation_intent,
|
|
12
|
+
build_preference_intent, build_workflow_start_intent, build_workflow_step_intent,
|
|
13
|
+
build_workflow_complete_intent, build_action_proposed_intent, build_action_completed_intent,
|
|
14
|
+
build_checkpoint_created_intent, build_compensation_completed_intent,
|
|
15
|
+
create_online_observation_delivery, create_offline_observation_delivery,
|
|
16
|
+
offline_signing_digest, utc_now_iso, verify_agent_native_observation_intent,
|
|
17
|
+
)
|
|
18
|
+
from digest import canonical_json, sha256_canonical
|
|
19
|
+
from runtime import BehaviorExecutionContext, ModelTarget, RuntimeHost, RuntimeLifecycle
|
|
20
|
+
from integration import PythonFrameworkBridge, RuntimeIntegrationManifest, create_framework_bridge
|
|
21
|
+
from execution_boundary_v2 import ExecutionBoundaryIdentityV2
|
|
22
|
+
from runtime_v2 import ViraRuntimeCompatibilityError, parse_runtime_v2_compatibility_envelope, require_runtime_v2_enforcement
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ViraApiError", "ViraClient", "ViraResponse",
|
|
26
|
+
"build_observation_intent", "build_lifecycle_intent", "build_handoff_intent", "build_agent_native_observation_intent",
|
|
27
|
+
"build_behavior_lifecycle_observation_intent", "build_preference_intent", "build_workflow_start_intent", "build_workflow_step_intent", "build_workflow_complete_intent",
|
|
28
|
+
"build_action_proposed_intent", "build_action_completed_intent", "build_checkpoint_created_intent", "build_compensation_completed_intent",
|
|
29
|
+
"create_online_delivery", "create_offline_delivery", "create_online_observation_delivery", "create_offline_observation_delivery", "offline_signing_digest", "utc_now_iso", "verify_observation_intent", "verify_agent_native_observation_intent",
|
|
30
|
+
"canonical_json", "sha256_canonical", "BehaviorExecutionContext", "ModelTarget", "RuntimeHost", "RuntimeLifecycle", "PythonFrameworkBridge", "RuntimeIntegrationManifest", "create_framework_bridge", "ExecutionBoundaryIdentityV2", "ViraRuntimeCompatibilityError", "parse_runtime_v2_compatibility_envelope", "require_runtime_v2_enforcement",
|
|
31
|
+
]
|
|
32
|
+
|
|
File without changes
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Typed Python SDK boundary for the versioned Vira control API.
|
|
2
|
+
|
|
3
|
+
The client deliberately reuses the same admitted-observation authority as the
|
|
4
|
+
JavaScript SDK; it does not create a second telemetry or learning store.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
12
|
+
from urllib.error import HTTPError, URLError
|
|
13
|
+
from urllib.request import Request, urlopen
|
|
14
|
+
|
|
15
|
+
Json = Any
|
|
16
|
+
Transport = Callable[[str, str, Mapping[str, str], bytes | None, float], tuple[int, Mapping[str, str], bytes]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ViraApiError(RuntimeError):
|
|
20
|
+
def __init__(self, status: int, body: Json, request_id: str = "") -> None:
|
|
21
|
+
super().__init__(f"VIRA_API_{status}")
|
|
22
|
+
self.status, self.body, self.request_id = status, body, request_id
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ViraResponse:
|
|
27
|
+
status: int
|
|
28
|
+
body: Json
|
|
29
|
+
request_id: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _default_transport(url: str, method: str, headers: Mapping[str, str], body: bytes | None, timeout: float) -> tuple[int, Mapping[str, str], bytes]:
|
|
33
|
+
try:
|
|
34
|
+
with urlopen(Request(url, data=body, headers=dict(headers), method=method), timeout=timeout) as response:
|
|
35
|
+
return response.status, response.headers, response.read()
|
|
36
|
+
except HTTPError as error:
|
|
37
|
+
return error.code, error.headers, error.read()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ViraClient:
|
|
41
|
+
def __init__(self, base_url: str, *, api_key: str | None = None, timeout: float = 15.0, retries: int = 2,
|
|
42
|
+
failure_mode: str = "throw", transport: Transport | None = None) -> None:
|
|
43
|
+
if not base_url.strip(): raise ValueError("SDK_BASE_URL_REQUIRED")
|
|
44
|
+
if failure_mode not in {"throw", "open"}: raise ValueError("SDK_FAILURE_MODE_INVALID")
|
|
45
|
+
self.base_url, self.api_key = base_url.rstrip("/"), api_key
|
|
46
|
+
self.timeout, self.retries, self.failure_mode = max(1.0, min(timeout, 120.0)), max(0, min(retries, 5)), failure_mode
|
|
47
|
+
self.transport = transport or _default_transport
|
|
48
|
+
|
|
49
|
+
def _request(self, method: str, path: str, *, query: Mapping[str, str] | None = None,
|
|
50
|
+
body: Mapping[str, Json] | None = None, idempotency_key: str | None = None,
|
|
51
|
+
enforcing: bool = False) -> ViraResponse | None:
|
|
52
|
+
from urllib.parse import urlencode
|
|
53
|
+
url = self.base_url + path + (("?" + urlencode(query)) if query else "")
|
|
54
|
+
headers = {"accept": "application/json"}
|
|
55
|
+
if self.api_key: headers["authorization"] = f"Bearer {self.api_key}"
|
|
56
|
+
if idempotency_key: headers["idempotency-key"] = idempotency_key
|
|
57
|
+
encoded = json.dumps(body, separators=(",", ":")).encode() if body is not None else None
|
|
58
|
+
if encoded is not None: headers["content-type"] = "application/json"
|
|
59
|
+
for attempt in range(self.retries + 1):
|
|
60
|
+
try:
|
|
61
|
+
status, response_headers, raw = self.transport(url, method, headers, encoded, self.timeout)
|
|
62
|
+
try: parsed = json.loads(raw) if raw else None
|
|
63
|
+
except json.JSONDecodeError: parsed = raw.decode(errors="replace")
|
|
64
|
+
request_id = response_headers.get("x-request-id", "")
|
|
65
|
+
if status >= 400:
|
|
66
|
+
if status >= 500 and attempt < self.retries: time.sleep(0.025 * (2 ** attempt)); continue
|
|
67
|
+
if self.failure_mode == "open" and not enforcing and status >= 500: return None
|
|
68
|
+
raise ViraApiError(status, parsed, request_id)
|
|
69
|
+
return ViraResponse(status, parsed, request_id)
|
|
70
|
+
except (TimeoutError, URLError, OSError) as error:
|
|
71
|
+
if attempt < self.retries: time.sleep(0.025 * (2 ** attempt)); continue
|
|
72
|
+
if self.failure_mode == "open" and not enforcing: return None
|
|
73
|
+
raise error
|
|
74
|
+
raise RuntimeError("SDK_REQUEST_EXHAUSTED")
|
|
75
|
+
|
|
76
|
+
def connect(self, name: str, system_type: str, scope: Mapping[str, str], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
77
|
+
return self._request("POST", "/v1/ai-systems", body={"name": name, "systemType": system_type, **dict(scope)}, idempotency_key=idempotency_key)
|
|
78
|
+
|
|
79
|
+
def connect_adapter(self, system_id: str, adapter_type: str, adapter_version: str, *, capabilities: list[str] | None = None, config: Json | None = None, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
80
|
+
if not str(system_id).strip(): raise ValueError("SDK_SYSTEM_ID_REQUIRED")
|
|
81
|
+
return self._request("POST", f"/v1/ai-systems/{system_id}/connections", body={"adapterType": adapter_type, "adapterVersion": adapter_version, "capabilities": list(capabilities or []), "config": config or {}}, idempotency_key=idempotency_key)
|
|
82
|
+
|
|
83
|
+
def connect_once(self, name: str, system_type: str, scope: Mapping[str, str], adapter_type: str, adapter_version: str, *, capabilities: list[str] | None = None, config: Json | None = None, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
84
|
+
system = self.connect(name, system_type, scope, idempotency_key=idempotency_key)
|
|
85
|
+
if system is None: return None
|
|
86
|
+
system_id = str(system.body.get("system", {}).get("id", ""))
|
|
87
|
+
connection = self.connect_adapter(system_id, adapter_type, adapter_version, capabilities=capabilities, config=config, idempotency_key=idempotency_key)
|
|
88
|
+
if connection is None: return None
|
|
89
|
+
return ViraResponse(connection.status, {"system": system.body.get("system"), "connection": connection.body.get("connection"), "handshake": connection.body.get("handshake")}, connection.request_id)
|
|
90
|
+
|
|
91
|
+
def observe(self, event: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
92
|
+
required = ("eventId", "eventType", "aiSystemId", "connectionId")
|
|
93
|
+
if any(not str(event.get(key, "")).strip() for key in required): raise ValueError("SDK_OBSERVATION_FIELD_REQUIRED")
|
|
94
|
+
return self._request("POST", "/v1/observations", body=dict(event), idempotency_key=idempotency_key or str(event["eventId"]))
|
|
95
|
+
|
|
96
|
+
def observe_batch(self, events: list[Mapping[str, Json]], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
97
|
+
if not 1 <= len(events) <= 100: raise ValueError("SDK_OBSERVATION_BATCH_INVALID")
|
|
98
|
+
return self._request("POST", "/v1/observations/batch", body={"events": events}, idempotency_key=idempotency_key)
|
|
99
|
+
|
|
100
|
+
def trace(self, event: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None: return self.observe({**event, "eventType": "model.response"}, **kwargs)
|
|
101
|
+
def outcome(self, event: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None: return self.observe({**event, "eventType": "business.outcome"}, **kwargs)
|
|
102
|
+
def feedback(self, event: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None: return self.observe({**event, "eventType": "feedback.user"}, **kwargs)
|
|
103
|
+
|
|
104
|
+
def lifecycle(self, kind: str, payload: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
105
|
+
"""Emit an agent-native lifecycle observation with a client-owned digest."""
|
|
106
|
+
from agent_native import build_lifecycle_intent, create_online_delivery
|
|
107
|
+
intent = build_lifecycle_intent(kind, payload)
|
|
108
|
+
return self._request("POST", "/v1/observations/sdk", body=create_online_delivery(
|
|
109
|
+
client_instance_id=str(payload.get("clientInstanceId", "python-sdk")),
|
|
110
|
+
client_sequence=int(payload.get("clientSequence", 0)), intent=intent,
|
|
111
|
+
), idempotency_key=idempotency_key or str(intent["eventId"]))
|
|
112
|
+
|
|
113
|
+
def preference(self, payload: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
114
|
+
return self.lifecycle("workflow.step", {**payload, "payload": {"preference": dict(payload)}} , idempotency_key=idempotency_key)
|
|
115
|
+
|
|
116
|
+
def handoff(self, payload: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
117
|
+
"""Admit an agent handoff as an authority-free observation intent."""
|
|
118
|
+
from agent_native import build_handoff_intent, create_online_delivery
|
|
119
|
+
intent = build_handoff_intent(payload)
|
|
120
|
+
return self._request("POST", "/v1/observations/sdk", body=create_online_delivery(
|
|
121
|
+
client_instance_id=str(payload.get("clientInstanceId", "python-sdk")),
|
|
122
|
+
client_sequence=int(payload.get("clientSequence", 0)), intent=intent,
|
|
123
|
+
), idempotency_key=idempotency_key or str(intent["eventId"]))
|
|
124
|
+
|
|
125
|
+
def workflow_start(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
126
|
+
return self.lifecycle("workflow.started", payload, **kwargs)
|
|
127
|
+
|
|
128
|
+
def workflow_step(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
129
|
+
return self.lifecycle("workflow.step", payload, **kwargs)
|
|
130
|
+
|
|
131
|
+
def workflow_complete(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
132
|
+
return self.lifecycle("workflow.completed", payload, **kwargs)
|
|
133
|
+
|
|
134
|
+
def action_proposed(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
135
|
+
return self.lifecycle("action.proposed", payload, **kwargs)
|
|
136
|
+
|
|
137
|
+
def action_completed(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
138
|
+
return self.lifecycle("action.completed", payload, **kwargs)
|
|
139
|
+
|
|
140
|
+
def checkpoint_created(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
141
|
+
return self.lifecycle("checkpoint.created", payload, **kwargs)
|
|
142
|
+
|
|
143
|
+
def compensation_completed(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
|
|
144
|
+
return self.lifecycle("compensation.completed", payload, **kwargs)
|
|
145
|
+
|
|
146
|
+
def execute_runtime(self, request: Mapping[str, Json]) -> ViraResponse | None:
|
|
147
|
+
return self._request("POST", "/v1/runtime/execute", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")), enforcing=True)
|
|
148
|
+
|
|
149
|
+
def evaluate_quality(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
150
|
+
"""Rank bounded chatbot candidates through Vira's quality/JEV gate."""
|
|
151
|
+
return self._request("POST", "/v1/quality/evaluate", body=dict(request), idempotency_key=idempotency_key, enforcing=True)
|
|
152
|
+
|
|
153
|
+
def create_signal_discovery_proposal(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
154
|
+
return self._request("POST", "/v1/signals/discovery-proposals", body=dict(request), idempotency_key=idempotency_key or str(request.get("signalStableKey", "")))
|
|
155
|
+
|
|
156
|
+
def review_signal_discovery_proposal(self, proposal_id: str, decision: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
157
|
+
if not proposal_id.strip(): raise ValueError("SDK_SIGNAL_PROPOSAL_ID_REQUIRED")
|
|
158
|
+
if decision not in {"APPROVE", "REJECT"}: raise ValueError("SDK_SIGNAL_DECISION_INVALID")
|
|
159
|
+
return self._request("POST", f"/v1/signals/discovery-proposals/{proposal_id}/review", body={"decision": decision}, idempotency_key=idempotency_key or f"signal-review:{proposal_id}")
|
|
160
|
+
|
|
161
|
+
def materialize_signal_discovery(self, proposal_id: str, request: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
162
|
+
if not proposal_id.strip(): raise ValueError("SDK_SIGNAL_PROPOSAL_ID_REQUIRED")
|
|
163
|
+
return self._request("POST", f"/v1/signals/discovery-proposals/{proposal_id}/materialize", body=dict(request or {}), idempotency_key=idempotency_key or f"signal-materialize:{proposal_id}")
|
|
164
|
+
|
|
165
|
+
def create_signal_mapping(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
166
|
+
return self._request("POST", "/v1/signals/mappings", body=dict(request), idempotency_key=idempotency_key or str(request.get("signalStableKey", "")))
|
|
167
|
+
|
|
168
|
+
def get_signal_mapping(self, mapping_id: str) -> ViraResponse | None:
|
|
169
|
+
if not mapping_id.strip(): raise ValueError("SDK_SIGNAL_MAPPING_ID_REQUIRED")
|
|
170
|
+
return self._request("GET", f"/v1/signals/mappings/{mapping_id}")
|
|
171
|
+
|
|
172
|
+
def review_signal_mapping(self, mapping_id: str, request: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
173
|
+
if not mapping_id.strip(): raise ValueError("SDK_SIGNAL_MAPPING_ID_REQUIRED")
|
|
174
|
+
return self._request("POST", f"/v1/signals/mappings/{mapping_id}/review", body=dict(request or {}), idempotency_key=idempotency_key or f"signal-mapping-review:{mapping_id}")
|
|
175
|
+
|
|
176
|
+
def compile_behavior_capsule(self, request: Mapping[str, Json]) -> ViraResponse | None:
|
|
177
|
+
if not str(request.get("programId", "")).strip() or not str(request.get("input", "")).strip():
|
|
178
|
+
raise ValueError("SDK_BEHAVIOR_CAPSULE_INPUT_REQUIRED")
|
|
179
|
+
return self._request("POST", "/v1/runtime/behavior-capsule", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")))
|
|
180
|
+
|
|
181
|
+
def serve_behavior_projection(self, request: Mapping[str, Json]) -> ViraResponse | None:
|
|
182
|
+
if not str(request.get("input", "")).strip() or not str(request.get("provider", "")).strip():
|
|
183
|
+
raise ValueError("SDK_BEHAVIOR_PROJECTION_INPUT_REQUIRED")
|
|
184
|
+
return self._request("POST", "/v1/runtime/behavior-projection", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")))
|
|
185
|
+
|
|
186
|
+
def resolve(self, request: Mapping[str, Json]) -> ViraResponse | None:
|
|
187
|
+
"""Backward-compatible alias for the explicit runtime resolver."""
|
|
188
|
+
return self.resolve_runtime(request)
|
|
189
|
+
|
|
190
|
+
def resolve_runtime(self, request: Mapping[str, Json]) -> ViraResponse | None:
|
|
191
|
+
"""Resolve a persisted route through the governed runtime boundary.
|
|
192
|
+
|
|
193
|
+
A caller may provide either an explicit behaviorPoint or a canonical
|
|
194
|
+
semanticContext produced by semantic activation. The latter is the
|
|
195
|
+
boundary-only path used by framework integrations.
|
|
196
|
+
"""
|
|
197
|
+
boundary = request.get("executionBoundary")
|
|
198
|
+
nested = boundary.get("behaviorPoint") if isinstance(boundary, Mapping) else None
|
|
199
|
+
semantic = request.get("semanticContext")
|
|
200
|
+
selected = semantic.get("behaviorPoint") if isinstance(semantic, Mapping) else None
|
|
201
|
+
if not str(request.get("behaviorPoint") or nested or selected or "").strip(): raise ValueError("SDK_RUNTIME_ROUTE_REQUIRED")
|
|
202
|
+
return self._request("POST", "/v1/runtime/resolve", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")), enforcing=True)
|
|
203
|
+
|
|
204
|
+
def resolve_boundary(self, execution_boundary: Mapping[str, Json], *, semantic_context: Mapping[str, Json] | None = None, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
205
|
+
"""Resolve a route without requiring a caller-selected BehaviorPoint."""
|
|
206
|
+
if not isinstance(execution_boundary, Mapping) or not execution_boundary:
|
|
207
|
+
raise ValueError("SDK_EXECUTION_BOUNDARY_REQUIRED")
|
|
208
|
+
body: dict[str, Json] = {"executionBoundary": dict(execution_boundary)}
|
|
209
|
+
if semantic_context is not None: body["semanticContext"] = dict(semantic_context)
|
|
210
|
+
if idempotency_key: body["idempotencyKey"] = idempotency_key
|
|
211
|
+
return self.resolve_runtime(body)
|
|
212
|
+
|
|
213
|
+
def resolve_semantic(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
214
|
+
if not str(request.get("input", "")).strip(): raise ValueError("SDK_SEMANTIC_INPUT_REQUIRED")
|
|
215
|
+
return self._request("POST", "/v1/semantic/resolve", body=dict(request), idempotency_key=idempotency_key or f"sdk:semantic:{request['input']}")
|
|
216
|
+
|
|
217
|
+
def resolve_semantic_runtime(self, request: Mapping[str, Json]) -> ViraResponse | None:
|
|
218
|
+
"""Resolve free-form user intent, then resolve its governed runtime behavior."""
|
|
219
|
+
if not str(request.get("input", "")).strip(): raise ValueError("SDK_SEMANTIC_INPUT_REQUIRED")
|
|
220
|
+
semantic = self.resolve_semantic(request, idempotency_key=str(request.get("idempotencyKey", "")) or None)
|
|
221
|
+
if not semantic: return None
|
|
222
|
+
body = semantic.body if isinstance(semantic.body, Mapping) else {}
|
|
223
|
+
receipt = body.get("receipt") if isinstance(body.get("receipt"), Mapping) else {}
|
|
224
|
+
if receipt.get("outcome") != "select" or not str(receipt.get("taskKey", "")).strip():
|
|
225
|
+
return ViraResponse(status=semantic.status, body={"semantic": semantic.body, "runtime": None}, request_id=semantic.request_id)
|
|
226
|
+
capsules = body.get("capsules") if isinstance(body.get("capsules"), list) else []
|
|
227
|
+
capsule = capsules[0] if capsules and isinstance(capsules[0], Mapping) else {}
|
|
228
|
+
behavior_point = str(capsule.get("behaviorKey") or receipt.get("taskKey") or "").strip()
|
|
229
|
+
if not behavior_point:
|
|
230
|
+
return ViraResponse(status=semantic.status, body={"semantic": semantic.body, "runtime": None}, request_id=semantic.request_id)
|
|
231
|
+
runtime_request: dict[str, Json] = {"semanticContext": {"taskKey": str(receipt.get("taskKey")), "behaviorPoint": behavior_point, "capsule": dict(capsule)}, "idempotencyKey": f"sdk:runtime:{request.get('idempotencyKey') or receipt.get('requestId') or request['input']}"}
|
|
232
|
+
if isinstance(request.get("executionBoundary"), Mapping): runtime_request["executionBoundary"] = dict(request["executionBoundary"])
|
|
233
|
+
runtime = self.resolve_runtime(runtime_request)
|
|
234
|
+
return ViraResponse(status=runtime.status, body={"semantic": semantic.body, "runtime": runtime.body}, request_id=runtime.request_id)
|
|
235
|
+
|
|
236
|
+
def list_behavior_programs(self) -> ViraResponse | None: return self._request("GET", "/v1/behavior-programs")
|
|
237
|
+
def list_behavior_strategies(self) -> ViraResponse | None: return self._request("GET", "/v1/behavior-strategies")
|
|
238
|
+
def get_behavior_strategy(self, strategy_id: str) -> ViraResponse | None:
|
|
239
|
+
if not strategy_id.strip(): raise ValueError("SDK_STRATEGY_ID_REQUIRED")
|
|
240
|
+
from urllib.parse import quote
|
|
241
|
+
return self._request("GET", f"/v1/behavior-strategies/{quote(strategy_id, safe='')}")
|
|
242
|
+
def create_behavior_strategy(self, definition: Json, *, strategy_id: str | None = None) -> ViraResponse | None:
|
|
243
|
+
if not definition: raise ValueError("SDK_STRATEGY_DEFINITION_REQUIRED")
|
|
244
|
+
return self._request("POST", "/v1/behavior-strategies", body={"definition": definition, **({"id": strategy_id} if strategy_id else {})})
|
|
245
|
+
def validate_behavior_strategy(self, strategy_id: str) -> ViraResponse | None:
|
|
246
|
+
if not strategy_id.strip(): raise ValueError("SDK_STRATEGY_ID_REQUIRED")
|
|
247
|
+
from urllib.parse import quote
|
|
248
|
+
return self._request("POST", f"/v1/behavior-strategies/{quote(strategy_id, safe='')}/validate", body={})
|
|
249
|
+
def simulate_behavior_strategy(self, strategy_id: str, initial_state: Mapping[str, Json] | None = None) -> ViraResponse | None:
|
|
250
|
+
if not strategy_id.strip(): raise ValueError("SDK_STRATEGY_ID_REQUIRED")
|
|
251
|
+
from urllib.parse import quote
|
|
252
|
+
return self._request("POST", f"/v1/behavior-strategies/{quote(strategy_id, safe='')}/simulate", body={"initialState": dict(initial_state or {})})
|
|
253
|
+
def start_strategy_execution(self, artifact_digest: str, *, organization_id: str, project_id: str, environment_id: str, revision_id: str, idempotency_key: str, nonce: str, expires_at: int | float, decision_plan_id: str, candidates: Sequence[Mapping[str, Json]], policy: str, behavior_point: str | None = None, execution_boundary: Mapping[str, Json] | None = None, allowed_ids: Sequence[str] | None = None, calibration_id: str | None = None, post_model_judge: Json | None = None, shadow: Json | None = None, trace_id: str | None = None, execution_id: str | None = None, state: Mapping[str, Json] | None = None) -> ViraResponse | None:
|
|
254
|
+
if not artifact_digest.strip(): raise ValueError("SDK_ARTIFACT_DIGEST_REQUIRED")
|
|
255
|
+
for value in (organization_id, project_id, environment_id, revision_id, idempotency_key, nonce, decision_plan_id, policy):
|
|
256
|
+
if not isinstance(value, str) or not value.strip(): raise ValueError("SDK_STRATEGY_START_FIELD_REQUIRED")
|
|
257
|
+
if not isinstance(expires_at, (int, float)): raise ValueError("SDK_EXPIRY_REQUIRED")
|
|
258
|
+
if not candidates: raise ValueError("SDK_STRATEGY_CANDIDATES_REQUIRED")
|
|
259
|
+
if behavior_point is not None and not behavior_point.strip(): raise ValueError("SDK_BEHAVIOR_POINT_REQUIRED")
|
|
260
|
+
body: Json = {"artifactDigest": artifact_digest, "organizationId": organization_id, "projectId": project_id, "environmentId": environment_id, "revisionId": revision_id, "idempotencyKey": idempotency_key, "nonce": nonce, "expiresAt": expires_at, "decisionPlanId": decision_plan_id, "candidates": [dict(candidate) for candidate in candidates], "policy": policy, **({"behaviorPoint": behavior_point} if behavior_point else {}), **({"executionBoundary": dict(execution_boundary)} if execution_boundary is not None else {}), **({"allowedIds": list(allowed_ids)} if allowed_ids is not None else {}), **({"calibrationId": calibration_id} if calibration_id else {}), **({"postModelJudge": post_model_judge} if post_model_judge is not None else {}), **({"shadow": shadow} if shadow is not None else {}), **({"traceId": trace_id} if trace_id else {}), **({"executionId": execution_id} if execution_id else {}), **({"state": dict(state)} if state is not None else {})}
|
|
261
|
+
return self._request("POST", "/v1/runtime/strategy-executions", body=body, idempotency_key=idempotency_key)
|
|
262
|
+
def get_strategy_execution(self, execution_id: str) -> ViraResponse | None:
|
|
263
|
+
if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
|
|
264
|
+
from urllib.parse import quote
|
|
265
|
+
return self._request("GET", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}")
|
|
266
|
+
def get_strategy_execution_trace(self, execution_id: str) -> ViraResponse | None:
|
|
267
|
+
if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
|
|
268
|
+
from urllib.parse import quote
|
|
269
|
+
return self._request("GET", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}/trace")
|
|
270
|
+
def continue_strategy_execution(self, execution_id: str, authority_digest: str | None = None, *, state: Mapping[str, Json] | None = None, references: Mapping[str, Json] | None = None) -> ViraResponse | None:
|
|
271
|
+
if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
|
|
272
|
+
from urllib.parse import quote
|
|
273
|
+
body: dict[str, Json] = {}
|
|
274
|
+
if authority_digest: body["authorityDigest"] = authority_digest
|
|
275
|
+
if state is not None: body["state"] = dict(state)
|
|
276
|
+
if references is not None: body["references"] = dict(references)
|
|
277
|
+
return self._request("POST", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}/continue", body=body)
|
|
278
|
+
def cancel_strategy_execution(self, execution_id: str) -> ViraResponse | None:
|
|
279
|
+
if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
|
|
280
|
+
from urllib.parse import quote
|
|
281
|
+
return self._request("POST", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}/cancel", body={})
|
|
282
|
+
def list_packs(self) -> ViraResponse | None: return self._request("GET", "/v1/packs")
|
|
283
|
+
def import_pack(self, manifest: Json | None = None, objects: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None, archive_base64: str | None = None, file_name: str | None = None) -> ViraResponse | None:
|
|
284
|
+
if manifest is None and not archive_base64: raise ValueError("SDK_PACK_MANIFEST_OR_ARCHIVE_REQUIRED")
|
|
285
|
+
if archive_base64 and not file_name: raise ValueError("SDK_PACK_FILE_NAME_REQUIRED")
|
|
286
|
+
body: dict[str, Json] = {"objects": dict(objects or {})}
|
|
287
|
+
if manifest is not None: body["manifest"] = manifest
|
|
288
|
+
if archive_base64: body.update({"archiveBase64": archive_base64, "fileName": file_name})
|
|
289
|
+
return self._request("POST", "/v1/packs/import", body=body, idempotency_key=idempotency_key)
|
|
290
|
+
def install_pack(self, pack_id: str, variables: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
291
|
+
if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
|
|
292
|
+
from urllib.parse import quote
|
|
293
|
+
return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/install", body={"variables": dict(variables or {})}, idempotency_key=idempotency_key or f"sdk:pack:install:{pack_id}")
|
|
294
|
+
def evaluate_pack(self, pack_id: str, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
295
|
+
if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
|
|
296
|
+
from urllib.parse import quote
|
|
297
|
+
return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/evaluate", body=dict(request), idempotency_key=idempotency_key or f"sdk:pack:evaluate:{pack_id}")
|
|
298
|
+
def release_pack(self, pack_id: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
299
|
+
if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
|
|
300
|
+
from urllib.parse import quote
|
|
301
|
+
return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/release", body={}, idempotency_key=idempotency_key or f"sdk:pack:release:{pack_id}")
|
|
302
|
+
def approve_pack(self, pack_id: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
303
|
+
if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
|
|
304
|
+
from urllib.parse import quote
|
|
305
|
+
return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/approve", body={}, idempotency_key=idempotency_key or f"sdk:pack:approve:{pack_id}")
|
|
306
|
+
def preview_pack_upgrade(self, pack_id: str, incoming: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
307
|
+
if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
|
|
308
|
+
if not isinstance(incoming, Mapping): raise ValueError("SDK_PACK_UPGRADE_INPUT_REQUIRED")
|
|
309
|
+
from urllib.parse import quote
|
|
310
|
+
return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/upgrade-preview", body={"incoming": dict(incoming)}, idempotency_key=idempotency_key or f"sdk:pack:upgrade-preview:{pack_id}")
|
|
311
|
+
def get_behavior_program(self, program_id: str) -> ViraResponse | None:
|
|
312
|
+
if not program_id.strip(): raise ValueError("SDK_PROGRAM_ID_REQUIRED")
|
|
313
|
+
from urllib.parse import quote
|
|
314
|
+
return self._request("GET", f"/v1/behavior-programs/{quote(program_id, safe='')}")
|
|
315
|
+
def create_behavior_program(self, program: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
316
|
+
if not isinstance(program, Mapping) or not str(program.get("id", "")).strip(): raise ValueError("SDK_PROGRAM_REQUIRED")
|
|
317
|
+
return self._request("POST", "/v1/behavior-programs", body=dict(program), idempotency_key=idempotency_key or f"sdk:program:{program['id']}")
|
|
318
|
+
|
|
319
|
+
def list_discovery_candidates(self, review_state: str | None = None) -> ViraResponse | None:
|
|
320
|
+
return self._request("GET", "/v1/discovery/candidates", query={"reviewState": review_state} if review_state else None)
|
|
321
|
+
def start_evaluation(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
322
|
+
return self._request("POST", "/v1/evaluations", body=dict(request), idempotency_key=idempotency_key)
|
|
323
|
+
def get_evaluation(self, evaluation_id: str) -> ViraResponse | None:
|
|
324
|
+
if not evaluation_id.strip(): raise ValueError("SDK_EVALUATION_ID_REQUIRED")
|
|
325
|
+
from urllib.parse import quote
|
|
326
|
+
return self._request("GET", f"/v1/evaluations/{quote(evaluation_id, safe='')}")
|
|
327
|
+
def create_release(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
328
|
+
return self._request("POST", "/v1/releases", body=dict(request), idempotency_key=idempotency_key)
|
|
329
|
+
def submit_teach_evidence(self, evidence: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
330
|
+
return self._request("POST", "/v1/teach/evidence", body=dict(evidence), idempotency_key=idempotency_key)
|
|
331
|
+
|
|
332
|
+
def attest_host_capabilities(self, attestation: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
333
|
+
if attestation.get("schemaVersion") != 2:
|
|
334
|
+
raise ValueError("HOST_CAPABILITY_SCHEMA_VERSION_INVALID")
|
|
335
|
+
return self._request("POST", "/v1/runtime/capabilities/attest", body=dict(attestation), idempotency_key=idempotency_key)
|
|
336
|
+
|
|
337
|
+
def resolve_with_capabilities(self, attestation_id: str, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
338
|
+
context = request.get("executionContext")
|
|
339
|
+
if not attestation_id.strip() or not isinstance(context, Mapping) or not str(context.get("executionId", "")).strip():
|
|
340
|
+
raise ValueError("HOST_CAPABILITY_EXECUTION_ID_REQUIRED")
|
|
341
|
+
return self._request("POST", "/v1/runtime/capabilities/resolve", body={"attestationId": attestation_id, "request": dict(request)}, idempotency_key=idempotency_key, enforcing=True)
|
|
342
|
+
|
|
343
|
+
def trace_with_capabilities(self, decision_digest: str, trace: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
344
|
+
context = trace.get("executionContext")
|
|
345
|
+
if not decision_digest.strip() or not str(trace.get("id", "")).strip() or not isinstance(context, Mapping) or not str(context.get("executionId", "")).strip():
|
|
346
|
+
raise ValueError("HOST_CAPABILITY_TRACE_IDENTITY_REQUIRED")
|
|
347
|
+
return self._request("POST", "/v1/runtime/capabilities/traces", body={"decisionDigest": decision_digest, "trace": dict(trace)}, idempotency_key=idempotency_key)
|
|
348
|
+
|
|
349
|
+
def model_selection(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
350
|
+
return self._request("POST", "/v1/model-serving/resolve", body=dict(request), idempotency_key=idempotency_key, enforcing=True)
|
|
351
|
+
|
|
352
|
+
def served_trace(self, trace: Mapping[str, Json], selection: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
353
|
+
if not str(trace.get("id", "")).strip() or not isinstance(trace.get("executionContext"), Mapping):
|
|
354
|
+
raise ValueError("MODEL_SERVING_TRACE_IDENTITY_REQUIRED")
|
|
355
|
+
return self._request("POST", "/v1/model-serving/traces", body={"trace": dict(trace), "selection": dict(selection)}, idempotency_key=idempotency_key)
|
|
356
|
+
|
|
357
|
+
def teach(self, evidence: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
358
|
+
return self._request("POST", "/v1/teach/evidence", body=dict(evidence), idempotency_key=idempotency_key)
|
|
359
|
+
|
|
360
|
+
def compare_rollout(self, deployment_id: str, comparison: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
361
|
+
if not deployment_id.strip():
|
|
362
|
+
raise ValueError("DEPLOYMENT_ID_REQUIRED")
|
|
363
|
+
from urllib.parse import quote
|
|
364
|
+
return self._request("POST", f"/v1/deployments/{quote(deployment_id, safe='')}/comparisons", body=dict(comparison), idempotency_key=idempotency_key)
|
|
365
|
+
|
|
366
|
+
def rollout_health(self, deployment_id: str, health: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
367
|
+
if not deployment_id.strip():
|
|
368
|
+
raise ValueError("DEPLOYMENT_ID_REQUIRED")
|
|
369
|
+
from urllib.parse import quote
|
|
370
|
+
return self._request("POST", f"/v1/deployments/{quote(deployment_id, safe='')}/authoritative-health", body=dict(health), idempotency_key=idempotency_key)
|
|
371
|
+
|
|
372
|
+
def connection_health(self, connection_id: str) -> ViraResponse | None: return self._request("GET", f"/v1/connections/{connection_id}/health")
|
|
373
|
+
def inventory(self, system_id: str) -> ViraResponse | None: return self._request("GET", f"/v1/ai-systems/{system_id}/inventory")
|
|
374
|
+
def test_connection(self, connection_id: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
|
|
375
|
+
if not connection_id.strip(): raise ValueError("SDK_CONNECTION_ID_REQUIRED")
|
|
376
|
+
from urllib.parse import quote
|
|
377
|
+
return self._request("POST", f"/v1/connections/{quote(connection_id, safe='')}/test", body={}, idempotency_key=idempotency_key)
|
|
378
|
+
|
|
379
|
+
def close(self) -> None:
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def __enter__(self) -> "ViraClient":
|
|
383
|
+
return self
|
|
384
|
+
|
|
385
|
+
def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
|
|
386
|
+
self.close()
|