Skip to content

Commit 6ab41b9

Browse files
authored
feat: backend for user feedback (#1019)
* feat: backend for user feedback Signed-off-by: Tomas Weiss <tomas.weiss2@gmail.com> * feat: backend for user feedback Signed-off-by: Tomas Weiss <tomas.weiss2@gmail.com> * fix: alembic script & using post for creation Signed-off-by: Tomas Weiss <tomas.weiss2@gmail.com> --------- Signed-off-by: Tomas Weiss <tomas.weiss2@gmail.com>
1 parent 4d8be51 commit 6ab41b9

File tree

12 files changed

+243
-2
lines changed

12 files changed

+243
-2
lines changed

apps/beeai-server/src/beeai_server/api/dependencies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from beeai_server.service_layer.services.env import EnvService
1414
from beeai_server.service_layer.services.files import FileService
1515
from beeai_server.service_layer.services.provider import ProviderService
16+
from beeai_server.service_layer.services.user_feedback import UserFeedbackService
1617
from beeai_server.service_layer.services.users import UserService
1718
from beeai_server.service_layer.services.vector_stores import VectorStoreService
1819

@@ -23,6 +24,7 @@
2324
FileServiceDependency = Annotated[FileService, Depends(lambda: di[FileService])]
2425
UserServiceDependency = Annotated[UserService, Depends(lambda: di[UserService])]
2526
VectorStoreServiceDependency = Annotated[VectorStoreService, Depends(lambda: di[VectorStoreService])]
27+
UserFeedbackServiceDependency = Annotated[UserFeedbackService, Depends(lambda: di[UserFeedbackService])]
2628

2729
# Auth
2830

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import fastapi
5+
6+
from beeai_server.api.dependencies import AuthenticatedUserDependency, UserFeedbackServiceDependency
7+
from beeai_server.api.schema.user_feedback import InsertUserFeedbackRequest
8+
9+
router = fastapi.APIRouter()
10+
11+
12+
@router.post("", status_code=fastapi.status.HTTP_201_CREATED)
13+
async def user_feedback(
14+
request: InsertUserFeedbackRequest,
15+
user_feedback_service: UserFeedbackServiceDependency,
16+
user: AuthenticatedUserDependency,
17+
) -> None:
18+
await user_feedback_service.create_user_feedback(
19+
provider_id=request.provider_id,
20+
task_id=request.task_id,
21+
context_id=request.context_id,
22+
rating=request.rating,
23+
comment=request.comment,
24+
comment_tags=request.comment_tags,
25+
message=request.message,
26+
user=user,
27+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from uuid import UUID
5+
6+
from pydantic import BaseModel, Field, field_validator
7+
8+
9+
class InsertUserFeedbackRequest(BaseModel):
10+
"""Request to create a user feedback."""
11+
12+
provider_id: UUID
13+
task_id: UUID
14+
context_id: UUID
15+
rating: int = Field(..., description="Rating thats either 1 or -1")
16+
message: str
17+
comment_tags: list[str] | None = None
18+
comment: str | None = None
19+
20+
@field_validator("rating")
21+
@classmethod
22+
def validate_rating(cls, v):
23+
if v not in [1, -1]:
24+
raise ValueError("Rating must be either 1 or -1")
25+
return v

apps/beeai-server/src/beeai_server/application.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from beeai_server.api.routes.llm import router as llm_router
2222
from beeai_server.api.routes.provider import router as provider_router
2323
from beeai_server.api.routes.ui import router as ui_router
24+
from beeai_server.api.routes.user_feedback import router as user_feedback_router
2425
from beeai_server.api.routes.vector_stores import router as vector_stores_router
2526
from beeai_server.bootstrap import bootstrap_dependencies_sync
2627
from beeai_server.configuration import Configuration
@@ -81,6 +82,7 @@ def mount_routes(app: FastAPI):
8182
server_router.include_router(ui_router, prefix="/ui", tags=["ui"])
8283
server_router.include_router(embeddings_router, prefix="/llm", tags=["embeddings"])
8384
server_router.include_router(vector_stores_router, prefix="/vector_stores", tags=["vector_stores"])
85+
server_router.include_router(user_feedback_router, prefix="/user_feedback", tags=["user_feedback"])
8486

8587
app.include_router(server_router, prefix="/api/v1", tags=["provider"])
8688

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from uuid import UUID, uuid4
5+
6+
from pydantic import AwareDatetime, BaseModel, Field, field_validator
7+
8+
from beeai_server.utils.utils import utc_now
9+
10+
11+
class UserFeedback(BaseModel):
12+
id: UUID = Field(default_factory=uuid4)
13+
provider_id: UUID
14+
task_id: UUID
15+
context_id: UUID
16+
rating: int = Field(..., description="Rating thats either 1 or -1")
17+
message: str
18+
comment_tags: list[str] | None = None
19+
comment: str | None = None
20+
created_at: AwareDatetime = Field(default_factory=utc_now)
21+
created_by: UUID
22+
23+
@field_validator("rating")
24+
@classmethod
25+
def validate_rating(cls, v: int) -> int:
26+
if v not in [1, -1]:
27+
raise ValueError("Rating must be either 1 or -1")
28+
return v
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from typing import Protocol, runtime_checkable
5+
6+
from beeai_server.domain.models.user_feedback import UserFeedback
7+
8+
9+
@runtime_checkable
10+
class IUserFeedbackRepository(Protocol):
11+
async def create(self, *, user_feedback: UserFeedback) -> None: ...
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""empty message
5+
6+
Revision ID: b2d91792f9b5
7+
Revises: 644ccacc48f3
8+
Create Date: 2025-08-06 16:12:21.149930
9+
10+
"""
11+
12+
from collections.abc import Sequence
13+
14+
import sqlalchemy as sa
15+
from alembic import op
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = "b2d91792f9b5"
19+
down_revision: str | None = "644ccacc48f3"
20+
branch_labels: str | Sequence[str] | None = None
21+
depends_on: str | Sequence[str] | None = None
22+
23+
24+
def upgrade() -> None:
25+
"""Upgrade schema."""
26+
op.create_table(
27+
"user_feedback",
28+
sa.Column("id", sa.UUID(), nullable=False),
29+
sa.Column("provider_id", sa.UUID(), nullable=False),
30+
sa.Column("task_id", sa.UUID(), nullable=False),
31+
sa.Column("context_id", sa.UUID(), nullable=False),
32+
sa.Column("message", sa.Text(), nullable=False),
33+
sa.Column("comment", sa.Text(), nullable=True),
34+
sa.Column("comment_tags", sa.ARRAY(sa.Text()), nullable=True),
35+
sa.Column("rating", sa.Integer(), nullable=False),
36+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
37+
sa.Column("created_by", sa.UUID(), nullable=False),
38+
sa.ForeignKeyConstraint(["provider_id"], ["providers.id"], ondelete="CASCADE"),
39+
sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="CASCADE"),
40+
sa.PrimaryKeyConstraint("id"),
41+
)
42+
op.create_check_constraint("user_feedback_rating_check", "user_feedback", "rating IN (1, -1)")
43+
44+
45+
def downgrade() -> None:
46+
op.execute("DROP TABLE IF EXISTS user_feedback CASCADE")
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from kink import inject
5+
from sqlalchemy import ARRAY, CheckConstraint, Column, DateTime, Integer, Table, Text
6+
from sqlalchemy import UUID as SQL_UUID
7+
from sqlalchemy.ext.asyncio import AsyncConnection
8+
9+
from beeai_server.domain.models.user_feedback import UserFeedback
10+
from beeai_server.domain.repositories.user_feedback import IUserFeedbackRepository
11+
from beeai_server.infrastructure.persistence.repositories.db_metadata import metadata
12+
13+
user_feedback_table = Table(
14+
"user_feedback",
15+
metadata,
16+
Column("id", SQL_UUID, primary_key=True),
17+
Column("provider_id", SQL_UUID, nullable=False),
18+
Column("task_id", SQL_UUID, nullable=False),
19+
Column("context_id", SQL_UUID, nullable=False),
20+
Column("rating", Integer, nullable=False),
21+
Column("message", Text, nullable=False),
22+
Column("comment", Text, nullable=True),
23+
Column("comment_tags", ARRAY(Text), nullable=True),
24+
Column("created_at", DateTime(timezone=True), nullable=False),
25+
Column("created_by", SQL_UUID, nullable=False),
26+
CheckConstraint("rating IN (1, -1)", name="rating_check"),
27+
)
28+
29+
30+
@inject
31+
class SqlAlchemyUserFeedbackRepository(IUserFeedbackRepository):
32+
def __init__(self, connection: AsyncConnection):
33+
self.connection = connection
34+
35+
async def create(self, *, user_feedback: UserFeedback) -> None:
36+
query = user_feedback_table.insert().values(
37+
id=user_feedback.id,
38+
provider_id=user_feedback.provider_id,
39+
task_id=user_feedback.task_id,
40+
context_id=user_feedback.context_id,
41+
rating=user_feedback.rating,
42+
comment=user_feedback.comment,
43+
comment_tags=user_feedback.comment_tags,
44+
message=user_feedback.message,
45+
created_at=user_feedback.created_at,
46+
created_by=user_feedback.created_by,
47+
)
48+
await self.connection.execute(query)

apps/beeai-server/src/beeai_server/infrastructure/persistence/unit_of_work.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
from beeai_server.domain.repositories.file import IFileRepository
1212
from beeai_server.domain.repositories.provider import IProviderRepository
1313
from beeai_server.domain.repositories.user import IUserRepository
14+
from beeai_server.domain.repositories.user_feedback import IUserFeedbackRepository
1415
from beeai_server.domain.repositories.vector_store import IVectorDatabaseRepository, IVectorStoreRepository
1516
from beeai_server.infrastructure.persistence.repositories.env import SqlAlchemyEnvVariableRepository
1617
from beeai_server.infrastructure.persistence.repositories.file import SqlAlchemyFileRepository
1718
from beeai_server.infrastructure.persistence.repositories.provider import SqlAlchemyProviderRepository
1819
from beeai_server.infrastructure.persistence.repositories.user import SqlAlchemyUserRepository
20+
from beeai_server.infrastructure.persistence.repositories.user_feedback import SqlAlchemyUserFeedbackRepository
1921
from beeai_server.infrastructure.persistence.repositories.vector_store import SqlAlchemyVectorStoreRepository
2022
from beeai_server.infrastructure.vector_database.vector_db import VectorDatabaseRepository
2123
from beeai_server.service_layer.unit_of_work import IUnitOfWork, IUnitOfWorkFactory
@@ -33,6 +35,7 @@ class SQLAlchemyUnitOfWork(IUnitOfWork):
3335
users: IUserRepository
3436
vector_stores: IVectorStoreRepository
3537
vector_database: IVectorDatabaseRepository
38+
user_feedback: IUserFeedbackRepository
3639

3740
def __init__(self, engine: AsyncEngine, config: Configuration) -> None:
3841
self._engine: AsyncEngine = engine
@@ -55,6 +58,7 @@ async def __aenter__(self) -> Self:
5558
self.vector_database = VectorDatabaseRepository(
5659
self._connection, schema_name=self._config.persistence.vector_db_schema
5760
)
61+
self.user_feedback = SqlAlchemyUserFeedbackRepository(self._connection)
5862

5963
except Exception as e:
6064
if self._connection:
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import logging
5+
from uuid import UUID
6+
7+
from kink import inject
8+
9+
from beeai_server.domain.models.user import User
10+
from beeai_server.domain.models.user_feedback import UserFeedback
11+
from beeai_server.service_layer.unit_of_work import IUnitOfWorkFactory
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
@inject
17+
class UserFeedbackService:
18+
def __init__(self, uow: IUnitOfWorkFactory):
19+
self._uow = uow
20+
21+
async def create_user_feedback(
22+
self,
23+
*,
24+
provider_id: UUID,
25+
rating: int,
26+
comment: str | None = None,
27+
comment_tags: list[str] | None = None,
28+
message: str,
29+
task_id: UUID,
30+
context_id: UUID,
31+
user: User,
32+
):
33+
async with self._uow() as uow:
34+
user_feedback = UserFeedback(
35+
provider_id=provider_id,
36+
rating=rating,
37+
comment=comment,
38+
comment_tags=comment_tags,
39+
message=message,
40+
task_id=task_id,
41+
context_id=context_id,
42+
created_by=user.id,
43+
)
44+
await uow.user_feedback.create(user_feedback=user_feedback)
45+
await uow.commit()
46+
return user_feedback

0 commit comments

Comments
 (0)