@pi-in-go/pigpen-pi-typesafe 0.1.0
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.
- package/CREDITS.md +14 -0
- package/LICENSE +22 -0
- package/README.md +45 -0
- package/extensions/pi-typesafe/branches_test.go +185 -0
- package/extensions/pi-typesafe/command.go +319 -0
- package/extensions/pi-typesafe/export_test.go +9 -0
- package/extensions/pi-typesafe/extension.go +188 -0
- package/extensions/pi-typesafe/extension_test.go +321 -0
- package/extensions/pi-typesafe/fakehost_test.go +548 -0
- package/extensions/pi-typesafe/format.go +191 -0
- package/extensions/pi-typesafe/format_test.go +75 -0
- package/extensions/pi-typesafe/go.mod +10 -0
- package/extensions/pi-typesafe/go.sum +2 -0
- package/extensions/pi-typesafe/go.work +11 -0
- package/extensions/pi-typesafe/harness_test.go +200 -0
- package/extensions/pi-typesafe/ownmodel_test.go +100 -0
- package/extensions/pi-typesafe/review_test.go +134 -0
- package/extensions/pi-typesafe/tool.go +193 -0
- package/extensions/pi-typesafe/twin_test.go +28 -0
- package/libs/pi-typesafe-api/CREDITS.md +14 -0
- package/libs/pi-typesafe-api/LICENSE +22 -0
- package/libs/pi-typesafe-api/README.md +30 -0
- package/libs/pi-typesafe-api/ask.go +62 -0
- package/libs/pi-typesafe-api/ask_test.go +76 -0
- package/libs/pi-typesafe-api/auth.go +249 -0
- package/libs/pi-typesafe-api/auth_test.go +131 -0
- package/libs/pi-typesafe-api/backends.go +336 -0
- package/libs/pi-typesafe-api/backends_test.go +404 -0
- package/libs/pi-typesafe-api/batch.go +202 -0
- package/libs/pi-typesafe-api/batch_test.go +202 -0
- package/libs/pi-typesafe-api/battery_test.go +41 -0
- package/libs/pi-typesafe-api/calibrate.go +354 -0
- package/libs/pi-typesafe-api/calibrate_test.go +186 -0
- package/libs/pi-typesafe-api/client.go +615 -0
- package/libs/pi-typesafe-api/client_test.go +490 -0
- package/libs/pi-typesafe-api/credentials.go +252 -0
- package/libs/pi-typesafe-api/credentials_test.go +216 -0
- package/libs/pi-typesafe-api/doc.go +14 -0
- package/libs/pi-typesafe-api/errors.go +143 -0
- package/libs/pi-typesafe-api/evaluation.go +86 -0
- package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
- package/libs/pi-typesafe-api/gaps_test.go +77 -0
- package/libs/pi-typesafe-api/go.mod +9 -0
- package/libs/pi-typesafe-api/go.sum +2 -0
- package/libs/pi-typesafe-api/helpers_test.go +169 -0
- package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
- package/libs/pi-typesafe-api/json.go +299 -0
- package/libs/pi-typesafe-api/json_test.go +92 -0
- package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
- package/libs/pi-typesafe-api/package.json +40 -0
- package/libs/pi-typesafe-api/provenance.json +18 -0
- package/libs/pi-typesafe-api/review_test.go +23 -0
- package/libs/pi-typesafe-api/schema.go +473 -0
- package/libs/pi-typesafe-api/schema_test.go +262 -0
- package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
- package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
- package/libs/pi-typesafe-api/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
- package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
- package/libs/pi-typesafe-api/ui/login.go +106 -0
- package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
- package/libs/pi-typesafe-api/usage.go +366 -0
- package/libs/pi-typesafe-api/usage_test.go +139 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +98 -0
- package/port/accepted-gaps.json +3 -0
- package/port/golden/enable-confirm.jsonl +11 -0
- package/port/golden/enable-decline.jsonl +20 -0
- package/port/golden/enable-missing-key.jsonl +4 -0
- package/port/golden/login-shadow.jsonl +4 -0
- package/port/golden/logout-env-key.jsonl +6 -0
- package/port/golden/playground-cancel.jsonl +4 -0
- package/port/golden/playground-invalid-json.jsonl +5 -0
- package/port/golden/playground-invalid-questions.jsonl +5 -0
- package/port/golden/status-env-key.jsonl +6 -0
- package/port/golden/status-no-key.jsonl +6 -0
- package/port/golden/tool-disabled.jsonl +18 -0
- package/port/golden/trailing-words.jsonl +10 -0
- package/port/library-mutations.py +44 -0
- package/port/mutations.json +302 -0
- package/port/oracle/.env.example +4 -0
- package/port/oracle/CHANGELOG.md +91 -0
- package/port/oracle/CONTRIBUTING.md +35 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +159 -0
- package/port/oracle/docs/api.md +143 -0
- package/port/oracle/docs/ci-cd.md +97 -0
- package/port/oracle/examples/decision-extension.ts +41 -0
- package/port/oracle/extensions/index.js +2 -0
- package/port/oracle/package.json +89 -0
- package/port/oracle/scripts/dev-pi.mjs +23 -0
- package/port/oracle/scripts/live-smoke.mjs +35 -0
- package/port/oracle/src/ask.ts +42 -0
- package/port/oracle/src/auth.ts +171 -0
- package/port/oracle/src/backends.ts +196 -0
- package/port/oracle/src/batch.ts +170 -0
- package/port/oracle/src/calibrate.ts +237 -0
- package/port/oracle/src/client.ts +310 -0
- package/port/oracle/src/credentials.ts +136 -0
- package/port/oracle/src/errors.ts +53 -0
- package/port/oracle/src/extension.ts +204 -0
- package/port/oracle/src/index.ts +31 -0
- package/port/oracle/src/key-prompt.ts +51 -0
- package/port/oracle/src/login.ts +60 -0
- package/port/oracle/src/schema.ts +158 -0
- package/port/oracle/src/ui.ts +4 -0
- package/port/oracle/src/usage.ts +258 -0
- package/port/oracle/tests/ask.test.ts +63 -0
- package/port/oracle/tests/auth.test.ts +141 -0
- package/port/oracle/tests/backends.test.ts +380 -0
- package/port/oracle/tests/batch.test.ts +156 -0
- package/port/oracle/tests/calibrate.test.ts +144 -0
- package/port/oracle/tests/client.test.ts +499 -0
- package/port/oracle/tests/credentials.test.ts +144 -0
- package/port/oracle/tests/extension.test.ts +276 -0
- package/port/oracle/tests/key-prompt.test.ts +47 -0
- package/port/oracle/tests/login.test.ts +101 -0
- package/port/oracle/tests/schema.test.ts +85 -0
- package/port/oracle/tests/usage.test.ts +106 -0
- package/port/oracle/tsconfig.build.json +10 -0
- package/port/oracle/tsconfig.json +14 -0
- package/port/scenarios/enable-confirm.json +5 -0
- package/port/scenarios/enable-decline.json +3 -0
- package/port/scenarios/enable-missing-key.json +2 -0
- package/port/scenarios/login-shadow.json +2 -0
- package/port/scenarios/logout-env-key.json +3 -0
- package/port/scenarios/playground-cancel.json +2 -0
- package/port/scenarios/playground-invalid-json.json +2 -0
- package/port/scenarios/playground-invalid-questions.json +2 -0
- package/port/scenarios/status-env-key.json +3 -0
- package/port/scenarios/status-no-key.json +3 -0
- package/port/scenarios/tool-disabled.json +2 -0
- package/port/scenarios/trailing-words.json +5 -0
- package/port/upstream-tests.json +160 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
_ "embed"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// DefaultMaxInputBytes is the default UTF-8 JSON byte budget for one evaluation request; the tool and the client share it.
|
|
13
|
+
const DefaultMaxInputBytes = 65_536
|
|
14
|
+
|
|
15
|
+
// DefaultMaxQuestions is the number of questions one request may ask. More than this needs ChunkRequest, which splits and fans out.
|
|
16
|
+
const DefaultMaxQuestions = 32
|
|
17
|
+
|
|
18
|
+
// Request is an admitted evaluation request: state, questions in order, and an optional model id.
|
|
19
|
+
type Request struct {
|
|
20
|
+
State any
|
|
21
|
+
Questions *Object
|
|
22
|
+
// Model is empty when the request names none.
|
|
23
|
+
Model string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// PrepareOptions configure admission.
|
|
27
|
+
type PrepareOptions struct {
|
|
28
|
+
// MaxInputBytes is the UTF-8 JSON bytes of the serialized request. Default: DefaultMaxInputBytes.
|
|
29
|
+
MaxInputBytes int
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var usageText = fmt.Sprintf(`Expected { state, questions: { <id>: { type: "choice", instructions, criteria: { label: description|null } } | { type: "score", instructions, criteria: [level0, level1, ...] } | { type: "noul", instructions } } }; 1–%d questions, Choice 1–64 options, Score 2–32 levels.`, DefaultMaxQuestions)
|
|
33
|
+
|
|
34
|
+
const jsonSafetyMessage = "state and questions must be plain JSON."
|
|
35
|
+
|
|
36
|
+
// tree returns the request as an *Object tree with the model that was asked for.
|
|
37
|
+
func (r *Request) tree() *Object {
|
|
38
|
+
o := NewObject()
|
|
39
|
+
o.Set("state", r.State)
|
|
40
|
+
o.Set("questions", r.Questions)
|
|
41
|
+
if r.Model != "" {
|
|
42
|
+
o.Set("model", r.Model)
|
|
43
|
+
}
|
|
44
|
+
return o
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// MarshalJSON serializes state, questions and model in that order, questions in their order.
|
|
48
|
+
func (r *Request) MarshalJSON() ([]byte, error) { return EncodeJSON(r.tree()) }
|
|
49
|
+
|
|
50
|
+
// WithModel returns a copy carrying model.
|
|
51
|
+
func (r *Request) WithModel(model string) *Request {
|
|
52
|
+
c := *r
|
|
53
|
+
c.Model = model
|
|
54
|
+
return &c
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Typed converts the admitted request to the shared client's request type, keeping order.
|
|
58
|
+
func (r *Request) Typed() (typesafe.SystemOneRequest, error) {
|
|
59
|
+
var state typesafe.Entry
|
|
60
|
+
switch s := r.State.(type) {
|
|
61
|
+
case nil:
|
|
62
|
+
state = typesafe.Null
|
|
63
|
+
case string:
|
|
64
|
+
state = typesafe.Text(s)
|
|
65
|
+
default:
|
|
66
|
+
raw, err := EncodeJSON(s)
|
|
67
|
+
if err != nil {
|
|
68
|
+
return typesafe.SystemOneRequest{}, err
|
|
69
|
+
}
|
|
70
|
+
state = typesafe.Value(json.RawMessage(raw))
|
|
71
|
+
}
|
|
72
|
+
rawQuestions, err := EncodeJSON(r.Questions)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return typesafe.SystemOneRequest{}, err
|
|
75
|
+
}
|
|
76
|
+
questions, err := typesafe.ParseQuestions(rawQuestions)
|
|
77
|
+
if err != nil {
|
|
78
|
+
return typesafe.SystemOneRequest{}, err
|
|
79
|
+
}
|
|
80
|
+
return typesafe.SystemOneRequest{State: state, Questions: questions, Model: r.Model}, nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// depth limit of the JSON-safety walk.
|
|
84
|
+
const maxDepth = 64
|
|
85
|
+
|
|
86
|
+
func jsonSafe(v any, depth int) bool {
|
|
87
|
+
if depth > maxDepth {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
switch t := v.(type) {
|
|
91
|
+
case nil, bool, string, float64:
|
|
92
|
+
return true
|
|
93
|
+
case *Object:
|
|
94
|
+
for _, k := range t.keys {
|
|
95
|
+
if !jsonSafe(t.vals[k], depth+1) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true
|
|
100
|
+
case []any:
|
|
101
|
+
for _, e := range t {
|
|
102
|
+
if !jsonSafe(e, depth+1) {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// asTree converts input to a tree, or reports that it is not plain JSON.
|
|
112
|
+
func asTree(value any) (any, error) {
|
|
113
|
+
tree, err := FromGo(value)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return nil, errorf(CodeValidation, "Invalid evaluation request: %s %s", jsonSafetyMessage, usageText)
|
|
116
|
+
}
|
|
117
|
+
if obj, ok := tree.(*Object); ok && !jsonSafe(obj, 0) {
|
|
118
|
+
return nil, errorf(CodeValidation, "Invalid evaluation request: %s %s", jsonSafetyMessage, usageText)
|
|
119
|
+
}
|
|
120
|
+
return tree, nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func isEntry(v any) bool {
|
|
124
|
+
switch v.(type) {
|
|
125
|
+
case nil, string, []any, *Object:
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type issues struct{ list []string }
|
|
132
|
+
|
|
133
|
+
func (i *issues) add(path, message string) {
|
|
134
|
+
if len(i.list) >= 3 {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if path == "" {
|
|
138
|
+
path = "request"
|
|
139
|
+
}
|
|
140
|
+
if len(path) > 120 {
|
|
141
|
+
path = path[:120]
|
|
142
|
+
}
|
|
143
|
+
i.list = append(i.list, path+": "+message)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func (i *issues) full() bool { return len(i.list) >= 3 }
|
|
147
|
+
|
|
148
|
+
func join(path, key string) string {
|
|
149
|
+
if path == "" {
|
|
150
|
+
return key
|
|
151
|
+
}
|
|
152
|
+
return path + "." + key
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func validateEntry(is *issues, path string, v any) {
|
|
156
|
+
if !isEntry(v) {
|
|
157
|
+
// TypeBox lists the first three branches of the union it failed.
|
|
158
|
+
is.add(path, "must be string")
|
|
159
|
+
is.add(path, "must be null")
|
|
160
|
+
is.add(path, "must be array")
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func validateQuestion(is *issues, path string, v any) {
|
|
165
|
+
q, ok := v.(*Object)
|
|
166
|
+
if !ok {
|
|
167
|
+
// TypeBox reports each of the three branches.
|
|
168
|
+
is.add(path, "must be object")
|
|
169
|
+
is.add(path, "must be object")
|
|
170
|
+
is.add(path, "must be object")
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
typeValue, _ := q.Get("type")
|
|
174
|
+
kind, _ := typeValue.(string)
|
|
175
|
+
var allowed map[string]bool
|
|
176
|
+
switch kind {
|
|
177
|
+
case "noul":
|
|
178
|
+
allowed = map[string]bool{"type": true, "instructions": true, "criteria": true}
|
|
179
|
+
case "choice", "score":
|
|
180
|
+
allowed = map[string]bool{"type": true, "instructions": true, "criteria": true}
|
|
181
|
+
default:
|
|
182
|
+
is.add(join(path, "type"), "must be one of noul, choice, score")
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
for _, k := range q.keys {
|
|
186
|
+
if !allowed[k] {
|
|
187
|
+
is.add(join(path, k), "must not have additional properties")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if ins, ok := q.Get("instructions"); ok {
|
|
191
|
+
validateEntry(is, join(path, "instructions"), ins)
|
|
192
|
+
}
|
|
193
|
+
criteria, hasCriteria := q.Get("criteria")
|
|
194
|
+
cpath := join(path, "criteria")
|
|
195
|
+
switch kind {
|
|
196
|
+
case "noul":
|
|
197
|
+
if !hasCriteria || criteria == nil {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
obj, ok := criteria.(*Object)
|
|
201
|
+
if !ok {
|
|
202
|
+
is.add(cpath, "must be null or object")
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
for _, k := range obj.keys {
|
|
206
|
+
if k != "true" && k != "false" {
|
|
207
|
+
is.add(join(cpath, k), "must not have additional properties")
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
validateEntry(is, join(cpath, k), obj.vals[k])
|
|
211
|
+
}
|
|
212
|
+
case "choice":
|
|
213
|
+
if !hasCriteria {
|
|
214
|
+
is.add(path, "must have required property 'criteria'")
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
obj, ok := criteria.(*Object)
|
|
218
|
+
if !ok {
|
|
219
|
+
is.add(cpath, "must be object")
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if obj.Len() < 1 || obj.Len() > 64 {
|
|
223
|
+
is.add(cpath, countMessage(obj.Len(), 1, 64, "properties"))
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
for _, k := range obj.keys {
|
|
227
|
+
if n := utf16Len(k); n < 1 || n > 200 {
|
|
228
|
+
is.add(join(cpath, k), countMessage(n, 1, 200, "characters"))
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
validateEntry(is, join(cpath, k), obj.vals[k])
|
|
232
|
+
}
|
|
233
|
+
case "score":
|
|
234
|
+
if !hasCriteria {
|
|
235
|
+
is.add(path, "must have required property 'criteria'")
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
arr, ok := criteria.([]any)
|
|
239
|
+
if !ok {
|
|
240
|
+
is.add(cpath, "must be array")
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if len(arr) < 2 || len(arr) > 32 {
|
|
244
|
+
is.add(cpath, countMessage(len(arr), 2, 32, "items"))
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
for i, e := range arr {
|
|
248
|
+
validateEntry(is, fmt.Sprintf("%s.%d", cpath, i), e)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// validateRequest returns up to three "path: message" issues; paths and messages only, never submitted values.
|
|
254
|
+
func validateRequest(v any) []string {
|
|
255
|
+
is := &issues{}
|
|
256
|
+
req, ok := v.(*Object)
|
|
257
|
+
if !ok {
|
|
258
|
+
is.add("", "must be object")
|
|
259
|
+
return is.list
|
|
260
|
+
}
|
|
261
|
+
extra := false
|
|
262
|
+
for _, k := range req.keys {
|
|
263
|
+
if k != "state" && k != "questions" && k != "model" {
|
|
264
|
+
is.add(k, "schema is false")
|
|
265
|
+
extra = true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if extra {
|
|
269
|
+
is.add("", "must not have additional properties")
|
|
270
|
+
}
|
|
271
|
+
state, hasState := req.Get("state")
|
|
272
|
+
qv, hasQuestions := req.Get("questions")
|
|
273
|
+
var missing []string
|
|
274
|
+
if !hasState {
|
|
275
|
+
missing = append(missing, "state")
|
|
276
|
+
}
|
|
277
|
+
if !hasQuestions {
|
|
278
|
+
missing = append(missing, "questions")
|
|
279
|
+
}
|
|
280
|
+
if len(missing) > 0 {
|
|
281
|
+
is.add("", "must have required properties "+strings.Join(missing, ", "))
|
|
282
|
+
}
|
|
283
|
+
if hasState {
|
|
284
|
+
validateEntry(is, "state", state)
|
|
285
|
+
}
|
|
286
|
+
if hasQuestions {
|
|
287
|
+
validateQuestions(is, qv)
|
|
288
|
+
}
|
|
289
|
+
if m, ok := req.Get("model"); ok {
|
|
290
|
+
s, isStr := m.(string)
|
|
291
|
+
if !isStr {
|
|
292
|
+
is.add("model", "must be string")
|
|
293
|
+
} else if n := utf16Len(s); n < 1 || n > 100 {
|
|
294
|
+
is.add("model", countMessage(n, 1, 100, "characters"))
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return is.list
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
func validateQuestions(is *issues, qv any) {
|
|
301
|
+
if qs, ok := qv.(*Object); !ok {
|
|
302
|
+
is.add("questions", "must be object")
|
|
303
|
+
} else {
|
|
304
|
+
if qs.Len() < 1 || qs.Len() > DefaultMaxQuestions {
|
|
305
|
+
is.add("questions", countMessage(qs.Len(), 1, DefaultMaxQuestions, "properties"))
|
|
306
|
+
}
|
|
307
|
+
for _, id := range qs.keys {
|
|
308
|
+
if n := utf16Len(id); n < 1 || n > 100 {
|
|
309
|
+
is.add("questions", countMessage(n, 1, 100, "characters"))
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
validateQuestion(is, join("questions", id), qs.vals[id])
|
|
313
|
+
if is.full() {
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ParseEvaluationRequest validates without including submitted content in validation errors. Prefer
|
|
321
|
+
// PrepareEvaluationRequest, which also accepts near-misses and enforces the byte budget.
|
|
322
|
+
func ParseEvaluationRequest(value any) (*Request, error) {
|
|
323
|
+
tree, err := asTree(value)
|
|
324
|
+
if err != nil {
|
|
325
|
+
return nil, err
|
|
326
|
+
}
|
|
327
|
+
if problems := validateRequest(tree); len(problems) > 0 {
|
|
328
|
+
return nil, errorf(CodeValidation, "Invalid evaluation request at %s. %s", strings.Join(problems, "; "), usageText)
|
|
329
|
+
}
|
|
330
|
+
req := tree.(*Object)
|
|
331
|
+
state, _ := req.Get("state")
|
|
332
|
+
questions, _ := req.Get("questions")
|
|
333
|
+
out := &Request{State: state, Questions: questions.(*Object)}
|
|
334
|
+
if m, ok := req.Get("model"); ok {
|
|
335
|
+
out.Model = m.(string)
|
|
336
|
+
}
|
|
337
|
+
return out, nil
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// NormalizeEvaluationRequest accepts common near-misses from language models without loosening the schema
|
|
341
|
+
// itself. Prefer PrepareEvaluationRequest, which applies this before validating. Input that is not a request
|
|
342
|
+
// object with a questions object is returned unchanged.
|
|
343
|
+
func NormalizeEvaluationRequest(value any) any {
|
|
344
|
+
tree, err := FromGo(value)
|
|
345
|
+
if err != nil {
|
|
346
|
+
return value
|
|
347
|
+
}
|
|
348
|
+
request, ok := tree.(*Object)
|
|
349
|
+
if !ok {
|
|
350
|
+
return tree
|
|
351
|
+
}
|
|
352
|
+
qv, _ := request.Get("questions")
|
|
353
|
+
questions, ok := qv.(*Object)
|
|
354
|
+
if !ok {
|
|
355
|
+
return tree
|
|
356
|
+
}
|
|
357
|
+
normalized := NewObject()
|
|
358
|
+
for _, id := range questions.keys {
|
|
359
|
+
question, ok := questions.vals[id].(*Object)
|
|
360
|
+
if !ok {
|
|
361
|
+
normalized.Set(id, questions.vals[id])
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
item := NewObject()
|
|
365
|
+
for _, k := range question.keys {
|
|
366
|
+
if k == "options" || k == "levels" || k == "choices" {
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
item.Set(k, question.vals[k])
|
|
370
|
+
}
|
|
371
|
+
if !item.Has("criteria") {
|
|
372
|
+
options, _ := question.Get("options")
|
|
373
|
+
levels, _ := question.Get("levels")
|
|
374
|
+
choices, hasChoices := question.Get("choices")
|
|
375
|
+
var alias any
|
|
376
|
+
present := false
|
|
377
|
+
switch {
|
|
378
|
+
case options != nil:
|
|
379
|
+
alias, present = options, true
|
|
380
|
+
case levels != nil:
|
|
381
|
+
alias, present = levels, true
|
|
382
|
+
case hasChoices:
|
|
383
|
+
alias, present = choices, true
|
|
384
|
+
}
|
|
385
|
+
if present {
|
|
386
|
+
item.Set("criteria", alias)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
kind, _ := item.vals["type"].(string)
|
|
390
|
+
criteria, _ := item.Get("criteria")
|
|
391
|
+
if arr, ok := criteria.([]any); ok && kind == "choice" {
|
|
392
|
+
labels := NewObject()
|
|
393
|
+
all := true
|
|
394
|
+
for _, l := range arr {
|
|
395
|
+
if s, ok := l.(string); !ok || s == "" {
|
|
396
|
+
all = false
|
|
397
|
+
break
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if all {
|
|
401
|
+
for _, l := range arr {
|
|
402
|
+
labels.Set(l.(string), nil)
|
|
403
|
+
}
|
|
404
|
+
item.Set("criteria", labels)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if s, ok := criteria.(string); ok && kind == "noul" {
|
|
408
|
+
c := NewObject()
|
|
409
|
+
c.Set("true", s)
|
|
410
|
+
item.Set("criteria", c)
|
|
411
|
+
}
|
|
412
|
+
normalized.Set(id, item)
|
|
413
|
+
}
|
|
414
|
+
out := request.Clone()
|
|
415
|
+
out.Set("questions", normalized)
|
|
416
|
+
return out
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// AssertWithinByteLimit measures the serialized request and names the configured limit: one byte rule for every limit check.
|
|
420
|
+
func AssertWithinByteLimit(text []byte, maxInputBytes int) error {
|
|
421
|
+
if len(text) > maxInputBytes {
|
|
422
|
+
return errorf(CodeValidation, "Evaluation exceeds the %d-byte input limit.", maxInputBytes)
|
|
423
|
+
}
|
|
424
|
+
return nil
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// PrepareEvaluationRequest is the one admission rule: normalize known near-miss aliases, validate the schema
|
|
428
|
+
// and JSON-safety, then enforce the byte budget, always in that order. The tool, the playground, and Evaluate
|
|
429
|
+
// all pass through here, so what one accepts the others accept.
|
|
430
|
+
func PrepareEvaluationRequest(value any, opts PrepareOptions) (*Request, error) {
|
|
431
|
+
tree, err := asTree(value)
|
|
432
|
+
if err != nil {
|
|
433
|
+
return nil, err
|
|
434
|
+
}
|
|
435
|
+
req, err := ParseEvaluationRequest(NormalizeEvaluationRequest(tree))
|
|
436
|
+
if err != nil {
|
|
437
|
+
return nil, err
|
|
438
|
+
}
|
|
439
|
+
limit := opts.MaxInputBytes
|
|
440
|
+
if limit == 0 {
|
|
441
|
+
limit = DefaultMaxInputBytes
|
|
442
|
+
}
|
|
443
|
+
body, err := req.MarshalJSON()
|
|
444
|
+
if err != nil {
|
|
445
|
+
return nil, errorf(CodeValidation, "Invalid evaluation request: %s", jsonSafetyMessage)
|
|
446
|
+
}
|
|
447
|
+
if err := AssertWithinByteLimit(body, limit); err != nil {
|
|
448
|
+
return nil, err
|
|
449
|
+
}
|
|
450
|
+
return req, nil
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//go:embed evaluation_schema.json
|
|
454
|
+
var evaluationSchemaJSON []byte
|
|
455
|
+
|
|
456
|
+
// EvaluationSchema is the JSON Schema used by the tool and the programmatic interface: exactly what the
|
|
457
|
+
// original's TypeBox schema serializes to (generated from port/oracle/src/schema.ts, see port/PORT.md). Every field the agent authors says what it means, because
|
|
458
|
+
// the schema is the only shape guidance the model gets before its first call. Each call returns a fresh copy.
|
|
459
|
+
func EvaluationSchema() map[string]any {
|
|
460
|
+
var schema map[string]any
|
|
461
|
+
if err := json.Unmarshal(evaluationSchemaJSON, &schema); err != nil {
|
|
462
|
+
panic("pitypesafe: embedded evaluation schema is invalid: " + err.Error())
|
|
463
|
+
}
|
|
464
|
+
return schema
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// countMessage is TypeBox's wording for a size outside its bounds.
|
|
468
|
+
func countMessage(n, minimum, maximum int, unit string) string {
|
|
469
|
+
if n < minimum {
|
|
470
|
+
return fmt.Sprintf("must not have fewer than %d %s", minimum, unit)
|
|
471
|
+
}
|
|
472
|
+
return fmt.Sprintf("must not have more than %d %s", maximum, unit)
|
|
473
|
+
}
|