Blodeuwedd Initial

This commit is contained in:
Sarah Jamie Lewis 2023-03-28 13:30:37 -07:00
parent e5e4084fae
commit 5f3ff65f6a
6 changed files with 325 additions and 13 deletions

View File

@ -0,0 +1,281 @@
package blodeuwedd
import (
"bufio"
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"os"
"os/exec"
path "path/filepath"
"strconv"
"strings"
"sync"
"unicode"
)
const BlodueweddSummary = event.Type("BlodeuweddSummary")
const Summary = event.Field("Summary")
const BlodueweddTranslation = event.Type("BlodeuweddTranslation")
const Translation = event.Field("Translation")
// Functionality groups some common UI triggered functions for contacts...
type Functionality struct {
port *bufio.ReadWriter
lock sync.Mutex
process *exec.Cmd
path string
}
func Init(acn connectivity.ACN, appdir string) *Functionality {
bld := new(Functionality)
return bld
}
func (bld *Functionality) OnACNStatusEvent(appl app.Application, e *event.Event) {
}
func (bld *Functionality) Enable(application app.Application, acn connectivity.ACN) {
settings := application.ReadSettings()
if settings.ExperimentsEnabled {
if enabled, exists := settings.Experiments[constants.BlodeuweddExperiment]; enabled && exists {
if settings.BlodeuweddPath != bld.path {
bld.path = settings.BlodeuweddPath
bld.runProcess()
}
// short circuit here so we don't kill the existing process...
return
}
}
}
func (bld *Functionality) UpdateSettings(application app.Application, acn connectivity.ACN) {
bld.Enable(application, acn)
}
func (f Functionality) EventsToRegister() []event.Type {
return []event.Type{}
}
func (f Functionality) ExperimentsToRegister() []string {
return []string{constants.BlodeuweddExperiment}
}
type BlodeweddJob struct {
ID string
Error string
}
// Translate calls on the Blodewedd assistant to translate a particular message
func (bld *Functionality) Translate(profile peer.CwtchPeer, conversation int, mid int, language string) string {
log.Infof("Translating Message....")
if profile.IsFeatureEnabled(constants.BlodeuweddExperiment) {
message, _, err := profile.GetChannelMessage(conversation, 0, mid)
if err == nil {
contents := getMessageContents(message)
ev := event.NewEvent(BlodueweddTranslation, map[event.Field]string{})
ev.Data[event.ProfileOnion] = profile.GetOnion()
ev.Data[event.ConversationID] = strconv.Itoa(conversation)
ev.Data[event.Index] = strconv.Itoa(mid)
log.Infof("Running Prompt....")
go bld.runPrompt(profile, ev, "Translation", contents, language)
}
}
return ""
}
func getMessageContents(msg string) string {
mw := new(model.MessageWrapper)
err := json.Unmarshal([]byte(msg), &mw)
if err == nil {
if mw.Overlay == model.OverlayChat {
return mw.Data
}
} else {
log.Infof("ignored err %v", err)
}
return ""
}
// Summarize calls on the Blodewedd assistant to summarize the last 20 message in a conversation.
func (bld *Functionality) Summarize(profile peer.CwtchPeer, conversation int) string {
log.Infof("Summarizing Conversation....")
if profile.IsFeatureEnabled(constants.BlodeuweddExperiment) {
log.Infof("Experiments Enabled Summarizing Conversation....")
messages, err := profile.GetMostRecentMessages(conversation, 0, 0, 10)
if err != nil {
errJob := BlodeweddJob{
ID: "",
Error: err.Error(),
}
data, _ := json.Marshal(errJob)
return string(data)
}
transcript := ""
for i := len(messages) - 1; i >= 0; i-- {
msg := messages[i]
sender := msg.Attr[constants.AttrAuthor]
if ci, err := profile.FetchConversationInfo(sender); err == nil {
if nickname, exists := ci.GetAttribute(attr.PublicScope, attr.ProfileZone, constants.Name); exists {
nickname = strings.Map(func(r rune) rune {
if r < unicode.MaxASCII {
return r
}
return -1
}, nickname)
sender = nickname
}
}
if sender == profile.GetOnion() {
if nickname, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name); exists {
sender = nickname
}
}
contents := getMessageContents(msg.Body)
if contents != "" {
transcript = fmt.Sprintf("%s%s: %s\n", transcript, sender, contents)
}
}
ev := event.NewEvent(BlodueweddSummary, map[event.Field]string{})
ev.Data[event.ProfileOnion] = profile.GetOnion()
ev.Data[event.ConversationID] = strconv.Itoa(conversation)
log.Infof("Running Prompt....")
go bld.runPrompt(profile, ev, "Summarization", transcript, "")
successJob := BlodeweddJob{
ID: ev.EventID,
Error: "",
}
data, _ := json.Marshal(successJob)
return string(data)
}
errJob := BlodeweddJob{
ID: "",
Error: "blodeuwedd experiment is not enabled",
}
data, _ := json.Marshal(errJob)
return string(data)
}
type BlodeuweddTaskConfig struct {
TaskType string `json:"task_type""`
Input string `json:"input""`
Context string `json:"context"`
}
func (bld *Functionality) runPrompt(profile peer.CwtchPeer, ev event.Event, task_type string, input string, context string) string {
bld.lock.Lock()
defer bld.lock.Unlock()
log.Infof("Loading Blodeuwedd Model: %v", bld)
if bld.process == nil || bld.port == nil {
return "could not run blodeuwedd"
}
// don't allow prompt to contain a terminating sequence
log.Infof("Sending Prompt to Blodeuwedd")
taskConfig := &BlodeuweddTaskConfig{TaskType: task_type, Input: input, Context: context}
data, _ := json.Marshal(taskConfig)
log.Infof("Sending the following task to blodeuwedd: %s", data)
bld.port.WriteString(fmt.Sprintf("%s\n", data))
bld.port.Flush()
log.Infof("reading response")
response := ""
word := getWord(bld.port)
for strings.TrimSpace(word) != "<BLODEUWEDD_END>" {
log.Infof("reading response %v", word)
switch task_type {
case "Translation":
ev.Data[Translation] = word
case "Summarization":
ev.Data[Summary] = word
}
profile.PublishEvent(ev)
word = getWord(bld.port)
}
log.Infof("Received Response from Blodeuwedd")
return response
}
func getWord(reader *bufio.ReadWriter) string {
word := []rune{}
for {
b, _, err := reader.ReadRune()
if err != nil {
return string(word)
}
word = append(word, b)
if b == '\n' {
return string(word)
} else if b == ' ' {
return string(word)
}
}
}
func (bld *Functionality) killProcess() {
if bld.process != nil {
if bld.process.Process != nil {
bld.process.Process.Kill()
bld.process.Wait()
}
}
}
func (bld *Functionality) runProcess() {
bld.lock.Lock()
defer bld.lock.Unlock()
bld.killProcess()
executable := path.Join(bld.path, "blodeuwedd")
log.Infof("starting blodeuwedd process: %v", executable)
cmd := exec.Command(executable)
cmd.Stderr = os.Stderr
stdin, err := cmd.StdinPipe()
if nil != err {
log.Errorf("Error obtaining stdin: %s", err.Error())
return
}
stdout, err := cmd.StdoutPipe()
if nil != err {
log.Errorf("Error obtaining stdout: %s", err.Error())
return
}
bld.port = bufio.NewReadWriter(bufio.NewReader(stdout), bufio.NewWriter(stdin))
err = cmd.Start()
if err != nil {
log.Error("error starting blodeuwedd process: %v %v", executable, err)
return
}
log.Infof("loading blodeuwedd model")
line, err := bld.port.ReadString('\n')
if strings.TrimSpace(line) == "done" {
log.Infof("blodeuwedd is loaded")
} else {
log.Errorf("failed to load blodeuwedd %s %v", line, err)
}
bld.process = cmd
log.Infof("Loading Blodeuwedd Model: %v", bld)
}

View File

@ -91,6 +91,8 @@ func main() {
generatedBindings = generateAppFunction(generatedBindings, fName, args)
case "exp":
generatedBindings = generateExpFunction(generatedBindings, fName, experiment, args)
case "json(exp)":
generatedBindings = generateJsonExpFunction(generatedBindings, fName, experiment, args)
case "(json)app":
generatedBindings = generateJsonAppFunction(generatedBindings, fName, args)
case "profile":
@ -310,6 +312,37 @@ func {{FNAME}}({{GO_ARGS_SPEC}}) {
return bindings
}
func generateJsonExpFunction(bindings string, name string, exp string, argsTypes []string) string {
appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}}) *C.char {
return C.CString({{FNAME}}({{C2GO_ARGS}}))
}
func {{FNAME}}({{GO_ARGS_SPEC}}) string {
cwtchProfile := application.GetPeer(profile)
if cwtchProfile != nil {
return {{EXPERIMENT}}.{{LIBNAME}}(cwtchProfile, {{GO_ARG}})
}
return ""
}
`
cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)
appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", exp)
appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
// We need to prepend a set of profile handle arguments...
pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype()
appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ","))
appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ","))
appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ","))
appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)
bindings += appPrototype
return bindings
}
func generateJsonAppFunction(bindings string, name string, argsTypes []string) string {
appPrototype := `
//export c_{{FNAME}}

5
go.mod
View File

@ -10,6 +10,8 @@ require (
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
)
replace cwtch.im/cwtch v0.19.3 => /home/sarah/workspace/src/cwtch.im/cwtch
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/cwtch.im/tapir v0.6.0 // indirect
@ -19,9 +21,6 @@ require (
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
golang.org/x/tools v0.1.12 // indirect
)

10
go.sum
View File

@ -1,8 +1,4 @@
cwtch.im/cwtch v0.18.0/go.mod h1:StheazFFY7PKqBbEyDVLhzWW6WOat41zV0ckC240c5Y=
cwtch.im/cwtch v0.19.2 h1:H7DrSKQ9J7aNkKQkdyGGWckEV+dPKbL5PMRq0GoAn6I=
cwtch.im/cwtch v0.19.2/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
cwtch.im/cwtch v0.19.3 h1:LRcJFgSw5LwUlOOcVtDC5mRb9NsuXUwNloGo5zNZb9A=
cwtch.im/cwtch v0.19.3/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
@ -92,12 +88,8 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -148,8 +140,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

6
spec
View File

@ -42,6 +42,12 @@ import "cwtch.im/cwtch/functionality/filesharing"
@(json)profile-experiment EnhancedGetSharedFiles filesharing conversation
# Blodeuwedd Management
!blodeuweddExperiment import "git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/blodeuwedd"
!blodeuweddExperiment global blodeuweddExperiment *blodeuwedd.Functionality blodeuwedd
!blodeuweddExperiment json(exp) Summarize conversation
!blodeuweddExperiment json(exp) Translate conversation int:message string:language
# Server Hosting Experiment
!serverExperiment import "git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/servers"
!serverExperiment global serverExperiment *servers.ServersFunctionality servers

View File

@ -4,6 +4,7 @@ import (
"cwtch.im/cwtch/settings"
"encoding/json"
"fmt"
"git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/blodeuwedd"
"git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/servers"
"os"
"strconv"
@ -627,6 +628,8 @@ func (eh *EventHandler) startHandlingPeer(onion string) {
eventBus.Subscribe(event.FileDownloaded, q)
eventBus.Subscribe(event.TokenManagerInfo, q)
eventBus.Subscribe(event.ProtocolEngineCreated, q)
eventBus.Subscribe(blodeuwedd.BlodueweddSummary, q)
eventBus.Subscribe(blodeuwedd.BlodueweddTranslation, q)
go eh.forwardProfileMessages(onion, q)
}