Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
napsy committed Mar 31, 2024
0 parents commit 08dc272
Show file tree
Hide file tree
Showing 13 changed files with 1,247 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## AI-powered email assistant


## GMail authentication

If you're running this locally, Google won't be able to redirect back to the web app, once you authenticate.

This meansCopy the returned state token, once authorized to Google (the code from the URL)
166 changes: 166 additions & 0 deletions ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"bytes"
"context"
"encoding/json"
"io"
"log"
"net/http"
"time"

"github.com/jmorganca/ollama/api"
)

type LLM interface {
bio(string)
summary(string) (string, error)
actionItems(string) ([]string, error)
}

type ollamaLLM struct {
model string
biography string
c *api.Client
}

func newOllama(model string) (*ollamaLLM, error) {
c, err := api.ClientFromEnvironment()
if err != nil {
return nil, err
}

return &ollamaLLM{
model: model,
c: c,
}, nil
}

var (
promptMessage = "Create a short summary in bullet points and any possible action items for me of the following email. Based on my given bio, propritize the action items accordingly by using either 'low', 'med' or 'high' qualifiers and identify the urgency and importance of the message. Always separate action items from the summary. The email conversation is : "
promptSystem = "You are an assitant that summarizes email conversations. When interpreting the contex of the email content, use my bio to identify action item priorities. My bio is: "
)

func (ollama *ollamaLLM) bio(summary string) {
ollama.biography = summary
}

func (ollama *ollamaLLM) summary(msg string) (string, error) {
a := ""
//prompt := "Create a really short summary in bullet points of the following email: " + msg
rq := api.GenerateRequest{
Model: "zephyr",
Prompt: promptMessage + msg,
Template: "",
System: promptSystem + ollama.biography,
Context: []int{},
Raw: false,
Format: "",
KeepAlive: &api.Duration{},
Images: []api.ImageData{},
Options: map[string]interface{}{},
}

ctx, _ := context.WithTimeout(context.Background(), time.Duration(5*time.Minute))
if err := ollama.c.Generate(ctx, &rq, func(resp api.GenerateResponse) error {
a += resp.Response
return nil
}); err != nil {
return "", err
}
return a, nil
}

func (ollama *ollamaLLM) actionItems(msg string) ([]string, error) {
return []string{}, nil
}

// Define structures to match the JSON response format
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}

type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
Logprobs *json.RawMessage `json:"logprobs"` // Use a pointer to RawMessage for potential null value
FinishReason string `json:"finish_reason"`
}

type Response struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage map[string]int `json:"usage"`
SystemFingerprint string `json:"system_fingerprint"`
}

type openAI struct {
biography string
token string
}

func newOpenAI(token string) (*openAI, error) {
return &openAI{token: token}, nil
}

func (openai *openAI) bio(summary string) {
openai.biography = summary
}

func (openai *openAI) summary(msg string) (string, error) {
apiURL := "https://api.openai.com/v1/chat/completions"

payload := map[string]interface{}{
"model": "gpt-3.5-turbo",
"messages": []map[string]string{
{"role": "system", "content": promptSystem + openai.biography},
{"role": "user", "content": promptMessage + msg},
},
"temperature": 0.7,
}

payloadBytes, err := json.Marshal(payload)
if err != nil {
log.Fatalf("Error marshalling payload: %v", err)
}

req, err := http.NewRequest("POST", apiURL, bytes.NewReader(payloadBytes))
if err != nil {
log.Fatalf("Error creating request: %v", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+openai.token)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}

var response Response
responseContent := ""
if err := json.Unmarshal(body, &response); err != nil {
return "", err
}

if len(response.Choices) > 0 && response.Choices[0].Message.Content != "" {
responseContent = response.Choices[0].Message.Content
}

return responseContent, nil
}

func (openai *openAI) actionItems(msg string) ([]string, error) {
return []string{}, nil
}
109 changes: 109 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"fmt"
"hash/crc32"
"time"

"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

func hashMail(date, from, subject string) string {
str := date + from + subject
h := crc32.ChecksumIEEE([]byte(str))
return fmt.Sprintf("%X", h)
}

type localDB struct {
read []string
}

func newLocalDB() *localDB {
return &localDB{}
}

func (db *localDB) wasRead(hash string) bool {
for i := range db.read {
if db.read[i] == hash {
return true
}
}
return false
}
func (db *localDB) markRead(hash string) {
db.read = append(db.read, hash)
}

type sqlMessage struct {
ID uint
Date time.Time
From string
Subject string

Summary string
Original string

// Metadata
Tags []string
Deleted bool
}
type sqliteDB struct {
db *gorm.DB
showDeleted bool
}

func newSqlite() (*sqliteDB, error) {
db, err := gorm.Open(sqlite.Open("mailassist.db"), &gorm.Config{})
if err != nil {
return nil, err
}

// Migrate the schema
db.AutoMigrate(&sqlMessage{})
/*
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // find product with integer primary key
db.First(&product, "code = ?", "D42") // find product with code D42
// Update - update product's price to 200
db.Model(&product).Update("Price", 200)
// Update - update multiple fields
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - delete product
db.Delete(&product, 1)
*/
return &sqliteDB{
db: db,
}, nil
}

func (db *sqliteDB) saveMessage(date, from, subject, message, original string) error {
// Fri, 29 Mar 2024 17:48:24 +0000 (UTC)
// Mon Jan 2 15:04:05 MST 2006
d, err := time.Parse("Mon, 02 Jan 2006 15:04:05 +0000 (MST)", date)
if err != nil {
return err
}
db.db.Create(&sqlMessage{
Date: d,
From: from,
Subject: subject,
Original: original,
Summary: message,
})
return nil
}
func (db *sqliteDB) getMessages(from, to time.Time) []sqlMessage {
return []sqlMessage{}
}

func (db *sqliteDB) getTags(tags []string) []sqlMessage {
return []sqlMessage{}
}
24 changes: 24 additions & 0 deletions desktop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"fmt"
"os/exec"
)

type desktop struct {
notifications bool
}

func newDesktop() *desktop {
return &desktop{notifications: true}
}

func (d *desktop) notify(msg string) error {
if !d.notifications {
return nil
}
// execute notify
cmd := exec.Command("notify-send", "Incoming emails", fmt.Sprintf("%q", msg))
cmd.Run()
return nil
}
Loading

0 comments on commit 08dc272

Please sign in to comment.