khronoz commited on
Commit
d5e0a0f
·
unverified ·
1 Parent(s): 540eabc

V.0.2.1 (#25)

Browse files

* Fixed 'Nonetype' is not subscriptable error

* Fixed not getting secure next-auth cookies

* Updated search to comply with document set selection

* Updated run.py to exit after CREATE_VECTOR_STORE

* Updated token limit for memory

* Bugfix in checking for wrong value types

* Better error handling

backend/backend/app/api/routers/chat.py CHANGED
@@ -110,7 +110,7 @@ async def chat(
110
 
111
  memory = ChatMemoryBuffer.from_defaults(
112
  chat_history=messages,
113
- token_limit=3900,
114
  )
115
 
116
  logger.info(f"Memory: {memory.get()}")
 
110
 
111
  memory = ChatMemoryBuffer.from_defaults(
112
  chat_history=messages,
113
+ token_limit=4096,
114
  )
115
 
116
  logger.info(f"Memory: {memory.get()}")
backend/backend/app/api/routers/query.py CHANGED
@@ -4,7 +4,6 @@ from typing import List
4
  from fastapi import APIRouter, Depends, HTTPException, Request, status
5
  from fastapi.responses import StreamingResponse
6
  from fastapi.websockets import WebSocketDisconnect
7
- from llama_index import VectorStoreIndex
8
  from llama_index.llms.types import MessageRole
9
  from pydantic import BaseModel
10
 
@@ -28,6 +27,7 @@ class _Message(BaseModel):
28
 
29
  class _ChatData(BaseModel):
30
  messages: List[_Message]
 
31
 
32
 
33
  @r.post("")
@@ -36,8 +36,13 @@ async def query(
36
  # Note: To support clients sending a JSON object using content-type "text/plain",
37
  # we need to use Depends(json_to_model(_ChatData)) here
38
  data: _ChatData = Depends(json_to_model(_ChatData)),
39
- index: VectorStoreIndex = Depends(get_index),
40
  ):
 
 
 
 
 
 
41
  # check preconditions and get last message which is query
42
  if len(data.messages) == 0:
43
  raise HTTPException(
@@ -50,7 +55,6 @@ async def query(
50
  status_code=status.HTTP_400_BAD_REQUEST,
51
  detail="Last message must be from user",
52
  )
53
- logger = logging.getLogger("uvicorn")
54
  logger.info(f"Query: {lastMessage}")
55
 
56
  # Query index
 
4
  from fastapi import APIRouter, Depends, HTTPException, Request, status
5
  from fastapi.responses import StreamingResponse
6
  from fastapi.websockets import WebSocketDisconnect
 
7
  from llama_index.llms.types import MessageRole
8
  from pydantic import BaseModel
9
 
 
27
 
28
  class _ChatData(BaseModel):
29
  messages: List[_Message]
30
+ document: str
31
 
32
 
33
  @r.post("")
 
36
  # Note: To support clients sending a JSON object using content-type "text/plain",
37
  # we need to use Depends(json_to_model(_ChatData)) here
38
  data: _ChatData = Depends(json_to_model(_ChatData)),
 
39
  ):
40
+ logger = logging.getLogger("uvicorn")
41
+ # get the document set selected from the request body
42
+ document_set = data.document
43
+ logger.info(f"Document Set: {document_set}")
44
+ # get the index for the selected document set
45
+ index = get_index(collection_name=document_set)
46
  # check preconditions and get last message which is query
47
  if len(data.messages) == 0:
48
  raise HTTPException(
 
55
  status_code=status.HTTP_400_BAD_REQUEST,
56
  detail="Last message must be from user",
57
  )
 
58
  logger.info(f"Query: {lastMessage}")
59
 
60
  # Query index
backend/backend/app/api/routers/search.py CHANGED
@@ -2,7 +2,6 @@ import logging
2
  import re
3
 
4
  from fastapi import APIRouter, Depends, HTTPException, Request, status
5
- from llama_index import VectorStoreIndex
6
  from llama_index.postprocessor import SimilarityPostprocessor
7
  from llama_index.retrievers import VectorIndexRetriever
8
 
@@ -22,16 +21,18 @@ Instead it returns the relevant information from the index.
22
  @r.get("")
23
  async def search(
24
  request: Request,
25
- index: VectorStoreIndex = Depends(get_index),
26
  query: str = None,
 
27
  ):
28
  # query = request.query_params.get("query")
29
  logger = logging.getLogger("uvicorn")
30
- logger.info(f"Search: {query}")
31
- if query is None:
 
 
32
  raise HTTPException(
33
  status_code=status.HTTP_400_BAD_REQUEST,
34
- detail="No search info provided",
35
  )
36
 
37
  # configure retriever
 
2
  import re
3
 
4
  from fastapi import APIRouter, Depends, HTTPException, Request, status
 
5
  from llama_index.postprocessor import SimilarityPostprocessor
6
  from llama_index.retrievers import VectorIndexRetriever
7
 
 
21
  @r.get("")
22
  async def search(
23
  request: Request,
 
24
  query: str = None,
25
+ docSelected: str = None,
26
  ):
27
  # query = request.query_params.get("query")
28
  logger = logging.getLogger("uvicorn")
29
+ logger.info(f"Document Set: {docSelected} | Search: {query}")
30
+ # get the index for the selected document set
31
+ index = get_index(collection_name=docSelected)
32
+ if query is None or docSelected is None:
33
  raise HTTPException(
34
  status_code=status.HTTP_400_BAD_REQUEST,
35
+ detail="No search info/document set provided",
36
  )
37
 
38
  # configure retriever
backend/backend/app/utils/auth.py CHANGED
@@ -71,16 +71,18 @@ def get_user_from_JWT(token: str):
71
  )
72
 
73
  payload = decodeJWT(token)
74
- user_id = payload["sub"]
75
 
76
- if user_id is not None:
 
77
  # Try to get the user from the database using the user_id
78
  response = supabase.table("users").select("*").eq("id", user_id).execute()
79
  # print(response.data)
80
  if len(response.data) == 0:
81
  return False
82
- return True
83
- return False
 
 
84
 
85
 
86
  async def validate_user(
@@ -89,13 +91,17 @@ async def validate_user(
89
  ):
90
  try:
91
  logger = logging.getLogger("uvicorn")
92
- # logger.debug(f"Auth Token: {auth_token} | API Key: {api_key}")
93
  if auth_token is not None or api_key is not None:
94
  # If the access token is empty, use the 'X-API-Key' from the header
95
- if auth_token is None:
96
  # Access the 'X-API-Key' header directly
97
  if BACKEND_API_KEY is None:
98
  raise ValueError("Backend API key is not set in Backend Service!")
 
 
 
 
99
  # If the 'X-API-Key' does not match the backend API key, raise an error
100
  if api_key != BACKEND_API_KEY:
101
  raise ValueError(
@@ -123,7 +129,7 @@ async def validate_user(
123
  "Invalid token scheme. Please use the format 'Bearer [token]'"
124
  )
125
  # Verify the JWT token is valid
126
- if verify_jwt(jwtoken=jwtoken) is None:
127
  return "Invalid token. Please provide a valid token."
128
  # Check if the user exists in the database
129
  if get_user_from_JWT(token=jwtoken):
 
71
  )
72
 
73
  payload = decodeJWT(token)
 
74
 
75
+ if payload is not None:
76
+ user_id = payload["sub"]
77
  # Try to get the user from the database using the user_id
78
  response = supabase.table("users").select("*").eq("id", user_id).execute()
79
  # print(response.data)
80
  if len(response.data) == 0:
81
  return False
82
+ else:
83
+ return True
84
+ else:
85
+ return False
86
 
87
 
88
  async def validate_user(
 
91
  ):
92
  try:
93
  logger = logging.getLogger("uvicorn")
94
+ # logger.info(f"Auth Token: {auth_token} | API Key: {api_key}")
95
  if auth_token is not None or api_key is not None:
96
  # If the access token is empty, use the 'X-API-Key' from the header
97
+ if auth_token is None or "null" in auth_token:
98
  # Access the 'X-API-Key' header directly
99
  if BACKEND_API_KEY is None:
100
  raise ValueError("Backend API key is not set in Backend Service!")
101
+ if "null" in api_key:
102
+ raise ValueError(
103
+ "Invalid API key provided in the 'X-API-Key' header!"
104
+ )
105
  # If the 'X-API-Key' does not match the backend API key, raise an error
106
  if api_key != BACKEND_API_KEY:
107
  raise ValueError(
 
129
  "Invalid token scheme. Please use the format 'Bearer [token]'"
130
  )
131
  # Verify the JWT token is valid
132
+ if verify_jwt(jwtoken=jwtoken):
133
  return "Invalid token. Please provide a valid token."
134
  # Check if the user exists in the database
135
  if get_user_from_JWT(token=jwtoken):
backend/backend/app/utils/index.py CHANGED
@@ -230,7 +230,7 @@ def load_existing_index(collection_name="PSSCOC"):
230
  logger.info(f"Indexing [{collection_name}] vector store...")
231
  vector_store._collection.create_index()
232
  logger.info(f"Finished indexing [{collection_name}] vector store")
233
- logger.info(vector_store._collection.name)
234
  index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
235
  logger.info(f"Finished loading [{collection_name}] index from Supabase")
236
  logger.info(f"Index ID: {index.index_id}")
 
230
  logger.info(f"Indexing [{collection_name}] vector store...")
231
  vector_store._collection.create_index()
232
  logger.info(f"Finished indexing [{collection_name}] vector store")
233
+ # logger.info(f"Collection Name: {vector_store._collection.name}")
234
  index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
235
  logger.info(f"Finished loading [{collection_name}] index from Supabase")
236
  logger.info(f"Index ID: {index.index_id}")
backend/backend/run.py CHANGED
@@ -31,11 +31,11 @@ if __name__ == "__main__":
31
  # Create the vector store
32
  from backend.app.utils.index import create_index
33
 
34
- logger.info("Creating vector stores first...")
35
  create_index()
36
- logger.info("Vector stores created successfully! Running App...")
37
  # Run the app
38
- run_app()
39
  else:
40
  # Run the app
41
  run_app()
 
31
  # Create the vector store
32
  from backend.app.utils.index import create_index
33
 
34
+ logger.info("Indexing Documents & Creating Vector Stores...")
35
  create_index()
36
+ logger.info("Vector Stores created successfully! Exiting...")
37
  # Run the app
38
+ # run_app()
39
  else:
40
  # Run the app
41
  run_app()
frontend/app/api/status/route.ts CHANGED
@@ -1,28 +1,39 @@
1
- export async function GET(request: Request) {
 
 
2
  const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API as string;
3
 
4
  // Retrieve the session token from the request headers
5
  let session = request.headers.get('Authorization');
6
 
7
- console.log('Status API - headers:', request.headers);
8
 
9
  // Public API key
10
  let api_key = null;
11
 
12
  // If no session, use the public API key
13
- if (!session) {
 
14
  api_key = process.env.BACKEND_API_KEY as string;
 
15
  }
16
 
17
- const res = await fetch(healthcheck_api, {
18
- signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds
19
- headers: {
20
- 'Content-Type': 'application/json',
21
- 'Authorization': session,
22
- 'X-API-Key': api_key,
23
- } as any,
24
- })
25
- const data = await res.json()
26
-
27
- return Response.json({ data })
 
 
 
 
 
 
 
28
  }
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function GET(request: NextRequest) {
4
  const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API as string;
5
 
6
  // Retrieve the session token from the request headers
7
  let session = request.headers.get('Authorization');
8
 
9
+ // console.log('Status API - headers:', request.headers);
10
 
11
  // Public API key
12
  let api_key = null;
13
 
14
  // If no session, use the public API key
15
+ if (session === null || session === undefined || session.includes('undefined')) {
16
+ console.log('No session token found, using public API key');
17
  api_key = process.env.BACKEND_API_KEY as string;
18
+ session = null; // Clear the session token
19
  }
20
 
21
+ try {
22
+ const res = await fetch(healthcheck_api, {
23
+ signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'Authorization': session,
27
+ 'X-API-Key': api_key,
28
+ } as any,
29
+ })
30
+ const data = await res.json()
31
+ if (!res.ok) {
32
+ throw new Error(data.detail || 'Unknown Error');
33
+ }
34
+ return NextResponse.json({ data })
35
+ } catch (error : any) {
36
+ console.error(`${error}`);
37
+ return NextResponse.json({ error: error.message }, { status: 500 })
38
+ }
39
  }
frontend/app/components/header.tsx CHANGED
@@ -53,7 +53,7 @@ export default function Header() {
53
  const signinPage = "/sign-in?callbackUrl=" + encodedPath;
54
 
55
  // Get user session for conditional rendering of user profile and logout buttons and for fetching the API status
56
- const { data: session, status } = useSession();
57
  // console.log('session:', session, 'status:', status);
58
  const supabaseAccessToken = session?.supabaseAccessToken;
59
  // Use SWR for API status fetching
@@ -94,7 +94,7 @@ export default function Header() {
94
 
95
  useEffect(() => {
96
  setMounted(true);
97
- }, []);
98
 
99
  const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
100
 
 
53
  const signinPage = "/sign-in?callbackUrl=" + encodedPath;
54
 
55
  // Get user session for conditional rendering of user profile and logout buttons and for fetching the API status
56
+ const { data: session, status } = useSession()
57
  // console.log('session:', session, 'status:', status);
58
  const supabaseAccessToken = session?.supabaseAccessToken;
59
  // Use SWR for API status fetching
 
94
 
95
  useEffect(() => {
96
  setMounted(true);
97
+ }, [session]);
98
 
99
  const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
100
 
frontend/app/components/query-section.tsx CHANGED
@@ -4,9 +4,12 @@ import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
  import { AutofillQuestion } from "./ui/autofill-prompt";
6
  import { useSession } from "next-auth/react";
 
7
 
8
  export default function QuerySection() {
9
  const { data: session } = useSession();
 
 
10
  const {
11
  messages,
12
  input,
@@ -15,12 +18,16 @@ export default function QuerySection() {
15
  handleInputChange,
16
  reload,
17
  stop,
18
- } = useChat({
19
  api: process.env.NEXT_PUBLIC_QUERY_API,
20
- // Add the access token to the request headers
21
  headers: {
22
- 'Authorization': `Bearer ${session?.supabaseAccessToken}`,
23
- }
 
 
 
 
 
24
  });
25
 
26
  return (
 
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
  import { AutofillQuestion } from "./ui/autofill-prompt";
6
  import { useSession } from "next-auth/react";
7
+ import { useState } from "react";
8
 
9
  export default function QuerySection() {
10
  const { data: session } = useSession();
11
+ const supabaseAccessToken = session?.supabaseAccessToken;
12
+ const [docSelected, setDocSelected] = useState<string>('');
13
  const {
14
  messages,
15
  input,
 
18
  handleInputChange,
19
  reload,
20
  stop,
21
+ } = useChat({
22
  api: process.env.NEXT_PUBLIC_QUERY_API,
 
23
  headers: {
24
+ // Add the access token to the request headers
25
+ 'Authorization': `Bearer ${supabaseAccessToken}`,
26
+ },
27
+ body: {
28
+ // Add the selected document to the request body
29
+ document: docSelected,
30
+ },
31
  });
32
 
33
  return (
frontend/app/components/search-section.tsx CHANGED
@@ -19,7 +19,7 @@ const SearchSection: React.FC = () => {
19
  const handleSearchSubmit = (e: FormEvent) => {
20
  e.preventDefault();
21
  setSearchButtonPressed(true);
22
- handleSearch(query);
23
  };
24
 
25
  return (
 
19
  const handleSearchSubmit = (e: FormEvent) => {
20
  e.preventDefault();
21
  setSearchButtonPressed(true);
22
+ handleSearch(query, docSelected);
23
  };
24
 
25
  return (
frontend/app/components/ui/search/search-results.tsx CHANGED
@@ -6,8 +6,8 @@ import 'react-toastify/dist/ReactToastify.css';
6
  import { SearchHandler, SearchResult } from "@/app/components/ui/search/search.interface";
7
 
8
  export default function SearchResults(
9
- props: Pick<SearchHandler, "query" | "results" | "isLoading" | "searchButtonPressed">
10
- ) {
11
  const [sortedResults, setSortedResults] = useState<SearchResult[]>([]);
12
  const [expandedResult, setExpandedResult] = useState<number | null>(null);
13
 
@@ -17,11 +17,10 @@ export default function SearchResults(
17
  // Reset sortedResults when query is empty
18
  setSortedResults([]);
19
  } else if (props.query.trim() !== "" && props.searchButtonPressed) {
20
- // if results are empty
21
- if (props.results.length === 0) {
22
  setSortedResults([]);
23
- }
24
- else {
25
  // Sort results by similarity score
26
  const sorted = props.results.slice().sort((a, b) => b.similarity_score - a.similarity_score);
27
  // Update sortedResults state
 
6
  import { SearchHandler, SearchResult } from "@/app/components/ui/search/search.interface";
7
 
8
  export default function SearchResults(
9
+ props: Pick<SearchHandler, "query" | "results" | "isLoading" | "searchButtonPressed">
10
+ ) {
11
  const [sortedResults, setSortedResults] = useState<SearchResult[]>([]);
12
  const [expandedResult, setExpandedResult] = useState<number | null>(null);
13
 
 
17
  // Reset sortedResults when query is empty
18
  setSortedResults([]);
19
  } else if (props.query.trim() !== "" && props.searchButtonPressed) {
20
+ // if results are empty or not an array
21
+ if (!Array.isArray(props.results) || props.results.length === 0) {
22
  setSortedResults([]);
23
+ } else {
 
24
  // Sort results by similarity score
25
  const sorted = props.results.slice().sort((a, b) => b.similarity_score - a.similarity_score);
26
  // Update sortedResults state
frontend/app/components/ui/search/useSearch.tsx CHANGED
@@ -7,7 +7,7 @@ import { useSession } from 'next-auth/react';
7
  interface UseSearchResult {
8
  searchResults: SearchResult[];
9
  isLoading: boolean;
10
- handleSearch: (query: string) => Promise<void>;
11
  }
12
 
13
  const search_api = process.env.NEXT_PUBLIC_SEARCH_API;
@@ -20,7 +20,7 @@ const useSearch = (): UseSearchResult => {
20
  // console.log('session:', session, 'status:', status);
21
  const supabaseAccessToken = session?.supabaseAccessToken;
22
 
23
- const handleSearch = async (query: string): Promise<void> => {
24
  setIsSearchButtonPressed(isSearchButtonPressed);
25
  setIsLoading(true);
26
 
@@ -40,7 +40,7 @@ const useSearch = (): UseSearchResult => {
40
  setIsLoading(false);
41
  return;
42
  }
43
- const response = await fetch(`${search_api}?query=${query}`, {
44
  signal: AbortSignal.timeout(120000), // Abort the request if it takes longer than 120 seconds
45
  // Add the access token to the request headers
46
  headers: {
 
7
  interface UseSearchResult {
8
  searchResults: SearchResult[];
9
  isLoading: boolean;
10
+ handleSearch: (query: string, docSelected: string) => Promise<void>;
11
  }
12
 
13
  const search_api = process.env.NEXT_PUBLIC_SEARCH_API;
 
20
  // console.log('session:', session, 'status:', status);
21
  const supabaseAccessToken = session?.supabaseAccessToken;
22
 
23
+ const handleSearch = async (query: string, docSelected: string): Promise<void> => {
24
  setIsSearchButtonPressed(isSearchButtonPressed);
25
  setIsLoading(true);
26
 
 
40
  setIsLoading(false);
41
  return;
42
  }
43
+ const response = await fetch(`${search_api}?query=${query}&docSelected=${docSelected}`, {
44
  signal: AbortSignal.timeout(120000), // Abort the request if it takes longer than 120 seconds
45
  // Add the access token to the request headers
46
  headers: {
frontend/auth.ts CHANGED
@@ -124,11 +124,12 @@ export const config = {
124
  token.accessToken = account.access_token
125
  token.id = profile?.sub
126
  }
127
- return token
128
  },
129
  async session({ session, token, user }) {
130
  // Send properties to the client, like an access_token from a provider.
131
  const signingSecret = process.env.SUPABASE_JWT_SECRET
 
132
  if (signingSecret) {
133
  const payload = {
134
  aud: "authenticated",
@@ -137,11 +138,12 @@ export const config = {
137
  // email: user.email,
138
  role: "authenticated",
139
  }
140
- session.supabaseAccessToken = jwt.sign(payload, signingSecret)
 
141
  // session.jwt = token.jwt as string;
142
  // session.id = token.id as string;
143
  }
144
- return session
145
  },
146
 
147
  }
 
124
  token.accessToken = account.access_token
125
  token.id = profile?.sub
126
  }
127
+ return token;
128
  },
129
  async session({ session, token, user }) {
130
  // Send properties to the client, like an access_token from a provider.
131
  const signingSecret = process.env.SUPABASE_JWT_SECRET
132
+ // console.log('Signing Secret:', signingSecret);
133
  if (signingSecret) {
134
  const payload = {
135
  aud: "authenticated",
 
138
  // email: user.email,
139
  role: "authenticated",
140
  }
141
+ session.supabaseAccessToken = jwt.sign(payload, signingSecret) as string;
142
+ // console.log('New Session:', session);
143
  // session.jwt = token.jwt as string;
144
  // session.id = token.id as string;
145
  }
146
+ return session;
147
  },
148
 
149
  }
frontend/middleware.ts CHANGED
@@ -8,7 +8,7 @@ export const middleware = async (request: NextRequest) => {
8
  // Add callbackUrl params to the signinPage URL
9
  signinPage.searchParams.set('callbackUrl', pathname);
10
  // Retrieve the session token from the request cookies
11
- const session = request.cookies.get('next-auth.session-token');
12
 
13
  if (session) {
14
  // console.log('session:', session);
 
8
  // Add callbackUrl params to the signinPage URL
9
  signinPage.searchParams.set('callbackUrl', pathname);
10
  // Retrieve the session token from the request cookies
11
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
12
 
13
  if (session) {
14
  // console.log('session:', session);