Skip to content

Commit

Permalink
Evaluation Results Data Explorer (#96)
Browse files Browse the repository at this point in the history
Using a Google Sheet To View the evaluation results isn't working too
well. So we create our own viewer to display the results.

Create a connect-rpc service for listing the evaluation results

* This is the first step to building a viewer for the evaluation
results; we need a service to allow the client app to read the results.
* We decided to go with
[connect-rpc](https://connectrpc.com/docs/go/getting-started) because
this should simplify things by eliminating the need to use grpc and gprc
gateway in order to have a service that we can connect to from the
browser
* Furthermore, hopefully we can use the connect-rpc generated clients
even when using WASM

* Fix the go_package option in most of the proto files. We seem to be
missing a directory "foyle" in the path. This doesn't appear to matter
until we start using connect-rpc and then the imports are broken unless
we fix the package

Refactor our existing go-app to better support multiple pages. Each page
might render the right hand side differently.

* Add a status bar to display the GOAPP_VERSION; this makes it easier to
ensure we have picked up any changes.

The app is pretty ugly. I will try to fix the styling in a subsequent
PR.
  • Loading branch information
jlewi authored May 7, 2024
1 parent ac42d0a commit dca28ab
Show file tree
Hide file tree
Showing 29 changed files with 1,417 additions and 124 deletions.
7 changes: 4 additions & 3 deletions app/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.1
replace github.com/jlewi/foyle/protos/go => ../protos/go

require (
connectrpc.com/connect v1.16.1
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/agnivade/levenshtein v1.1.1
github.com/cockroachdb/pebble v1.1.0
Expand Down Expand Up @@ -37,9 +38,12 @@ require (
go.opentelemetry.io/otel/trace v1.25.0
go.uber.org/zap v1.27.0
gonum.org/v1/gonum v0.15.0
google.golang.org/api v0.162.0
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.33.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.27.3
sigs.k8s.io/kustomize/kyaml v0.13.9
)

require (
Expand Down Expand Up @@ -189,7 +193,6 @@ require (
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.15.0 // indirect
google.golang.org/api v0.162.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
Expand All @@ -198,13 +201,11 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apimachinery v0.27.3 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
4 changes: 2 additions & 2 deletions app/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis=
connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
Expand Down Expand Up @@ -348,8 +350,6 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jlewi/hydros v0.0.6 h1:gylMzSH5VvO+uTtz20JUtJPPr3ZLAQxvH3j/0FHK4FE=
github.com/jlewi/hydros v0.0.6/go.mod h1:4fV+JUCnexPY2ZbKzdfV/RsyrfralN832MsUSq/7FqE=
github.com/jlewi/hydros v0.0.7-0.20240503183011-8f99ead373fb h1:2G2k606S3Qcg40czr7gnkeIG5KgQ2wXJ1BMxAuC+P3I=
github.com/jlewi/hydros v0.0.7-0.20240503183011-8f99ead373fb/go.mod h1:4fV+JUCnexPY2ZbKzdfV/RsyrfralN832MsUSq/7FqE=
github.com/jlewi/monogo v0.0.0-20240123191147-401afe194d74 h1:pbOw/rOMs0AZ494bGnI6DieGKwqoJQEjHWaJZrvxsJo=
Expand Down
2 changes: 2 additions & 0 deletions app/pkg/eval/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ func (e *Evaluator) loadFoyleFiles(ctx context.Context, db *pebble.DB, files []s
continue
}

// TODO(https://github.com/jlewi/foyle/issues/95): We should assign an ID to each example that is stable
// across experiments.
id := uuid.NewString()
example := &v1alpha1.Example{
Id: id,
Expand Down
71 changes: 71 additions & 0 deletions app/pkg/eval/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package eval

import (
"context"

"connectrpc.com/connect"
"github.com/cockroachdb/pebble"
"github.com/jlewi/foyle/app/pkg/logs"
"github.com/jlewi/foyle/protos/go/foyle/v1alpha1"
"github.com/jlewi/monogo/helpers"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
)

// EvalServer is the server that implements the Eval service interface.
// This is used to make results available to the frontend.
type EvalServer struct{}

func (s *EvalServer) List(
ctx context.Context,
req *connect.Request[v1alpha1.EvalResultListRequest],
) (*connect.Response[v1alpha1.EvalResultListResponse], error) {
log := logs.FromContext(ctx)

if req.Msg.GetDatabase() == "" {
err := connect.NewError(connect.CodeInvalidArgument, errors.New("Request is missing database"))
log.Error(err, "Invalid EvalResultListRequest")
return nil, err
}

db, err := pebble.Open(req.Msg.GetDatabase(), &pebble.Options{})
if err != nil {
log.Error(err, "Failed to open database")
return nil, connect.NewError(connect.CodeInternal, err)
}
defer helpers.DeferIgnoreError(db.Close)

iter, err := db.NewIterWithContext(ctx, nil)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
defer iter.Close()

results := &v1alpha1.EvalResultListResponse{
Items: make([]*v1alpha1.EvalResult, 0, 100),
}

for iter.First(); iter.Valid(); iter.Next() {
key := iter.Key()
if key == nil {
break
}

value, err := iter.ValueAndErr()
if err != nil {
log.Error(err, "Failed to read value for key", "key", string(key))
continue
}

result := &v1alpha1.EvalResult{}
if err := proto.Unmarshal(value, result); err != nil {
log.Error(err, "Failed to unmarshal value for", "key", string(key))
continue
}
results.Items = append(results.Items, result)
}

res := connect.NewResponse(results)
res.Header().Set("Eval-Version", "v1alpha1")
return res, nil
}
99 changes: 99 additions & 0 deletions app/pkg/logsviewer/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package logsviewer

import (
"github.com/go-logr/zapr"
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/pkg/errors"
"go.uber.org/zap"
)

type page string
type view string

const (
getAction = "/get"
setPage = "/setPage"

errorView view = "error"
generatedBlockView view = "generatedBlock"
executedBlockView view = "executedBlock"
rawView view = "raw"

blockLogsView page = "blockLogs"
evalsView page = "evals"

getErrorState = "/getError"
blockLogState = "/blocklog"
)

// MainApp is the main window of the application.
//
// The main application consists of a left hand navigation bar and a right hand side component that is the page
// to display. When you click on one of the left hand navigation buttons it fires of an action setPage to change the
// view. The handler for this action loads the appropriate page and sets MainApp.page to the component for that
// page.
type MainApp struct {
app.Compo
// Page keeps track of the page to display in the right hand side.
page app.UI
}

func (m *MainApp) Render() app.UI {
if m.page == nil {
// TODO(jeremy): Could we keep track of the last view so if we refresh we show the same data?
// One way to do that is to update the URL with query arguments containing the relevant state information.
// Then when we click refresh we could get the information directly from the URL
m.page = &BlockViewer{}
}
return app.Div().Class("main-layout").Body(
app.Div().Class("content").Body(
app.Div().Class("sidebar").Body(
&navigationBar{},
),
app.Div().Class("page-window").Body(
m.page,
),
), &StatusBar{},
)
}

func (m *MainApp) OnMount(ctx app.Context) {
// register to handle the setPage action
ctx.Handle(setPage, m.handleSetPage)
}

// handleSetPage handles the setPage action. The event will tell us which view to display.
func (m *MainApp) handleSetPage(ctx app.Context, action app.Action) {
log := zapr.NewLogger(zap.L())
pageValue, ok := action.Value.(page)
if !ok {
log.Error(errors.New("No page provided"), "Invalid action")
return
}
log.Info("Handling set page action", "page", pageValue)
switch pageValue {
case blockLogsView:
if _, ok := m.page.(*BlockViewer); !ok {
log.Info("Setting page to BlockViewer")
m.page = &BlockViewer{}
}
case evalsView:
if _, ok := m.page.(*EvalViewer); !ok {
log.Info("Setting page to EvalViewer")
m.page = &EvalViewer{}
}
}
// We need to call update to trigger a re-render of the component.
m.Update()
}

// StatusBar at the bottom of the page. Inspired by the vscode/intellij status bar.
// We use this to show useful information like the version number.
type StatusBar struct {
app.Compo
}

func (s *StatusBar) Render() app.UI {
version := app.Getenv("GOAPP_VERSION")
return app.Div().Class("status-bar").Text("goapp version: " + version)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,85 @@ import (
"go.uber.org/zap"
)

// mainWindow is the main window of the application.
// BlockViewer is the page that displays the block logs.
//
// How it works:
// Clicking load fetches the blocklog from the server.
// The log is then stored in the application context (https://go-app.dev/states)
// this allows other components to use it. Load then fires off an UpdateView event to trigger
// the blockLogView to update its content.
// The UpdateView event takes a string argument which is what view should be rendered.
// There is a left hand navigation bar with buttons to display different views of the current log.
// Changing the view is achieved by sending UpdateView events to change the view
type BlockViewer struct {
app.Compo
main *blockLogView
}

func (c *BlockViewer) Render() app.UI {
if c.main == nil {
c.main = &blockLogView{}
}
return app.Div().Class("main-layout").Body(
app.Div().Class("header").Body(
&blockSelector{},
),
app.Div().Class("content").Body(
app.Div().Class("sidebar").Body(
&sideBar{},
),
app.Div().Class("main-window").Body(
c.main,
),
),
)
}

// sideBar adds a navigation bar between the views to the left side.
type sideBar struct {
app.Compo
}

func (s *sideBar) Render() app.UI {
return app.Div().Body(
// Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons.
app.Div().Body(
app.Button().Text("Generated Block").OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue(getAction, generatedBlockView)
}),
),
app.Div().Body(
app.Button().Text("Executed Block")).OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue(getAction, executedBlockView)
}),
app.Div().Body(
app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue(getAction, rawView)
}))
}

// blockLogView is the main window of the application.
// What it displays will change depending on the view selected.
// The content of the main window is HTML which gets set by the action handler for different events.
//
// The main window registers a handler for the getAction event. The getAction event is triggered when ever
// a blockLog is loaded. The handler for the getAction event will set the HTML content of the main windowß
type mainWindow struct {
type blockLogView struct {
app.Compo
HTMLContent string
}

func (m *mainWindow) Render() app.UI {
func (m *blockLogView) Render() app.UI {
// Raw requires the value to have a single root element. So we enclose the HTML content in a div to ensure
// that is all ways true.
return app.Raw("<div>" + m.HTMLContent + "</div>")
}

func (m *mainWindow) OnMount(ctx app.Context) {
func (m *blockLogView) OnMount(ctx app.Context) {
ctx.Handle(getAction, m.handleGetAction)
}

func (m *mainWindow) handleGetAction(ctx app.Context, action app.Action) {
func (m *blockLogView) handleGetAction(ctx app.Context, action app.Action) {
log := zapr.NewLogger(zap.L())
viewValue, ok := action.Value.(view) // Checks if a name was given.
if !ok {
Expand Down
Loading

0 comments on commit dca28ab

Please sign in to comment.