-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 08dc272
Showing
13 changed files
with
1,247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.