@pi-in-go/pigpen-pi-typesafe-api 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/evaluation.go ADDED
@@ -0,0 +1,86 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "sort"
6
+ )
7
+
8
+ // MarshalJSON encodes the evaluation the way the original's JSON.stringify(result) does: model, answers
9
+ // (in the order the questions were asked), usage, then elapsedMs.
10
+ func (e *Evaluation) MarshalJSON() ([]byte, error) {
11
+ raw, err := json.Marshal(e.SystemOneResult)
12
+ if err != nil {
13
+ return nil, err
14
+ }
15
+ tree, err := ParseJSON(raw)
16
+ if err != nil {
17
+ return nil, err
18
+ }
19
+ source, ok := tree.(*Object)
20
+ if !ok {
21
+ return nil, newError(CodeResponse, "TypeSafe returned an unreadable or unexpected response.")
22
+ }
23
+ out := NewObject()
24
+ if v, ok := source.Get("model"); ok {
25
+ out.Set("model", v)
26
+ }
27
+ if v, ok := source.Get("answers"); ok {
28
+ out.Set("answers", e.orderAnswers(v))
29
+ }
30
+ if v, ok := source.Get("usage"); ok {
31
+ out.Set("usage", v)
32
+ }
33
+ for _, k := range source.Keys() {
34
+ if !out.Has(k) {
35
+ out.Set(k, source.vals[k])
36
+ }
37
+ }
38
+ out.Set("elapsedMs", float64(e.ElapsedMs))
39
+ return EncodeJSON(out)
40
+ }
41
+
42
+ // orderAnswers reorders an answers object to the question order; unknown ids follow, sorted.
43
+ func (e *Evaluation) orderAnswers(v any) any {
44
+ answers, ok := v.(*Object)
45
+ if !ok {
46
+ return v
47
+ }
48
+ ordered := NewObject()
49
+ for _, id := range e.Order {
50
+ if a, ok := answers.Get(id); ok {
51
+ ordered.Set(id, a)
52
+ }
53
+ }
54
+ rest := []string{}
55
+ for _, id := range answers.Keys() {
56
+ if !ordered.Has(id) {
57
+ rest = append(rest, id)
58
+ }
59
+ }
60
+ sort.Strings(rest)
61
+ for _, id := range rest {
62
+ a, _ := answers.Get(id)
63
+ ordered.Set(id, a)
64
+ }
65
+ return ordered
66
+ }
67
+
68
+ // DetailsJSON is MarshalJSON plus an "order" array naming the answer ids in question order. A host that decodes
69
+ // tool details into maps loses key order, so a renderer reads the order from here.
70
+ func (e *Evaluation) DetailsJSON() ([]byte, error) {
71
+ raw, err := e.MarshalJSON()
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+ tree, err := ParseJSON(raw)
76
+ if err != nil {
77
+ return nil, err
78
+ }
79
+ obj := tree.(*Object)
80
+ order := make([]any, len(e.Order))
81
+ for i, id := range e.Order {
82
+ order[i] = id
83
+ }
84
+ obj.Set("order", order)
85
+ return EncodeJSON(obj)
86
+ }
@@ -0,0 +1,264 @@
1
+ {
2
+ "type": "object",
3
+ "required": [
4
+ "state",
5
+ "questions"
6
+ ],
7
+ "properties": {
8
+ "state": {
9
+ "anyOf": [
10
+ {
11
+ "type": "string"
12
+ },
13
+ {
14
+ "type": "null"
15
+ },
16
+ {
17
+ "type": "array",
18
+ "items": {}
19
+ },
20
+ {
21
+ "type": "object",
22
+ "patternProperties": {
23
+ "^.*$": {}
24
+ }
25
+ }
26
+ ],
27
+ "description": "What to judge: text, or an object whose fields the questions name."
28
+ },
29
+ "questions": {
30
+ "type": "object",
31
+ "patternProperties": {
32
+ "^.*$": {
33
+ "anyOf": [
34
+ {
35
+ "type": "object",
36
+ "required": [
37
+ "type"
38
+ ],
39
+ "properties": {
40
+ "type": {
41
+ "type": "string",
42
+ "const": "noul",
43
+ "description": "Yes or no: the probability the instructions hold."
44
+ },
45
+ "instructions": {
46
+ "anyOf": [
47
+ {
48
+ "type": "string"
49
+ },
50
+ {
51
+ "type": "null"
52
+ },
53
+ {
54
+ "type": "array",
55
+ "items": {}
56
+ },
57
+ {
58
+ "type": "object",
59
+ "patternProperties": {
60
+ "^.*$": {}
61
+ }
62
+ }
63
+ ],
64
+ "description": "One judgment about the whole state, phrased as a question or a statement."
65
+ },
66
+ "criteria": {
67
+ "anyOf": [
68
+ {
69
+ "type": "null"
70
+ },
71
+ {
72
+ "type": "object",
73
+ "properties": {
74
+ "true": {
75
+ "anyOf": [
76
+ {
77
+ "type": "string"
78
+ },
79
+ {
80
+ "type": "null"
81
+ },
82
+ {
83
+ "type": "array",
84
+ "items": {}
85
+ },
86
+ {
87
+ "type": "object",
88
+ "patternProperties": {
89
+ "^.*$": {}
90
+ }
91
+ }
92
+ ]
93
+ },
94
+ "false": {
95
+ "anyOf": [
96
+ {
97
+ "type": "string"
98
+ },
99
+ {
100
+ "type": "null"
101
+ },
102
+ {
103
+ "type": "array",
104
+ "items": {}
105
+ },
106
+ {
107
+ "type": "object",
108
+ "patternProperties": {
109
+ "^.*$": {}
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ },
115
+ "additionalProperties": false
116
+ }
117
+ ],
118
+ "description": "Optional: what counts as yes and what counts as no, { true, false }."
119
+ }
120
+ },
121
+ "additionalProperties": false
122
+ },
123
+ {
124
+ "type": "object",
125
+ "required": [
126
+ "type",
127
+ "criteria"
128
+ ],
129
+ "properties": {
130
+ "type": {
131
+ "type": "string",
132
+ "const": "choice",
133
+ "description": "Pick one criteria label."
134
+ },
135
+ "instructions": {
136
+ "anyOf": [
137
+ {
138
+ "type": "string"
139
+ },
140
+ {
141
+ "type": "null"
142
+ },
143
+ {
144
+ "type": "array",
145
+ "items": {}
146
+ },
147
+ {
148
+ "type": "object",
149
+ "patternProperties": {
150
+ "^.*$": {}
151
+ }
152
+ }
153
+ ],
154
+ "description": "One judgment about the whole state, phrased as a question or a statement."
155
+ },
156
+ "criteria": {
157
+ "type": "object",
158
+ "patternProperties": {
159
+ "^.*$": {
160
+ "anyOf": [
161
+ {
162
+ "type": "string"
163
+ },
164
+ {
165
+ "type": "null"
166
+ },
167
+ {
168
+ "type": "array",
169
+ "items": {}
170
+ },
171
+ {
172
+ "type": "object",
173
+ "patternProperties": {
174
+ "^.*$": {}
175
+ }
176
+ }
177
+ ]
178
+ }
179
+ },
180
+ "minProperties": 1,
181
+ "maxProperties": 64,
182
+ "description": "The options as a map from label to when it applies: { billing: \"Charges and payments\", other: null }. 1–64 entries."
183
+ }
184
+ },
185
+ "additionalProperties": false
186
+ },
187
+ {
188
+ "type": "object",
189
+ "required": [
190
+ "type",
191
+ "criteria"
192
+ ],
193
+ "properties": {
194
+ "type": {
195
+ "type": "string",
196
+ "const": "score",
197
+ "description": "Rate against the ordered criteria levels."
198
+ },
199
+ "instructions": {
200
+ "anyOf": [
201
+ {
202
+ "type": "string"
203
+ },
204
+ {
205
+ "type": "null"
206
+ },
207
+ {
208
+ "type": "array",
209
+ "items": {}
210
+ },
211
+ {
212
+ "type": "object",
213
+ "patternProperties": {
214
+ "^.*$": {}
215
+ }
216
+ }
217
+ ],
218
+ "description": "One judgment about the whole state, phrased as a question or a statement."
219
+ },
220
+ "criteria": {
221
+ "type": "array",
222
+ "items": {
223
+ "anyOf": [
224
+ {
225
+ "type": "string"
226
+ },
227
+ {
228
+ "type": "null"
229
+ },
230
+ {
231
+ "type": "array",
232
+ "items": {}
233
+ },
234
+ {
235
+ "type": "object",
236
+ "patternProperties": {
237
+ "^.*$": {}
238
+ }
239
+ }
240
+ ]
241
+ },
242
+ "minItems": 2,
243
+ "maxItems": 32,
244
+ "description": "Ordered rubric levels, lowest first: [\"neutral\", \"angry\"]. 2–32 levels."
245
+ }
246
+ },
247
+ "additionalProperties": false
248
+ }
249
+ ]
250
+ }
251
+ },
252
+ "minProperties": 1,
253
+ "maxProperties": 32,
254
+ "description": "Questions keyed by a short id, as an object map, not an array: { \"urgent\": { type: \"noul\", instructions: ... } }."
255
+ },
256
+ "model": {
257
+ "type": "string",
258
+ "minLength": 1,
259
+ "maxLength": 100,
260
+ "description": "Jev model id, e.g. jev-latest. Omit for the default."
261
+ }
262
+ },
263
+ "additionalProperties": false
264
+ }
package/gaps_test.go ADDED
@@ -0,0 +1,77 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "context"
5
+ "os"
6
+ "runtime"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
11
+ )
12
+
13
+ // Cases the mutation check showed unproven by the ported twins.
14
+
15
+ func TestGroupReadableKeyFileIsRefused(t *testing.T) {
16
+ if runtime.GOOS == "windows" {
17
+ t.Skip("POSIX permission bits")
18
+ }
19
+ isolate(t)
20
+ _, _ = StoreAPIKey(validKey)
21
+ _ = os.Chmod(CredentialsPath(), 0o640)
22
+ if s, _ := KeySituationFor(nil); s.Kind != KeyUnusable {
23
+ t.Fatalf("a group-readable key file must be unusable: %+v", s)
24
+ }
25
+ }
26
+
27
+ func TestAskDefaultDeadlineIsFifteenSeconds(t *testing.T) {
28
+ var remaining time.Duration
29
+ judge := judgeFunc(func(ctx context.Context, _ typesafe.SystemOneRequest) (*Evaluation, error) {
30
+ deadline, ok := ctx.Deadline()
31
+ if !ok {
32
+ t.Error("Ask must set a deadline")
33
+ }
34
+ remaining = time.Until(deadline)
35
+ return &Evaluation{SystemOneResult: &typesafe.SystemOneResult{}}, nil
36
+ })
37
+ Ask(context.Background(), judge, sampleRequest(), AskOptions{})
38
+ if remaining < 14*time.Second || remaining > 15*time.Second {
39
+ t.Fatalf("deadline in %v", remaining)
40
+ }
41
+ }
42
+
43
+ func TestAbortedFailureStopsTheBatchToo(t *testing.T) {
44
+ calls := 0
45
+ judge := batchJudge(func(ctx context.Context, _ typesafe.SystemOneRequest) (*Evaluation, error) {
46
+ calls++
47
+ return nil, newError(CodeAborted, "cancelled")
48
+ })
49
+ batch := EvaluateMany(context.Background(), judge, []typesafe.SystemOneRequest{sampleRequest(), sampleRequest(), sampleRequest()}, BatchOptions{Concurrency: 1})
50
+ if calls != 1 || batch.Skipped != 2 {
51
+ t.Fatalf("calls=%d skipped=%d", calls, batch.Skipped)
52
+ }
53
+ }
54
+
55
+ type batchJudge func(context.Context, typesafe.SystemOneRequest) (*Evaluation, error)
56
+
57
+ func (f batchJudge) Evaluate(ctx context.Context, r typesafe.SystemOneRequest) (*Evaluation, error) {
58
+ return f(ctx, r)
59
+ }
60
+
61
+ func TestOnlyHTTPRejectionsDegradeTheKey(t *testing.T) {
62
+ isolate(t)
63
+ t.Setenv("TYPESAFE_API_KEY", "env-key-0123456789abcdef")
64
+ // A 401 status on a non-HTTP failure code is not a rejection.
65
+ RecordAuthFailure(&IntegrationError{Code: CodeResponse, Message: "odd", Status: 401}, time.Now())
66
+ if !mustAuth(t, nil).Usable {
67
+ t.Fatal("only an HTTP 401 or 403 rejects the key")
68
+ }
69
+ RecordAuthFailure(&IntegrationError{Code: CodeHTTP, Message: "forbidden", Status: 403}, time.Now())
70
+ if mustAuth(t, nil).Usable {
71
+ t.Fatal("a 403 rejects the key")
72
+ }
73
+ RecordAuthFailure(&IntegrationError{Code: CodeHTTP, Message: "gone", Status: 404}, time.Now())
74
+ if !mustAuth(t, nil).Usable {
75
+ t.Fatal("a 404 does not")
76
+ }
77
+ }
package/go.mod ADDED
@@ -0,0 +1,9 @@
1
+ module github.com/MichaelKinsy/pigpen/components/pi-typesafe-api
2
+
3
+ go 1.26
4
+
5
+ // PiG resolves this requirement to the version-matched staged SDK at build time.
6
+ require github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1
7
+
8
+ // The shared TypeSafe client (components/typesafe), found through go.work.
9
+ require github.com/MichaelKinsy/pigpen/components/typesafe v0.0.0
package/go.sum ADDED
@@ -0,0 +1,2 @@
1
+ github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1/go.mod h1:4j0XhCW0EPOBWvqYE7XKjTvIAahI7K1VyyW3KpjTKj8=
2
+ github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1 h1:Ql9sTkc6dvAXaYQLq5nxqmSZgqu8S0L53ZTs8MkOuxw=
@@ -0,0 +1,169 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "io"
8
+ "net/http"
9
+ "os"
10
+ "path/filepath"
11
+ "regexp"
12
+ "strings"
13
+ "sync"
14
+ "testing"
15
+
16
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
17
+ )
18
+
19
+ // isolate points every path and every key variable at a temporary directory (rule 17): a test never
20
+ // reads or writes the real agent directory, and never sees a key from the environment.
21
+ func isolate(t *testing.T) string {
22
+ t.Helper()
23
+ dir := t.TempDir()
24
+ t.Setenv("HOME", dir)
25
+ t.Setenv("PIG_HOME", filepath.Join(dir, "pighome"))
26
+ t.Setenv("PIG_CODING_AGENT_DIR", filepath.Join(dir, "agent"))
27
+ t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(dir, "pi-agent"))
28
+ t.Setenv("PIG_USE_PI_DIRS", "")
29
+ for _, name := range []string{"TYPESAFE_API_KEY", "OPENROUTER_API_KEY", "COMMANDCODE_API_KEY", "GATEWAY_JEV_KEY",
30
+ "PI_TYPESAFE_ENABLED", "PI_TYPESAFE_MAX_REQUESTS_PER_DAY", "PI_TYPESAFE_MAX_INPUT_TOKENS_PER_DAY", "PI_TYPESAFE_MAX_USD_PER_DAY",
31
+ "TYPESAFE_BASE_URL", "TYPESAFE_LOG_LEVEL"} {
32
+ t.Setenv(name, "")
33
+ }
34
+ return filepath.Join(dir, "agent")
35
+ }
36
+
37
+ type doerFunc func(*http.Request) (*http.Response, error)
38
+
39
+ func (f doerFunc) Do(r *http.Request) (*http.Response, error) { return f(r) }
40
+
41
+ func jsonResponse(status int, body any, header http.Header) *http.Response {
42
+ data, _ := json.Marshal(body)
43
+ if header == nil {
44
+ header = http.Header{}
45
+ }
46
+ header.Set("Content-Type", "application/json")
47
+ return &http.Response{StatusCode: status, Status: http.StatusText(status), Header: header, Body: io.NopCloser(bytes.NewReader(data)), ContentLength: int64(len(data))}
48
+ }
49
+
50
+ func rawResponse(status int, body string) *http.Response {
51
+ return &http.Response{StatusCode: status, Header: http.Header{}, Body: io.NopCloser(bytes.NewReader([]byte(body)))}
52
+ }
53
+
54
+ // answersFor builds the answers a server returns for questions: the first option with certainty.
55
+ func answersFor(questions typesafe.Questions) map[string]any {
56
+ answers := map[string]any{}
57
+ for _, nq := range questions {
58
+ switch q := nq.Question.(type) {
59
+ case typesafe.NoulQuestion:
60
+ answers[nq.Name] = map[string]any{"type": "noul", "noul": 0.9}
61
+ case typesafe.ChoiceQuestion:
62
+ probs := map[string]any{}
63
+ for i, o := range q.Criteria {
64
+ probs[o.Label] = map[bool]float64{true: 1, false: 0}[i == 0]
65
+ }
66
+ answers[nq.Name] = map[string]any{"type": "choice", "choice": q.Criteria[0].Label, "confidence": 1, "probabilities": probs}
67
+ case typesafe.ScoreQuestion:
68
+ probs, legend := map[string]any{}, map[string]any{}
69
+ for i, level := range q.Criteria {
70
+ probs[itoa(i)] = map[bool]float64{true: 1, false: 0}[i == 0]
71
+ legend[itoa(i)] = level.Data()
72
+ }
73
+ answers[nq.Name] = map[string]any{"type": "score", "score": 0, "confidence": 1, "probabilities": probs, "legend": legend}
74
+ }
75
+ }
76
+ return answers
77
+ }
78
+
79
+ func itoa(i int) string { b, _ := json.Marshal(i); return string(b) }
80
+
81
+ // responseFor is the successful reply of the fake TypeSafe server to a request body.
82
+ func responseFor(body []byte) *http.Response {
83
+ var req struct {
84
+ Questions json.RawMessage `json:"questions"`
85
+ }
86
+ _ = json.Unmarshal(body, &req)
87
+ questions, err := typesafe.ParseQuestions(req.Questions)
88
+ if err != nil {
89
+ return jsonResponse(400, map[string]any{"error": "bad questions"}, nil)
90
+ }
91
+ return jsonResponse(200, map[string]any{"model": "jev-test", "answers": answersFor(questions), "usage": map[string]any{"input_tokens": 42, "output_tokens": 0}}, nil)
92
+ }
93
+
94
+ func requestBody(t *testing.T, r *http.Request) []byte {
95
+ t.Helper()
96
+ if r.Body == nil {
97
+ return nil
98
+ }
99
+ data, err := io.ReadAll(r.Body)
100
+ if err != nil {
101
+ t.Fatal(err)
102
+ }
103
+ return data
104
+ }
105
+
106
+ // answering is a fake server that answers every questions request and counts the calls.
107
+ type answering struct {
108
+ mu sync.Mutex
109
+ calls int
110
+ urls []string
111
+ seen []*http.Request
112
+ }
113
+
114
+ func (a *answering) Do(r *http.Request) (*http.Response, error) {
115
+ body, _ := io.ReadAll(r.Body)
116
+ a.mu.Lock()
117
+ a.calls++
118
+ a.urls = append(a.urls, r.URL.String())
119
+ a.seen = append(a.seen, r)
120
+ a.mu.Unlock()
121
+ return responseFor(body), nil
122
+ }
123
+
124
+ func (a *answering) count() int { a.mu.Lock(); defer a.mu.Unlock(); return a.calls }
125
+
126
+ func sampleRequest() typesafe.SystemOneRequest {
127
+ return typesafe.SystemOneRequest{State: typesafe.Text("synthetic"), Questions: typesafe.Questions{typesafe.Ask("yes", typesafe.Noul("Is this synthetic?"))}}
128
+ }
129
+
130
+ func manyQuestions(n int) typesafe.Questions {
131
+ qs := make(typesafe.Questions, n)
132
+ for i := range qs {
133
+ qs[i] = typesafe.Ask("q"+itoa(i), typesafe.Noul("Question "+itoa(i)+"?"))
134
+ }
135
+ return qs
136
+ }
137
+
138
+ func hasCode(err error, code ErrorCode) bool {
139
+ ie, ok := err.(*IntegrationError)
140
+ return ok && ie.Code == code
141
+ }
142
+
143
+ func mustTree(t *testing.T, text string) any {
144
+ t.Helper()
145
+ v, err := ParseJSON([]byte(text))
146
+ if err != nil {
147
+ t.Fatal(err)
148
+ }
149
+ return v
150
+ }
151
+
152
+ func writeFile(t *testing.T, path, content string, mode os.FileMode) {
153
+ t.Helper()
154
+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
155
+ t.Fatal(err)
156
+ }
157
+ if err := os.WriteFile(path, []byte(content), mode); err != nil {
158
+ t.Fatal(err)
159
+ }
160
+ _ = os.Chmod(path, mode)
161
+ }
162
+
163
+ var _ = context.Background
164
+
165
+ func regexpMatch(pattern, s string) bool { return regexp.MustCompile(pattern).MatchString(s) }
166
+
167
+ func contains(s, sub string) bool { return strings.Contains(s, sub) }
168
+
169
+ func apiError(status int) error { return typesafe.NewAPIError(status, nil, http.Header{}) }
@@ -0,0 +1,87 @@
1
+ // Package hostmodel is the own-model backend on the PiG Go SDK: it answers TypeSafe's typed questions with the
2
+ // model PiG is configured with, chosen at call time from the request's Context, so a model switch takes effect
3
+ // on the next call without rebuilding the client. Nothing here names a provider or a model.
4
+ //
5
+ // Pass the Evaluator to pitypesafe.New as Options.Evaluator with Backend "ownmodel", and put the tool call's
6
+ // Context in the request context with [WithContext].
7
+ package hostmodel
8
+
9
+ import (
10
+ "context"
11
+
12
+ sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
13
+ pitypesafe "github.com/MichaelKinsy/pigpen/components/pi-typesafe-api"
14
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/ownmodel"
15
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/pigmodel"
16
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
17
+ )
18
+
19
+ type contextKey struct{}
20
+
21
+ // WithContext returns a context that carries the PiG extension Context the model is read from.
22
+ func WithContext(ctx context.Context, host sdk.Context) context.Context {
23
+ return context.WithValue(ctx, contextKey{}, host)
24
+ }
25
+
26
+ // Options configure the backend built for each call.
27
+ type Options struct {
28
+ // AnswerMode is ownmodel.Probabilities (the default) or ownmodel.Discrete.
29
+ AnswerMode ownmodel.AnswerMode
30
+ // NormalizeProbabilities rescales invalid probability distributions to sum to 1.
31
+ NormalizeProbabilities bool
32
+ // MalformedRetries is the number of corrective retries when the output fails validation.
33
+ MalformedRetries int
34
+ // Logger receives request summaries; nil discards.
35
+ Logger typesafe.Logger
36
+ }
37
+
38
+ // Evaluator answers questions with the host's active model. It is safe for concurrent use.
39
+ type Evaluator struct{ opts Options }
40
+
41
+ // New returns an Evaluator.
42
+ func New(opts Options) *Evaluator { return &Evaluator{opts: opts} }
43
+
44
+ var _ typesafe.Evaluator = (*Evaluator)(nil)
45
+
46
+ // ModelRef names the model a Context is configured with.
47
+ type ModelRef struct{ Provider, ID string }
48
+
49
+ // Active returns the model PiG is configured with, or an error when none is selected. The setup errors here
50
+ // are configuration errors (*pitypesafe.IntegrationError) so their reason reaches the operator unchanged.
51
+ func Active(host sdk.Context) (ModelRef, error) {
52
+ info, err := host.GetModelInfo()
53
+ if err != nil {
54
+ return ModelRef{}, err
55
+ }
56
+ if info == nil || info.ID == "" {
57
+ return ModelRef{}, &pitypesafe.IntegrationError{Code: pitypesafe.CodeConfiguration, Message: "No model is configured in PiG; select one with /model first."}
58
+ }
59
+ return ModelRef{Provider: info.Provider, ID: info.ID}, nil
60
+ }
61
+
62
+ // SystemOne implements typesafe.Evaluator on the model of the Context in ctx.
63
+ func (e *Evaluator) SystemOne(ctx context.Context, req typesafe.SystemOneRequest, opts *typesafe.RequestOptions) (*typesafe.SystemOneResult, error) {
64
+ host, ok := ctx.Value(contextKey{}).(sdk.Context)
65
+ if !ok {
66
+ return nil, &pitypesafe.IntegrationError{Code: pitypesafe.CodeConfiguration, Message: "The own-model backend needs the PiG Context in the request context (hostmodel.WithContext)."}
67
+ }
68
+ ref, err := Active(host)
69
+ if err != nil {
70
+ return nil, err
71
+ }
72
+ model, err := pigmodel.New(host.ModelRegistry(), pigmodel.Ref{Provider: ref.Provider, ID: ref.ID})
73
+ if err != nil {
74
+ return nil, err
75
+ }
76
+ backend, err := ownmodel.New(ownmodel.Options{
77
+ Model: model,
78
+ AnswerMode: e.opts.AnswerMode,
79
+ NormalizeProbabilities: e.opts.NormalizeProbabilities,
80
+ MalformedRetries: e.opts.MalformedRetries,
81
+ Logger: e.opts.Logger,
82
+ })
83
+ if err != nil {
84
+ return nil, err
85
+ }
86
+ return backend.SystemOne(ctx, req, opts)
87
+ }