Skip to content

Commit 085ee6b

Browse files
committed
add config in repo with ledger
1 parent 1885536 commit 085ee6b

File tree

6 files changed

+159
-78
lines changed

6 files changed

+159
-78
lines changed

app/bot/bot.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ type Opts struct {
2323
Github struct {
2424
URL string `long:"url" env:"URL" required:"true" description:"github repo url"`
2525
Token string `long:"token" env:"TOKEN" required:"true" description:"fine-grained personal access tokens for repo with RW Contents scope"`
26-
MainLedgerFile string `long:"main-ledger-file" env:"MAIN_LEDGER_FILE" required:"true" description:"main ledger file path from the repo root"`
2726
} `group:"github" namespace:"github" env-namespace:"GITHUB"`
2827

2928
OpenAI struct {
@@ -50,7 +49,7 @@ func NewBot(opts *Opts) (*Bot, error) {
5049
rs := repo.NewInMemoryRepo(opts.Github.URL, opts.Github.Token)
5150
llmGenerator := ledger.NewOpenAITransactionGenerator(opts.OpenAI.Token)
5251

53-
ldgr := ledger.NewLedger(rs, llmGenerator, opts.Github.MainLedgerFile, true)
52+
ldgr := ledger.NewLedger(rs, llmGenerator)
5453
tel := teledger.NewTeledger(ldgr)
5554

5655
return &Bot{

app/ledger/ledger.go

+108-49
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,55 @@ import (
88
"os"
99
"os/exec"
1010
"strings"
11-
"time"
1211
"text/template"
12+
"time"
13+
14+
"bytes"
15+
"context"
16+
_ "embed"
17+
"encoding/json"
1318

19+
"github.com/dustin/go-humanize"
1420
"github.com/mput/teledger/app/repo"
1521
"github.com/mput/teledger/app/utils"
16-
"github.com/dustin/go-humanize"
1722
openai "github.com/sashabaranov/go-openai"
18-
_ "embed"
19-
"context"
20-
"bytes"
21-
"encoding/json"
23+
"gopkg.in/yaml.v3"
2224
)
2325

2426
// Ledger is a wrapper around the ledger command line tool
2527
type Ledger struct {
2628
repo repo.Service
27-
mainFile string
28-
strict bool
29+
// mainFile string
30+
// strict bool
2931
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"`
3046
}
3147

32-
func NewLedger(rs repo.Service,gen TransactionGenerator , mainFile string, strict bool) *Ledger {
48+
func NewLedger(rs repo.Service, gen TransactionGenerator) *Ledger {
3349
return &Ledger{
34-
repo: rs,
50+
repo: rs,
3551
generator: gen,
36-
mainFile: mainFile,
37-
strict: strict,
3852
}
3953
}
4054

4155
const ledgerBinary = "ledger"
4256

43-
4457
//go:embed templates/default_prompt.txt
4558
var defaultPromtpTemplate string
4659

47-
4860
func resolveIncludesReader(rs repo.Service, file string) (io.ReadCloser, error) {
4961
ledgerFile, err := rs.Open(file)
5062
if err != nil {
@@ -99,27 +111,27 @@ func resolveIncludesReader(rs repo.Service, file string) (io.ReadCloser, error)
99111
}
100112

101113
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)
103115

104116
if err != nil {
105117
return "", fmt.Errorf("ledger file opening error: %v", err)
106118
}
107119

108120
if additional != "" {
109-
r = utils.MultiReadCloser(r, io.NopCloser( strings.NewReader(additional) ))
121+
r = utils.MultiReadCloser(r, io.NopCloser(strings.NewReader(additional)))
110122
}
111123

112124
fargs := []string{"-f", "-"}
113-
if l.strict {
125+
if l.Config.StrictMode {
114126
fargs = append(fargs, "--pedantic")
115127
}
116128
fargs = append(fargs, args...)
117129

118-
cmddir, err := os.MkdirTemp("", "ledger")
130+
cmddir, err := os.MkdirTemp("", "ledger")
119131
if err != nil {
120132
return "", fmt.Errorf("ledger temp dir creation error: %v", err)
121133
}
122-
defer os.RemoveAll(cmddir)
134+
defer os.RemoveAll(cmddir)
123135

124136
cmd := exec.Command(ledgerBinary, fargs...)
125137

@@ -154,14 +166,16 @@ func (l *Ledger) execute(args ...string) (string, error) {
154166
return l.executeWith("", args...)
155167
}
156168

157-
158-
159169
func (l *Ledger) Execute(args ...string) (string, error) {
160170
err := l.repo.Init()
161171
defer l.repo.Free()
162172
if err != nil {
163173
return "", fmt.Errorf("unable to init repo: %v", err)
164174
}
175+
err = l.setConfig()
176+
if err != nil {
177+
return "", fmt.Errorf("unable to set config: %v", err)
178+
}
165179

166180
return l.execute(args...)
167181
}
@@ -182,7 +196,7 @@ func (l *Ledger) addTransaction(transaction string) error {
182196
return fmt.Errorf("invalid transaction: transaction doesn't change balance")
183197
}
184198

185-
r, err := l.repo.OpenForAppend(l.mainFile)
199+
r, err := l.repo.OpenForAppend(l.Config.MainFile)
186200
if err != nil {
187201
return fmt.Errorf("unable to open main ledger file: %v", err)
188202
}
@@ -203,6 +217,11 @@ func (l *Ledger) AddTransaction(transaction string) error {
203217
if err != nil {
204218
return fmt.Errorf("unable to init repo: %v", err)
205219
}
220+
err = l.setConfig()
221+
if err != nil {
222+
return fmt.Errorf("unable to set config: %v", err)
223+
}
224+
206225
return l.addTransaction(transaction)
207226
}
208227

@@ -235,8 +254,12 @@ func (l *Ledger) AddComment(comment string) (string, error) {
235254
if err != nil {
236255
return "", fmt.Errorf("unable to init repo: %v", err)
237256
}
257+
err = l.setConfig()
258+
if err != nil {
259+
return "", fmt.Errorf("unable to set config: %v", err)
260+
}
238261

239-
r, err := l.repo.OpenForAppend(l.mainFile)
262+
r, err := l.repo.OpenForAppend(l.Config.MainFile)
240263
if err != nil {
241264
return "", fmt.Errorf("unable to open main ledger file: %v", err)
242265
}
@@ -247,7 +270,6 @@ func (l *Ledger) AddComment(comment string) (string, error) {
247270
return "", fmt.Errorf("empty comment provided")
248271
}
249272

250-
251273
_, err = fmt.Fprintf(r, "\n%s\n", res)
252274

253275
if err != nil {
@@ -270,26 +292,26 @@ func (l *Ledger) AddComment(comment string) (string, error) {
270292

271293
// Transaction represents a single transaction in a ledger.
272294
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
277299
RealDateTime time.Time
278300
}
279301

280302
func (t *Transaction) Format(withComment bool) string {
281303
var res strings.Builder
282304
if withComment {
283305
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)),
285307
)
286308
res.WriteString("\n")
287309
}
288310
res.WriteString(fmt.Sprintf("%s * %s\n", t.RealDateTime.Format("2006-01-02"), t.Description))
289311
for _, p := range t.Postings {
290312
// format float to 2 decimal places
291313
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))
293315

294316
}
295317
return res.String()
@@ -304,6 +326,7 @@ type Posting struct {
304326

305327
// TransactionGenerator is an interface for generating transactions from user input
306328
// using LLM.
329+
//
307330
//go:generate moq -out transaction_generator_mock.go -with-resets . TransactionGenerator
308331
type TransactionGenerator interface {
309332
GenerateTransaction(promptCtx PromptCtx) (Transaction, error)
@@ -329,7 +352,6 @@ func (b OpenAITransactionGenerator) GenerateTransaction(promptCtx PromptCtx) (Tr
329352

330353
prompt := buf.String()
331354

332-
333355
resp, err := b.openai.CreateChatCompletion(
334356
context.Background(),
335357
openai.ChatCompletionRequest{
@@ -384,7 +406,7 @@ func parseCommodityOrAccount(ledger io.Reader, directive string) ([]string, erro
384406
}
385407

386408
func (l *Ledger) extractAccounts() ([]string, error) {
387-
r, err := resolveIncludesReader(l.repo, l.mainFile)
409+
r, err := resolveIncludesReader(l.repo, l.Config.MainFile)
388410
if err != nil {
389411
return nil, err
390412
}
@@ -394,14 +416,12 @@ func (l *Ledger) extractAccounts() ([]string, error) {
394416
return nil, fmt.Errorf("unable to extract accounts from directives: %v", err)
395417
}
396418

397-
398419
accsFromTrxsS, err := l.execute("accounts")
399420
if err != nil {
400421
return nil, fmt.Errorf("unable to extract accounts from transactions: %v", err)
401422
}
402423
accsFromTrxs := strings.Split(strings.TrimSpace(accsFromTrxsS), "\n")
403424

404-
405425
accs = append(accs, accsFromTrxs...)
406426
accsdedup := make([]string, 0)
407427
accsmap := make(map[string]struct{})
@@ -415,7 +435,7 @@ func (l *Ledger) extractAccounts() ([]string, error) {
415435
}
416436

417437
func (l *Ledger) extractCommodities() ([]string, error) {
418-
r, err := resolveIncludesReader(l.repo, l.mainFile)
438+
r, err := resolveIncludesReader(l.repo, l.Config.MainFile)
419439
if err != nil {
420440
return nil, err
421441
}
@@ -425,14 +445,12 @@ func (l *Ledger) extractCommodities() ([]string, error) {
425445
return nil, fmt.Errorf("unable to extract accounts from directives: %v", err)
426446
}
427447

428-
429448
comsFromTrxsS, err := l.execute("commodities")
430449
if err != nil {
431450
return nil, fmt.Errorf("unable to extract accounts from transactions: %v", err)
432451
}
433452
comsFromTrxs := strings.Split(strings.TrimSpace(comsFromTrxsS), "\n")
434453

435-
436454
coms = append(coms, comsFromTrxs...)
437455
dedup := make([]string, 0)
438456
dedupm := make(map[string]struct{})
@@ -445,7 +463,6 @@ func (l *Ledger) extractCommodities() ([]string, error) {
445463
return dedup, nil
446464
}
447465

448-
449466
// Receive a short free-text description of a transaction
450467
// and returns a formatted transaction validated with the
451468
// ledger file.
@@ -460,12 +477,11 @@ func (l *Ledger) proposeTransaction(userInput string) (Transaction, error) {
460477
return Transaction{}, err
461478
}
462479

463-
464480
promptCtx := PromptCtx{
465-
Accounts: accounts,
481+
Accounts: accounts,
466482
Commodities: commodities,
467-
UserInput: userInput,
468-
Datetime: time.Now(),
483+
UserInput: userInput,
484+
Datetime: time.Now(),
469485
}
470486

471487
trx, err := l.generator.GenerateTransaction(promptCtx)
@@ -482,13 +498,59 @@ func (l *Ledger) proposeTransaction(userInput string) (Transaction, error) {
482498

483499
}
484500

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) {
486544
wasGenerated = false
487545
err = l.repo.Init()
488546
defer l.repo.Free()
489547
if err != nil {
490548
return wasGenerated, tr, err
491549
}
550+
err = l.setConfig()
551+
if err != nil {
552+
return wasGenerated, tr, fmt.Errorf("unable to set config: %v", err)
553+
}
492554

493555
// first try to add userInput as transaction
494556
err = l.addTransaction(userInput)
@@ -523,12 +585,9 @@ func (l *Ledger) AddOrProposeTransaction(userInput string, attempts int) (wasGen
523585
return wasGenerated, tr, err
524586
}
525587

526-
527588
type PromptCtx struct {
528-
Accounts []string
589+
Accounts []string
529590
Commodities []string
530-
UserInput string
531-
Datetime time.Time
591+
UserInput string
592+
Datetime time.Time
532593
}
533-
534-

0 commit comments

Comments
 (0)