@@ -8,43 +8,55 @@ import (
8
8
"os"
9
9
"os/exec"
10
10
"strings"
11
- "time"
12
11
"text/template"
12
+ "time"
13
+
14
+ "bytes"
15
+ "context"
16
+ _ "embed"
17
+ "encoding/json"
13
18
19
+ "github.com/dustin/go-humanize"
14
20
"github.com/mput/teledger/app/repo"
15
21
"github.com/mput/teledger/app/utils"
16
- "github.com/dustin/go-humanize"
17
22
openai "github.com/sashabaranov/go-openai"
18
- _ "embed"
19
- "context"
20
- "bytes"
21
- "encoding/json"
23
+ "gopkg.in/yaml.v3"
22
24
)
23
25
24
26
// Ledger is a wrapper around the ledger command line tool
25
27
type Ledger struct {
26
28
repo repo.Service
27
- mainFile string
28
- strict bool
29
+ // mainFile string
30
+ // strict bool
29
31
generator TransactionGenerator
32
+ Config * Config
33
+ }
34
+
35
+ type Report struct {
36
+ Title string
37
+ Command []string
38
+ }
39
+
40
+ type Config struct {
41
+ Reports []Report `yaml:"reports"`
42
+ MainFile string `yaml:"mainFile"`
43
+ PromptTemplate string `yaml:"promptTemplate"`
44
+ StrictMode bool `yaml:"strict"`
45
+ Version string `yaml:"version"`
30
46
}
31
47
32
- func NewLedger (rs repo.Service ,gen TransactionGenerator , mainFile string , strict bool ) * Ledger {
48
+ func NewLedger (rs repo.Service , gen TransactionGenerator ) * Ledger {
33
49
return & Ledger {
34
- repo : rs ,
50
+ repo : rs ,
35
51
generator : gen ,
36
- mainFile : mainFile ,
37
- strict : strict ,
38
52
}
39
53
}
40
54
41
55
const ledgerBinary = "ledger"
42
56
43
-
44
57
//go:embed templates/default_prompt.txt
45
58
var defaultPromtpTemplate string
46
59
47
-
48
60
func resolveIncludesReader (rs repo.Service , file string ) (io.ReadCloser , error ) {
49
61
ledgerFile , err := rs .Open (file )
50
62
if err != nil {
@@ -99,27 +111,27 @@ func resolveIncludesReader(rs repo.Service, file string) (io.ReadCloser, error)
99
111
}
100
112
101
113
func (l * Ledger ) executeWith (additional string , args ... string ) (string , error ) {
102
- r , err := resolveIncludesReader (l .repo , l .mainFile )
114
+ r , err := resolveIncludesReader (l .repo , l .Config . MainFile )
103
115
104
116
if err != nil {
105
117
return "" , fmt .Errorf ("ledger file opening error: %v" , err )
106
118
}
107
119
108
120
if additional != "" {
109
- r = utils .MultiReadCloser (r , io .NopCloser ( strings .NewReader (additional ) ))
121
+ r = utils .MultiReadCloser (r , io .NopCloser (strings .NewReader (additional )))
110
122
}
111
123
112
124
fargs := []string {"-f" , "-" }
113
- if l .strict {
125
+ if l .Config . StrictMode {
114
126
fargs = append (fargs , "--pedantic" )
115
127
}
116
128
fargs = append (fargs , args ... )
117
129
118
- cmddir , err := os .MkdirTemp ("" , "ledger" )
130
+ cmddir , err := os .MkdirTemp ("" , "ledger" )
119
131
if err != nil {
120
132
return "" , fmt .Errorf ("ledger temp dir creation error: %v" , err )
121
133
}
122
- defer os .RemoveAll (cmddir )
134
+ defer os .RemoveAll (cmddir )
123
135
124
136
cmd := exec .Command (ledgerBinary , fargs ... )
125
137
@@ -154,14 +166,16 @@ func (l *Ledger) execute(args ...string) (string, error) {
154
166
return l .executeWith ("" , args ... )
155
167
}
156
168
157
-
158
-
159
169
func (l * Ledger ) Execute (args ... string ) (string , error ) {
160
170
err := l .repo .Init ()
161
171
defer l .repo .Free ()
162
172
if err != nil {
163
173
return "" , fmt .Errorf ("unable to init repo: %v" , err )
164
174
}
175
+ err = l .setConfig ()
176
+ if err != nil {
177
+ return "" , fmt .Errorf ("unable to set config: %v" , err )
178
+ }
165
179
166
180
return l .execute (args ... )
167
181
}
@@ -182,7 +196,7 @@ func (l *Ledger) addTransaction(transaction string) error {
182
196
return fmt .Errorf ("invalid transaction: transaction doesn't change balance" )
183
197
}
184
198
185
- r , err := l .repo .OpenForAppend (l .mainFile )
199
+ r , err := l .repo .OpenForAppend (l .Config . MainFile )
186
200
if err != nil {
187
201
return fmt .Errorf ("unable to open main ledger file: %v" , err )
188
202
}
@@ -203,6 +217,11 @@ func (l *Ledger) AddTransaction(transaction string) error {
203
217
if err != nil {
204
218
return fmt .Errorf ("unable to init repo: %v" , err )
205
219
}
220
+ err = l .setConfig ()
221
+ if err != nil {
222
+ return fmt .Errorf ("unable to set config: %v" , err )
223
+ }
224
+
206
225
return l .addTransaction (transaction )
207
226
}
208
227
@@ -235,8 +254,12 @@ func (l *Ledger) AddComment(comment string) (string, error) {
235
254
if err != nil {
236
255
return "" , fmt .Errorf ("unable to init repo: %v" , err )
237
256
}
257
+ err = l .setConfig ()
258
+ if err != nil {
259
+ return "" , fmt .Errorf ("unable to set config: %v" , err )
260
+ }
238
261
239
- r , err := l .repo .OpenForAppend (l .mainFile )
262
+ r , err := l .repo .OpenForAppend (l .Config . MainFile )
240
263
if err != nil {
241
264
return "" , fmt .Errorf ("unable to open main ledger file: %v" , err )
242
265
}
@@ -247,7 +270,6 @@ func (l *Ledger) AddComment(comment string) (string, error) {
247
270
return "" , fmt .Errorf ("empty comment provided" )
248
271
}
249
272
250
-
251
273
_ , err = fmt .Fprintf (r , "\n %s\n " , res )
252
274
253
275
if err != nil {
@@ -270,26 +292,26 @@ func (l *Ledger) AddComment(comment string) (string, error) {
270
292
271
293
// Transaction represents a single transaction in a ledger.
272
294
type Transaction struct {
273
- Date string `json:"date"` // The date of the transaction
274
- Description string `json:"description"` // A description of the transaction
275
- Postings []Posting `json:"postings"` // A slice of postings that belong to this transaction
276
- Comment string
295
+ Date string `json:"date"` // The date of the transaction
296
+ Description string `json:"description"` // A description of the transaction
297
+ Postings []Posting `json:"postings"` // A slice of postings that belong to this transaction
298
+ Comment string
277
299
RealDateTime time.Time
278
300
}
279
301
280
302
func (t * Transaction ) Format (withComment bool ) string {
281
303
var res strings.Builder
282
304
if withComment {
283
305
res .WriteString (
284
- wrapIntoComment (fmt .Sprintf ("%s: %s" ,t .RealDateTime .Format ("2006-01-02 15:04:05 Monday" ), t .Comment )),
306
+ wrapIntoComment (fmt .Sprintf ("%s: %s" , t .RealDateTime .Format ("2006-01-02 15:04:05 Monday" ), t .Comment )),
285
307
)
286
308
res .WriteString ("\n " )
287
309
}
288
310
res .WriteString (fmt .Sprintf ("%s * %s\n " , t .RealDateTime .Format ("2006-01-02" ), t .Description ))
289
311
for _ , p := range t .Postings {
290
312
// format float to 2 decimal places
291
313
vf := humanize .FormatFloat ("#.###,##" , p .Amount )
292
- res .WriteString (fmt .Sprintf (" %s %s %s\n " ,p .Account , vf , p .Currency ))
314
+ res .WriteString (fmt .Sprintf (" %s %s %s\n " , p .Account , vf , p .Currency ))
293
315
294
316
}
295
317
return res .String ()
@@ -304,6 +326,7 @@ type Posting struct {
304
326
305
327
// TransactionGenerator is an interface for generating transactions from user input
306
328
// using LLM.
329
+ //
307
330
//go:generate moq -out transaction_generator_mock.go -with-resets . TransactionGenerator
308
331
type TransactionGenerator interface {
309
332
GenerateTransaction (promptCtx PromptCtx ) (Transaction , error )
@@ -329,7 +352,6 @@ func (b OpenAITransactionGenerator) GenerateTransaction(promptCtx PromptCtx) (Tr
329
352
330
353
prompt := buf .String ()
331
354
332
-
333
355
resp , err := b .openai .CreateChatCompletion (
334
356
context .Background (),
335
357
openai.ChatCompletionRequest {
@@ -384,7 +406,7 @@ func parseCommodityOrAccount(ledger io.Reader, directive string) ([]string, erro
384
406
}
385
407
386
408
func (l * Ledger ) extractAccounts () ([]string , error ) {
387
- r , err := resolveIncludesReader (l .repo , l .mainFile )
409
+ r , err := resolveIncludesReader (l .repo , l .Config . MainFile )
388
410
if err != nil {
389
411
return nil , err
390
412
}
@@ -394,14 +416,12 @@ func (l *Ledger) extractAccounts() ([]string, error) {
394
416
return nil , fmt .Errorf ("unable to extract accounts from directives: %v" , err )
395
417
}
396
418
397
-
398
419
accsFromTrxsS , err := l .execute ("accounts" )
399
420
if err != nil {
400
421
return nil , fmt .Errorf ("unable to extract accounts from transactions: %v" , err )
401
422
}
402
423
accsFromTrxs := strings .Split (strings .TrimSpace (accsFromTrxsS ), "\n " )
403
424
404
-
405
425
accs = append (accs , accsFromTrxs ... )
406
426
accsdedup := make ([]string , 0 )
407
427
accsmap := make (map [string ]struct {})
@@ -415,7 +435,7 @@ func (l *Ledger) extractAccounts() ([]string, error) {
415
435
}
416
436
417
437
func (l * Ledger ) extractCommodities () ([]string , error ) {
418
- r , err := resolveIncludesReader (l .repo , l .mainFile )
438
+ r , err := resolveIncludesReader (l .repo , l .Config . MainFile )
419
439
if err != nil {
420
440
return nil , err
421
441
}
@@ -425,14 +445,12 @@ func (l *Ledger) extractCommodities() ([]string, error) {
425
445
return nil , fmt .Errorf ("unable to extract accounts from directives: %v" , err )
426
446
}
427
447
428
-
429
448
comsFromTrxsS , err := l .execute ("commodities" )
430
449
if err != nil {
431
450
return nil , fmt .Errorf ("unable to extract accounts from transactions: %v" , err )
432
451
}
433
452
comsFromTrxs := strings .Split (strings .TrimSpace (comsFromTrxsS ), "\n " )
434
453
435
-
436
454
coms = append (coms , comsFromTrxs ... )
437
455
dedup := make ([]string , 0 )
438
456
dedupm := make (map [string ]struct {})
@@ -445,7 +463,6 @@ func (l *Ledger) extractCommodities() ([]string, error) {
445
463
return dedup , nil
446
464
}
447
465
448
-
449
466
// Receive a short free-text description of a transaction
450
467
// and returns a formatted transaction validated with the
451
468
// ledger file.
@@ -460,12 +477,11 @@ func (l *Ledger) proposeTransaction(userInput string) (Transaction, error) {
460
477
return Transaction {}, err
461
478
}
462
479
463
-
464
480
promptCtx := PromptCtx {
465
- Accounts : accounts ,
481
+ Accounts : accounts ,
466
482
Commodities : commodities ,
467
- UserInput : userInput ,
468
- Datetime : time .Now (),
483
+ UserInput : userInput ,
484
+ Datetime : time .Now (),
469
485
}
470
486
471
487
trx , err := l .generator .GenerateTransaction (promptCtx )
@@ -482,13 +498,59 @@ func (l *Ledger) proposeTransaction(userInput string) (Transaction, error) {
482
498
483
499
}
484
500
485
- func (l * Ledger ) AddOrProposeTransaction (userInput string , attempts int ) (wasGenerated bool , tr Transaction ,err error ) {
501
+ func parseConfig (r io.Reader , c * Config ) error {
502
+ err := yaml .NewDecoder (r ).Decode (c )
503
+ if err != nil {
504
+ slog .Warn ("error decoding config file" , "error" , err )
505
+ return err
506
+ }
507
+ return nil
508
+ }
509
+
510
+
511
+ func (l * Ledger ) setConfig () error {
512
+ if l .Config == nil {
513
+ l .Config = & Config {}
514
+ }
515
+ const configFile = "teledger.yaml"
516
+ r , err := l .repo .Open (configFile )
517
+ if err == nil {
518
+ err = parseConfig (r , l .Config )
519
+ if err != nil {
520
+ return err
521
+ }
522
+ } else if ! os .IsNotExist (err ) {
523
+ fmt .Println ("unable to open config file" , "error" , err )
524
+ return err
525
+ }
526
+ // set defaults:
527
+ if l .Config .MainFile == "" {
528
+ l .Config .MainFile = "main.ledger"
529
+ }
530
+
531
+ if l .Config .PromptTemplate == "" {
532
+ l .Config .PromptTemplate = defaultPromtpTemplate
533
+ }
534
+
535
+ if l .Config .Version == "" {
536
+ l .Config .Version = "0"
537
+ }
538
+
539
+ return nil
540
+ }
541
+
542
+
543
+ func (l * Ledger ) AddOrProposeTransaction (userInput string , attempts int ) (wasGenerated bool , tr Transaction , err error ) {
486
544
wasGenerated = false
487
545
err = l .repo .Init ()
488
546
defer l .repo .Free ()
489
547
if err != nil {
490
548
return wasGenerated , tr , err
491
549
}
550
+ err = l .setConfig ()
551
+ if err != nil {
552
+ return wasGenerated , tr , fmt .Errorf ("unable to set config: %v" , err )
553
+ }
492
554
493
555
// first try to add userInput as transaction
494
556
err = l .addTransaction (userInput )
@@ -523,12 +585,9 @@ func (l *Ledger) AddOrProposeTransaction(userInput string, attempts int) (wasGen
523
585
return wasGenerated , tr , err
524
586
}
525
587
526
-
527
588
type PromptCtx struct {
528
- Accounts []string
589
+ Accounts []string
529
590
Commodities []string
530
- UserInput string
531
- Datetime time.Time
591
+ UserInput string
592
+ Datetime time.Time
532
593
}
533
-
534
-
0 commit comments