Skip to content

Commit

Permalink
[WIP] transaction generator
Browse files Browse the repository at this point in the history
  • Loading branch information
mput committed Jun 22, 2024
1 parent 16eadcf commit 4a647e0
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 8 deletions.
2 changes: 1 addition & 1 deletion app/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func NewBot(opts *Opts) (*Bot, error) {
}

rs := repo.NewInMemoryRepo(opts.Github.URL, opts.Github.Token)
ldgr := ledger.NewLedger(rs, opts.Github.MainLedgerFile, true)
ldgr := ledger.NewLedger(rs, nil, opts.Github.MainLedgerFile, true)
tel := teledger.NewTeledger(ldgr)

return &Bot{
Expand Down
114 changes: 113 additions & 1 deletion app/ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"strings"
"time"

"github.com/mput/teledger/app/repo"
)
Expand All @@ -17,11 +18,13 @@ type Ledger struct {
repo repo.Service
mainFile string
strict bool
generator TransactionGenerator
}

func NewLedger(rs repo.Service, mainFile string, strict bool) *Ledger {
func NewLedger(rs repo.Service,gen TransactionGenerator , mainFile string, strict bool) *Ledger {
return &Ledger{
repo: rs,
generator: gen,
mainFile: mainFile,
strict: strict,
}
Expand Down Expand Up @@ -223,3 +226,112 @@ func (l *Ledger) AddComment(comment string) (string, error) {
}
return res, nil
}

// Transaction represents a single transaction in a ledger.
type Transaction struct {
Date time.Time `json:"date"` // The date of the transaction
Description string `json:"description"` // A description of the transaction
Postings []Posting `json:"postings"` // A slice of postings that belong to this transaction
}

func (t Transaction) toString() string {
var res strings.Builder
res.WriteString(fmt.Sprintf("%s %s\n", t.Date.Format("2006-01-02"), t.Description))
for _, p := range t.Postings {
// format float to 2 decimal places
res.WriteString(fmt.Sprintf(" %s %8.2f %s\n", p.Account, p.Amount, p.Currency))

}
return res.String()
}

// Posting represents a single posting in a transaction, linking an account with an amount and currency.
type Posting struct {
Account string `json:"account"` // The name of the account
Amount float64 `json:"amount"` // The amount posted to the account
Currency string `json:"currency"` // The currency of the amount
}

// TransactionGenerator is an interface for generating transactions from user input
// using LLM.
//go:generate moq -out transaction_generator_mock.go -with-resets . TransactionGenerator
type TransactionGenerator interface {
GenerateTransaction(promptCtx PromptCtx) (Transaction, error)
}

type OpenAITransactionGenerator struct {
token string
}

func (b OpenAITransactionGenerator) GenerateTransaction(promptCtx PromptCtx) (Transaction, error) {
return Transaction{}, nil
}


// Receive a short free-text description of a transaction
// and returns a formatted transaction validated with the
// ledger file.
func (l *Ledger) ProposeTransaction(userInput string) (string, error) {
err := l.repo.Init()
defer l.repo.Free()
if err != nil {
return "", err
}

accounts := []string{"Assets:Cards:Wise-EUR", "Assets:Cards:Wise-USD", "Assets:Cards:Wise-RUB"}

promptCtx := PromptCtx{
Accounts: accounts,
UserInput: userInput,
}

trx, err := l.generator.GenerateTransaction(promptCtx)
if err != nil {
return "", fmt.Errorf("unable to generate transaction: %v", err)
}

return trx.toString(), nil

}


type PromptCtx struct {
Accounts []string
UserInput string
}


const template = `
Your goal is to propose a transaction in the ledger cli format.
Your responses MUST be in JSON and adhere to the Transaction struct ONLY with no additional narrative or markup, backquotes or anything.
Bellow is the list of accounts you MUST use in your transaction:
{{range .Accounts}}
"{{.}}"
{{end}}
Use "EUR" as the default currency if nothing else is specified in user request.
Another possible currency is "USD", "RUB".
All descriptions should be in English.
// Transaction represents a single transaction in a ledger.
type Transaction struct {
Date time.Time json:"date" // The date of the transaction
Description string json:"description" // A description of the transaction
Postings []Posting json:"postings" // A slice of postings that belong to this transaction
}
// Posting represents a single posting in a transaction, linking an account with an amount and currency.
type Posting struct {
Account string json:"account" // The name of the account
Amount float64 json:"amount" // The amount posted to the account
Currency string json:"currency" // The currency of the amount
}
Use Assets:Cards:Wise-EUR as default account if nothing else is specified in user request
Bellow is the message from the USER for which you need to propose a transaction:
{{.UserInput}}
`
79 changes: 73 additions & 6 deletions app/ledger/ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package ledger
import (
"strings"
"testing"
"time"

"github.com/mput/teledger/app/repo"
"github.com/stretchr/testify/assert"
)

func TestLedger_Execute(t *testing.T) {
Expand All @@ -20,6 +22,7 @@ func TestLedger_Execute(t *testing.T) {

ledger := NewLedger(
&repo.Mock{Files: map[string]string{"main.ledger": testFile}},
nil,
"main.ledger",
false,
)
Expand Down Expand Up @@ -67,7 +70,7 @@ commodity EUR
Files: files,
}

ledger := NewLedger(repomock, "main.ledger", true)
ledger := NewLedger(repomock, nil, "main.ledger", true)

res, err := ledger.Execute("bal")

Expand All @@ -89,7 +92,6 @@ commodity EUR

}


func TestLedger_AddTransaction(t *testing.T) {
t.Run("success path", func(t *testing.T) {
t.Parallel()
Expand All @@ -102,11 +104,11 @@ func TestLedger_AddTransaction(t *testing.T) {

ledger := NewLedger(
&repo.Mock{Files: map[string]string{"main.ledger": testFile}},
nil,
"main.ledger",
false,
)


err := ledger.AddTransaction(`
2024-02-14 * Test
Assets:Cash 42.00 EUR
Expand All @@ -133,15 +135,13 @@ func TestLedger_AddTransaction(t *testing.T) {
t.Fatalf("Expected: '%s', got: '%s'", expected, res)
}


err = ledger.AddTransaction(`
dummy
`)
if err == nil {
t.Fatalf("Expected error")
}


err = ledger.AddTransaction(`
dummy dummy
`)
Expand All @@ -154,7 +154,6 @@ dummy dummy
t.Fatalf("Expected error")
}


err = ledger.AddTransaction(`
`)
Expand All @@ -164,3 +163,71 @@ dummy dummy

})
}


func TestLedger_ProposeTransaction(t *testing.T) {
t.Run("happy path", func(t *testing.T) {

mockedTransactionGenerator := &TransactionGeneratorMock{
GenerateTransactionFunc: func(promptCtx PromptCtx) (Transaction, error) {
dt, _ := time.Parse(time.RFC3339, "2014-11-12T11:45:26.371Z")
tr := Transaction{
Date: dt,
Description: "My tr",
Postings: []Posting{
Posting{
Account: "cash",
Amount: -30.43,
Currency: "EUR",
},
Posting{
Account: "taxi",
Amount: 30.43,
Currency: "EUR",
},
},
}
return tr, nil
},
}

const testFile = `
2024-02-13 * Test
Assets:Cash 100.00 EUR
Equity
`

ledger := NewLedger(
&repo.Mock{Files: map[string]string{"main.ledger": testFile}},
mockedTransactionGenerator,
"main.ledger",
false,
)

tr, err := ledger.ProposeTransaction("20 Taco Bell")


assert.NoError(t, err)

assert.Equal(t,
`2014-11-12 My tr
cash -30.43 EUR
taxi 30.43 EUR
`,
tr,
)

assert.Equal(t, len(mockedTransactionGenerator.calls.GenerateTransaction), 1)


assert.Equal(t,
mockedTransactionGenerator.calls.GenerateTransaction[0].PromptCtx,
PromptCtx{
Accounts: []string{"Assets:Cards:Wise-EUR", "Assets:Cards:Wise-USD", "Assets:Cards:Wise-RUB"},
UserInput: "20 Taco Bell",
},
)


})
}
88 changes: 88 additions & 0 deletions app/ledger/transaction_generator_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions app/teledger/teledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ func (tel *Teledger) AddComment(comment string) (string, error) {
func (tel *Teledger) Balance() (string, error) {
return tel.ledger.Execute("bal")
}


// // Receive a short free-text description of a transaction
// // and propose a formatted transaction validated with the
// // ledger file.
// // Store the transaction in a state, so the user can confirm
// // or reject it.
// func (tel *Teledger) proposeTransaction(desc string) (string, error) {
// prompt := fmt.Sprintf("Proposed transaction:\n```\n%s\n```", desc)
// structuredTrx, err := tel.openai.GetStructuredResponse(desc, &transaction{})
// trx := structuredTrx.toString()

// }
Loading

0 comments on commit 4a647e0

Please sign in to comment.