jevzilla 0.1.0__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.
jevzilla-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ujjwalkumar Soni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: jevzilla
3
+ Version: 0.1.0
4
+ Summary: A tiny, fearless client for the JEV evaluation API
5
+ Requires-Python: >=3.8
6
+ License-File: LICENSE
7
+ Requires-Dist: requests>=2.28
8
+ Dynamic: license-file
@@ -0,0 +1,424 @@
1
+ # JEVzilla 🦖
2
+
3
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![PyPI](https://img.shields.io/badge/pypi-jevzilla-brightgreen.svg)](https://pypi.org/project/jevzilla/)
6
+
7
+ A fearless, lightweight Python client for the JEV decision evaluation API with **150+ production-ready business decision examples** across 30 sectors.
8
+
9
+ Write decision payloads exactly like the [official JEV API docs](https://jevplayground.com/jev-api), and JEVzilla handles the translation automatically. Perfect for operational decisions, compliance automation, and AI-driven business logic.
10
+
11
+ ## 🤔 What is JEV?
12
+
13
+ **JEV** is a **decision layer for software applications** — not a chatbot or text generator, but a system designed to make **structured, typed decisions** that your code can consume and act on.
14
+
15
+ Given a piece of context (called **state**), JEV answers **typed questions** and returns **structured answers with probability signals**. Your application then branches on these decisions to automate workflows, route requests, or flag for human review.
16
+
17
+ ### Why JEV is Different
18
+
19
+ | Traditional LLM | JEV |
20
+ |---|---|
21
+ | Open-ended text responses | **Bounded, typed decisions** |
22
+ | Requires parsing output | **Structured JSON answers** |
23
+ | May hallucinate or ramble | **Focused yes/no, choice, or score** |
24
+ | Hard to integrate into workflows | **Built for programmatic consumption** |
25
+
26
+ **JEV is best for:**
27
+ - ✅ Operational triage & routing (which team owns this?)
28
+ - ✅ Risk/severity scoring (how critical is this?)
29
+ - ✅ Yes/no classification (should this be escalated?)
30
+ - ✅ Conditional business logic (decide what happens next)
31
+
32
+ **JEV is NOT for:**
33
+ - ❌ Open-ended explanations
34
+ - ❌ Creative content generation
35
+ - ❌ Long conversational responses
36
+ - → Use a generative LLM for those
37
+
38
+ ## ✨ What Makes JEVzilla Special
39
+
40
+ - **Production-Ready Examples**: 150+ real-world business decision scripts, fully runnable
41
+ - **Multi-Sector Coverage**: 30 sectors including HR, Finance, Healthcare, Legal, Banking, Insurance, Cybersecurity, Retail, Supply Chain, Construction, Agriculture, Transportation, Food & Beverage, Pharmaceutical, Government, Aviation, Tourism, Environmental, Sports, and more
42
+ - **Simple, Intuitive API**: Just `noul()` (yes/no), `choice()` (select one), and `score()` (prioritize)
43
+ - **Flexible Backends**: Playground, Official API, or OpenRouter
44
+ - **Zero Boilerplate**: Auto-translates payloads — no manual wire format needed
45
+ - **Battle-Tested**: Retry logic, exponential backoff, comprehensive error handling
46
+
47
+ ## 🚀 Quick Start
48
+
49
+ ### Installation
50
+
51
+ ```bash
52
+ pip install jevzilla
53
+ ```
54
+
55
+ ### Basic Usage
56
+
57
+ ```python
58
+ from jevzilla import JEVzilla, noul, choice, score
59
+
60
+ # Initialize the client
61
+ jev = JEVzilla()
62
+
63
+ # Ask a decision
64
+ response = jev.evaluate({
65
+ "state": "Customer charged twice, cannot access account.",
66
+ "questions": {
67
+ "refund_review": noul("Does this need refund review?"),
68
+ "team": choice("Which team?", {
69
+ "billing": "Payment issues",
70
+ "technical": "Access problems"
71
+ }),
72
+ "urgency": score("How urgent?", ["Routine", "Time-sensitive", "Blocked"]),
73
+ },
74
+ })
75
+
76
+ print(response.status_code, response.answers)
77
+ # Output: 200 {'refund_review': {...}, 'team': {...}, 'urgency': {...}}
78
+ ```
79
+
80
+ ### Inspecting the Payload
81
+
82
+ ```python
83
+ # See the exact payload that gets sent
84
+ jev = JEVzilla()
85
+ payload = jev.build({
86
+ "state": "High CPU usage detected",
87
+ "questions": {
88
+ "urgent": noul("Is this critical?"),
89
+ }
90
+ })
91
+ print(payload)
92
+ ```
93
+
94
+ ## 🎯 The Three Question Types
95
+
96
+ JEV supports three core question types for making decisions:
97
+
98
+ ### **Noul** — Yes/No Judgment
99
+ A **focused, proposition-based question** that returns a probability between 0 and 1.
100
+
101
+ **When to use:** Single yes/no decisions, risk flags, human review gates
102
+ ```python
103
+ noul("Does this request contain an urgent deadline?")
104
+ # Returns: {"noul": 0.87} (87% confidence it's urgent)
105
+ ```
106
+
107
+ **Real examples:**
108
+ - "Should this order be flagged for manual review?"
109
+ - "Does the customer have churn risk?"
110
+ - "Is this transaction fraudulent?"
111
+
112
+ ---
113
+
114
+ ### **Choice** — Classification & Routing
115
+ Select **one option from a predefined set** of categories, plus confidence scores.
116
+
117
+ **When to use:** Routing decisions, ticket classification, routing to teams
118
+ ```python
119
+ choice("Which team should handle this?", {
120
+ "billing": "Payments & billing issues",
121
+ "technical": "Technical problems",
122
+ "support": "General support requests"
123
+ })
124
+ # Returns: {"choice": "billing", "confidence": 0.92}
125
+ ```
126
+
127
+ **Real examples:**
128
+ - "Route this ticket to: Sales, Support, or Technical?"
129
+ - "Classify this transaction: Normal, Suspicious, or Blocked?"
130
+ - "Which priority bucket: Low, Medium, High, or Critical?"
131
+
132
+ ---
133
+
134
+ ### **Score** — Severity, Priority, or Quality Ranking
135
+ Rank context on an **ordered scale** with confidence signals (e.g., Low → Medium → High → Critical).
136
+
137
+ **When to use:** Risk/severity scoring, priority ranking, quality assessment
138
+ ```python
139
+ score("How urgent is this incident?", ["Routine", "Time-sensitive", "Blocked"])
140
+ # Returns: {"score": 2, "legend": ["Routine", "Time-sensitive", "Blocked"]}
141
+ ```
142
+
143
+ **Real examples:**
144
+ - "Incident severity: Info, Warning, or Critical?"
145
+ - "Churn risk: Low, Medium, or High?"
146
+ - "Loan approval score: 1–5?"
147
+
148
+ ---
149
+
150
+ ### Summary Table
151
+
152
+ | Type | Question | Answer | Use Case |
153
+ |------|----------|--------|----------|
154
+ | **noul** | Yes/no proposition | 0.0–1.0 probability | Gate decisions, flags |
155
+ | **choice** | Pick one category | One option + confidence | Routing, classification |
156
+ | **score** | Rank on ordered scale | Position + probability | Risk/severity scoring |
157
+
158
+ ## 🔧 Advanced Features
159
+
160
+ ### Convenience Shortcuts
161
+
162
+ ```python
163
+ # Single-question shortcuts
164
+ response = jev.ask_noul(
165
+ state="Server down for 2 hours",
166
+ instructions="Is this a P1 incident?",
167
+ )
168
+
169
+ response = jev.ask_choice(
170
+ state="New customer inquiry",
171
+ instructions="Which team should handle this?",
172
+ criteria={"sales": "Enterprise Sales", "support": "Support"},
173
+ )
174
+
175
+ response = jev.ask_score(
176
+ state="Database backup failed",
177
+ instructions="How critical?",
178
+ levels=["Informational", "Warning", "Critical"],
179
+ )
180
+ ```
181
+
182
+ ### Custom Backends
183
+
184
+ ```python
185
+ # Official TypeSafe API (requires TYPESAFE_API_KEY env var)
186
+ jev = JEVzilla(backend="typesafe")
187
+
188
+ # OpenRouter API (requires OPENROUTER_API_KEY env var)
189
+ jev = JEVzilla(backend="openrouter")
190
+
191
+ # Custom URL or API key
192
+ jev = JEVzilla(backend="typesafe", api_key="your-api-key-here")
193
+ ```
194
+
195
+ ### Retry & Timeout Control
196
+
197
+ ```python
198
+ jev = JEVzilla(
199
+ timeout=45.0, # Request timeout in seconds
200
+ retries=4, # Max retry attempts
201
+ backoff=1.0, # Initial backoff multiplier
202
+ max_backoff=30.0, # Maximum backoff cap
203
+ )
204
+ ```
205
+
206
+ ### Batch Processing
207
+
208
+ ```python
209
+ # Process multiple decisions
210
+ decisions = [
211
+ {"state": "...", "questions": {...}},
212
+ {"state": "...", "questions": {...}},
213
+ ]
214
+
215
+ for decision in decisions:
216
+ response = jev.evaluate(decision)
217
+ if response.ok:
218
+ print(f"Answer: {response.answers}")
219
+ ```
220
+
221
+ ## 📚 150+ Real-World Examples Across 30 Sectors
222
+
223
+ JEVzilla includes **150+ production-ready business decision scripts** organized by sector:
224
+
225
+ ### Original 20 Sectors (100 examples)
226
+
227
+ | Sector | Scripts | Use Cases |
228
+ |--------|---------|-----------|
229
+ | **HR** | 5 | Resume screening, Interview feedback, Grievance triage, Performance calibration, Offer risk |
230
+ | **Finance** | 5 | Invoice review, Expense audit, Budget variance, Credit applications, Anomaly detection |
231
+ | **Healthcare** | 5 | Patient intake, Pre-auth review, No-show risk, Billing disputes, Feedback analysis |
232
+ | **Legal** | 5 | Contract review, NDA analysis, Compliance gaps, Litigation holds, Trademark disputes |
233
+ | **Banking** | 5 | Loan applications, Fraud detection, Account opening, Disputes, AML alerts |
234
+ | **Insurance** | 5 | Claims triage, Renewal risk, Underwriting, Subrogation, Complaints |
235
+ | **IT/DevOps** | 5 | Incident severity, Change risk, Code review, Access requests, Vendor security |
236
+ | **Manufacturing** | 5 | Defect triage, Supplier audit, Safety incidents, Maintenance, Change requests |
237
+ | **Logistics** | 5 | Shipment exceptions, Supplier delays, Customs review, Carrier performance, Warehouse |
238
+ | **Retail** | 5 | Return fraud, Review moderation, Reorder priority, Complaint routing, Price matching |
239
+ | **Real Estate** | 5 | Tenant screening, Maintenance, Lease renewal, Inspections, Offers |
240
+ | **Education** | 5 | Submission review, Admissions, Financial aid, Integrity checks, Feedback |
241
+ | **Cybersecurity** | 5 | Phishing triage, Vulnerability review, Access anomalies, Incident response, Vendor risk |
242
+ | **Marketing** | 5 | Content review, Influencer vetting, Ad compliance, Survey sentiment, Brand mentions |
243
+ | **Customer Support** | 5 | Ticket triage, Refund decisions, Churn flags, Escalation, CSAT follow-up |
244
+ | **Energy/Utilities** | 5 | Outage triage, Meter anomalies, Safety incidents, Assistance review, Maintenance |
245
+ | **Hospitality** | 5 | Guest complaints, Booking fraud, Overbooking, Travel refunds, Review responses |
246
+ | **Media/Entertainment** | 5 | Content moderation, Copyright claims, Brand safety, Comments, Licensing |
247
+ | **Telecom** | 5 | Outage triage, SIM swap review, Churn risk, Billing disputes, Upgrade eligibility |
248
+ | **Sales** | 5 | Lead qualification, Deal risk, Churn prediction, Proposals, Discount approvals |
249
+
250
+ ### NEW 10 Sectors (50 examples)
251
+
252
+ | Sector | Scripts | Use Cases |
253
+ |--------|---------|-----------|
254
+ | **Construction** | 5 | Site safety triage, Contractor vetting, Project delay risk, Equipment maintenance, Permit compliance |
255
+ | **Agriculture** | 5 | Crop health assessment, Equipment maintenance, Pest/disease triage, Supplier quality, Harvest readiness |
256
+ | **Transportation** | 5 | Vehicle maintenance scheduling, Accident damage assessment, Warranty claims, Fuel efficiency, Recall prioritization |
257
+ | **Food & Beverage** | 5 | Health inspection escalation, Food safety incidents, Supplier quality, Customer complaints, Menu performance |
258
+ | **Pharmaceutical** | 5 | Clinical trial eligibility, Adverse event severity, Drug compound quality, Regulatory compliance gaps, Research proposals |
259
+ | **Government** | 5 | Permit applications, Public assistance eligibility, Compliance violations, Budget allocation, FOIA requests |
260
+ | **Aviation** | 5 | Maintenance scheduling, Safety incidents, Pilot incidents, Regulatory compliance, Crew scheduling |
261
+ | **Tourism** | 5 | Booking fraud detection, Travel insurance claims, Destination safety, Customer complaints, Tour guide vetting |
262
+ | **Environmental** | 5 | Environmental incidents, Compliance violations, Carbon footprint, Waste management, Green certification |
263
+ | **Sports & Fitness** | 5 | Gym membership eligibility, Training injury assessment, Equipment maintenance, Member complaints, Program recommendations |
264
+
265
+ ### Running Examples
266
+
267
+ ```bash
268
+ # Install the package
269
+ pip install -e .
270
+
271
+ # Set your API key
272
+ export OPENROUTER_API_KEY=your-key-here
273
+
274
+ # Run any example
275
+ cd examples/hr
276
+ python resume_screening.py
277
+
278
+ # Or run from any sector
279
+ cd examples/banking
280
+ python fraud_transaction_flag.py
281
+ ```
282
+
283
+ Every script follows the same **gate + route + priority** pattern:
284
+ - **Gate** (`noul`): Should this be escalated/flagged?
285
+ - **Route** (`choice`): Which team owns this?
286
+ - **Priority** (`score`): How urgent/severe?
287
+
288
+ This consistency makes it easy to adapt examples to your own workflows.
289
+
290
+ ## 🔐 Security & Compliance
291
+
292
+ - **No hardcoded keys**: All API keys via environment variables
293
+ - **Regulated data guidance**: Examples for Healthcare, Banking, Legal include compliance notes
294
+ - **Safe by default**: Demos use invented data — swap in yours with compliance approval
295
+ - **Transparent**: Source available, audit-ready
296
+
297
+ ## 📊 Error Handling
298
+
299
+ ```python
300
+ from jevzilla import JEVError, JEVValidationError, JEVHTTPError
301
+
302
+ try:
303
+ response = jev.evaluate({"state": "...", "questions": {...}})
304
+ except JEVValidationError as e:
305
+ print(f"Invalid payload: {e}")
306
+ except JEVHTTPError as e:
307
+ print(f"API error {e.status_code}: {e.body}")
308
+ except JEVError as e:
309
+ print(f"Other error: {e}")
310
+ ```
311
+
312
+ ## 🛠️ API Reference
313
+
314
+ ### `JEVzilla` Class
315
+
316
+ ```python
317
+ jev = JEVzilla(
318
+ backend: str = "playground", # "playground", "typesafe", or "openrouter"
319
+ api_key: Optional[str] = None, # API key (or use env vars)
320
+ model: str = "jev-latest", # Model version
321
+ url: Optional[str] = None, # Custom endpoint URL
322
+ timeout: float = 30.0, # Request timeout (seconds)
323
+ retries: int = 4, # Max retry attempts
324
+ backoff: float = 1.0, # Initial backoff multiplier
325
+ max_backoff: float = 30.0, # Maximum backoff (seconds)
326
+ session: Optional[requests.Session] = None, # Custom session
327
+ extra_headers: Optional[Dict] = None, # Extra HTTP headers
328
+ )
329
+ ```
330
+
331
+ ### Methods
332
+
333
+ | Method | Purpose |
334
+ |--------|---------|
335
+ | `evaluate(payload, raise_for_status=True)` | Send a decision request |
336
+ | `build(payload)` | Get the exact body without sending |
337
+ | `ask_noul(state, instructions, ...)` | Quick yes/no question |
338
+ | `ask_choice(state, instructions, criteria, ...)` | Quick choice question |
339
+ | `ask_score(state, instructions, levels, ...)` | Quick score question |
340
+
341
+ ### Response Object
342
+
343
+ ```python
344
+ response.ok # bool: status in 200-299
345
+ response.status_code # int: HTTP status
346
+ response.text # str: raw response body
347
+ response.data # dict: parsed JSON response
348
+ response.answers # dict or list: the answers
349
+ response.noul(key, threshold) # Extract boolean answer (0-1 probability)
350
+ response.choice(key) # Extract choice answer
351
+ response.score_label(key) # Extract score label
352
+ ```
353
+
354
+ ## 📦 Dependencies
355
+
356
+ - **Python**: 3.8+
357
+ - **requests**: 2.28+
358
+
359
+ ## 📄 License
360
+
361
+ MIT License — see LICENSE file for details.
362
+
363
+ ## 👨‍💻 Author
364
+
365
+ **Ujjwalkumar Soni**
366
+
367
+ ## 🤝 Contributing
368
+
369
+ Contributions welcome! Submit issues, feature requests, or pull requests on GitHub.
370
+
371
+ ## 🎓 Use Cases & Patterns
372
+
373
+ ### Gate + Route + Priority Pattern
374
+
375
+ Every JEVzilla example follows the same battle-tested pattern:
376
+
377
+ 1. **Gate** (`noul`): **Should this be escalated/flagged?**
378
+ - "Does this need manual review?"
379
+ - "Is this a fraud risk?"
380
+
381
+ 2. **Route** (`choice`): **Which team owns this?**
382
+ - "Route to: Billing, Technical, or Support?"
383
+ - "Assign to: Team A, Team B, or Team C?"
384
+
385
+ 3. **Priority** (`score`): **How urgent/severe is it?**
386
+ - "Severity: Low, Medium, High, or Critical?"
387
+ - "Priority: Routine, Standard, or Urgent?"
388
+
389
+ This consistency makes decisions **predictable, auditable, and easy to act on**.
390
+
391
+ ### Real-World Applications
392
+
393
+ - **Compliance & Automation**: Flag risky applications, contracts, or transactions
394
+ - **Operational Triage**: Route tickets, incidents, and requests to the right team
395
+ - **Risk Assessment**: Score vendors, customers, transactions for underwriting/approval
396
+ - **Quality Control**: Review content, code, or documentation
397
+ - **Decision Support**: Augment human judgment with structured, consistent reasoning
398
+ - **Fraud Detection**: Identify suspicious patterns and route for investigation
399
+ - **Customer Support**: Triage complaints, determine response level, route to specialists
400
+ - **HR & Recruiting**: Screen resumes, assess candidates, route for interviews
401
+ - **Incident Response**: Classify severity, route to on-call engineer, set SLA
402
+
403
+ ## ⚠️ Important Notes
404
+
405
+ - These **examples use invented data** — adapt to your own workflows
406
+ - For regulated data (PHI, financial account info, legal docs), ensure **organizational compliance approval** before sending to third-party APIs
407
+ - Always **rotate API keys** before committing or sharing
408
+ - This library translates payloads but doesn't replace human oversight for critical decisions
409
+
410
+ ## 🔗 Resources
411
+
412
+ - [JEV API Playground](https://jevplayground.com/jev-api)
413
+ - [TypeSafe Official API](https://api.typesafe.ai/)
414
+ - [OpenRouter API](https://openrouter.ai/api/alpha/decisions)
415
+
416
+ ## 📞 Support
417
+
418
+ - Found a bug? Open an issue
419
+ - Have a question? Check the examples first
420
+ - Want to share a use case? We'd love to hear it!
421
+
422
+ ---
423
+
424
+ **Star this repo if JEVzilla saves you time or helps you build smarter business decisions!** ⭐
@@ -0,0 +1,294 @@
1
+ """
2
+ JEVzilla - a tiny, fearless client for JEV.
3
+
4
+ You write payloads exactly like the JEV API docs (https://jevplayground.com/jev-api):
5
+
6
+ {"state": "...", "questions": {"urgent": {"type": "noul", "instructions": "..."}}}
7
+
8
+ Question types follow the docs: noul (alias: boolean), choice, score.
9
+ Before sending, JEVzilla translates the payload to the wire format the
10
+ playground endpoint (/api/evaluate) expects:
11
+
12
+ noul, no criteria -> {"instructions", "type": "boolean"}
13
+ noul, with criteria -> {"instructions", "type": "choice", "criteria": {"true": ..., "false": ...}}
14
+ choice -> {"instructions", "type": "choice", "criteria": {label: description}}
15
+ score -> {"instructions", "type": "score", "criteria": [level, ...]}
16
+
17
+ state (str / dict / list) -> always a string on the playground wire.
18
+
19
+ backend="typesafe" instead sends the docs format as-is to the official
20
+ endpoint (POST https://api.typesafe.ai/v1/systemone) with a Bearer API key.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import copy
25
+ import json
26
+ import os
27
+ import random
28
+ import time
29
+ from dataclasses import dataclass, field
30
+ from typing import Any, Dict, Optional, Sequence, Tuple
31
+
32
+ import requests
33
+
34
+ __all__ = [
35
+ "JEVzilla", "JEVResponse", "JEVError", "JEVValidationError", "JEVHTTPError",
36
+ "to_playground_payload", "to_typesafe_payload",
37
+ "noul", "choice", "score", "Noul", "Choice", "Score",
38
+ ]
39
+ __version__ = "0.3.0"
40
+
41
+ PLAYGROUND_URL = "https://jevplayground.com/api/evaluate"
42
+ TYPESAFE_URL = "https://api.typesafe.ai/v1/systemone"
43
+ OPENROUTER_URL = "https://openrouter.ai/api/alpha/decisions"
44
+ _ORIGIN = "https://jevplayground.com"
45
+ _RETRY_STATUSES = {429, 502, 503, 504, 529}
46
+
47
+ # docs-style type names accepted on input -> canonical
48
+ _TYPE_ALIASES = {
49
+ "noul": "noul", "boolean": "noul", "bool": "noul",
50
+ "choice": "choice",
51
+ "score": "score",
52
+ }
53
+
54
+
55
+ class JEVError(Exception):
56
+ """Base error for JEVzilla."""
57
+
58
+
59
+ class JEVValidationError(JEVError, ValueError):
60
+ """Payload could not be translated into a valid JEV request."""
61
+
62
+
63
+ class JEVHTTPError(JEVError):
64
+ def __init__(self, status_code: int, body: str):
65
+ super().__init__(f"JEV API returned HTTP {status_code}: {body[:300]}")
66
+ self.status_code = status_code
67
+ self.body = body
68
+
69
+
70
+ # ------------------------------------------------- docs-style builders
71
+ def noul(instructions: str, criteria: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
72
+ q: Dict[str, Any] = {"type": "noul", "instructions": instructions}
73
+ if criteria is not None:
74
+ q["criteria"] = criteria
75
+ return q
76
+
77
+
78
+ def choice(instructions: str, criteria: Dict[str, str]) -> Dict[str, Any]:
79
+ return {"type": "choice", "instructions": instructions, "criteria": criteria}
80
+
81
+
82
+ def score(instructions: str, criteria: Sequence[str]) -> Dict[str, Any]:
83
+ return {"type": "score", "instructions": instructions, "criteria": list(criteria)}
84
+
85
+
86
+ Noul, Choice, Score = noul, choice, score # mirror the SDK's capitalized names
87
+
88
+
89
+ # ------------------------------------------------------ parsing (docs style)
90
+ def _parse_question(name: str, q: Any) -> Tuple[str, str, Any]:
91
+ """Validate one docs-style question -> (canonical type, instructions, criteria)."""
92
+ where = f"questions[{name!r}]"
93
+ if not isinstance(q, dict):
94
+ raise JEVValidationError(f"{where} must be an object")
95
+
96
+ instructions = q.get("instructions")
97
+ if not isinstance(instructions, str) or not instructions.strip():
98
+ raise JEVValidationError(f"{where}.instructions must be a non-empty string")
99
+
100
+ raw = q.get("type")
101
+ qtype = _TYPE_ALIASES.get(str(raw).strip().lower()) if raw is not None else None
102
+ if qtype is None:
103
+ raise JEVValidationError(f"{where}.type must be one of noul | boolean | choice | score (got {raw!r})")
104
+
105
+ criteria = q.get("criteria")
106
+
107
+ if qtype == "noul":
108
+ if criteria in (None, {}, []):
109
+ return qtype, instructions, None
110
+ if not isinstance(criteria, dict):
111
+ raise JEVValidationError(f"{where}.criteria for 'noul' must be an object like {{'true': ..., 'false': ...}}")
112
+ return qtype, instructions, {str(k): str(v) for k, v in criteria.items()}
113
+
114
+ if qtype == "choice":
115
+ if isinstance(criteria, dict) and criteria:
116
+ return qtype, instructions, {str(k): str(v) for k, v in criteria.items()}
117
+ if isinstance(criteria, (list, tuple)) and criteria: # lenient: ["a","b"] -> {"a":"a","b":"b"}
118
+ return qtype, instructions, {str(c): str(c) for c in criteria}
119
+ raise JEVValidationError(f"{where}.criteria must be a non-empty object for 'choice'")
120
+
121
+ # score
122
+ if isinstance(criteria, (list, tuple)) and criteria:
123
+ return qtype, instructions, [str(c) for c in criteria]
124
+ raise JEVValidationError(f"{where}.criteria must be a non-empty list of levels for 'score'")
125
+
126
+
127
+ def _parse_payload(payload: Dict[str, Any]) -> Tuple[Any, Dict[str, Tuple[str, str, Any]]]:
128
+ if not isinstance(payload, dict):
129
+ raise JEVValidationError("payload must be a dict")
130
+ state = payload.get("state")
131
+ if state is None or (isinstance(state, str) and not state.strip()) or state in ({}, []):
132
+ raise JEVValidationError("payload.state must be a non-empty string, object or array")
133
+ if not isinstance(state, (str, dict, list)):
134
+ raise JEVValidationError("payload.state must be a string, object or array")
135
+ questions = payload.get("questions")
136
+ if not isinstance(questions, dict) or not questions:
137
+ raise JEVValidationError("payload.questions must be a non-empty dict")
138
+ parsed = {str(n): _parse_question(str(n), copy.deepcopy(q)) for n, q in questions.items()}
139
+ return state, parsed
140
+
141
+
142
+ # ------------------------------------------------------------- translators
143
+ def to_playground_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
144
+ """docs-style payload -> the exact shape /api/evaluate expects."""
145
+ state, parsed = _parse_payload(payload)
146
+ if not isinstance(state, str):
147
+ state = json.dumps(state, ensure_ascii=False, indent=2)
148
+
149
+ questions: Dict[str, Any] = {}
150
+ for name, (qtype, instructions, criteria) in parsed.items():
151
+ if qtype == "noul":
152
+ if criteria is None:
153
+ questions[name] = {"instructions": instructions, "type": "boolean"}
154
+ else: # true/false criteria -> the "choice" shape from the working example
155
+ questions[name] = {"instructions": instructions, "type": "choice", "criteria": criteria}
156
+ else: # choice / score pass through
157
+ questions[name] = {"instructions": instructions, "type": qtype, "criteria": criteria}
158
+ return {"state": state, "questions": questions}
159
+
160
+
161
+ def to_typesafe_payload(payload: Dict[str, Any], model: str = "jev-latest") -> Dict[str, Any]:
162
+ """docs-style payload -> validated body for the official /v1/systemone endpoint."""
163
+ state, parsed = _parse_payload(payload)
164
+ questions: Dict[str, Any] = {}
165
+ for name, (qtype, instructions, criteria) in parsed.items():
166
+ q = {"type": qtype, "instructions": instructions}
167
+ if criteria is not None:
168
+ q["criteria"] = criteria
169
+ questions[name] = q
170
+ return {"model": model, "state": state, "questions": questions}
171
+
172
+
173
+ # ------------------------------------------------------------------ client
174
+ @dataclass
175
+ class JEVResponse:
176
+ status_code: int
177
+ text: str
178
+ data: Any = None
179
+ request: Dict[str, Any] = field(default_factory=dict) # the body actually sent
180
+
181
+ @property
182
+ def ok(self) -> bool:
183
+ return 200 <= self.status_code < 300
184
+
185
+ @property
186
+ def answers(self) -> Any:
187
+ """`data["answers"]` when present (official shape), else the raw parsed body."""
188
+ if isinstance(self.data, dict) and "answers" in self.data:
189
+ return self.data["answers"]
190
+ return self.data
191
+
192
+ def __getitem__(self, key):
193
+ return self.answers[key]
194
+
195
+ # ---- convenience readers for the real answer shapes ----
196
+ def noul(self, key: str, threshold: float = 0.5) -> bool:
197
+ """True/False reading of a noul answer (backend returns a 0-1 probability)."""
198
+ return self.answers[key]["noul"] >= threshold
199
+
200
+ def choice(self, key: str) -> str:
201
+ return self.answers[key]["choice"]
202
+
203
+ def score_label(self, key: str) -> str:
204
+ a = self.answers[key]
205
+ return a["legend"][str(round(a["score"]))]
206
+
207
+
208
+ class JEVzilla:
209
+ """
210
+ >>> jev = JEVzilla() # playground endpoint
211
+ >>> jev.evaluate({
212
+ ... "state": "The customer says their production service is down.",
213
+ ... "questions": {"urgent": {"type": "noul", "instructions": "Is this request urgent?"}},
214
+ ... })
215
+
216
+ >>> jev = JEVzilla(backend="typesafe") # official API, reads TYPESAFE_API_KEY
217
+ """
218
+
219
+ def __init__(self, backend: str = "playground", api_key: Optional[str] = None,
220
+ model: str = "jev-latest", url: Optional[str] = None, timeout: float = 30.0,
221
+ retries: int = 4, backoff: float = 1.0, max_backoff: float = 30.0,
222
+ session: Optional[requests.Session] = None,
223
+ extra_headers: Optional[Dict[str, str]] = None):
224
+ if backend not in ("playground", "typesafe", "openrouter"):
225
+ raise ValueError("backend must be 'playground', 'typesafe' or 'openrouter'")
226
+ self.backend, self.model, self.timeout = backend, model, timeout
227
+ self.retries, self.backoff, self.max_backoff = max(0, retries), backoff, max_backoff
228
+ self.session = session or requests.Session()
229
+ if backend == "playground":
230
+ self.url = url or PLAYGROUND_URL
231
+ self.headers = {"Content-Type": "application/json", "Origin": _ORIGIN, "Referer": _ORIGIN + "/"}
232
+ elif backend == "typesafe":
233
+ key = api_key or os.environ.get("TYPESAFE_API_KEY")
234
+ if not key:
235
+ raise JEVError("backend='typesafe' needs api_key= or the TYPESAFE_API_KEY env var")
236
+ self.url = url or TYPESAFE_URL
237
+ self.headers = {"Content-Type": "application/json", "Authorization": f"Bearer {key}"}
238
+ else: # openrouter
239
+ key = api_key or os.environ.get("OPENROUTER_API_KEY")
240
+ if not key:
241
+ raise JEVError("backend='openrouter' needs api_key= or the OPENROUTER_API_KEY env var")
242
+ self.url = url or OPENROUTER_URL
243
+ self.model = model if model != "jev-latest" else "typesafe/jev-1.13"
244
+ self.headers = {"Content-Type": "application/json", "Authorization": f"Bearer {key}"}
245
+ self.headers.update(extra_headers or {})
246
+
247
+ def build(self, payload: Dict[str, Any]) -> Dict[str, Any]:
248
+ """The exact body that would be sent, without sending it."""
249
+ if self.backend == "playground":
250
+ return to_playground_payload(payload)
251
+ return to_typesafe_payload(payload, self.model)
252
+
253
+ def _sleep_time(self, attempt: int, resp: Optional[requests.Response]) -> float:
254
+ if resp is not None:
255
+ ra = resp.headers.get("Retry-After")
256
+ if ra and ra.replace(".", "", 1).isdigit():
257
+ return min(float(ra), self.max_backoff)
258
+ return min(self.backoff * (2 ** attempt), self.max_backoff) * random.uniform(0.75, 1.25)
259
+
260
+ def evaluate(self, payload: Dict[str, Any], raise_for_status: bool = True) -> JEVResponse:
261
+ """POST the translated payload. Retries transient failures (429/502/503/504/529,
262
+ timeouts, connection errors) with exponential backoff; 4xx like 401/422 are not retried."""
263
+ body = self.build(payload)
264
+ r: Optional[requests.Response] = None
265
+ for attempt in range(self.retries + 1):
266
+ try:
267
+ r = self.session.post(self.url, json=body, headers=self.headers, timeout=self.timeout)
268
+ except (requests.ConnectionError, requests.Timeout):
269
+ if attempt == self.retries:
270
+ raise
271
+ time.sleep(self._sleep_time(attempt, None))
272
+ continue
273
+ if r.status_code in _RETRY_STATUSES and attempt < self.retries:
274
+ time.sleep(self._sleep_time(attempt, r))
275
+ continue
276
+ break
277
+ try:
278
+ data = r.json()
279
+ except ValueError:
280
+ data = None
281
+ res = JEVResponse(r.status_code, r.text, data, body)
282
+ if raise_for_status and not res.ok:
283
+ raise JEVHTTPError(r.status_code, r.text)
284
+ return res
285
+
286
+ # single-question shortcuts (answer key defaults to "decision")
287
+ def ask_noul(self, state, instructions, criteria=None, key="decision", **kw) -> JEVResponse:
288
+ return self.evaluate({"state": state, "questions": {key: noul(instructions, criteria)}}, **kw)
289
+
290
+ def ask_choice(self, state, instructions, criteria, key="decision", **kw) -> JEVResponse:
291
+ return self.evaluate({"state": state, "questions": {key: choice(instructions, criteria)}}, **kw)
292
+
293
+ def ask_score(self, state, instructions, levels, key="decision", **kw) -> JEVResponse:
294
+ return self.evaluate({"state": state, "questions": {key: score(instructions, levels)}}, **kw)
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: jevzilla
3
+ Version: 0.1.0
4
+ Summary: A tiny, fearless client for the JEV evaluation API
5
+ Requires-Python: >=3.8
6
+ License-File: LICENSE
7
+ Requires-Dist: requests>=2.28
8
+ Dynamic: license-file
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ jevzilla/__init__.py
5
+ jevzilla.egg-info/PKG-INFO
6
+ jevzilla.egg-info/SOURCES.txt
7
+ jevzilla.egg-info/dependency_links.txt
8
+ jevzilla.egg-info/requires.txt
9
+ jevzilla.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.28
@@ -0,0 +1 @@
1
+ jevzilla
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jevzilla"
7
+ version = "0.1.0"
8
+ description = "A tiny, fearless client for the JEV evaluation API"
9
+ requires-python = ">=3.8"
10
+ dependencies = ["requests>=2.28"]
11
+
12
+ [tool.setuptools]
13
+ packages = ["jevzilla"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+