Skip to content

feat: advanced filters for feedbacks and chats admin api #525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 11, 2025
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
Empty file.
23 changes: 23 additions & 0 deletions backend/app/api/admin_routes/chat/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi_pagination import Page, Params

from app.models.chat import ChatOrigin
from app.api.deps import CurrentSuperuserDep, SessionDep
from app.repositories import chat_repo


router = APIRouter(
prefix="/admin/chats",
tags=["admin/chats"],
)


@router.get("/origins")
def list_chat_origins(
db_session: SessionDep,
user: CurrentSuperuserDep,
search: Optional[str] = None,
params: Params = Depends(),
) -> Page[ChatOrigin]:
return chat_repo.list_chat_origins(db_session, search, params)
46 changes: 26 additions & 20 deletions backend/app/api/admin_routes/feedback.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
from fastapi import APIRouter, Depends
from typing import Annotated, Optional

from fastapi import APIRouter, Depends, Query
from fastapi_pagination import Params, Page
from fastapi_pagination.ext.sqlmodel import paginate
from sqlmodel import select

from app.api.deps import SessionDep, CurrentSuperuserDep
from app.models import Feedback, AdminFeedbackPublic
from app.models import AdminFeedbackPublic, FeedbackFilters
from app.models.feedback import FeedbackOrigin
from app.repositories import feedback_repo

router = APIRouter()
router = APIRouter(
prefix="/admin/feedbacks",
tags=["admin/feedback"],
)


@router.get("/admin/feedbacks")
@router.get("/")
def list_feedbacks(
session: SessionDep,
user: CurrentSuperuserDep,
filters: Annotated[FeedbackFilters, Query()],
params: Params = Depends(),
) -> Page[AdminFeedbackPublic]:
return paginate(
session,
select(Feedback).order_by(Feedback.created_at.desc()),
params,
transformer=lambda items: [
AdminFeedbackPublic(
**item.model_dump(),
chat_title=item.chat.title,
chat_origin=item.chat.origin,
chat_message_content=item.chat_message.content,
user_email=item.user.email if item.user else None,
)
for item in items
],
return feedback_repo.paginate(
session=session,
filters=filters,
params=params,
)


@router.get("/origins")
def list_feedback_origins(
session: SessionDep,
user: CurrentSuperuserDep,
search: Optional[str] = None,
params: Params = Depends(),
) -> Page[FeedbackOrigin]:
return feedback_repo.list_feedback_origins(session, search, params)
1 change: 1 addition & 0 deletions backend/app/api/admin_routes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class EmbeddingModelDescriptor(EmbeddingModelItem):

class UserDescriptor(BaseModel):
id: UUID
email: str


class KnowledgeBaseDescriptor(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/admin_routes/stats.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from datetime import date
from pydantic import BaseModel
from fastapi import APIRouter

from app.api.deps import CurrentSuperuserDep, SessionDep
from app.repositories import chat_repo


router = APIRouter()


Expand Down
24 changes: 24 additions & 0 deletions backend/app/api/admin_routes/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi_pagination import Page, Params

from app.repositories.user import user_repo
from app.api.deps import SessionDep, CurrentSuperuserDep
from app.api.admin_routes.models import (
UserDescriptor,
)

router = APIRouter(
prefix="/admin/users",
tags=["admin/users"],
)


@router.get("/search")
def search_users(
db_session: SessionDep,
user: CurrentSuperuserDep,
search: Optional[str] = None,
params: Params = Depends(),
) -> Page[UserDescriptor]:
return user_repo.search_users(db_session, search, params)
10 changes: 6 additions & 4 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from fastapi import APIRouter


from app.api.routes import (
index,
chat,
Expand Down Expand Up @@ -35,6 +33,7 @@
from app.api.admin_routes.reranker_model.routes import (
router as admin_reranker_model_router,
)
from app.api.admin_routes.chat.routes import router as admin_user_router
from app.api.admin_routes import (
chat_engine as admin_chat_engine,
feedback as admin_feedback,
Expand All @@ -44,6 +43,7 @@
stats as admin_stats,
semantic_cache as admin_semantic_cache,
langfuse as admin_langfuse,
user as admin_user,
)
from app.api.admin_routes.evaluation import (
evaluation_task as admin_evaluation_task,
Expand All @@ -63,9 +63,10 @@
api_router.include_router(api_key.router, tags=["auth"])
api_router.include_router(document.router, tags=["documents"])
api_router.include_router(retrieve_routes.router, tags=["retrieve"])
api_router.include_router(admin_chat_engine.router, tags=["admin/chat_engine"])
api_router.include_router(admin_user_router)
api_router.include_router(admin_chat_engine.router, tags=["admin/chat-engines"])
api_router.include_router(admin_document_router, tags=["admin/documents"])
api_router.include_router(admin_feedback.router, tags=["admin/feedback"])
api_router.include_router(admin_feedback.router)
api_router.include_router(admin_site_settings.router, tags=["admin/site_settings"])
api_router.include_router(admin_upload.router, tags=["admin/upload"])
api_router.include_router(admin_knowledge_base_router, tags=["admin/knowledge_base"])
Expand All @@ -92,6 +93,7 @@
api_router.include_router(
admin_evaluation_dataset.router, tags=["admin/evaluation/dataset"]
)
api_router.include_router(admin_user.router)

api_router.include_router(
fastapi_users.get_auth_router(auth_backend), prefix="/auth", tags=["auth"]
Expand Down
11 changes: 6 additions & 5 deletions backend/app/api/routes/chat.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import logging
from uuid import UUID
from typing import List, Optional
from typing import List, Optional, Annotated
from http import HTTPStatus

from pydantic import (
BaseModel,
field_validator,
)
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from fastapi.responses import StreamingResponse
from fastapi_pagination import Params, Page

from app.api.deps import SessionDep, OptionalUserDep, CurrentUserDep
from app.rag.chat.chat_flow import ChatFlow
from app.rag.retrievers.knowledge_graph.schema import KnowledgeGraphRetrievalResult
from app.repositories import chat_repo
from app.models import Chat, ChatUpdate

from app.rag.chat.chat_service import get_final_chat_result
from app.models import Chat, ChatUpdate, ChatFilters
from app.rag.chat.chat_service import (
get_final_chat_result,
user_can_view_chat,
user_can_edit_chat,
get_chat_message_subgraph,
Expand Down Expand Up @@ -99,10 +99,11 @@ def list_chats(
request: Request,
session: SessionDep,
user: OptionalUserDep,
filters: Annotated[ChatFilters, Query()],
params: Params = Depends(),
) -> Page[Chat]:
browser_id = request.state.browser_id
return chat_repo.paginate(session, user, browser_id, params)
return chat_repo.paginate(session, user, browser_id, filters, params)


@router.get("/chats/{chat_id}")
Expand Down
4 changes: 3 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
Feedback,
FeedbackType,
AdminFeedbackPublic,
FeedbackFilters,
FeedbackOrigin,
)
from .semantic_cache import SemanticCache
from .staff_action_log import StaffActionLog
from .chat_engine import ChatEngine, ChatEngineUpdate
from .chat import Chat, ChatUpdate, ChatVisibility
from .chat import Chat, ChatUpdate, ChatVisibility, ChatFilters, ChatOrigin
from .chat_message import ChatMessage
from .document import Document, DocIndexTaskStatus
from .chunk import Chunk, KgIndexStatus
Expand Down
15 changes: 15 additions & 0 deletions backend/app/models/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,18 @@ class Chat(UUIDBaseModel, UpdatableBaseModel, table=True):
class ChatUpdate(BaseModel):
title: Optional[str] = None
visibility: Optional[ChatVisibility] = None


class ChatFilters(BaseModel):
created_at_start: Optional[datetime] = None
created_at_end: Optional[datetime] = None
updated_at_start: Optional[datetime] = None
updated_at_end: Optional[datetime] = None
chat_origin: Optional[str] = None
# user_id: Optional[UUID] = None # no use now
engine_id: Optional[int] = None


class ChatOrigin(BaseModel):
origin: str
chats: int
16 changes: 16 additions & 0 deletions backend/app/models/feedback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import enum
from uuid import UUID
from typing import Optional
from pydantic import BaseModel
from datetime import datetime

from sqlmodel import (
Field,
Expand Down Expand Up @@ -63,3 +65,17 @@ class AdminFeedbackPublic(BaseFeedback):
chat_message_content: str
user_id: Optional[UUID]
user_email: Optional[str]


class FeedbackFilters(BaseModel):
created_at_start: Optional[datetime] = None
created_at_end: Optional[datetime] = None
feedback_origin: Optional[str] = None
chat_id: Optional[UUID] = None
feedback_type: Optional[FeedbackType] = None
user_id: Optional[UUID] = None


class FeedbackOrigin(BaseModel):
origin: str
feedbacks: int
1 change: 1 addition & 0 deletions backend/app/repositories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
from .chunk import chunk_repo
from .data_source import data_source_repo
from .knowledge_base import knowledge_base_repo
from .feedback import feedback_repo
from .llm import llm_repo
from .embedding_model import embed_model_repo
49 changes: 47 additions & 2 deletions backend/app/repositories/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from datetime import datetime, UTC, date, timedelta
from collections import defaultdict

from sqlmodel import select, Session, or_, func, case, desc
from sqlmodel import select, Session, or_, func, case, desc, col
from fastapi_pagination import Params, Page
from fastapi_pagination.ext.sqlmodel import paginate

from app.models import Chat, User, ChatMessage, ChatUpdate
from app.models import Chat, User, ChatMessage, ChatUpdate, ChatFilters, ChatOrigin
from app.repositories.base_repo import BaseRepo
from app.exceptions import ChatNotFound, ChatMessageNotFound

Expand All @@ -21,6 +21,7 @@ def paginate(
session: Session,
user: User | None,
browser_id: str | None,
filters: ChatFilters,
params: Params | None = Params(),
) -> Page[Chat]:
query = select(Chat).where(Chat.deleted_at == None)
Expand All @@ -31,6 +32,23 @@ def paginate(
)
else:
query = query.where(Chat.browser_id == browser_id, Chat.user_id == None)

# filters
if filters.created_at_start:
query = query.where(Chat.created_at >= filters.created_at_start)
if filters.created_at_end:
query = query.where(Chat.created_at <= filters.created_at_end)
if filters.updated_at_start:
query = query.where(Chat.updated_at >= filters.updated_at_start)
if filters.updated_at_end:
query = query.where(Chat.updated_at <= filters.updated_at_end)
if filters.chat_origin:
query = query.where(col(Chat.origin).contains(filters.chat_origin))
# if filters.user_id:
# query = query.where(Chat.user_id == filters.user_id)
if filters.engine_id:
query = query.where(Chat.engine_id == filters.engine_id)

query = query.order_by(Chat.created_at.desc())
return paginate(session, query, params)

Expand Down Expand Up @@ -224,5 +242,32 @@ def chat_trend_by_origin(
stats.sort(key=lambda x: x["date"])
return stats

def list_chat_origins(
self,
db_session: Session,
search: Optional[str] = None,
params: Params = Params(),
) -> Page[ChatOrigin]:
query = (
select(Chat.origin, func.count(Chat.id).label("chats"))
.where(Chat.deleted_at == None)
.where(Chat.origin != None)
.where(Chat.origin != "")
)

if search:
query = query.where(Chat.origin.ilike(f"%{search}%"))

query = query.group_by(Chat.origin).order_by(desc("chats"))

return paginate(
db_session,
query,
params,
transformer=lambda chats: [
ChatOrigin(origin=chat.origin, chats=chat.chats) for chat in chats
],
)


chat_repo = ChatRepo()
Loading
Loading