@pi-in-go/pigpen-jev 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 +22 -0
- package/LICENSE +22 -0
- package/README.md +237 -0
- package/extensions/jev/ask.go +166 -0
- package/extensions/jev/ask_test.go +218 -0
- package/extensions/jev/backend.go +128 -0
- package/extensions/jev/bench_test.go +64 -0
- package/extensions/jev/boundaries_test.go +159 -0
- package/extensions/jev/command.go +224 -0
- package/extensions/jev/commands_test.go +214 -0
- package/extensions/jev/config.go +450 -0
- package/extensions/jev/errors_test.go +191 -0
- package/extensions/jev/extension.go +391 -0
- package/extensions/jev/fakehost_test.go +548 -0
- package/extensions/jev/gate.go +125 -0
- package/extensions/jev/gate_test.go +610 -0
- package/extensions/jev/gatekey_test.go +24 -0
- package/extensions/jev/go.mod +9 -0
- package/extensions/jev/go.sum +2 -0
- package/extensions/jev/go.work +10 -0
- package/extensions/jev/helpers_test.go +404 -0
- package/extensions/jev/memo.go +88 -0
- package/extensions/jev/output.go +89 -0
- package/extensions/jev/output_test.go +187 -0
- package/extensions/jev/ownmodel_test.go +118 -0
- package/extensions/jev/render.go +136 -0
- package/extensions/jev/review_test.go +310 -0
- package/extensions/jev/source_test.go +57 -0
- package/extensions/jev/text.go +174 -0
- package/extensions/jev/trust_test.go +335 -0
- package/extensions/jev/types.go +227 -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 +107 -0
- package/port/e2e/gate-and-output.py +35 -0
- package/port/e2e/jev-ask.py +36 -0
- package/port/e2e/model-switch.py +44 -0
- package/port/e2e/off-by-default.py +34 -0
- package/port/gen-scenarios.py +103 -0
- package/port/golden/cache-identical-calls.jsonl +30 -0
- package/port/golden/clear.jsonl +22 -0
- package/port/golden/commands.jsonl +43 -0
- package/port/golden/enforce-accept.jsonl +23 -0
- package/port/golden/enforce-decline.jsonl +22 -0
- package/port/golden/jev-ask.jsonl +20 -0
- package/port/golden/output-advice.jsonl +23 -0
- package/port/golden/output-leak.jsonl +24 -0
- package/port/golden/output-low-confidence.jsonl +22 -0
- package/port/golden/shadow-flagged.jsonl +23 -0
- package/port/golden/unjudged-tools.jsonl +19 -0
- package/port/golden/write-elision.jsonl +21 -0
- package/port/mutate-unit.py +63 -0
- package/port/mutations.json +578 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +181 -0
- package/port/oracle/SHA256SUMS +8 -0
- package/port/oracle/package.json +43 -0
- package/port/oracle/src/client.ts +409 -0
- package/port/oracle/src/config.ts +363 -0
- package/port/oracle/src/gate.ts +229 -0
- package/port/oracle/src/index.ts +649 -0
- package/port/oracle/src/output.ts +163 -0
- package/port/red-run.log +309 -0
- package/port/scenarios/cache-identical-calls.json +71 -0
- package/port/scenarios/clear.json +61 -0
- package/port/scenarios/commands.json +119 -0
- package/port/scenarios/enforce-accept.json +66 -0
- package/port/scenarios/enforce-decline.json +57 -0
- package/port/scenarios/jev-ask.json +83 -0
- package/port/scenarios/output-advice.json +61 -0
- package/port/scenarios/output-leak.json +61 -0
- package/port/scenarios/output-low-confidence.json +61 -0
- package/port/scenarios/shadow-flagged.json +61 -0
- package/port/scenarios/unjudged-tools.json +55 -0
- package/port/scenarios/write-elision.json +53 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
package ownmodel
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"sort"
|
|
9
|
+
"strconv"
|
|
10
|
+
"strings"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// The Python oracle serializes with pydantic_core.to_json: compact, no ASCII escaping, key
|
|
14
|
+
// order kept. These helpers reproduce it so that prompts match the oracle byte for byte.
|
|
15
|
+
|
|
16
|
+
// writeJSONString writes s like serde_json: only the quote, the backslash and the
|
|
17
|
+
// control characters are escaped.
|
|
18
|
+
func writeJSONString(b *strings.Builder, s string) {
|
|
19
|
+
b.WriteByte('"')
|
|
20
|
+
for _, r := range s {
|
|
21
|
+
switch r {
|
|
22
|
+
case '"':
|
|
23
|
+
b.WriteString(`\"`)
|
|
24
|
+
case '\\':
|
|
25
|
+
b.WriteString(`\\`)
|
|
26
|
+
case '\b':
|
|
27
|
+
b.WriteString(`\b`)
|
|
28
|
+
case '\f':
|
|
29
|
+
b.WriteString(`\f`)
|
|
30
|
+
case '\n':
|
|
31
|
+
b.WriteString(`\n`)
|
|
32
|
+
case '\r':
|
|
33
|
+
b.WriteString(`\r`)
|
|
34
|
+
case '\t':
|
|
35
|
+
b.WriteString(`\t`)
|
|
36
|
+
default:
|
|
37
|
+
if r < 0x20 {
|
|
38
|
+
fmt.Fprintf(b, `\u%04x`, r)
|
|
39
|
+
} else {
|
|
40
|
+
b.WriteRune(r)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
b.WriteByte('"')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// canonicalJSON re-encodes JSON text the way pydantic_core.to_json encodes the parsed
|
|
48
|
+
// value: compact, key order kept, strings escaped like serde_json. Number literals are
|
|
49
|
+
// kept as written.
|
|
50
|
+
func canonicalJSON(raw []byte) (string, error) {
|
|
51
|
+
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
52
|
+
dec.UseNumber()
|
|
53
|
+
var b strings.Builder
|
|
54
|
+
if err := canonValue(dec, &b); err != nil {
|
|
55
|
+
return "", err
|
|
56
|
+
}
|
|
57
|
+
if _, err := dec.Token(); err != io.EOF {
|
|
58
|
+
return "", fmt.Errorf("unexpected data after the JSON value")
|
|
59
|
+
}
|
|
60
|
+
return b.String(), nil
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func canonValue(dec *json.Decoder, b *strings.Builder) error {
|
|
64
|
+
tok, err := dec.Token()
|
|
65
|
+
if err != nil {
|
|
66
|
+
return err
|
|
67
|
+
}
|
|
68
|
+
switch v := tok.(type) {
|
|
69
|
+
case json.Delim:
|
|
70
|
+
switch v {
|
|
71
|
+
case '{':
|
|
72
|
+
b.WriteByte('{')
|
|
73
|
+
first := true
|
|
74
|
+
for dec.More() {
|
|
75
|
+
kt, err := dec.Token()
|
|
76
|
+
if err != nil {
|
|
77
|
+
return err
|
|
78
|
+
}
|
|
79
|
+
if !first {
|
|
80
|
+
b.WriteByte(',')
|
|
81
|
+
}
|
|
82
|
+
first = false
|
|
83
|
+
writeJSONString(b, kt.(string))
|
|
84
|
+
b.WriteByte(':')
|
|
85
|
+
if err := canonValue(dec, b); err != nil {
|
|
86
|
+
return err
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if _, err := dec.Token(); err != nil {
|
|
90
|
+
return err
|
|
91
|
+
}
|
|
92
|
+
b.WriteByte('}')
|
|
93
|
+
case '[':
|
|
94
|
+
b.WriteByte('[')
|
|
95
|
+
first := true
|
|
96
|
+
for dec.More() {
|
|
97
|
+
if !first {
|
|
98
|
+
b.WriteByte(',')
|
|
99
|
+
}
|
|
100
|
+
first = false
|
|
101
|
+
if err := canonValue(dec, b); err != nil {
|
|
102
|
+
return err
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if _, err := dec.Token(); err != nil {
|
|
106
|
+
return err
|
|
107
|
+
}
|
|
108
|
+
b.WriteByte(']')
|
|
109
|
+
}
|
|
110
|
+
case string:
|
|
111
|
+
writeJSONString(b, v)
|
|
112
|
+
case json.Number:
|
|
113
|
+
b.WriteString(v.String())
|
|
114
|
+
case bool:
|
|
115
|
+
b.WriteString(strconv.FormatBool(v))
|
|
116
|
+
case nil:
|
|
117
|
+
b.WriteString("null")
|
|
118
|
+
}
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// kv and obj are an ordered JSON object, so schemas keep pydantic's key order.
|
|
123
|
+
type kv struct {
|
|
124
|
+
k string
|
|
125
|
+
v any
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type obj []kv
|
|
129
|
+
|
|
130
|
+
func writeAny(b *strings.Builder, v any) {
|
|
131
|
+
switch x := v.(type) {
|
|
132
|
+
case obj:
|
|
133
|
+
b.WriteByte('{')
|
|
134
|
+
for i, e := range x {
|
|
135
|
+
if i > 0 {
|
|
136
|
+
b.WriteByte(',')
|
|
137
|
+
}
|
|
138
|
+
writeJSONString(b, e.k)
|
|
139
|
+
b.WriteByte(':')
|
|
140
|
+
writeAny(b, e.v)
|
|
141
|
+
}
|
|
142
|
+
b.WriteByte('}')
|
|
143
|
+
case []string:
|
|
144
|
+
b.WriteByte('[')
|
|
145
|
+
for i, e := range x {
|
|
146
|
+
if i > 0 {
|
|
147
|
+
b.WriteByte(',')
|
|
148
|
+
}
|
|
149
|
+
writeJSONString(b, e)
|
|
150
|
+
}
|
|
151
|
+
b.WriteByte(']')
|
|
152
|
+
case string:
|
|
153
|
+
writeJSONString(b, x)
|
|
154
|
+
case bool:
|
|
155
|
+
b.WriteString(strconv.FormatBool(x))
|
|
156
|
+
default:
|
|
157
|
+
panic(fmt.Sprintf("writeAny: unsupported %T", v))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// toMap converts an ordered object to the generic form handed to a Model.
|
|
162
|
+
func toMap(v any) any {
|
|
163
|
+
switch x := v.(type) {
|
|
164
|
+
case obj:
|
|
165
|
+
m := make(map[string]any, len(x))
|
|
166
|
+
for _, e := range x {
|
|
167
|
+
m[e.k] = toMap(e.v)
|
|
168
|
+
}
|
|
169
|
+
return m
|
|
170
|
+
case []string:
|
|
171
|
+
out := make([]any, len(x))
|
|
172
|
+
for i, s := range x {
|
|
173
|
+
out[i] = s
|
|
174
|
+
}
|
|
175
|
+
return out
|
|
176
|
+
}
|
|
177
|
+
return v
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func sortedKeys[V any](m map[string]V) []string {
|
|
181
|
+
keys := make([]string, 0, len(m))
|
|
182
|
+
for k := range m {
|
|
183
|
+
keys = append(keys, k)
|
|
184
|
+
}
|
|
185
|
+
sort.Strings(keys)
|
|
186
|
+
return keys
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// validUTF8 replaces invalid bytes so that a prompt is always valid UTF-8 (Python str cannot hold them).
|
|
190
|
+
func validUTF8(s string) string { return strings.ToValidUTF8(s, "\uFFFD") }
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package ownmodel
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"math"
|
|
5
|
+
|
|
6
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// probabilityTolerance is the largest deviation of a probability sum from 1 that is not
|
|
10
|
+
// reported as invalid (the oracle's PROBABILITY_TOLERANCE).
|
|
11
|
+
const probabilityTolerance = 1e-6
|
|
12
|
+
|
|
13
|
+
// normalizeConfidence returns probs as a distribution; a zero total gives a uniform one.
|
|
14
|
+
func normalizeConfidence(probs []float64) []float64 {
|
|
15
|
+
total := 0.0
|
|
16
|
+
for _, p := range probs {
|
|
17
|
+
total += p
|
|
18
|
+
}
|
|
19
|
+
out := make([]float64, len(probs))
|
|
20
|
+
for i, p := range probs {
|
|
21
|
+
if total == 0 {
|
|
22
|
+
out[i] = 1 / float64(len(probs))
|
|
23
|
+
} else {
|
|
24
|
+
out[i] = p / total
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// scoreConfidence measures score concentration around its modal score.
|
|
31
|
+
func scoreConfidence(probs []float64) float64 {
|
|
32
|
+
if len(probs) == 1 {
|
|
33
|
+
return 1
|
|
34
|
+
}
|
|
35
|
+
n := normalizeConfidence(probs)
|
|
36
|
+
mode := 0
|
|
37
|
+
for i := range n {
|
|
38
|
+
if n[i] > n[mode] { // the first maximum, like Python's max
|
|
39
|
+
mode = i
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
distance := 0.0
|
|
43
|
+
for i, p := range n {
|
|
44
|
+
distance += p * math.Abs(float64(i-mode))
|
|
45
|
+
}
|
|
46
|
+
center := float64(len(n)-1) / 2
|
|
47
|
+
deviation := 0.0
|
|
48
|
+
for i := range n {
|
|
49
|
+
deviation += math.Abs(float64(i) - center)
|
|
50
|
+
}
|
|
51
|
+
deviation /= float64(len(n))
|
|
52
|
+
return math.Max(0, 1-distance/deviation)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// choiceConfidence scales peak choice probability from uniform to certainty.
|
|
56
|
+
func choiceConfidence(probs []float64) float64 {
|
|
57
|
+
if len(probs) == 1 {
|
|
58
|
+
return 1
|
|
59
|
+
}
|
|
60
|
+
n := normalizeConfidence(probs)
|
|
61
|
+
peak := n[0]
|
|
62
|
+
for _, p := range n {
|
|
63
|
+
peak = math.Max(peak, p)
|
|
64
|
+
}
|
|
65
|
+
uniform := 1 / float64(len(n))
|
|
66
|
+
return (peak - uniform) / (1 - uniform)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// probNorm is the probability distribution of one answer, its error against a sum of 1,
|
|
70
|
+
// and the original distribution when normalization changed it.
|
|
71
|
+
type probNorm struct {
|
|
72
|
+
Order []string
|
|
73
|
+
Probabilities map[string]float64
|
|
74
|
+
Error float64
|
|
75
|
+
Original map[string]float64
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// rescale rescales probabilities to sum to 1, falling back to uniform for a zero total.
|
|
79
|
+
func rescale(order []string, probs map[string]float64) map[string]float64 {
|
|
80
|
+
total := 0.0
|
|
81
|
+
for _, a := range order {
|
|
82
|
+
total += probs[a]
|
|
83
|
+
}
|
|
84
|
+
out := make(map[string]float64, len(order))
|
|
85
|
+
for _, a := range order {
|
|
86
|
+
if total == 0 {
|
|
87
|
+
out[a] = 1 / float64(len(order))
|
|
88
|
+
} else {
|
|
89
|
+
out[a] = probs[a] / total
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// normalizeProbabilities builds and optionally rescales a distribution. In discrete mode
|
|
96
|
+
// selected is the chosen answer; in probabilities mode probs is the model's distribution.
|
|
97
|
+
func normalizeProbabilities(answers []string, probs map[string]float64, selected string, mode AnswerMode, enabled bool) probNorm {
|
|
98
|
+
if mode == Discrete {
|
|
99
|
+
out := make(map[string]float64, len(answers))
|
|
100
|
+
for _, a := range answers {
|
|
101
|
+
out[a] = 0
|
|
102
|
+
if a == selected {
|
|
103
|
+
out[a] = 1
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return probNorm{Order: answers, Probabilities: out}
|
|
107
|
+
}
|
|
108
|
+
original := make(map[string]float64, len(answers))
|
|
109
|
+
total := 0.0
|
|
110
|
+
for _, a := range answers {
|
|
111
|
+
original[a] = probs[a]
|
|
112
|
+
total += probs[a]
|
|
113
|
+
}
|
|
114
|
+
errAmount := math.Abs(total - 1)
|
|
115
|
+
if !enabled || errAmount <= probabilityTolerance {
|
|
116
|
+
return probNorm{Order: answers, Probabilities: original, Error: errAmount}
|
|
117
|
+
}
|
|
118
|
+
return probNorm{Order: answers, Probabilities: rescale(answers, original), Error: errAmount, Original: original}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// probDebug is the probability diagnostics of an evaluation.
|
|
122
|
+
type probDebug struct {
|
|
123
|
+
MaxError float64
|
|
124
|
+
InvalidProbs int
|
|
125
|
+
ProbabilityErrors map[string]float64
|
|
126
|
+
Original map[string]map[string]float64
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// probabilityDebugData summarizes normalizations keyed by question name (nil for a Noul).
|
|
130
|
+
func probabilityDebugData(norms map[string]*probNorm) probDebug {
|
|
131
|
+
d := probDebug{ProbabilityErrors: map[string]float64{}, Original: map[string]map[string]float64{}}
|
|
132
|
+
for name, n := range norms {
|
|
133
|
+
if n == nil {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
d.MaxError = math.Max(d.MaxError, n.Error)
|
|
137
|
+
if n.Error > probabilityTolerance {
|
|
138
|
+
d.ProbabilityErrors[name] = n.Error
|
|
139
|
+
}
|
|
140
|
+
if n.Original != nil {
|
|
141
|
+
d.Original[name] = n.Original
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
d.InvalidProbs = len(d.ProbabilityErrors)
|
|
145
|
+
return d
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// convert turns a validated output into typed answers and the normalizations of the
|
|
149
|
+
// Score and Choice answers.
|
|
150
|
+
func (p *plan) convert(d *decoded, normalize bool) (map[string]typesafe.Answer, map[string]*probNorm, error) {
|
|
151
|
+
answers := make(map[string]typesafe.Answer, len(p.questions))
|
|
152
|
+
norms := make(map[string]*probNorm, len(p.questions))
|
|
153
|
+
for i := range p.questions {
|
|
154
|
+
q := &p.questions[i]
|
|
155
|
+
v := d.answers[q.name]
|
|
156
|
+
norms[q.name] = nil
|
|
157
|
+
switch q.kind {
|
|
158
|
+
case typesafe.TypeNoul:
|
|
159
|
+
prob := v.prob
|
|
160
|
+
if p.mode == Discrete {
|
|
161
|
+
prob = 0
|
|
162
|
+
if v.flag {
|
|
163
|
+
prob = 1
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
answers[q.name] = typesafe.NoulAnswer{Noul: prob}
|
|
167
|
+
case typesafe.TypeScore:
|
|
168
|
+
n := normalizeProbabilities(q.labels, v.probs, v.selected, p.mode, normalize)
|
|
169
|
+
norms[q.name] = &n
|
|
170
|
+
// The score is an expected value, meaningful only over a distribution summing to 1:
|
|
171
|
+
// rescale here, whatever normalize says about the reported probabilities.
|
|
172
|
+
dist := rescale(q.labels, n.Probabilities)
|
|
173
|
+
score := 0.0
|
|
174
|
+
ordered := make([]float64, len(q.labels))
|
|
175
|
+
probs := make(map[int]float64, len(q.labels))
|
|
176
|
+
legend := make(map[int]any, len(q.labels))
|
|
177
|
+
for j, label := range q.labels {
|
|
178
|
+
score += float64(j) * dist[label]
|
|
179
|
+
ordered[j] = n.Probabilities[label]
|
|
180
|
+
probs[j] = n.Probabilities[label]
|
|
181
|
+
legend[j] = q.legend[j]
|
|
182
|
+
}
|
|
183
|
+
answers[q.name] = typesafe.ScoreAnswer{Score: score, Confidence: scoreConfidence(ordered), Probabilities: probs, Legend: legend}
|
|
184
|
+
case typesafe.TypeChoice:
|
|
185
|
+
n := normalizeProbabilities(q.labels, v.probs, v.selected, p.mode, normalize)
|
|
186
|
+
norms[q.name] = &n
|
|
187
|
+
best := 0
|
|
188
|
+
ordered := make([]float64, len(q.labels))
|
|
189
|
+
for j, label := range q.labels {
|
|
190
|
+
ordered[j] = n.Probabilities[label]
|
|
191
|
+
if n.Probabilities[label] > n.Probabilities[q.labels[best]] {
|
|
192
|
+
best = j
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
answers[q.name] = typesafe.ChoiceAnswer{Choice: q.labels[best], Confidence: choiceConfidence(ordered), Probabilities: n.Probabilities}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return answers, norms, nil
|
|
199
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Package ownmodel answers the same typed questions as the TypeSafe API with the
|
|
2
|
+
// model PiG is configured with. It is a port of system-one-adapter-python
|
|
3
|
+
// (https://github.com/typesafe-ai/system-one-adapter-python, MIT): the same prompts,
|
|
4
|
+
// the same per-question answer schemas, probability and discrete answer modes,
|
|
5
|
+
// probability normalization, confidence metrics, corrective retries on malformed
|
|
6
|
+
// output, and transient retries through the typesafe retry policy.
|
|
7
|
+
//
|
|
8
|
+
// A [Backend] implements typesafe.Evaluator, so an extension can hold one
|
|
9
|
+
// typesafe.Evaluator and pick the TypeSafe API or the own model by configuration.
|
|
10
|
+
// No provider or model name is built in: the model is whatever the [Model] the
|
|
11
|
+
// caller passes is; package pigmodel provides a Model backed by the PiG Go SDK's
|
|
12
|
+
// ModelRegistry (the model PiG is configured with, its auth, its provider layer).
|
|
13
|
+
//
|
|
14
|
+
// Content is sent to the configured model's provider, not to TypeSafe.
|
|
15
|
+
package ownmodel
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package ownmodel
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"math"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
|
|
13
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// The differential check against the Python oracle: port/equivalence/scenarios/*.json were
|
|
17
|
+
// run through the unmodified system-one-adapter 0.2.1 (e1d4cc9) by record_python.py, which
|
|
18
|
+
// recorded golden/*.json. Here the same scenarios run through the Go backend with the same
|
|
19
|
+
// scripted model responses, and the model calls (prompts and JSON Schema), the answers, the
|
|
20
|
+
// usage counters and the diagnostics must be the same.
|
|
21
|
+
|
|
22
|
+
type scenario struct {
|
|
23
|
+
Name string `json:"name"`
|
|
24
|
+
Mode string `json:"mode"`
|
|
25
|
+
Structured bool `json:"structured"`
|
|
26
|
+
Normalize bool `json:"normalize"`
|
|
27
|
+
MalformedRetries int `json:"malformedRetries"`
|
|
28
|
+
State json.RawMessage `json:"state"`
|
|
29
|
+
Questions json.RawMessage `json:"questions"`
|
|
30
|
+
Responses []json.RawMessage `json:"responses"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type golden struct {
|
|
34
|
+
Calls []struct {
|
|
35
|
+
Messages []Message `json:"messages"`
|
|
36
|
+
Schema map[string]any `json:"schema"`
|
|
37
|
+
Structured bool `json:"structured"`
|
|
38
|
+
} `json:"calls"`
|
|
39
|
+
Answers map[string]any `json:"answers"`
|
|
40
|
+
Usage map[string]any `json:"usage"`
|
|
41
|
+
Debug map[string]any `json:"debug"`
|
|
42
|
+
RetryReasons []string `json:"retryReasons"`
|
|
43
|
+
Error *struct {
|
|
44
|
+
Type string `json:"type"`
|
|
45
|
+
Message string `json:"message"`
|
|
46
|
+
} `json:"error"`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func loadJSON(t *testing.T, path string, v any) {
|
|
50
|
+
t.Helper()
|
|
51
|
+
raw, err := os.ReadFile(path)
|
|
52
|
+
noErr(t, err)
|
|
53
|
+
noErr(t, json.Unmarshal(raw, v))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const (
|
|
57
|
+
correctionPrefix = "The previous response did not match the required schema: "
|
|
58
|
+
correctionSuffix = "\nReturn a single JSON object that matches the schema exactly, with no other text."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
func TestEquivalence_GoBackendMatchesThePythonOracle(t *testing.T) {
|
|
62
|
+
dir := filepath.Join("..", "..", "port", "equivalence")
|
|
63
|
+
files, err := filepath.Glob(filepath.Join(dir, "scenarios", "*.json"))
|
|
64
|
+
noErr(t, err)
|
|
65
|
+
if len(files) < 15 {
|
|
66
|
+
t.Fatalf("expected the recorded scenarios, found %d", len(files))
|
|
67
|
+
}
|
|
68
|
+
for _, file := range files {
|
|
69
|
+
var sc scenario
|
|
70
|
+
loadJSON(t, file, &sc)
|
|
71
|
+
var g golden
|
|
72
|
+
loadJSON(t, filepath.Join(dir, "golden", filepath.Base(file)), &g)
|
|
73
|
+
t.Run(sc.Name, func(t *testing.T) {
|
|
74
|
+
qs, err := typesafe.ParseQuestions(sc.Questions)
|
|
75
|
+
noErr(t, err)
|
|
76
|
+
var steps []any
|
|
77
|
+
for _, r := range sc.Responses {
|
|
78
|
+
if bytes.HasPrefix(bytes.TrimSpace(r), []byte(`"`)) {
|
|
79
|
+
var s string
|
|
80
|
+
noErr(t, json.Unmarshal(r, &s))
|
|
81
|
+
steps = append(steps, s)
|
|
82
|
+
} else {
|
|
83
|
+
var buf bytes.Buffer
|
|
84
|
+
noErr(t, json.Compact(&buf, r))
|
|
85
|
+
steps = append(steps, buf.String())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
model := newScripted(steps...)
|
|
89
|
+
b := mustNew(t, Options{Model: model, AnswerMode: AnswerMode(sc.Mode), StructuredOutputs: sc.Structured, NormalizeProbabilities: sc.Normalize, MalformedRetries: sc.MalformedRetries})
|
|
90
|
+
ev, err := b.Evaluate(context.Background(), typesafe.SystemOneRequest{State: typesafe.Value(sc.State), Questions: qs}, nil)
|
|
91
|
+
|
|
92
|
+
// The model calls: prompts and schema.
|
|
93
|
+
eq(t, len(model.calls), len(g.Calls))
|
|
94
|
+
for i, want := range g.Calls {
|
|
95
|
+
got := model.calls[i]
|
|
96
|
+
eq(t, len(got), len(want.Messages))
|
|
97
|
+
for j, m := range want.Messages {
|
|
98
|
+
if m.Role == RoleUser && strings.HasPrefix(m.Content, correctionPrefix) {
|
|
99
|
+
// The validation error text is this port's own; the frame around it is the oracle's.
|
|
100
|
+
if got[j].Role != RoleUser || !strings.HasPrefix(got[j].Content, correctionPrefix) || !strings.HasSuffix(got[j].Content, correctionSuffix) {
|
|
101
|
+
t.Fatalf("call %d message %d: correction frame differs:\n%q", i, j, got[j].Content)
|
|
102
|
+
}
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if got[j] != m {
|
|
106
|
+
t.Fatalf("call %d message %d differs\n got: %s\nwant: %s", i, j, got[j].Content, m.Content)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
eq(t, model.structured[i], want.Structured)
|
|
110
|
+
sameJSON(t, model.schemas[i], want.Schema, "schema of call "+string(rune('0'+i)))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if g.Error != nil {
|
|
114
|
+
if err == nil {
|
|
115
|
+
t.Fatalf("the oracle failed with %s, Go succeeded", g.Error.Type)
|
|
116
|
+
}
|
|
117
|
+
eq(t, g.Error.Type, "TypeSafeAPIResponseValidationError")
|
|
118
|
+
mustAs[*MalformedOutputError](t, err)
|
|
119
|
+
de := mustAs[*DebugError](t, err)
|
|
120
|
+
eq(t, categories(de.Debug.RetryReasons), g.RetryReasons)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
noErr(t, err)
|
|
124
|
+
raw, err := json.Marshal(ev.Result.Answers)
|
|
125
|
+
noErr(t, err)
|
|
126
|
+
var answers map[string]any
|
|
127
|
+
noErr(t, json.Unmarshal(raw, &answers))
|
|
128
|
+
sameJSON(t, answers, g.Answers, "answers")
|
|
129
|
+
usage := map[string]any{
|
|
130
|
+
"input_tokens": intOrNil(ev.Usage.InputTokens), "output_tokens": intOrNil(ev.Usage.OutputTokens),
|
|
131
|
+
"input_tokens_total": intOrNil(ev.Usage.InputTokensTotal), "output_tokens_total": intOrNil(ev.Usage.OutputTokensTotal),
|
|
132
|
+
"n_retries": float64(ev.Usage.Retries), "n_retries_malformed_structure": float64(ev.Usage.MalformedRetries),
|
|
133
|
+
}
|
|
134
|
+
sameJSON(t, usage, g.Usage, "usage")
|
|
135
|
+
eq(t, categories(ev.Debug.RetryReasons), g.RetryReasons)
|
|
136
|
+
dbg := map[string]any{"max_error": ev.Debug.MaxError, "invalid_probs": float64(ev.Debug.InvalidProbs), "probability_errors": toAny(ev.Debug.ProbabilityErrors)}
|
|
137
|
+
if len(ev.Debug.OriginalProbabilities) > 0 {
|
|
138
|
+
dbg["original_probabilities"] = toAny(ev.Debug.OriginalProbabilities)
|
|
139
|
+
}
|
|
140
|
+
sameJSON(t, dbg, g.Debug, "debug")
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func intOrNil(p *int) any {
|
|
146
|
+
if p == nil {
|
|
147
|
+
return nil
|
|
148
|
+
}
|
|
149
|
+
return float64(*p)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func toAny(v any) any {
|
|
153
|
+
raw, _ := json.Marshal(v)
|
|
154
|
+
var out any
|
|
155
|
+
_ = json.Unmarshal(raw, &out)
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// sameJSON compares two decoded JSON values, numbers within a relative 1e-12.
|
|
160
|
+
func sameJSON(t *testing.T, got, want any, what string) {
|
|
161
|
+
t.Helper()
|
|
162
|
+
if !jsonEqual(toAny(got), toAny(want)) {
|
|
163
|
+
g, _ := json.MarshalIndent(got, "", " ")
|
|
164
|
+
w, _ := json.MarshalIndent(want, "", " ")
|
|
165
|
+
t.Fatalf("%s differs\n got: %s\nwant: %s", what, g, w)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func jsonEqual(a, b any) bool {
|
|
170
|
+
switch x := a.(type) {
|
|
171
|
+
case float64:
|
|
172
|
+
y, ok := b.(float64)
|
|
173
|
+
return ok && math.Abs(x-y) <= 1e-12*math.Max(1, math.Abs(y))
|
|
174
|
+
case map[string]any:
|
|
175
|
+
y, ok := b.(map[string]any)
|
|
176
|
+
if !ok || len(x) != len(y) {
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
for k, v := range x {
|
|
180
|
+
w, ok := y[k]
|
|
181
|
+
if !ok || !jsonEqual(v, w) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true
|
|
186
|
+
case []any:
|
|
187
|
+
y, ok := b.([]any)
|
|
188
|
+
if !ok || len(x) != len(y) {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
for i := range x {
|
|
192
|
+
if !jsonEqual(x[i], y[i]) {
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
return a == b
|
|
199
|
+
}
|