Skip to content

Commit 786a0a4

Browse files
authored
Merge pull request #142 from AET-DevOps25/feature/improve-rag-structure
Improve rag structure and add auth service
2 parents c95f938 + 1737d9b commit 786a0a4

File tree

14 files changed

+529
-29
lines changed

14 files changed

+529
-29
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,12 @@ The LLM service will be available at [http://localhost:8000](http://localhost:80
295295

296296
##### Integration
297297

298-
- The client UI sends user requests to the server, which forwards them to the GenAI service along with the user’s query and chat history to support multi-turn conversations. GenAI service then makes a similarity search in the vector database with the given query, and generates a respective answer. GenAI service is able to provide a proper answer altough no similar context is found in the vector database. (Endpoint: POST - `genai/generate`)
299-
- If the user wants to upload a recipe file, client UI sends the file content directly to the GenAI service, where the content of the file is chunked, embedded, and stored in the vector database. (Endpoint: POST - `genai/upload`)
298+
- The client UI sends user requests to the API gateway, which forwards them to the chat service. Chat service forwards them to the GenAI service along with the user’s query and chat history to support multi-turn conversations. GenAI service then makes a similarity search in the vector database with the given query, and generates a respective answer. GenAI service is able to provide a proper answer altough no similar context is found in the vector database. (Endpoint: POST - `genai/generate`)
299+
- If the user wants to upload a recipe file, client UI sends the file content directly to the API gateway, which forwards to the GenAI service, where the content of the file is chunked, embedded, and stored in the vector database. (Endpoint: POST - `genai/upload`)
300+
- For using/testing the upload functionality, you can find some recipe PDFs to test the upload under `recipe_pdfs` folder. If you want, you can also modify the content of the script to generate your own recipe PDFs which can be found under `recipe_pdfs/scripts` folder. You can run the script from the root folder like:
301+
- ```bash
302+
python recipe_pdfs/scripts/basic_recipes.py
303+
```
300304

301305
##### Vector Database - Qdrant
302306

genai/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ python-multipart==0.0.20
3131
# Testing
3232
pytest==8.4.1
3333
fpdf==1.7.2
34-
pypdf==5.6.0
34+
pypdf==5.6.0
35+
36+
# Authentication
37+
python-jose[cryptography]==3.3.0

genai/routes/routes.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from fastapi import APIRouter, UploadFile, File, HTTPException, Request
1+
from fastapi import (
2+
APIRouter,
3+
UploadFile,
4+
File,
5+
HTTPException,
6+
Request,
7+
Depends
8+
)
29
from fastapi.responses import JSONResponse
310
import os
411

@@ -15,6 +22,7 @@
1522
prepare_prompt,
1623
process_raw_messages
1724
)
25+
from service.auth_service import get_current_user, UserInfo
1826
from metrics import (
1927
file_upload_request_counter,
2028
file_upload_successfully_counter,
@@ -68,12 +76,16 @@
6876

6977

7078
@router.post("/upload")
71-
async def upload_file(file: UploadFile = File(...)):
79+
async def upload_file(
80+
file: UploadFile = File(...),
81+
current_user: UserInfo = Depends(get_current_user)
82+
):
7283
file_upload_request_counter.inc()
7384
start_time = perf_counter()
7485
logger.info(
75-
"Upload endpoint is called in genai for the file %s",
76-
file.filename
86+
"Upload endpoint is called in genai for the file %s by user %s",
87+
file.filename,
88+
current_user.username
7789
)
7890

7991
if not file.filename.endswith(".pdf"):
@@ -89,7 +101,7 @@ async def upload_file(file: UploadFile = File(...)):
89101
with open(file_path, "wb") as buffer:
90102
buffer.write(await file.read())
91103

92-
collection_name = "recipes"
104+
collection_name = f"recipes_{current_user.user_id}"
93105
if (
94106
qdrant.client.collection_exists(collection_name)
95107
and qdrant.collection_contains_file(
@@ -98,7 +110,10 @@ async def upload_file(file: UploadFile = File(...)):
98110
filename
99111
)
100112
):
101-
logger.info("File already exists in qdrant")
113+
logger.info(
114+
"File already exists in qdrant for user %s",
115+
current_user.username
116+
)
102117
return {"message": f"File '{filename}' already uploaded."}
103118

104119
vector_store = qdrant.create_and_get_vector_storage(collection_name)
@@ -130,16 +145,21 @@ async def generate(request: Request):
130145
logger.info("Generate endpoint is called in genai")
131146

132147
body = await request.json()
133-
if "query" not in body or "messages" not in body:
134-
logger.error("Missing 'query' or 'messages' in the request body")
148+
if "query" not in body or "messages" not in body or "user_id" not in body:
149+
logger.error(
150+
"Missing 'query', 'messages', or 'user_id' in the request body"
151+
)
135152
raise HTTPException(
136153
status_code=400,
137-
detail="Missing 'query' or 'messages'"
154+
detail="Missing 'query', 'messages', or 'user_id'"
138155
)
139156

140157
query = body["query"]
141158
messages_raw = body["messages"]
142-
collection_name = "recipes"
159+
user_id = body["user_id"]
160+
collection_name = f"recipes_{user_id}"
161+
162+
logger.info("Generate endpoint called for user_id: %s", user_id)
143163

144164
try:
145165
retrieved_docs = ""
@@ -148,8 +168,9 @@ async def generate(request: Request):
148168
collection_name
149169
)
150170
logger.info(
151-
"Vector store is created for the collection %s",
152-
collection_name
171+
"Vector store is created for the collection %s for user_id %s",
172+
collection_name,
173+
user_id
153174
)
154175
retrieved_docs = retrieve_similar_docs(vector_store, query)
155176
logger.info("Similar docs retrieved from the vector store")

genai/service/auth_service.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import requests
2+
from fastapi import HTTPException, Header
3+
from typing import Optional
4+
from logger import logger
5+
6+
7+
class UserInfo:
8+
def __init__(self, user_id: int, username: str):
9+
self.user_id = user_id
10+
self.username = username
11+
12+
13+
async def get_current_user(
14+
authorization: Optional[str] = Header(None)
15+
) -> UserInfo:
16+
"""
17+
Extract user information from the Authorization header.
18+
19+
This function validates the OAuth token by calling the user service
20+
and returns the user information including user_id.
21+
"""
22+
if not authorization:
23+
logger.error("Authorization header is missing")
24+
raise HTTPException(
25+
status_code=401,
26+
detail="Authorization header required"
27+
)
28+
29+
if not authorization.startswith("Bearer "):
30+
logger.error("Invalid authorization header format")
31+
raise HTTPException(
32+
status_code=401,
33+
detail="Invalid authorization header format"
34+
)
35+
36+
token = authorization.split(" ")[1]
37+
38+
# use dev and prod URLs for user service, where prod is the fallback
39+
user_service_urls = [
40+
"http://user-service:8081/user/info",
41+
"http://localhost:8081/user/info",
42+
]
43+
44+
for url in user_service_urls:
45+
try:
46+
response = requests.get(
47+
url,
48+
headers={"Authorization": f"Bearer {token}"},
49+
timeout=10
50+
)
51+
if response.status_code == 200:
52+
user_data = response.json()
53+
logger.info(
54+
f"User authenticated: {user_data.get('username')}"
55+
)
56+
return UserInfo(
57+
user_id=user_data.get("id"),
58+
username=user_data.get("username")
59+
)
60+
else:
61+
logger.error(f"{url} returned status {response.status_code}")
62+
except requests.exceptions.RequestException as e:
63+
logger.warning(f"Failed to reach {url}: {e}")
64+
65+
raise HTTPException(
66+
status_code=500,
67+
detail="Authentication service unavailable"
68+
)

genai/tests/integration/generation_test.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ def test_generate_endpoint_success(_mock_exists, mock_invoke):
1717
"messages": [
1818
{"role": "USER", "content": "I have rice and eggs"},
1919
{"role": "ASSISTANT", "content": "How about fried rice?"}
20-
]
20+
],
21+
"user_id": 123
2122
}
2223

2324
response = client.post("/genai/generate", json=payload)
@@ -39,7 +40,8 @@ def test_generate_endpoint_empty_messages(_mock_exists, mock_invoke):
3940

4041
payload = {
4142
"query": "Can I cook with lentils?",
42-
"messages": []
43+
"messages": [],
44+
"user_id": 123
4345
}
4446

4547
response = client.post("/genai/generate", json=payload)
@@ -59,4 +61,6 @@ def test_generate_endpoint_missing_fields():
5961
response = client.post("/genai/generate", json=payload)
6062

6163
assert response.status_code == 400
62-
assert response.json() == {"detail": "Missing 'query' or 'messages'"}
64+
assert response.json() == {
65+
"detail": "Missing 'query', 'messages', or 'user_id'"
66+
}

genai/tests/integration/upload_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest.mock import patch, MagicMock
33
from fastapi.testclient import TestClient
44
from main import app
5+
from service.auth_service import UserInfo, get_current_user
56

67
client = TestClient(app)
78

@@ -14,6 +15,10 @@ def test_upload_file_success(
1415
_mock_vector_store,
1516
_mock_exists
1617
):
18+
# Mock the authentication by overriding the dependency
19+
mock_user = UserInfo(user_id=123, username="test_user")
20+
app.dependency_overrides[get_current_user] = lambda: mock_user
21+
1722
mock_pipeline = MagicMock()
1823
mock_pipeline_class.return_value = mock_pipeline
1924

@@ -32,8 +37,15 @@ def test_upload_file_success(
3237
mock_pipeline_class.assert_called_once()
3338
mock_pipeline.ingest.assert_called_once()
3439

40+
# Clean up the dependency override
41+
app.dependency_overrides.clear()
42+
3543

3644
def test_upload_file_invalid_type():
45+
# Mock the authentication by overriding the dependency
46+
mock_user = UserInfo(user_id=123, username="test_user")
47+
app.dependency_overrides[get_current_user] = lambda: mock_user
48+
3749
file = io.BytesIO(b"just some text")
3850
file.name = "notes.txt"
3951

@@ -46,10 +58,17 @@ def test_upload_file_invalid_type():
4658
assert (response.json()["detail"] ==
4759
"Invalid file type. Only PDF files are allowed.")
4860

61+
# Clean up the dependency override
62+
app.dependency_overrides.clear()
63+
4964

5065
@patch("routes.routes.qdrant.client.collection_exists", return_value=True)
5166
@patch("routes.routes.qdrant.collection_contains_file", return_value=True)
5267
def test_upload_file_already_exists(_mock_contains, _mock_exists):
68+
# Mock the authentication
69+
mock_user = UserInfo(user_id=123, username="test_user")
70+
app.dependency_overrides[get_current_user] = lambda: mock_user
71+
5372
file = io.BytesIO(b"%PDF-1.4")
5473
file.name = "existing.pdf"
5574

@@ -60,3 +79,6 @@ def test_upload_file_already_exists(_mock_contains, _mock_exists):
6079

6180
assert response.status_code == 200
6281
assert "already uploaded" in response.json()["message"]
82+
83+
# Clean up the dependency override
84+
app.dependency_overrides.clear()

recipe_pdfs/basic_recipes.pdf

10.5 KB
Binary file not shown.

recipe_pdfs/basic_recipes2.pdf

10.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)