prodloop-observability-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prodloop/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from prodloop.client import ProdloopClient
2
+ from prodloop.exceptions import APIError, ProdloopError, ValidationError
3
+ from prodloop.models import EvaluationParameter
4
+
5
+ __all__ = [
6
+ "ProdloopClient",
7
+ "EvaluationParameter",
8
+ "ProdloopError",
9
+ "ValidationError",
10
+ "APIError",
11
+ ]
prodloop/client.py ADDED
@@ -0,0 +1,125 @@
1
+ import json
2
+ import mimetypes
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Iterable, Mapping, Optional, Sequence
5
+
6
+ import requests
7
+
8
+ from prodloop.exceptions import APIError, ValidationError
9
+ from prodloop.models import EvaluationParameter, ParameterInput, coerce_parameter
10
+
11
+
12
+ class ProdloopClient:
13
+ def __init__(
14
+ self,
15
+ api_key: str,
16
+ base_url: str = "https://asia-south1-prodloop.cloudfunctions.net/prodloop-evaluator-fn",
17
+ timeout_seconds: int = 180,
18
+ session: Optional[requests.Session] = None,
19
+ ):
20
+ if not api_key or not api_key.strip():
21
+ raise ValidationError("api_key is required.")
22
+ if not base_url or not base_url.strip():
23
+ raise ValidationError("base_url is required.")
24
+
25
+ self.api_key = api_key.strip()
26
+ self.base_url = base_url.rstrip("/")
27
+ self.timeout_seconds = timeout_seconds
28
+ self._session = session or requests.Session()
29
+
30
+ def evaluate_call(
31
+ self,
32
+ audio_file_path: str,
33
+ parameters: Sequence[ParameterInput],
34
+ thresholds: Optional[Mapping[str, Any]] = None,
35
+ extraction_schema: Optional[Mapping[str, str]] = None,
36
+ bot_captured_variables: Optional[Mapping[str, Any]] = None,
37
+ timeout_seconds: Optional[int] = None,
38
+ ) -> Dict[str, Any]:
39
+ normalized_parameters = self._normalize_parameters(parameters)
40
+ payload: Dict[str, Any] = {
41
+ "parameters": [param.value for param in normalized_parameters],
42
+ }
43
+
44
+ if thresholds:
45
+ payload["thresholds"] = dict(thresholds)
46
+ if extraction_schema:
47
+ payload["extraction_schema"] = dict(extraction_schema)
48
+ if bot_captured_variables:
49
+ payload["bot_captured_variables"] = dict(bot_captured_variables)
50
+
51
+ if EvaluationParameter.EXTRACTION_VARIABLES in normalized_parameters:
52
+ if not extraction_schema:
53
+ raise ValidationError(
54
+ "extraction_schema is required when using 'extraction_variables'."
55
+ )
56
+ if not bot_captured_variables:
57
+ raise ValidationError(
58
+ "bot_captured_variables is required when using 'extraction_variables'."
59
+ )
60
+ missing_keys = [
61
+ key for key in extraction_schema.keys() if key not in bot_captured_variables
62
+ ]
63
+ if missing_keys:
64
+ raise ValidationError(
65
+ "bot_captured_variables is missing keys from extraction_schema: "
66
+ + ", ".join(missing_keys)
67
+ )
68
+
69
+ audio_path = Path(audio_file_path)
70
+ if not audio_path.exists():
71
+ raise ValidationError(f"Audio file not found: {audio_file_path}")
72
+ if not audio_path.is_file():
73
+ raise ValidationError(f"Audio path is not a file: {audio_file_path}")
74
+
75
+ request_timeout = timeout_seconds or self.timeout_seconds
76
+ url = self.base_url
77
+ headers = {"Authorization": f"Bearer {self.api_key}"}
78
+ mime_type = self._guess_mime_type(audio_path)
79
+
80
+ with audio_path.open("rb") as audio_file:
81
+ files = {
82
+ "audio_file": (audio_path.name, audio_file, mime_type),
83
+ "metadata": (None, json.dumps(payload), "application/json"),
84
+ }
85
+ response = self._session.post(
86
+ url, headers=headers, files=files, timeout=request_timeout
87
+ )
88
+
89
+ self._raise_for_api_error(response)
90
+ return self._safe_json(response)
91
+
92
+ @staticmethod
93
+ def _normalize_parameters(parameters: Iterable[ParameterInput]) -> Sequence[EvaluationParameter]:
94
+ normalized = [coerce_parameter(param) for param in parameters]
95
+ if not normalized:
96
+ raise ValidationError("At least one evaluation parameter is required.")
97
+ return normalized
98
+
99
+ @staticmethod
100
+ def _guess_mime_type(audio_path: Path) -> str:
101
+ guessed, _ = mimetypes.guess_type(str(audio_path))
102
+ return guessed or "application/octet-stream"
103
+
104
+ @staticmethod
105
+ def _safe_json(response: requests.Response) -> Dict[str, Any]:
106
+ try:
107
+ payload = response.json()
108
+ except ValueError as exc:
109
+ raise APIError(response.status_code, "Backend returned non-JSON response.") from exc
110
+ if not isinstance(payload, dict):
111
+ raise APIError(response.status_code, "Backend response must be a JSON object.")
112
+ return payload
113
+
114
+ @staticmethod
115
+ def _raise_for_api_error(response: requests.Response) -> None:
116
+ if response.ok:
117
+ return
118
+
119
+ try:
120
+ payload = response.json()
121
+ message = payload.get("error") or payload.get("message") or str(payload)
122
+ except ValueError:
123
+ message = response.text or "Unknown backend error"
124
+
125
+ raise APIError(response.status_code, message)
prodloop/exceptions.py ADDED
@@ -0,0 +1,15 @@
1
+ class ProdloopError(Exception):
2
+ """Base exception for SDK errors."""
3
+
4
+
5
+ class ValidationError(ProdloopError):
6
+ """Raised when request inputs are invalid."""
7
+
8
+
9
+ class APIError(ProdloopError):
10
+ """Raised when backend API returns an error response."""
11
+
12
+ def __init__(self, status_code: int, message: str):
13
+ super().__init__(f"Prodloop API error ({status_code}): {message}")
14
+ self.status_code = status_code
15
+ self.message = message
prodloop/models.py ADDED
@@ -0,0 +1,29 @@
1
+ from enum import Enum
2
+ from typing import Union
3
+
4
+
5
+ class EvaluationParameter(str, Enum):
6
+ E2E_RESPONSE_TIME = "e2e_response_time"
7
+ TURN_BY_TURN_LATENCY = "turn_by_turn_latency"
8
+ HALLUCINATION = "hallucination"
9
+ EXTRACTION_VARIABLES = "extraction_variables"
10
+ INTERRUPTION_BEHAVIOR = "interruption_behavior"
11
+
12
+
13
+ ParameterInput = Union[EvaluationParameter, str]
14
+
15
+ SUPPORTED_PARAMETERS = tuple(param.value for param in EvaluationParameter)
16
+
17
+
18
+ def coerce_parameter(value: ParameterInput) -> EvaluationParameter:
19
+ if isinstance(value, EvaluationParameter):
20
+ return value
21
+
22
+ normalized = str(value).strip().lower()
23
+ for parameter in EvaluationParameter:
24
+ if parameter.value == normalized:
25
+ return parameter
26
+
27
+ raise ValueError(
28
+ f"Unsupported parameter '{value}'. Supported values: {', '.join(SUPPORTED_PARAMETERS)}."
29
+ )
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: prodloop-observability-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for evaluating AI voice bot calls via Prodloop APIs.
5
+ Project-URL: Homepage, https://prodloop.com
6
+ Project-URL: Documentation, https://prodloop.com/docs
7
+ Project-URL: Repository, https://github.com/prodloop/prodloop-observability-sdk
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests>=2.31.0
11
+ Provides-Extra: docs
12
+ Requires-Dist: mkdocs>=1.6.0; extra == "docs"
13
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
14
+ Requires-Dist: mkdocstrings[python]>=0.25.0; extra == "docs"
15
+
16
+ # Prodloop Observability SDK
17
+
18
+ Python SDK to evaluate AI voice bot calls through the Prodloop evaluation service.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install prodloop-observability-sdk
24
+ ```
25
+
26
+ ## Quickstart
27
+
28
+ ```python
29
+ from prodloop import ProdloopClient, EvaluationParameter
30
+
31
+ client = ProdloopClient(api_key="sk_live_...")
32
+
33
+ result = client.evaluate_call(
34
+ audio_file_path="call.mp3",
35
+ parameters=[
36
+ EvaluationParameter.E2E_RESPONSE_TIME,
37
+ EvaluationParameter.HALLUCINATION,
38
+ ],
39
+ thresholds={"e2e_response_time_max_ms": 800},
40
+ )
41
+
42
+ print(result)
43
+ ```
44
+
45
+ ## Extraction Validation
46
+
47
+ To validate extraction quality, pass both `extraction_schema` and `bot_captured_variables`:
48
+
49
+ ```python
50
+ result = client.evaluate_call(
51
+ audio_file_path="call.mp3",
52
+ parameters=[EvaluationParameter.EXTRACTION_VARIABLES],
53
+ extraction_schema={"customer_name": "string"},
54
+ bot_captured_variables={"customer_name": "ram"},
55
+ )
56
+ ```
57
+
58
+ Response includes:
59
+
60
+ - `extraction_variables`
61
+ - `extraction_validation`
62
+
63
+ ## Supported Parameters
64
+
65
+ - `e2e_response_time`
66
+ - `turn_by_turn_latency`
67
+ - `hallucination`
68
+ - `extraction_variables`
69
+ - `interruption_behavior`
70
+
71
+ ## Authentication
72
+
73
+ Pass your Prodloop API key in the SDK constructor.
74
+ The SDK sends it as a `Bearer` token in the `Authorization` header.
75
+
76
+ ## Errors
77
+
78
+ - `ValidationError`: invalid local inputs (file path, parameters, etc.)
79
+ - `APIError`: backend/API-level failures (`status_code`, `message`)
80
+
81
+ ## Local Demo
82
+
83
+ Use the included demo script:
84
+
85
+ ```bash
86
+ export PRODLOOP_API_KEY="sk_live_..."
87
+ python demo/use_sdk.py --audio-file "/absolute/path/to/call.mp3"
88
+ ```
89
+
90
+ ## Documentation Site
91
+
92
+ This repo includes MkDocs docs under `docs/`.
93
+
94
+ ```bash
95
+ pip install -e ".[docs]"
96
+ mkdocs serve
97
+ ```
@@ -0,0 +1,8 @@
1
+ prodloop/__init__.py,sha256=YjKexKVjh20ShSwrinFpv4x0zzlRvFQ-PrYgx-NVLac,299
2
+ prodloop/client.py,sha256=Vex9ftFmv1rEyGe6t-hN1nXqA7PTmRI84hLsAYZNYwA,4897
3
+ prodloop/exceptions.py,sha256=N77tevCDy8gHUGZEmw1IImHyd1LHYn_wWNRatMHxiuI,474
4
+ prodloop/models.py,sha256=Oes9YKRyAq9WtRHpG48oY8TGmLoQ4eZ9vqiEYviz0Co,897
5
+ prodloop_observability_sdk-0.1.0.dist-info/METADATA,sha256=T7LVcXqbnzddEwVzxiW3faWbyadOIK_IhK85h1eZBCU,2309
6
+ prodloop_observability_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ prodloop_observability_sdk-0.1.0.dist-info/top_level.txt,sha256=dDhEPtIqQuBgiiVeNB458bBuVZk_N3Uj6tzVGEiaPIA,9
8
+ prodloop_observability_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+