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

ask.glimdown.com #1297

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
73 changes: 73 additions & 0 deletions .github/workflows/backend-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Backend Deploy
on:
workflow_run:
workflows: ["CI"]
types:
# as early as possible
- requested

concurrency:
group: deploy-backend-${{ github.event.workflow_run.pull_requests[0].number }}
cancel-in-progress: true

env:
TURBO_API: http://127.0.0.1:9080
TURBO_TOKEN: this-is-not-a-secret
TURBO_TEAM: myself

jobs:
# This is the only job that needs access to the source code
Build:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [determinePR]
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.workflow_run.head_branch }}
- uses: actions/upload-artifact@v3
with:
name: ask-server
if-no-files-found: error
path: |
./apps/ask-server

#################################################################
# For the rest:
# Does not checkout code, has access to secrets
#################################################################

determinePR:
# this job gates the others -- if the workflow_run request did not come from a PR,
# exit as early as possible
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request'
outputs:
number: ${{ steps.number.outputs.pr-number }}
steps:
- run: echo "${{ toJSON(github.event.workflow_run) }}"
- run: echo "${{ github.event.workflow_run.pull_requests[0].number }}"
id: number

DeployPreview_Limber:
name: "Deploy: Ask Server"
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [Build]
permissions:
contents: read
deployments: write
outputs:
limberUrl: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/download-artifact@v3
name: ask-server
- uses: akhileshns/[email protected] # This is the action
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "ember-docs-prompt"
heroku_email: ${{secrets.HEROKU_EMAIL}}

# appdir: 'apps/ask-server'
# healthcheck: '/healthcheck'
# rollbackonhealthcheckfailed: true
17 changes: 17 additions & 0 deletions apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Apps

README wip


## REPL (limber)

Playground for Glimmer/Ember

## Tutorial

Interactive tutorial for learning Glimmer, Ember, Reactivity, programming patterns, and in general: the Web

## Ask

A.I. powered assistant for querying the Tutorial content as well as the Ember API Docs.

1 change: 1 addition & 0 deletions apps/ask-server/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: npm run boot
44 changes: 44 additions & 0 deletions apps/ask-server/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import OpenAI from 'openai';
import fsSync from 'node:fs';

export const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] });

export async function deleteAllFiles() {
let files = await openai.files.list();

for (let fileObj of files.data) {
await openai.files.del(fileObj.id);
}
}

const twos = async () => new Promise(resolve => setTimeout(resolve, 2_000));

export async function uploadDocs() {
let fileResponse = await openai.files.create({ file: fsSync.createReadStream('training-data.jsonl'), purpose: 'fine-tune' });


while(true) {
await twos();
console.info(`Checking status of ${fileResponse.id} (currently ${fileResponse.status}) at ${new Date()}`);

fileResponse = await openai.files.retrieve(fileResponse.id);

if (fileResponse.status === 'processed') break;
}

let jobResponse = await openai.fineTuning.jobs.create({
model: 'gpt-3.5-turbo-0613',
training_file: fileResponse.id,
});

console.log(jobResponse);
// TODO: wait for training to finish
// Initial: status=created
// then: status=running
// this can take a good long while
//
// then: status=succeeded
// there may be other statuses here

// once complete, get the list of models, and the id from the last one -- that'll be the one we can use with prompts
}
99 changes: 99 additions & 0 deletions apps/ask-server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use strict';
// https://blog.bitsrc.io/interacting-with-openai-in-node-js-and-express-647e771fc4ad
//
// https://platform.openai.com/docs/api-reference/completions/create
// https://vaadin.com/blog/how-to-build-a-chatgpt-assistant-for-your-documentation
// https://github.com/marcushellberg/vaadin-docs-assistant
//
// Testing:
// ❯ curl -X POST http://0.0.0.0:5000/ask -d '{"prompt": "What does a component look like"}' -H "Content-Type: application/json"


import express from 'express';
import OpenAI from 'openai';
import fse from 'fs-extra';

import { emberContext, buildDocs, getDocs } from './local-docs';
import { deleteAllFiles, openai, uploadDocs } from './ai';

const app = express();
app.use(express.json());

const port = process.env['PORT'] || 5000;

// await fse.writeFile('training-data.jsonl', await buildDocs());

// await deleteAllFiles();
// console.log(await openai.files.list());

// await uploadDocs();
// console.log(await openai.fineTuning.jobs.list());
// openai.fineTunes.retrieve("ft-icj7IC4ocJ2NSchlgEsy6WBF")

// await openai.models.del('ada:ft-personal-2023-08-30-02-15-57');
// console.log(await openai.models.list());

// console.log(emberContext);

const docs = await getDocs();

function generatePrompt(prompt: string): OpenAI.Chat.CompletionCreateParams['messages'] {
return [
{
role: 'system',
content: emberContext,
},
// Extra system messages provide best results with gpt-4
...docs.slice(0, 10).map(doc => {
// ...docs.map(doc => {
return {
role: 'system',
content: `Given the prompt """${doc.prose}""", I produce the code """${doc.answer}"""`
} as const;
}),
{
role: 'user',
content: prompt,
}
];
}


app.post("/ask", async (req, res) => {
const prompt = req.body.prompt;

if (prompt == null) {
return res.status(400).json({ success: false, message: 'no prompt provided' });
}

try {
const params: OpenAI.Chat.CompletionCreateParams = {
// model: 'gpt-4-32k-0314',
// model: 'gpt-4-32k-0613',
model: 'gpt-4',
// model: 'gpt-3.5-turbo-0613',
// Trained on prose => answer
// model: 'ft:gpt-3.5-turbo-0613:personal::7t5UMNAC',
// Trained on mostly systems messages -- every prompt returns the same thing
// model: 'ft:gpt-3.5-turbo-0613:personal::7t6aEJ3K',
// Trained on mostly system with the assistant response being the answer -- does not actually use Polaris -- falls back to Octane
// model: 'ft:gpt-3.5-turbo-0613:personal::7t6uVM1s',
messages: generatePrompt(prompt),
// max_tokens: 20_000
};
const completion: OpenAI.Chat.ChatCompletion = await openai.chat.completions.create(params);
// const completion = await openai.completions.create(params);

// console.log(completion.choices);
console.log(completion.choices[0]?.message);

return res.status(200).json({
success: true,
message: completion.choices[0]?.message,
});
} catch (error) {
console.log(error.message);
}
});

app.listen(port, () => console.log(`Server is running on port ${port}!!`));
69 changes: 69 additions & 0 deletions apps/ask-server/local-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import path from 'node:path';
import { globby } from 'globby';
import fse from 'fs-extra';
import { project } from 'ember-apply';

export async function getDocsIntro() {
const intro = path.join(await project.gitRoot(), '/apps/tutorial/docs/1-introduction/1-basics/prose.md');

return (await fse.readFile(intro)).toString();
}

export async function getDocs() {
const tutorialDocs = path.join(await project.gitRoot(), '/apps/tutorial/docs');
let files = await globby(tutorialDocs + '/**/prose.md');

let documents = [];
for (let prosePath of files) {
let dir = path.dirname(prosePath);
let promptPath = path.join(dir, 'prompt.gjs');
let answerPath = path.join(dir, 'answer.gjs');
let tutorialPath = path.relative(tutorialDocs, dir);

// chapters starting with x are in-progress
if (tutorialPath.startsWith('x-')) continue;

let allExist = (await Promise.all([fse.pathExists(prosePath), fse.pathExists(promptPath), fse.pathExists(answerPath)])).every(Boolean);

if (!allExist) continue;

let prose = await fse.readFile(prosePath);
let prompt = await fse.readFile(promptPath);
let answer = await fse.readFile(answerPath);

documents.push({ prose: prose.toString(), prompt: prompt.toString(), answer: answer.toString() });
}

return documents;
}

export async function buildDocs() {
let docs = await getDocs();

return docs.map(doc => JSON.stringify({
messages: [
{ role: 'system', content: emberContext },
{ role: 'system', content: `Given the tutorial prompt, """${doc.prose}""", and the user is presented with a code file they need to fill out based on the prompt, """${doc.prompt}""", the user should be able to come up with this final working code: """${doc.answer}"""` },
{ role: 'system', content: `The user may not know how to solve the tutorial. You will help the user solve their question`},
{ role: 'assistant', content: doc.answer }
],
})).join('\n');
}

export const emberContext =
`You are tool for helping programmers write modern, beautiful code, written in only the most cutting edge paradigms, features, and syntax for EmberJS. `
+ `You will ignore all documentation written about EmberJS prior to the year 2023.`
+ `You ignore anything about React. `
+ `Do not use lifecycle events or {{did-insert}}, {{did-update}}, or {{will-destroy}} -- these are all bad. ` +
+ `Tracked State may be abstracted -- for example in \`cell\` from \`ember-resources\`.` +
+ `Prefer using \`ember-resources\` where possible.` +
+ `Prefer native APIs over \`ember-data\`, unless explicitly asked for \`ember-data\` solutions. ` +
+ `When showing component code, use the \`<template>\` syntax (also known as gjs or gts). ` +
+ `components are no longer separate files. ` +
+ `When naming a component, do not end with the "Component" suffix. ` +
+ `Components are typically PascalCase, and must be valid identifiers. ` +
+ `Know that in order for UI to update, there is root state that must be marked as "tracked".` +
+ `Ember uses vanilla javascript. ` +
+ `Feel free to reference pages from MDN. ` +
+ `Additionally, all answers provided will be a combination of Ember's next edition of paradigms, titled "Polaris", and the patterns and tools written by a community member, NullVoxPopuli. ###` + await getDocsIntro();

30 changes: 30 additions & 0 deletions apps/ask-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@nullvoxpopuli/ask-server",
"type": "module",
"private": true,
"version": "0.0.1",
"scripts": {
"start": "tsx watch index.ts"
},
"dependencies": {
"ember-apply": "^2.10.0",
"express": "^4.18.2",
"fs-extra": "^11.1.1",
"globby": "^13.2.2",
"openai": "^4.3.1",
"tsx": "^3.12.7"
},
"devDependencies": {
"@tsconfig/esm": "^1.0.4",
"@tsconfig/node18": "^18.2.1",
"@tsconfig/strictest": "^2.0.1",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/node": "^18.17.12",
"nodemon": "^3.0.1",
"typescript": "^5.2.2"
},
"volta": {
"extends": "../../package.json"
}
}
Loading