Skip to content

Commit 2c3bebe

Browse files
authored
Merge pull request #4 from HexmosTech/rijul/schema-validation
Add schema validation for requests with defined schemas
2 parents 13c6d7c + 0f62d5b commit 2c3bebe

File tree

4 files changed

+65
-15
lines changed

4 files changed

+65
-15
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/riverqueue/river/riverdriver v0.26.0 // indirect
2727
github.com/riverqueue/river/rivershared v0.26.0 // indirect
2828
github.com/riverqueue/river/rivertype v0.26.0 // indirect
29+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
2930
github.com/spf13/cobra v1.10.2 // indirect
3031
github.com/spf13/pflag v1.0.9 // indirect
3132
github.com/stretchr/testify v1.11.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
6161
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
6262
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
6363
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
64+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
65+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
6466
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
6567
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
6668
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ func main() {
122122
}
123123
},
124124
}
125-
viewCmd.Flags().BoolVar(&totalGroups, "total-groups", false, "Display total number of groups")
126-
viewCmd.Flags().IntVar(&groupID, "group", 0, "Display results for a specific group ID")
125+
// viewCmd.Flags().BoolVar(&totalGroups, "total-groups", false, "Display total number of groups")
126+
// viewCmd.Flags().IntVar(&groupID, "group", 0, "Display results for a specific group ID")
127127
viewCmd.Flags().IntVarP(&n, "number", "n", 10, "Number of results to display")
128128

129129
// ---- Delete-group subcommand ----
@@ -348,7 +348,7 @@ func main() {
348348
)
349349

350350
// Add subcommands
351-
rootCmd.AddCommand(clientCmd, workerCmd, viewCmd, deleteGroupCmd, queueCmd, exportCmd)
351+
rootCmd.AddCommand(clientCmd, workerCmd, viewCmd, queueCmd, exportCmd)
352352

353353
if err := rootCmd.Execute(); err != nil {
354354
log.Fatal().Err(err).Msg("Command execution failed")

ollama.go

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"github.com/AlecAivazis/survey/v2"
8+
"github.com/BurntSushi/toml"
9+
"github.com/santhosh-tekuri/jsonschema/v5"
710
"io"
811
"net/http"
912
"os/exec"
1013
"runtime"
14+
"strings"
1115
"time"
12-
13-
"github.com/AlecAivazis/survey/v2"
14-
"github.com/BurntSushi/toml"
1516
)
1617

1718
func LoadLLMConfig(configPath string) (*LLMConfig, error) {
@@ -41,7 +42,7 @@ func CallOllama(
4142
// Build request
4243
req := map[string]any{
4344
"model": llmConfig.Model,
44-
"stream": false,
45+
"stream": true,
4546
"messages": []map[string]string{
4647
{"role": "system", "content": basePrompt},
4748
{"role": "user", "content": prompt},
@@ -79,18 +80,32 @@ func CallOllama(
7980
return "", fmt.Errorf("ollama API returned %s", resp.Status)
8081
}
8182

82-
// Read & decode response
83-
body, err := io.ReadAll(resp.Body)
84-
if err != nil {
85-
return "", err
83+
// Decode streamed JSON objects one by one
84+
decoder := json.NewDecoder(resp.Body)
85+
86+
var fullContent strings.Builder
87+
for {
88+
var chunk OllamaResponse
89+
if err := decoder.Decode(&chunk); err != nil {
90+
if err == io.EOF {
91+
break
92+
}
93+
return "", err
94+
}
95+
96+
fullContent.WriteString(chunk.Message.Content)
8697
}
8798

88-
var ollamaResp OllamaResponse
89-
if err := json.Unmarshal(body, &ollamaResp); err != nil {
90-
return "", err
99+
output := fullContent.String()
100+
101+
// schema validation (fail job if invalid)
102+
if schema != nil {
103+
if err := validateJSONAgainstSchema(output, schema); err != nil {
104+
return "", err
105+
}
91106
}
92107

93-
return ollamaResp.Message.Content, nil
108+
return output, nil
94109
}
95110

96111
func isOllamaRunning() bool {
@@ -221,3 +236,35 @@ func waitForOllama(timeout time.Duration) error {
221236

222237
return fmt.Errorf("ollama did not start within %s", timeout)
223238
}
239+
240+
func validateJSONAgainstSchema(output string, schema any) error {
241+
if schema == nil {
242+
return nil
243+
}
244+
245+
schemaBytes, err := json.Marshal(schema)
246+
if err != nil {
247+
return fmt.Errorf("failed to marshal schema: %w", err)
248+
}
249+
250+
compiler := jsonschema.NewCompiler()
251+
if err := compiler.AddResource("schema.json", bytes.NewReader(schemaBytes)); err != nil {
252+
return fmt.Errorf("failed to add schema: %w", err)
253+
}
254+
255+
compiledSchema, err := compiler.Compile("schema.json")
256+
if err != nil {
257+
return fmt.Errorf("failed to compile schema: %w", err)
258+
}
259+
260+
var instance any
261+
if err := json.Unmarshal([]byte(output), &instance); err != nil {
262+
return fmt.Errorf("output is not valid JSON: %w", err)
263+
}
264+
265+
if err := compiledSchema.Validate(instance); err != nil {
266+
return fmt.Errorf("schema validation failed: %w", err)
267+
}
268+
269+
return nil
270+
}

0 commit comments

Comments
 (0)