prodloop-observability-sdk 0.1.8__tar.gz → 0.1.9__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.
- {prodloop_observability_sdk-0.1.8/prodloop_observability_sdk.egg-info → prodloop_observability_sdk-0.1.9}/PKG-INFO +57 -2
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/README.md +56 -1
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/examples.md +59 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/.env.example +12 -0
- prodloop_observability_sdk-0.1.9/examples/audit_discovery_demo.py +96 -0
- prodloop_observability_sdk-0.1.9/examples/post_call_prompt_aware_demo.py +97 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/__init__.py +2 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/client.py +74 -16
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/models.py +32 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9/prodloop_observability_sdk.egg-info}/PKG-INFO +57 -2
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/SOURCES.txt +2 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/pyproject.toml +1 -1
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/CHANGELOG.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/MANIFEST.in +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/api-reference.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/authentication.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/errors.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/getting-started.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/index.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/parameters.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/visual-overview.md +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/demo.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/demo_gpt.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/user_orchestrated_demo.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/user_orchestrated_demo_gpt.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/mkdocs.yml +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/exceptions.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/__init__.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/_utils.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/litellm.py +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/dependency_links.txt +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/requires.txt +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/top_level.txt +0 -0
- {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prodloop-observability-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Python SDK for evaluating AI voice bot calls via Prodloop APIs.
|
|
5
5
|
Project-URL: Homepage, https://prodloop.com
|
|
6
6
|
Project-URL: Documentation, https://observability-sdk-docs.pages.dev/
|
|
@@ -27,7 +27,7 @@ pip install prodloop-observability-sdk
|
|
|
27
27
|
## Quickstart
|
|
28
28
|
|
|
29
29
|
```python
|
|
30
|
-
from prodloop import
|
|
30
|
+
from prodloop import CustomEvaluationParameter, EvaluationParameter, ProdloopClient
|
|
31
31
|
|
|
32
32
|
client = ProdloopClient(api_key="sk_live_...")
|
|
33
33
|
|
|
@@ -38,12 +38,40 @@ result = client.evaluate_call(
|
|
|
38
38
|
EvaluationParameter.HALLUCINATION,
|
|
39
39
|
],
|
|
40
40
|
thresholds={"e2e_response_time_max_ms": 800},
|
|
41
|
+
custom_parameters=[
|
|
42
|
+
CustomEvaluationParameter(
|
|
43
|
+
key="resolution_quality",
|
|
44
|
+
label="Resolution quality",
|
|
45
|
+
description="Check whether the bot correctly understood the issue and reached a useful final outcome.",
|
|
46
|
+
),
|
|
47
|
+
],
|
|
41
48
|
input_prompt="Bot instructions used during this call...",
|
|
42
49
|
)
|
|
43
50
|
|
|
44
51
|
print(result)
|
|
45
52
|
```
|
|
46
53
|
|
|
54
|
+
## Custom Parameters
|
|
55
|
+
|
|
56
|
+
Use `custom_parameters` for audit dimensions that are not part of the fixed `EvaluationParameter` enum. Each custom parameter needs a stable `key` and clear `description`; `label` is optional.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
result = client.evaluate_call(
|
|
60
|
+
audio_file_path="call.mp3",
|
|
61
|
+
parameters=[EvaluationParameter.HALLUCINATION],
|
|
62
|
+
custom_parameters=[
|
|
63
|
+
{
|
|
64
|
+
"key": "driver_resolution_quality",
|
|
65
|
+
"label": "Driver resolution quality",
|
|
66
|
+
"description": "Evaluate whether the bot handled driver-not-found or cancellation cases correctly and empathetically.",
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
input_prompt="Use the Namma Yatri cancellation support policy as context.",
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Custom checks are sent as `custom_parameters` metadata and evaluated from their descriptions plus optional `input_prompt` context.
|
|
74
|
+
|
|
47
75
|
## Extraction Validation
|
|
48
76
|
|
|
49
77
|
To validate extraction quality, pass both `extraction_schema` and `bot_captured_variables`:
|
|
@@ -89,6 +117,27 @@ Prompt-aware parameter results use a compact shape:
|
|
|
89
117
|
|
|
90
118
|
`passed` can be `"true"`, `"false"`, or `"N/A"`. `"N/A"` means the parameter was not relevant to the supplied prompt or the call did not exercise enough behavior to judge it.
|
|
91
119
|
|
|
120
|
+
Example prompt-aware response:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"section_sequencing": {
|
|
125
|
+
"passed": "false",
|
|
126
|
+
"explanation": "The bot did not follow the section flow defined in the supplied prompt."
|
|
127
|
+
},
|
|
128
|
+
"mandatory_field_gating": {
|
|
129
|
+
"passed": "N/A",
|
|
130
|
+
"explanation": "The prompt-defined gated action was not triggered in this call."
|
|
131
|
+
},
|
|
132
|
+
"prompt_injection": {
|
|
133
|
+
"passed": "N/A",
|
|
134
|
+
"explanation": "The caller did not attempt to override instructions or inject commands."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Runnable example: `examples/post_call_prompt_aware_demo.py`. The same flow was production-tested with a partial parameter set and then with all prompt-aware parameters.
|
|
140
|
+
|
|
92
141
|
## Supported Parameters
|
|
93
142
|
|
|
94
143
|
- `e2e_response_time`
|
|
@@ -169,6 +218,8 @@ There are two modes:
|
|
|
169
218
|
- `self_simulation`: Prodloop backend runs the tester and bot conversation. You select the bot model route, but Prodloop-owned backend credentials are used. No bot credentials are sent from your code.
|
|
170
219
|
- `user_orchestrated`: Prodloop backend runs the tester and grader. Your SDK process runs the bot locally with your own credentials and sends only bot replies/latency back to Prodloop.
|
|
171
220
|
|
|
221
|
+
The production backend also supports `audit_discovery` for deeper prompt-risk discovery. Runnable examples are available in `examples/audit_discovery_demo.py` and `simulation_demo/prod_testing/after_pypi/audit_discovery_demo.py`.
|
|
222
|
+
|
|
172
223
|
Simulation currently accepts exactly one parameter per request. To test multiple parameters, start one simulation per parameter. `max_turns` is configurable from `1` to `10`.
|
|
173
224
|
|
|
174
225
|
Discover currently enabled simulation parameters at runtime:
|
|
@@ -259,6 +310,10 @@ For `user_orchestrated`, configure bot credentials locally for either Vertex AI
|
|
|
259
310
|
|
|
260
311
|
For adaptive simulations, `max_turns` controls turns per conversation and `adaptive_max_conversations` controls the maximum number of conversations to explore.
|
|
261
312
|
|
|
313
|
+
### Audit Discovery
|
|
314
|
+
|
|
315
|
+
Audit discovery plans targeted risk scenarios for one selected parameter, runs them against the bot, and returns passed/failed scenarios plus patch guidance for failures. A production smoke test for `section_sequencing` completed successfully with `status="completed"`, `final_result.overall_pass=true`, and `final_result.stop_reason="audit_discovery_completed"`.
|
|
316
|
+
|
|
262
317
|
### Result Shape
|
|
263
318
|
|
|
264
319
|
Simulation responses include:
|
|
@@ -11,7 +11,7 @@ pip install prodloop-observability-sdk
|
|
|
11
11
|
## Quickstart
|
|
12
12
|
|
|
13
13
|
```python
|
|
14
|
-
from prodloop import
|
|
14
|
+
from prodloop import CustomEvaluationParameter, EvaluationParameter, ProdloopClient
|
|
15
15
|
|
|
16
16
|
client = ProdloopClient(api_key="sk_live_...")
|
|
17
17
|
|
|
@@ -22,12 +22,40 @@ result = client.evaluate_call(
|
|
|
22
22
|
EvaluationParameter.HALLUCINATION,
|
|
23
23
|
],
|
|
24
24
|
thresholds={"e2e_response_time_max_ms": 800},
|
|
25
|
+
custom_parameters=[
|
|
26
|
+
CustomEvaluationParameter(
|
|
27
|
+
key="resolution_quality",
|
|
28
|
+
label="Resolution quality",
|
|
29
|
+
description="Check whether the bot correctly understood the issue and reached a useful final outcome.",
|
|
30
|
+
),
|
|
31
|
+
],
|
|
25
32
|
input_prompt="Bot instructions used during this call...",
|
|
26
33
|
)
|
|
27
34
|
|
|
28
35
|
print(result)
|
|
29
36
|
```
|
|
30
37
|
|
|
38
|
+
## Custom Parameters
|
|
39
|
+
|
|
40
|
+
Use `custom_parameters` for audit dimensions that are not part of the fixed `EvaluationParameter` enum. Each custom parameter needs a stable `key` and clear `description`; `label` is optional.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
result = client.evaluate_call(
|
|
44
|
+
audio_file_path="call.mp3",
|
|
45
|
+
parameters=[EvaluationParameter.HALLUCINATION],
|
|
46
|
+
custom_parameters=[
|
|
47
|
+
{
|
|
48
|
+
"key": "driver_resolution_quality",
|
|
49
|
+
"label": "Driver resolution quality",
|
|
50
|
+
"description": "Evaluate whether the bot handled driver-not-found or cancellation cases correctly and empathetically.",
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
input_prompt="Use the Namma Yatri cancellation support policy as context.",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Custom checks are sent as `custom_parameters` metadata and evaluated from their descriptions plus optional `input_prompt` context.
|
|
58
|
+
|
|
31
59
|
## Extraction Validation
|
|
32
60
|
|
|
33
61
|
To validate extraction quality, pass both `extraction_schema` and `bot_captured_variables`:
|
|
@@ -73,6 +101,27 @@ Prompt-aware parameter results use a compact shape:
|
|
|
73
101
|
|
|
74
102
|
`passed` can be `"true"`, `"false"`, or `"N/A"`. `"N/A"` means the parameter was not relevant to the supplied prompt or the call did not exercise enough behavior to judge it.
|
|
75
103
|
|
|
104
|
+
Example prompt-aware response:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"section_sequencing": {
|
|
109
|
+
"passed": "false",
|
|
110
|
+
"explanation": "The bot did not follow the section flow defined in the supplied prompt."
|
|
111
|
+
},
|
|
112
|
+
"mandatory_field_gating": {
|
|
113
|
+
"passed": "N/A",
|
|
114
|
+
"explanation": "The prompt-defined gated action was not triggered in this call."
|
|
115
|
+
},
|
|
116
|
+
"prompt_injection": {
|
|
117
|
+
"passed": "N/A",
|
|
118
|
+
"explanation": "The caller did not attempt to override instructions or inject commands."
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Runnable example: `examples/post_call_prompt_aware_demo.py`. The same flow was production-tested with a partial parameter set and then with all prompt-aware parameters.
|
|
124
|
+
|
|
76
125
|
## Supported Parameters
|
|
77
126
|
|
|
78
127
|
- `e2e_response_time`
|
|
@@ -153,6 +202,8 @@ There are two modes:
|
|
|
153
202
|
- `self_simulation`: Prodloop backend runs the tester and bot conversation. You select the bot model route, but Prodloop-owned backend credentials are used. No bot credentials are sent from your code.
|
|
154
203
|
- `user_orchestrated`: Prodloop backend runs the tester and grader. Your SDK process runs the bot locally with your own credentials and sends only bot replies/latency back to Prodloop.
|
|
155
204
|
|
|
205
|
+
The production backend also supports `audit_discovery` for deeper prompt-risk discovery. Runnable examples are available in `examples/audit_discovery_demo.py` and `simulation_demo/prod_testing/after_pypi/audit_discovery_demo.py`.
|
|
206
|
+
|
|
156
207
|
Simulation currently accepts exactly one parameter per request. To test multiple parameters, start one simulation per parameter. `max_turns` is configurable from `1` to `10`.
|
|
157
208
|
|
|
158
209
|
Discover currently enabled simulation parameters at runtime:
|
|
@@ -243,6 +294,10 @@ For `user_orchestrated`, configure bot credentials locally for either Vertex AI
|
|
|
243
294
|
|
|
244
295
|
For adaptive simulations, `max_turns` controls turns per conversation and `adaptive_max_conversations` controls the maximum number of conversations to explore.
|
|
245
296
|
|
|
297
|
+
### Audit Discovery
|
|
298
|
+
|
|
299
|
+
Audit discovery plans targeted risk scenarios for one selected parameter, runs them against the bot, and returns passed/failed scenarios plus patch guidance for failures. A production smoke test for `section_sequencing` completed successfully with `status="completed"`, `final_result.overall_pass=true`, and `final_result.stop_reason="audit_discovery_completed"`.
|
|
300
|
+
|
|
246
301
|
### Result Shape
|
|
247
302
|
|
|
248
303
|
Simulation responses include:
|
|
@@ -69,6 +69,25 @@ print(result["section_sequencing"])
|
|
|
69
69
|
|
|
70
70
|
For prompt-aware parameters, `passed` is `"true"`, `"false"`, or `"N/A"`. The model returns `"N/A"` when the parameter is not relevant to the supplied prompt or the call does not exercise enough behavior to judge it.
|
|
71
71
|
|
|
72
|
+
This flow was tested against production with both a small subset and all prompt-aware parameters. Example response for a call that did not match the supplied bot prompt:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"section_sequencing": {
|
|
77
|
+
"passed": "false",
|
|
78
|
+
"explanation": "The bot did not follow the section flow defined in the supplied prompt."
|
|
79
|
+
},
|
|
80
|
+
"mandatory_field_gating": {
|
|
81
|
+
"passed": "N/A",
|
|
82
|
+
"explanation": "The prompt-defined gated action was not triggered in this call."
|
|
83
|
+
},
|
|
84
|
+
"prompt_injection": {
|
|
85
|
+
"passed": "N/A",
|
|
86
|
+
"explanation": "The caller did not attempt to override instructions or inject commands."
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
72
91
|
|
|
73
92
|
## Self Simulation
|
|
74
93
|
|
|
@@ -101,6 +120,34 @@ while True:
|
|
|
101
120
|
time.sleep(2)
|
|
102
121
|
```
|
|
103
122
|
|
|
123
|
+
## Audit Discovery
|
|
124
|
+
|
|
125
|
+
Audit discovery is a production backend mode for deeper prompt-risk discovery. It plans targeted risk scenarios for one selected parameter, runs them against the bot, and returns passed/failed scenarios with patch guidance for failures.
|
|
126
|
+
|
|
127
|
+
The after-PyPI production demo lives at `simulation_demo/prod_testing/after_pypi/audit_discovery_demo.py`. A production smoke test for `section_sequencing` completed with:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"status": "completed",
|
|
132
|
+
"final_result": {
|
|
133
|
+
"overall_pass": true,
|
|
134
|
+
"stop_reason": "audit_discovery_completed",
|
|
135
|
+
"stop_message": "Audit discovery completed across planned risk scenarios.",
|
|
136
|
+
"audit_discovery": {
|
|
137
|
+
"enabled": true,
|
|
138
|
+
"passed_scenarios": [
|
|
139
|
+
{
|
|
140
|
+
"risk_id": "fatal_emergency_interruption",
|
|
141
|
+
"planned_risk_passed": true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"failed_scenarios": [],
|
|
145
|
+
"error_scenarios": []
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
104
151
|
## User Orchestrated Simulation
|
|
105
152
|
|
|
106
153
|
```python
|
|
@@ -166,6 +213,18 @@ The repository includes copy-pasteable examples in `examples/`. These are embedd
|
|
|
166
213
|
--8<-- "examples/demo_gpt.py"
|
|
167
214
|
```
|
|
168
215
|
|
|
216
|
+
### Prompt-Aware Post-Call Evaluation
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
--8<-- "examples/post_call_prompt_aware_demo.py"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Audit Discovery
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
--8<-- "examples/audit_discovery_demo.py"
|
|
226
|
+
```
|
|
227
|
+
|
|
169
228
|
### Vertex AI User Orchestrated Simulation
|
|
170
229
|
|
|
171
230
|
```python
|
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
# Bot provider credentials for self_simulation are configured on the Prodloop backend.
|
|
7
7
|
PRODLOOP_API_KEY=
|
|
8
8
|
|
|
9
|
+
# Post-call prompt-aware demo:
|
|
10
|
+
# Used by post_call_prompt_aware_demo.py.
|
|
11
|
+
POST_CALL_AUDIO_FILE=sample_call.mp3
|
|
12
|
+
POST_CALL_PROMPT_FILE=sample_prompt.txt
|
|
13
|
+
|
|
14
|
+
# Audit discovery demo:
|
|
15
|
+
# Used by audit_discovery_demo.py.
|
|
16
|
+
AUDIT_DISCOVERY_PARAMETER=section_sequencing
|
|
17
|
+
AUDIT_DISCOVERY_BOT_MODEL=azure/<deployment-name>
|
|
18
|
+
AUDIT_DISCOVERY_MAX_SCENARIOS=1
|
|
19
|
+
AUDIT_DISCOVERY_MAX_TURNS=6
|
|
20
|
+
|
|
9
21
|
# Azure OpenAI: fill this section when running GPT/Azure demos:
|
|
10
22
|
# - demo_gpt.py
|
|
11
23
|
# - user_orchestrated_demo_gpt.py
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Audit discovery using the installed Prodloop SDK.
|
|
2
|
+
|
|
3
|
+
Set POST_CALL_PROMPT_FILE, AUDIT_DISCOVERY_PARAMETER, and bot model settings
|
|
4
|
+
in examples/.env first. Audit discovery runs one selected prompt-risk parameter
|
|
5
|
+
through planned backend scenarios and returns passed/failed scenario evidence.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from prodloop import ProdloopClient, plugins
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ENV_PATH = Path(__file__).with_name(".env")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_env() -> None:
|
|
20
|
+
try:
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
except Exception:
|
|
23
|
+
return
|
|
24
|
+
load_dotenv(dotenv_path=ENV_PATH, override=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _required_path(env_name: str) -> Path:
|
|
28
|
+
raw_value = os.getenv(env_name, "").strip()
|
|
29
|
+
if not raw_value:
|
|
30
|
+
raise RuntimeError(f"Set {env_name} in {ENV_PATH}.")
|
|
31
|
+
path = Path(raw_value)
|
|
32
|
+
if not path.exists():
|
|
33
|
+
raise RuntimeError(f"{env_name} path not found: {path}")
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _int_env(name: str, default: int) -> int:
|
|
38
|
+
raw_value = os.getenv(name, "").strip()
|
|
39
|
+
return int(raw_value) if raw_value else default
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _safe_bot_payload(client: ProdloopClient, model: str, max_tokens: int) -> dict[str, Any]:
|
|
43
|
+
bot_llm = plugins.LiteLLM(
|
|
44
|
+
model=model,
|
|
45
|
+
temperature=0.2,
|
|
46
|
+
max_tokens=max_tokens,
|
|
47
|
+
)
|
|
48
|
+
return client._self_simulation_bot_payload(bot_llm.to_payload()) # noqa: SLF001
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main() -> None:
|
|
52
|
+
_load_env()
|
|
53
|
+
api_key = os.getenv("PRODLOOP_API_KEY", "").strip()
|
|
54
|
+
if not api_key:
|
|
55
|
+
raise RuntimeError(f"Set PRODLOOP_API_KEY in {ENV_PATH}.")
|
|
56
|
+
|
|
57
|
+
client = ProdloopClient(api_key=api_key, timeout_seconds=1800)
|
|
58
|
+
prompt_path = _required_path("POST_CALL_PROMPT_FILE")
|
|
59
|
+
parameter = os.getenv("AUDIT_DISCOVERY_PARAMETER", "section_sequencing").strip()
|
|
60
|
+
bot_model = os.getenv("AUDIT_DISCOVERY_BOT_MODEL", "azure/<deployment-name>").strip()
|
|
61
|
+
max_scenarios = _int_env("AUDIT_DISCOVERY_MAX_SCENARIOS", 1)
|
|
62
|
+
max_turns = _int_env("AUDIT_DISCOVERY_MAX_TURNS", 6)
|
|
63
|
+
|
|
64
|
+
start = client._post_json( # noqa: SLF001
|
|
65
|
+
"/simulate/start",
|
|
66
|
+
{
|
|
67
|
+
"simulation_mode": "audit_discovery",
|
|
68
|
+
"prompt": prompt_path.read_text(encoding="utf-8"),
|
|
69
|
+
"parameters": [parameter],
|
|
70
|
+
"bot_llm": _safe_bot_payload(client, bot_model, max_tokens=700),
|
|
71
|
+
"max_turns": max_turns,
|
|
72
|
+
"adaptive_max_conversations": max_scenarios,
|
|
73
|
+
"scenario": "Audit discovery smoke test for the selected parameter.",
|
|
74
|
+
},
|
|
75
|
+
None,
|
|
76
|
+
)
|
|
77
|
+
chat_id = start["chat_id"]
|
|
78
|
+
print("chat_id:", chat_id, flush=True)
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
result = client.get_simulation(chat_id)
|
|
82
|
+
print(
|
|
83
|
+
"status=",
|
|
84
|
+
result.get("status"),
|
|
85
|
+
"turns=",
|
|
86
|
+
len(result.get("turns") or []),
|
|
87
|
+
flush=True,
|
|
88
|
+
)
|
|
89
|
+
if result.get("status") in {"completed", "failed"}:
|
|
90
|
+
print(json.dumps(result, indent=2, ensure_ascii=False), flush=True)
|
|
91
|
+
return
|
|
92
|
+
time.sleep(5)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Post-call prompt-aware evaluation using the installed Prodloop SDK.
|
|
2
|
+
|
|
3
|
+
Run:
|
|
4
|
+
python post_call_prompt_aware_demo.py partial
|
|
5
|
+
python post_call_prompt_aware_demo.py all
|
|
6
|
+
|
|
7
|
+
Set POST_CALL_AUDIO_FILE and POST_CALL_PROMPT_FILE in examples/.env first.
|
|
8
|
+
Prompt-aware parameters return passed="true", "false", or "N/A".
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Sequence
|
|
15
|
+
|
|
16
|
+
from prodloop import EvaluationParameter, ProdloopClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ENV_PATH = Path(__file__).with_name(".env")
|
|
20
|
+
|
|
21
|
+
PROMPT_AWARE_PARAMETERS: tuple[EvaluationParameter, ...] = (
|
|
22
|
+
EvaluationParameter.SECTION_SEQUENCING,
|
|
23
|
+
EvaluationParameter.MANDATORY_FIELD_GATING,
|
|
24
|
+
EvaluationParameter.INTERRUPT_RESUME_PRECISION,
|
|
25
|
+
EvaluationParameter.CLOSING_VERBATIM_DELIVERY,
|
|
26
|
+
EvaluationParameter.SINGLE_ATTEMPT_CONSTRAINTS,
|
|
27
|
+
EvaluationParameter.INFO_DUMP_HANDLING,
|
|
28
|
+
EvaluationParameter.MID_FLOW_INTENT_SWITCH,
|
|
29
|
+
EvaluationParameter.SIDE_TALK_LEAKAGE,
|
|
30
|
+
EvaluationParameter.AMBIGUOUS_PARTIAL_RESPONSES,
|
|
31
|
+
EvaluationParameter.INTERNAL_JARGON_LEAKAGE,
|
|
32
|
+
EvaluationParameter.IDENTITY_EXTRACTION,
|
|
33
|
+
EvaluationParameter.PROMPT_INJECTION,
|
|
34
|
+
EvaluationParameter.COMMITMENT_EXTRACTION,
|
|
35
|
+
EvaluationParameter.SCOPE_BOUNDARY_TESTING,
|
|
36
|
+
EvaluationParameter.ROLEPLAY_JAILBREAK,
|
|
37
|
+
EvaluationParameter.CONTEXT_MEMORY_ACROSS_TURNS,
|
|
38
|
+
EvaluationParameter.HALLUCINATION_FABRICATION,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
PARTIAL_PARAMETERS: tuple[EvaluationParameter, ...] = (
|
|
42
|
+
EvaluationParameter.SECTION_SEQUENCING,
|
|
43
|
+
EvaluationParameter.MANDATORY_FIELD_GATING,
|
|
44
|
+
EvaluationParameter.PROMPT_INJECTION,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _load_env() -> None:
|
|
49
|
+
try:
|
|
50
|
+
from dotenv import load_dotenv
|
|
51
|
+
except Exception:
|
|
52
|
+
return
|
|
53
|
+
load_dotenv(dotenv_path=ENV_PATH, override=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parameters_for_mode(mode: str) -> Sequence[EvaluationParameter]:
|
|
57
|
+
if mode == "partial":
|
|
58
|
+
return PARTIAL_PARAMETERS
|
|
59
|
+
if mode == "all":
|
|
60
|
+
return PROMPT_AWARE_PARAMETERS
|
|
61
|
+
raise RuntimeError("Usage: python post_call_prompt_aware_demo.py [partial|all]")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _required_path(env_name: str) -> Path:
|
|
65
|
+
raw_value = os.getenv(env_name, "").strip()
|
|
66
|
+
if not raw_value:
|
|
67
|
+
raise RuntimeError(f"Set {env_name} in {ENV_PATH}.")
|
|
68
|
+
path = Path(raw_value)
|
|
69
|
+
if not path.exists():
|
|
70
|
+
raise RuntimeError(f"{env_name} path not found: {path}")
|
|
71
|
+
return path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> None:
|
|
75
|
+
_load_env()
|
|
76
|
+
api_key = os.getenv("PRODLOOP_API_KEY", "").strip()
|
|
77
|
+
if not api_key:
|
|
78
|
+
raise RuntimeError(f"Set PRODLOOP_API_KEY in {ENV_PATH}.")
|
|
79
|
+
|
|
80
|
+
mode = sys.argv[1].strip().lower() if len(sys.argv) > 1 else "partial"
|
|
81
|
+
parameters = _parameters_for_mode(mode)
|
|
82
|
+
audio_path = _required_path("POST_CALL_AUDIO_FILE")
|
|
83
|
+
prompt_path = _required_path("POST_CALL_PROMPT_FILE")
|
|
84
|
+
|
|
85
|
+
client = ProdloopClient(api_key=api_key, timeout_seconds=1800)
|
|
86
|
+
result = client.evaluate_call(
|
|
87
|
+
audio_file_path=str(audio_path),
|
|
88
|
+
input_prompt=prompt_path.read_text(encoding="utf-8"),
|
|
89
|
+
parameters=parameters,
|
|
90
|
+
timeout_seconds=1800,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
print(json.dumps(result, indent=2, ensure_ascii=False), flush=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -3,6 +3,7 @@ from prodloop.client import ProdloopClient
|
|
|
3
3
|
from prodloop.exceptions import APIError, ProdloopError, ValidationError
|
|
4
4
|
from prodloop.models import (
|
|
5
5
|
BotLLMConnection,
|
|
6
|
+
CustomEvaluationParameter,
|
|
6
7
|
EvaluationParameter,
|
|
7
8
|
SimulationMode,
|
|
8
9
|
)
|
|
@@ -11,6 +12,7 @@ from prodloop.plugins import LiteLLM, LiteLLMBot
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"ProdloopClient",
|
|
13
14
|
"BotLLMConnection",
|
|
15
|
+
"CustomEvaluationParameter",
|
|
14
16
|
"LiteLLM",
|
|
15
17
|
"LiteLLMBot",
|
|
16
18
|
"EvaluationParameter",
|
|
@@ -2,23 +2,37 @@ import json
|
|
|
2
2
|
import mimetypes
|
|
3
3
|
import time
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Callable,
|
|
8
|
+
Dict,
|
|
9
|
+
Iterable,
|
|
10
|
+
Mapping,
|
|
11
|
+
Optional,
|
|
12
|
+
Sequence,
|
|
13
|
+
Tuple,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
6
16
|
|
|
7
17
|
import requests
|
|
8
18
|
|
|
9
19
|
from prodloop.exceptions import APIError, ValidationError
|
|
10
20
|
from prodloop.models import (
|
|
11
21
|
BotLLMInput,
|
|
22
|
+
CustomParameterInput,
|
|
12
23
|
EvaluationParameter,
|
|
13
24
|
ParameterInput,
|
|
14
25
|
SimulationMode,
|
|
15
26
|
coerce_bot_llm,
|
|
27
|
+
coerce_custom_parameter,
|
|
16
28
|
coerce_parameter,
|
|
17
29
|
)
|
|
18
30
|
|
|
19
31
|
|
|
20
32
|
class ProdloopClient:
|
|
21
|
-
_SERVICE_URL =
|
|
33
|
+
_SERVICE_URL = (
|
|
34
|
+
"https://asia-south1-prodloop.cloudfunctions.net/prodloop-evaluator-fn"
|
|
35
|
+
)
|
|
22
36
|
|
|
23
37
|
def __init__(
|
|
24
38
|
self,
|
|
@@ -38,16 +52,25 @@ class ProdloopClient:
|
|
|
38
52
|
audio_file_path: str,
|
|
39
53
|
parameters: Sequence[ParameterInput],
|
|
40
54
|
thresholds: Optional[Mapping[str, Any]] = None,
|
|
55
|
+
custom_parameters: Optional[Sequence[CustomParameterInput]] = None,
|
|
41
56
|
extraction_schema: Optional[Mapping[str, str]] = None,
|
|
42
57
|
bot_captured_variables: Optional[Mapping[str, Any]] = None,
|
|
43
58
|
input_prompt: Optional[str] = None,
|
|
44
59
|
timeout_seconds: Optional[int] = None,
|
|
45
60
|
) -> Dict[str, Any]:
|
|
46
61
|
normalized_parameters = self._normalize_parameters(parameters)
|
|
62
|
+
normalized_custom_parameters = self._normalize_custom_parameters(
|
|
63
|
+
custom_parameters or []
|
|
64
|
+
)
|
|
65
|
+
if not normalized_parameters and not normalized_custom_parameters:
|
|
66
|
+
raise ValidationError("At least one evaluation parameter is required.")
|
|
67
|
+
|
|
47
68
|
normalized_input_prompt = (input_prompt or "").strip()
|
|
48
69
|
payload: Dict[str, Any] = {
|
|
49
70
|
"parameters": [param.value for param in normalized_parameters],
|
|
50
71
|
}
|
|
72
|
+
if normalized_custom_parameters:
|
|
73
|
+
payload["custom_parameters"] = normalized_custom_parameters
|
|
51
74
|
|
|
52
75
|
if thresholds:
|
|
53
76
|
payload["thresholds"] = dict(thresholds)
|
|
@@ -68,7 +91,9 @@ class ProdloopClient:
|
|
|
68
91
|
"bot_captured_variables is required when using 'extraction_variables'."
|
|
69
92
|
)
|
|
70
93
|
missing_keys = [
|
|
71
|
-
key
|
|
94
|
+
key
|
|
95
|
+
for key in extraction_schema.keys()
|
|
96
|
+
if key not in bot_captured_variables
|
|
72
97
|
]
|
|
73
98
|
if missing_keys:
|
|
74
99
|
raise ValidationError(
|
|
@@ -189,7 +214,9 @@ class ProdloopClient:
|
|
|
189
214
|
|
|
190
215
|
handler_start = time.perf_counter()
|
|
191
216
|
handler_output = bot_turn_handler(tester_message, transcript)
|
|
192
|
-
measured_handler_latency_ms = round(
|
|
217
|
+
measured_handler_latency_ms = round(
|
|
218
|
+
(time.perf_counter() - handler_start) * 1000.0, 2
|
|
219
|
+
)
|
|
193
220
|
bot_response, bot_response_latency_ms = self._normalize_bot_turn_output(
|
|
194
221
|
handler_output=handler_output,
|
|
195
222
|
measured_handler_latency_ms=measured_handler_latency_ms,
|
|
@@ -284,7 +311,12 @@ class ProdloopClient:
|
|
|
284
311
|
"token",
|
|
285
312
|
):
|
|
286
313
|
livekit_kwargs.pop(secret_key, None)
|
|
287
|
-
for backend_owned_key in (
|
|
314
|
+
for backend_owned_key in (
|
|
315
|
+
"project",
|
|
316
|
+
"location",
|
|
317
|
+
"vertex_project",
|
|
318
|
+
"vertex_location",
|
|
319
|
+
):
|
|
288
320
|
endpoint.pop(backend_owned_key, None)
|
|
289
321
|
livekit_kwargs.pop(backend_owned_key, None)
|
|
290
322
|
|
|
@@ -298,14 +330,29 @@ class ProdloopClient:
|
|
|
298
330
|
}
|
|
299
331
|
|
|
300
332
|
@staticmethod
|
|
301
|
-
def _normalize_parameters(
|
|
333
|
+
def _normalize_parameters(
|
|
334
|
+
parameters: Iterable[ParameterInput],
|
|
335
|
+
) -> Sequence[EvaluationParameter]:
|
|
302
336
|
normalized = [coerce_parameter(param) for param in parameters]
|
|
303
|
-
if not normalized:
|
|
304
|
-
raise ValidationError("At least one evaluation parameter is required.")
|
|
305
337
|
return normalized
|
|
306
338
|
|
|
307
339
|
@staticmethod
|
|
308
|
-
def
|
|
340
|
+
def _normalize_custom_parameters(
|
|
341
|
+
parameters: Iterable[CustomParameterInput],
|
|
342
|
+
) -> Sequence[Dict[str, str]]:
|
|
343
|
+
normalized = [coerce_custom_parameter(param) for param in parameters]
|
|
344
|
+
seen: set[str] = set()
|
|
345
|
+
for parameter in normalized:
|
|
346
|
+
key = parameter["key"]
|
|
347
|
+
if key in seen:
|
|
348
|
+
raise ValidationError(f"Duplicate custom parameter key: {key}")
|
|
349
|
+
seen.add(key)
|
|
350
|
+
return normalized
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _normalize_simulation_parameters(
|
|
354
|
+
parameters: Iterable[ParameterInput],
|
|
355
|
+
) -> Sequence[str]:
|
|
309
356
|
normalized: list[str] = []
|
|
310
357
|
for parameter in parameters:
|
|
311
358
|
if isinstance(parameter, EvaluationParameter):
|
|
@@ -342,9 +389,13 @@ class ProdloopClient:
|
|
|
342
389
|
try:
|
|
343
390
|
payload = response.json()
|
|
344
391
|
except ValueError as exc:
|
|
345
|
-
raise APIError(
|
|
392
|
+
raise APIError(
|
|
393
|
+
response.status_code, "Backend returned non-JSON response."
|
|
394
|
+
) from exc
|
|
346
395
|
if not isinstance(payload, dict):
|
|
347
|
-
raise APIError(
|
|
396
|
+
raise APIError(
|
|
397
|
+
response.status_code, "Backend response must be a JSON object."
|
|
398
|
+
)
|
|
348
399
|
return payload
|
|
349
400
|
|
|
350
401
|
@staticmethod
|
|
@@ -368,14 +419,18 @@ class ProdloopClient:
|
|
|
368
419
|
if isinstance(handler_output, str):
|
|
369
420
|
response_text = handler_output.strip()
|
|
370
421
|
if not response_text:
|
|
371
|
-
raise ValidationError(
|
|
422
|
+
raise ValidationError(
|
|
423
|
+
"bot_turn_handler must return a non-empty string."
|
|
424
|
+
)
|
|
372
425
|
return response_text, measured_handler_latency_ms
|
|
373
426
|
|
|
374
427
|
if isinstance(handler_output, tuple) and len(handler_output) == 2:
|
|
375
428
|
response_raw, latency_raw = handler_output
|
|
376
429
|
response_text = str(response_raw or "").strip()
|
|
377
430
|
if not response_text:
|
|
378
|
-
raise ValidationError(
|
|
431
|
+
raise ValidationError(
|
|
432
|
+
"bot_turn_handler tuple must include a non-empty response."
|
|
433
|
+
)
|
|
379
434
|
try:
|
|
380
435
|
latency_ms = float(latency_raw)
|
|
381
436
|
except (TypeError, ValueError) as exc:
|
|
@@ -383,7 +438,9 @@ class ProdloopClient:
|
|
|
383
438
|
"bot_turn_handler tuple latency must be numeric."
|
|
384
439
|
) from exc
|
|
385
440
|
if latency_ms < 0:
|
|
386
|
-
raise ValidationError(
|
|
441
|
+
raise ValidationError(
|
|
442
|
+
"bot_turn_handler tuple latency cannot be negative."
|
|
443
|
+
)
|
|
387
444
|
return response_text, round(latency_ms, 2)
|
|
388
445
|
|
|
389
446
|
if isinstance(handler_output, Mapping):
|
|
@@ -406,11 +463,12 @@ class ProdloopClient:
|
|
|
406
463
|
"bot_turn_handler mapping latency must be numeric."
|
|
407
464
|
) from exc
|
|
408
465
|
if latency_ms < 0:
|
|
409
|
-
raise ValidationError(
|
|
466
|
+
raise ValidationError(
|
|
467
|
+
"bot_turn_handler mapping latency cannot be negative."
|
|
468
|
+
)
|
|
410
469
|
return response_text, round(latency_ms, 2)
|
|
411
470
|
|
|
412
471
|
raise ValidationError(
|
|
413
472
|
"bot_turn_handler must return either a string, "
|
|
414
473
|
"(response, llm_latency_ms), or a mapping with response and latency."
|
|
415
474
|
)
|
|
416
|
-
|
|
@@ -30,7 +30,27 @@ class EvaluationParameter(str, Enum):
|
|
|
30
30
|
HALLUCINATION_FABRICATION = "hallucination_fabrication"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class CustomEvaluationParameter:
|
|
35
|
+
key: str
|
|
36
|
+
description: str
|
|
37
|
+
label: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
def to_payload(self) -> Dict[str, str]:
|
|
40
|
+
key = self.key.strip().lower().replace(" ", "_")
|
|
41
|
+
description = self.description.strip()
|
|
42
|
+
if not key:
|
|
43
|
+
raise ValueError("custom parameter key is required.")
|
|
44
|
+
if not description:
|
|
45
|
+
raise ValueError("custom parameter description is required.")
|
|
46
|
+
payload = {"key": key, "description": description}
|
|
47
|
+
if self.label and self.label.strip():
|
|
48
|
+
payload["label"] = self.label.strip()
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
|
|
33
52
|
ParameterInput = Union[EvaluationParameter, str]
|
|
53
|
+
CustomParameterInput = Union[CustomEvaluationParameter, Mapping[str, Any]]
|
|
34
54
|
|
|
35
55
|
SUPPORTED_PARAMETERS = tuple(param.value for param in EvaluationParameter)
|
|
36
56
|
|
|
@@ -86,6 +106,18 @@ def coerce_parameter(value: ParameterInput) -> EvaluationParameter:
|
|
|
86
106
|
)
|
|
87
107
|
|
|
88
108
|
|
|
109
|
+
def coerce_custom_parameter(value: CustomParameterInput) -> Dict[str, str]:
|
|
110
|
+
if isinstance(value, CustomEvaluationParameter):
|
|
111
|
+
return value.to_payload()
|
|
112
|
+
if not isinstance(value, Mapping):
|
|
113
|
+
raise ValueError("custom parameter must be a CustomEvaluationParameter or mapping.")
|
|
114
|
+
return CustomEvaluationParameter(
|
|
115
|
+
key=str(value.get("key", "")),
|
|
116
|
+
label=str(value.get("label", "")) if value.get("label") is not None else None,
|
|
117
|
+
description=str(value.get("description", "")),
|
|
118
|
+
).to_payload()
|
|
119
|
+
|
|
120
|
+
|
|
89
121
|
def coerce_bot_llm(value: Optional[BotLLMInput]) -> Optional[Dict[str, Any]]:
|
|
90
122
|
if value is None:
|
|
91
123
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prodloop-observability-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Python SDK for evaluating AI voice bot calls via Prodloop APIs.
|
|
5
5
|
Project-URL: Homepage, https://prodloop.com
|
|
6
6
|
Project-URL: Documentation, https://observability-sdk-docs.pages.dev/
|
|
@@ -27,7 +27,7 @@ pip install prodloop-observability-sdk
|
|
|
27
27
|
## Quickstart
|
|
28
28
|
|
|
29
29
|
```python
|
|
30
|
-
from prodloop import
|
|
30
|
+
from prodloop import CustomEvaluationParameter, EvaluationParameter, ProdloopClient
|
|
31
31
|
|
|
32
32
|
client = ProdloopClient(api_key="sk_live_...")
|
|
33
33
|
|
|
@@ -38,12 +38,40 @@ result = client.evaluate_call(
|
|
|
38
38
|
EvaluationParameter.HALLUCINATION,
|
|
39
39
|
],
|
|
40
40
|
thresholds={"e2e_response_time_max_ms": 800},
|
|
41
|
+
custom_parameters=[
|
|
42
|
+
CustomEvaluationParameter(
|
|
43
|
+
key="resolution_quality",
|
|
44
|
+
label="Resolution quality",
|
|
45
|
+
description="Check whether the bot correctly understood the issue and reached a useful final outcome.",
|
|
46
|
+
),
|
|
47
|
+
],
|
|
41
48
|
input_prompt="Bot instructions used during this call...",
|
|
42
49
|
)
|
|
43
50
|
|
|
44
51
|
print(result)
|
|
45
52
|
```
|
|
46
53
|
|
|
54
|
+
## Custom Parameters
|
|
55
|
+
|
|
56
|
+
Use `custom_parameters` for audit dimensions that are not part of the fixed `EvaluationParameter` enum. Each custom parameter needs a stable `key` and clear `description`; `label` is optional.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
result = client.evaluate_call(
|
|
60
|
+
audio_file_path="call.mp3",
|
|
61
|
+
parameters=[EvaluationParameter.HALLUCINATION],
|
|
62
|
+
custom_parameters=[
|
|
63
|
+
{
|
|
64
|
+
"key": "driver_resolution_quality",
|
|
65
|
+
"label": "Driver resolution quality",
|
|
66
|
+
"description": "Evaluate whether the bot handled driver-not-found or cancellation cases correctly and empathetically.",
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
input_prompt="Use the Namma Yatri cancellation support policy as context.",
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Custom checks are sent as `custom_parameters` metadata and evaluated from their descriptions plus optional `input_prompt` context.
|
|
74
|
+
|
|
47
75
|
## Extraction Validation
|
|
48
76
|
|
|
49
77
|
To validate extraction quality, pass both `extraction_schema` and `bot_captured_variables`:
|
|
@@ -89,6 +117,27 @@ Prompt-aware parameter results use a compact shape:
|
|
|
89
117
|
|
|
90
118
|
`passed` can be `"true"`, `"false"`, or `"N/A"`. `"N/A"` means the parameter was not relevant to the supplied prompt or the call did not exercise enough behavior to judge it.
|
|
91
119
|
|
|
120
|
+
Example prompt-aware response:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"section_sequencing": {
|
|
125
|
+
"passed": "false",
|
|
126
|
+
"explanation": "The bot did not follow the section flow defined in the supplied prompt."
|
|
127
|
+
},
|
|
128
|
+
"mandatory_field_gating": {
|
|
129
|
+
"passed": "N/A",
|
|
130
|
+
"explanation": "The prompt-defined gated action was not triggered in this call."
|
|
131
|
+
},
|
|
132
|
+
"prompt_injection": {
|
|
133
|
+
"passed": "N/A",
|
|
134
|
+
"explanation": "The caller did not attempt to override instructions or inject commands."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Runnable example: `examples/post_call_prompt_aware_demo.py`. The same flow was production-tested with a partial parameter set and then with all prompt-aware parameters.
|
|
140
|
+
|
|
92
141
|
## Supported Parameters
|
|
93
142
|
|
|
94
143
|
- `e2e_response_time`
|
|
@@ -169,6 +218,8 @@ There are two modes:
|
|
|
169
218
|
- `self_simulation`: Prodloop backend runs the tester and bot conversation. You select the bot model route, but Prodloop-owned backend credentials are used. No bot credentials are sent from your code.
|
|
170
219
|
- `user_orchestrated`: Prodloop backend runs the tester and grader. Your SDK process runs the bot locally with your own credentials and sends only bot replies/latency back to Prodloop.
|
|
171
220
|
|
|
221
|
+
The production backend also supports `audit_discovery` for deeper prompt-risk discovery. Runnable examples are available in `examples/audit_discovery_demo.py` and `simulation_demo/prod_testing/after_pypi/audit_discovery_demo.py`.
|
|
222
|
+
|
|
172
223
|
Simulation currently accepts exactly one parameter per request. To test multiple parameters, start one simulation per parameter. `max_turns` is configurable from `1` to `10`.
|
|
173
224
|
|
|
174
225
|
Discover currently enabled simulation parameters at runtime:
|
|
@@ -259,6 +310,10 @@ For `user_orchestrated`, configure bot credentials locally for either Vertex AI
|
|
|
259
310
|
|
|
260
311
|
For adaptive simulations, `max_turns` controls turns per conversation and `adaptive_max_conversations` controls the maximum number of conversations to explore.
|
|
261
312
|
|
|
313
|
+
### Audit Discovery
|
|
314
|
+
|
|
315
|
+
Audit discovery plans targeted risk scenarios for one selected parameter, runs them against the bot, and returns passed/failed scenarios plus patch guidance for failures. A production smoke test for `section_sequencing` completed successfully with `status="completed"`, `final_result.overall_pass=true`, and `final_result.stop_reason="audit_discovery_completed"`.
|
|
316
|
+
|
|
262
317
|
### Result Shape
|
|
263
318
|
|
|
264
319
|
Simulation responses include:
|
|
@@ -12,8 +12,10 @@ docs/index.md
|
|
|
12
12
|
docs/parameters.md
|
|
13
13
|
docs/visual-overview.md
|
|
14
14
|
examples/.env.example
|
|
15
|
+
examples/audit_discovery_demo.py
|
|
15
16
|
examples/demo.py
|
|
16
17
|
examples/demo_gpt.py
|
|
18
|
+
examples/post_call_prompt_aware_demo.py
|
|
17
19
|
examples/user_orchestrated_demo.py
|
|
18
20
|
examples/user_orchestrated_demo_gpt.py
|
|
19
21
|
prodloop/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/authentication.md
RENAMED
|
File without changes
|
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/getting-started.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/visual-overview.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/exceptions.py
RENAMED
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/__init__.py
RENAMED
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/_utils.py
RENAMED
|
File without changes
|
{prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/litellm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|