diff --git a/api/controllers/chat-controller.ts b/api/controllers/chat-controller.ts index 7a18e6e..c49e099 100644 --- a/api/controllers/chat-controller.ts +++ b/api/controllers/chat-controller.ts @@ -1,9 +1,6 @@ import * as express from "express"; import { ChatHandler } from "../handlers/chat.handler"; -import { - chatHistorySchema, - chatRequestSchema, -} from "../lib/validation-schemas"; +import { chatHistorySchema, chatRequestSchema } from "../lib/validation-schemas"; import { Result } from "../lib/result"; import { generateErrorResponse } from "../utils/utils"; export class ChatController { @@ -17,23 +14,17 @@ export class ChatController { this.router.post(`${this.path}`, this.chat); } - async chat( - req: express.Request, - res: express.Response, - next: express.NextFunction - ) { + async chat(req: express.Request, res: express.Response, next: express.NextFunction) { try { - const { question, chatHistory } = chatRequestSchema.parse(req.body); + const { question, chatHistory, documentId } = chatRequestSchema.parse(req.body); const chatHandler = new ChatHandler(); const history = chatHistorySchema.parse(JSON.parse(chatHistory)); - const data = await chatHandler.handle({ question, chatHistory: history }); + const data = await chatHandler.handle({ question, chatHistory: history, documentId }); if (data) { const result = Result.ok(data.getValue()); res.status(200).json(result); } else { - res - .status(400) - .json(Result.fail("Unable to create document type", 400)); + res.status(400).json(Result.fail("Unable to generate model response", 400)); } } catch (error) { generateErrorResponse(error, res, next); diff --git a/api/controllers/document-controller.ts b/api/controllers/document-controller.ts new file mode 100644 index 0000000..55c00b8 --- /dev/null +++ b/api/controllers/document-controller.ts @@ -0,0 +1,31 @@ +import * as express from "express"; +import { Result } from "../lib/result"; +import { generateErrorResponse } from "../utils/utils"; +import { GetDocumentsHandler } from "../handlers/get-documents-handler"; + +export class DocumentController { + path = "/documents"; + router = express.Router(); + + constructor() { + this.initializeRoute(); + } + + initializeRoute() { + this.router.get(this.path, this.getDocument); + } + + async getDocument(req: express.Request, res: any, next: express.NextFunction) { + try { + const documentHandler = new GetDocumentsHandler(); + const data = await documentHandler.handle(); + if (data) { + const result = Result.ok(data.getValue()); + res.status(200).json(result); + } + } catch (error) { + generateErrorResponse(error, res, next); + next(error); + } + } +} diff --git a/api/handlers/chat.handler.ts b/api/handlers/chat.handler.ts index f6a47e0..710a250 100644 --- a/api/handlers/chat.handler.ts +++ b/api/handlers/chat.handler.ts @@ -6,25 +6,14 @@ import { EmbeddingService } from "../services/embed.service"; import { getValue } from "../utils"; import { CHAT_PARAMS } from "./../../presentation/src/constants"; -export class ChatHandler - implements - IRequestHandler>> -{ +export class ChatHandler implements IRequestHandler>> { private readonly apiKey: string = getValue("API_KEY"); - async handle({ - question, - chatHistory, - }: IChatRequestDTO): Promise>> { + async handle({ question, chatHistory, documentId }: IChatRequestDTO): Promise>> { try { - const embeddingService: EmbeddingService = new EmbeddingService( - this.apiKey - ); + const embeddingService: EmbeddingService = new EmbeddingService(this.apiKey); const { MATCH_COUNT, SIMILARITY_THRESHOLD } = CHAT_PARAMS; - const matches = await embeddingService.getQueryMatches( - question, - MATCH_COUNT, - SIMILARITY_THRESHOLD - ); + //Query here + const matches = await embeddingService.getQueryMatches(question, MATCH_COUNT, SIMILARITY_THRESHOLD, documentId); // if (!matches?.length) { // //take care of empty results here // return "No matches for user query"; diff --git a/api/handlers/document-handler.ts b/api/handlers/create-document-handler.ts similarity index 86% rename from api/handlers/document-handler.ts rename to api/handlers/create-document-handler.ts index 51cb284..e997cea 100644 --- a/api/handlers/document-handler.ts +++ b/api/handlers/create-document-handler.ts @@ -4,7 +4,7 @@ import { DocumentRepository } from "../repositories/document.repository"; import { ICreateDocumentDTO } from "../repositories/dtos/dtos"; import { IDocumentModel } from "../repositories/model"; -export class DocumentHandler implements IRequestHandler> { +export class CreateDocumentHandler implements IRequestHandler> { async handle(request: ICreateDocumentDTO): Promise> { try { let response: IDocumentModel | undefined; diff --git a/api/handlers/get-documents-handler.ts b/api/handlers/get-documents-handler.ts new file mode 100644 index 0000000..8527fd3 --- /dev/null +++ b/api/handlers/get-documents-handler.ts @@ -0,0 +1,17 @@ +import { IRequestHandler } from "../interfaces/handler"; +import { Result } from "../lib/result"; +import { DocumentRepository } from "../repositories/document.repository"; +import { IDocumentModel } from "../repositories/model"; + +export class GetDocumentsHandler implements IRequestHandler<{}, Result> { + async handle(): Promise> { + try { + let response: IDocumentModel[]; + const documentRespository: DocumentRepository = new DocumentRepository(); + response = await documentRespository.getDocuments(); + return Result.ok(response); + } catch (error) { + console.error(error); + } + } +} diff --git a/api/index.ts b/api/index.ts index b6a6eb4..a0bbbf6 100644 --- a/api/index.ts +++ b/api/index.ts @@ -4,10 +4,17 @@ import { ChatController } from "./controllers/chat-controller"; import { DocmentTypeController } from "./controllers/document-type.controller"; import { DomainController } from "./controllers/domain.controller"; import { EmbeddingController } from "./controllers/embed.controller"; +import { DocumentController } from "./controllers/document-controller"; const port: number = Number(process.env.PORT) || 3000; const app = new App( - [new EmbeddingController(), new DomainController(), new DocmentTypeController(), new ChatController()], + [ + new EmbeddingController(), + new DomainController(), + new DocmentTypeController(), + new ChatController(), + new DocumentController(), + ], port ); app.listen(); diff --git a/api/interfaces/embedding-service.interface.ts b/api/interfaces/embedding-service.interface.ts index f2a1c0d..3221a06 100644 --- a/api/interfaces/embedding-service.interface.ts +++ b/api/interfaces/embedding-service.interface.ts @@ -6,7 +6,7 @@ import { IQueryMatch } from "./generic-interface"; export interface IEmbeddingService { generateEmbeddings( taskType: TaskType, - role?: string, + role?: string ): Promise<{ embedding: number[]; text: string; @@ -16,11 +16,12 @@ export interface IEmbeddingService { createDocumentsEmbeddings( title: string, documentType: DocumentTypeEnum, - domain: DomainEnum, + domain: DomainEnum ): Promise>; getQueryMatches( query: string, matchCount: number, similarityThreshold: number, + documentId: number ): Promise; } diff --git a/api/interfaces/handler.ts b/api/interfaces/handler.ts index b368722..756e7d5 100644 --- a/api/interfaces/handler.ts +++ b/api/interfaces/handler.ts @@ -1,3 +1,4 @@ +// Todo, refactor IRequestHandler< TResponse, TRequest = any> so as to make TRequest optional type export interface IRequestHandler { handle(request?: TRequest): Promise; } diff --git a/api/lib/validation-schemas.ts b/api/lib/validation-schemas.ts index 3e30f29..8fc03b9 100644 --- a/api/lib/validation-schemas.ts +++ b/api/lib/validation-schemas.ts @@ -12,7 +12,10 @@ export const domainRequestSchema = z.object({ name }); const docType = z.nativeEnum(DocumentTypeEnum); export const docTypeRequestSchema = z.object({ name: docType }); +export const createDocumentSchema = z.object({ title: z.string() }); + export const chatRequestSchema = z.object({ + documentId: z.number(), question: z.string(), metaData: z.optional( z.object({ diff --git a/api/repositories/document.repository.ts b/api/repositories/document.repository.ts index 49dcae7..f28bfeb 100644 --- a/api/repositories/document.repository.ts +++ b/api/repositories/document.repository.ts @@ -17,10 +17,7 @@ export class DocumentRepository extends Database { //In your instructiona prompt, ask the AI to genrate code if any is available const docExists: IDocumentModel = await this.findOne(title); if (docExists) { - throw new HttpException( - HTTP_RESPONSE_CODE.BAD_REQUEST, - "document already exists", - ); + throw new HttpException(HTTP_RESPONSE_CODE.BAD_REQUEST, "document already exists"); } return await this.prisma.documents.create({ data: { @@ -44,6 +41,14 @@ export class DocumentRepository extends Database { } } + async getDocuments(): Promise { + try { + return await this.prisma.documents.findMany(); + } catch (error) { + console.error(error); + } + } + async insertMany(): Promise> { try { const result = await this.prisma.documents.createMany(); diff --git a/api/repositories/dtos/dtos.ts b/api/repositories/dtos/dtos.ts index 5d372f8..4795e3f 100644 --- a/api/repositories/dtos/dtos.ts +++ b/api/repositories/dtos/dtos.ts @@ -27,6 +27,7 @@ export interface ICreateDocumentTypeRequestDTO { } export interface IChatRequestDTO { + documentId: number; question: string; metaData?: { documentId: number; diff --git a/api/repositories/embedding.repository.ts b/api/repositories/embedding.repository.ts index de675b4..5894394 100644 --- a/api/repositories/embedding.repository.ts +++ b/api/repositories/embedding.repository.ts @@ -132,7 +132,14 @@ export class EmbeddingRepository extends Database { /** * Queries the database for listings that are similar to a given embedding. */ - async matchDocuments(embedding: any, matchCount: number, matchThreshold: number): Promise { + //Raw query failed. Code: `42601`. Message: `ERROR: syntax error at or near "WHERE"` + async matchDocuments( + embedding: any, + matchCount: number, + matchThreshold: number, + documentId: number + ): Promise { + console.log({ documentId }); //change text to document_embedding //check how to select textembedding from DB const matches = await this.prisma.$queryRaw` @@ -141,7 +148,9 @@ export class EmbeddingRepository extends Database { 1 - ("textEmbedding" <=> ${embedding}::vector) as similarity FROM "Embeddings" - WHERE + WHERE + "documentId" = ${documentId} + AND 1 - ("textEmbedding" <=> ${embedding}::vector) > ${matchThreshold} ORDER BY similarity DESC diff --git a/api/services/embed.service.ts b/api/services/embed.service.ts index 7156ebd..136ef8d 100644 --- a/api/services/embed.service.ts +++ b/api/services/embed.service.ts @@ -265,7 +265,12 @@ export class EmbeddingService extends GenerativeAIService implements IEmbeddingS * @returns An array of query matches. * @throws {HttpException} if query embeddings could not be generated. **/ - async getQueryMatches(query: string, matchCount: number, similarityThreshold: number): Promise { + async getQueryMatches( + query: string, + matchCount: number, + similarityThreshold: number, + documentId: number + ): Promise { const queryEmbeddings = await this.generateUserQueryEmbeddings(query); if (!queryEmbeddings?.length) { throw new HttpException(HTTP_RESPONSE_CODE.BAD_REQUEST, "Unable to generate user query embeddings"); @@ -273,7 +278,7 @@ export class EmbeddingService extends GenerativeAIService implements IEmbeddingS const embeddingRepository: EmbeddingRepository = new EmbeddingRepository(); const embeddings = queryEmbeddings.map((embedding) => //passing in the documentId here. - embeddingRepository.matchDocuments(embedding, matchCount, similarityThreshold) + embeddingRepository.matchDocuments(embedding, matchCount, similarityThreshold, documentId) ); const matches = await Promise.all(embeddings); return matches.flat(); diff --git a/presentation/src/ErrorFallBackComponent.tsx b/presentation/src/ErrorFallBackComponent.tsx new file mode 100644 index 0000000..c961c0f --- /dev/null +++ b/presentation/src/ErrorFallBackComponent.tsx @@ -0,0 +1,8 @@ +export function ErrorFallBackComponent() { + return ( +
+

Oops! Something went wrong.

+

Please try again later.

+
+ ); +} diff --git a/presentation/src/components/ChatForm.tsx b/presentation/src/components/ChatForm.tsx index 519fab6..19eccff 100644 --- a/presentation/src/components/ChatForm.tsx +++ b/presentation/src/components/ChatForm.tsx @@ -5,6 +5,7 @@ import useAxiosPrivate from "../hooks/useAxiosPrivate"; import NavBar from "./NavBar"; import markdownIt from "markdown-it"; import Books from "./DropDown"; +import { IDocument } from "../interfaces/document.interface"; interface IHistory { role: string; @@ -17,6 +18,11 @@ export function Thread() { const [question, setQuestion] = useState(""); const [chatHistory, setChatHistory] = useState([]); const [loading, setLoading] = useState(false); + const [selectedBook, setSelectedBook] = useState(); + + const handleBookSelect = (bookData: IDocument) => { + setSelectedBook(bookData); + }; const formAction = async () => { if (!question) { @@ -28,6 +34,7 @@ export function Thread() { setQuestion(""); console.log(chatHistory); const response = await axiosPrivate.post("/chat", { + documentId: selectedBook?.id, question, chatHistory: JSON.stringify(chatHistory.slice(0, 4)), }); @@ -76,7 +83,7 @@ export function Thread() {
- +
@@ -113,7 +120,7 @@ export function Thread() { style={{ marginBottom: "10px", marginTop: "10px", - height: "70px", + height: "30px", }} >
@@ -131,8 +138,17 @@ export function Thread() {
{chatHistory.map((chatItem, index) => ( - - {chatItem.role && chatItem.role === "user" ? "Question" : "Answer"} + {chatItem.parts.map((part, i) => ( void; +} + +function Books({ onBookSelect }: Readonly) { + const axiosPrivate = useAxiosPrivate(); const [selectedBook, setSelectedBook] = useState("Select Book"); + const [books, setBooks] = useState([]); - const handleSelect = (eventKey: string | null) => { + useEffect(() => { + const fetchData = async () => { + try { + const response = await axiosPrivate.get("/documents"); + setBooks(response.data.data); + } catch (error) { + console.error(error); + } + }; + fetchData(); + }, []); + + const handleSelect = async (eventKey: string | null) => { if (eventKey) { setSelectedBook(eventKey); + const selectedBookData = books.find((book: { title: string }) => book.title === eventKey); + if (selectedBookData) { + onBookSelect(selectedBookData); + } } }; - return ( - - - {selectedBook} - - - - MyBid - Microservice Pattern - Pregmatic Engineer - - - ); + try { + return ( + + + {selectedBook} + + + {books?.map((document: { title: string; id: number }) => ( + + {document.title} + + ))} + + + ); + } catch (error) { + console.error(error); + } } export default Books; diff --git a/presentation/src/components/ErrorBoundry.tsx b/presentation/src/components/ErrorBoundry.tsx new file mode 100644 index 0000000..868dd63 --- /dev/null +++ b/presentation/src/components/ErrorBoundry.tsx @@ -0,0 +1,37 @@ +import React, { ErrorInfo } from "react"; + +interface ErrorBoundaryProps { + fallBackComponent: React.ReactNode; + children?: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static getDerivedStateFromError(_: Error): ErrorBoundaryState { + return { hasError: true }; + } + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error("Error:", error); + console.error("Error Info:", errorInfo); + } + + render() { + const { hasError } = this.state; + const { fallBackComponent, children } = this.props; + if (hasError) { + return fallBackComponent; + } + return <>{children}; + } +} diff --git a/presentation/src/components/NavBar.tsx b/presentation/src/components/NavBar.tsx index 9f41b4c..39a0427 100644 --- a/presentation/src/components/NavBar.tsx +++ b/presentation/src/components/NavBar.tsx @@ -1,20 +1,20 @@ -import Container from 'react-bootstrap/Container'; -import Nav from 'react-bootstrap/Nav'; -import Navbar from 'react-bootstrap/Navbar'; +import Container from "react-bootstrap/Container"; +import Nav from "react-bootstrap/Nav"; +import Navbar from "react-bootstrap/Navbar"; function NavBar() { return ( - - - - - + + + + + ); } -export default NavBar; \ No newline at end of file +export default NavBar; diff --git a/presentation/src/index.css b/presentation/src/index.css index 83efd9b..d058d69 100644 --- a/presentation/src/index.css +++ b/presentation/src/index.css @@ -29,15 +29,14 @@ } body { - background-color: #f8f9fa; + background-color: #000; } .loader { position: relative; - background-color: rgb(235, 235, 235); + background-color: #4e575b; max-width: 100%; height: auto; - background: #efefee; overflow: hidden; border-radius: 4px; margin-bottom: 4px; @@ -62,4 +61,12 @@ body { 100% { transform: translateX(100%); } +} + +pre { + border-radius: 4px; + padding: 10px; + overflow-x: auto; + background-color: #282c34; + color: rgb(97, 175, 239) } \ No newline at end of file diff --git a/presentation/src/interfaces/document.interface.ts b/presentation/src/interfaces/document.interface.ts new file mode 100644 index 0000000..aeb8dc9 --- /dev/null +++ b/presentation/src/interfaces/document.interface.ts @@ -0,0 +1,4 @@ +export interface IDocument { + id: number; + title: string; +} diff --git a/presentation/src/main.tsx b/presentation/src/main.tsx index 1cd3608..04a87da 100644 --- a/presentation/src/main.tsx +++ b/presentation/src/main.tsx @@ -3,9 +3,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; +import { ErrorBoundary } from "./components/ErrorBoundry.tsx"; +import { ErrorFallBackComponent } from "./ErrorFallBackComponent.tsx"; ReactDOM.createRoot(document.getElementById("root")!).render( - + + + );