feat: app actions

This commit is contained in:
BetaHuhn 2023-09-21 11:41:42 +02:00
parent b0a72d8517
commit 09c01e4b38
16 changed files with 3438 additions and 106 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ node_modules
.parcel-cache
dist
notes.txt
lib

View file

@ -21,3 +21,4 @@ micros:
dev: npm run dev
public_routes:
- "/public/*"
provide_actions: true

View file

@ -1,38 +0,0 @@
import express from 'express'
import { Deta } from 'deta'
const app = express()
const deta = Deta()
const chats = deta.Base('chats')
const messages = deta.Base('messages')
app.use(express.json())
app.get('/public/chats/:key', async (req, res) => {
const key = req.params.key
const chat = await chats.get(key)
if (!chat || !chat.shared) {
return res.status(404).json({ message: 'Chat not found' })
}
res.json(chat)
})
app.get('/public/chats/:key/messages', async (req, res) => {
const key = req.params.key
const chat = await chats.get(key)
if (!chat || !chat.shared) {
return res.status(404).json({ message: 'Chat not found' })
}
const items = await messages.fetch({ chatId: key }, { desc: false })
res.json(items)
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})

3005
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,17 +2,28 @@
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "src/index.ts",
"type": "module",
"scripts": {
"dev": "node --watch index.js",
"start": "node index.js"
"build": "tsc",
"dev": "nodemon --esm src/index.ts",
"start": "ts-node --esm src/index.ts"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"deta": "^2.0.0",
"express": "^4.18.2"
"deta-space-actions": "^0.1.9",
"express": "^4.18.2",
"nanoid": "^3.3.6"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.6.3",
"nodemon": "^3.0.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}

18
backend/src/db.ts Normal file
View file

@ -0,0 +1,18 @@
import { Deta } from 'deta'
import { nanoid } from "nanoid"
export * from '../../src/db/types.js'
const deta = Deta()
export const chats = deta.Base('chats')
export const prompts = deta.Base('prompts')
export const messages = deta.Base('messages')
export const settings = deta.Base('settings')
export const generateKey = (ascending = true) => {
const maxDateNowValue = 8.64e15
const timestamp = ascending ? Date.now() : maxDateNowValue - Date.now()
return `${ timestamp.toString(16) }${ nanoid(5) }`
}

205
backend/src/index.ts Normal file
View file

@ -0,0 +1,205 @@
import express from 'express'
import { CardType, createActions, Inputs } from 'deta-space-actions'
import { chats, messages, prompts, settings, generateKey } from './db.js'
import type { Chat, Prompt, Settings } from './db.js'
import { createChatWithMessage } from './openai.js'
const app = express()
const actions = createActions()
const domain = process.env.DETA_SPACE_APP_HOSTNAME
app.use(express.json())
app.get('/public/chats/:key', async (req, res) => {
const key = req.params.key
const chat = await chats.get(key)
if (!chat || !chat.shared) {
return res.status(404).json({ message: 'Chat not found' })
}
res.json(chat)
})
app.get('/public/chats/:key/messages', async (req, res) => {
const key = req.params.key
const chat = await chats.get(key)
if (!chat || !chat.shared) {
return res.status(404).json({ message: 'Chat not found' })
}
const items = await messages.fetch({ chatId: key }, { desc: false })
res.json(items)
})
actions.add<{ prompt?: string }>({
name: 'create_chat',
title: 'Create Chat',
input: [
Inputs('prompt').String().Optional()
],
card: CardType.DETAIL,
handler: async (event) => {
console.log(`Creating new chat`)
const { prompt: promptName } = event
let prompt
if (promptName) {
const { items } = await prompts.fetch({ title: promptName })
prompt = items[0]
if (!prompt) {
return {
title: 'Prompt not found',
description: 'Please try again with a valid prompt key',
}
}
}
const chat = await chats.put({
description: "New Chat",
...(prompt ? {
prompt: prompt.key,
writingInstructions: prompt.content,
writingCharacter: prompt.writingCharacter,
writingTone: prompt.writingTone,
writingStyle: prompt.writingStyle,
writingFormat: prompt.writingFormat,
} : {}),
totalTokens: 0,
private: false,
createdAt: new Date().toISOString(),
}, generateKey())
if (!chat) {
return {
title: 'Error creating chat',
description: 'Please try again',
}
}
return {
title: chat?.description,
url: `https://${ domain }/chats/${ chat?.key }`
}
}
})
actions.add<{ prompt?: string, message: string }>({
name: 'run_prompt',
title: 'Get Chat Response',
input: [
Inputs('message').String(),
Inputs('prompt').String().Optional()
],
card: CardType.DETAIL,
handler: async (event) => {
const { prompt: promptName, message } = event
let prompt
if (promptName) {
console.log(`Checking if prompt exists`)
const { items } = await prompts.fetch({ title: promptName })
prompt = items[0] as unknown as Prompt
if (!prompt) {
console.log(`Prompt not found`)
}
}
console.log(`Creating new chat`)
const chatRes = await chats.put({
description: "New Chat",
...(prompt ? {
prompt: prompt.key,
writingInstructions: prompt.content,
writingCharacter: prompt.writingCharacter,
writingTone: prompt.writingTone,
writingStyle: prompt.writingStyle,
writingFormat: prompt.writingFormat,
} : {
writingInstructions: promptName,
}),
totalTokens: 0,
private: false,
createdAt: new Date().toISOString(),
}, generateKey())
if (!chatRes) {
return {
title: 'Error creating chat',
description: 'Please try again',
}
}
let chat = (chatRes as unknown as Chat)
const config = (await settings.get('general')) as unknown as Settings;
console.log(`Generating chat response`)
const response = await createChatWithMessage(chat, config, message, prompt)
console.log('response:', response)
chat = (await chats.get(chat?.key)) as unknown as Chat
return {
title: chat?.description,
text: response,
url: `http://localhost:4200/chats/${ chat?.key }`
}
}
})
actions.add({
name: 'list_chats',
title: 'List Chats',
input: [],
card: CardType.LIST,
handler: async () => {
console.log(`Getting all chats`)
const { items } = await chats.fetch({})
return {
items: items.map((chat) => ({
title: chat.description,
url: `https://${ domain }/chats/${ chat.key }`,
card: {
type: '@deta/detail',
data: {
title: chat.description,
ref: `https://${ domain }/chats/${ chat.key }`,
url: `https://${ domain }/chats/${ chat.key }`,
}
}
}))
}
}
})
actions.add({
name: 'list_prompts',
title: 'List Prompts',
input: [],
card: CardType.LIST,
handler: async () => {
console.log(`Getting all prompts`)
const { items } = await prompts.fetch({})
return {
items: items.map((prompt) => ({
title: prompt.title,
url: `https://${ domain }/chats/${ prompt.key }`,
}))
}
}
})
app.use(actions.middleware)
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})

113
backend/src/openai.ts Normal file
View file

@ -0,0 +1,113 @@
import { chats, messages, prompts, generateKey, Prompt, Message, Chat, Settings } from './db.js'
import { getSystemMessage, createChatCompletion } from '../../src/utils/openai.js'
export const createChatWithMessage = async (chat: Chat, settings: Settings, content: string, prompt?: Prompt, previousMessages?: Message[]) => {
let systemMessageValue = ""
if (prompt) {
systemMessageValue = getSystemMessage({
content: prompt.content,
character: prompt?.writingCharacter ?? undefined,
tone: prompt?.writingTone ?? undefined,
style: prompt?.writingStyle ?? undefined,
format: prompt?.writingFormat ?? undefined,
})
const updates = {
prompt: prompt.key,
writingInstructions: prompt.content,
writingCharacter: prompt.writingCharacter,
writingTone: prompt.writingTone,
writingStyle: prompt.writingStyle,
writingFormat: prompt.writingFormat,
}
await chats.update(updates, chat.key)
}
let model = settings?.openAiModel
if (chat?.model) {
model = chat.model
}
if (!systemMessageValue) {
systemMessageValue = getSystemMessage({
content: chat?.writingInstructions ?? undefined,
character: chat?.writingCharacter ?? undefined,
tone: chat?.writingTone ?? undefined,
style: chat?.writingStyle ?? undefined,
format: chat?.writingFormat ?? undefined,
})
}
await messages.put({
chatId: chat.key,
content,
role: "user",
createdAt: new Date().toISOString(),
}, generateKey())
const systemMessage = await messages.put({
chatId: chat.key,
content: "█",
role: "assistant",
createdAt: new Date().toISOString(),
}, generateKey())
const messageId = systemMessage!.key as string
const completionResponse = await createChatCompletion(
{ ...settings, openAiModel: model },
[
{
role: "system",
content: systemMessageValue,
},
...(previousMessages ?? []).map((message) => ({
role: message.role,
content: message.content,
})),
{ role: "user", content },
]
);
const completionContent =
completionResponse.data.choices[0].message?.content;
await messages.update({ content: completionContent }, messageId);
if (chat?.description === "New Chat" || chat?.description === "New Private Chat") {
const res = await messages.fetch({ chatId: chat.key })
const allMessages = res.items as unknown as Message[]
const createChatDescription = await createChatCompletion(
{ ...settings, openAiModel: model },
[
{
role: "system",
content: systemMessageValue,
},
...(allMessages ?? []).map((message) => ({
role: message.role,
content: message.content,
})),
{
role: "user",
content:
"What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed. Don't use quotation marks, just return the text.",
},
]
);
const chatDescription =
createChatDescription.data.choices[0].message?.content;
if (createChatDescription.data.usage) {
const chatUpdates = {
description: chatDescription ?? "New Chat",
// todo: add to existing count instead of replacing
totalTokens: createChatDescription.data.usage!.total_tokens,
}
await chats.update(chatUpdates, chat.key)
}
}
return completionContent
}

17
backend/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"outDir": "lib",
"module": "ES2020",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Recommended",
"include": ["src/**/*", "../../src/db", "../../src/utils"],
"exclude": ["node_modules", "**/*.spec.ts"]
}

View file

@ -2,6 +2,7 @@
"name": "dialogue",
"private": true,
"source": "src/main.tsx",
"type": "module",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"dev": "vite",

View file

@ -1,4 +1,4 @@
{
export default {
"defaultModel": "gpt-3.5-turbo",
"defaultType": "openai",
"defaultAuth": "api-key",

View file

@ -1,51 +1,7 @@
import { Deta } from "deta";
import "dexie-export-import";
import { nanoid } from "nanoid";
export interface Chat {
key: string;
description: string;
totalTokens: number;
prompt?: string | null;
writingInstructions?: string | null;
writingCharacter?: string | null;
writingTone?: string | null;
writingStyle?: string | null;
writingFormat?: string | null;
model?: string | null;
private?: boolean;
shared?: boolean;
createdAt: string;
}
export interface Message {
key: string;
chatId: string;
role: "system" | "assistant" | "user";
content: string;
createdAt: string;
}
export interface Prompt {
key: string;
title: string;
content: string;
writingCharacter?: string | null;
writingTone?: string | null;
writingStyle?: string | null;
writingFormat?: string | null;
createdAt: string;
}
export interface Settings {
key: "general";
openAiApiKey?: string;
openAiModel?: string;
openAiApiType?: 'openai' | 'custom';
openAiApiAuth?: 'none' | 'bearer-token' | 'api-key';
openAiApiBase?: string;
openAiApiVersion?: string;
}
export * from './types.js'
export const deta = Deta()
@ -54,6 +10,7 @@ export const detaDB = {
messages: deta.Base("messages"),
prompts: deta.Base("prompts"),
settings: deta.Base("settings"),
integrations: deta.Base("integrations"),
}
// Used as large number to make sure keys are generated in descending order

50
src/db/types.ts Normal file
View file

@ -0,0 +1,50 @@
export interface Chat {
key: string;
description: string;
totalTokens: number;
prompt?: string | null;
writingInstructions?: string | null;
writingCharacter?: string | null;
writingTone?: string | null;
writingStyle?: string | null;
writingFormat?: string | null;
model?: string | null;
private?: boolean;
shared?: boolean;
createdAt: string;
}
export interface Message {
key: string;
chatId: string;
role: "system" | "assistant" | "user";
content: string;
createdAt: string;
}
export interface Prompt {
key: string;
title: string;
content: string;
writingCharacter?: string | null;
writingTone?: string | null;
writingStyle?: string | null;
writingFormat?: string | null;
createdAt: string;
}
export interface Settings {
key: "general";
openAiApiKey?: string;
openAiModel?: string;
openAiApiType?: 'openai' | 'custom';
openAiApiAuth?: 'none' | 'bearer-token' | 'api-key';
openAiApiBase?: string;
openAiApiVersion?: string;
}
export interface Integration {
key: string;
instance: string
apiKey: string
}

View file

@ -2,12 +2,9 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './components/App'
import './styles/markdown.scss'
import { loadConfig } from './utils/config'
loadConfig().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View file

@ -1,3 +1,5 @@
import configData from '../config.js'
interface Config {
defaultModel: AvailableModel["value"];
defaultType: 'openai' | 'custom';
@ -35,8 +37,4 @@ interface WritingFormat {
label: string;
}
export let config: Config;
export async function loadConfig() {
config = await import('../config.json') as Config;
}
export let config = configData as Config

View file

@ -1,9 +1,9 @@
import { encode } from "gpt-token-utils";
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
import { OpenAIExt } from "openai-ext";
import { Settings, detaDB } from "../db";
import { config } from "./config";
import { useDebounce } from "./debounce";
import { Settings, detaDB } from "../db/index.js";
import { config } from "./config.js";
import { useDebounce } from "./debounce.js";
function getClient(
apiKey: string,