snodo-validator-tool-jev 0.23.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snodo_validator_tool_jev/__init__.py +173 -0
- snodo_validator_tool_jev-0.23.3.dist-info/METADATA +28 -0
- snodo_validator_tool_jev-0.23.3.dist-info/RECORD +6 -0
- snodo_validator_tool_jev-0.23.3.dist-info/WHEEL +5 -0
- snodo_validator_tool_jev-0.23.3.dist-info/entry_points.txt +2 -0
- snodo_validator_tool_jev-0.23.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""TypeSafe Jev decision tool for Snodo LLM validators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from snodo.validators.tool_registry import ValidatorTool, ValidatorToolContext
|
|
12
|
+
|
|
13
|
+
_MAX_FILES = 5
|
|
14
|
+
|
|
15
|
+
def handle_decide(arguments: Mapping[str, Any], context: ValidatorToolContext) -> str:
|
|
16
|
+
"""Send validated questions and selected context to Jev; return safe text."""
|
|
17
|
+
key_name = str(context.tool_options.get("api_key_env", "TYPESAFE_API_KEY"))
|
|
18
|
+
api_key = os.environ.get(key_name)
|
|
19
|
+
if not api_key:
|
|
20
|
+
return f"TypeSafe Jev unavailable: environment variable {key_name} is not set."
|
|
21
|
+
try:
|
|
22
|
+
questions = _normalize_questions(arguments.get("questions"))
|
|
23
|
+
state = _get_context(arguments, context)
|
|
24
|
+
max_chars = max(1, int(context.tool_options.get("max_input_chars", 50000)))
|
|
25
|
+
truncated = len(state) > max_chars
|
|
26
|
+
state = state[:max_chars]
|
|
27
|
+
timeout = float(context.tool_options.get("timeout_seconds", 30))
|
|
28
|
+
base_url = str(context.tool_options.get("base_url", "https://api.typesafe.ai")).rstrip("/")
|
|
29
|
+
model = str(context.tool_options.get("model", "jev-latest"))
|
|
30
|
+
response = httpx.post(
|
|
31
|
+
f"{base_url}/v1/systemone",
|
|
32
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
33
|
+
json={"model": model, "state": state, "questions": questions},
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
payload = response.json()
|
|
38
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("answers"), dict):
|
|
39
|
+
return "TypeSafe Jev error: malformed response; expected an answers object."
|
|
40
|
+
return _render(payload, questions, truncated)
|
|
41
|
+
except httpx.TimeoutException:
|
|
42
|
+
return "TypeSafe Jev error: request timed out. Check the endpoint or increase timeout_seconds."
|
|
43
|
+
except httpx.HTTPStatusError as exc:
|
|
44
|
+
return f"TypeSafe Jev error: HTTP {exc.response.status_code} from the decision API."
|
|
45
|
+
except httpx.HTTPError:
|
|
46
|
+
return "TypeSafe Jev error: could not complete the HTTP request. Check the endpoint and network."
|
|
47
|
+
except (ValueError, TypeError, OSError, json.JSONDecodeError) as exc:
|
|
48
|
+
# Do not include exception text: paths or HTTP internals must not leak secrets.
|
|
49
|
+
return f"TypeSafe Jev error: {type(exc).__name__}; check the tool arguments and options."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalize_questions(raw: Any) -> dict[str, dict[str, Any]]:
|
|
53
|
+
if not isinstance(raw, list) or not raw:
|
|
54
|
+
raise ValueError("questions must be a non-empty list")
|
|
55
|
+
normalized: dict[str, dict[str, Any]] = {}
|
|
56
|
+
for question in raw:
|
|
57
|
+
if not isinstance(question, Mapping):
|
|
58
|
+
raise ValueError("each question must be an object")
|
|
59
|
+
name, kind, instructions = question.get("name"), question.get("type"), question.get("instructions")
|
|
60
|
+
if not all(isinstance(value, str) and value.strip() for value in (name, kind, instructions)):
|
|
61
|
+
raise ValueError("question name, type, and instructions are required")
|
|
62
|
+
if name in normalized or kind not in {"noul", "choice", "score"}:
|
|
63
|
+
raise ValueError("question names must be unique and types must be noul, choice, or score")
|
|
64
|
+
item: dict[str, Any] = {"type": kind, "instructions": instructions}
|
|
65
|
+
criteria = question.get("criteria")
|
|
66
|
+
if kind == "choice":
|
|
67
|
+
if not isinstance(criteria, Mapping) or not criteria:
|
|
68
|
+
raise ValueError("choice questions require criteria as an option-description map")
|
|
69
|
+
item["criteria"] = dict(criteria)
|
|
70
|
+
elif kind == "score":
|
|
71
|
+
if not isinstance(criteria, list) or not criteria:
|
|
72
|
+
raise ValueError("score questions require criteria as an ordered list of levels")
|
|
73
|
+
item["criteria"] = criteria
|
|
74
|
+
elif criteria is not None:
|
|
75
|
+
item["criteria"] = criteria
|
|
76
|
+
normalized[name] = item
|
|
77
|
+
return normalized
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_context(arguments: Mapping[str, Any], context: ValidatorToolContext) -> str:
|
|
81
|
+
selected = arguments.get("context")
|
|
82
|
+
if selected == "spec":
|
|
83
|
+
return context.spec_text
|
|
84
|
+
if selected == "diff":
|
|
85
|
+
if context.diff_accessor is None:
|
|
86
|
+
raise ValueError("task diff is unavailable")
|
|
87
|
+
if context.diff_refs:
|
|
88
|
+
return str(context.diff_accessor(*context.diff_refs))
|
|
89
|
+
return str(context.diff_accessor())
|
|
90
|
+
if selected != "files":
|
|
91
|
+
raise ValueError("context must be diff, spec, or files")
|
|
92
|
+
paths = arguments.get("files")
|
|
93
|
+
if not isinstance(paths, list) or not 1 <= len(paths) <= _MAX_FILES:
|
|
94
|
+
raise ValueError(f"files context requires between 1 and {_MAX_FILES} paths")
|
|
95
|
+
root = context.workspace_root.resolve()
|
|
96
|
+
contents = []
|
|
97
|
+
for value in paths:
|
|
98
|
+
if not isinstance(value, str) or not value:
|
|
99
|
+
raise ValueError("file paths must be non-empty strings")
|
|
100
|
+
target = (root / value).resolve()
|
|
101
|
+
if not target.is_relative_to(root) or not target.is_file():
|
|
102
|
+
raise ValueError(f"file path is outside the workspace or unreadable: {value}")
|
|
103
|
+
contents.append(f"--- {value} ---\n{target.read_text(encoding='utf-8')}")
|
|
104
|
+
return "\n".join(contents)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _render(payload: Mapping[str, Any], questions: Mapping[str, Any], truncated: bool) -> str:
|
|
108
|
+
answers = payload["answers"]
|
|
109
|
+
lines = []
|
|
110
|
+
for name, question in questions.items():
|
|
111
|
+
answer = answers.get(name)
|
|
112
|
+
if not isinstance(answer, Mapping):
|
|
113
|
+
return "TypeSafe Jev error: malformed response; an answer is missing or invalid."
|
|
114
|
+
kind = question["type"]
|
|
115
|
+
if kind == "noul":
|
|
116
|
+
lines.append(f"{name} [noul]: {answer.get('noul')} (probability)")
|
|
117
|
+
elif kind == "choice":
|
|
118
|
+
lines.append(
|
|
119
|
+
f"{name} [choice]: {answer.get('choice')} (probabilities={_compact(answer.get('probabilities'))}; "
|
|
120
|
+
f"confidence={answer.get('confidence')})"
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
lines.append(
|
|
124
|
+
f"{name} [score]: {answer.get('score')} (legend={_compact(answer.get('legend'))}; "
|
|
125
|
+
f"probabilities={_compact(answer.get('probabilities'))}; confidence={answer.get('confidence')})"
|
|
126
|
+
)
|
|
127
|
+
usage = payload.get("usage", {})
|
|
128
|
+
if not isinstance(usage, Mapping):
|
|
129
|
+
usage = {}
|
|
130
|
+
lines.append(f"usage: input_tokens={usage.get('input_tokens', usage.get('input', 'unknown'))}, "
|
|
131
|
+
f"output_tokens={usage.get('output_tokens', usage.get('output', 'unknown'))}")
|
|
132
|
+
if truncated:
|
|
133
|
+
lines.append("context truncated to max_input_chars")
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _compact(value: Any) -> str:
|
|
138
|
+
return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
tool = ValidatorTool(
|
|
142
|
+
definition={
|
|
143
|
+
"type": "function",
|
|
144
|
+
"function": {
|
|
145
|
+
"name": "decide",
|
|
146
|
+
"description": "Ask TypeSafe Jev typed questions about the task context.",
|
|
147
|
+
"parameters": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"context": {"type": "string", "enum": ["diff", "spec", "files"]},
|
|
151
|
+
"files": {"type": "array", "items": {"type": "string"}, "maxItems": _MAX_FILES},
|
|
152
|
+
"questions": {
|
|
153
|
+
"type": "array",
|
|
154
|
+
"items": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"name": {"type": "string"},
|
|
158
|
+
"type": {"type": "string", "enum": ["noul", "choice", "score"]},
|
|
159
|
+
"instructions": {"type": "string"},
|
|
160
|
+
"criteria": {"type": ["object", "array", "null"]},
|
|
161
|
+
},
|
|
162
|
+
"required": ["name", "type", "instructions"],
|
|
163
|
+
"additionalProperties": False,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"required": ["context", "questions"],
|
|
168
|
+
"additionalProperties": False,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
handler=handle_decide,
|
|
173
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snodo-validator-tool-jev
|
|
3
|
+
Version: 0.23.3
|
|
4
|
+
Summary: TypeSafe Jev decision tool for Snodo validators
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: snodo-engine==0.23.3
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
|
|
10
|
+
# Snodo TypeSafe Jev validator tool
|
|
11
|
+
|
|
12
|
+
An optional plugin that adds the `decide` function to opted-in LLM validators.
|
|
13
|
+
Jev returns typed answers and calibrated probabilities; the LLM remains
|
|
14
|
+
responsible for writing the validator verdict.
|
|
15
|
+
|
|
16
|
+
Install it into the same Python environment as Snodo and configure
|
|
17
|
+
`validator_tools: [{name: decide}]` on an LLM validator. The tool accepts
|
|
18
|
+
`context` as `diff`, `spec`, or `files` (with up to a small number of repository
|
|
19
|
+
relative file paths), and a list of named `questions` using `noul`, `choice`, or
|
|
20
|
+
`score` types. Choice criteria map option names to descriptions; score criteria
|
|
21
|
+
are ordered level labels.
|
|
22
|
+
|
|
23
|
+
Per-tool protocol options are `model` (default `jev-latest`), `api_key_env`
|
|
24
|
+
(default `TYPESAFE_API_KEY`), `base_url` (default `https://api.typesafe.ai`),
|
|
25
|
+
`timeout_seconds`, and `max_input_chars`. The key is read only from the named
|
|
26
|
+
environment variable. Each result line reports the typed answer and
|
|
27
|
+
probabilities; usage and any context truncation follow. Jev pricing is $0.042
|
|
28
|
+
per million input tokens; its maximum context length is unpublished.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
snodo_validator_tool_jev/__init__.py,sha256=WKBmnZbHCX-W3_ChyDKO4BdbpA8ol4N_r7yCRqWr0l8,8123
|
|
2
|
+
snodo_validator_tool_jev-0.23.3.dist-info/METADATA,sha256=UzPwt_fRqReO_yupatqGu9FO4uc8EAJo7n2fl6uAioA,1349
|
|
3
|
+
snodo_validator_tool_jev-0.23.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
4
|
+
snodo_validator_tool_jev-0.23.3.dist-info/entry_points.txt,sha256=CNkD6gYvVQKBcSQsraApRYYpFncWEjL2PNSaYMvqE94,63
|
|
5
|
+
snodo_validator_tool_jev-0.23.3.dist-info/top_level.txt,sha256=kTPY7849BaCRdRMvwYrlGlmFmpiBEhJWEKj-8H5Z7vY,25
|
|
6
|
+
snodo_validator_tool_jev-0.23.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
snodo_validator_tool_jev
|