Skip to content

Commit

Permalink
Implement generate in the front end
Browse files Browse the repository at this point in the history
* Add conversion routines to and from the protos to the vscode APIs
* Add doc vscode_apis.md which to explain it.
* Use separate addresses to configure the Executor and Agent service in vscode
  so they don't have to be colocated
* Scaffold an initial agent in the golang server to handle the requests.
  • Loading branch information
jlewi committed Apr 6, 2024
1 parent 40c65df commit 2abea2a
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 83 deletions.
24 changes: 24 additions & 0 deletions app/pkg/agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package agent

import (
"context"

"github.com/jlewi/foyle/protos/go/foyle/v1alpha1"
)

// Agent is the agent.
type Agent struct {
v1alpha1.UnimplementedGenerateServiceServer
}

func (e *Agent) Generate(context.Context, *v1alpha1.GenerateRequest) (*v1alpha1.GenerateResponse, error) {
resp := &v1alpha1.GenerateResponse{
Blocks: []*v1alpha1.Block{
{
Kind: v1alpha1.BlockKind_MARKUP,
Contents: "Hello From The Foyle Server! Your generate request was recieved.",
},
},
}
return resp, nil
}
8 changes: 8 additions & 0 deletions app/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/go-logr/zapr"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/jlewi/foyle/app/pkg/agent"
"github.com/jlewi/foyle/app/pkg/config"
"github.com/jlewi/foyle/app/pkg/executor"
"github.com/jlewi/foyle/app/pkg/logs"
Expand All @@ -44,6 +45,7 @@ type Server struct {
// builtinExtensionPaths is a list of serving paths to the built in extensions
builtinExtensionPaths []string

agent *agent.Agent
executor *executor.Executor
conn *grpc.ClientConn
}
Expand All @@ -53,6 +55,7 @@ func NewServer(config config.Config) (*Server, error) {
s := &Server{
config: config,
executor: &executor.Executor{},
agent: &agent.Agent{},
}

if err := s.createGinEngine(); err != nil {
Expand Down Expand Up @@ -324,6 +327,7 @@ func (s *Server) startGRPCServer(lis net.Listener) error {
s.grpcServer = grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()))

v1alpha1.RegisterExecuteServiceServer(s.grpcServer, s.executor)
v1alpha1.RegisterGenerateServiceServer(s.grpcServer, s.agent)

// So that gRPC curl can be used to inspect it
reflection.Register(s.grpcServer)
Expand Down Expand Up @@ -372,6 +376,10 @@ func (s *Server) registerGRPCGatewayRoutes() error {
return err
}

if err := v1alpha1.RegisterGenerateServiceHandler(ctx, gwMux, conn); err != nil {
return err
}

// Configure gin to delegate to the grpc gateway
handleFunc := func(c *gin.Context) {
log.V(logs.Debug).Info("Delegating request to grpc gateway")
Expand Down
2 changes: 2 additions & 0 deletions frontend/foyle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This the vscode extension that is the frontend for foyle.

The extension was initialized using the [vscode extension generator](https://code.visualstudio.com/api/get-started/your-first-extension).

see [vscode_apis.md](vscode_apis.md) for explanation of the vscode apis.

## Developer Guide

* In vscode for web you can go to the extensions tab and search for "@builtin" to see if the extension is loaded
Expand Down
9 changes: 7 additions & 2 deletions frontend/foyle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@
"configuration": {
"title": "Foyle",
"properties": {
"foyle-notebook.address": {
"foyle-notebook.agent-address": {
"type": "string",
"default": "http://localhost:8080",
"description": "The address of the Foyle. This should in the form {protocol}://{host}:{port}."
"description": "The address of the Foyle Agent. The agent generates completions. This should in the form {protocol}://{host}:{port}."
},
"foyle-notebook.executor-address": {
"type": "string",
"default": "http://localhost:8080",
"description": "The address of the Foyle Executor. The executor executes shell commands. This should in the form {protocol}://{host}:{port}."
}
}
},
Expand Down
63 changes: 47 additions & 16 deletions frontend/foyle/src/web/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,63 @@ import { extName } from "./constants";
// TODO(jeremy): One of
export class FoyleClient {

// TODO(jeremy): How do we deal with the async nature of the fetch call?
// We should probably return a promise.
// Execute a request.
public Execute(request: agentpb.ExecuteRequest): Promise<agentpb.ExecuteResponse> {
// getConfiguration takes a section name.
const config = vscode.workspace.getConfiguration(extName);
// Include a default so that address is always well defined
const address = config.get<string>("address", "http://localhost:8080");
const resp = new agentpb.ExecuteResponse();
const o = new docpb.BlockOutput();
const i = new docpb.BlockOutputItem();
i.textData = "some output";
o.items = [i];
resp.outputs = [
o,
];
// getConfiguration takes a section name.
const config = vscode.workspace.getConfiguration(extName);
// Include a default so that address is always well defined
const address = config.get<string>("executor-address", "http://localhost:8080");

return fetch(address + "/api/v1alpha1/execute", {
method: 'POST',
body: resp.toJsonString(),
body: request.toJsonString(),
headers: { 'Content-Type': 'application/json' },
})
.then(response => response.json())
.then(data => {
const resp = agentpb.ExecuteResponse.fromJson(data);
return resp;
});
}
}

// generate a completion.
public generate(request: agentpb.GenerateRequest): Promise<agentpb.GenerateResponse> {
// getConfiguration takes a section name.
const config = vscode.workspace.getConfiguration(extName);
// Include a default so that address is always well defined
const address = config.get<string>("agent-address", "http://localhost:8080");

return fetch(address + "/api/v1alpha1/generate", {
method: 'POST',
body: request.toJsonString(),
headers: { 'Content-Type': 'application/json' },
})
.then(response => response.json())
.then(data => {
const resp = agentpb.GenerateResponse.fromJson(data);
return resp;
});
}
}


// getTraceID takes a list of blocks and returns the most recent traceId
// if there is one.
// Returns the empty string if there is no traceId
export function getTraceID(blocks: docpb.Block[]): string {
if (blocks.length <= 0) {
return "";
}
var lastBlock = blocks.at(-1);
if (lastBlock === undefined) {
return "";
}
if (lastBlock.traceIds.length === 0) {
return "";
}
const v = lastBlock.traceIds.at(-1);
if (v === undefined) {
return "";
}
return v;
}
133 changes: 133 additions & 0 deletions frontend/foyle/src/web/converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Conversion routines between the protocol buffer format and VSCode's representation of a notebook
//
// VSCode has two different APIs
// 1. Data APIs (NotebookData, NotebookCellData)
// https://github.com/microsoft/vscode/blob/98332892fd2cb3c948ced33f542698e20c6279b9/src/vs/workbench/api/common/extHostTypes.ts#L3598
// * These are concrete classes
// * This is what the serialization uses
// * i.e. Serialize.serializeNotebook passes a VSCodeNotebookData as input
// * The deserializer returns a VSCodeNotebookData
// * NotebookCellData has an interface and concrete class
// concrete class - https://github.com/microsoft/vscode/blob/e4595ad3998a436bbf68d32a6c59e9d49fc63e32/src/vs/workbench/api/common/extHostTypes.ts#L3668
// interface - https://github.com/microsoft/vscode/blob/e4595ad3998a436bbf68d32a6c59e9d49fc63e32/src/vscode-dts/vscode.proposed.notebookMime.d.ts#L10
//
// 2. Editor APIs (NotebookDocument, NotebookCell)
// * These are interfaces
// * These are used by the editor
// * To my knowledge there aren't concrete classes
// 3. Both APIs use NotebookCellOutput, NotebookCellOutputItem
// * These are concrete classes
// 4. NotebookCellData isn't compatible with the interface NotebookCell
// * NotebookCellData has extra fields such as index and document.
//
// However (NotebookCell and NotebookCellData) share the following fields:
// kind: NotebookCellKind
// metadata: [key: string];
// outputs: NotebookCellOutput[];
//
// However the differe in how the actual value is stored.

// NotebookCellData uses the fields:
// value: string;
// languageId: string;
//
// Whereas NotebookCell uses the field:
// document: TextDocument;
//
// TextDocument has the following fields and methods for our purposes
// languageId: string;
// getText(): string;
//
//

import * as vscode from 'vscode';
import * as docpb from "../gen/foyle/v1alpha1/doc_pb";
import * as metadata from "./metadata";
import * as constants from "./constants";

// cellToCellData converts a NotebookCell to a NotebookCellData.
// NotebookCell is an interface used by the editor.
// NotebookCellData is a concrete class.
export function cellToCellData(cell: vscode.NotebookCell): vscode.NotebookCellData {
let data = new vscode.NotebookCellData(cell.kind, cell.document.getText(), cell.document.languageId);

data.metadata = cell.metadata;
data.outputs = [];
for (let o of cell.outputs) {
data.outputs.push(o);
}
return data;
}

// cellDataToBlock converts an instance of NotebookCellData to the Block proto.
export function cellDataToBlock(cell: vscode.NotebookCellData): docpb.Block {
let block = new docpb.Block();
block.contents = cell.value;
block.language = cell.languageId;
if (cell.kind === vscode.NotebookCellKind.Code) {
block.kind = docpb.BlockKind.CODE;
} else {
block.kind = docpb.BlockKind.MARKUP;
}

if (cell.metadata !== undefined) {
metadata.setBlockFromMeta(block, cell.metadata);
}

let cellOutputs: vscode.NotebookCellOutput[] = [];
if (cell.outputs !== undefined) {
cellOutputs = cell.outputs;
}
for (const output of cellOutputs) {
let outBlock = new docpb.BlockOutput();
outBlock.items = [];
for (const item of output.items) {
let outItem = new docpb.BlockOutputItem();
outItem.textData = new TextDecoder().decode(item.data);
outItem.mime = item.mime;
outBlock.items.push(outItem);
}
block.outputs.push(outBlock);
}

return block;
}

// blockToCellData converts a Block proto to a NotebookCellData.
export function blockToCellData(block: docpb.Block): vscode.NotebookCellData {
let kind = vscode.NotebookCellKind.Markup;
if (block.kind === docpb.BlockKind.CODE) {
kind = vscode.NotebookCellKind.Code;
}
let language = block.language;
if (language === "" && kind === vscode.NotebookCellKind.Code) {
// Default to the bash language
language = constants.bashLang;

if (language !== constants.bashLang) {
console.log(`Unsupported language: ${language}`);
}
}

let newCell = new vscode.NotebookCellData(
kind,
block.contents,
language
);

newCell.metadata = metadata.getCellMetadata(block);
newCell.outputs = [];
for (let output of block.outputs) {
let items: vscode.NotebookCellOutputItem[] = [];
for (let item of output.items) {
if (item.textData !== "") {
const text = new TextEncoder().encode(item.textData);
items.push(new vscode.NotebookCellOutputItem(text, item.mime));
} else {
console.log("Unknown output type");
}
}
newCell.outputs.push(new vscode.NotebookCellOutput(items));
}
return newCell;
}
5 changes: 4 additions & 1 deletion frontend/foyle/src/web/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as vscode from 'vscode';
import { Controller } from './controller';
import {FoyleClient} from './client';
import { Serializer } from './serializer';

import * as generate from './generate';
// Create a client for the backend.
const client = new FoyleClient;

Expand Down Expand Up @@ -49,6 +49,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.showInformationMessage('Hello World from foyle in a web extension host!');
});

// Here's where we register the command that will generate a completion using the AI model
// You can set a keybinding for this command in the package.json file
context.subscriptions.push(vscode.commands.registerCommand("foyle.generate", generate.generateCompletion));
context.subscriptions.push(disposable);
}

Expand Down
Loading

0 comments on commit 2abea2a

Please sign in to comment.