Repository navigation
Expand file tree
/
Copy pathjev
More file actions
executable file
·159 lines (127 loc) · 5.61 KB
/
Copy pathjev
File metadata and controls
executable file
·159 lines (127 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python3
"""
jev — CLI for the local gemma-jev decision server (Gemma 4 E4B).
Designed for AI agents (Claude Code, Hermes, custom harnesses): zero
dependencies beyond Python 3.9+ stdlib, machine-friendly output, one
decision per invocation.
Server endpoints (env JEV_URL, default http://127.0.0.1:8300):
/health liveness /ready one real read
/v1/request typed decisions
Usage:
jev ask "state text" "question?" # yes/no -> yes|no + p
jev ask "state" "which team?" a=billing b=engineering # choice -> label + p
jev noul "state" "is it urgent?" # explicit noul
jev choice "state" "pick one" a="desc a" b="desc b" # explicit choice
jev score "state" "quality?" 1=low 2=mid 3=high # ordered levels
jev health # server status
jev serve # start server ( foreground)
Output (stdout): the chosen value only — ideal for shell substitution:
TEAM=$(jev ask "$ticket" "which team?" a=billing b=eng)
Flags:
--json full answer object (value, probability, distribution, ms)
--min-p FLOAT exit 2 with "LOW_CONFIDENCE" if p < FLOAT (default: off)
--label-prefix for score: prefix output lines
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
JEV_URL = os.environ.get("JEV_URL", "http://127.0.0.1:8300")
SERVER_PY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "server.py")
def post(path: str, body: dict) -> dict:
req = urllib.request.Request(
f"{JEV_URL}{path}", json.dumps(body).encode(),
{"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
return json.load(r)
def get(path: str) -> dict:
with urllib.request.urlopen(f"{JEV_URL}{path}", timeout=10) as r:
return json.load(r)
def parse_kv(args):
"""['a=billing', 'b=engineering'] -> {'a': 'billing', 'b': 'engineering'}"""
out = {}
for a in args:
if "=" not in a:
sys.stderr.write(f"error: expected key=value, got {a!r}\n")
sys.exit(2)
k, v = a.split("=", 1)
out[k] = v
return out
def main():
ap = argparse.ArgumentParser(prog="jev", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
p_ask = sub.add_parser("ask", help="auto: noul if no options, choice if given")
p_ask.add_argument("state")
p_ask.add_argument("question")
p_ask.add_argument("options", nargs="*", help="label=description pairs")
p_noul = sub.add_parser("noul", help="yes/no question")
p_noul.add_argument("state")
p_noul.add_argument("question")
p_choice = sub.add_parser("choice", help="pick one of N labeled options")
p_choice.add_argument("state")
p_choice.add_argument("question")
p_choice.add_argument("options", nargs="+", help="label=description pairs")
p_score = sub.add_parser("score", help="ordered rubric levels")
p_score.add_argument("state")
p_score.add_argument("question")
p_score.add_argument("levels", nargs="+", help="level=description pairs, ordered")
sub.add_parser("health", help="server health")
p_serve = sub.add_parser("serve", help="start the decision server (foreground)")
p_serve.add_argument("--port", type=int, default=None)
for p in (p_ask, p_noul, p_choice, p_score):
p.add_argument("--json", action="store_true", help="print full answer object")
p.add_argument("--min-p", type=float, default=None, help="exit 2 if p below this")
a = ap.parse_args()
if a.cmd == "health":
try:
print(json.dumps(get("/health")))
except Exception as e:
print(f"DOWN: {e}", file=sys.stderr)
sys.exit(1)
return
if a.cmd == "serve":
os.execv(sys.executable, [sys.executable, SERVER_PY])
# build the request
state, question = a.state, a.question
if a.cmd == "noul":
qtype, criteria = "noul", None
elif a.cmd == "choice":
qtype, criteria = "choice", parse_kv(a.options)
elif a.cmd == "score":
qtype, criteria = "score", parse_kv(a.levels)
else: # ask
if a.options:
qtype, criteria = "choice", parse_kv(a.options)
else:
qtype, criteria = "noul", None
# Agent ergonomics: when the caller passes generic letter keys
# (a=..., b=..., c=...), remap the criteria to the descriptions themselves
# so the answer comes back as the meaningful label, not "a".
remap = None
if criteria:
keys = list(criteria.keys())
generic = all(k in ("a", "b", "c", "d", "e", "f", "g", "h") for k in keys) and len(set(criteria.values())) == len(keys)
if generic and len(keys) >= 2:
remap = {k: v for k, v in zip(keys, keys)} # marker -> original key
criteria = {v: v for v in criteria.values()}
q = {"type": qtype, "instructions": question}
if criteria:
q["criteria"] = criteria
try:
resp = post("/v1/request", {"state": state, "questions": {"q": q}})
except urllib.error.URLError as e:
print(f"server unreachable at {JEV_URL} ({e}) — run `jev serve` or setup.sh",
file=sys.stderr)
sys.exit(1)
ans = resp["answers"]["q"]
if a.json:
print(json.dumps(ans))
else:
print(ans["value"])
if a.min_p is not None and ans["probability"] < a.min_p:
print(f"LOW_CONFIDENCE p={ans['probability']} < {a.min_p}", file=sys.stderr)
sys.exit(2)
if __name__ == "__main__":
main()