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.
Files changed (34) hide show
  1. {prodloop_observability_sdk-0.1.8/prodloop_observability_sdk.egg-info → prodloop_observability_sdk-0.1.9}/PKG-INFO +57 -2
  2. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/README.md +56 -1
  3. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/examples.md +59 -0
  4. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/.env.example +12 -0
  5. prodloop_observability_sdk-0.1.9/examples/audit_discovery_demo.py +96 -0
  6. prodloop_observability_sdk-0.1.9/examples/post_call_prompt_aware_demo.py +97 -0
  7. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/__init__.py +2 -0
  8. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/client.py +74 -16
  9. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/models.py +32 -0
  10. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9/prodloop_observability_sdk.egg-info}/PKG-INFO +57 -2
  11. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/SOURCES.txt +2 -0
  12. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/pyproject.toml +1 -1
  13. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/CHANGELOG.md +0 -0
  14. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/MANIFEST.in +0 -0
  15. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/api-reference.md +0 -0
  16. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/authentication.md +0 -0
  17. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/errors.md +0 -0
  18. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/getting-started.md +0 -0
  19. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/index.md +0 -0
  20. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/parameters.md +0 -0
  21. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/docs/visual-overview.md +0 -0
  22. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/demo.py +0 -0
  23. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/demo_gpt.py +0 -0
  24. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/user_orchestrated_demo.py +0 -0
  25. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/examples/user_orchestrated_demo_gpt.py +0 -0
  26. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/mkdocs.yml +0 -0
  27. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/exceptions.py +0 -0
  28. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/__init__.py +0 -0
  29. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/_utils.py +0 -0
  30. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop/plugins/litellm.py +0 -0
  31. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/dependency_links.txt +0 -0
  32. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/requires.txt +0 -0
  33. {prodloop_observability_sdk-0.1.8 → prodloop_observability_sdk-0.1.9}/prodloop_observability_sdk.egg-info/top_level.txt +0 -0
  34. {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.8
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 ProdloopClient, EvaluationParameter
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 ProdloopClient, EvaluationParameter
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 Any, Callable, Dict, Iterable, Mapping, Optional, Sequence, Tuple, Union
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 = "https://asia-south1-prodloop.cloudfunctions.net/prodloop-evaluator-fn"
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 for key in extraction_schema.keys() if key not in bot_captured_variables
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((time.perf_counter() - handler_start) * 1000.0, 2)
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 ("project", "location", "vertex_project", "vertex_location"):
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(parameters: Iterable[ParameterInput]) -> Sequence[EvaluationParameter]:
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 _normalize_simulation_parameters(parameters: Iterable[ParameterInput]) -> Sequence[str]:
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(response.status_code, "Backend returned non-JSON response.") from exc
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(response.status_code, "Backend response must be a JSON object.")
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("bot_turn_handler must return a non-empty string.")
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("bot_turn_handler tuple must include a non-empty response.")
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("bot_turn_handler tuple latency cannot be negative.")
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("bot_turn_handler mapping latency cannot be negative.")
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.8
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 ProdloopClient, EvaluationParameter
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "prodloop-observability-sdk"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  description = "Python SDK for evaluating AI voice bot calls via Prodloop APIs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"