From df5fefd2cbcf2317cf23030d70844dd27e987344 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Tue, 6 May 2025 13:39:56 +0530 Subject: [PATCH 01/20] endpoints and test cases --- .../9baa692f9a5d_add_threads_table.py | 33 +++++ backend/app/api/routes/threads.py | 90 ++++++++++++++ backend/app/crud/__init__.py | 2 + backend/app/crud/thread_results.py | 21 ++++ backend/app/models/__init__.py | 2 + backend/app/models/threads.py | 11 ++ backend/app/tests/api/routes/test_threads.py | 117 +++++++++++++++++- backend/app/tests/crud/test_thread_result.py | 38 ++++++ 8 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py create mode 100644 backend/app/crud/thread_results.py create mode 100644 backend/app/models/threads.py create mode 100644 backend/app/tests/crud/test_thread_result.py diff --git a/backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py b/backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py new file mode 100644 index 00000000..489d2a94 --- /dev/null +++ b/backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py @@ -0,0 +1,33 @@ +"""add threads table + +Revision ID: 9baa692f9a5d +Revises: 543f97951bd0 +Create Date: 2025-05-05 23:25:37.195415 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = "9baa692f9a5d" +down_revision = "543f97951bd0" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "threadresponse", + sa.Column("thread_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("message", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("question", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("thread_id"), + ) + + +def downgrade(): + op.drop_table("threadresponse") diff --git a/backend/app/api/routes/threads.py b/backend/app/api/routes/threads.py index b310c026..c8e26fb6 100644 --- a/backend/app/api/routes/threads.py +++ b/backend/app/api/routes/threads.py @@ -9,6 +9,7 @@ from app.api.deps import get_current_user_org, get_db from app.core import logging, settings from app.models import UserOrganization +from app.crud import upsert_thread_result, get_thread_result from app.utils import APIResponse logger = logging.getLogger(__name__) @@ -137,6 +138,32 @@ def process_run(request: dict, client: OpenAI): send_callback(request["callback_url"], callback_response.model_dump()) +def poll_run_and_prepare_response(request: dict, client: OpenAI, db: Session): + "process a run and send result to DB" + thread_id = request["thread_id"] + question = request["question"] + + try: + run = client.beta.threads.runs.create_and_poll( + thread_id=thread_id, + assistant_id=request["assistant_id"], + ) + + if run.status == "completed": + messages = client.beta.threads.messages.list(thread_id=thread_id) + latest_message = messages.data[0] + message_content = latest_message.content[0].text.value + processed_message = process_message_content( + message_content, request.get("remove_citation", False) + ) + upsert_thread_result(db, thread_id, question, processed_message) + else: + upsert_thread_result(db, thread_id, question, None) + + except openai.OpenAIError: + upsert_thread_result(db, thread_id, question, None) + + @router.post("/threads") async def threads( request: dict, @@ -214,3 +241,66 @@ async def threads_sync( except openai.OpenAIError as e: return APIResponse.failure_response(error=handle_openai_error(e)) + + +@router.post("/threads/start") +async def start_thread( + request: dict, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + _current_user: UserOrganization = Depends(get_current_user_org), +): + """ + Create a new OpenAI thread for the given question and start polling in the background. + + - If successful, returns the thread ID and a 'processing' status. + - Stores the thread and question in the database. + """ + + question = request["question"] + + client = OpenAI(api_key=settings.OPENAI_API_KEY) + + is_success, error = setup_thread(client, request) + if not is_success: + return APIResponse.failure_response(error=error) + + thread_id = request["thread_id"] + upsert_thread_result(db, thread_id, question, None) + + background_tasks.add_task(poll_run_and_prepare_response, request, client, db) + + return APIResponse.success_response( + data={ + "thread_id": thread_id, + "question": question, + "status": "processing", + "message": "Thread created and polling started in background.", + } + ) + + +@router.get("/threads/result/{thread_id}") +async def get_thread_result_by_id( + thread_id: str, + db: Session = Depends(get_db), + _current_user: UserOrganization = Depends(get_current_user_org), +): + """ + Retrieve the result of a previously started OpenAI thread using its thread ID. + """ + result = get_thread_result(db, thread_id) + + if not result: + return APIResponse.failure_response(error="Thread not found.") + + status = "success" if result.message else "processing" + + return APIResponse.success_response( + data={ + "thread_id": result.thread_id, + "question": result.question, + "status": status, + "message": result.message, + } + ) diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index c19c098a..9776342c 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -24,3 +24,5 @@ get_api_keys_by_organization, delete_api_key, ) + +from .thread_results import upsert_thread_result, get_thread_result diff --git a/backend/app/crud/thread_results.py b/backend/app/crud/thread_results.py new file mode 100644 index 00000000..78496f56 --- /dev/null +++ b/backend/app/crud/thread_results.py @@ -0,0 +1,21 @@ +from sqlmodel import Session +from datetime import datetime +from app.models import ThreadResponse + + +def upsert_thread_result( + session: Session, thread_id: str, question: str, message: str | None +): + session.merge( + ThreadResponse( + thread_id=thread_id, + question=question, + message=message, + updated_at=datetime.utcnow(), + ) + ) + session.commit() + + +def get_thread_result(session: Session, thread_id: str) -> ThreadResponse | None: + return session.get(ThreadResponse, thread_id) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index eaa9507c..5af968d3 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -50,3 +50,5 @@ CredsPublic, CredsUpdate, ) + +from .threads import ThreadResponse diff --git a/backend/app/models/threads.py b/backend/app/models/threads.py new file mode 100644 index 00000000..1214c6fe --- /dev/null +++ b/backend/app/models/threads.py @@ -0,0 +1,11 @@ +from sqlmodel import SQLModel, Field +from typing import Optional +from datetime import datetime + + +class ThreadResponse(SQLModel, table=True): + thread_id: str = Field(primary_key=True) + message: Optional[str] + question: str + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 8a652a9a..359d1c5e 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock, patch -import pytest +import pytest, uuid from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import select @@ -12,8 +12,9 @@ setup_thread, process_message_content, handle_openai_error, + poll_run_and_prepare_response, ) -from app.models import APIKey +from app.models import APIKey, ThreadResponse import openai # Wrap the router in a FastAPI app instance. @@ -386,3 +387,115 @@ def test_handle_openai_error_with_none_body(): error.__str__.return_value = "None body error" result = handle_openai_error(error) assert result == "None body error" + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_completed(mock_openai, db): + mock_client = MagicMock() + mock_run = MagicMock() + mock_run.status = "completed" + mock_client.beta.threads.runs.create_and_poll.return_value = mock_run + + mock_message = MagicMock() + mock_message.content = [MagicMock(text=MagicMock(value="Answer "))] + mock_client.beta.threads.messages.list.return_value.data = [mock_message] + mock_openai.return_value = mock_client + + request = { + "question": "What is Glific?", + "assistant_id": "assist_123", + "thread_id": "test_thread_001", + "remove_citation": True, + } + + poll_run_and_prepare_response(request, mock_client, db) + + result = db.get(ThreadResponse, "test_thread_001") + assert result.message.strip() == "Answer" + + +@patch("app.api.routes.threads.OpenAI") +def test_threads_start_endpoint_creates_thread(mock_openai, db): + """Test /threads/start creates thread and schedules background task.""" + mock_client = MagicMock() + mock_thread = MagicMock() + mock_thread.id = "mock_thread_001" + mock_client.beta.threads.create.return_value = mock_thread + mock_client.beta.threads.messages.create.return_value = None + mock_openai.return_value = mock_client + + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: + pytest.skip("No API key found in the database for testing") + + headers = {"X-API-KEY": api_key_record.key} + data = {"question": "What's 2+2?", "assistant_id": "assist_123"} + + response = client.post("/threads/start", json=data, headers=headers) + assert response.status_code == 200 + res_json = response.json() + assert res_json["success"] + assert res_json["data"]["thread_id"] == "mock_thread_001" + assert res_json["data"]["status"] == "processing" + assert res_json["data"]["question"] == "What's 2+2?" + + +def test_threads_result_endpoint_success(db): + """Test /threads/result/{thread_id} returns completed thread.""" + thread_id = f"test_processing_{uuid.uuid4()}" + question = "Capital of France?" + message = "Paris." + + db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) + db.commit() + + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: + pytest.skip("No API key found in the database for testing") + + headers = {"X-API-KEY": api_key_record.key} + response = client.get(f"/threads/result/{thread_id}", headers=headers) + + assert response.status_code == 200 + data = response.json()["data"] + assert data["status"] == "success" + assert data["message"] == "Paris." + assert data["thread_id"] == thread_id + assert data["question"] == question + + +def test_threads_result_endpoint_processing(db): + """Test /threads/result/{thread_id} returns processing status if no message yet.""" + thread_id = f"test_processing_{uuid.uuid4()}" + question = "What is Glific?" + + db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) + db.commit() + + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: + pytest.skip("No API key found in the database for testing") + + headers = {"X-API-KEY": api_key_record.key} + response = client.get(f"/threads/result/{thread_id}", headers=headers) + + assert response.status_code == 200 + data = response.json()["data"] + assert data["status"] == "processing" + assert data["message"] is None + assert data["thread_id"] == thread_id + assert data["question"] == question + + +def test_threads_result_not_found(db): + """Test /threads/result/{thread_id} returns error for nonexistent thread.""" + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: + pytest.skip("No API key found in the database for testing") + + headers = {"X-API-KEY": api_key_record.key} + response = client.get("/threads/result/nonexistent_thread", headers=headers) + + assert response.status_code == 200 + assert response.json()["success"] is False + assert "not found" in response.json()["error"].lower() diff --git a/backend/app/tests/crud/test_thread_result.py b/backend/app/tests/crud/test_thread_result.py new file mode 100644 index 00000000..d0a0cff8 --- /dev/null +++ b/backend/app/tests/crud/test_thread_result.py @@ -0,0 +1,38 @@ +import pytest +from sqlmodel import SQLModel, Session, create_engine + +from app.models import ThreadResponse +from app.crud import upsert_thread_result, get_thread_result + + +@pytest.fixture +def session(): + """Creates a new in-memory database session for each test.""" + engine = create_engine("sqlite://", echo=False) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +def test_upsert_and_get_thread_result(session: Session): + thread_id = "thread_test_123" + question = "What is the capital of Spain?" + message = "Madrid is the capital of Spain." + + # Initially insert + upsert_thread_result(session, thread_id, question, message) + + # Retrieve + result = get_thread_result(session, thread_id) + + assert result is not None + assert result.thread_id == thread_id + assert result.question == question + assert result.message == message + + # Update with new message + updated_message = "Madrid." + upsert_thread_result(session, thread_id, question, updated_message) + + result_updated = get_thread_result(session, thread_id) + assert result_updated.message == updated_message From b5a3a6ce01badd1a9f5bb34aaca2079d4df27003 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 01:47:21 +0530 Subject: [PATCH 02/20] test cases --- backend/app/tests/api/routes/test_threads.py | 91 +++++++++++++++----- 1 file changed, 70 insertions(+), 21 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 359d1c5e..b44266f3 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -4,6 +4,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import select +from openai import OpenAIError from app.api.routes.threads import ( process_run, @@ -392,8 +393,7 @@ def test_handle_openai_error_with_none_body(): @patch("app.api.routes.threads.OpenAI") def test_poll_run_and_prepare_response_completed(mock_openai, db): mock_client = MagicMock() - mock_run = MagicMock() - mock_run.status = "completed" + mock_run = MagicMock(status="completed") mock_client.beta.threads.runs.create_and_poll.return_value = mock_run mock_message = MagicMock() @@ -409,14 +409,48 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): } poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_thread_001") assert result.message.strip() == "Answer" +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): + mock_client = MagicMock() + mock_error = OpenAIError("Simulated OpenAI error") + mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error + mock_openai.return_value = mock_client + + request = { + "question": "Failing run", + "assistant_id": "assist_123", + "thread_id": "test_openai_error", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_openai_error") + assert result.message is None + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_non_completed(mock_openai, db): + mock_client = MagicMock() + mock_run = MagicMock(status="failed") + mock_client.beta.threads.runs.create_and_poll.return_value = mock_run + mock_openai.return_value = mock_client + + request = { + "question": "Incomplete run", + "assistant_id": "assist_123", + "thread_id": "test_non_complete", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_non_complete") + assert result.message is None + + @patch("app.api.routes.threads.OpenAI") def test_threads_start_endpoint_creates_thread(mock_openai, db): - """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() mock_thread = MagicMock() mock_thread.id = "mock_thread_001" @@ -424,13 +458,12 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} - headers = {"X-API-KEY": api_key_record.key} data = {"question": "What's 2+2?", "assistant_id": "assist_123"} - response = client.post("/threads/start", json=data, headers=headers) assert response.status_code == 200 res_json = response.json() @@ -441,19 +474,18 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): def test_threads_result_endpoint_success(db): - """Test /threads/result/{thread_id} returns completed thread.""" - thread_id = f"test_processing_{uuid.uuid4()}" + thread_id = f"test_result_success_{uuid.uuid4()}" question = "Capital of France?" message = "Paris." db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) db.commit() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} - headers = {"X-API-KEY": api_key_record.key} response = client.get(f"/threads/result/{thread_id}", headers=headers) assert response.status_code == 200 @@ -465,18 +497,17 @@ def test_threads_result_endpoint_success(db): def test_threads_result_endpoint_processing(db): - """Test /threads/result/{thread_id} returns processing status if no message yet.""" thread_id = f"test_processing_{uuid.uuid4()}" question = "What is Glific?" db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) db.commit() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} - headers = {"X-API-KEY": api_key_record.key} response = client.get(f"/threads/result/{thread_id}", headers=headers) assert response.status_code == 200 @@ -488,14 +519,32 @@ def test_threads_result_endpoint_processing(db): def test_threads_result_not_found(db): - """Test /threads/result/{thread_id} returns error for nonexistent thread.""" - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} - headers = {"X-API-KEY": api_key_record.key} response = client.get("/threads/result/nonexistent_thread", headers=headers) assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() + + +@patch("app.api.routes.threads.setup_thread") +@patch("app.api.routes.threads.OpenAI") +def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): + mock_setup_thread.return_value = (False, "Assistant not found") + mock_openai.return_value = MagicMock() + + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: + pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} + + data = {"question": "Test fail", "assistant_id": "bad_assist"} + response = client.post("/threads/start", json=data, headers=headers) + body = response.json() + assert response.status_code == 200 + assert body["success"] is False + assert "Assistant not found" in body["error"] From 1c929df0f72a7ab8170187886063790daa3bd1c9 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 01:54:56 +0530 Subject: [PATCH 03/20] test cases --- backend/app/tests/api/routes/test_threads.py | 34 +++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index b44266f3..88d1c5d6 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -458,11 +458,11 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + headers = {"X-API-KEY": api_key_record.key} data = {"question": "What's 2+2?", "assistant_id": "assist_123"} response = client.post("/threads/start", json=data, headers=headers) assert response.status_code == 200 @@ -481,10 +481,11 @@ def test_threads_result_endpoint_success(db): db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) db.commit() - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + + headers = {"X-API-KEY": api_key_record.key} response = client.get(f"/threads/result/{thread_id}", headers=headers) @@ -503,10 +504,11 @@ def test_threads_result_endpoint_processing(db): db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) db.commit() - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + + headers = {"X-API-KEY": api_key_record.key} response = client.get(f"/threads/result/{thread_id}", headers=headers) @@ -519,10 +521,11 @@ def test_threads_result_endpoint_processing(db): def test_threads_result_not_found(db): - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + + headers = {"X-API-KEY": api_key_record.key} response = client.get("/threads/result/nonexistent_thread", headers=headers) @@ -537,10 +540,11 @@ def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): mock_setup_thread.return_value = (False, "Assistant not found") mock_openai.return_value = MagicMock() - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + + headers = {"X-API-KEY": api_key_record.key} data = {"question": "Test fail", "assistant_id": "bad_assist"} response = client.post("/threads/start", json=data, headers=headers) From 58b0f472e12c65ec8de66fd745f69dd7ca56e527 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 09:52:59 +0530 Subject: [PATCH 04/20] initial test cases --- backend/app/tests/api/routes/test_threads.py | 170 +------------------ 1 file changed, 2 insertions(+), 168 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 88d1c5d6..8a652a9a 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -1,10 +1,9 @@ from unittest.mock import MagicMock, patch -import pytest, uuid +import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import select -from openai import OpenAIError from app.api.routes.threads import ( process_run, @@ -13,9 +12,8 @@ setup_thread, process_message_content, handle_openai_error, - poll_run_and_prepare_response, ) -from app.models import APIKey, ThreadResponse +from app.models import APIKey import openai # Wrap the router in a FastAPI app instance. @@ -388,167 +386,3 @@ def test_handle_openai_error_with_none_body(): error.__str__.return_value = "None body error" result = handle_openai_error(error) assert result == "None body error" - - -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_completed(mock_openai, db): - mock_client = MagicMock() - mock_run = MagicMock(status="completed") - mock_client.beta.threads.runs.create_and_poll.return_value = mock_run - - mock_message = MagicMock() - mock_message.content = [MagicMock(text=MagicMock(value="Answer "))] - mock_client.beta.threads.messages.list.return_value.data = [mock_message] - mock_openai.return_value = mock_client - - request = { - "question": "What is Glific?", - "assistant_id": "assist_123", - "thread_id": "test_thread_001", - "remove_citation": True, - } - - poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_thread_001") - assert result.message.strip() == "Answer" - - -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): - mock_client = MagicMock() - mock_error = OpenAIError("Simulated OpenAI error") - mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error - mock_openai.return_value = mock_client - - request = { - "question": "Failing run", - "assistant_id": "assist_123", - "thread_id": "test_openai_error", - } - - poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_openai_error") - assert result.message is None - - -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_non_completed(mock_openai, db): - mock_client = MagicMock() - mock_run = MagicMock(status="failed") - mock_client.beta.threads.runs.create_and_poll.return_value = mock_run - mock_openai.return_value = mock_client - - request = { - "question": "Incomplete run", - "assistant_id": "assist_123", - "thread_id": "test_non_complete", - } - - poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_non_complete") - assert result.message is None - - -@patch("app.api.routes.threads.OpenAI") -def test_threads_start_endpoint_creates_thread(mock_openai, db): - mock_client = MagicMock() - mock_thread = MagicMock() - mock_thread.id = "mock_thread_001" - mock_client.beta.threads.create.return_value = mock_thread - mock_client.beta.threads.messages.create.return_value = None - mock_openai.return_value = mock_client - - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - data = {"question": "What's 2+2?", "assistant_id": "assist_123"} - response = client.post("/threads/start", json=data, headers=headers) - assert response.status_code == 200 - res_json = response.json() - assert res_json["success"] - assert res_json["data"]["thread_id"] == "mock_thread_001" - assert res_json["data"]["status"] == "processing" - assert res_json["data"]["question"] == "What's 2+2?" - - -def test_threads_result_endpoint_success(db): - thread_id = f"test_result_success_{uuid.uuid4()}" - question = "Capital of France?" - message = "Paris." - - db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) - db.commit() - - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - - response = client.get(f"/threads/result/{thread_id}", headers=headers) - - assert response.status_code == 200 - data = response.json()["data"] - assert data["status"] == "success" - assert data["message"] == "Paris." - assert data["thread_id"] == thread_id - assert data["question"] == question - - -def test_threads_result_endpoint_processing(db): - thread_id = f"test_processing_{uuid.uuid4()}" - question = "What is Glific?" - - db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) - db.commit() - - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - - response = client.get(f"/threads/result/{thread_id}", headers=headers) - - assert response.status_code == 200 - data = response.json()["data"] - assert data["status"] == "processing" - assert data["message"] is None - assert data["thread_id"] == thread_id - assert data["question"] == question - - -def test_threads_result_not_found(db): - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - - response = client.get("/threads/result/nonexistent_thread", headers=headers) - - assert response.status_code == 200 - assert response.json()["success"] is False - assert "not found" in response.json()["error"].lower() - - -@patch("app.api.routes.threads.setup_thread") -@patch("app.api.routes.threads.OpenAI") -def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): - mock_setup_thread.return_value = (False, "Assistant not found") - mock_openai.return_value = MagicMock() - - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - - data = {"question": "Test fail", "assistant_id": "bad_assist"} - response = client.post("/threads/start", json=data, headers=headers) - body = response.json() - assert response.status_code == 200 - assert body["success"] is False - assert "Assistant not found" in body["error"] From 89540226a2d25466863f4b32767e8ff5f2ca549b Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 09:56:59 +0530 Subject: [PATCH 05/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 167 ++++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 8a652a9a..0a754781 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock, patch -import pytest +import pytest, uuid from fastapi import FastAPI from fastapi.testclient import TestClient from sqlmodel import select @@ -13,8 +13,11 @@ process_message_content, handle_openai_error, ) -from app.models import APIKey +from app.models import APIKey, ThreadResponse, APIKey import openai +from openai import OpenAIError +from app.api.routes.threads import poll_run_and_prepare_response +from app.crud import get_thread_result # Wrap the router in a FastAPI app instance. app = FastAPI() @@ -386,3 +389,163 @@ def test_handle_openai_error_with_none_body(): error.__str__.return_value = "None body error" result = handle_openai_error(error) assert result == "None body error" + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_completed(mock_openai, db): + mock_client = MagicMock() + mock_run = MagicMock(status="completed") + mock_client.beta.threads.runs.create_and_poll.return_value = mock_run + + mock_message = MagicMock() + mock_message.content = [MagicMock(text=MagicMock(value="Answer "))] + mock_client.beta.threads.messages.list.return_value.data = [mock_message] + mock_openai.return_value = mock_client + + request = { + "question": "What is Glific?", + "assistant_id": "assist_123", + "thread_id": "test_thread_001", + "remove_citation": True, + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_thread_001") + assert result.message.strip() == "Answer" + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): + mock_client = MagicMock() + mock_error = OpenAIError("Simulated OpenAI error") + mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error + mock_openai.return_value = mock_client + + request = { + "question": "Failing run", + "assistant_id": "assist_123", + "thread_id": "test_openai_error", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_openai_error") + assert result.message is None + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_non_completed(mock_openai, db): + mock_client = MagicMock() + mock_run = MagicMock(status="failed") + mock_client.beta.threads.runs.create_and_poll.return_value = mock_run + mock_openai.return_value = mock_client + + request = { + "question": "Incomplete run", + "assistant_id": "assist_123", + "thread_id": "test_non_complete", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_non_complete") + assert result.message is None + + +@patch("app.api.routes.threads.OpenAI") +def test_threads_start_endpoint_creates_thread(mock_openai, db): + mock_client = MagicMock() + mock_thread = MagicMock() + mock_thread.id = "mock_thread_001" + mock_client.beta.threads.create.return_value = mock_thread + mock_client.beta.threads.messages.create.return_value = None + mock_openai.return_value = mock_client + + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: + pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} + + data = {"question": "What's 2+2?", "assistant_id": "assist_123"} + response = client.post("/threads/start", json=data, headers=headers) + assert response.status_code == 200 + res_json = response.json() + assert res_json["success"] + assert res_json["data"]["thread_id"] == "mock_thread_001" + assert res_json["data"]["status"] == "processing" + assert res_json["data"]["question"] == "What's 2+2?" + + +def test_threads_result_endpoint_success(db): + thread_id = f"test_result_success_{uuid.uuid4()}" + question = "Capital of France?" + message = "Paris." + + db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) + db.commit() + + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: + pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} + + response = client.get(f"/threads/result/{thread_id}", headers=headers) + + assert response.status_code == 200 + data = response.json()["data"] + assert data["status"] == "success" + assert data["message"] == "Paris." + assert data["thread_id"] == thread_id + assert data["question"] == question + + +def test_threads_result_endpoint_processing(db): + thread_id = f"test_processing_{uuid.uuid4()}" + question = "What is Glific?" + + db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) + db.commit() + + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: + pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} + + response = client.get(f"/threads/result/{thread_id}", headers=headers) + + assert response.status_code == 200 + data = response.json()["data"] + assert data["status"] == "processing" + assert data["message"] is None + assert data["thread_id"] == thread_id + assert data["question"] == question + + +def test_threads_result_not_found(db): + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: + pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} + + response = client.get("/threads/result/nonexistent_thread", headers=headers) + + assert response.status_code == 200 + assert response.json()["success"] is False + assert "not found" in response.json()["error"].lower() + + +@patch("app.api.routes.threads.setup_thread") +@patch("app.api.routes.threads.OpenAI") +def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): + mock_setup_thread.return_value = (False, "Assistant not found") + mock_openai.return_value = MagicMock() + + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: + pytest.skip("No API key found in the database for testing") + headers = {"X-API-KEY": api_key.key} + + data = {"question": "Test fail", "assistant_id": "bad_assist"} + response = client.post("/threads/start", json=data, headers=headers) + body = response.json() + assert response.status_code == 200 + assert body["success"] is False + assert "Assistant not found" in body["error"] From 521dfe0857b6c1b320894ac134dbb94f79419cfb Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 11:28:00 +0530 Subject: [PATCH 06/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 0a754781..f8163e44 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -17,7 +17,6 @@ import openai from openai import OpenAIError from app.api.routes.threads import poll_run_and_prepare_response -from app.crud import get_thread_result # Wrap the router in a FastAPI app instance. app = FastAPI() @@ -459,10 +458,10 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + headers = {"X-API-KEY": api_key_record.key} data = {"question": "What's 2+2?", "assistant_id": "assist_123"} response = client.post("/threads/start", json=data, headers=headers) From 1d9013bb0a1661c848367e6e57d3aa0eaccd3fc2 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 11:54:10 +0530 Subject: [PATCH 07/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index f8163e44..440dc992 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -458,10 +458,10 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key_record: + api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() + if not api_key: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key_record.key} + headers = {"X-API-KEY": api_key.key} data = {"question": "What's 2+2?", "assistant_id": "assist_123"} response = client.post("/threads/start", json=data, headers=headers) From c4703e36a7ad84bca946a27363a6c6a67c59c9e9 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 12:00:10 +0530 Subject: [PATCH 08/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 95 +++++--------------- 1 file changed, 23 insertions(+), 72 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 440dc992..359d1c5e 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -12,11 +12,10 @@ setup_thread, process_message_content, handle_openai_error, + poll_run_and_prepare_response, ) -from app.models import APIKey, ThreadResponse, APIKey +from app.models import APIKey, ThreadResponse import openai -from openai import OpenAIError -from app.api.routes.threads import poll_run_and_prepare_response # Wrap the router in a FastAPI app instance. app = FastAPI() @@ -393,7 +392,8 @@ def test_handle_openai_error_with_none_body(): @patch("app.api.routes.threads.OpenAI") def test_poll_run_and_prepare_response_completed(mock_openai, db): mock_client = MagicMock() - mock_run = MagicMock(status="completed") + mock_run = MagicMock() + mock_run.status = "completed" mock_client.beta.threads.runs.create_and_poll.return_value = mock_run mock_message = MagicMock() @@ -409,48 +409,14 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): } poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_thread_001") assert result.message.strip() == "Answer" -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): - mock_client = MagicMock() - mock_error = OpenAIError("Simulated OpenAI error") - mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error - mock_openai.return_value = mock_client - - request = { - "question": "Failing run", - "assistant_id": "assist_123", - "thread_id": "test_openai_error", - } - - poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_openai_error") - assert result.message is None - - -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_non_completed(mock_openai, db): - mock_client = MagicMock() - mock_run = MagicMock(status="failed") - mock_client.beta.threads.runs.create_and_poll.return_value = mock_run - mock_openai.return_value = mock_client - - request = { - "question": "Incomplete run", - "assistant_id": "assist_123", - "thread_id": "test_non_complete", - } - - poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_non_complete") - assert result.message is None - - @patch("app.api.routes.threads.OpenAI") def test_threads_start_endpoint_creates_thread(mock_openai, db): + """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() mock_thread = MagicMock() mock_thread.id = "mock_thread_001" @@ -458,12 +424,13 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + headers = {"X-API-KEY": api_key_record.key} data = {"question": "What's 2+2?", "assistant_id": "assist_123"} + response = client.post("/threads/start", json=data, headers=headers) assert response.status_code == 200 res_json = response.json() @@ -474,18 +441,19 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): def test_threads_result_endpoint_success(db): - thread_id = f"test_result_success_{uuid.uuid4()}" + """Test /threads/result/{thread_id} returns completed thread.""" + thread_id = f"test_processing_{uuid.uuid4()}" question = "Capital of France?" message = "Paris." db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) db.commit() - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + headers = {"X-API-KEY": api_key_record.key} response = client.get(f"/threads/result/{thread_id}", headers=headers) assert response.status_code == 200 @@ -497,17 +465,18 @@ def test_threads_result_endpoint_success(db): def test_threads_result_endpoint_processing(db): + """Test /threads/result/{thread_id} returns processing status if no message yet.""" thread_id = f"test_processing_{uuid.uuid4()}" question = "What is Glific?" db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) db.commit() - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + headers = {"X-API-KEY": api_key_record.key} response = client.get(f"/threads/result/{thread_id}", headers=headers) assert response.status_code == 200 @@ -519,32 +488,14 @@ def test_threads_result_endpoint_processing(db): def test_threads_result_not_found(db): - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: + """Test /threads/result/{thread_id} returns error for nonexistent thread.""" + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} + headers = {"X-API-KEY": api_key_record.key} response = client.get("/threads/result/nonexistent_thread", headers=headers) assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() - - -@patch("app.api.routes.threads.setup_thread") -@patch("app.api.routes.threads.OpenAI") -def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): - mock_setup_thread.return_value = (False, "Assistant not found") - mock_openai.return_value = MagicMock() - - api_key = db.exec(select(APIKey).where(APIKey.is_deleted == False)).first() - if not api_key: - pytest.skip("No API key found in the database for testing") - headers = {"X-API-KEY": api_key.key} - - data = {"question": "Test fail", "assistant_id": "bad_assist"} - response = client.post("/threads/start", json=data, headers=headers) - body = response.json() - assert response.status_code == 200 - assert body["success"] is False - assert "Assistant not found" in body["error"] From 6bfe8bcf1a2256effe6544f8affca9cca2eefd15 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 12:05:19 +0530 Subject: [PATCH 09/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 359d1c5e..a8ace2ba 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -16,6 +16,7 @@ ) from app.models import APIKey, ThreadResponse import openai +from openai import OpenAIError # Wrap the router in a FastAPI app instance. app = FastAPI() @@ -499,3 +500,21 @@ def test_threads_result_not_found(db): assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): + mock_client = MagicMock() + mock_error = OpenAIError("Simulated OpenAI error") + mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error + mock_openai.return_value = mock_client + + request = { + "question": "Failing run", + "assistant_id": "assist_123", + "thread_id": "test_openai_error", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_openai_error") + assert result.message is None From 63234bcb28f8e4e3f36201fac795221475131e3a Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 12:09:03 +0530 Subject: [PATCH 10/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 54 +++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index a8ace2ba..1980c0ec 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -415,6 +415,42 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): assert result.message.strip() == "Answer" +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): + mock_client = MagicMock() + mock_error = OpenAIError("Simulated OpenAI error") + mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error + mock_openai.return_value = mock_client + + request = { + "question": "Failing run", + "assistant_id": "assist_123", + "thread_id": "test_openai_error", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_openai_error") + assert result.message is None + + +@patch("app.api.routes.threads.OpenAI") +def test_poll_run_and_prepare_response_non_completed(mock_openai, db): + mock_client = MagicMock() + mock_run = MagicMock(status="failed") + mock_client.beta.threads.runs.create_and_poll.return_value = mock_run + mock_openai.return_value = mock_client + + request = { + "question": "Incomplete run", + "assistant_id": "assist_123", + "thread_id": "test_non_complete", + } + + poll_run_and_prepare_response(request, mock_client, db) + result = db.get(ThreadResponse, "test_non_complete") + assert result.message is None + + @patch("app.api.routes.threads.OpenAI") def test_threads_start_endpoint_creates_thread(mock_openai, db): """Test /threads/start creates thread and schedules background task.""" @@ -500,21 +536,3 @@ def test_threads_result_not_found(db): assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() - - -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): - mock_client = MagicMock() - mock_error = OpenAIError("Simulated OpenAI error") - mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error - mock_openai.return_value = mock_client - - request = { - "question": "Failing run", - "assistant_id": "assist_123", - "thread_id": "test_openai_error", - } - - poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_openai_error") - assert result.message is None From 6b618689b6f97801bc4ae7c6fb9d5847a9354603 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 12:11:24 +0530 Subject: [PATCH 11/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 1980c0ec..b9d431c8 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -536,3 +536,23 @@ def test_threads_result_not_found(db): assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() + + +@patch("app.api.routes.threads.setup_thread") +@patch("app.api.routes.threads.OpenAI") +def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): + mock_setup_thread.return_value = (False, "Assistant not found") + mock_openai.return_value = MagicMock() + + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: + pytest.skip("No API key found in the database for testing") + + headers = {"X-API-KEY": api_key_record.key} + + data = {"question": "Test fail", "assistant_id": "bad_assist"} + response = client.post("/threads/start", json=data, headers=headers) + body = response.json() + assert response.status_code == 200 + assert body["success"] is False + assert "Assistant not found" in body["error"] From b72abf33578b2e5c2b7a875d344dffd49656724f Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 12:14:30 +0530 Subject: [PATCH 12/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index b9d431c8..1980c0ec 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -536,23 +536,3 @@ def test_threads_result_not_found(db): assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() - - -@patch("app.api.routes.threads.setup_thread") -@patch("app.api.routes.threads.OpenAI") -def test_start_thread_setup_fails(mock_openai, mock_setup_thread, db): - mock_setup_thread.return_value = (False, "Assistant not found") - mock_openai.return_value = MagicMock() - - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - - data = {"question": "Test fail", "assistant_id": "bad_assist"} - response = client.post("/threads/start", json=data, headers=headers) - body = response.json() - assert response.status_code == 200 - assert body["success"] is False - assert "Assistant not found" in body["error"] From d2ae0afb8ce46a5d500983afad7e28cc0720309e Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 9 May 2025 12:19:51 +0530 Subject: [PATCH 13/20] added test cases --- backend/app/tests/api/routes/test_threads.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 1980c0ec..a635c8d5 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -536,3 +536,23 @@ def test_threads_result_not_found(db): assert response.status_code == 200 assert response.json()["success"] is False assert "not found" in response.json()["error"].lower() + + +@patch("app.api.routes.threads.OpenAI") +def test_threads_start_missing_question(mock_openai, db): + """Test /threads/start with missing 'question' key in request.""" + mock_openai.return_value = MagicMock() + + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + if not api_key_record: + pytest.skip("No API key found in the database for testing") + + headers = {"X-API-KEY": api_key_record.key} + + bad_data = {"assistant_id": "assist_123"} # no "question" key + + response = client.post("/threads/start", json=bad_data, headers=headers) + + assert response.status_code == 422 # Unprocessable Entity (FastAPI will raise 422) + error_response = response.json() + assert "detail" in error_response From e2f67330bf8dd7edf6809995fbd52312506bf948 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Mon, 12 May 2025 15:51:04 +0530 Subject: [PATCH 14/20] alembic fix --- .../79e47bc3aac6_add_threads_table.py | 70 +++++++++++++++++++ .../9baa692f9a5d_add_threads_table.py | 33 --------- 2 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 backend/app/alembic/versions/79e47bc3aac6_add_threads_table.py delete mode 100644 backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py diff --git a/backend/app/alembic/versions/79e47bc3aac6_add_threads_table.py b/backend/app/alembic/versions/79e47bc3aac6_add_threads_table.py new file mode 100644 index 00000000..ea1fd6c1 --- /dev/null +++ b/backend/app/alembic/versions/79e47bc3aac6_add_threads_table.py @@ -0,0 +1,70 @@ +"""add threads table + +Revision ID: 79e47bc3aac6 +Revises: f23675767ed2 +Create Date: 2025-05-12 15:49:39.142806 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = "79e47bc3aac6" +down_revision = "f23675767ed2" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "openai_thread", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("thread_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("prompt", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("response", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("error", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("inserted_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_openai_thread_thread_id"), "openai_thread", ["thread_id"], unique=True + ) + op.drop_constraint( + "credential_organization_id_fkey", "credential", type_="foreignkey" + ) + op.create_foreign_key( + None, "credential", "organization", ["organization_id"], ["id"] + ) + op.drop_constraint("project_organization_id_fkey", "project", type_="foreignkey") + op.create_foreign_key(None, "project", "organization", ["organization_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "project", type_="foreignkey") + op.create_foreign_key( + "project_organization_id_fkey", + "project", + "organization", + ["organization_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_constraint(None, "credential", type_="foreignkey") + op.create_foreign_key( + "credential_organization_id_fkey", + "credential", + "organization", + ["organization_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_index(op.f("ix_openai_thread_thread_id"), table_name="openai_thread") + op.drop_table("openai_thread") + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py b/backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py deleted file mode 100644 index 489d2a94..00000000 --- a/backend/app/alembic/versions/9baa692f9a5d_add_threads_table.py +++ /dev/null @@ -1,33 +0,0 @@ -"""add threads table - -Revision ID: 9baa692f9a5d -Revises: 543f97951bd0 -Create Date: 2025-05-05 23:25:37.195415 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel.sql.sqltypes - - -# revision identifiers, used by Alembic. -revision = "9baa692f9a5d" -down_revision = "543f97951bd0" -branch_labels = None -depends_on = None - - -def upgrade(): - op.create_table( - "threadresponse", - sa.Column("thread_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("message", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("question", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint("thread_id"), - ) - - -def downgrade(): - op.drop_table("threadresponse") From 9db16b6c1c4487c7e0f27156bc25327eef2d9ddf Mon Sep 17 00:00:00 2001 From: nishika26 Date: Mon, 12 May 2025 16:37:34 +0530 Subject: [PATCH 15/20] changes --- backend/app/api/routes/threads.py | 94 +++++++++++++------- backend/app/crud/thread_results.py | 34 +++---- backend/app/models/__init__.py | 2 +- backend/app/models/threads.py | 20 +++-- backend/app/tests/api/routes/test_threads.py | 61 ++++++++----- backend/app/tests/crud/test_thread_result.py | 44 ++++++--- 6 files changed, 170 insertions(+), 85 deletions(-) diff --git a/backend/app/api/routes/threads.py b/backend/app/api/routes/threads.py index b165f3c4..d27b578e 100644 --- a/backend/app/api/routes/threads.py +++ b/backend/app/api/routes/threads.py @@ -9,7 +9,7 @@ from app.api.deps import get_current_user_org, get_db from app.core import logging, settings -from app.models import UserOrganization +from app.models import UserOrganization, OpenAIThreadCreate from app.crud import upsert_thread_result, get_thread_result from app.utils import APIResponse @@ -114,6 +114,24 @@ def create_success_response(request: dict, message: str) -> APIResponse: ) +def run_and_poll_thread(client: OpenAI, thread_id: str, assistant_id: str): + """Runs and polls a thread with the specified assistant using the OpenAI client.""" + return client.beta.threads.runs.create_and_poll( + thread_id=thread_id, + assistant_id=assistant_id, + ) + + +def extract_response_from_thread( + client: OpenAI, thread_id: str, remove_citation: bool = False +) -> str: + """Fetches and processes the latest message from a thread.""" + messages = client.beta.threads.messages.list(thread_id=thread_id) + latest_message = messages.data[0] + message_content = latest_message.content[0].text.value + return process_message_content(message_content, remove_citation) + + @observe(as_type="generation") def process_run(request: dict, client: OpenAI): """Process a run and send callback with results.""" @@ -161,29 +179,37 @@ def process_run(request: dict, client: OpenAI): def poll_run_and_prepare_response(request: dict, client: OpenAI, db: Session): - "process a run and send result to DB" + """Handles a thread run, processes the response, and upserts the result to the database.""" thread_id = request["thread_id"] - question = request["question"] + prompt = request["question"] try: - run = client.beta.threads.runs.create_and_poll( - thread_id=thread_id, - assistant_id=request["assistant_id"], - ) + run = run_and_poll_thread(client, thread_id, request["assistant_id"]) - if run.status == "completed": - messages = client.beta.threads.messages.list(thread_id=thread_id) - latest_message = messages.data[0] - message_content = latest_message.content[0].text.value - processed_message = process_message_content( - message_content, request.get("remove_citation", False) + status = run.status or "unknown" + response = None + error = None + + if status == "completed": + response = extract_response_from_thread( + client, thread_id, request.get("remove_citation", False) ) - upsert_thread_result(db, thread_id, question, processed_message) - else: - upsert_thread_result(db, thread_id, question, None) - except openai.OpenAIError: - upsert_thread_result(db, thread_id, question, None) + except openai.OpenAIError as e: + status = "failed" + error = str(e) + response = None + + upsert_thread_result( + db, + OpenAIThreadCreate( + thread_id=thread_id, + prompt=prompt, + response=response, + status=status, + error=error, + ), + ) @router.post("/threads") @@ -271,20 +297,15 @@ async def threads_sync( @router.post("/threads/start") async def start_thread( - request: dict, + request: OpenAIThreadCreate, background_tasks: BackgroundTasks, db: Session = Depends(get_db), _current_user: UserOrganization = Depends(get_current_user_org), ): """ Create a new OpenAI thread for the given question and start polling in the background. - - - If successful, returns the thread ID and a 'processing' status. - - Stores the thread and question in the database. """ - - question = request["question"] - + prompt = request["question"] client = OpenAI(api_key=settings.OPENAI_API_KEY) is_success, error = setup_thread(client, request) @@ -292,14 +313,24 @@ async def start_thread( return APIResponse.failure_response(error=error) thread_id = request["thread_id"] - upsert_thread_result(db, thread_id, question, None) + + upsert_thread_result( + db, + OpenAIThreadCreate( + thread_id=thread_id, + prompt=prompt, + response=None, + status="processing", + error=None, + ), + ) background_tasks.add_task(poll_run_and_prepare_response, request, client, db) return APIResponse.success_response( data={ "thread_id": thread_id, - "question": question, + "prompt": prompt, "status": "processing", "message": "Thread created and polling started in background.", } @@ -307,7 +338,7 @@ async def start_thread( @router.get("/threads/result/{thread_id}") -async def get_thread_result_by_id( +async def get_thread( thread_id: str, db: Session = Depends(get_db), _current_user: UserOrganization = Depends(get_current_user_org), @@ -320,13 +351,14 @@ async def get_thread_result_by_id( if not result: return APIResponse.failure_response(error="Thread not found.") - status = "success" if result.message else "processing" + status = result.status or ("success" if result.response else "processing") return APIResponse.success_response( data={ "thread_id": result.thread_id, - "question": result.question, + "prompt": result.prompt, "status": status, - "message": result.message, + "response": result.response, + "error": result.error, } ) diff --git a/backend/app/crud/thread_results.py b/backend/app/crud/thread_results.py index 78496f56..cd72ef18 100644 --- a/backend/app/crud/thread_results.py +++ b/backend/app/crud/thread_results.py @@ -1,21 +1,25 @@ -from sqlmodel import Session +from sqlmodel import Session, select from datetime import datetime -from app.models import ThreadResponse +from app.models import OpenAIThreadCreate, OpenAI_Thread -def upsert_thread_result( - session: Session, thread_id: str, question: str, message: str | None -): - session.merge( - ThreadResponse( - thread_id=thread_id, - question=question, - message=message, - updated_at=datetime.utcnow(), - ) - ) +def upsert_thread_result(session: Session, data: OpenAIThreadCreate): + statement = select(OpenAI_Thread).where(OpenAI_Thread.thread_id == data.thread_id) + existing = session.exec(statement).first() + + if existing: + existing.prompt = data.prompt + existing.response = data.response + existing.status = data.status + existing.error = data.error + existing.updated_at = datetime.utcnow() + else: + new_thread = OpenAI_Thread(**data.dict()) + session.add(new_thread) + session.commit() -def get_thread_result(session: Session, thread_id: str) -> ThreadResponse | None: - return session.get(ThreadResponse, thread_id) +def get_thread_result(session: Session, thread_id: str) -> OpenAI_Thread | None: + statement = select(OpenAI_Thread).where(OpenAI_Thread.thread_id == thread_id) + return session.exec(statement).first() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index af47d4c0..f88e019d 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -52,4 +52,4 @@ CredsUpdate, ) -from .threads import ThreadResponse +from .threads import OpenAI_Thread, OpenAIThreadBase, OpenAIThreadCreate diff --git a/backend/app/models/threads.py b/backend/app/models/threads.py index 1214c6fe..e353c676 100644 --- a/backend/app/models/threads.py +++ b/backend/app/models/threads.py @@ -3,9 +3,19 @@ from datetime import datetime -class ThreadResponse(SQLModel, table=True): - thread_id: str = Field(primary_key=True) - message: Optional[str] - question: str - created_at: datetime = Field(default_factory=datetime.utcnow) +class OpenAIThreadBase(SQLModel): + thread_id: str = Field(index=True, unique=True) + prompt: str + response: Optional[str] = None + status: Optional[str] = None + error: Optional[str] = None + + +class OpenAIThreadCreate(OpenAIThreadBase): + pass # Used for requests, no `id` or timestamps + + +class OpenAI_Thread(OpenAIThreadBase, table=True): + id: int = Field(default=None, primary_key=True) + inserted_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index a635c8d5..0b6afba1 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -14,7 +14,8 @@ handle_openai_error, poll_run_and_prepare_response, ) -from app.models import APIKey, ThreadResponse +from app.models import APIKey, OpenAI_Thread +from app.crud import get_thread_result import openai from openai import OpenAIError @@ -398,7 +399,7 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): mock_client.beta.threads.runs.create_and_poll.return_value = mock_run mock_message = MagicMock() - mock_message.content = [MagicMock(text=MagicMock(value="Answer "))] + mock_message.content = [MagicMock(text=MagicMock(value="Answer"))] mock_client.beta.threads.messages.list.return_value.data = [mock_message] mock_openai.return_value = mock_client @@ -411,8 +412,8 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_thread_001") - assert result.message.strip() == "Answer" + result = get_thread_result(db, "test_thread_001") + assert result.response.strip() == "Answer" @patch("app.api.routes.threads.OpenAI") @@ -429,8 +430,17 @@ def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): } poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_openai_error") - assert result.message is None + + # Since thread_id is not the primary key, use select query + statement = select(OpenAI_Thread).where( + OpenAI_Thread.thread_id == "test_openai_error" + ) + result = db.exec(statement).first() + + assert result is not None + assert result.response is None + assert result.status == "failed" + assert "Simulated OpenAI error" in (result.error or "") @patch("app.api.routes.threads.OpenAI") @@ -447,21 +457,32 @@ def test_poll_run_and_prepare_response_non_completed(mock_openai, db): } poll_run_and_prepare_response(request, mock_client, db) - result = db.get(ThreadResponse, "test_non_complete") - assert result.message is None + + # thread_id is not the primary key, so we query using SELECT + statement = select(OpenAI_Thread).where( + OpenAI_Thread.thread_id == "test_non_complete" + ) + result = db.exec(statement).first() + + assert result is not None + assert result.response is None + assert result.status == "failed" @patch("app.api.routes.threads.OpenAI") def test_threads_start_endpoint_creates_thread(mock_openai, db): """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() + + # Simulate created thread with a known ID mock_thread = MagicMock() mock_thread.id = "mock_thread_001" mock_client.beta.threads.create.return_value = mock_thread mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + # Get a valid API key from test DB + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted.is_(False))).first() if not api_key_record: pytest.skip("No API key found in the database for testing") @@ -470,11 +491,12 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): response = client.post("/threads/start", json=data, headers=headers) assert response.status_code == 200 + res_json = response.json() assert res_json["success"] assert res_json["data"]["thread_id"] == "mock_thread_001" assert res_json["data"]["status"] == "processing" - assert res_json["data"]["question"] == "What's 2+2?" + assert res_json["data"]["prompt"] == "What's 2+2?" def test_threads_result_endpoint_success(db): @@ -483,7 +505,7 @@ def test_threads_result_endpoint_success(db): question = "Capital of France?" message = "Paris." - db.add(ThreadResponse(thread_id=thread_id, question=question, message=message)) + db.add(OpenAI_Thread(thread_id=thread_id, prompt=question, response=message)) db.commit() api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() @@ -496,9 +518,9 @@ def test_threads_result_endpoint_success(db): assert response.status_code == 200 data = response.json()["data"] assert data["status"] == "success" - assert data["message"] == "Paris." + assert data["response"] == "Paris." assert data["thread_id"] == thread_id - assert data["question"] == question + assert data["prompt"] == question def test_threads_result_endpoint_processing(db): @@ -506,7 +528,7 @@ def test_threads_result_endpoint_processing(db): thread_id = f"test_processing_{uuid.uuid4()}" question = "What is Glific?" - db.add(ThreadResponse(thread_id=thread_id, question=question, message=None)) + db.add(OpenAI_Thread(thread_id=thread_id, prompt=question, response=None)) db.commit() api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() @@ -521,7 +543,7 @@ def test_threads_result_endpoint_processing(db): assert data["status"] == "processing" assert data["message"] is None assert data["thread_id"] == thread_id - assert data["question"] == question + assert data["prompt"] == question def test_threads_result_not_found(db): @@ -543,16 +565,15 @@ def test_threads_start_missing_question(mock_openai, db): """Test /threads/start with missing 'question' key in request.""" mock_openai.return_value = MagicMock() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted.is_(False))).first() if not api_key_record: pytest.skip("No API key found in the database for testing") headers = {"X-API-KEY": api_key_record.key} - - bad_data = {"assistant_id": "assist_123"} # no "question" key + bad_data = {"assistant_id": "assist_123"} # missing "question" / "prompt" response = client.post("/threads/start", json=bad_data, headers=headers) - assert response.status_code == 422 # Unprocessable Entity (FastAPI will raise 422) + assert response.status_code in (422, 500) error_response = response.json() - assert "detail" in error_response + assert "detail" in error_response or "error" in error_response diff --git a/backend/app/tests/crud/test_thread_result.py b/backend/app/tests/crud/test_thread_result.py index d0a0cff8..47e95b81 100644 --- a/backend/app/tests/crud/test_thread_result.py +++ b/backend/app/tests/crud/test_thread_result.py @@ -1,7 +1,7 @@ import pytest from sqlmodel import SQLModel, Session, create_engine -from app.models import ThreadResponse +from app.models import OpenAI_Thread, OpenAIThreadCreate from app.crud import upsert_thread_result, get_thread_result @@ -16,23 +16,41 @@ def session(): def test_upsert_and_get_thread_result(session: Session): thread_id = "thread_test_123" - question = "What is the capital of Spain?" - message = "Madrid is the capital of Spain." - - # Initially insert - upsert_thread_result(session, thread_id, question, message) + prompt = "What is the capital of Spain?" + response = "Madrid is the capital of Spain." + + # Insert + upsert_thread_result( + session, + OpenAIThreadCreate( + thread_id=thread_id, + prompt=prompt, + response=response, + status="completed", + error=None, + ), + ) # Retrieve result = get_thread_result(session, thread_id) assert result is not None assert result.thread_id == thread_id - assert result.question == question - assert result.message == message - - # Update with new message - updated_message = "Madrid." - upsert_thread_result(session, thread_id, question, updated_message) + assert result.prompt == prompt + assert result.response == response + + # Update with new response + updated_response = "Madrid." + upsert_thread_result( + session, + OpenAIThreadCreate( + thread_id=thread_id, + prompt=prompt, + response=updated_response, + status="completed", + error=None, + ), + ) result_updated = get_thread_result(session, thread_id) - assert result_updated.message == updated_message + assert result_updated.response == updated_response From 79c6191f2e38c24f42f2e47b205c3cd2405a82b5 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Mon, 12 May 2025 17:10:14 +0530 Subject: [PATCH 16/20] test cases failure --- backend/app/tests/api/routes/test_creds.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/backend/app/tests/api/routes/test_creds.py b/backend/app/tests/api/routes/test_creds.py index bdba0d8e..2f1a0b78 100644 --- a/backend/app/tests/api/routes/test_creds.py +++ b/backend/app/tests/api/routes/test_creds.py @@ -43,21 +43,16 @@ def create_organization_and_creds(db: Session, superuser_token_headers: dict[str def test_set_creds_for_org(db: Session, superuser_token_headers: dict[str, str]): - unique_org_id = 2 - existing_org = ( - db.query(Organization).filter(Organization.id == unique_org_id).first() - ) + unique_name = "Test Organization " + generate_random_string(5) - if not existing_org: - new_org = Organization( - id=unique_org_id, name="Test Organization", is_active=True - ) - db.add(new_org) - db.commit() + new_org = Organization(name=unique_name, is_active=True) + db.add(new_org) + db.commit() + db.refresh(new_org) api_key = "sk-" + generate_random_string(10) creds_data = { - "organization_id": unique_org_id, + "organization_id": new_org.id, "is_active": True, "credential": {"openai": {"api_key": api_key}}, } @@ -69,10 +64,9 @@ def test_set_creds_for_org(db: Session, superuser_token_headers: dict[str, str]) ) assert response.status_code == 200 - created_creds = response.json() assert "data" in created_creds - assert created_creds["data"]["organization_id"] == unique_org_id + assert created_creds["data"]["organization_id"] == new_org.id assert created_creds["data"]["credential"]["openai"]["api_key"] == api_key From 3b52721a2c3320eced54ea363d5ef1bb7f746da6 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Mon, 12 May 2025 17:13:19 +0530 Subject: [PATCH 17/20] clean db after test --- backend/app/tests/conftest.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 9cd6c497..a2805fce 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -7,13 +7,7 @@ from app.core.config import settings from app.core.db import engine, init_db from app.main import app -from app.models import ( - APIKey, - Organization, - Project, - ProjectUser, - User, -) +from app.models import APIKey, Organization, Project, ProjectUser, User, OpenAI_Thread from app.tests.utils.user import authentication_token_from_email from app.tests.utils.utils import get_superuser_token_headers @@ -29,6 +23,7 @@ def db() -> Generator[Session, None, None]: session.execute(delete(Organization)) session.execute(delete(APIKey)) session.execute(delete(User)) + session.execute(delete(OpenAI_Thread)) session.commit() From 512c2eada5a06cf58031576a0b3d68d3da3c3dcf Mon Sep 17 00:00:00 2001 From: nishika26 Date: Mon, 12 May 2025 17:17:40 +0530 Subject: [PATCH 18/20] clean db after test --- backend/app/tests/conftest.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index a2805fce..fa36ddf0 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -7,7 +7,15 @@ from app.core.config import settings from app.core.db import engine, init_db from app.main import app -from app.models import APIKey, Organization, Project, ProjectUser, User, OpenAI_Thread +from app.models import ( + APIKey, + Organization, + Project, + ProjectUser, + User, + OpenAI_Thread, + Credential, +) from app.tests.utils.user import authentication_token_from_email from app.tests.utils.utils import get_superuser_token_headers @@ -20,6 +28,7 @@ def db() -> Generator[Session, None, None]: # Delete data in reverse dependency order session.execute(delete(ProjectUser)) # Many-to-many relationship session.execute(delete(Project)) + session.execute(delete(Credential)) session.execute(delete(Organization)) session.execute(delete(APIKey)) session.execute(delete(User)) From a74aada6e280c406238c3161bab1799bc90d5c75 Mon Sep 17 00:00:00 2001 From: nishika26 Date: Mon, 12 May 2025 17:26:27 +0530 Subject: [PATCH 19/20] test cases --- backend/app/tests/api/routes/test_threads.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 0b6afba1..04c7dadd 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -473,16 +473,13 @@ def test_poll_run_and_prepare_response_non_completed(mock_openai, db): def test_threads_start_endpoint_creates_thread(mock_openai, db): """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() - - # Simulate created thread with a known ID mock_thread = MagicMock() mock_thread.id = "mock_thread_001" mock_client.beta.threads.create.return_value = mock_thread mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - # Get a valid API key from test DB - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted.is_(False))).first() + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() if not api_key_record: pytest.skip("No API key found in the database for testing") @@ -491,7 +488,6 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): response = client.post("/threads/start", json=data, headers=headers) assert response.status_code == 200 - res_json = response.json() assert res_json["success"] assert res_json["data"]["thread_id"] == "mock_thread_001" @@ -565,15 +561,16 @@ def test_threads_start_missing_question(mock_openai, db): """Test /threads/start with missing 'question' key in request.""" mock_openai.return_value = MagicMock() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted.is_(False))).first() + api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() if not api_key_record: pytest.skip("No API key found in the database for testing") headers = {"X-API-KEY": api_key_record.key} - bad_data = {"assistant_id": "assist_123"} # missing "question" / "prompt" + + bad_data = {"assistant_id": "assist_123"} # no "question" key response = client.post("/threads/start", json=bad_data, headers=headers) - assert response.status_code in (422, 500) + assert response.status_code == 422 # Unprocessable Entity (FastAPI will raise 422) error_response = response.json() - assert "detail" in error_response or "error" in error_response + assert "detail" in error_response From 7bb4498d587c5d6ca709b6b4c93b04291a346b5b Mon Sep 17 00:00:00 2001 From: nishika26 Date: Tue, 13 May 2025 12:35:51 +0530 Subject: [PATCH 20/20] removing sqlite session --- backend/app/tests/crud/test_thread_result.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/backend/app/tests/crud/test_thread_result.py b/backend/app/tests/crud/test_thread_result.py index 47e95b81..00c581de 100644 --- a/backend/app/tests/crud/test_thread_result.py +++ b/backend/app/tests/crud/test_thread_result.py @@ -5,23 +5,14 @@ from app.crud import upsert_thread_result, get_thread_result -@pytest.fixture -def session(): - """Creates a new in-memory database session for each test.""" - engine = create_engine("sqlite://", echo=False) - SQLModel.metadata.create_all(engine) - with Session(engine) as session: - yield session - - -def test_upsert_and_get_thread_result(session: Session): +def test_upsert_and_get_thread_result(db: Session): thread_id = "thread_test_123" prompt = "What is the capital of Spain?" response = "Madrid is the capital of Spain." # Insert upsert_thread_result( - session, + db, OpenAIThreadCreate( thread_id=thread_id, prompt=prompt, @@ -32,7 +23,7 @@ def test_upsert_and_get_thread_result(session: Session): ) # Retrieve - result = get_thread_result(session, thread_id) + result = get_thread_result(db, thread_id) assert result is not None assert result.thread_id == thread_id @@ -42,7 +33,7 @@ def test_upsert_and_get_thread_result(session: Session): # Update with new response updated_response = "Madrid." upsert_thread_result( - session, + db, OpenAIThreadCreate( thread_id=thread_id, prompt=prompt, @@ -52,5 +43,5 @@ def test_upsert_and_get_thread_result(session: Session): ), ) - result_updated = get_thread_result(session, thread_id) + result_updated = get_thread_result(db, thread_id) assert result_updated.response == updated_response