viewise 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- viewise-0.1.2/PKG-INFO +92 -0
- viewise-0.1.2/README.md +36 -0
- viewise-0.1.2/README.pypi.md +73 -0
- viewise-0.1.2/pyproject.toml +44 -0
- viewise-0.1.2/setup.cfg +4 -0
- viewise-0.1.2/viewise/__init__.py +876 -0
- viewise-0.1.2/viewise.egg-info/PKG-INFO +92 -0
- viewise-0.1.2/viewise.egg-info/SOURCES.txt +9 -0
- viewise-0.1.2/viewise.egg-info/dependency_links.txt +1 -0
- viewise-0.1.2/viewise.egg-info/requires.txt +1 -0
- viewise-0.1.2/viewise.egg-info/top_level.txt +1 -0
viewise-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: viewise
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python SDK for Viewise semantic telemetry ingestion.
|
|
5
|
+
Author: Viewise
|
|
6
|
+
Project-URL: Homepage, https://github.com/Tanishv/xalpha
|
|
7
|
+
Project-URL: Repository, https://github.com/Tanishv/xalpha
|
|
8
|
+
Project-URL: Issues, https://github.com/Tanishv/xalpha/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
|
|
20
|
+
# Viewise Python SDK
|
|
21
|
+
|
|
22
|
+
Viewise observes agent runs through a small semantic telemetry contract. The SDK
|
|
23
|
+
sends events to a Viewise backend and keeps agent code resilient when telemetry
|
|
24
|
+
is unavailable.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -m pip install viewise
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configure
|
|
33
|
+
|
|
34
|
+
Set the runtime environment values copied from the Viewise Setup page:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export VIEWISE_BASE_URL="https://viewise-backend.fly.dev"
|
|
38
|
+
export VIEWISE_INGEST_KEY="<workspace-api-key-from-viewise-setup>"
|
|
39
|
+
export VIEWISE_AGENT_REF="workspace.agentx"
|
|
40
|
+
export VIEWISE_AGENT_NAME="agentx"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You can also configure the SDK in code:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import viewise
|
|
47
|
+
|
|
48
|
+
viewise.configure(
|
|
49
|
+
base_url="https://viewise-backend.fly.dev",
|
|
50
|
+
ingest_key="<workspace-api-key-from-viewise-setup>",
|
|
51
|
+
agent_ref="workspace.agentx",
|
|
52
|
+
agent_name="agentx",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Never commit API keys or place them in prompts, logs, or source code.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
viewise.start_run(run_ref, task, occurred_at=None)
|
|
62
|
+
viewise.observe_evidence(run_ref, source, retrieved_at=None, evidence_ref=None)
|
|
63
|
+
viewise.complete_run(run_ref, result, occurred_at=None)
|
|
64
|
+
viewise.fail_run(run_ref, exc_or_error, occurred_at=None)
|
|
65
|
+
viewise.sanitize_error(exc_or_error)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Supported event types are:
|
|
69
|
+
|
|
70
|
+
- `run.started`
|
|
71
|
+
- `evidence.observed`
|
|
72
|
+
- `run.completed`
|
|
73
|
+
- `run.failed`
|
|
74
|
+
|
|
75
|
+
Top-level telemetry calls return a delivery result. They do not raise when
|
|
76
|
+
Viewise or the network is unavailable, so agent work can continue.
|
|
77
|
+
|
|
78
|
+
## Smoke Test
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import viewise
|
|
82
|
+
|
|
83
|
+
run_ref = "local-run-001"
|
|
84
|
+
|
|
85
|
+
viewise.start_run(run_ref, "SDK install smoke test")
|
|
86
|
+
viewise.observe_evidence(
|
|
87
|
+
run_ref,
|
|
88
|
+
{"provider": "local", "kind": "smoke"},
|
|
89
|
+
evidence_ref="smoke-001",
|
|
90
|
+
)
|
|
91
|
+
viewise.complete_run(run_ref, {"status": "ok"})
|
|
92
|
+
```
|
viewise-0.1.2/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Financial Agent Lab
|
|
2
|
+
|
|
3
|
+
This repository contains the xalpha observability product and its maintained
|
|
4
|
+
evidence/wiki layer.
|
|
5
|
+
|
|
6
|
+
- [xalpha](xalpha/README.md) is the planned SaaS for observing, explaining, and
|
|
7
|
+
evaluating financial agents. Its current phase and gate status are maintained
|
|
8
|
+
in [xalpha/PHASES.md](xalpha/PHASES.md).
|
|
9
|
+
- [Professor](https://github.com/Tanishv/professor) is the separate dogfood
|
|
10
|
+
financial research agent. It owns research runtime code and local trace
|
|
11
|
+
generation.
|
|
12
|
+
|
|
13
|
+
The [mission wiki](wiki/index.md) keeps historical experiment evidence and
|
|
14
|
+
learnings from Professor that inform xalpha's design. The broader charter remains
|
|
15
|
+
[`vision.md`](vision.md), and durable working rules live in
|
|
16
|
+
[`AGENTS.md`](AGENTS.md).
|
|
17
|
+
|
|
18
|
+
## Repository map
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
xalpha/ SaaS source, local stack, source-of-truth docs, and build guidance
|
|
22
|
+
wiki/ Shared experiments, learnings, roadmap, and distribution notes
|
|
23
|
+
sources/ Shared raw source material and intake rules
|
|
24
|
+
scripts/ Repository-level maintenance checks
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Offline validation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
source .venv/bin/activate
|
|
31
|
+
docker compose -f xalpha/docker-compose.yml up -d postgres
|
|
32
|
+
python -m scripts.repository_checks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
These commands do not call the OpenAI API. Stop the local PostgreSQL service
|
|
36
|
+
afterward with `docker compose -f xalpha/docker-compose.yml down`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Viewise Python SDK
|
|
2
|
+
|
|
3
|
+
Viewise observes agent runs through a small semantic telemetry contract. The SDK
|
|
4
|
+
sends events to a Viewise backend and keeps agent code resilient when telemetry
|
|
5
|
+
is unavailable.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python -m pip install viewise
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
Set the runtime environment values copied from the Viewise Setup page:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export VIEWISE_BASE_URL="https://viewise-backend.fly.dev"
|
|
19
|
+
export VIEWISE_INGEST_KEY="<workspace-api-key-from-viewise-setup>"
|
|
20
|
+
export VIEWISE_AGENT_REF="workspace.agentx"
|
|
21
|
+
export VIEWISE_AGENT_NAME="agentx"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can also configure the SDK in code:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import viewise
|
|
28
|
+
|
|
29
|
+
viewise.configure(
|
|
30
|
+
base_url="https://viewise-backend.fly.dev",
|
|
31
|
+
ingest_key="<workspace-api-key-from-viewise-setup>",
|
|
32
|
+
agent_ref="workspace.agentx",
|
|
33
|
+
agent_name="agentx",
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Never commit API keys or place them in prompts, logs, or source code.
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
viewise.start_run(run_ref, task, occurred_at=None)
|
|
43
|
+
viewise.observe_evidence(run_ref, source, retrieved_at=None, evidence_ref=None)
|
|
44
|
+
viewise.complete_run(run_ref, result, occurred_at=None)
|
|
45
|
+
viewise.fail_run(run_ref, exc_or_error, occurred_at=None)
|
|
46
|
+
viewise.sanitize_error(exc_or_error)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Supported event types are:
|
|
50
|
+
|
|
51
|
+
- `run.started`
|
|
52
|
+
- `evidence.observed`
|
|
53
|
+
- `run.completed`
|
|
54
|
+
- `run.failed`
|
|
55
|
+
|
|
56
|
+
Top-level telemetry calls return a delivery result. They do not raise when
|
|
57
|
+
Viewise or the network is unavailable, so agent work can continue.
|
|
58
|
+
|
|
59
|
+
## Smoke Test
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import viewise
|
|
63
|
+
|
|
64
|
+
run_ref = "local-run-001"
|
|
65
|
+
|
|
66
|
+
viewise.start_run(run_ref, "SDK install smoke test")
|
|
67
|
+
viewise.observe_evidence(
|
|
68
|
+
run_ref,
|
|
69
|
+
{"provider": "local", "kind": "smoke"},
|
|
70
|
+
evidence_ref="smoke-001",
|
|
71
|
+
)
|
|
72
|
+
viewise.complete_run(run_ref, {"status": "ok"})
|
|
73
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "viewise"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Python SDK for Viewise semantic telemetry ingestion."
|
|
9
|
+
readme = "README.pypi.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["httpx>=0.27"]
|
|
12
|
+
authors = [{ name = "Viewise" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/Tanishv/xalpha"
|
|
25
|
+
Repository = "https://github.com/Tanishv/xalpha"
|
|
26
|
+
Issues = "https://github.com/Tanishv/xalpha/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
include = ["viewise*"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 88
|
|
33
|
+
target-version = "py313"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
37
|
+
|
|
38
|
+
[tool.mypy]
|
|
39
|
+
python_version = "3.13"
|
|
40
|
+
strict = true
|
|
41
|
+
|
|
42
|
+
[[tool.mypy.overrides]]
|
|
43
|
+
module = "fastapi.*"
|
|
44
|
+
ignore_missing_imports = false
|
viewise-0.1.2/setup.cfg
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from time import sleep
|
|
10
|
+
from typing import Any, Protocol, cast
|
|
11
|
+
from uuid import UUID, uuid4
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.2"
|
|
16
|
+
SCHEMA_VERSION = "1.0"
|
|
17
|
+
EVENTS_ENDPOINT = "/v1/semantic/events"
|
|
18
|
+
SUPPORTED_EVENT_TYPES = frozenset(
|
|
19
|
+
{"run.started", "evidence.observed", "run.completed", "run.failed"}
|
|
20
|
+
)
|
|
21
|
+
CONTEXT_SNAPSHOT_KEYS = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"raw_trace_sha256",
|
|
24
|
+
"artifact_ref",
|
|
25
|
+
"response_id",
|
|
26
|
+
"usage",
|
|
27
|
+
"web_actions_count",
|
|
28
|
+
"cited_sources_count",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
_CREDENTIAL_PATTERNS = (
|
|
32
|
+
re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]+", re.IGNORECASE),
|
|
33
|
+
re.compile(r"xak_[A-Za-z0-9._~+/=-]+"),
|
|
34
|
+
re.compile(r"sk-[A-Za-z0-9._~+/=-]+"),
|
|
35
|
+
re.compile(
|
|
36
|
+
r"(?i)(api[_ -]?key|token|secret|authorization)(['\":=\s]+)"
|
|
37
|
+
r"([A-Za-z0-9._~+/=-]+)"
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
_REQUEST_DETAIL_PATTERNS = (
|
|
41
|
+
re.compile(
|
|
42
|
+
r"(?i)(request[_ -]?id)(['\":=\s]+)([A-Za-z0-9._~+/=-]+)"
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
_SAFE_ERROR_MESSAGE_LIMIT = 500
|
|
46
|
+
_runtime_config: RuntimeConfig | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ViewiseError(RuntimeError):
|
|
50
|
+
"""Base error raised by the Viewise SDK."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ViewiseConfigError(ViewiseError):
|
|
54
|
+
"""Raised when SDK configuration or call input is invalid."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ViewiseAuthError(ViewiseError):
|
|
58
|
+
"""Raised when Viewise rejects the configured ingest key."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ViewiseRequestError(ViewiseError):
|
|
62
|
+
"""Raised when Viewise rejects a semantic event."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ViewiseServiceError(ViewiseError):
|
|
66
|
+
"""Raised when Viewise or the network cannot accept an event after retries."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Response(Protocol):
|
|
70
|
+
status_code: int
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def text(self) -> str: ...
|
|
74
|
+
|
|
75
|
+
def json(self) -> object: ...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class HttpSender(Protocol):
|
|
79
|
+
def post(
|
|
80
|
+
self,
|
|
81
|
+
url: str,
|
|
82
|
+
*,
|
|
83
|
+
headers: dict[str, str],
|
|
84
|
+
json: dict[str, Any],
|
|
85
|
+
) -> Response: ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class EventReceipt:
|
|
90
|
+
event_id: str
|
|
91
|
+
run_id: str
|
|
92
|
+
event_type: str
|
|
93
|
+
status: str
|
|
94
|
+
run_status: str
|
|
95
|
+
raw: Mapping[str, object]
|
|
96
|
+
agent_ref: str | None = None
|
|
97
|
+
run_ref: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class DeliveryResult:
|
|
102
|
+
delivered: bool
|
|
103
|
+
event_type: str
|
|
104
|
+
run_ref: str
|
|
105
|
+
event_id: str = ""
|
|
106
|
+
run_id: str = ""
|
|
107
|
+
status: str = ""
|
|
108
|
+
run_status: str = ""
|
|
109
|
+
raw: Mapping[str, object] | None = None
|
|
110
|
+
agent_ref: str | None = None
|
|
111
|
+
error: str | None = None
|
|
112
|
+
skipped_metadata_keys: tuple[str, ...] = ()
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_receipt(
|
|
116
|
+
cls,
|
|
117
|
+
receipt: EventReceipt,
|
|
118
|
+
*,
|
|
119
|
+
run_ref: str,
|
|
120
|
+
skipped_metadata_keys: tuple[str, ...] = (),
|
|
121
|
+
) -> DeliveryResult:
|
|
122
|
+
return cls(
|
|
123
|
+
delivered=True,
|
|
124
|
+
event_type=receipt.event_type,
|
|
125
|
+
run_ref=receipt.run_ref or run_ref,
|
|
126
|
+
event_id=receipt.event_id,
|
|
127
|
+
run_id=receipt.run_id,
|
|
128
|
+
status=receipt.status,
|
|
129
|
+
run_status=receipt.run_status,
|
|
130
|
+
raw=receipt.raw,
|
|
131
|
+
agent_ref=receipt.agent_ref,
|
|
132
|
+
skipped_metadata_keys=skipped_metadata_keys,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class RuntimeConfig:
|
|
138
|
+
base_url: str
|
|
139
|
+
ingest_key: str
|
|
140
|
+
agent_ref: str
|
|
141
|
+
agent_name: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class HttpxSender:
|
|
145
|
+
def __init__(self, *, timeout: float = 10.0) -> None:
|
|
146
|
+
self._timeout = timeout
|
|
147
|
+
|
|
148
|
+
def post(
|
|
149
|
+
self,
|
|
150
|
+
url: str,
|
|
151
|
+
*,
|
|
152
|
+
headers: dict[str, str],
|
|
153
|
+
json: dict[str, Any],
|
|
154
|
+
) -> Response:
|
|
155
|
+
return httpx.post(url, headers=headers, json=json, timeout=self._timeout)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Client:
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
base_url: str,
|
|
163
|
+
ingest_key: str | None = None,
|
|
164
|
+
api_key: str | None = None,
|
|
165
|
+
agent_ref: str | None = None,
|
|
166
|
+
agent_id: str | None = None,
|
|
167
|
+
sender: HttpSender | None = None,
|
|
168
|
+
max_attempts: int = 3,
|
|
169
|
+
retry_backoff_seconds: float = 0.0,
|
|
170
|
+
idempotency_prefix: str = "viewise",
|
|
171
|
+
credential_label: str = "ingest key",
|
|
172
|
+
agent_name: str | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
self._base_url = require_non_blank(base_url, "base url").rstrip("/")
|
|
175
|
+
self._credential_label = require_non_blank(credential_label, "credential label")
|
|
176
|
+
self._ingest_key = require_one_key(
|
|
177
|
+
ingest_key=ingest_key,
|
|
178
|
+
api_key=api_key,
|
|
179
|
+
label=self._credential_label,
|
|
180
|
+
)
|
|
181
|
+
self._agent_ref = normalize_ref(agent_ref, "agent ref") if agent_ref else None
|
|
182
|
+
self._agent_id = require_uuid_text(agent_id, "agent id") if agent_id else None
|
|
183
|
+
if (self._agent_ref is None) == (self._agent_id is None):
|
|
184
|
+
raise ViewiseConfigError("exactly one agent_ref or agent_id is required")
|
|
185
|
+
if max_attempts < 1:
|
|
186
|
+
raise ViewiseConfigError("max_attempts must be at least 1")
|
|
187
|
+
if retry_backoff_seconds < 0:
|
|
188
|
+
raise ViewiseConfigError("retry_backoff_seconds cannot be negative")
|
|
189
|
+
self._sender = sender or HttpxSender()
|
|
190
|
+
self._max_attempts = max_attempts
|
|
191
|
+
self._retry_backoff_seconds = retry_backoff_seconds
|
|
192
|
+
self._idempotency_prefix = require_non_blank(
|
|
193
|
+
idempotency_prefix,
|
|
194
|
+
"idempotency prefix",
|
|
195
|
+
)
|
|
196
|
+
self._agent_name = agent_name.strip() if agent_name else None
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_env(
|
|
200
|
+
cls,
|
|
201
|
+
*,
|
|
202
|
+
sender: HttpSender | None = None,
|
|
203
|
+
max_attempts: int = 3,
|
|
204
|
+
retry_backoff_seconds: float = 0.0,
|
|
205
|
+
) -> Client:
|
|
206
|
+
base_url = require_env("VIEWISE_BASE_URL")
|
|
207
|
+
ingest_key = os.environ.get("VIEWISE_INGEST_KEY") or os.environ.get(
|
|
208
|
+
"VIEWISE_API_KEY"
|
|
209
|
+
)
|
|
210
|
+
if ingest_key is None or not ingest_key.strip():
|
|
211
|
+
raise ViewiseConfigError(
|
|
212
|
+
"VIEWISE_INGEST_KEY or VIEWISE_API_KEY is required"
|
|
213
|
+
)
|
|
214
|
+
agent_ref = require_env("VIEWISE_AGENT_REF")
|
|
215
|
+
agent_name = require_env("VIEWISE_AGENT_NAME")
|
|
216
|
+
return cls(
|
|
217
|
+
base_url=base_url,
|
|
218
|
+
ingest_key=ingest_key,
|
|
219
|
+
agent_ref=agent_ref,
|
|
220
|
+
agent_name=agent_name,
|
|
221
|
+
sender=sender,
|
|
222
|
+
max_attempts=max_attempts,
|
|
223
|
+
retry_backoff_seconds=retry_backoff_seconds,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def start_run(
|
|
227
|
+
self,
|
|
228
|
+
*,
|
|
229
|
+
run_ref: str | None = None,
|
|
230
|
+
run_id: str | None = None,
|
|
231
|
+
task: str,
|
|
232
|
+
occurred_at: datetime | None = None,
|
|
233
|
+
effective_config: Mapping[str, object] | None = None,
|
|
234
|
+
run_context_snapshot: Mapping[str, object] | None = None,
|
|
235
|
+
experiment_id: str | None = None,
|
|
236
|
+
entity_ref: str | None = None,
|
|
237
|
+
) -> EventReceipt:
|
|
238
|
+
payload: dict[str, object] = {"task": require_non_blank(task, "task")}
|
|
239
|
+
if effective_config is not None:
|
|
240
|
+
payload["effective_config"] = dict(effective_config)
|
|
241
|
+
if run_context_snapshot is not None:
|
|
242
|
+
payload["run_context_snapshot"] = compact_context_snapshot(
|
|
243
|
+
run_context_snapshot
|
|
244
|
+
)
|
|
245
|
+
if experiment_id is not None:
|
|
246
|
+
payload["experiment_id"] = require_non_blank(experiment_id, "experiment id")
|
|
247
|
+
if entity_ref is not None:
|
|
248
|
+
payload["entity_ref"] = require_non_blank(entity_ref, "entity ref")
|
|
249
|
+
return self._post_event(
|
|
250
|
+
run_ref=run_ref,
|
|
251
|
+
run_id=run_id,
|
|
252
|
+
event_type="run.started",
|
|
253
|
+
occurred_at=occurred_at,
|
|
254
|
+
payload=payload,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def observe_evidence(
|
|
258
|
+
self,
|
|
259
|
+
*,
|
|
260
|
+
run_ref: str | None = None,
|
|
261
|
+
run_id: str | None = None,
|
|
262
|
+
source: object,
|
|
263
|
+
retrieved_at: datetime,
|
|
264
|
+
evidence_ref: str | None = None,
|
|
265
|
+
occurred_at: datetime | None = None,
|
|
266
|
+
published_at: datetime | None = None,
|
|
267
|
+
effective_at: datetime | None = None,
|
|
268
|
+
effective_period_start_at: datetime | None = None,
|
|
269
|
+
effective_period_end_at: datetime | None = None,
|
|
270
|
+
available_at: datetime | None = None,
|
|
271
|
+
available_at_status: str | None = None,
|
|
272
|
+
) -> EventReceipt:
|
|
273
|
+
source_text = normalize_source(source)
|
|
274
|
+
payload: dict[str, object] = {
|
|
275
|
+
"source": source_text,
|
|
276
|
+
"retrieved_at": format_timestamp(retrieved_at),
|
|
277
|
+
}
|
|
278
|
+
add_optional_timestamp(payload, "published_at", published_at)
|
|
279
|
+
add_optional_timestamp(payload, "effective_at", effective_at)
|
|
280
|
+
add_optional_timestamp(
|
|
281
|
+
payload,
|
|
282
|
+
"effective_period_start_at",
|
|
283
|
+
effective_period_start_at,
|
|
284
|
+
)
|
|
285
|
+
add_optional_timestamp(
|
|
286
|
+
payload,
|
|
287
|
+
"effective_period_end_at",
|
|
288
|
+
effective_period_end_at,
|
|
289
|
+
)
|
|
290
|
+
if available_at is None and available_at_status == "unknown":
|
|
291
|
+
payload["available_at"] = None
|
|
292
|
+
else:
|
|
293
|
+
add_optional_timestamp(payload, "available_at", available_at)
|
|
294
|
+
if available_at_status is not None:
|
|
295
|
+
payload["available_at_status"] = require_non_blank(
|
|
296
|
+
available_at_status,
|
|
297
|
+
"available_at_status",
|
|
298
|
+
)
|
|
299
|
+
return self._post_event(
|
|
300
|
+
run_ref=run_ref,
|
|
301
|
+
run_id=run_id,
|
|
302
|
+
event_type="evidence.observed",
|
|
303
|
+
occurred_at=occurred_at or retrieved_at,
|
|
304
|
+
payload=payload,
|
|
305
|
+
idempotency_suffix=evidence_ref or source_text,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def complete_run(
|
|
309
|
+
self,
|
|
310
|
+
*,
|
|
311
|
+
run_ref: str | None = None,
|
|
312
|
+
run_id: str | None = None,
|
|
313
|
+
output: object,
|
|
314
|
+
occurred_at: datetime | None = None,
|
|
315
|
+
) -> EventReceipt:
|
|
316
|
+
return self._post_event(
|
|
317
|
+
run_ref=run_ref,
|
|
318
|
+
run_id=run_id,
|
|
319
|
+
event_type="run.completed",
|
|
320
|
+
occurred_at=occurred_at,
|
|
321
|
+
payload={"output": output},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def fail_run(
|
|
325
|
+
self,
|
|
326
|
+
*,
|
|
327
|
+
run_ref: str | None = None,
|
|
328
|
+
run_id: str | None = None,
|
|
329
|
+
error: object,
|
|
330
|
+
occurred_at: datetime | None = None,
|
|
331
|
+
) -> EventReceipt:
|
|
332
|
+
return self._post_event(
|
|
333
|
+
run_ref=run_ref,
|
|
334
|
+
run_id=run_id,
|
|
335
|
+
event_type="run.failed",
|
|
336
|
+
occurred_at=occurred_at,
|
|
337
|
+
payload={"error": error},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def _post_event(
|
|
341
|
+
self,
|
|
342
|
+
*,
|
|
343
|
+
run_ref: str | None,
|
|
344
|
+
run_id: str | None,
|
|
345
|
+
event_type: str,
|
|
346
|
+
occurred_at: datetime | None,
|
|
347
|
+
payload: dict[str, object],
|
|
348
|
+
idempotency_suffix: str | None = None,
|
|
349
|
+
) -> EventReceipt:
|
|
350
|
+
identity = self._identity(run_ref=run_ref, run_id=run_id)
|
|
351
|
+
idempotency_key = self._idempotency_key(
|
|
352
|
+
event_type=event_type,
|
|
353
|
+
identity=identity,
|
|
354
|
+
suffix=idempotency_suffix,
|
|
355
|
+
)
|
|
356
|
+
last_error: Exception | None = None
|
|
357
|
+
for attempt in range(1, self._max_attempts + 1):
|
|
358
|
+
body = {
|
|
359
|
+
**identity,
|
|
360
|
+
"event_id": f"evt_{uuid4()}",
|
|
361
|
+
"schema_version": SCHEMA_VERSION,
|
|
362
|
+
"event_type": event_type,
|
|
363
|
+
"occurred_at": format_timestamp(occurred_at or datetime.now(UTC)),
|
|
364
|
+
"idempotency_key": idempotency_key,
|
|
365
|
+
"payload": payload,
|
|
366
|
+
}
|
|
367
|
+
try:
|
|
368
|
+
response = self._sender.post(
|
|
369
|
+
f"{self._base_url}{EVENTS_ENDPOINT}",
|
|
370
|
+
headers={"Authorization": f"Bearer {self._ingest_key}"},
|
|
371
|
+
json=body,
|
|
372
|
+
)
|
|
373
|
+
except httpx.TransportError as exc:
|
|
374
|
+
last_error = exc
|
|
375
|
+
if attempt < self._max_attempts:
|
|
376
|
+
self._sleep_before_retry()
|
|
377
|
+
continue
|
|
378
|
+
raise ViewiseServiceError(
|
|
379
|
+
redact(
|
|
380
|
+
f"semantic ingestion request failed: {exc}",
|
|
381
|
+
self._ingest_key,
|
|
382
|
+
)
|
|
383
|
+
) from exc
|
|
384
|
+
if response.status_code >= 500 and attempt < self._max_attempts:
|
|
385
|
+
self._sleep_before_retry()
|
|
386
|
+
continue
|
|
387
|
+
return parse_receipt(
|
|
388
|
+
response,
|
|
389
|
+
event_type=event_type,
|
|
390
|
+
secret=self._ingest_key,
|
|
391
|
+
credential_label=self._credential_label,
|
|
392
|
+
)
|
|
393
|
+
raise ViewiseServiceError(
|
|
394
|
+
redact(f"semantic ingestion request failed: {last_error}", self._ingest_key)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def _identity(self, *, run_ref: str | None, run_id: str | None) -> dict[str, str]:
|
|
398
|
+
if self._agent_ref is not None:
|
|
399
|
+
if run_id is not None:
|
|
400
|
+
raise ViewiseConfigError("run_id cannot be used with agent_ref")
|
|
401
|
+
return {
|
|
402
|
+
"agent_ref": self._agent_ref,
|
|
403
|
+
"run_ref": normalize_ref(run_ref, "run ref"),
|
|
404
|
+
}
|
|
405
|
+
if run_ref is not None:
|
|
406
|
+
raise ViewiseConfigError("run_ref cannot be used with agent_id")
|
|
407
|
+
if self._agent_id is None:
|
|
408
|
+
raise ViewiseConfigError("agent_id is required")
|
|
409
|
+
return {
|
|
410
|
+
"agent_id": self._agent_id,
|
|
411
|
+
"run_id": require_uuid_text(run_id, "run id"),
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
def _idempotency_key(
|
|
415
|
+
self,
|
|
416
|
+
*,
|
|
417
|
+
event_type: str,
|
|
418
|
+
identity: Mapping[str, str],
|
|
419
|
+
suffix: str | None,
|
|
420
|
+
) -> str:
|
|
421
|
+
if "agent_ref" in identity:
|
|
422
|
+
parts = [
|
|
423
|
+
self._idempotency_prefix,
|
|
424
|
+
identity["agent_ref"],
|
|
425
|
+
identity["run_ref"],
|
|
426
|
+
event_type,
|
|
427
|
+
]
|
|
428
|
+
else:
|
|
429
|
+
parts = [self._idempotency_prefix, event_type, identity["run_id"]]
|
|
430
|
+
if suffix is not None:
|
|
431
|
+
parts.append(normalize_idempotency_part(suffix))
|
|
432
|
+
return ":".join(parts)
|
|
433
|
+
|
|
434
|
+
def _sleep_before_retry(self) -> None:
|
|
435
|
+
if self._retry_backoff_seconds > 0:
|
|
436
|
+
sleep(self._retry_backoff_seconds)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def configure(
|
|
440
|
+
base_url: str,
|
|
441
|
+
ingest_key: str,
|
|
442
|
+
agent_ref: str,
|
|
443
|
+
agent_name: str,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Configure top-level SDK calls without reading environment variables."""
|
|
446
|
+
|
|
447
|
+
global _runtime_config
|
|
448
|
+
_runtime_config = RuntimeConfig(
|
|
449
|
+
base_url=require_non_blank(base_url, "VIEWISE_BASE_URL"),
|
|
450
|
+
ingest_key=require_non_blank(ingest_key, "VIEWISE_INGEST_KEY"),
|
|
451
|
+
agent_ref=normalize_ref(agent_ref, "VIEWISE_AGENT_REF"),
|
|
452
|
+
agent_name=require_non_blank(agent_name, "VIEWISE_AGENT_NAME"),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def configured_client(*, sender: HttpSender | None = None) -> Client:
|
|
457
|
+
if _runtime_config is None:
|
|
458
|
+
return Client.from_env(sender=sender)
|
|
459
|
+
return Client(
|
|
460
|
+
base_url=_runtime_config.base_url,
|
|
461
|
+
ingest_key=_runtime_config.ingest_key,
|
|
462
|
+
agent_ref=_runtime_config.agent_ref,
|
|
463
|
+
agent_name=_runtime_config.agent_name,
|
|
464
|
+
sender=sender,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _reset_configuration_for_tests() -> None:
|
|
469
|
+
global _runtime_config
|
|
470
|
+
_runtime_config = None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def start_run(
|
|
474
|
+
run_ref: str,
|
|
475
|
+
task: str,
|
|
476
|
+
occurred_at: datetime | None = None,
|
|
477
|
+
metadata: Mapping[str, object] | None = None,
|
|
478
|
+
*,
|
|
479
|
+
sender: HttpSender | None = None,
|
|
480
|
+
) -> DeliveryResult:
|
|
481
|
+
mapped, skipped = map_start_metadata(metadata)
|
|
482
|
+
return deliver_safely(
|
|
483
|
+
event_type="run.started",
|
|
484
|
+
run_ref=run_ref,
|
|
485
|
+
skipped_metadata_keys=skipped,
|
|
486
|
+
call=lambda: configured_client(sender=sender).start_run(
|
|
487
|
+
run_ref=run_ref,
|
|
488
|
+
task=task,
|
|
489
|
+
occurred_at=occurred_at,
|
|
490
|
+
effective_config=optional_mapping(mapped.get("effective_config")),
|
|
491
|
+
run_context_snapshot=optional_mapping(mapped.get("run_context_snapshot")),
|
|
492
|
+
experiment_id=optional_str(mapped.get("experiment_id")),
|
|
493
|
+
entity_ref=optional_str(mapped.get("entity_ref")),
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def observe_evidence(
|
|
499
|
+
run_ref: str,
|
|
500
|
+
source: object,
|
|
501
|
+
retrieved_at: datetime | None = None,
|
|
502
|
+
evidence_ref: str | None = None,
|
|
503
|
+
metadata: Mapping[str, object] | None = None,
|
|
504
|
+
*,
|
|
505
|
+
sender: HttpSender | None = None,
|
|
506
|
+
) -> DeliveryResult:
|
|
507
|
+
mapped, skipped = map_evidence_metadata(metadata)
|
|
508
|
+
observed_at = retrieved_at or datetime.now(UTC)
|
|
509
|
+
return deliver_safely(
|
|
510
|
+
event_type="evidence.observed",
|
|
511
|
+
run_ref=run_ref,
|
|
512
|
+
skipped_metadata_keys=skipped,
|
|
513
|
+
call=lambda: configured_client(sender=sender).observe_evidence(
|
|
514
|
+
run_ref=run_ref,
|
|
515
|
+
source=source,
|
|
516
|
+
retrieved_at=observed_at,
|
|
517
|
+
evidence_ref=evidence_ref,
|
|
518
|
+
occurred_at=optional_datetime(mapped.get("occurred_at")) or observed_at,
|
|
519
|
+
published_at=optional_datetime(mapped.get("published_at")),
|
|
520
|
+
effective_at=optional_datetime(mapped.get("effective_at")),
|
|
521
|
+
effective_period_start_at=optional_datetime(
|
|
522
|
+
mapped.get("effective_period_start_at")
|
|
523
|
+
),
|
|
524
|
+
effective_period_end_at=optional_datetime(
|
|
525
|
+
mapped.get("effective_period_end_at")
|
|
526
|
+
),
|
|
527
|
+
available_at=optional_datetime(mapped.get("available_at")),
|
|
528
|
+
available_at_status=optional_str(mapped.get("available_at_status")),
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def complete_run(
|
|
534
|
+
run_ref: str,
|
|
535
|
+
result: object,
|
|
536
|
+
occurred_at: datetime | None = None,
|
|
537
|
+
*,
|
|
538
|
+
sender: HttpSender | None = None,
|
|
539
|
+
) -> DeliveryResult:
|
|
540
|
+
return deliver_safely(
|
|
541
|
+
event_type="run.completed",
|
|
542
|
+
run_ref=run_ref,
|
|
543
|
+
skipped_metadata_keys=(),
|
|
544
|
+
call=lambda: configured_client(sender=sender).complete_run(
|
|
545
|
+
run_ref=run_ref,
|
|
546
|
+
output=result,
|
|
547
|
+
occurred_at=occurred_at,
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def fail_run(
|
|
553
|
+
run_ref: str,
|
|
554
|
+
exc_or_error: object,
|
|
555
|
+
occurred_at: datetime | None = None,
|
|
556
|
+
*,
|
|
557
|
+
sender: HttpSender | None = None,
|
|
558
|
+
) -> DeliveryResult:
|
|
559
|
+
return deliver_safely(
|
|
560
|
+
event_type="run.failed",
|
|
561
|
+
run_ref=run_ref,
|
|
562
|
+
skipped_metadata_keys=(),
|
|
563
|
+
call=lambda: configured_client(sender=sender).fail_run(
|
|
564
|
+
run_ref=run_ref,
|
|
565
|
+
error=sanitize_error(exc_or_error),
|
|
566
|
+
occurred_at=occurred_at,
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def deliver_safely(
|
|
572
|
+
*,
|
|
573
|
+
event_type: str,
|
|
574
|
+
run_ref: str,
|
|
575
|
+
skipped_metadata_keys: tuple[str, ...],
|
|
576
|
+
call: Callable[[], EventReceipt],
|
|
577
|
+
) -> DeliveryResult:
|
|
578
|
+
try:
|
|
579
|
+
return DeliveryResult.from_receipt(
|
|
580
|
+
call(),
|
|
581
|
+
run_ref=run_ref,
|
|
582
|
+
skipped_metadata_keys=skipped_metadata_keys,
|
|
583
|
+
)
|
|
584
|
+
except Exception as exc:
|
|
585
|
+
return DeliveryResult(
|
|
586
|
+
delivered=False,
|
|
587
|
+
event_type=event_type,
|
|
588
|
+
run_ref=run_ref,
|
|
589
|
+
error=redact(
|
|
590
|
+
str(exc),
|
|
591
|
+
os.environ.get("VIEWISE_INGEST_KEY", ""),
|
|
592
|
+
os.environ.get("VIEWISE_API_KEY", ""),
|
|
593
|
+
configured_ingest_key(),
|
|
594
|
+
),
|
|
595
|
+
skipped_metadata_keys=skipped_metadata_keys,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def map_start_metadata(
|
|
600
|
+
metadata: Mapping[str, object] | None,
|
|
601
|
+
) -> tuple[dict[str, object], tuple[str, ...]]:
|
|
602
|
+
return map_metadata(
|
|
603
|
+
metadata,
|
|
604
|
+
allowed={
|
|
605
|
+
"effective_config",
|
|
606
|
+
"run_context_snapshot",
|
|
607
|
+
"experiment_id",
|
|
608
|
+
"entity_ref",
|
|
609
|
+
},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def map_evidence_metadata(
|
|
614
|
+
metadata: Mapping[str, object] | None,
|
|
615
|
+
) -> tuple[dict[str, object], tuple[str, ...]]:
|
|
616
|
+
return map_metadata(
|
|
617
|
+
metadata,
|
|
618
|
+
allowed={
|
|
619
|
+
"occurred_at",
|
|
620
|
+
"published_at",
|
|
621
|
+
"effective_at",
|
|
622
|
+
"effective_period_start_at",
|
|
623
|
+
"effective_period_end_at",
|
|
624
|
+
"available_at",
|
|
625
|
+
"available_at_status",
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def map_metadata(
|
|
631
|
+
metadata: Mapping[str, object] | None,
|
|
632
|
+
*,
|
|
633
|
+
allowed: set[str],
|
|
634
|
+
) -> tuple[dict[str, object], tuple[str, ...]]:
|
|
635
|
+
if metadata is None:
|
|
636
|
+
return {}, ()
|
|
637
|
+
mapped = {key: value for key, value in metadata.items() if key in allowed}
|
|
638
|
+
skipped = tuple(sorted(key for key in metadata if key not in allowed))
|
|
639
|
+
return mapped, skipped
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def optional_env(name: str) -> str | None:
|
|
643
|
+
value = os.environ.get(name)
|
|
644
|
+
if value is None or not value.strip():
|
|
645
|
+
return None
|
|
646
|
+
return value.strip()
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def optional_mapping(value: object) -> Mapping[str, object] | None:
|
|
650
|
+
if value is None:
|
|
651
|
+
return None
|
|
652
|
+
if not isinstance(value, Mapping):
|
|
653
|
+
raise ViewiseConfigError("metadata value must be an object")
|
|
654
|
+
return cast(Mapping[str, object], value)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def optional_datetime(value: object) -> datetime | None:
|
|
658
|
+
if value is None:
|
|
659
|
+
return None
|
|
660
|
+
if not isinstance(value, datetime):
|
|
661
|
+
raise ViewiseConfigError("metadata timestamp must be a datetime")
|
|
662
|
+
return value
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def optional_str(value: object) -> str | None:
|
|
666
|
+
if value is None:
|
|
667
|
+
return None
|
|
668
|
+
if not isinstance(value, str):
|
|
669
|
+
raise ViewiseConfigError("metadata value must be a string")
|
|
670
|
+
return value
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def sanitize_error(error: object) -> dict[str, str]:
|
|
674
|
+
if isinstance(error, Mapping) and "message" in error:
|
|
675
|
+
message = str(error.get("message") or "unknown error")
|
|
676
|
+
else:
|
|
677
|
+
message = str(error or "unknown error")
|
|
678
|
+
message = redact(message, *environment_secrets())
|
|
679
|
+
for pattern in _REQUEST_DETAIL_PATTERNS:
|
|
680
|
+
message = pattern.sub(_omit_match, message)
|
|
681
|
+
if len(message) > _SAFE_ERROR_MESSAGE_LIMIT:
|
|
682
|
+
message = message[: _SAFE_ERROR_MESSAGE_LIMIT - 3].rstrip() + "..."
|
|
683
|
+
return {"message": message}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def normalize_source(source: object) -> str:
|
|
687
|
+
if isinstance(source, str):
|
|
688
|
+
return require_non_blank(source, "source")
|
|
689
|
+
try:
|
|
690
|
+
serialized = json.dumps(
|
|
691
|
+
source,
|
|
692
|
+
ensure_ascii=False,
|
|
693
|
+
separators=(",", ":"),
|
|
694
|
+
sort_keys=True,
|
|
695
|
+
)
|
|
696
|
+
except TypeError:
|
|
697
|
+
serialized = str(source)
|
|
698
|
+
return require_non_blank(serialized, "source")
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def parse_receipt(
|
|
702
|
+
response: Response,
|
|
703
|
+
*,
|
|
704
|
+
event_type: str,
|
|
705
|
+
secret: str,
|
|
706
|
+
credential_label: str = "ingest key",
|
|
707
|
+
) -> EventReceipt:
|
|
708
|
+
if response.status_code in (401, 403):
|
|
709
|
+
raise ViewiseAuthError(
|
|
710
|
+
redact(response_detail(response) or f"invalid {credential_label}", secret)
|
|
711
|
+
)
|
|
712
|
+
if response.status_code >= 500:
|
|
713
|
+
detail = response_detail(response) or response.text
|
|
714
|
+
raise ViewiseServiceError(
|
|
715
|
+
redact(
|
|
716
|
+
f"semantic ingestion failed with {response.status_code}: {detail}",
|
|
717
|
+
secret,
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
if response.status_code >= 400:
|
|
721
|
+
detail = response_detail(response) or response.text
|
|
722
|
+
raise ViewiseRequestError(
|
|
723
|
+
redact(
|
|
724
|
+
f"semantic ingestion failed with {response.status_code}: {detail}",
|
|
725
|
+
secret,
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
try:
|
|
729
|
+
payload = response.json()
|
|
730
|
+
except ValueError as exc:
|
|
731
|
+
raise ViewiseRequestError(
|
|
732
|
+
redact("semantic ingestion returned invalid receipt", secret)
|
|
733
|
+
) from exc
|
|
734
|
+
if not isinstance(payload, Mapping):
|
|
735
|
+
raise ViewiseRequestError("semantic ingestion returned invalid receipt")
|
|
736
|
+
return EventReceipt(
|
|
737
|
+
event_id=str(payload.get("event_id", "")),
|
|
738
|
+
run_id=str(payload.get("run_id", "")),
|
|
739
|
+
event_type=str(payload.get("event_type", event_type)),
|
|
740
|
+
status=str(payload.get("status", "")),
|
|
741
|
+
run_status=str(payload.get("run_status", "")),
|
|
742
|
+
raw=cast(Mapping[str, object], payload),
|
|
743
|
+
agent_ref=optional_text(payload.get("agent_ref")),
|
|
744
|
+
run_ref=optional_text(payload.get("run_ref")),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def response_detail(response: Response) -> str | None:
|
|
749
|
+
try:
|
|
750
|
+
payload = response.json()
|
|
751
|
+
except ValueError:
|
|
752
|
+
return None
|
|
753
|
+
if isinstance(payload, Mapping):
|
|
754
|
+
detail = payload.get("detail")
|
|
755
|
+
if detail is not None:
|
|
756
|
+
return str(detail)
|
|
757
|
+
return None
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def compact_context_snapshot(value: Mapping[str, object]) -> dict[str, object]:
|
|
761
|
+
return {key: value[key] for key in CONTEXT_SNAPSHOT_KEYS if key in value}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def add_optional_timestamp(
|
|
765
|
+
payload: dict[str, object],
|
|
766
|
+
key: str,
|
|
767
|
+
value: datetime | None,
|
|
768
|
+
) -> None:
|
|
769
|
+
if value is not None:
|
|
770
|
+
payload[key] = format_timestamp(value)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def format_timestamp(value: datetime) -> str:
|
|
774
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
775
|
+
raise ViewiseConfigError("timestamps must include timezone")
|
|
776
|
+
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def require_env(name: str) -> str:
|
|
780
|
+
return require_non_blank(os.environ.get(name, ""), name)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def require_one_key(
|
|
784
|
+
*,
|
|
785
|
+
ingest_key: str | None,
|
|
786
|
+
api_key: str | None,
|
|
787
|
+
label: str,
|
|
788
|
+
) -> str:
|
|
789
|
+
if ingest_key is not None and api_key is not None:
|
|
790
|
+
raise ViewiseConfigError("provide only one of ingest_key or api_key")
|
|
791
|
+
return require_non_blank(ingest_key or api_key or "", label)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def require_non_blank(value: str, label: str) -> str:
|
|
795
|
+
stripped = value.strip()
|
|
796
|
+
if not stripped:
|
|
797
|
+
raise ViewiseConfigError(f"{label} is required")
|
|
798
|
+
return stripped
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def normalize_ref(value: str | None, label: str) -> str:
|
|
802
|
+
if value is None:
|
|
803
|
+
raise ViewiseConfigError(f"{label} is required")
|
|
804
|
+
stripped = require_non_blank(value, label)
|
|
805
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", stripped):
|
|
806
|
+
raise ViewiseConfigError(f"{label} must be a valid ref")
|
|
807
|
+
return stripped
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def require_uuid_text(value: str | None, label: str) -> str:
|
|
811
|
+
if value is None:
|
|
812
|
+
raise ViewiseConfigError(f"{label} is required")
|
|
813
|
+
try:
|
|
814
|
+
return str(UUID(require_non_blank(value, label)))
|
|
815
|
+
except ValueError as exc:
|
|
816
|
+
raise ViewiseConfigError(f"{label} must be a valid UUID") from exc
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def normalize_idempotency_part(value: str) -> str:
|
|
820
|
+
return require_non_blank(value, "idempotency part").replace("\n", " ")
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def optional_text(value: object) -> str | None:
|
|
824
|
+
if value is None:
|
|
825
|
+
return None
|
|
826
|
+
return str(value)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def redact(message: str, *secrets: str) -> str:
|
|
830
|
+
redacted = message
|
|
831
|
+
for secret in secrets:
|
|
832
|
+
if secret:
|
|
833
|
+
redacted = redacted.replace(secret, "[REDACTED]")
|
|
834
|
+
for pattern in _CREDENTIAL_PATTERNS:
|
|
835
|
+
redacted = pattern.sub(_redact_match, redacted)
|
|
836
|
+
return redacted
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def environment_secrets() -> tuple[str, ...]:
|
|
840
|
+
names = {
|
|
841
|
+
"VIEWISE_INGEST_KEY",
|
|
842
|
+
"VIEWISE_API_KEY",
|
|
843
|
+
"OPENAI_API_KEY",
|
|
844
|
+
"ANTHROPIC_API_KEY",
|
|
845
|
+
}
|
|
846
|
+
values = []
|
|
847
|
+
for name, value in os.environ.items():
|
|
848
|
+
upper_name = name.upper()
|
|
849
|
+
if name in names or any(
|
|
850
|
+
token in upper_name for token in ("API_KEY", "TOKEN", "SECRET")
|
|
851
|
+
):
|
|
852
|
+
stripped = value.strip()
|
|
853
|
+
if len(stripped) >= 8:
|
|
854
|
+
values.append(stripped)
|
|
855
|
+
configured = configured_ingest_key()
|
|
856
|
+
if configured:
|
|
857
|
+
values.append(configured)
|
|
858
|
+
return tuple(values)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def configured_ingest_key() -> str:
|
|
862
|
+
if _runtime_config is None:
|
|
863
|
+
return ""
|
|
864
|
+
return _runtime_config.ingest_key
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _redact_match(match: re.Match[str]) -> str:
|
|
868
|
+
if len(match.groups()) >= 3:
|
|
869
|
+
return f"{match.group(1)}{match.group(2)}[REDACTED]"
|
|
870
|
+
if match.group(0).lower().startswith("bearer"):
|
|
871
|
+
return "Bearer [REDACTED]"
|
|
872
|
+
return "[REDACTED]"
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _omit_match(match: re.Match[str]) -> str:
|
|
876
|
+
return f"{match.group(1)}{match.group(2)}[OMITTED]"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: viewise
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python SDK for Viewise semantic telemetry ingestion.
|
|
5
|
+
Author: Viewise
|
|
6
|
+
Project-URL: Homepage, https://github.com/Tanishv/xalpha
|
|
7
|
+
Project-URL: Repository, https://github.com/Tanishv/xalpha
|
|
8
|
+
Project-URL: Issues, https://github.com/Tanishv/xalpha/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
|
|
20
|
+
# Viewise Python SDK
|
|
21
|
+
|
|
22
|
+
Viewise observes agent runs through a small semantic telemetry contract. The SDK
|
|
23
|
+
sends events to a Viewise backend and keeps agent code resilient when telemetry
|
|
24
|
+
is unavailable.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -m pip install viewise
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configure
|
|
33
|
+
|
|
34
|
+
Set the runtime environment values copied from the Viewise Setup page:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export VIEWISE_BASE_URL="https://viewise-backend.fly.dev"
|
|
38
|
+
export VIEWISE_INGEST_KEY="<workspace-api-key-from-viewise-setup>"
|
|
39
|
+
export VIEWISE_AGENT_REF="workspace.agentx"
|
|
40
|
+
export VIEWISE_AGENT_NAME="agentx"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You can also configure the SDK in code:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import viewise
|
|
47
|
+
|
|
48
|
+
viewise.configure(
|
|
49
|
+
base_url="https://viewise-backend.fly.dev",
|
|
50
|
+
ingest_key="<workspace-api-key-from-viewise-setup>",
|
|
51
|
+
agent_ref="workspace.agentx",
|
|
52
|
+
agent_name="agentx",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Never commit API keys or place them in prompts, logs, or source code.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
viewise.start_run(run_ref, task, occurred_at=None)
|
|
62
|
+
viewise.observe_evidence(run_ref, source, retrieved_at=None, evidence_ref=None)
|
|
63
|
+
viewise.complete_run(run_ref, result, occurred_at=None)
|
|
64
|
+
viewise.fail_run(run_ref, exc_or_error, occurred_at=None)
|
|
65
|
+
viewise.sanitize_error(exc_or_error)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Supported event types are:
|
|
69
|
+
|
|
70
|
+
- `run.started`
|
|
71
|
+
- `evidence.observed`
|
|
72
|
+
- `run.completed`
|
|
73
|
+
- `run.failed`
|
|
74
|
+
|
|
75
|
+
Top-level telemetry calls return a delivery result. They do not raise when
|
|
76
|
+
Viewise or the network is unavailable, so agent work can continue.
|
|
77
|
+
|
|
78
|
+
## Smoke Test
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import viewise
|
|
82
|
+
|
|
83
|
+
run_ref = "local-run-001"
|
|
84
|
+
|
|
85
|
+
viewise.start_run(run_ref, "SDK install smoke test")
|
|
86
|
+
viewise.observe_evidence(
|
|
87
|
+
run_ref,
|
|
88
|
+
{"provider": "local", "kind": "smoke"},
|
|
89
|
+
evidence_ref="smoke-001",
|
|
90
|
+
)
|
|
91
|
+
viewise.complete_run(run_ref, {"status": "ok"})
|
|
92
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.27
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
viewise
|