code.oscarkilo.com/klex-git

Hash:
cd12dfd6532e2b1660f0e5fd4f12d8b75fa292b1
Author:
pjo <[email protected]>
Date:
Thu Nov 20 18:36:06 2025 -0800
Message:
add bunnie ransom note example
diff --git a/exemplary/README.md b/exemplary/README.md
index 4b9a3b5..3cfe9c4 100644
--- a/exemplary/README.md
+++ b/exemplary/README.md
@@ -10,9 +10,10 @@ Take a look at the files in the `example` folder.
- `2025-10-10.out` is the desired output for that letter.
- `2025-10-20.jpg` is a photo of a handwritten letter.
- `2025-11-11.txt` is another letter, whose output will be generated.
+- `2025-11-19.webp` is ransom note from big lebowski.

-To generate `2025-10-20.out` and `2025-11-11.out`, run the following command
-in the `example/` directory.
+To generate `2025-10-20.out` and `2025-11-11.out` and `2025-11-19.out`,
+run the following command in the `example/` directory:

```bash
go run oscarkilo.com/klex-git/exemplary
diff --git a/exemplary/example/2025-11-19.webp b/exemplary/example/2025-11-19.webp
new file mode 100644
index 0000000..c72a08b
Binary files /dev/null and b/exemplary/example/2025-11-19.webp differ
diff --git a/exemplary/main.go b/exemplary/main.go
index 9f189d9..6b4720f 100644
--- a/exemplary/main.go
+++ b/exemplary/main.go
@@ -49,7 +49,7 @@ func scanForCases() []Case {
name := strings.Join(chunks[0:len(chunks)-1], ".")
suffix := chunks[len(chunks)-1]
switch suffix {
- case "txt", "json", "jpg", "jpeg", "png":
+ case "txt", "json", "jpg", "jpeg", "png", "webp":
before[name] = append(before[name], suffix)
case "out":
after[name] = suffix
a/exemplary/README.md
b/exemplary/README.md
1
### Exemplary
1
### Exemplary
2
2
3
...is an AI tool for transforming documents based on a small set of
3
...is an AI tool for transforming documents based on a small set of
4
examples. Fittingly, let's demonstrate its use with an example.
4
examples. Fittingly, let's demonstrate its use with an example.
5
5
6
Take a look at the files in the `example` folder.
6
Take a look at the files in the `example` folder.
7
7
8
- `system_prompt.txt` explains the task, which is to summarize incoming mail.
8
- `system_prompt.txt` explains the task, which is to summarize incoming mail.
9
- `2025-10-10.txt` is an example of a personal letter.
9
- `2025-10-10.txt` is an example of a personal letter.
10
- `2025-10-10.out` is the desired output for that letter.
10
- `2025-10-10.out` is the desired output for that letter.
11
- `2025-10-20.jpg` is a photo of a handwritten letter.
11
- `2025-10-20.jpg` is a photo of a handwritten letter.
12
- `2025-11-11.txt` is another letter, whose output will be generated.
12
- `2025-11-11.txt` is another letter, whose output will be generated.
13
- `2025-11-19.webp` is ransom note from big lebowski.
13
14
14
To generate `2025-10-20.out` and `2025-11-11.out`, run the following command
15
To generate `2025-10-20.out` and `2025-11-11.out` and `2025-11-19.out`,
15
in the `example/` directory.
16
run the following command in the `example/` directory:
16
17
17
```bash
18
```bash
18
go run oscarkilo.com/klex-git/exemplary
19
go run oscarkilo.com/klex-git/exemplary
19
```
20
```
20
21
a/exemplary/main.go
b/exemplary/main.go
1
package main
1
package main
2
2
3
import "encoding/base64"
3
import "encoding/base64"
4
import "encoding/json"
4
import "encoding/json"
5
import "flag"
5
import "flag"
6
import "io/ioutil"
6
import "io/ioutil"
7
import "log"
7
import "log"
8
import "net/http"
8
import "net/http"
9
import "os"
9
import "os"
10
import "path"
10
import "path"
11
import "sort"
11
import "sort"
12
import "strings"
12
import "strings"
13
13
14
import "oscarkilo.com/klex-git/api"
14
import "oscarkilo.com/klex-git/api"
15
import "oscarkilo.com/klex-git/config"
15
import "oscarkilo.com/klex-git/config"
16
16
17
var dir = flag.String("dir", ".", "Directory to scan and write to.")
17
var dir = flag.String("dir", ".", "Directory to scan and write to.")
18
var model = flag.String("model", "Gemini 3 Pro", "")
18
var model = flag.String("model", "Gemini 3 Pro", "")
19
var format = flag.String("format", "text", "text|json|jsonindent")
19
var format = flag.String("format", "text", "text|json|jsonindent")
20
var dry_run = flag.Bool("dry_run", false, "")
20
var dry_run = flag.Bool("dry_run", false, "")
21
var debug = flag.Bool("debug", false, "")
21
var debug = flag.Bool("debug", false, "")
22
22
23
type Case struct {
23
type Case struct {
24
Name string
24
Name string
25
Before []string
25
Before []string
26
After string
26
After string
27
Request *api.MessagesRequest
27
Request *api.MessagesRequest
28
}
28
}
29
29
30
func scanForCases() []Case {
30
func scanForCases() []Case {
31
entries, err := os.ReadDir(*dir)
31
entries, err := os.ReadDir(*dir)
32
if err != nil {
32
if err != nil {
33
log.Fatalf("Failed to read dir %s: %v", *dir, err)
33
log.Fatalf("Failed to read dir %s: %v", *dir, err)
34
}
34
}
35
35
36
before := make(map[string][]string)
36
before := make(map[string][]string)
37
after := make(map[string]string)
37
after := make(map[string]string)
38
for _, entry := range entries {
38
for _, entry := range entries {
39
if entry.IsDir() {
39
if entry.IsDir() {
40
continue
40
continue
41
}
41
}
42
if entry.Name() == "system_prompt.txt" {
42
if entry.Name() == "system_prompt.txt" {
43
continue
43
continue
44
}
44
}
45
chunks := strings.Split(entry.Name(), ".")
45
chunks := strings.Split(entry.Name(), ".")
46
if len(chunks) < 2 {
46
if len(chunks) < 2 {
47
continue
47
continue
48
}
48
}
49
name := strings.Join(chunks[0:len(chunks)-1], ".")
49
name := strings.Join(chunks[0:len(chunks)-1], ".")
50
suffix := chunks[len(chunks)-1]
50
suffix := chunks[len(chunks)-1]
51
switch suffix {
51
switch suffix {
52
case "txt", "json", "jpg", "jpeg", "png":
52
case "txt", "json", "jpg", "jpeg", "png", "webp":
53
before[name] = append(before[name], suffix)
53
before[name] = append(before[name], suffix)
54
case "out":
54
case "out":
55
after[name] = suffix
55
after[name] = suffix
56
}
56
}
57
}
57
}
58
58
59
var cases []Case
59
var cases []Case
60
for name, b := range before {
60
for name, b := range before {
61
sort.Slice(b, func(i, j int) bool {
61
sort.Slice(b, func(i, j int) bool {
62
if b[i] == "txt" && b[j] != "txt" {
62
if b[i] == "txt" && b[j] != "txt" {
63
return true
63
return true
64
}
64
}
65
if b[i] != "txt" && b[j] == "txt" {
65
if b[i] != "txt" && b[j] == "txt" {
66
return false
66
return false
67
}
67
}
68
return b[i] < b[j]
68
return b[i] < b[j]
69
})
69
})
70
cases = append(cases, Case{
70
cases = append(cases, Case{
71
Name: name,
71
Name: name,
72
Before: b,
72
Before: b,
73
After: after[name],
73
After: after[name],
74
})
74
})
75
}
75
}
76
76
77
sort.Slice(cases, func(i, j int) bool {
77
sort.Slice(cases, func(i, j int) bool {
78
a, b := cases[i], cases[j]
78
a, b := cases[i], cases[j]
79
if a.After != "" && b.After == "" {
79
if a.After != "" && b.After == "" {
80
return true
80
return true
81
}
81
}
82
if a.After == "" && b.After != "" {
82
if a.After == "" && b.After != "" {
83
return false
83
return false
84
}
84
}
85
return a.Name < b.Name
85
return a.Name < b.Name
86
})
86
})
87
87
88
num_examples := 0
88
num_examples := 0
89
for ; num_examples < len(cases); num_examples++ {
89
for ; num_examples < len(cases); num_examples++ {
90
if cases[num_examples].After == "" {
90
if cases[num_examples].After == "" {
91
break
91
break
92
}
92
}
93
}
93
}
94
log.Printf(
94
log.Printf(
95
"%s:\n num_examples = %d\n num_inputs = %d",
95
"%s:\n num_examples = %d\n num_inputs = %d",
96
*dir,
96
*dir,
97
num_examples,
97
num_examples,
98
len(cases) - num_examples,
98
len(cases) - num_examples,
99
)
99
)
100
100
101
return cases
101
return cases
102
}
102
}
103
103
104
func readFile(fname string) []byte {
104
func readFile(fname string) []byte {
105
data, err := ioutil.ReadFile(path.Join(*dir, fname))
105
data, err := ioutil.ReadFile(path.Join(*dir, fname))
106
if err != nil {
106
if err != nil {
107
log.Fatalf("Failed to read file %s: %v", fname, err)
107
log.Fatalf("Failed to read file %s: %v", fname, err)
108
}
108
}
109
return data
109
return data
110
}
110
}
111
111
112
func writeFile(fname string, data []byte) error {
112
func writeFile(fname string, data []byte) error {
113
return ioutil.WriteFile(path.Join(*dir, fname), data, 0644)
113
return ioutil.WriteFile(path.Join(*dir, fname), data, 0644)
114
}
114
}
115
115
116
func copyRequest(req api.MessagesRequest) *api.MessagesRequest {
116
func copyRequest(req api.MessagesRequest) *api.MessagesRequest {
117
bytes, err := json.Marshal(req)
117
bytes, err := json.Marshal(req)
118
if err != nil {
118
if err != nil {
119
log.Fatalf("Failed to copy request: %+v", err)
119
log.Fatalf("Failed to copy request: %+v", err)
120
}
120
}
121
req2 := &api.MessagesRequest{}
121
req2 := &api.MessagesRequest{}
122
err = json.Unmarshal(bytes, req2)
122
err = json.Unmarshal(bytes, req2)
123
if err != nil {
123
if err != nil {
124
log.Fatalf("Failed to copy request: %+v", err)
124
log.Fatalf("Failed to copy request: %+v", err)
125
}
125
}
126
return req2
126
return req2
127
}
127
}
128
128
129
func main() {
129
func main() {
130
flag.Parse()
130
flag.Parse()
131
131
132
// Find the API keys and configure a Klex client.
132
// Find the API keys and configure a Klex client.
133
config, err := config.ReadConfig()
133
config, err := config.ReadConfig()
134
if err != nil {
134
if err != nil {
135
log.Fatalf("Failed to read config: %v", err)
135
log.Fatalf("Failed to read config: %v", err)
136
}
136
}
137
client := api.NewClient(config.KlexUrl, config.ApiKey)
137
client := api.NewClient(config.KlexUrl, config.ApiKey)
138
if client == nil {
138
if client == nil {
139
log.Fatalf("Failed to create Klex client")
139
log.Fatalf("Failed to create Klex client")
140
}
140
}
141
141
142
// Create MessagesRequest objects.
142
// Create MessagesRequest objects.
143
req := api.MessagesRequest{
143
req := api.MessagesRequest{
144
Model: *model,
144
Model: *model,
145
System: string(readFile("system_prompt.txt")),
145
System: string(readFile("system_prompt.txt")),
146
}
146
}
147
cases := scanForCases()
147
cases := scanForCases()
148
for i, c := range cases {
148
for i, c := range cases {
149
user := api.ChatMessage{Role: "user"}
149
user := api.ChatMessage{Role: "user"}
150
text := "Case name: " + c.Name + "\n\n"
150
text := "Case name: " + c.Name + "\n\n"
151
for _, suffix := range c.Before {
151
for _, suffix := range c.Before {
152
switch suffix {
152
switch suffix {
153
case "txt":
153
case "txt":
154
text += string(readFile(c.Name + ".txt"))
154
text += string(readFile(c.Name + ".txt"))
155
case "json":
155
case "json":
156
text += "\n\n```json\n"
156
text += "\n\n```json\n"
157
text += string(readFile(c.Name + ".json"))
157
text += string(readFile(c.Name + ".json"))
158
text += "\n```\n"
158
text += "\n```\n"
159
case "jpg", "jpeg", "png", "webp":
159
case "jpg", "jpeg", "png", "webp":
160
bytes := readFile(c.Name + "." + suffix)
160
bytes := readFile(c.Name + "." + suffix)
161
user.Content = append(user.Content, api.ContentBlock{
161
user.Content = append(user.Content, api.ContentBlock{
162
Type: "image",
162
Type: "image",
163
Source: &api.ContentSource{
163
Source: &api.ContentSource{
164
Type: "base64",
164
Type: "base64",
165
MediaType: http.DetectContentType(bytes),
165
MediaType: http.DetectContentType(bytes),
166
Data: base64.StdEncoding.EncodeToString(bytes),
166
Data: base64.StdEncoding.EncodeToString(bytes),
167
},
167
},
168
})
168
})
169
default:
169
default:
170
log.Fatalf("Unsupported suffix %s in case %s", suffix, c.Name)
170
log.Fatalf("Unsupported suffix %s in case %s", suffix, c.Name)
171
}
171
}
172
}
172
}
173
user.Content = append(user.Content, api.ContentBlock{
173
user.Content = append(user.Content, api.ContentBlock{
174
Type: "text",
174
Type: "text",
175
Text: text,
175
Text: text,
176
})
176
})
177
req.Messages = append(req.Messages, user)
177
req.Messages = append(req.Messages, user)
178
if c.After != "" {
178
if c.After != "" {
179
asst := api.ChatMessage{Role: "assistant"}
179
asst := api.ChatMessage{Role: "assistant"}
180
asst.Content = append(asst.Content, api.ContentBlock{
180
asst.Content = append(asst.Content, api.ContentBlock{
181
Type: "text",
181
Type: "text",
182
Text: string(readFile(c.Name + ".out")),
182
Text: string(readFile(c.Name + ".out")),
183
})
183
})
184
req.Messages = append(req.Messages, asst)
184
req.Messages = append(req.Messages, asst)
185
} else {
185
} else {
186
cases[i].Request = copyRequest(req)
186
cases[i].Request = copyRequest(req)
187
req.Messages = req.Messages[:len(req.Messages)-1]
187
req.Messages = req.Messages[:len(req.Messages)-1]
188
}
188
}
189
}
189
}
190
190
191
if *dry_run {
191
if *dry_run {
192
log.Printf("Dry run; not sending requests.")
192
log.Printf("Dry run; not sending requests.")
193
return
193
return
194
}
194
}
195
195
196
// Send 'em.
196
// Send 'em.
197
// TODO: parallelize
197
// TODO: parallelize
198
for _, c := range cases {
198
for _, c := range cases {
199
if c.Request == nil {
199
if c.Request == nil {
200
continue
200
continue
201
}
201
}
202
if *debug {
202
if *debug {
203
log.Printf("Case %s: sending request:", c.Name)
203
log.Printf("Case %s: sending request:", c.Name)
204
enc := json.NewEncoder(os.Stderr)
204
enc := json.NewEncoder(os.Stderr)
205
enc.SetIndent("", " ")
205
enc.SetIndent("", " ")
206
enc.Encode(c.Request)
206
enc.Encode(c.Request)
207
}
207
}
208
res, err := client.Messages(*c.Request)
208
res, err := client.Messages(*c.Request)
209
if err != nil {
209
if err != nil {
210
log.Printf("Case %s: request failed: %+v", c.Name, err)
210
log.Printf("Case %s: request failed: %+v", c.Name, err)
211
continue
211
continue
212
}
212
}
213
if len(res.Content) != 1 {
213
if len(res.Content) != 1 {
214
log.Printf("Case %s: empty response", c.Name)
214
log.Printf("Case %s: empty response", c.Name)
215
continue
215
continue
216
}
216
}
217
c0 := res.Content[0]
217
c0 := res.Content[0]
218
if c0.Type != "text" {
218
if c0.Type != "text" {
219
log.Printf("Case %s: Content[0].Type = %s", c.Name, c0.Type)
219
log.Printf("Case %s: Content[0].Type = %s", c.Name, c0.Type)
220
continue
220
continue
221
}
221
}
222
err = writeFile(c.Name + ".out", append([]byte(c0.Text), '\n'))
222
err = writeFile(c.Name + ".out", append([]byte(c0.Text), '\n'))
223
if err != nil {
223
if err != nil {
224
log.Printf("Case %s: failed to write %s.out: %+v", c.Name, c.Name, err)
224
log.Printf("Case %s: failed to write %s.out: %+v", c.Name, c.Name, err)
225
continue
225
continue
226
}
226
}
227
log.Printf("Case %s: wrote %s.out", c.Name, c.Name)
227
log.Printf("Case %s: wrote %s.out", c.Name, c.Name)
228
}
228
}
229
}
229
}