Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
kardolus
GitHub Repository: kardolus/chatgpt-cli
Path: blob/main/config/store.go
2649 views
1
package config
2
3
import (
4
"fmt"
5
"github.com/kardolus/chatgpt-cli/internal"
6
"gopkg.in/yaml.v3"
7
"os"
8
"path/filepath"
9
"reflect"
10
"strconv"
11
"strings"
12
)
13
14
const (
15
openAIName = "openai"
16
openAIModel = "gpt-4o"
17
openAIMaxTokens = 4096
18
openAIContextWindow = 8192
19
openAIURL = "https://api.openai.com"
20
openAICompletionsPath = "/v1/chat/completions"
21
openAIResponsesPath = "/v1/responses"
22
openAITranscriptionsPath = "/v1/audio/transcriptions"
23
openAISpeechPath = "/v1/audio/speech"
24
openAIImageGenerationsPath = "/v1/images/generations"
25
openAIImageEditsPath = "/v1/images/edits"
26
openAIModelsPath = "/v1/models"
27
openAIAuthHeader = "Authorization"
28
openAIAuthTokenPrefix = "Bearer "
29
openAIRole = "You are a helpful assistant."
30
openAIThread = "default"
31
openAITemperature = 1.0
32
openAITopP = 1.0
33
openAIFrequencyPenalty = 0.0
34
openAIPresencePenalty = 0.0
35
openAICommandPrompt = "[%datetime] [Q%counter]"
36
openAIEffort = "low"
37
openAIVoice = "voice"
38
)
39
40
type Store interface {
41
Delete(string) error
42
List() ([]string, error)
43
Read() (Config, error)
44
ReadDefaults() Config
45
Write(Config) error
46
}
47
48
// FileNotFoundError is a custom error type for non-existent files
49
type FileNotFoundError struct {
50
Path string
51
}
52
53
func (e *FileNotFoundError) Error() string {
54
return fmt.Sprintf("no threads matched the pattern %s", e.Path)
55
}
56
57
// Ensure FileIO implements ConfigStore interface
58
var _ Store = &FileIO{}
59
60
type FileIO struct {
61
configFilePath string
62
historyFilePath string
63
}
64
65
func NewStore() *FileIO {
66
configPath, _ := getPath()
67
historyPath, _ := internal.GetDataHome()
68
69
return &FileIO{
70
configFilePath: configPath,
71
historyFilePath: historyPath,
72
}
73
}
74
75
func (f *FileIO) WithConfigPath(configFilePath string) *FileIO {
76
f.configFilePath = configFilePath
77
return f
78
}
79
80
func (f *FileIO) WithHistoryPath(historyPath string) *FileIO {
81
f.historyFilePath = historyPath
82
return f
83
}
84
85
func (f *FileIO) Delete(pattern string) error {
86
if !strings.HasSuffix(pattern, "*") && !strings.HasSuffix(pattern, ".json") {
87
pattern += ".json"
88
} else if strings.HasSuffix(pattern, "*") {
89
pattern += ".json"
90
}
91
92
fullPattern := filepath.Join(f.historyFilePath, pattern)
93
94
matches, err := filepath.Glob(fullPattern)
95
if err != nil {
96
return fmt.Errorf("failed to process pattern %s: %w", fullPattern, err)
97
}
98
99
if len(matches) == 0 {
100
return &FileNotFoundError{Path: fullPattern}
101
}
102
103
for _, path := range matches {
104
if err := os.Remove(path); err != nil {
105
return fmt.Errorf("failed to delete file %s: %w", path, err)
106
}
107
}
108
109
return nil
110
}
111
112
func (f *FileIO) List() ([]string, error) {
113
var result []string
114
115
files, err := os.ReadDir(f.historyFilePath)
116
if err != nil {
117
return nil, err
118
}
119
120
for _, file := range files {
121
result = append(result, file.Name())
122
}
123
124
return result, nil
125
}
126
127
func (f *FileIO) Read() (Config, error) {
128
result, err := parseFile(f.configFilePath)
129
if err != nil {
130
return Config{}, err
131
}
132
133
return migrate(result), nil
134
}
135
136
func (f *FileIO) ReadDefaults() Config {
137
return Config{
138
Name: openAIName,
139
Model: openAIModel,
140
Role: openAIRole,
141
MaxTokens: openAIMaxTokens,
142
ContextWindow: openAIContextWindow,
143
URL: openAIURL,
144
CompletionsPath: openAICompletionsPath,
145
ResponsesPath: openAIResponsesPath,
146
TranscriptionsPath: openAITranscriptionsPath,
147
SpeechPath: openAISpeechPath,
148
ImageGenerationsPath: openAIImageGenerationsPath,
149
ImageEditsPath: openAIImageEditsPath,
150
ModelsPath: openAIModelsPath,
151
AuthHeader: openAIAuthHeader,
152
AuthTokenPrefix: openAIAuthTokenPrefix,
153
Thread: openAIThread,
154
Temperature: openAITemperature,
155
TopP: openAITopP,
156
FrequencyPenalty: openAIFrequencyPenalty,
157
PresencePenalty: openAIPresencePenalty,
158
CommandPrompt: openAICommandPrompt,
159
Effort: openAIEffort,
160
Voice: openAIVoice,
161
}
162
}
163
164
func (f *FileIO) Write(config Config) error {
165
rootNode, err := f.readNode()
166
167
// If readNode returns an error or there was a problem reading the rootNode, initialize a new rootNode.
168
if err != nil || rootNode.Kind == 0 {
169
rootNode = yaml.Node{Kind: yaml.DocumentNode}
170
rootNode.Content = append(rootNode.Content, &yaml.Node{Kind: yaml.MappingNode})
171
}
172
173
updateNodeFromConfig(&rootNode, config)
174
175
modifiedContent, err := yaml.Marshal(&rootNode)
176
if err != nil {
177
return err
178
}
179
180
return os.WriteFile(f.configFilePath, modifiedContent, 0644)
181
}
182
183
func (f *FileIO) readNode() (yaml.Node, error) {
184
var rootNode yaml.Node
185
186
content, err := os.ReadFile(f.configFilePath)
187
if err != nil {
188
return rootNode, err
189
}
190
191
if err := yaml.Unmarshal(content, &rootNode); err != nil {
192
return rootNode, err
193
}
194
195
return rootNode, nil
196
}
197
198
func getPath() (string, error) {
199
homeDir, err := internal.GetConfigHome()
200
if err != nil {
201
return "", err
202
}
203
204
return filepath.Join(homeDir, "config.yaml"), nil
205
}
206
207
func migrate(config Config) Config {
208
// the "old" max_tokens became context_window
209
if config.ContextWindow == 0 && config.MaxTokens > 0 {
210
config.ContextWindow = config.MaxTokens
211
// set it to the default in case the value is small
212
if config.ContextWindow < openAIContextWindow {
213
config.ContextWindow = openAIContextWindow
214
}
215
config.MaxTokens = openAIMaxTokens
216
}
217
return config
218
}
219
220
func parseFile(fileName string) (Config, error) {
221
var result Config
222
223
buf, err := os.ReadFile(fileName)
224
if err != nil {
225
return Config{}, err
226
}
227
228
if err := yaml.Unmarshal(buf, &result); err != nil {
229
return Config{}, err
230
}
231
232
return result, nil
233
}
234
235
// updateNodeFromConfig updates the specified yaml.Node with values from the Config struct.
236
// It uses reflection to match struct fields with YAML tags, updating the node accordingly.
237
func updateNodeFromConfig(node *yaml.Node, config Config) {
238
t := reflect.TypeOf(config)
239
v := reflect.ValueOf(config)
240
241
for i := 0; i < t.NumField(); i++ {
242
field := t.Field(i)
243
value := v.Field(i)
244
yamlTag := field.Tag.Get("yaml")
245
246
if yamlTag == "" || yamlTag == "-" {
247
continue // Skip fields without yaml tag or marked to be ignored
248
}
249
250
// Convert value to string; adjust for different data types as needed
251
var strValue string
252
switch value.Kind() {
253
case reflect.String:
254
strValue = value.String()
255
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
256
strValue = strconv.FormatInt(value.Int(), 10)
257
case reflect.Float32, reflect.Float64:
258
strValue = strconv.FormatFloat(value.Float(), 'f', -1, 64)
259
case reflect.Bool:
260
strValue = strconv.FormatBool(value.Bool())
261
default:
262
continue // Skip unsupported types for simplicity
263
}
264
265
setField(node, yamlTag, strValue)
266
}
267
}
268
269
// setField either updates an existing field or adds a new field to the YAML mapping node.
270
// It assumes the root node is a DocumentNode containing a MappingNode.
271
func setField(root *yaml.Node, key string, newValue string) {
272
found := false
273
274
if root.Kind == yaml.DocumentNode {
275
root = root.Content[0] // Move from document node to the actual mapping node.
276
}
277
278
if root.Kind != yaml.MappingNode {
279
return // If the root is not a mapping node, we can't do anything.
280
}
281
282
for i := 0; i < len(root.Content); i += 2 {
283
keyNode := root.Content[i]
284
if keyNode.Value == key {
285
valueNode := root.Content[i+1]
286
valueNode.Value = newValue
287
found = true
288
break
289
}
290
}
291
292
if !found { // If the key wasn't found, add it.
293
root.Content = append(root.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, &yaml.Node{Kind: yaml.ScalarNode, Value: newValue})
294
}
295
}
296
297