Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Change vector_store_id to vector_store_ids in openai_assistant table
Revision ID: f2589428c1d0
Revises: 3389c67fdcb4
Create Date: 2025-07-10 11:18:21.223114
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "f2589428c1d0"
down_revision = "3389c67fdcb4"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"openai_assistant",
sa.Column("vector_store_ids", postgresql.ARRAY(sa.String()), nullable=True),
)

op.execute(
"""
UPDATE openai_assistant
SET vector_store_ids = ARRAY[vector_store_id]
WHERE vector_store_id IS NOT NULL
"""
)

op.drop_column("openai_assistant", "vector_store_id")
# ### end Alembic commands ###


def downgrade():
# Add back the single vector_store_id column as nullable for safe data migration
op.add_column(
"openai_assistant",
sa.Column(
"vector_store_id",
sa.VARCHAR(length=255),
autoincrement=False,
nullable=True, # Allow nulls temporarily for safe migration
),
)

op.execute(
"""
UPDATE openai_assistant
SET vector_store_id = vector_store_ids[1]
WHERE vector_store_ids IS NOT NULL AND array_length(vector_store_ids, 1) > 0
"""
)

op.drop_column("openai_assistant", "vector_store_ids")
2 changes: 2 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from app.api.routes import (
api_keys,
assistants,
collections,
documents,
login,
Expand All @@ -20,6 +21,7 @@

api_router = APIRouter()
api_router.include_router(api_keys.router)
api_router.include_router(assistants.router)
api_router.include_router(collections.router)
api_router.include_router(credentials.router)
api_router.include_router(documents.router)
Expand Down
43 changes: 43 additions & 0 deletions backend/app/api/routes/assistants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Annotated

from fastapi import APIRouter, Depends, Path
from sqlmodel import Session

from app.api.deps import get_db, get_current_user_org_project
from app.crud import (
fetch_assistant_from_openai,
sync_assistant,
)
from app.models import UserProjectOrg
from app.utils import APIResponse, get_openai_client

router = APIRouter(prefix="/assistant", tags=["Assistants"])


@router.post(
"/{assistant_id}/ingest",
response_model=APIResponse,
status_code=201,
)
def ingest_assistant_route(
assistant_id: Annotated[str, Path(description="The ID of the assistant to ingest")],
session: Session = Depends(get_db),
current_user: UserProjectOrg = Depends(get_current_user_org_project),
):
"""
Ingest an assistant from OpenAI and store it in the platform.
"""

client = get_openai_client(
session, current_user.organization_id, current_user.project_id
)

openai_assistant = fetch_assistant_from_openai(assistant_id, client)
assistant = sync_assistant(
session=session,
organization_id=current_user.organization_id,
project_id=current_user.project_id,
openai_assistant=openai_assistant,
)

return APIResponse.success_response(assistant)
4 changes: 2 additions & 2 deletions backend/app/api/routes/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ def process_response(
"input": [{"role": "user", "content": request.question}],
}

if assistant.vector_store_id:
if assistant.vector_store_ids:
params["tools"] = [
{
"type": "file_search",
"vector_store_ids": [assistant.vector_store_id],
"vector_store_ids": assistant.vector_store_ids,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just confirm did u test if it comes in the same format and the API works

Copy link
Collaborator Author

@avirajsingh7 avirajsingh7 Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AkhileshNegi vector_store_id does not exist in db.
What do you mean by in same format?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so in the im sarying that vector_store_ids key expects value to be list, do we get list here and the API is working

"max_num_results": assistant.max_num_results,
}
]
Expand Down
7 changes: 6 additions & 1 deletion backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
update_user,
)
from .collection import CollectionCrud

from .document import DocumentCrud
from .document_collection import DocumentCollectionCrud

Expand Down Expand Up @@ -43,4 +44,8 @@

from .thread_results import upsert_thread_result, get_thread_result

from .assistants import get_assistant_by_id
from .assistants import (
get_assistant_by_id,
fetch_assistant_from_openai,
sync_assistant,
)
101 changes: 98 additions & 3 deletions backend/app/crud/assistants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from typing import Optional, List, Tuple
from sqlmodel import Session, select, and_
import logging

from typing import Optional

import openai
from fastapi import HTTPException
from openai import OpenAI
from openai.types.beta import Assistant as OpenAIAssistant
from sqlmodel import Session, and_, select

from app.core.util import now
from app.models import Assistant
from app.utils import mask_string

logger = logging.getLogger(__name__)


def get_assistant_by_id(
Expand All @@ -16,3 +25,89 @@ def get_assistant_by_id(
)
)
return session.exec(statement).first()


def fetch_assistant_from_openai(assistant_id: str, client: OpenAI) -> OpenAIAssistant:
"""
Fetch an assistant from OpenAI.
Returns OpenAI Assistant model.
"""

try:
assistant = client.beta.assistants.retrieve(assistant_id=assistant_id)
return assistant
except openai.NotFoundError as e:
logger.error(
f"[fetch_assistant_from_openai] Assistant not found: {mask_string(assistant_id)} | {e}"
)
raise HTTPException(status_code=404, detail="Assistant not found in OpenAI.")
except openai.OpenAIError as e:
logger.error(
f"[fetch_assistant_from_openai] OpenAI API error while retrieving assistant {mask_string(assistant_id)}: {e}"
)
raise HTTPException(status_code=502, detail=f"OpenAI API error: {e}")


def sync_assistant(
session: Session,
organization_id: int,
project_id: int,
openai_assistant: OpenAIAssistant,
) -> Assistant:
"""
Insert an assistant into the database by converting OpenAI Assistant to local Assistant model.
"""
assistant_id = openai_assistant.id

existing_assistant = get_assistant_by_id(session, assistant_id, organization_id)
if existing_assistant:
logger.info(
f"[sync_assistant] Assistant with ID {mask_string(assistant_id)} already exists in the database."
)
raise HTTPException(
status_code=409,
detail=f"Assistant with ID {assistant_id} already exists.",
)

if not openai_assistant.instructions:
raise HTTPException(
status_code=400,
detail="Assistant has no instruction.",
)

vector_store_ids = []
if openai_assistant.tool_resources and hasattr(
openai_assistant.tool_resources, "file_search"
):
file_search = openai_assistant.tool_resources.file_search
if file_search and hasattr(file_search, "vector_store_ids"):
vector_store_ids = file_search.vector_store_ids or []

max_num_results = 20
for tool in openai_assistant.tools or []:
if tool.type == "file_search":
file_search = getattr(tool, "file_search", None)
if file_search and hasattr(file_search, "max_num_results"):
max_num_results = file_search.max_num_results
break

db_assistant = Assistant(
assistant_id=openai_assistant.id,
name=openai_assistant.name or openai_assistant.id,
instructions=openai_assistant.instructions,
model=openai_assistant.model,
vector_store_ids=vector_store_ids,
temperature=openai_assistant.temperature or 0.1,
max_num_results=max_num_results,
project_id=project_id,
organization_id=organization_id,
)

session.add(db_assistant)
session.commit()
session.refresh(db_assistant)

logger.info(
f"[sync_assistant] Successfully ingested assistant with ID {mask_string(assistant_id)}."
)
return db_assistant
6 changes: 5 additions & 1 deletion backend/app/models/assistants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime
from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel
from sqlalchemy import Column, String
from sqlalchemy.dialects.postgresql import ARRAY

from app.core.util import now

Expand All @@ -10,7 +12,9 @@ class AssistantBase(SQLModel):
name: str
instructions: str
model: str
vector_store_id: str
vector_store_ids: List[str] = Field(
default_factory=list, sa_column=Column(ARRAY(String))
)
temperature: float = 0.1
max_num_results: int = 20
project_id: int = Field(foreign_key="project.id")
Expand Down
27 changes: 23 additions & 4 deletions backend/app/seed_data/seed_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,42 @@
{
"is_active": true,
"provider": "openai",
"credential": "{\"openai\": {\"api_key\": \"sk-proj-YxK21qI3i5SCxN\"}}",
"credential": "{\"api_key\": \"sk-proj-GlificI3i5SCxN\"}",
"project_name": "Glific",
"organization_name": "Project Tech4dev",
"deleted_at": null
},
{
"is_active": true,
"provider": "openai",
"credential": "{\"api_key\": \"sk-proj-DalgoI3i5SCxN\"}",
"project_name": "Dalgo",
"organization_name": "Project Tech4dev",
"deleted_at": null
}
],
"assistants": [
{
"assistant_id": "assistant_123",
"name": "Test Assistant",
"assistant_id": "assistant_glific",
"name": "Test Assistant Glific",
"instructions": "Test instructions",
"model": "gpt-4o",
"vector_store_id": "vs_123",
"vector_store_ids": ["vs_glific"],
"temperature": 0.1,
"max_num_results": 20,
"project_name": "Glific",
"organization_name": "Project Tech4dev"
},
{
"assistant_id": "assistant_dalgo",
"name": "Test Assistant Dalgo",
"instructions": "Test instructions",
"model": "gpt-4o",
"vector_store_ids": ["vs_dalgo"],
"temperature": 0.1,
"max_num_results": 20,
"project_name": "Dalgo",
"organization_name": "Project Tech4dev"
}
]
}
4 changes: 2 additions & 2 deletions backend/app/seed_data/seed_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class AssistantData(BaseModel):
name: str
instructions: str
model: str
vector_store_id: str
vector_store_ids: list[str]
temperature: float
max_num_results: int
project_name: str
Expand Down Expand Up @@ -261,7 +261,7 @@ def create_assistant(session: Session, assistant_data_raw: dict) -> Assistant:
name=assistant_data.name,
instructions=assistant_data.instructions,
model=assistant_data.model,
vector_store_id=assistant_data.vector_store_id,
vector_store_ids=assistant_data.vector_store_ids,
temperature=assistant_data.temperature,
max_num_results=assistant_data.max_num_results,
organization_id=organization.id,
Expand Down
31 changes: 31 additions & 0 deletions backend/app/tests/api/routes/test_assistants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch
from app.tests.utils.openai import mock_openai_assistant


@pytest.fixture
def normal_user_api_key_header():
return {"X-API-KEY": "ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j9"}


@patch("app.api.routes.assistants.fetch_assistant_from_openai")
def test_ingest_assistant_success(
mock_fetch_assistant,
client: TestClient,
normal_user_api_key_header: str,
):
"""Test successful assistant ingestion from OpenAI."""
mock_assistant = mock_openai_assistant()

mock_fetch_assistant.return_value = mock_assistant

response = client.post(
f"/api/v1/assistant/{mock_assistant.id}/ingest",
headers=normal_user_api_key_header,
)

assert response.status_code == 201
response_json = response.json()
assert response_json["success"] is True
assert response_json["data"]["assistant_id"] == mock_assistant.id
4 changes: 2 additions & 2 deletions backend/app/tests/api/routes/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_responses_endpoint_success(

headers = {"X-API-KEY": original_api_key}
request_data = {
"assistant_id": "assistant_123",
"assistant_id": "assistant_glific",
"question": "What is Glific?",
"callback_url": "http://example.com/callback",
}
Expand Down Expand Up @@ -80,7 +80,7 @@ def test_responses_endpoint_without_vector_store(
mock_assistant.model = "gpt-4"
mock_assistant.instructions = "Test instructions"
mock_assistant.temperature = 0.1
mock_assistant.vector_store_id = None # No vector store configured
mock_assistant.vector_store_ids = [] # No vector store configured
mock_get_assistant.return_value = mock_assistant

# Setup mock OpenAI client
Expand Down
Loading