Skip to content

Commit a4c2916

Browse files
authored
Merge pull request #64 from zackify/feature/context-routes
feat: Add context storage and retrieval routes with MCP tools
2 parents 65a55b3 + 74862b7 commit a4c2916

16 files changed

+860
-105
lines changed

src/database/database.ts

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,9 @@ if (process.env.SQLITE_PATH) {
66
Database.setCustomSQLite(process.env.SQLITE_PATH);
77
}
88

9-
/**
10-
* Get the database instance
11-
* This function always returns the current database,
12-
* checking for a test database first, then falling back to the singleton
13-
*/
14-
export function getDb(): Database {
15-
// For tests, we support injecting a test database instance
16-
const testDb = (globalThis as any).testDb;
17-
if (testDb) {
18-
return testDb;
19-
}
20-
21-
// For normal operation, create a singleton
22-
if (!(globalThis as any)._dbSingleton) {
23-
const dbPath = process.env.DATABASE_PATH || "./data/db.sqlite";
24-
console.log(`Creating database connection to ${dbPath}`);
25-
26-
(globalThis as any)._dbSingleton = new Database(dbPath);
27-
(globalThis as any)._dbSingleton.exec("PRAGMA journal_mode = WAL;");
28-
sqliteVec.load((globalThis as any)._dbSingleton);
29-
}
30-
31-
return (globalThis as any)._dbSingleton;
32-
}
9+
const dbPath = process.env.DATABASE_PATH || "./data/db.sqlite";
10+
console.log(`Creating database connection to ${dbPath}`);
3311

34-
// Create and export a db proxy that always returns the current database instance
35-
// This ensures that even code using the imported db directly will get the test db when appropriate
36-
export const db = new Proxy({} as Database, {
37-
get: function(target, prop) {
38-
const currentDb = getDb();
39-
const value = currentDb[prop as keyof Database];
40-
41-
// If it's a function, bind it to the correct database instance
42-
if (typeof value === 'function') {
43-
return value.bind(currentDb);
44-
}
45-
46-
return value;
47-
}
48-
});
12+
export const db = new Database(dbPath);
13+
db.exec("PRAGMA journal_mode = WAL;");
14+
sqliteVec.load(db);

src/database/metadata.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import { db } from "./database";
22

3+
/**
4+
* Checks if the metadata table exists
5+
* @returns true if metadata table exists, false otherwise
6+
*/
7+
export const metadataTableExists = (): boolean => {
8+
try {
9+
const result = db
10+
.query("SELECT name FROM sqlite_master WHERE type='table' AND name='metadata'")
11+
.get();
12+
return !!result;
13+
} catch (error) {
14+
return false;
15+
}
16+
};
17+
318
/**
419
* Gets a metadata value from the database
520
* @param key The metadata key to retrieve
621
* @returns The value as string or null if not found
722
*/
823
export const getMetadataValue = (key: string): string | null => {
924
try {
25+
// Return null if metadata table doesn't exist (brand new database)
26+
if (!metadataTableExists()) {
27+
return null;
28+
}
29+
1030
const result = db
1131
.query("SELECT value FROM metadata WHERE key = ?")
1232
.get(key);
@@ -26,7 +46,12 @@ export const getMetadataValue = (key: string): string | null => {
2646
*/
2747
export const setMetadataValue = (key: string, value: string): boolean => {
2848
try {
29-
// Ensure the metadata table exists before writing to it
49+
// If metadata table doesn't exist, we can't set metadata yet
50+
// This will be handled after migrations run
51+
if (!metadataTableExists()) {
52+
console.warn(`Cannot set metadata for key ${key}: metadata table does not exist yet`);
53+
return false;
54+
}
3055

3156
// Check if the key already exists
3257
const exists = db.query("SELECT 1 FROM metadata WHERE key = ?").get(key);

src/database/migrations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ export const migrations: Migration[] = [
9898
return db.exec(`DROP TABLE metadata;`);
9999
},
100100
},
101+
{
102+
name: "create_external_id_source_index",
103+
up: () => {
104+
return db.exec(`
105+
CREATE INDEX IF NOT EXISTS idx_documents_external_id_source
106+
ON documents(external_id, source);
107+
`);
108+
},
109+
down: () => {
110+
return db.exec(`DROP INDEX IF EXISTS idx_documents_external_id_source;`);
111+
},
112+
},
101113

102114
// Add more migrations here
103115
];

src/database/reembedding.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const reembedAllDocuments = async (): Promise<void> => {
2424
console.log("Updating database schema to accommodate new embedding size...");
2525

2626
// Alter documents table to handle potential changes in embedding dimensions
27-
await db.exec(`
27+
db.exec(`
2828
PRAGMA foreign_keys=off;
2929
BEGIN TRANSACTION;
3030
@@ -33,10 +33,12 @@ export const reembedAllDocuments = async (): Promise<void> => {
3333
3434
-- Drop original table
3535
DROP TABLE documents;
36-
37-
-- Recreate table with updated schema and embedding size
38-
${createDocumentsTableSQL(embeddingSize)}
39-
36+
`);
37+
38+
// Recreate table with updated schema and embedding size
39+
db.exec(createDocumentsTableSQL(embeddingSize));
40+
41+
db.exec(`
4042
-- Copy data back
4143
INSERT INTO documents (id, external_id, text, metadata, source, created_at)
4244
SELECT id, external_id, text, metadata, source, created_at FROM temp_documents;
@@ -49,7 +51,7 @@ export const reembedAllDocuments = async (): Promise<void> => {
4951
`);
5052

5153
// Similar process for document_chunks table
52-
await db.exec(`
54+
db.exec(`
5355
PRAGMA foreign_keys=off;
5456
BEGIN TRANSACTION;
5557
@@ -58,10 +60,12 @@ export const reembedAllDocuments = async (): Promise<void> => {
5860
5961
-- Drop original table
6062
DROP TABLE document_chunks;
61-
62-
-- Recreate table with updated schema and embedding size
63-
${createDocumentChunksTableSQL(embeddingSize)}
64-
63+
`);
64+
65+
// Recreate table with updated schema and embedding size
66+
db.exec(createDocumentChunksTableSQL(embeddingSize));
67+
68+
db.exec(`
6569
-- Copy data back
6670
INSERT INTO document_chunks (id, document_id, text, created_at)
6771
SELECT id, document_id, text, created_at FROM temp_document_chunks;
@@ -76,7 +80,7 @@ export const reembedAllDocuments = async (): Promise<void> => {
7680
console.log("Database schema updated successfully");
7781

7882
// Prepare database for batch operations
79-
await db.run("BEGIN TRANSACTION");
83+
db.exec("BEGIN TRANSACTION");
8084

8185
// Get all documents
8286
const documents = db.query("SELECT id, text FROM documents").all() as { id: number; text: string }[];
@@ -105,8 +109,8 @@ export const reembedAllDocuments = async (): Promise<void> => {
105109

106110
// Commit in batches to avoid holding transaction too long
107111
if (processedDocs % 100 === 0 && processedDocs !== documents.length) {
108-
await db.run("COMMIT");
109-
await db.run("BEGIN TRANSACTION");
112+
db.exec("COMMIT");
113+
db.exec("BEGIN TRANSACTION");
110114
}
111115
}
112116
}
@@ -116,10 +120,10 @@ export const reembedAllDocuments = async (): Promise<void> => {
116120
}
117121

118122
// Commit document changes
119-
await db.run("COMMIT");
123+
db.exec("COMMIT");
120124

121125
// Start new transaction for chunks
122-
await db.run("BEGIN TRANSACTION");
126+
db.exec("BEGIN TRANSACTION");
123127

124128
// Get all document chunks
125129
const chunks = db.query("SELECT id, document_id, text FROM document_chunks").all() as {
@@ -153,8 +157,8 @@ export const reembedAllDocuments = async (): Promise<void> => {
153157

154158
// Commit in batches to avoid holding transaction too long
155159
if (processedChunks % 200 === 0 && processedChunks !== chunks.length) {
156-
await db.run("COMMIT");
157-
await db.run("BEGIN TRANSACTION");
160+
db.exec("COMMIT");
161+
db.exec("BEGIN TRANSACTION");
158162
}
159163
}
160164
}
@@ -168,13 +172,13 @@ export const reembedAllDocuments = async (): Promise<void> => {
168172
setMetadataValue("AI_EMBEDDING_SIZE", embeddingSize || "");
169173

170174
// Commit chunk changes
171-
await db.run("COMMIT");
175+
db.exec("COMMIT");
172176

173177
console.log("Re-embedding process completed successfully");
174178
} catch (error) {
175179
// Ensure transaction is rolled back if an error occurs
176180
try {
177-
await db.run("ROLLBACK");
181+
db.exec("ROLLBACK");
178182
} catch (rollbackError) {
179183
console.error("Error during rollback:", rollbackError);
180184
}

src/routes/context/getContext.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { z } from "zod";
2+
import { corsHeaders as headers } from "../../shared/corsHeaders";
3+
import { db } from "../../database/database";
4+
5+
const schema = z.object({
6+
key: z.string({ required_error: "Key field is required" }),
7+
});
8+
9+
export type GetContextResult = { error: string } | { context: { key: string; message: string; metadata: Record<string, any> } };
10+
export type GetContextProps = z.infer<typeof schema>;
11+
12+
export const getContext = async (body: GetContextProps): Promise<GetContextResult> => {
13+
const { error, data, success } = schema.safeParse(body);
14+
15+
if (!success) {
16+
return {
17+
error: error.issues?.[0]?.message || "Validation failed",
18+
};
19+
}
20+
21+
try {
22+
const context = db
23+
.query("SELECT text, metadata FROM documents WHERE external_id = ? AND source = 'context'")
24+
.get(data.key) as
25+
| { text: string; metadata: string }
26+
| undefined;
27+
28+
if (!context) {
29+
return { error: "Context not found" };
30+
}
31+
32+
return {
33+
context: {
34+
key: data.key,
35+
message: context.text,
36+
metadata: JSON.parse(context.metadata),
37+
},
38+
};
39+
} catch (e) {
40+
console.error("Error getting context:", e);
41+
return { error: "Failed to get context" };
42+
}
43+
};
44+
45+
export const getContextRoute = async (request: Request) => {
46+
const body = await request.json();
47+
const result = await getContext(body);
48+
49+
if ("error" in result) {
50+
return Response.json(result, {
51+
status: 400,
52+
headers,
53+
});
54+
}
55+
56+
return Response.json(result, { headers });
57+
};

src/routes/context/setContext.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { z } from "zod";
2+
import { generateEmbeddings } from "../../shared/generateEmbeddings";
3+
import { corsHeaders as headers } from "../../shared/corsHeaders";
4+
import { db } from "../../database/database";
5+
6+
const schema = z.object({
7+
key: z.string({ required_error: "Key field is required" }),
8+
message: z.string({ required_error: "Message field is required" }),
9+
});
10+
11+
export type SetContextResult = { error: string } | { message: string };
12+
export type SetContextProps = z.infer<typeof schema>;
13+
14+
export const setContext = async (body: SetContextProps): Promise<SetContextResult> => {
15+
const { error, data, success } = schema.safeParse(body);
16+
17+
if (!success) {
18+
return {
19+
error: error.issues?.[0]?.message || "Validation failed",
20+
};
21+
}
22+
23+
const embeddings = await generateEmbeddings(data.message, {
24+
apiKey: process.env.AI_API_KEY as string,
25+
baseURL: process.env.AI_BASE_URL,
26+
});
27+
28+
if (!embeddings) {
29+
return { error: "Failed to generate embeddings" };
30+
}
31+
32+
try {
33+
// Use a transaction for atomic operation
34+
db.run("BEGIN TRANSACTION");
35+
36+
try {
37+
// Use JSON.stringify for embeddings array (more efficient than join)
38+
const embeddingsStr = JSON.stringify(embeddings);
39+
const metadata = JSON.stringify({ type: "context" });
40+
41+
// Use INSERT OR REPLACE for simpler logic
42+
db.query(
43+
`
44+
INSERT OR REPLACE INTO documents (external_id, text, embeddings, source, metadata)
45+
VALUES (?, ?, ?, 'context', ?)
46+
`
47+
).run(
48+
data.key,
49+
data.message,
50+
embeddingsStr,
51+
metadata
52+
);
53+
54+
db.run("COMMIT");
55+
return { message: "Context successfully set" };
56+
} catch (e) {
57+
db.run("ROLLBACK");
58+
throw e;
59+
}
60+
} catch (e) {
61+
console.error("Error setting context:", e);
62+
return { error: "Failed to set context" };
63+
}
64+
};
65+
66+
export const setContextRoute = async (request: Request) => {
67+
const body = await request.json();
68+
const result = await setContext(body);
69+
70+
if ("error" in result) {
71+
return Response.json(result, {
72+
status: 400,
73+
headers,
74+
});
75+
}
76+
77+
return Response.json(result, { headers });
78+
};

0 commit comments

Comments
 (0)