Skip to content

Commit

Permalink
[WIP] Working integration with openAI
Browse files Browse the repository at this point in the history
  • Loading branch information
mput committed Jun 24, 2024
1 parent 847a19f commit cf9d3c9
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 34 deletions.
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

start:
. ./.env.dev
go run app/main.go

test:
. ./.env.test
go test -v ./...
4 changes: 1 addition & 3 deletions app/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ func NewBot(opts *Opts) (*Bot, error) {
}

rs := repo.NewInMemoryRepo(opts.Github.URL, opts.Github.Token)
llmGenerator := ledger.OpenAITransactionGenerator{
Token: opts.OpenAI.Token,
}
llmGenerator := ledger.NewOpenAITransactionGenerator(opts.OpenAI.Token)

ldgr := ledger.NewLedger(rs, llmGenerator, opts.Github.MainLedgerFile, true)
tel := teledger.NewTeledger(ldgr)
Expand Down
75 changes: 68 additions & 7 deletions app/ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ import (
"os/exec"
"strings"
"time"
"text/template"

"github.com/mput/teledger/app/repo"
"github.com/mput/teledger/app/utils"
"github.com/dustin/go-humanize"
openai "github.com/sashabaranov/go-openai"
_ "embed"
"context"
"bytes"
"encoding/json"
)

// Ledger is a wrapper around the ledger command line tool
Expand All @@ -36,7 +42,7 @@ const ledgerBinary = "ledger"


//go:embed templates/default_prompt.txt
var defaultPrompt string
var defaultPromtpTemplate string


func resolveIncludesReader(rs repo.Service, file string) (io.ReadCloser, error) {
Expand Down Expand Up @@ -250,21 +256,23 @@ func (l *Ledger) AddComment(comment string) (string, error) {

// Transaction represents a single transaction in a ledger.
type Transaction struct {
Date time.Time `json:"date"` // The date of the transaction
Date string `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
Comment string
RealDateTime time.Time
}

func (t *Transaction) ToString() string {
var res strings.Builder
if t.Comment != "" {
res.WriteString(fmt.Sprintf(";; %s: %s\n",t.Date.Format("2006-01-02 15:04:05 Monday"), t.Comment))
res.WriteString(fmt.Sprintf(";; %s: %s\n",t.RealDateTime.Format("2006-01-02 15:04:05 Monday"), t.Comment))
}
res.WriteString(fmt.Sprintf("%s * %s\n", t.Date.Format("2006-01-02"), t.Description))
res.WriteString(fmt.Sprintf("%s * %s\n", t.RealDateTime.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))
vf := humanize.FormatFloat("#.###,##", p.Amount)
res.WriteString(fmt.Sprintf(" %s %s %s\n",p.Account, vf, p.Currency))

}
return res.String()
Expand All @@ -285,11 +293,60 @@ type TransactionGenerator interface {
}

type OpenAITransactionGenerator struct {
Token string
openai *openai.Client
}

func NewOpenAITransactionGenerator(token string) *OpenAITransactionGenerator {
return &OpenAITransactionGenerator{
openai: openai.NewClient(token),
}
}

func (b OpenAITransactionGenerator) GenerateTransaction(promptCtx PromptCtx) (Transaction, error) {
return Transaction{}, nil
var buf bytes.Buffer
prTmp := template.Must(template.New("letter").Parse(defaultPromtpTemplate))
err := prTmp.Execute(&buf, promptCtx)
if err != nil {
return Transaction{}, fmt.Errorf("unable to execute template: %v", err)
}

prompt := buf.String()


resp, err := b.openai.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: prompt,
},
{
Role: openai.ChatMessageRoleUser,
Content: promptCtx.UserInput,
},
},
},
)

if err != nil {
fmt.Println("ChatCompletion error: ", err)
return Transaction{}, fmt.Errorf("chatCompletion error: %v", err)
}

fmt.Println(resp.Choices[0].Message.Content)

res := Transaction{}
err = json.Unmarshal([]byte(resp.Choices[0].Message.Content), &res)
if err != nil {
return Transaction{}, fmt.Errorf("unable to unmarshal response: %v", err)
}

res.Comment = promptCtx.UserInput
res.RealDateTime = promptCtx.Datetime

return res, nil
}

func parseCommodityOrAccount(ledger io.Reader, directive string) ([]string, error) {
Expand Down Expand Up @@ -393,13 +450,16 @@ func (l *Ledger) proposeTransaction(userInput string) (Transaction, error) {
Accounts: accounts,
Commodities: commodities,
UserInput: userInput,
Datetime: time.Now(),
}

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

fmt.Println(trx.ToString())

err = l.validateWith(trx.ToString())
if err != nil {
return Transaction{}, fmt.Errorf("unable to validate transaction: %v", err)
Expand Down Expand Up @@ -438,6 +498,7 @@ type PromptCtx struct {
Accounts []string
Commodities []string
UserInput string
Datetime time.Time
}


43 changes: 28 additions & 15 deletions app/ledger/ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,36 +178,36 @@ func TestLedger_ProposeTransaction(t *testing.T) {
// for the test Ledger file
if len(mockedTransactionGenerator.calls.GenerateTransaction) == 1 {
mocktr = Transaction{
Date: dt,
RealDateTime: dt,
Description: "My tr",
Comment: "invalid transaction",
Postings: []Posting{
Posting{
Account: "cash",
Amount: -30.43,
Amount: -3000.43,
Currency: "EUR",
},
Posting{
Account: "taxi",
Amount: 30.43,
Amount: 3000.43,
Currency: "EUR",
},
},
}
} else {
mocktr = Transaction{
Date: dt,
RealDateTime: dt,
Comment: "valid transaction",
Description: "Tacos",
Postings: []Posting{
Posting{
Account: "Assets:Cash",
Amount: -30.43,
Amount: -3000.43,
Currency: "EUR",
},
Posting{
Account: "Food",
Amount: 30.43,
Amount: 3000.43,
Currency: "EUR",
},
},
Expand Down Expand Up @@ -249,20 +249,33 @@ account Equity

assert.Equal(t, "valid transaction", tr.Comment)

assert.Equal(t,
PromptCtx{
Accounts: []string{"Food", "Assets:Cash", "Equity" },
Commodities: []string{"EUR", "USD"},
UserInput: "20 Taco Bell",
},
mockedTransactionGenerator.calls.GenerateTransaction[0].PromptCtx,

assert.Equal(
t,
[]string{"Food", "Assets:Cash", "Equity" },
mockedTransactionGenerator.calls.GenerateTransaction[0].PromptCtx.Accounts,
)


assert.Equal(
t,
[]string{"EUR", "USD"},
mockedTransactionGenerator.calls.GenerateTransaction[0].PromptCtx.Commodities,
)


assert.Equal(
t,
"20 Taco Bell",
mockedTransactionGenerator.calls.GenerateTransaction[0].PromptCtx.UserInput,
)


assert.Equal(t,
`;; 2014-11-12 11:45:26 Wednesday: valid transaction
2014-11-12 * Tacos
Assets:Cash -30.43 EUR
Food 30.43 EUR
Assets:Cash -3.000,43 EUR
Food 3.000,43 EUR
`,
tr.ToString(),
)
Expand Down
17 changes: 9 additions & 8 deletions app/ledger/templates/default_prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ Bellow is the list of accounts you MUST use in your transaction:
"{{.}}"
{{end}}


Use "EUR" as the default currency if nothing else is specified in user request.
Another possible currency is "USD", "RUB".
Today is {{.Datetime}}
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
Date string 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
}
Expand All @@ -25,8 +23,11 @@ type Posting struct {
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:
Assume number in user input to be a price
Use {{ index .Commodities 0}} as the default currency if nothing else is specified in user request.
Another possible currency are:
{{range .Commodities}}
"{{.}}"
{{end}}

{{.UserInput}}
Use {{ index .Accounts 0}} as default assets account if nothing else is specified in user request
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ go 1.21.7
require github.com/jessevdk/go-flags v1.5.0

require (
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.27
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.11.0
github.com/joho/godotenv v1.5.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sashabaranov/go-openai v1.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24 h1:1T7RcpzlldaJ3qpZi0lNg/lBsfPCK+8n8Wc+R8EhAkU=
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24/go.mod h1:kL1v4iIjlalwm3gCYGvF4NLa3hs+aKEfRkNJvj4aoDU=
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.27 h1:rOlGzmYC3jPVPLVLWKMiiYuePQ6MV8Cyw5qJYBoMnkY=
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.27/go.mod h1:kL1v4iIjlalwm3gCYGvF4NLa3hs+aKEfRkNJvj4aoDU=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
Expand All @@ -19,6 +21,8 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
Expand Down Expand Up @@ -62,6 +66,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sashabaranov/go-openai v1.26.0 h1:upM565hxdqvCxNzuAcEBZ1XsfGehH0/9kgk9rFVpDxQ=
github.com/sashabaranov/go-openai v1.26.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
Expand Down

0 comments on commit cf9d3c9

Please sign in to comment.