Skip to content

Commit

Permalink
Initial OpenAI Integration (#14)
Browse files Browse the repository at this point in the history
* progress

* progress

* fix

* bob'
  • Loading branch information
GabiGrin authored Apr 30, 2023
1 parent 1926396 commit b6e7fe0
Show file tree
Hide file tree
Showing 19 changed files with 463 additions and 80 deletions.
2 changes: 1 addition & 1 deletion core/src/flow-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { VisualPart, Part, PartDefinition } from "./part";
import { VisualPart, PartDefinition, Part } from "./part";

const importSchema = z.record(z.string(), z.string().or(z.array(z.string())));
const position = z.strictObject({ x: z.number(), y: z.number() });
Expand Down
1 change: 1 addition & 0 deletions dev-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"find-git-root": "^1.0.4",
"glob": "^8.0.3",
"ignore": "^5.2.0",
"openai": "^3.2.1",
"pkg-up": "^3.0.0",
"resolve-from": "^5.0.0",
"source-map-support": "^0.5.21",
Expand Down
10 changes: 8 additions & 2 deletions dev-server/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import axios from "axios";
import {
BasePart,
FlydeFlow,
ImportableSource,
ResolvedDependenciesDefinitions,
ResolvedFlydeFlowDefinition,
} from "@flyde/core";
import { FolderStructure } from "./fs-helper/shared";
import type { ImportablesResult } from "./service/scan-importable-parts";
Expand Down Expand Up @@ -36,6 +35,13 @@ export const createDevServerClient = (baseUrl: string) => {
.get(`${baseUrl}/importables?filename=${filename}`)
.then((res) => res.data);
},
generatePartFromPrompt: (
prompt: string
): Promise<{ importablePart: ImportableSource }> => {
return axios
.post(`${baseUrl}/generatePart`, { prompt })
.then((res) => res.data);
},
};
};

Expand Down
28 changes: 25 additions & 3 deletions dev-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import { createService } from "./service/service";
import { setupRemoteDebuggerServer } from "@flyde/remote-debugger/dist/setup-server";
import { createServer } from "http";
import { scanImportableParts } from "./service/scan-importable-parts";
import { deserializeFlow, resolveDependencies } from "@flyde/resolver";
import {
deserializeFlow,
resolveCodePartDependencies,
resolveDependencies,
} from "@flyde/resolver";
import { join } from "path";

import { entries } from "@flyde/core";
import resolveFrom = require("resolve-from");
import { readFileSync } from "fs";
import { existsSync, readFileSync, writeFileSync } from "fs";
import {
generateAndSavePart,
generatePartCodeFromPrompt,
} from "./service/generate-part-from-prompt";

export const runDevServer = (
port: number,
Expand Down Expand Up @@ -84,8 +92,22 @@ export const runDevServer = (
console.error(e);
res.status(400).send(e);
}
});

app.post("/generatePart", async (req, res) => {
const { prompt } = req.body as { prompt: string };

if (prompt.trim().length === 0) {
res.status(400).send("prompt is empty");
return;
}

// res.send({...STDLIB_BACKUP, ...data});
try {
const data = await generateAndSavePart(rootDir, prompt);
res.send(data);
} catch (e) {
res.status(400).send(e);
}
});

app.use("/editor", express.static(editorStaticRoot));
Expand Down
109 changes: 109 additions & 0 deletions dev-server/src/service/generate-part-from-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
CodePart,
ImportableSource,
ImportedPart,
randomInt,
} from "@flyde/core";
import { resolveCodePartDependencies } from "@flyde/resolver";
import "dotenv/config";
import { existsSync, writeFile, writeFileSync } from "fs";

import { OpenAIApi, Configuration } from "openai";
import { join } from "path";

const primingNotice = `You create code-based parts for Flyde, a flow-based programming tool.
This is how a part looks like:
// fileName: limit-times.flyde.ts
import { CodePart } from "@flyde/core";
export const LimitTimes: CodePart = {
id: "Limit Times",
description: "Item will be emitted until the limit is reached",
inputs: {
item: { mode: "required", description: "The item to emit" },
times: {
mode: "required",
description: "The number of times to emit the item",
},
reset: { mode: "optional", description: "Reset the counter" },
},
outputs: { ok: {} },
run: function (inputs, outputs, adv) {
// magic here
const { state } = adv;
const { item, times, reset } = inputs;
const { ok } = outputs;
if (typeof reset !== "undefined") {
state.set("val", 0);
return;
}
let curr = state.get("val") || 0;
curr++;
state.set("val", curr);
if (curr >= times) {
adv.onError(new Error(\`Limit of \$\{times\} reached\`));
} else {
ok.next(item);
}
},
};
// end of part
you should reply only with code, no explanations
use no libraries. Assume NodeJS. Avoid hardcoded values. Prefer APIs
`;

export async function generatePartCodeFromPrompt(
prompt: string,
apiKey = process.env.OPEN_AI_API_KEY
) {
const configuration = new Configuration({
apiKey,
});
const openai = new OpenAIApi(configuration);
const completion = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: primingNotice },
{
role: "user",
content: `Create a part that does the following: ${prompt}`,
},
],
temperature: 0.1,
n: 1,
});

const code = completion.data.choices[0].message?.content;
const fileName = code?.match(/fileName: (.*)\.flyde\.ts/)?.[1];

console.log({ code, fileName });

return { fileName, code };
}

export async function generateAndSavePart(
rootDir: string,
prompt: string,
apiKey?: string
): Promise<ImportableSource> {
const { fileName, code } = await generatePartCodeFromPrompt(prompt, apiKey);
let filePath = join(rootDir, `${fileName}.flyde.ts`);
if (existsSync(filePath)) {
filePath = filePath.replace(/\.flyde\.ts$/, `${randomInt(9999)}.flyde.ts`);
}

writeFileSync(filePath, code);
const maybePart = resolveCodePartDependencies(filePath)[0];
if (!maybePart) {
throw new Error("Generated part is corrupt");
}

const part: ImportedPart = {
...maybePart.part,
source: { path: filePath, export: maybePart.exportName },
};
return { part, module: `./${fileName}.flyde.ts` };
}
6 changes: 6 additions & 0 deletions editor/src/vscode-ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export const createVsCodePorts = (): EditorPorts => {
onInstallRuntimeRequest: async () => {
return postMessageCallback("onInstallRuntimeRequest", {});
},
generatePartFromPrompt: async (dto) => {
return postMessageCallback("generatePartFromPrompt", dto);
},
onExternalFlowChange: (cb) => {
const handler = (event: MessageEvent) => {
const { data } = event;
Expand All @@ -87,6 +90,9 @@ export const createVsCodePorts = (): EditorPorts => {
reportEvent: (name, data) => {
return postMessageCallback("reportEvent", { name, data });
},
hasOpenAiToken: async () => {
return postMessageCallback("hasOpenAiToken", {});
},
};
};

Expand Down
6 changes: 6 additions & 0 deletions editor/src/web-ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,11 @@ export const createWebPorts = ({
reportEvent: (name, data) => {
console.info(`Analytics event: ${name}`, data);
},
generatePartFromPrompt: async ({ prompt }) => {
return devServerClient.generatePartFromPrompt(prompt);
},
hasOpenAiToken: async () => {
return true;
},
};
};
1 change: 1 addition & 0 deletions flow-editor/src/flow-editor/FlowEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export const FlowEditor: React.FC<FlydeFlowEditorProps> = React.memo(
hideOmnibar,
reportEvent,
onAddPartInstance,
visualEditorRef,
onImportPart,
editorBoardData.lastMousePos,
resolvedDependencies,
Expand Down
14 changes: 13 additions & 1 deletion flow-editor/src/flow-editor/ports/ports.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createContext, useContext } from "react";
import { FlydeFlow, ResolvedDependenciesDefinitions } from "@flyde/core";
import {
FlydeFlow,
ImportableSource,
ResolvedDependenciesDefinitions,
} from "@flyde/core";
import { FlowJob, ImportablesResult } from "@flyde/dev-server";
import { ReportEvent } from "./analytics";

Expand Down Expand Up @@ -40,6 +44,12 @@ export interface EditorPorts {
onStopFlow: () => Promise<void>;

reportEvent: ReportEvent;

generatePartFromPrompt: (dto: {
prompt: string;
}) => Promise<{ importablePart: ImportableSource }>;

hasOpenAiToken: () => Promise<boolean>;
}

const throwsNotImplemented: any = async () => {
Expand All @@ -61,6 +71,8 @@ export const defaultPorts: EditorPorts = {
onRunFlow: throwsNotImplemented,
onStopFlow: throwsNotImplemented,
reportEvent: throwsNotImplemented,
generatePartFromPrompt: throwsNotImplemented,
hasOpenAiToken: () => Promise.resolve(false),
};

export const PortsContext = createContext<EditorPorts>(defaultPorts);
Expand Down
Loading

0 comments on commit b6e7fe0

Please sign in to comment.