Skip to content

Commit

Permalink
Persist chat history (#382)
Browse files Browse the repository at this point in the history
* Initial pass at history. Uses localStorage for now

* Add history for AI chats in apps

* Put types in shared

* Fix a type
  • Loading branch information
nichochar authored Oct 17, 2024
1 parent b10e4c9 commit 9a189f9
Show file tree
Hide file tree
Showing 13 changed files with 417 additions and 51 deletions.
29 changes: 28 additions & 1 deletion packages/api/config.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { eq, and, inArray } from 'drizzle-orm';
import { type SecretWithAssociatedSessions, randomid } from '@srcbook/shared';
import { configs, type Config, secrets, type Secret, secretsToSession } from './db/schema.mjs';
import { MessageType, HistoryType } from '@srcbook/shared';
import {
configs,
type Config,
secrets,
type Secret,
secretsToSession,
apps,
} from './db/schema.mjs';
import { db } from './db/index.mjs';
import { HOME_DIR } from './constants.mjs';

Expand Down Expand Up @@ -44,6 +52,25 @@ export async function updateConfig(attrs: Partial<Config>) {
return db.update(configs).set(attrs).returning();
}

export async function getHistory(appId: string): Promise<HistoryType> {
const results = await db.select().from(apps).where(eq(apps.externalId, appId)).limit(1);
const history = results[0]!.history;
return JSON.parse(history);
}

export async function appendToHistory(appId: string, messages: MessageType | MessageType[]) {
const results = await db.select().from(apps).where(eq(apps.externalId, appId)).limit(1);
const history = results[0]!.history;
const decodedHistory = JSON.parse(history);
const newHistory = Array.isArray(messages)
? [...decodedHistory, ...messages]
: [...decodedHistory, messages];
await db
.update(apps)
.set({ history: JSON.stringify(newHistory) })
.where(eq(apps.externalId, appId));
}

export async function getSecrets(): Promise<Array<SecretWithAssociatedSessions>> {
const secretsResult = await db.select().from(secrets);
const secretsToSessionResult = await db
Expand Down
2 changes: 2 additions & 0 deletions packages/api/db/schema.mts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const apps = sqliteTable('apps', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
externalId: text('external_id').notNull().unique(),
history: text('history').notNull().default('[]'), // JSON encoded value of the history
historyVersion: integer('history_version').notNull().default(1), // internal versioning of history type for migrations
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`),
Expand Down
2 changes: 2 additions & 0 deletions packages/api/drizzle/0012_add_app_history.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `apps` ADD `history` text DEFAULT '[]' NOT NULL;--> statement-breakpoint
ALTER TABLE `apps` ADD `history_version` integer DEFAULT 1 NOT NULL;
262 changes: 262 additions & 0 deletions packages/api/drizzle/meta/0012_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0e479af1-dade-4a47-88c8-438284446e01",
"prevId": "07a808e8-5059-4731-9f5b-d1a3fc530501",
"tables": {
"apps": {
"name": "apps",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"external_id": {
"name": "external_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"history": {
"name": "history",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"history_version": {
"name": "history_version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {
"apps_external_id_unique": {
"name": "apps_external_id_unique",
"columns": [
"external_id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"config": {
"name": "config",
"columns": {
"base_dir": {
"name": "base_dir",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_language": {
"name": "default_language",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'typescript'"
},
"openai_api_key": {
"name": "openai_api_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"anthropic_api_key": {
"name": "anthropic_api_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled_analytics": {
"name": "enabled_analytics",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"srcbook_installation_id": {
"name": "srcbook_installation_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'jq2c0p9pf57ssvee9fp8bhs2sk'"
},
"ai_provider": {
"name": "ai_provider",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'openai'"
},
"ai_model": {
"name": "ai_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'gpt-4o'"
},
"ai_base_url": {
"name": "ai_base_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"subscription_email": {
"name": "subscription_email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"secrets": {
"name": "secrets",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"secrets_name_unique": {
"name": "secrets_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"secrets_to_sessions": {
"name": "secrets_to_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"secret_id": {
"name": "secret_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"secrets_to_sessions_session_id_secret_id_unique": {
"name": "secrets_to_sessions_session_id_secret_id_unique",
"columns": [
"session_id",
"secret_id"
],
"isUnique": true
}
},
"foreignKeys": {
"secrets_to_sessions_secret_id_secrets_id_fk": {
"name": "secrets_to_sessions_secret_id_secrets_id_fk",
"tableFrom": "secrets_to_sessions",
"tableTo": "secrets",
"columnsFrom": [
"secret_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
7 changes: 7 additions & 0 deletions packages/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"when": 1729112512747,
"tag": "0011_remove_language_from_apps",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1729193497907,
"tag": "0012_add_app_history",
"breakpoints": true
}
]
}
16 changes: 16 additions & 0 deletions packages/api/server/http.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
updateConfig,
getSecrets,
addSecret,
getHistory,
appendToHistory,
removeSecret,
associateSecretWithSession,
disassociateSecretWithSession,
Expand Down Expand Up @@ -693,3 +695,17 @@ router.post('/apps/:id/files/rename', cors(), async (req, res) => {
app.use('/api', router);

export default app;

router.options('/apps/:id/history', cors());
router.get('/apps/:id/history', cors(), async (req, res) => {
const { id } = req.params;
const history = await getHistory(id);
return res.json({ data: history });
});

router.post('/apps/:id/history', cors(), async (req, res) => {
const { id } = req.params;
const { messages } = req.body;
await appendToHistory(id, messages);
return res.json({ data: { success: true } });
});
1 change: 1 addition & 0 deletions packages/shared/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './src/schemas/websockets.mjs';
export * from './src/types/apps.mjs';
export * from './src/types/cells.mjs';
export * from './src/types/tsserver.mjs';
export * from './src/types/history.mjs';
export * from './src/types/websockets.mjs';
export * from './src/types/secrets.mjs';
export * from './src/utils.mjs';
Expand Down
Loading

0 comments on commit 9a189f9

Please sign in to comment.