├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── data ├── documents │ ├── getter.ts │ ├── setter.ts │ ├── vector-getter.ts │ └── vector-setter.ts ├── schema.sql └── schema.ts ├── helpers ├── embed.ts ├── env.ts ├── google.ts ├── llm.ts └── openai.ts ├── lib ├── assert.ts ├── not-empty.ts ├── text-splitter.ts ├── tokenize.ts ├── truncate.ts └── unique.ts ├── openapi.json ├── package.json ├── pnpm-lock.yaml ├── public ├── index.html └── openapi.json ├── src ├── middleware │ ├── auth.ts │ └── cache.ts ├── routes │ ├── chat-documents-suggest.ts │ ├── chat.ts │ ├── documents-ai-search.ts │ ├── documents-retrive.ts │ ├── documents-search.ts │ ├── documents-submit.ts │ └── documents-suggest.ts └── worker.ts ├── tsconfig.json └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | .dev* 4 | .wrangler 5 | TODO.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alex MacCaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LawGPT Search 2 | 3 | A vector search API for legal documents powered by Cloudflare Workers, D1, and Vectorize. This API allows you to submit legal documents, search through them semantically, and get AI-powered answers to legal questions. 4 | 5 | ## Features 6 | 7 | - Document submission with automatic text chunking and embedding 8 | - Semantic search across documents 9 | - AI-powered question answering using the document context 10 | - Namespace support for document organization 11 | - Built on Cloudflare's edge infrastructure 12 | 13 | ## Setup 14 | 15 | 1. Clone this repository 16 | 2. Set up a [Cloudflare](https://www.cloudflare.com/) Page with their admin 17 | 3. Run the setup commands: 18 | 19 | ```bash 20 | pnpm 21 | pnpm setup-cloudflare 22 | pnpm load-schema 23 | ``` 24 | 25 | ## Environment Variables 26 | 27 | - `OPENAI_API_KEY`: Your OpenAI API key 28 | - `AUTH_SECRET`: A secret used to authenticate requests to the API (optional) 29 | 30 | ## API Endpoints 31 | 32 | ### 1. Search Documents 33 | 34 | ```bash 35 | curl -X GET "http://localhost:8787/documents/search" \ 36 | -H "Content-Type: application/json" \ 37 | -d '{ 38 | "query": "your search query", 39 | "namespace": "default" 40 | }' 41 | ``` 42 | 43 | ### 2. Submit Document 44 | 45 | ```bash 46 | curl -X POST "http://localhost:8787/documents" \ 47 | -H "Content-Type: application/json" \ 48 | -H "Authorization: Bearer your-auth-secret" \ 49 | -d '{ 50 | "url": "https://example.com/document", 51 | "text": "Your document text here", 52 | "namespace": "default" 53 | }' 54 | ``` 55 | 56 | ### 3. Suggest Documents 57 | 58 | ```bash 59 | curl -X POST "http://localhost:8787/documents/suggest" \ 60 | -H "Content-Type: application/json" \ 61 | -d '{ 62 | "query": "document title search", 63 | "namespace": "default" 64 | }' 65 | ``` 66 | 67 | ### 4. Retrieve Document 68 | 69 | ```bash 70 | curl -X GET "http://localhost:8787/documents/123" \ 71 | -H "Content-Type: application/json" \ 72 | -H "Authorization: Bearer your-auth-secret" 73 | ``` 74 | 75 | ### 5. Chat/Answer 76 | 77 | ```bash 78 | curl -X POST "http://localhost:8787/chat" \ 79 | -H "Content-Type: application/json" \ 80 | -d '{ 81 | "query": "your question about the documents", 82 | "namespace": "default" 83 | }' 84 | ``` 85 | 86 | ### 6. Chat Document Suggestions 87 | 88 | ```bash 89 | curl -X POST "http://localhost:8787/chat/suggest" \ 90 | -H "Content-Type: application/json" \ 91 | -H "Authorization: Bearer your-auth-secret" \ 92 | -d '{ 93 | "query": "your question for document suggestions", 94 | "namespace": "default" 95 | }' 96 | ``` 97 | 98 | ## Development 99 | 100 | Run the development server: 101 | 102 | ```bash 103 | pnpm dev 104 | ``` 105 | 106 | ### Setup 107 | 108 | ```bash 109 | pnpm setup-cloudflare 110 | pnpm load-schema --remote 111 | ``` 112 | -------------------------------------------------------------------------------- /data/documents/getter.ts: -------------------------------------------------------------------------------- 1 | import { Env } from '@/helpers/env' 2 | import { Document, DocumentWithoutText } from '../schema' 3 | import { queryDocumentVectors } from './vector-getter' 4 | 5 | export async function searchPartialDocumentsByContent({ 6 | text, 7 | namespace, 8 | limit = 25, 9 | env, 10 | }: { 11 | text: string 12 | namespace: string 13 | env: Env 14 | limit?: number 15 | }): Promise { 16 | const documentMatches = await queryDocumentVectors({ 17 | text, 18 | namespace, 19 | env, 20 | limit, 21 | }) 22 | 23 | if (documentMatches.length === 0) { 24 | return [] 25 | } 26 | 27 | const documentsResult = await env.DB.prepare( 28 | `SELECT id, url, title, summary, namespace, indexed_at FROM documents WHERE id IN (${documentMatches 29 | .map(({ documentId }) => '?') 30 | .join(',')}) AND namespace = ?` 31 | ) 32 | .bind(...documentMatches.map(({ documentId }) => documentId), namespace) 33 | .all() 34 | 35 | return documentsResult.results 36 | } 37 | 38 | export async function searchDocumentsByContent({ 39 | text, 40 | namespace, 41 | limit = 25, 42 | env, 43 | }: { 44 | text: string 45 | namespace: string 46 | env: Env 47 | limit?: number 48 | }): Promise { 49 | const documentMatches = await queryDocumentVectors({ 50 | text, 51 | namespace, 52 | env, 53 | limit, 54 | }) 55 | 56 | if (documentMatches.length === 0) { 57 | return [] 58 | } 59 | 60 | const documentsResult = await env.DB.prepare( 61 | `SELECT * FROM documents WHERE id IN (${documentMatches 62 | .map(({ documentId }) => '?') 63 | .join(',')}) AND namespace = ?` 64 | ) 65 | .bind(...documentMatches.map(({ documentId }) => documentId), namespace) 66 | .all() 67 | 68 | return documentsResult.results 69 | } 70 | 71 | export async function searchDocumentsByTitle({ 72 | title, 73 | namespace, 74 | env, 75 | }: { 76 | title: string 77 | namespace: string 78 | env: Env 79 | }): Promise { 80 | const result = await env.DB.prepare( 81 | ` 82 | SELECT d.id, d.url, d.title, d.namespace, d.summary, d.indexed_at 83 | FROM documents d 84 | JOIN documents_search ds ON d.id = ds.rowid 85 | WHERE ds.title LIKE ? COLLATE NOCASE AND d.namespace = ? 86 | ` 87 | ) 88 | .bind(title, namespace) 89 | .all() 90 | 91 | return result.results 92 | } 93 | 94 | export async function getDocumentsByIds({ 95 | ids, 96 | namespace, 97 | env, 98 | }: { 99 | ids: number[] 100 | namespace: string 101 | env: Env 102 | }): Promise { 103 | const result = await env.DB.prepare( 104 | `SELECT * FROM documents WHERE id IN (${ids 105 | .map(() => '?') 106 | .join(',')}) AND namespace = ?` 107 | ) 108 | .bind(...ids, namespace) 109 | .all() 110 | 111 | return result.results 112 | } 113 | 114 | export async function getDocumentById({ 115 | id, 116 | namespace, 117 | env, 118 | }: { 119 | id: number 120 | namespace: string 121 | env: Env 122 | }): Promise { 123 | const result = await env.DB.prepare( 124 | `SELECT * FROM documents WHERE id = ? AND namespace = ?` 125 | ) 126 | .bind(id, namespace) 127 | .first() 128 | 129 | return result 130 | } 131 | -------------------------------------------------------------------------------- /data/documents/setter.ts: -------------------------------------------------------------------------------- 1 | import { Env } from '@/helpers/env' 2 | import { extractDocumentMetadata } from '@/helpers/llm' 3 | import { getOpenAIProvider } from '@/helpers/openai' 4 | import { Document } from '../schema' 5 | import { upsertDocumentVectors } from './vector-setter' 6 | 7 | interface InsertDocument { 8 | url: string 9 | namespace: string 10 | text: string 11 | } 12 | 13 | export async function insertDocument({ 14 | document, 15 | env, 16 | }: { 17 | document: InsertDocument 18 | env: Env 19 | }) { 20 | const openaiProvider = getOpenAIProvider(env) 21 | 22 | const metadata = await extractDocumentMetadata({ 23 | text: document.text, 24 | model: openaiProvider('gpt-3.5-turbo'), 25 | }) 26 | 27 | // Insert into D1 database first 28 | const result = await env.DB.prepare( 29 | `INSERT OR REPLACE INTO documents (url, namespace, title, text, summary) 30 | VALUES (?, ?, ?, ?, ?)` 31 | ) 32 | .bind( 33 | document.url, 34 | document.namespace, 35 | metadata.title, 36 | document.text, 37 | metadata.summary 38 | ) 39 | .run() 40 | 41 | if (!result.success) { 42 | throw new Error(`Failed to insert document into D1: ${result.error}`) 43 | } 44 | 45 | // Get the inserted document ID 46 | const documentId = result.meta.last_row_id 47 | 48 | await upsertDocumentVectors({ 49 | documentId, 50 | title: metadata.title, 51 | summary: metadata.summary, 52 | text: document.text, 53 | namespace: document.namespace, 54 | env, 55 | }) 56 | 57 | return documentId 58 | } 59 | -------------------------------------------------------------------------------- /data/documents/vector-getter.ts: -------------------------------------------------------------------------------- 1 | import { generateEmbedding } from '@/helpers/embed' 2 | import { Env } from '@/helpers/env' 3 | import { unique } from '@/lib/unique' 4 | 5 | export type DocumentMatch = { 6 | documentId: number 7 | score: number 8 | } 9 | 10 | export async function queryDocumentVectors({ 11 | text, 12 | namespace, 13 | env, 14 | limit, 15 | }: { 16 | text: string 17 | namespace: string 18 | env: Env 19 | limit: number 20 | }): Promise { 21 | const embedding = await generateEmbedding(text, env) 22 | 23 | const vectorizeMatches = await env.VECTORIZE.query(embedding, { 24 | namespace, 25 | topK: limit, 26 | returnValues: false, 27 | returnMetadata: 'indexed', 28 | }) 29 | 30 | const results = unique( 31 | vectorizeMatches.matches 32 | .map(({ metadata, score }) => ({ 33 | documentId: metadata?.document_id as number, 34 | score, 35 | })) 36 | .filter(({ documentId }) => documentId !== undefined) 37 | ) 38 | 39 | return results 40 | } 41 | -------------------------------------------------------------------------------- /data/documents/vector-setter.ts: -------------------------------------------------------------------------------- 1 | import { generateEmbedding } from '@/helpers/embed' 2 | import { Env } from '@/helpers/env' 3 | import { RecursiveCharacterTextSplitter } from '@/lib/text-splitter' 4 | import { truncateString } from '@/lib/truncate' 5 | import { DocumentVectorType } from '../schema' 6 | 7 | export async function upsertDocumentVectors({ 8 | documentId, 9 | title, 10 | summary, 11 | text, 12 | namespace, 13 | env, 14 | }: { 15 | documentId: number 16 | title: string 17 | summary: string 18 | text: string 19 | namespace: string 20 | env: Env 21 | }) { 22 | const vectorMetadataParagraphs = await splitDocumentText(text) 23 | 24 | const vectorMetadata: DocumentVectorMetadata[] = [ 25 | { 26 | document_id: documentId, 27 | text: title, 28 | type: 'title', 29 | }, 30 | { 31 | document_id: documentId, 32 | text: summary, 33 | type: 'summary', 34 | }, 35 | ...vectorMetadataParagraphs.slice(0, 300).map((text) => ({ 36 | document_id: documentId, 37 | text, 38 | type: 'paragraph' as DocumentVectorType, 39 | })), 40 | ] 41 | 42 | const vectorMetadataWithEmbeddings = await Promise.all( 43 | vectorMetadata.map(async (metadata) => ({ 44 | ...metadata, 45 | embedding: await generateEmbedding(metadata.text, env), 46 | })) 47 | ) 48 | 49 | const validVectorMetadataWithEmbeddings = vectorMetadataWithEmbeddings.filter( 50 | ({ embedding }) => embedding.length > 0 51 | ) 52 | 53 | // Insert into vector database 54 | await env.VECTORIZE.upsert( 55 | validVectorMetadataWithEmbeddings.map(({ text, embedding, type }) => ({ 56 | id: crypto.randomUUID(), 57 | values: embedding, 58 | namespace, 59 | metadata: { 60 | document_id: documentId, 61 | text: truncateString(text, MAX_METADATA_LENGTH), 62 | type, 63 | }, 64 | })) 65 | ) 66 | } 67 | 68 | const MAX_METADATA_LENGTH = 9216 69 | 70 | async function splitDocumentText(text: string): Promise { 71 | const splitter = new RecursiveCharacterTextSplitter({ 72 | chunkSize: 1000, 73 | chunkOverlap: 0, 74 | }) 75 | 76 | return await splitter.splitText(text) 77 | } 78 | 79 | type DocumentVectorMetadata = { 80 | document_id: number 81 | text: string 82 | type: DocumentVectorType 83 | } 84 | -------------------------------------------------------------------------------- /data/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS documents; 2 | DROP TABLE IF EXISTS documents_search; 3 | 4 | -- Main table with all constraints and types 5 | CREATE TABLE documents ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | url TEXT NOT NULL, 8 | title TEXT NOT NULL, 9 | namespace TEXT NOT NULL, 10 | text TEXT NOT NULL, 11 | summary TEXT NOT NULL, 12 | indexed_at INTEGER NOT NULL DEFAULT (unixepoch()), 13 | 14 | UNIQUE(url, namespace) 15 | ); 16 | 17 | -- FTS5 table for search functionality (only title and summary) 18 | CREATE VIRTUAL TABLE documents_search USING fts5 ( 19 | title, 20 | summary, 21 | content='documents', 22 | content_rowid='id' 23 | ); 24 | 25 | -- Indexes for the main table 26 | CREATE INDEX documents_namespace_idx ON documents(namespace); 27 | CREATE INDEX documents_url_idx ON documents(namespace, url); 28 | CREATE INDEX documents_title_idx ON documents(namespace, title); 29 | 30 | -- Triggers to keep FTS index up to date (modified to only sync title and summary) 31 | CREATE TRIGGER documents_ai AFTER INSERT ON documents BEGIN 32 | INSERT INTO documents_search(rowid, title, summary) 33 | VALUES (new.id, new.title, new.summary); 34 | END; 35 | 36 | CREATE TRIGGER documents_ad AFTER DELETE ON documents BEGIN 37 | INSERT INTO documents_search(documents_search, rowid, title, summary) 38 | VALUES('delete', old.id, old.title, old.summary); 39 | END; 40 | 41 | CREATE TRIGGER documents_au AFTER UPDATE ON documents BEGIN 42 | INSERT INTO documents_search(documents_search, rowid, title, summary) 43 | VALUES('delete', old.id, old.title, old.summary); 44 | INSERT INTO documents_search(rowid, title, summary) 45 | VALUES (new.id, new.title, new.summary); 46 | END; -------------------------------------------------------------------------------- /data/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Document { 2 | id: number 3 | url: string 4 | namespace: string 5 | title: string 6 | text: string 7 | summary: string 8 | indexed_at: number 9 | } 10 | 11 | export type DocumentWithoutText = Omit 12 | 13 | export type DocumentSearchResult = DocumentWithoutText & { 14 | score: number 15 | } 16 | 17 | export type DocumentVectorType = 'title' | 'summary' | 'paragraph' 18 | -------------------------------------------------------------------------------- /helpers/embed.ts: -------------------------------------------------------------------------------- 1 | import { Env } from './env' 2 | 3 | export async function generateEmbedding( 4 | text: string, 5 | env: Env 6 | ): Promise { 7 | if (!text) { 8 | throw new Error('Text is required') 9 | } 10 | 11 | const embedding = await env.AI.run('@cf/baai/bge-large-en-v1.5', { 12 | text, 13 | }) 14 | 15 | // Sometimes the embedding is [null, null, null, ...] 16 | if (!embedding.data[0] || embedding.data[0][0] === null) { 17 | console.warn(`Embedding is empty for text: ${text}`) 18 | return [] 19 | } 20 | 21 | return embedding.data[0] 22 | } 23 | -------------------------------------------------------------------------------- /helpers/env.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | OPENAI_API_KEY: string 3 | GOOGLE_API_KEY: string 4 | AUTH_SECRET: string 5 | AI: Ai 6 | VECTORIZE: Vectorize 7 | DB: D1Database 8 | } 9 | -------------------------------------------------------------------------------- /helpers/google.ts: -------------------------------------------------------------------------------- 1 | import { createGoogleGenerativeAI } from '@ai-sdk/google' 2 | import { Env } from './env' 3 | 4 | export function getGoogleAIProvider(env: Env) { 5 | return createGoogleGenerativeAI({ 6 | apiKey: env.GOOGLE_API_KEY, 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /helpers/llm.ts: -------------------------------------------------------------------------------- 1 | import { generateObject, LanguageModelV1 } from 'ai' 2 | import { z } from 'zod' 3 | import { truncateText } from '../lib/tokenize' 4 | 5 | const documentSchema = z.object({ 6 | title: z.string().describe('The title of the document'), 7 | summary: z.string().describe('A short summary of the document'), 8 | tags: z.array(z.string()).describe('A list of tags for the document'), 9 | }) 10 | 11 | export async function extractDocumentMetadata({ 12 | text, 13 | model, 14 | }: { 15 | text: string 16 | model: LanguageModelV1 17 | }) { 18 | const { object } = await generateObject({ 19 | model, 20 | schema: documentSchema, 21 | prompt: truncateText( 22 | `Analyze the following text and extract key metadata. Create a concise title that captures the main topic, write a brief summary (1-2 sentences), and generate relevant tags that categorize the content: ${text}`, 23 | MAX_TOKENS 24 | ), 25 | }) 26 | 27 | return object 28 | } 29 | 30 | // 16385 is the maximum number of tokens for the gpt-4o model 31 | const MAX_TOKENS = 16385 - 1000 32 | -------------------------------------------------------------------------------- /helpers/openai.ts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from '@ai-sdk/openai' 2 | import { Env } from './env' 3 | 4 | export function getOpenAIProvider(env: Env) { 5 | return createOpenAI({ 6 | apiKey: env.OPENAI_API_KEY, 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /lib/assert.ts: -------------------------------------------------------------------------------- 1 | class AssertError extends Error { 2 | constructor(message?: string) { 3 | super(message) 4 | this.name = 'AssertError' 5 | } 6 | } 7 | 8 | export function assert(condition: any, message?: string): asserts condition { 9 | if (!condition) { 10 | throw new AssertError(message) 11 | } 12 | } 13 | 14 | export function assertString( 15 | value: any, 16 | message?: string 17 | ): asserts value is string { 18 | if (typeof value !== 'string' || value.length === 0) { 19 | throw new AssertError(message) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/not-empty.ts: -------------------------------------------------------------------------------- 1 | export function notEmpty(value: T | null | undefined): value is T { 2 | return value !== null && value !== undefined 3 | } 4 | -------------------------------------------------------------------------------- /lib/text-splitter.ts: -------------------------------------------------------------------------------- 1 | // Taken from Langchain.js 2 | // License here: https://github.com/hwchase17/langchainjs/blob/main/LICENSE 3 | 4 | export interface DocumentInput< 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | Metadata extends Record = Record 7 | > { 8 | pageContent: string 9 | 10 | metadata?: Metadata 11 | } 12 | 13 | /** 14 | * Interface for interacting with a document. 15 | */ 16 | export class Document< 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | Metadata extends Record = Record 19 | > implements DocumentInput 20 | { 21 | pageContent: string 22 | 23 | metadata: Metadata 24 | 25 | constructor(fields: DocumentInput) { 26 | this.pageContent = fields.pageContent ? fields.pageContent.toString() : '' 27 | this.metadata = fields.metadata ?? ({} as Metadata) 28 | } 29 | } 30 | 31 | export interface TextSplitterParams { 32 | chunkSize: number 33 | 34 | chunkOverlap: number 35 | } 36 | 37 | export abstract class TextSplitter implements TextSplitterParams { 38 | chunkSize = 1000 39 | 40 | chunkOverlap = 200 41 | 42 | constructor(fields?: Partial) { 43 | this.chunkSize = fields?.chunkSize ?? this.chunkSize 44 | this.chunkOverlap = fields?.chunkOverlap ?? this.chunkOverlap 45 | if (this.chunkOverlap >= this.chunkSize) { 46 | throw new Error('Cannot have chunkOverlap >= chunkSize') 47 | } 48 | } 49 | 50 | abstract splitText(text: string): Promise 51 | 52 | async createDocuments( 53 | texts: string[], 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | metadatas: Record[] = [] 56 | ): Promise { 57 | const _metadatas = 58 | metadatas.length > 0 ? metadatas : new Array(texts.length).fill({}) 59 | const documents = new Array() 60 | for (let i = 0; i < texts.length; i += 1) { 61 | const text = texts[i] 62 | let lineCounterIndex = 1 63 | let prevChunk = null 64 | for (const chunk of await this.splitText(text)) { 65 | // we need to count the \n that are in the text before getting removed by the splitting 66 | let numberOfIntermediateNewLines = 0 67 | if (prevChunk) { 68 | const indexChunk = text.indexOf(chunk) 69 | const indexEndPrevChunk = text.indexOf(prevChunk) + prevChunk.length 70 | const removedNewlinesFromSplittingText = text.slice( 71 | indexEndPrevChunk, 72 | indexChunk 73 | ) 74 | numberOfIntermediateNewLines = ( 75 | removedNewlinesFromSplittingText.match(/\n/g) || [] 76 | ).length 77 | } 78 | lineCounterIndex += numberOfIntermediateNewLines 79 | const newLinesCount = (chunk.match(/\n/g) || []).length 80 | 81 | const loc = 82 | _metadatas[i].loc && typeof _metadatas[i].loc === 'object' 83 | ? { ..._metadatas[i].loc } 84 | : {} 85 | loc.lines = { 86 | from: lineCounterIndex, 87 | to: lineCounterIndex + newLinesCount, 88 | } 89 | const metadataWithLinesNumber = { 90 | ..._metadatas[i], 91 | loc, 92 | } 93 | documents.push( 94 | new Document({ 95 | pageContent: chunk, 96 | metadata: metadataWithLinesNumber, 97 | }) 98 | ) 99 | lineCounterIndex += newLinesCount 100 | prevChunk = chunk 101 | } 102 | } 103 | return documents 104 | } 105 | 106 | async splitDocuments(documents: Document[]): Promise { 107 | const selectedDocuments = documents.filter( 108 | (doc) => doc.pageContent !== undefined 109 | ) 110 | const texts = selectedDocuments.map((doc) => doc.pageContent) 111 | const metadatas = selectedDocuments.map((doc) => doc.metadata) 112 | return this.createDocuments(texts, metadatas) 113 | } 114 | 115 | private joinDocs(docs: string[], separator: string): string | null { 116 | const text = docs.join(separator).trim() 117 | return text === '' ? null : text 118 | } 119 | 120 | mergeSplits(splits: string[], separator: string): string[] { 121 | const docs: string[] = [] 122 | const currentDoc: string[] = [] 123 | let total = 0 124 | for (const d of splits) { 125 | const _len = d.length 126 | if (total + _len >= this.chunkSize) { 127 | if (total > this.chunkSize) { 128 | console.warn( 129 | `Created a chunk of size ${total}, + 130 | which is longer than the specified ${this.chunkSize}` 131 | ) 132 | } 133 | if (currentDoc.length > 0) { 134 | const doc = this.joinDocs(currentDoc, separator) 135 | if (doc !== null) { 136 | docs.push(doc) 137 | } 138 | // Keep on popping if: 139 | // - we have a larger chunk than in the chunk overlap 140 | // - or if we still have any chunks and the length is long 141 | while ( 142 | total > this.chunkOverlap || 143 | (total + _len > this.chunkSize && total > 0) 144 | ) { 145 | total -= currentDoc[0].length 146 | currentDoc.shift() 147 | } 148 | } 149 | } 150 | currentDoc.push(d) 151 | total += _len 152 | } 153 | const doc = this.joinDocs(currentDoc, separator) 154 | if (doc !== null) { 155 | docs.push(doc) 156 | } 157 | return docs 158 | } 159 | } 160 | 161 | export interface RecursiveCharacterTextSplitterParams 162 | extends TextSplitterParams { 163 | separators: string[] 164 | } 165 | 166 | export class RecursiveCharacterTextSplitter 167 | extends TextSplitter 168 | implements RecursiveCharacterTextSplitterParams 169 | { 170 | separators: string[] = ['\n\n', '\n', ' ', ''] 171 | 172 | constructor(fields?: Partial) { 173 | super(fields) 174 | this.separators = fields?.separators ?? this.separators 175 | } 176 | 177 | async splitText(text: string): Promise { 178 | const finalChunks: string[] = [] 179 | 180 | // Get appropriate separator to use 181 | let separator: string = this.separators[this.separators.length - 1] 182 | for (const s of this.separators) { 183 | if (s === '') { 184 | separator = s 185 | break 186 | } 187 | if (text.includes(s)) { 188 | separator = s 189 | break 190 | } 191 | } 192 | 193 | // Now that we have the separator, split the text 194 | let splits: string[] 195 | if (separator) { 196 | splits = text.split(separator) 197 | } else { 198 | splits = text.split('') 199 | } 200 | 201 | // Now go merging things, recursively splitting longer texts. 202 | let goodSplits: string[] = [] 203 | for (const s of splits) { 204 | if (s.length < this.chunkSize) { 205 | goodSplits.push(s) 206 | } else { 207 | if (goodSplits.length) { 208 | const mergedText = this.mergeSplits(goodSplits, separator) 209 | finalChunks.push(...mergedText) 210 | goodSplits = [] 211 | } 212 | const otherInfo = await this.splitText(s) 213 | finalChunks.push(...otherInfo) 214 | } 215 | } 216 | if (goodSplits.length) { 217 | const mergedText = this.mergeSplits(goodSplits, separator) 218 | finalChunks.push(...mergedText) 219 | } 220 | return finalChunks 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/tokenize.ts: -------------------------------------------------------------------------------- 1 | import { decode, encode } from 'gpt-tokenizer' 2 | 3 | export function limitedJoin(texts: string[], tokenLimit = 1500) { 4 | let tokenCount = 0 5 | let contextText = '' 6 | 7 | // Concat matched documents 8 | for (const text of texts) { 9 | const encoded = encode(text) 10 | tokenCount += encoded.length 11 | 12 | if (tokenCount > tokenLimit) { 13 | break 14 | } 15 | 16 | contextText += `${text.trim()}\n---\n` 17 | } 18 | 19 | return contextText 20 | } 21 | 22 | // Truncate text to a certain number of tokens so that it fits within context limits 23 | export function truncateText(text: string, tokenLimit = 1500) { 24 | const encoded = encode(text) 25 | 26 | if (encoded.length <= tokenLimit) { 27 | return text 28 | } 29 | 30 | // Decode only up to the token limit 31 | const truncated = encoded.slice(0, tokenLimit) 32 | return decode(truncated) 33 | } 34 | -------------------------------------------------------------------------------- /lib/truncate.ts: -------------------------------------------------------------------------------- 1 | // Add this new function alongside existing tokenize helpers 2 | export function truncateString( 3 | text: string, 4 | maxBytes = 9216, 5 | ellipsis = '...' 6 | ): string { 7 | // Early return for empty/small strings 8 | if (!text || text.length < 2000) { 9 | return text 10 | } 11 | 12 | const encoder = new TextEncoder() 13 | const bytes = encoder.encode(text) 14 | 15 | if (bytes.length <= maxBytes) { 16 | return text 17 | } 18 | 19 | // First do a rough cut based on character ratio 20 | // This reduces the number of binary search iterations needed 21 | const approxCharacters = Math.floor( 22 | maxBytes * (text.length / bytes.length) * 0.9 23 | ) 24 | let truncated = text.slice(0, approxCharacters) 25 | let currentBytes = encoder.encode(truncated) 26 | 27 | // Fine tune with binary search if needed 28 | if (currentBytes.length > maxBytes) { 29 | let start = 0 30 | let end = approxCharacters 31 | 32 | while (start < end - 1) { 33 | const mid = Math.floor((start + end) / 2) 34 | truncated = text.slice(0, mid) 35 | currentBytes = encoder.encode(truncated) 36 | 37 | if (currentBytes.length <= maxBytes) { 38 | start = mid 39 | } else { 40 | end = mid 41 | } 42 | } 43 | truncated = text.slice(0, start) 44 | } else { 45 | // Try to add more content if we have room 46 | let start = approxCharacters 47 | let end = text.length 48 | 49 | while (start < end - 1) { 50 | const mid = Math.floor((start + end) / 2) 51 | const attempt = text.slice(0, mid) 52 | currentBytes = encoder.encode(attempt) 53 | 54 | if (currentBytes.length <= maxBytes) { 55 | start = mid 56 | truncated = attempt 57 | } else { 58 | end = mid 59 | } 60 | } 61 | } 62 | 63 | // Try to break at a natural boundary 64 | const lastPeriod = truncated.lastIndexOf('.') 65 | const lastSpace = truncated.lastIndexOf(' ') 66 | const breakPoint = 67 | lastPeriod > truncated.length - 50 ? lastPeriod + 1 : lastSpace 68 | 69 | return breakPoint > truncated.length * 0.8 70 | ? truncated.slice(0, breakPoint) + ellipsis 71 | : truncated + ellipsis 72 | } 73 | -------------------------------------------------------------------------------- /lib/unique.ts: -------------------------------------------------------------------------------- 1 | export function unique(arr: T[]) { 2 | return [...new Set(arr)] 3 | } 4 | -------------------------------------------------------------------------------- /openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "LawGPT Search API", 5 | "description": "API for searching and retrieving laws and regulations", 6 | "version": "1.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "https://lawgpt-search.maccman.workers.dev", 11 | "description": "Production server" 12 | } 13 | ], 14 | "paths": { 15 | "/documents/search": { 16 | "get": { 17 | "summary": "Search documents", 18 | "description": "Search through documents using semantic search", 19 | "operationId": "searchDocuments", 20 | "parameters": [ 21 | { 22 | "name": "query", 23 | "in": "query", 24 | "required": true, 25 | "schema": { 26 | "type": "string", 27 | "description": "The search query" 28 | } 29 | }, 30 | { 31 | "name": "namespace", 32 | "in": "query", 33 | "required": true, 34 | "schema": { 35 | "type": "string", 36 | "description": "The namespace to search in", 37 | "enum": ["uk-legislation"] 38 | } 39 | } 40 | ], 41 | "responses": { 42 | "200": { 43 | "description": "Successful search results", 44 | "content": { 45 | "application/json": { 46 | "schema": { 47 | "type": "array", 48 | "items": { 49 | "type": "object", 50 | "properties": { 51 | "id": { 52 | "type": "number", 53 | "description": "Document ID" 54 | }, 55 | "url": { 56 | "type": "string", 57 | "description": "Document URL" 58 | }, 59 | "namespace": { 60 | "type": "string", 61 | "description": "Document namespace", 62 | "enum": ["uk-legislation"] 63 | }, 64 | "title": { 65 | "type": "string", 66 | "description": "Document title" 67 | }, 68 | "summary": { 69 | "type": "string", 70 | "description": "Document summary" 71 | }, 72 | "indexed_at": { 73 | "type": "number", 74 | "description": "Unix timestamp of when the document was indexed" 75 | } 76 | }, 77 | "required": [ 78 | "id", 79 | "url", 80 | "namespace", 81 | "title", 82 | "summary", 83 | "indexed_at" 84 | ] 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "/documents/{documentId}": { 94 | "get": { 95 | "summary": "Retrieve document", 96 | "description": "Get a specific document by ID", 97 | "operationId": "retrieveDocument", 98 | "parameters": [ 99 | { 100 | "name": "documentId", 101 | "in": "path", 102 | "required": true, 103 | "schema": { 104 | "type": "number", 105 | "description": "The ID of the document to retrieve" 106 | } 107 | }, 108 | { 109 | "name": "namespace", 110 | "in": "query", 111 | "required": true, 112 | "schema": { 113 | "type": "string", 114 | "description": "The namespace of the document", 115 | "enum": ["uk-legislation"] 116 | } 117 | } 118 | ], 119 | "responses": { 120 | "200": { 121 | "description": "Document found", 122 | "content": { 123 | "application/json": { 124 | "schema": { 125 | "type": "object", 126 | "properties": { 127 | "id": { 128 | "type": "number", 129 | "description": "Document ID" 130 | }, 131 | "url": { 132 | "type": "string", 133 | "description": "Document URL" 134 | }, 135 | "namespace": { 136 | "type": "string", 137 | "description": "Document namespace", 138 | "enum": ["uk-legislation"] 139 | }, 140 | "title": { 141 | "type": "string", 142 | "description": "Document title" 143 | }, 144 | "text": { 145 | "type": "string", 146 | "description": "Full document text" 147 | }, 148 | "summary": { 149 | "type": "string", 150 | "description": "Document summary" 151 | }, 152 | "indexed_at": { 153 | "type": "number", 154 | "description": "Unix timestamp of when the document was indexed" 155 | } 156 | }, 157 | "required": [ 158 | "id", 159 | "url", 160 | "namespace", 161 | "title", 162 | "text", 163 | "summary", 164 | "indexed_at" 165 | ] 166 | } 167 | } 168 | } 169 | }, 170 | "404": { 171 | "description": "Document not found", 172 | "content": { 173 | "application/json": { 174 | "schema": { 175 | "type": "object", 176 | "properties": { 177 | "error": { 178 | "type": "string", 179 | "example": "Document not found" 180 | } 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lawgpt-search", 3 | "private": true, 4 | "version": "0.0.0", 5 | "engines": { 6 | "node": "= 18" 7 | }, 8 | "scripts": { 9 | "dev": "wrangler dev --experimental-vectorize-bind-to-prod", 10 | "deploy": "wrangler deploy", 11 | "load-schema": "wrangler d1 execute lawgpt-d1 --file ./data/schema.sql", 12 | "setup-cloudflare-d1": "wrangler d1 create lawgpt-d1 && npm run load-schema", 13 | "setup-cloudflare-vectorize": "wrangler vectorize create lawgpt-vector2 --dimensions 1024 --metric=euclidean", 14 | "setup-cloudflare-vectorize-document-id-index": "wrangler vectorize create-metadata-index lawgpt-vector2 --property-name=document_id --type=number", 15 | "setup-cloudflare-vectorize-type-index": "wrangler vectorize create-metadata-index lawgpt-vector2 --property-name=type --type=string", 16 | "setup-cloudflare": "npm run setup-cloudflare-d1 && npm run setup-cloudflare-vectorize && npm run setup-cloudflare-vectorize-document-id-index", 17 | "drop-cloudflare": "npm run drop-cloudflare-d1 && npm run drop-cloudflare-vectorize", 18 | "drop-cloudflare-d1": "wrangler d1 delete lawgpt-d1", 19 | "drop-cloudflare-vectorize": "wrangler vectorize delete lawgpt-vector2" 20 | }, 21 | "devDependencies": { 22 | "@cloudflare/workers-types": "^4.20241112.0", 23 | "@types/common-tags": "^1.8.4", 24 | "env-cmd": "^10.1.0", 25 | "wrangler": "^3.90.0" 26 | }, 27 | "dependencies": { 28 | "@ai-sdk/google": "^1.0.5", 29 | "@ai-sdk/openai": "^1.0.4", 30 | "ai": "^4.0.4", 31 | "cloudflare-basics": "^0.0.9", 32 | "common-tags": "^1.8.2", 33 | "gpt-tokenizer": "^2.6.2", 34 | "zod": "^3.23.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@ai-sdk/google': 12 | specifier: ^1.0.5 13 | version: 1.0.5(zod@3.23.8) 14 | '@ai-sdk/openai': 15 | specifier: ^1.0.4 16 | version: 1.0.4(zod@3.23.8) 17 | ai: 18 | specifier: ^4.0.4 19 | version: 4.0.4(react@18.3.1)(zod@3.23.8) 20 | cloudflare-basics: 21 | specifier: ^0.0.9 22 | version: 0.0.9(zod@3.23.8) 23 | common-tags: 24 | specifier: ^1.8.2 25 | version: 1.8.2 26 | gpt-tokenizer: 27 | specifier: ^2.6.2 28 | version: 2.6.2 29 | zod: 30 | specifier: ^3.23.8 31 | version: 3.23.8 32 | devDependencies: 33 | '@cloudflare/workers-types': 34 | specifier: ^4.20241112.0 35 | version: 4.20241112.0 36 | '@types/common-tags': 37 | specifier: ^1.8.4 38 | version: 1.8.4 39 | env-cmd: 40 | specifier: ^10.1.0 41 | version: 10.1.0 42 | wrangler: 43 | specifier: ^3.90.0 44 | version: 3.90.0(@cloudflare/workers-types@4.20241112.0) 45 | 46 | packages: 47 | 48 | '@ai-sdk/google@1.0.5': 49 | resolution: {integrity: sha512-uq4wNVKIDNGXE18Wozp3grAOtAK2qHNy5wiBPLX34Zp4XHN11MmevrRj/R4+GXvnWy7czIVS3K04swXTB+m/4Q==} 50 | engines: {node: '>=18'} 51 | peerDependencies: 52 | zod: ^3.0.0 53 | 54 | '@ai-sdk/openai@1.0.4': 55 | resolution: {integrity: sha512-3QpgKmkCeJvUdeu3sVRL/ZKWzg8biO0tN2owQW/lFV95o8qskE3bN95R9H136Mmu0124/C28aY6ScxO93nUrtg==} 56 | engines: {node: '>=18'} 57 | peerDependencies: 58 | zod: ^3.0.0 59 | 60 | '@ai-sdk/provider-utils@2.0.2': 61 | resolution: {integrity: sha512-IAvhKhdlXqiSmvx/D4uNlFYCl8dWT+M9K+IuEcSgnE2Aj27GWu8sDIpAf4r4Voc+wOUkOECVKQhFo8g9pozdjA==} 62 | engines: {node: '>=18'} 63 | peerDependencies: 64 | zod: ^3.0.0 65 | peerDependenciesMeta: 66 | zod: 67 | optional: true 68 | 69 | '@ai-sdk/provider-utils@2.0.3': 70 | resolution: {integrity: sha512-Cyk7GlFEse2jQ4I3FWYuZ1Zhr5w1mD9SHMJTYm/in1rd7r89nmEoQiOy3h8YV2ZvTa2/6aR10xZ4M0k4B3BluA==} 71 | engines: {node: '>=18'} 72 | peerDependencies: 73 | zod: ^3.0.0 74 | peerDependenciesMeta: 75 | zod: 76 | optional: true 77 | 78 | '@ai-sdk/provider@1.0.1': 79 | resolution: {integrity: sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==} 80 | engines: {node: '>=18'} 81 | 82 | '@ai-sdk/react@1.0.2': 83 | resolution: {integrity: sha512-VQfQ6PMiUz4hDquAfjih0DIw4gsQvRFk91SFg2xWirDO4swMZByJzqGGcILPQKbww5ndCo48iZj9S1mLKZo5Dg==} 84 | engines: {node: '>=18'} 85 | peerDependencies: 86 | react: ^18 || ^19 || ^19.0.0-rc 87 | zod: ^3.0.0 88 | peerDependenciesMeta: 89 | react: 90 | optional: true 91 | zod: 92 | optional: true 93 | 94 | '@ai-sdk/ui-utils@1.0.2': 95 | resolution: {integrity: sha512-hHrUdeThGHu/rsGZBWQ9PjrAU9Htxgbo9MFyR5B/aWoNbBeXn1HLMY1+uMEnXL5pRPlmyVRjgIavWg7UgeNDOw==} 96 | engines: {node: '>=18'} 97 | peerDependencies: 98 | zod: ^3.0.0 99 | peerDependenciesMeta: 100 | zod: 101 | optional: true 102 | 103 | '@cloudflare/kv-asset-handler@0.3.4': 104 | resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} 105 | engines: {node: '>=16.13'} 106 | 107 | '@cloudflare/workerd-darwin-64@1.20241106.1': 108 | resolution: {integrity: sha512-zxvaToi1m0qzAScrxFt7UvFVqU8DxrCO2CinM1yQkv5no7pA1HolpIrwZ0xOhR3ny64Is2s/J6BrRjpO5dM9Zw==} 109 | engines: {node: '>=16'} 110 | cpu: [x64] 111 | os: [darwin] 112 | 113 | '@cloudflare/workerd-darwin-arm64@1.20241106.1': 114 | resolution: {integrity: sha512-j3dg/42D/bPgfNP3cRUBxF+4waCKO/5YKwXNj+lnVOwHxDu+ne5pFw9TIkKYcWTcwn0ZUkbNZNM5rhJqRn4xbg==} 115 | engines: {node: '>=16'} 116 | cpu: [arm64] 117 | os: [darwin] 118 | 119 | '@cloudflare/workerd-linux-64@1.20241106.1': 120 | resolution: {integrity: sha512-Ih+Ye8E1DMBXcKrJktGfGztFqHKaX1CeByqshmTbODnWKHt6O65ax3oTecUwyC0+abuyraOpAtdhHNpFMhUkmw==} 121 | engines: {node: '>=16'} 122 | cpu: [x64] 123 | os: [linux] 124 | 125 | '@cloudflare/workerd-linux-arm64@1.20241106.1': 126 | resolution: {integrity: sha512-mdQFPk4+14Yywn7n1xIzI+6olWM8Ybz10R7H3h+rk0XulMumCWUCy1CzIDauOx6GyIcSgKIibYMssVHZR30ObA==} 127 | engines: {node: '>=16'} 128 | cpu: [arm64] 129 | os: [linux] 130 | 131 | '@cloudflare/workerd-windows-64@1.20241106.1': 132 | resolution: {integrity: sha512-4rtcss31E/Rb/PeFocZfr+B9i1MdrkhsTBWizh8siNR4KMmkslU2xs2wPaH1z8+ErxkOsHrKRa5EPLh5rIiFeg==} 133 | engines: {node: '>=16'} 134 | cpu: [x64] 135 | os: [win32] 136 | 137 | '@cloudflare/workers-shared@0.8.0': 138 | resolution: {integrity: sha512-1OvFkNtslaMZAJsaocTmbACApgmWv55uLpNj50Pn2MGcxdAjpqykXJFQw5tKc+lGV9TDZh9oO3Rsk17IEQDzIg==} 139 | engines: {node: '>=16.7.0'} 140 | 141 | '@cloudflare/workers-types@4.20241112.0': 142 | resolution: {integrity: sha512-Q4p9bAWZrX14bSCKY9to19xl0KMU7nsO5sJ2cTVspHoypsjPUMeQCsjHjmsO2C4Myo8/LPeDvmqFmkyNAPPYZw==} 143 | 144 | '@cspotcode/source-map-support@0.8.1': 145 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 146 | engines: {node: '>=12'} 147 | 148 | '@esbuild-plugins/node-globals-polyfill@0.2.3': 149 | resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} 150 | peerDependencies: 151 | esbuild: '*' 152 | 153 | '@esbuild-plugins/node-modules-polyfill@0.2.2': 154 | resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} 155 | peerDependencies: 156 | esbuild: '*' 157 | 158 | '@esbuild/android-arm64@0.17.19': 159 | resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} 160 | engines: {node: '>=12'} 161 | cpu: [arm64] 162 | os: [android] 163 | 164 | '@esbuild/android-arm@0.17.19': 165 | resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} 166 | engines: {node: '>=12'} 167 | cpu: [arm] 168 | os: [android] 169 | 170 | '@esbuild/android-x64@0.17.19': 171 | resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} 172 | engines: {node: '>=12'} 173 | cpu: [x64] 174 | os: [android] 175 | 176 | '@esbuild/darwin-arm64@0.17.19': 177 | resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} 178 | engines: {node: '>=12'} 179 | cpu: [arm64] 180 | os: [darwin] 181 | 182 | '@esbuild/darwin-x64@0.17.19': 183 | resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} 184 | engines: {node: '>=12'} 185 | cpu: [x64] 186 | os: [darwin] 187 | 188 | '@esbuild/freebsd-arm64@0.17.19': 189 | resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} 190 | engines: {node: '>=12'} 191 | cpu: [arm64] 192 | os: [freebsd] 193 | 194 | '@esbuild/freebsd-x64@0.17.19': 195 | resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} 196 | engines: {node: '>=12'} 197 | cpu: [x64] 198 | os: [freebsd] 199 | 200 | '@esbuild/linux-arm64@0.17.19': 201 | resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} 202 | engines: {node: '>=12'} 203 | cpu: [arm64] 204 | os: [linux] 205 | 206 | '@esbuild/linux-arm@0.17.19': 207 | resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} 208 | engines: {node: '>=12'} 209 | cpu: [arm] 210 | os: [linux] 211 | 212 | '@esbuild/linux-ia32@0.17.19': 213 | resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} 214 | engines: {node: '>=12'} 215 | cpu: [ia32] 216 | os: [linux] 217 | 218 | '@esbuild/linux-loong64@0.17.19': 219 | resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} 220 | engines: {node: '>=12'} 221 | cpu: [loong64] 222 | os: [linux] 223 | 224 | '@esbuild/linux-mips64el@0.17.19': 225 | resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} 226 | engines: {node: '>=12'} 227 | cpu: [mips64el] 228 | os: [linux] 229 | 230 | '@esbuild/linux-ppc64@0.17.19': 231 | resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} 232 | engines: {node: '>=12'} 233 | cpu: [ppc64] 234 | os: [linux] 235 | 236 | '@esbuild/linux-riscv64@0.17.19': 237 | resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} 238 | engines: {node: '>=12'} 239 | cpu: [riscv64] 240 | os: [linux] 241 | 242 | '@esbuild/linux-s390x@0.17.19': 243 | resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} 244 | engines: {node: '>=12'} 245 | cpu: [s390x] 246 | os: [linux] 247 | 248 | '@esbuild/linux-x64@0.17.19': 249 | resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} 250 | engines: {node: '>=12'} 251 | cpu: [x64] 252 | os: [linux] 253 | 254 | '@esbuild/netbsd-x64@0.17.19': 255 | resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} 256 | engines: {node: '>=12'} 257 | cpu: [x64] 258 | os: [netbsd] 259 | 260 | '@esbuild/openbsd-x64@0.17.19': 261 | resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} 262 | engines: {node: '>=12'} 263 | cpu: [x64] 264 | os: [openbsd] 265 | 266 | '@esbuild/sunos-x64@0.17.19': 267 | resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} 268 | engines: {node: '>=12'} 269 | cpu: [x64] 270 | os: [sunos] 271 | 272 | '@esbuild/win32-arm64@0.17.19': 273 | resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} 274 | engines: {node: '>=12'} 275 | cpu: [arm64] 276 | os: [win32] 277 | 278 | '@esbuild/win32-ia32@0.17.19': 279 | resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} 280 | engines: {node: '>=12'} 281 | cpu: [ia32] 282 | os: [win32] 283 | 284 | '@esbuild/win32-x64@0.17.19': 285 | resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} 286 | engines: {node: '>=12'} 287 | cpu: [x64] 288 | os: [win32] 289 | 290 | '@fastify/busboy@2.1.1': 291 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 292 | engines: {node: '>=14'} 293 | 294 | '@jridgewell/resolve-uri@3.1.2': 295 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 296 | engines: {node: '>=6.0.0'} 297 | 298 | '@jridgewell/sourcemap-codec@1.5.0': 299 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 300 | 301 | '@jridgewell/trace-mapping@0.3.9': 302 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 303 | 304 | '@opentelemetry/api@1.9.0': 305 | resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} 306 | engines: {node: '>=8.0.0'} 307 | 308 | '@types/common-tags@1.8.4': 309 | resolution: {integrity: sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==} 310 | 311 | '@types/diff-match-patch@1.0.36': 312 | resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} 313 | 314 | '@types/node-forge@1.3.11': 315 | resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} 316 | 317 | '@types/node@22.9.3': 318 | resolution: {integrity: sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==} 319 | 320 | acorn-walk@8.3.4: 321 | resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} 322 | engines: {node: '>=0.4.0'} 323 | 324 | acorn@8.14.0: 325 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 326 | engines: {node: '>=0.4.0'} 327 | hasBin: true 328 | 329 | ai@4.0.4: 330 | resolution: {integrity: sha512-G3LJu2yIgvwNRXSgMmeWQmc5wK00nQNXqnfqn+PyUymGjNbdWP7xt+/VCOnvCpXNOonZ9z4czjdRqVaDhoFAoA==} 331 | engines: {node: '>=18'} 332 | peerDependencies: 333 | react: ^18 || ^19 || ^19.0.0-rc 334 | zod: ^3.0.0 335 | peerDependenciesMeta: 336 | react: 337 | optional: true 338 | zod: 339 | optional: true 340 | 341 | as-table@1.0.55: 342 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 343 | 344 | blake3-wasm@2.1.5: 345 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 346 | 347 | capnp-ts@0.7.0: 348 | resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} 349 | 350 | chalk@5.3.0: 351 | resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} 352 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 353 | 354 | chokidar@4.0.1: 355 | resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} 356 | engines: {node: '>= 14.16.0'} 357 | 358 | client-only@0.0.1: 359 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 360 | 361 | cloudflare-basics@0.0.9: 362 | resolution: {integrity: sha512-Bfzxb0HAySEEoIYM0oTloZlREgVjJpHTkTRI1gPsCaof5miXcV1r1+EGQwsA2AYXTm//ehFZMnJh2/cSD2BtUw==} 363 | peerDependencies: 364 | zod: ^3.22.1 365 | 366 | commander@4.1.1: 367 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 368 | engines: {node: '>= 6'} 369 | 370 | common-tags@1.8.2: 371 | resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} 372 | engines: {node: '>=4.0.0'} 373 | 374 | cookie@0.7.2: 375 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 376 | engines: {node: '>= 0.6'} 377 | 378 | cross-spawn@7.0.6: 379 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 380 | engines: {node: '>= 8'} 381 | 382 | data-uri-to-buffer@2.0.2: 383 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 384 | 385 | date-fns@4.1.0: 386 | resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} 387 | 388 | debug@4.3.7: 389 | resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} 390 | engines: {node: '>=6.0'} 391 | peerDependencies: 392 | supports-color: '*' 393 | peerDependenciesMeta: 394 | supports-color: 395 | optional: true 396 | 397 | defu@6.1.4: 398 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 399 | 400 | diff-match-patch@1.0.5: 401 | resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} 402 | 403 | env-cmd@10.1.0: 404 | resolution: {integrity: sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==} 405 | engines: {node: '>=8.0.0'} 406 | hasBin: true 407 | 408 | esbuild@0.17.19: 409 | resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} 410 | engines: {node: '>=12'} 411 | hasBin: true 412 | 413 | escape-string-regexp@4.0.0: 414 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 415 | engines: {node: '>=10'} 416 | 417 | estree-walker@0.6.1: 418 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 419 | 420 | eventsource-parser@3.0.0: 421 | resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} 422 | engines: {node: '>=18.0.0'} 423 | 424 | exit-hook@2.2.1: 425 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 426 | engines: {node: '>=6'} 427 | 428 | fsevents@2.3.3: 429 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 430 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 431 | os: [darwin] 432 | 433 | function-bind@1.1.2: 434 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 435 | 436 | get-source@2.0.12: 437 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 438 | 439 | glob-to-regexp@0.4.1: 440 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 441 | 442 | gpt-tokenizer@2.6.2: 443 | resolution: {integrity: sha512-OznIET3z069FiwbLtLFXJ9pVESYAa8EnX0BMogs6YJ4Fn2FIcyeZYEbxsp2grPiK0DVaqP1f+0JR/8t9R7/jlg==} 444 | 445 | hasown@2.0.2: 446 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 447 | engines: {node: '>= 0.4'} 448 | 449 | is-core-module@2.15.1: 450 | resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} 451 | engines: {node: '>= 0.4'} 452 | 453 | isexe@2.0.0: 454 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 455 | 456 | itty-time@1.0.6: 457 | resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} 458 | 459 | js-tokens@4.0.0: 460 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 461 | 462 | json-schema@0.4.0: 463 | resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} 464 | 465 | jsondiffpatch@0.6.0: 466 | resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} 467 | engines: {node: ^18.0.0 || >=20.0.0} 468 | hasBin: true 469 | 470 | loose-envify@1.4.0: 471 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 472 | hasBin: true 473 | 474 | magic-string@0.25.9: 475 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 476 | 477 | mime@3.0.0: 478 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 479 | engines: {node: '>=10.0.0'} 480 | hasBin: true 481 | 482 | miniflare@3.20241106.1: 483 | resolution: {integrity: sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==} 484 | engines: {node: '>=16.13'} 485 | hasBin: true 486 | 487 | ms@2.1.3: 488 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 489 | 490 | mustache@4.2.0: 491 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 492 | hasBin: true 493 | 494 | nanoid@3.3.7: 495 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 496 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 497 | hasBin: true 498 | 499 | node-forge@1.3.1: 500 | resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} 501 | engines: {node: '>= 6.13.0'} 502 | 503 | ohash@1.1.4: 504 | resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} 505 | 506 | path-key@3.1.1: 507 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 508 | engines: {node: '>=8'} 509 | 510 | path-parse@1.0.7: 511 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 512 | 513 | path-to-regexp@6.3.0: 514 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 515 | 516 | pathe@1.1.2: 517 | resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 518 | 519 | printable-characters@1.0.42: 520 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 521 | 522 | react@18.3.1: 523 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 524 | engines: {node: '>=0.10.0'} 525 | 526 | readdirp@4.0.2: 527 | resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} 528 | engines: {node: '>= 14.16.0'} 529 | 530 | resolve.exports@2.0.2: 531 | resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} 532 | engines: {node: '>=10'} 533 | 534 | resolve@1.22.8: 535 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 536 | hasBin: true 537 | 538 | rollup-plugin-inject@3.0.2: 539 | resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} 540 | deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. 541 | 542 | rollup-plugin-node-polyfills@0.2.1: 543 | resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} 544 | 545 | rollup-pluginutils@2.8.2: 546 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 547 | 548 | secure-json-parse@2.7.0: 549 | resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} 550 | 551 | selfsigned@2.4.1: 552 | resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} 553 | engines: {node: '>=10'} 554 | 555 | shebang-command@2.0.0: 556 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 557 | engines: {node: '>=8'} 558 | 559 | shebang-regex@3.0.0: 560 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 561 | engines: {node: '>=8'} 562 | 563 | source-map@0.6.1: 564 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 565 | engines: {node: '>=0.10.0'} 566 | 567 | sourcemap-codec@1.4.8: 568 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 569 | deprecated: Please use @jridgewell/sourcemap-codec instead 570 | 571 | stacktracey@2.1.8: 572 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 573 | 574 | stoppable@1.1.0: 575 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 576 | engines: {node: '>=4', npm: '>=6'} 577 | 578 | supports-preserve-symlinks-flag@1.0.0: 579 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 580 | engines: {node: '>= 0.4'} 581 | 582 | swr@2.2.5: 583 | resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} 584 | peerDependencies: 585 | react: ^16.11.0 || ^17.0.0 || ^18.0.0 586 | 587 | throttleit@2.1.0: 588 | resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} 589 | engines: {node: '>=18'} 590 | 591 | tslib@2.8.1: 592 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 593 | 594 | ufo@1.5.4: 595 | resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} 596 | 597 | undici-types@6.19.8: 598 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 599 | 600 | undici@5.28.4: 601 | resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} 602 | engines: {node: '>=14.0'} 603 | 604 | unenv-nightly@2.0.0-20241111-080453-894aa31: 605 | resolution: {integrity: sha512-0W39QQOQ9VE8kVVUpGwEG+pZcsCXk5wqNG6rDPE6Gr+fiA69LR0qERM61hW5KCOkC1/ArCFrfCGjwHyyv/bI0Q==} 606 | 607 | use-sync-external-store@1.2.2: 608 | resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} 609 | peerDependencies: 610 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 611 | 612 | which@2.0.2: 613 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 614 | engines: {node: '>= 8'} 615 | hasBin: true 616 | 617 | workerd@1.20241106.1: 618 | resolution: {integrity: sha512-1GdKl0kDw8rrirr/ThcK66Kbl4/jd4h8uHx5g7YHBrnenY5SX1UPuop2cnCzYUxlg55kPjzIqqYslz1muRFgFw==} 619 | engines: {node: '>=16'} 620 | hasBin: true 621 | 622 | wrangler@3.90.0: 623 | resolution: {integrity: sha512-E/6E9ORAl987+3kP8wDiE3L1lj9r4vQ32/dl5toIxIkSMssmPRQVdxqwgMxbxJrytbFNo8Eo6swgjd4y4nUaLg==} 624 | engines: {node: '>=16.17.0'} 625 | hasBin: true 626 | peerDependencies: 627 | '@cloudflare/workers-types': ^4.20241106.0 628 | peerDependenciesMeta: 629 | '@cloudflare/workers-types': 630 | optional: true 631 | 632 | ws@8.18.0: 633 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 634 | engines: {node: '>=10.0.0'} 635 | peerDependencies: 636 | bufferutil: ^4.0.1 637 | utf-8-validate: '>=5.0.2' 638 | peerDependenciesMeta: 639 | bufferutil: 640 | optional: true 641 | utf-8-validate: 642 | optional: true 643 | 644 | xxhash-wasm@1.1.0: 645 | resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} 646 | 647 | youch@3.3.4: 648 | resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 649 | 650 | zod-to-json-schema@3.23.5: 651 | resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} 652 | peerDependencies: 653 | zod: ^3.23.3 654 | 655 | zod@3.23.8: 656 | resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} 657 | 658 | snapshots: 659 | 660 | '@ai-sdk/google@1.0.5(zod@3.23.8)': 661 | dependencies: 662 | '@ai-sdk/provider': 1.0.1 663 | '@ai-sdk/provider-utils': 2.0.3(zod@3.23.8) 664 | zod: 3.23.8 665 | 666 | '@ai-sdk/openai@1.0.4(zod@3.23.8)': 667 | dependencies: 668 | '@ai-sdk/provider': 1.0.1 669 | '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) 670 | zod: 3.23.8 671 | 672 | '@ai-sdk/provider-utils@2.0.2(zod@3.23.8)': 673 | dependencies: 674 | '@ai-sdk/provider': 1.0.1 675 | eventsource-parser: 3.0.0 676 | nanoid: 3.3.7 677 | secure-json-parse: 2.7.0 678 | optionalDependencies: 679 | zod: 3.23.8 680 | 681 | '@ai-sdk/provider-utils@2.0.3(zod@3.23.8)': 682 | dependencies: 683 | '@ai-sdk/provider': 1.0.1 684 | eventsource-parser: 3.0.0 685 | nanoid: 3.3.7 686 | secure-json-parse: 2.7.0 687 | optionalDependencies: 688 | zod: 3.23.8 689 | 690 | '@ai-sdk/provider@1.0.1': 691 | dependencies: 692 | json-schema: 0.4.0 693 | 694 | '@ai-sdk/react@1.0.2(react@18.3.1)(zod@3.23.8)': 695 | dependencies: 696 | '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) 697 | '@ai-sdk/ui-utils': 1.0.2(zod@3.23.8) 698 | swr: 2.2.5(react@18.3.1) 699 | throttleit: 2.1.0 700 | optionalDependencies: 701 | react: 18.3.1 702 | zod: 3.23.8 703 | 704 | '@ai-sdk/ui-utils@1.0.2(zod@3.23.8)': 705 | dependencies: 706 | '@ai-sdk/provider': 1.0.1 707 | '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) 708 | zod-to-json-schema: 3.23.5(zod@3.23.8) 709 | optionalDependencies: 710 | zod: 3.23.8 711 | 712 | '@cloudflare/kv-asset-handler@0.3.4': 713 | dependencies: 714 | mime: 3.0.0 715 | 716 | '@cloudflare/workerd-darwin-64@1.20241106.1': 717 | optional: true 718 | 719 | '@cloudflare/workerd-darwin-arm64@1.20241106.1': 720 | optional: true 721 | 722 | '@cloudflare/workerd-linux-64@1.20241106.1': 723 | optional: true 724 | 725 | '@cloudflare/workerd-linux-arm64@1.20241106.1': 726 | optional: true 727 | 728 | '@cloudflare/workerd-windows-64@1.20241106.1': 729 | optional: true 730 | 731 | '@cloudflare/workers-shared@0.8.0': 732 | dependencies: 733 | mime: 3.0.0 734 | zod: 3.23.8 735 | 736 | '@cloudflare/workers-types@4.20241112.0': {} 737 | 738 | '@cspotcode/source-map-support@0.8.1': 739 | dependencies: 740 | '@jridgewell/trace-mapping': 0.3.9 741 | 742 | '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': 743 | dependencies: 744 | esbuild: 0.17.19 745 | 746 | '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': 747 | dependencies: 748 | esbuild: 0.17.19 749 | escape-string-regexp: 4.0.0 750 | rollup-plugin-node-polyfills: 0.2.1 751 | 752 | '@esbuild/android-arm64@0.17.19': 753 | optional: true 754 | 755 | '@esbuild/android-arm@0.17.19': 756 | optional: true 757 | 758 | '@esbuild/android-x64@0.17.19': 759 | optional: true 760 | 761 | '@esbuild/darwin-arm64@0.17.19': 762 | optional: true 763 | 764 | '@esbuild/darwin-x64@0.17.19': 765 | optional: true 766 | 767 | '@esbuild/freebsd-arm64@0.17.19': 768 | optional: true 769 | 770 | '@esbuild/freebsd-x64@0.17.19': 771 | optional: true 772 | 773 | '@esbuild/linux-arm64@0.17.19': 774 | optional: true 775 | 776 | '@esbuild/linux-arm@0.17.19': 777 | optional: true 778 | 779 | '@esbuild/linux-ia32@0.17.19': 780 | optional: true 781 | 782 | '@esbuild/linux-loong64@0.17.19': 783 | optional: true 784 | 785 | '@esbuild/linux-mips64el@0.17.19': 786 | optional: true 787 | 788 | '@esbuild/linux-ppc64@0.17.19': 789 | optional: true 790 | 791 | '@esbuild/linux-riscv64@0.17.19': 792 | optional: true 793 | 794 | '@esbuild/linux-s390x@0.17.19': 795 | optional: true 796 | 797 | '@esbuild/linux-x64@0.17.19': 798 | optional: true 799 | 800 | '@esbuild/netbsd-x64@0.17.19': 801 | optional: true 802 | 803 | '@esbuild/openbsd-x64@0.17.19': 804 | optional: true 805 | 806 | '@esbuild/sunos-x64@0.17.19': 807 | optional: true 808 | 809 | '@esbuild/win32-arm64@0.17.19': 810 | optional: true 811 | 812 | '@esbuild/win32-ia32@0.17.19': 813 | optional: true 814 | 815 | '@esbuild/win32-x64@0.17.19': 816 | optional: true 817 | 818 | '@fastify/busboy@2.1.1': {} 819 | 820 | '@jridgewell/resolve-uri@3.1.2': {} 821 | 822 | '@jridgewell/sourcemap-codec@1.5.0': {} 823 | 824 | '@jridgewell/trace-mapping@0.3.9': 825 | dependencies: 826 | '@jridgewell/resolve-uri': 3.1.2 827 | '@jridgewell/sourcemap-codec': 1.5.0 828 | 829 | '@opentelemetry/api@1.9.0': {} 830 | 831 | '@types/common-tags@1.8.4': {} 832 | 833 | '@types/diff-match-patch@1.0.36': {} 834 | 835 | '@types/node-forge@1.3.11': 836 | dependencies: 837 | '@types/node': 22.9.3 838 | 839 | '@types/node@22.9.3': 840 | dependencies: 841 | undici-types: 6.19.8 842 | 843 | acorn-walk@8.3.4: 844 | dependencies: 845 | acorn: 8.14.0 846 | 847 | acorn@8.14.0: {} 848 | 849 | ai@4.0.4(react@18.3.1)(zod@3.23.8): 850 | dependencies: 851 | '@ai-sdk/provider': 1.0.1 852 | '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) 853 | '@ai-sdk/react': 1.0.2(react@18.3.1)(zod@3.23.8) 854 | '@ai-sdk/ui-utils': 1.0.2(zod@3.23.8) 855 | '@opentelemetry/api': 1.9.0 856 | jsondiffpatch: 0.6.0 857 | zod-to-json-schema: 3.23.5(zod@3.23.8) 858 | optionalDependencies: 859 | react: 18.3.1 860 | zod: 3.23.8 861 | 862 | as-table@1.0.55: 863 | dependencies: 864 | printable-characters: 1.0.42 865 | 866 | blake3-wasm@2.1.5: {} 867 | 868 | capnp-ts@0.7.0: 869 | dependencies: 870 | debug: 4.3.7 871 | tslib: 2.8.1 872 | transitivePeerDependencies: 873 | - supports-color 874 | 875 | chalk@5.3.0: {} 876 | 877 | chokidar@4.0.1: 878 | dependencies: 879 | readdirp: 4.0.2 880 | 881 | client-only@0.0.1: {} 882 | 883 | cloudflare-basics@0.0.9(zod@3.23.8): 884 | dependencies: 885 | zod: 3.23.8 886 | 887 | commander@4.1.1: {} 888 | 889 | common-tags@1.8.2: {} 890 | 891 | cookie@0.7.2: {} 892 | 893 | cross-spawn@7.0.6: 894 | dependencies: 895 | path-key: 3.1.1 896 | shebang-command: 2.0.0 897 | which: 2.0.2 898 | 899 | data-uri-to-buffer@2.0.2: {} 900 | 901 | date-fns@4.1.0: {} 902 | 903 | debug@4.3.7: 904 | dependencies: 905 | ms: 2.1.3 906 | 907 | defu@6.1.4: {} 908 | 909 | diff-match-patch@1.0.5: {} 910 | 911 | env-cmd@10.1.0: 912 | dependencies: 913 | commander: 4.1.1 914 | cross-spawn: 7.0.6 915 | 916 | esbuild@0.17.19: 917 | optionalDependencies: 918 | '@esbuild/android-arm': 0.17.19 919 | '@esbuild/android-arm64': 0.17.19 920 | '@esbuild/android-x64': 0.17.19 921 | '@esbuild/darwin-arm64': 0.17.19 922 | '@esbuild/darwin-x64': 0.17.19 923 | '@esbuild/freebsd-arm64': 0.17.19 924 | '@esbuild/freebsd-x64': 0.17.19 925 | '@esbuild/linux-arm': 0.17.19 926 | '@esbuild/linux-arm64': 0.17.19 927 | '@esbuild/linux-ia32': 0.17.19 928 | '@esbuild/linux-loong64': 0.17.19 929 | '@esbuild/linux-mips64el': 0.17.19 930 | '@esbuild/linux-ppc64': 0.17.19 931 | '@esbuild/linux-riscv64': 0.17.19 932 | '@esbuild/linux-s390x': 0.17.19 933 | '@esbuild/linux-x64': 0.17.19 934 | '@esbuild/netbsd-x64': 0.17.19 935 | '@esbuild/openbsd-x64': 0.17.19 936 | '@esbuild/sunos-x64': 0.17.19 937 | '@esbuild/win32-arm64': 0.17.19 938 | '@esbuild/win32-ia32': 0.17.19 939 | '@esbuild/win32-x64': 0.17.19 940 | 941 | escape-string-regexp@4.0.0: {} 942 | 943 | estree-walker@0.6.1: {} 944 | 945 | eventsource-parser@3.0.0: {} 946 | 947 | exit-hook@2.2.1: {} 948 | 949 | fsevents@2.3.3: 950 | optional: true 951 | 952 | function-bind@1.1.2: {} 953 | 954 | get-source@2.0.12: 955 | dependencies: 956 | data-uri-to-buffer: 2.0.2 957 | source-map: 0.6.1 958 | 959 | glob-to-regexp@0.4.1: {} 960 | 961 | gpt-tokenizer@2.6.2: {} 962 | 963 | hasown@2.0.2: 964 | dependencies: 965 | function-bind: 1.1.2 966 | 967 | is-core-module@2.15.1: 968 | dependencies: 969 | hasown: 2.0.2 970 | 971 | isexe@2.0.0: {} 972 | 973 | itty-time@1.0.6: {} 974 | 975 | js-tokens@4.0.0: {} 976 | 977 | json-schema@0.4.0: {} 978 | 979 | jsondiffpatch@0.6.0: 980 | dependencies: 981 | '@types/diff-match-patch': 1.0.36 982 | chalk: 5.3.0 983 | diff-match-patch: 1.0.5 984 | 985 | loose-envify@1.4.0: 986 | dependencies: 987 | js-tokens: 4.0.0 988 | 989 | magic-string@0.25.9: 990 | dependencies: 991 | sourcemap-codec: 1.4.8 992 | 993 | mime@3.0.0: {} 994 | 995 | miniflare@3.20241106.1: 996 | dependencies: 997 | '@cspotcode/source-map-support': 0.8.1 998 | acorn: 8.14.0 999 | acorn-walk: 8.3.4 1000 | capnp-ts: 0.7.0 1001 | exit-hook: 2.2.1 1002 | glob-to-regexp: 0.4.1 1003 | stoppable: 1.1.0 1004 | undici: 5.28.4 1005 | workerd: 1.20241106.1 1006 | ws: 8.18.0 1007 | youch: 3.3.4 1008 | zod: 3.23.8 1009 | transitivePeerDependencies: 1010 | - bufferutil 1011 | - supports-color 1012 | - utf-8-validate 1013 | 1014 | ms@2.1.3: {} 1015 | 1016 | mustache@4.2.0: {} 1017 | 1018 | nanoid@3.3.7: {} 1019 | 1020 | node-forge@1.3.1: {} 1021 | 1022 | ohash@1.1.4: {} 1023 | 1024 | path-key@3.1.1: {} 1025 | 1026 | path-parse@1.0.7: {} 1027 | 1028 | path-to-regexp@6.3.0: {} 1029 | 1030 | pathe@1.1.2: {} 1031 | 1032 | printable-characters@1.0.42: {} 1033 | 1034 | react@18.3.1: 1035 | dependencies: 1036 | loose-envify: 1.4.0 1037 | 1038 | readdirp@4.0.2: {} 1039 | 1040 | resolve.exports@2.0.2: {} 1041 | 1042 | resolve@1.22.8: 1043 | dependencies: 1044 | is-core-module: 2.15.1 1045 | path-parse: 1.0.7 1046 | supports-preserve-symlinks-flag: 1.0.0 1047 | 1048 | rollup-plugin-inject@3.0.2: 1049 | dependencies: 1050 | estree-walker: 0.6.1 1051 | magic-string: 0.25.9 1052 | rollup-pluginutils: 2.8.2 1053 | 1054 | rollup-plugin-node-polyfills@0.2.1: 1055 | dependencies: 1056 | rollup-plugin-inject: 3.0.2 1057 | 1058 | rollup-pluginutils@2.8.2: 1059 | dependencies: 1060 | estree-walker: 0.6.1 1061 | 1062 | secure-json-parse@2.7.0: {} 1063 | 1064 | selfsigned@2.4.1: 1065 | dependencies: 1066 | '@types/node-forge': 1.3.11 1067 | node-forge: 1.3.1 1068 | 1069 | shebang-command@2.0.0: 1070 | dependencies: 1071 | shebang-regex: 3.0.0 1072 | 1073 | shebang-regex@3.0.0: {} 1074 | 1075 | source-map@0.6.1: {} 1076 | 1077 | sourcemap-codec@1.4.8: {} 1078 | 1079 | stacktracey@2.1.8: 1080 | dependencies: 1081 | as-table: 1.0.55 1082 | get-source: 2.0.12 1083 | 1084 | stoppable@1.1.0: {} 1085 | 1086 | supports-preserve-symlinks-flag@1.0.0: {} 1087 | 1088 | swr@2.2.5(react@18.3.1): 1089 | dependencies: 1090 | client-only: 0.0.1 1091 | react: 18.3.1 1092 | use-sync-external-store: 1.2.2(react@18.3.1) 1093 | 1094 | throttleit@2.1.0: {} 1095 | 1096 | tslib@2.8.1: {} 1097 | 1098 | ufo@1.5.4: {} 1099 | 1100 | undici-types@6.19.8: {} 1101 | 1102 | undici@5.28.4: 1103 | dependencies: 1104 | '@fastify/busboy': 2.1.1 1105 | 1106 | unenv-nightly@2.0.0-20241111-080453-894aa31: 1107 | dependencies: 1108 | defu: 6.1.4 1109 | ohash: 1.1.4 1110 | pathe: 1.1.2 1111 | ufo: 1.5.4 1112 | 1113 | use-sync-external-store@1.2.2(react@18.3.1): 1114 | dependencies: 1115 | react: 18.3.1 1116 | 1117 | which@2.0.2: 1118 | dependencies: 1119 | isexe: 2.0.0 1120 | 1121 | workerd@1.20241106.1: 1122 | optionalDependencies: 1123 | '@cloudflare/workerd-darwin-64': 1.20241106.1 1124 | '@cloudflare/workerd-darwin-arm64': 1.20241106.1 1125 | '@cloudflare/workerd-linux-64': 1.20241106.1 1126 | '@cloudflare/workerd-linux-arm64': 1.20241106.1 1127 | '@cloudflare/workerd-windows-64': 1.20241106.1 1128 | 1129 | wrangler@3.90.0(@cloudflare/workers-types@4.20241112.0): 1130 | dependencies: 1131 | '@cloudflare/kv-asset-handler': 0.3.4 1132 | '@cloudflare/workers-shared': 0.8.0 1133 | '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) 1134 | '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) 1135 | blake3-wasm: 2.1.5 1136 | chokidar: 4.0.1 1137 | date-fns: 4.1.0 1138 | esbuild: 0.17.19 1139 | itty-time: 1.0.6 1140 | miniflare: 3.20241106.1 1141 | nanoid: 3.3.7 1142 | path-to-regexp: 6.3.0 1143 | resolve: 1.22.8 1144 | resolve.exports: 2.0.2 1145 | selfsigned: 2.4.1 1146 | source-map: 0.6.1 1147 | unenv: unenv-nightly@2.0.0-20241111-080453-894aa31 1148 | workerd: 1.20241106.1 1149 | xxhash-wasm: 1.1.0 1150 | optionalDependencies: 1151 | '@cloudflare/workers-types': 4.20241112.0 1152 | fsevents: 2.3.3 1153 | transitivePeerDependencies: 1154 | - bufferutil 1155 | - supports-color 1156 | - utf-8-validate 1157 | 1158 | ws@8.18.0: {} 1159 | 1160 | xxhash-wasm@1.1.0: {} 1161 | 1162 | youch@3.3.4: 1163 | dependencies: 1164 | cookie: 0.7.2 1165 | mustache: 4.2.0 1166 | stacktracey: 2.1.8 1167 | 1168 | zod-to-json-schema@3.23.5(zod@3.23.8): 1169 | dependencies: 1170 | zod: 3.23.8 1171 | 1172 | zod@3.23.8: {} 1173 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

lawgpt-search

4 | 5 |

API endpoints

6 | openapi.json 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Cloudflare Vector Search API", 5 | "version": "1.0.0" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "https://example.com/api" 10 | } 11 | ], 12 | "paths": { 13 | "/submit": { 14 | "post": { 15 | "summary": "Submit a document", 16 | "operationId": "submitDocument", 17 | "security": [ 18 | { 19 | "bearerAuth": [] 20 | } 21 | ], 22 | "requestBody": { 23 | "content": { 24 | "application/json": { 25 | "schema": { 26 | "$ref": "#/components/schemas/SubmitDocument" 27 | } 28 | } 29 | } 30 | }, 31 | "responses": { 32 | "200": { 33 | "description": "Document submitted successfully", 34 | "content": { 35 | "application/json": { 36 | "schema": { 37 | "$ref": "#/components/schemas/DocumentId" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | }, 45 | "/bulk-submit": { 46 | "post": { 47 | "summary": "Submit a large document", 48 | "operationId": "submitLargeDocument", 49 | "security": [ 50 | { 51 | "bearerAuth": [] 52 | } 53 | ], 54 | "requestBody": { 55 | "content": { 56 | "application/json": { 57 | "schema": { 58 | "$ref": "#/components/schemas/SubmitDocument" 59 | } 60 | } 61 | } 62 | }, 63 | "responses": { 64 | "200": { 65 | "description": "Large document submitted successfully", 66 | "content": { 67 | "application/json": { 68 | "schema": { 69 | "$ref": "#/components/schemas/DocumentId" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "/search": { 78 | "get": { 79 | "summary": "Search documents", 80 | "operationId": "searchDocuments", 81 | "security": [ 82 | { 83 | "bearerAuth": [] 84 | } 85 | ], 86 | "parameters": [ 87 | { 88 | "in": "query", 89 | "name": "query", 90 | "schema": { 91 | "type": "string" 92 | }, 93 | "required": true 94 | }, 95 | { 96 | "in": "query", 97 | "name": "namespace", 98 | "schema": { 99 | "type": "string" 100 | }, 101 | "required": false 102 | } 103 | ], 104 | "responses": { 105 | "200": { 106 | "description": "Search results", 107 | "content": { 108 | "application/json": { 109 | "schema": { 110 | "type": "array", 111 | "items": { 112 | "$ref": "#/components/schemas/Document" 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "components": { 123 | "schemas": { 124 | "SubmitDocument": { 125 | "type": "object", 126 | "properties": { 127 | "text": { 128 | "type": "string", 129 | "description": "The text of the document" 130 | }, 131 | "namespace": { 132 | "type": "string", 133 | "description": "An optional namespace for the document" 134 | }, 135 | "metadata": { 136 | "type": "object", 137 | "description": "An optional key/value metadata object for the document" 138 | } 139 | }, 140 | "required": ["text"] 141 | }, 142 | "Document": { 143 | "type": "object", 144 | "properties": { 145 | "id": { 146 | "type": "string", 147 | "description": "The unique identifier of the document" 148 | }, 149 | "namespace": { 150 | "type": "string", 151 | "description": "The namespace of the document" 152 | }, 153 | "text": { 154 | "type": "string", 155 | "description": "The text of the document" 156 | }, 157 | "metadata": { 158 | "type": "object", 159 | "description": "The key/value metadata object for the document" 160 | }, 161 | "indexed_at": { 162 | "type": "string", 163 | "format": "date-time", 164 | "description": "The timestamp when the document was indexed" 165 | }, 166 | "similarity": { 167 | "type": "number", 168 | "format": "double", 169 | "description": "The similarity score between the document and the search query" 170 | } 171 | }, 172 | "required": [ 173 | "id", 174 | "namespace", 175 | "text", 176 | "metadata", 177 | "indexed_at", 178 | "similarity" 179 | ] 180 | }, 181 | "DocumentId": { 182 | "type": "object", 183 | "description": "Plain document identifier", 184 | "properties": { 185 | "id": { 186 | "type": "string", 187 | "description": "The unique identifier of the document" 188 | } 189 | } 190 | } 191 | }, 192 | "securitySchemes": { 193 | "bearerAuth": { 194 | "type": "http", 195 | "scheme": "bearer" 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { json, RouteHandler } from 'cloudflare-basics' 2 | 3 | export function withAuth( 4 | route: RouteHandler 5 | ): RouteHandler { 6 | return async (options) => { 7 | const authSecret = options.env.AUTH_SECRET 8 | 9 | if (!authSecret) { 10 | console.warn('AUTH_SECRET not set') 11 | return route(options) 12 | } 13 | 14 | const authorization = options.request.headers.get('Authorization') 15 | 16 | if (authorization !== `Bearer ${authSecret}`) { 17 | console.warn('Unauthorized', options.request.url) 18 | return json({ error: 'Unauthorized' }, 403) 19 | } 20 | 21 | return route(options) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/middleware/cache.ts: -------------------------------------------------------------------------------- 1 | import { RouteHandler } from 'cloudflare-basics' 2 | 3 | export function withCloudflareCache( 4 | route: RouteHandler 5 | ): RouteHandler { 6 | return async (options) => { 7 | const cacheUrl = new URL(options.request.url) 8 | 9 | const originalRequest = options.request.request 10 | 11 | // Construct the cache key from the cache URL 12 | const cacheKey = new Request(cacheUrl.toString(), originalRequest) 13 | const cache = caches.default 14 | 15 | // Check whether the value is already available in the cache 16 | // if not, you will need to fetch it from origin, and store it in the cache 17 | let response = await cache.match(cacheKey) 18 | 19 | if (!response) { 20 | // If not in cache, get it from origin 21 | response = await route(options) 22 | 23 | // We need to clone the response because we're editing the headers 24 | const clonedResponse = response.clone() 25 | 26 | // Cache API respects Cache-Control headers. Setting s-max-age to 10 27 | // will limit the response to be in cache for 10 seconds max 28 | // Any changes made to the response here will be reflected in the cached value 29 | clonedResponse.headers.append('Cache-Control', 's-maxage=10') 30 | 31 | options.ctx.waitUntil(cache.put(cacheKey, clonedResponse)) 32 | } 33 | 34 | return response 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/routes/chat-documents-suggest.ts: -------------------------------------------------------------------------------- 1 | import { searchPartialDocumentsByContent } from '@/data/documents/getter' 2 | import { Env } from '@/helpers/env' 3 | import { getOpenAIProvider } from '@/helpers/openai' 4 | import { generateText } from 'ai' 5 | import { json, withZod } from 'cloudflare-basics' 6 | import { stripIndent } from 'common-tags' 7 | import { z } from 'zod' 8 | import { withAuth } from '../middleware/auth' 9 | 10 | const messageSchema = z.object({ 11 | role: z.enum(['user', 'assistant']), 12 | content: z.string().min(1), 13 | }) 14 | 15 | const schema = z.object({ 16 | messages: z.array(messageSchema), 17 | namespace: z.string().default('default'), 18 | }) 19 | 20 | type schemaType = z.infer 21 | 22 | // Search for documents relevant to the user's question 23 | export const RouteChatDocumentsSuggest = withAuth( 24 | withZod(schema, async ({ env, data }) => { 25 | const openaiProvider = getOpenAIProvider(env) 26 | 27 | const prompt = stripIndent` 28 | You are a helpful assistant that can answer questions about the law. Based on the following conversation, 29 | what would be the most relevent search query to look up in the law? Return only the search query, no other text. 30 | 31 | ${data.messages 32 | .map((message) => `${message.role}: ${message.content}`) 33 | .join('\n')} 34 | ` 35 | 36 | const result = await generateText({ 37 | model: openaiProvider('gpt-3.5-turbo'), 38 | prompt, 39 | }) 40 | 41 | const documents = await searchPartialDocumentsByContent({ 42 | text: result.text, 43 | namespace: data.namespace, 44 | env, 45 | }) 46 | 47 | return json(documents) 48 | }) 49 | ) 50 | -------------------------------------------------------------------------------- /src/routes/chat.ts: -------------------------------------------------------------------------------- 1 | import { getDocumentsByIds } from '@/data/documents/getter' 2 | import { Env } from '@/helpers/env' 3 | import { streamText } from 'ai' 4 | import { oneLine, stripIndent } from 'common-tags' 5 | 6 | import { getOpenAIProvider } from '@/helpers/openai' 7 | import { withZod } from 'cloudflare-basics' 8 | import { z } from 'zod' 9 | import { limitedJoin } from '@/lib/tokenize' 10 | 11 | const messageSchema = z.object({ 12 | role: z.enum(['user', 'assistant']), 13 | content: z.string().min(1), 14 | }) 15 | 16 | const schema = z.object({ 17 | messages: z.array(messageSchema), 18 | document_ids: z.array(z.number()), 19 | namespace: z.string().default('default'), 20 | }) 21 | 22 | type schemaType = z.infer 23 | 24 | export const RouteChat = withZod( 25 | schema, 26 | async ({ data, env }) => { 27 | const documents = await getDocumentsByIds({ 28 | ids: data.document_ids, 29 | namespace: data.namespace, 30 | env, 31 | }) 32 | 33 | const contextText = limitedJoin( 34 | documents.map((doc) => doc.text), 35 | 1500 36 | ) 37 | 38 | const systemPrompt = stripIndent`${oneLine` 39 | You are a very enthusiastic bot who loves 40 | to help people! Given the following sections from the 41 | law, answer the question using only that information, 42 | outputted in markdown format. If you are unsure and the answer 43 | is not explicitly written in the provided context, say 44 | "Sorry, I don't know how to help with that."`} 45 | 46 | Context sections: 47 | ${contextText} 48 | ` 49 | 50 | const openaiProvider = getOpenAIProvider(env) 51 | 52 | const result = streamText({ 53 | model: openaiProvider('gpt-4o'), 54 | system: systemPrompt, 55 | messages: data.messages, 56 | }) 57 | 58 | return result.toDataStreamResponse() 59 | } 60 | ) 61 | -------------------------------------------------------------------------------- /src/routes/documents-ai-search.ts: -------------------------------------------------------------------------------- 1 | import { searchDocumentsByContent } from '@/data/documents/getter' 2 | import { Env } from '@/helpers/env' 3 | import { getGoogleAIProvider } from '@/helpers/google' 4 | import { generateText, streamText } from 'ai' 5 | import { withZod } from 'cloudflare-basics' 6 | import { stripIndent } from 'common-tags' 7 | import { z } from 'zod' 8 | 9 | const schema = z.object({ 10 | question: z.string().min(1), 11 | namespace: z.string().default('default'), 12 | }) 13 | 14 | type schemaType = z.infer 15 | 16 | export const RouteDocumentsAiSearch = withZod( 17 | schema, 18 | async ({ env, data }) => { 19 | const query = await generateText({ 20 | model: getGoogleAIProvider(env)('gemini-1.5-pro'), 21 | prompt: `Turn the following question into a search query: ${data.question}. Respond with the query, no other text.`, 22 | }) 23 | 24 | const documents = await searchDocumentsByContent({ 25 | text: query.text, 26 | namespace: data.namespace, 27 | env, 28 | }) 29 | 30 | const prompt = stripIndent` 31 | You are a helpful assistant that can answer questions about legal documents. Respond in markdown format. Only respond with the answer, no other text. 32 | 33 | Here is the user's question: 34 | ${data.question} 35 | 36 | Here are the documents: 37 | ${documents 38 | .map((document) => `# ${document.title}\n${document.text}`) 39 | .join('-----\n')} 40 | ` 41 | 42 | const result = streamText({ 43 | model: getGoogleAIProvider(env)('gemini-1.5-pro'), 44 | prompt, 45 | }) 46 | 47 | return result.toTextStreamResponse() 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /src/routes/documents-retrive.ts: -------------------------------------------------------------------------------- 1 | import { getDocumentById } from '@/data/documents/getter' 2 | import { Env } from '@/helpers/env' 3 | import { json, withZod } from 'cloudflare-basics' 4 | import { z } from 'zod' 5 | 6 | const schema = z.object({ 7 | documentId: z.coerce.number(), 8 | namespace: z.string().default('default'), 9 | }) 10 | 11 | type schemaType = z.infer 12 | 13 | export const RouteDocumentsRetrive = withZod( 14 | schema, 15 | async ({ data, env }) => { 16 | const document = await getDocumentById({ 17 | id: data.documentId, 18 | namespace: data.namespace, 19 | env, 20 | }) 21 | 22 | if (!document) { 23 | return json({ error: 'Document not found' }, 404) 24 | } 25 | 26 | return json(document) 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /src/routes/documents-search.ts: -------------------------------------------------------------------------------- 1 | import { searchPartialDocumentsByContent } from '@/data/documents/getter' 2 | import { Env } from '@/helpers/env' 3 | import { json, withZod } from 'cloudflare-basics' 4 | import { z } from 'zod' 5 | 6 | const schema = z.object({ 7 | query: z.string().min(1), 8 | namespace: z.string().default('default'), 9 | }) 10 | 11 | type schemaType = z.infer 12 | 13 | export const RouteDocumentsSearch = withZod( 14 | schema, 15 | async (options) => { 16 | const documents = await searchPartialDocumentsByContent({ 17 | text: options.data.query, 18 | namespace: options.data.namespace, 19 | env: options.env, 20 | }) 21 | 22 | return json(documents) 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /src/routes/documents-submit.ts: -------------------------------------------------------------------------------- 1 | import { insertDocument } from '@/data/documents/setter' 2 | import { Env } from '@/helpers/env' 3 | import { json, withZod } from 'cloudflare-basics' 4 | import { z } from 'zod' 5 | import { withAuth } from '../middleware/auth' 6 | 7 | const schema = z.object({ 8 | url: z.string().url(), 9 | text: z.string().min(1), 10 | namespace: z.string().default('default'), 11 | }) 12 | 13 | type schemaType = z.infer 14 | 15 | export const RouteDocumentsSubmit = withAuth( 16 | withZod(schema, async (options) => { 17 | const documentId = await insertDocument({ 18 | document: options.data, 19 | env: options.env, 20 | }) 21 | 22 | return json({ id: documentId }) 23 | }) 24 | ) 25 | -------------------------------------------------------------------------------- /src/routes/documents-suggest.ts: -------------------------------------------------------------------------------- 1 | import { searchDocumentsByTitle } from '@/data/documents/getter' 2 | import { Env } from '@/helpers/env' 3 | import { json, withZod } from 'cloudflare-basics' 4 | import { z } from 'zod' 5 | 6 | const schema = z.object({ 7 | query: z.string().min(1), 8 | namespace: z.string().default('default'), 9 | }) 10 | 11 | type schemaType = z.infer 12 | 13 | // Search documents based on their title 14 | export const RouteDocumentsSuggest = withZod( 15 | schema, 16 | async ({ env, data }) => { 17 | const documents = await searchDocumentsByTitle({ 18 | title: data.query, 19 | namespace: data.namespace, 20 | env, 21 | }) 22 | 23 | return json(documents) 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | import { Env } from '@/helpers/env' 2 | import { Router } from 'cloudflare-basics' 3 | import { RouteChat } from './routes/chat' 4 | import { RouteDocumentsSearch } from './routes/documents-search' 5 | import { RouteDocumentsSubmit } from './routes/documents-submit' 6 | import { RouteDocumentsSuggest } from './routes/documents-suggest' 7 | import { RouteChatDocumentsSuggest } from './routes/chat-documents-suggest' 8 | import { RouteDocumentsRetrive } from './routes/documents-retrive' 9 | import { RouteDocumentsAiSearch } from './routes/documents-ai-search' 10 | 11 | export default { 12 | async fetch( 13 | request: Request, 14 | env: Env, 15 | ctx: ExecutionContext 16 | ): Promise { 17 | const router = new Router() 18 | 19 | router.get('/', async ({ request }) => { 20 | return new Response('Welcome to LawGPT!') 21 | }) 22 | 23 | // Search route 24 | router.get('/documents/search', RouteDocumentsSearch) 25 | 26 | // AI search route 27 | router.get('/documents/ai-search', RouteDocumentsAiSearch) 28 | 29 | // Submit document route 30 | router.post('/documents', RouteDocumentsSubmit) 31 | 32 | // Suggest documents route 33 | router.get('/documents/suggest', RouteDocumentsSuggest) 34 | 35 | // Retrive document route 36 | router.get('/documents/:documentId', RouteDocumentsRetrive) 37 | 38 | // Chat/Answer route 39 | router.post('/chat', RouteChat) 40 | 41 | // Chat suggest documents route 42 | router.post('/chat/suggest', RouteChatDocumentsSuggest) 43 | 44 | // Suggest documents route 45 | 46 | return ( 47 | router.handle(request, env, ctx) ?? 48 | new Response('Not Found', { status: 404 }) 49 | ) 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "esnext", 5 | "lib": ["ES2021"], 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "types": ["@cloudflare/workers-types"], 9 | "paths": { 10 | "@/*": ["./*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "lawgpt-search" 2 | 3 | main = "src/worker.ts" 4 | 5 | compatibility_date = "2024-11-06" 6 | 7 | [vars] 8 | ENVIRONMENT = "production" 9 | 10 | [ai] 11 | binding = "AI" 12 | 13 | [[vectorize]] 14 | binding = "VECTORIZE" 15 | index_name = "lawgpt-vector2" 16 | 17 | [[d1_databases]] 18 | binding = "DB" 19 | database_name = "lawgpt-d1" 20 | database_id = "4d20edf7-a77b-4ef0-b46a-01c84469569f" --------------------------------------------------------------------------------