Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement generate in the front end #28

Merged
merged 1 commit into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading