Skip to content
Open
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,46 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""add_user_team_favorites_table

Revision ID: 2a3b4c5d6e7f
Revises: 1a2b3c4d5e6f
Create Date: 2025-07-16 10:00:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '2a3b4c5d6e7f'
down_revision = '1a2b3c4d5e6f'
branch_labels = None
depends_on = None


def upgrade() -> None:
# Create user_team_favorites table
op.create_table(
'user_team_favorites',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('team_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci'
)
op.create_index('ix_user_team_favorites_id', 'user_team_favorites', ['id'], unique=False)
op.create_index('ix_user_team_favorites_user_id', 'user_team_favorites', ['user_id'], unique=False)
op.create_index('ix_user_team_favorites_team_id', 'user_team_favorites', ['team_id'], unique=False)
op.create_index('idx_user_team_favorite', 'user_team_favorites', ['user_id', 'team_id'], unique=True)


def downgrade() -> None:
op.drop_index('idx_user_team_favorite', table_name='user_team_favorites')
op.drop_index('ix_user_team_favorites_team_id', table_name='user_team_favorites')
op.drop_index('ix_user_team_favorites_user_id', table_name='user_team_favorites')
op.drop_index('ix_user_team_favorites_id', table_name='user_team_favorites')
op.drop_table('user_team_favorites')
102 changes: 102 additions & 0 deletions backend/app/api/endpoints/adapter/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#
# SPDX-License-Identifier: Apache-2.0

from typing import Any, Dict, List

from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session

Expand All @@ -24,6 +26,7 @@
)
from app.services.adapters.team_kinds import team_kinds_service
from app.services.shared_team import shared_team_service
from app.services.team_favorite import team_favorite_service

router = APIRouter()

Expand All @@ -40,6 +43,14 @@ def list_teams(
items = team_kinds_service.get_user_teams(
db=db, user_id=current_user.id, skip=skip, limit=limit
)

# Add is_favorited field to each team
favorite_team_ids = team_favorite_service.get_user_favorite_team_ids(
db=db, user_id=current_user.id
)
for item in items:
item["is_favorited"] = item["id"] in favorite_team_ids

if page == 1 and len(items) < limit:
total = len(items)
else:
Expand Down Expand Up @@ -139,3 +150,94 @@ def join_shared_team(
return shared_team_service.join_shared_team(
db=db, share_token=request.share_token, user_id=current_user.id
)


@router.post("/{team_id}/favorite")
def add_team_to_favorites(
team_id: int,
current_user: User = Depends(security.get_current_user),
db: Session = Depends(get_db),
):
"""Add a team to user's favorites"""
return team_favorite_service.add_favorite(
db=db, team_id=team_id, user_id=current_user.id
)


@router.delete("/{team_id}/favorite")
def remove_team_from_favorites(
team_id: int,
current_user: User = Depends(security.get_current_user),
db: Session = Depends(get_db),
):
"""Remove a team from user's favorites"""
return team_favorite_service.remove_favorite(
db=db, team_id=team_id, user_id=current_user.id
)


@router.get("/showcase/recommended", response_model=List[Dict[str, Any]])
def get_recommended_teams(
limit: int = Query(6, ge=1, le=20, description="Max teams to return"),
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Get recommended teams (is_recommended=true)"""
from app.schemas.kind import Team

# Get all teams where isRecommended is true
teams = db.query(Kind).filter(Kind.kind == "Team", Kind.is_active == True).all()

recommended_teams = []
favorite_team_ids = team_favorite_service.get_user_favorite_team_ids(
db=db, user_id=current_user.id
)

for team in teams:
team_crd = Team.model_validate(team.json)
if team_crd.spec.isRecommended:
team_dict = team_kinds_service._convert_to_team_dict(team, db, team.user_id)
team_dict["is_favorited"] = team.id in favorite_team_ids
recommended_teams.append(team_dict)
if len(recommended_teams) >= limit:
break

return recommended_teams


@router.get("/showcase/favorites", response_model=List[Dict[str, Any]])
def get_favorite_teams(
limit: int = Query(6, ge=1, le=20, description="Max teams to return"),
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""Get user's favorite teams"""
from app.models.user_team_favorite import UserTeamFavorite

# Get user's favorite team IDs
favorites = (
db.query(UserTeamFavorite)
.filter(UserTeamFavorite.user_id == current_user.id)
.order_by(UserTeamFavorite.created_at.desc())
.limit(limit)
.all()
)

favorite_teams = []
for favorite in favorites:
team = (
db.query(Kind)
.filter(
Kind.id == favorite.team_id,
Kind.kind == "Team",
Kind.is_active == True,
)
.first()
)

if team:
team_dict = team_kinds_service._convert_to_team_dict(team, db, team.user_id)
team_dict["is_favorited"] = True
favorite_teams.append(team_dict)

return favorite_teams
3 changes: 2 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
# Do NOT import Base here to avoid conflicts with app.db.base.Base
# All models should import Base directly from app.db.base
from app.models.user import User
from app.models.user_team_favorite import UserTeamFavorite

__all__ = ["User", "Kind", "Subtask", "SharedTeam", "SkillBinary"]
__all__ = ["User", "Kind", "Subtask", "SharedTeam", "SkillBinary", "UserTeamFavorite"]
25 changes: 25 additions & 0 deletions backend/app/models/user_team_favorite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

from datetime import datetime

from sqlalchemy import Column, DateTime, Index, Integer

from app.db.base import Base


class UserTeamFavorite(Base):
"""User team favorite model for maintaining user-team favorite relationships"""

__tablename__ = "user_team_favorites"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, nullable=False, index=True) # User who favorited the team
team_id = Column(Integer, nullable=False, index=True) # Team that was favorited
created_at = Column(DateTime, default=datetime.now)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use datetime.utcnow or timezone-aware datetime instead of datetime.now.

Using datetime.now stores local server time, which can cause inconsistencies across different server timezones. Consider using datetime.utcnow for consistency with typical database practices.

-    created_at = Column(DateTime, default=datetime.now)
+    created_at = Column(DateTime, default=datetime.utcnow)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
created_at = Column(DateTime, default=datetime.now)
created_at = Column(DateTime, default=datetime.utcnow)
🤖 Prompt for AI Agents
In backend/app/models/user_team_favorite.py around line 17, the Column default
uses datetime.now which records local server time; replace it with a UTC or
timezone-aware default — e.g. use default=datetime.utcnow (imported from
datetime) or prefer a timezone-aware value like default=lambda:
datetime.now(timezone.utc) after importing timezone, or use SQLAlchemy's
func.now() to rely on DB-side UTC timestamps; update imports accordingly.


__table_args__ = (
Index("idx_user_team_favorite", "user_id", "team_id", unique=True),
{"mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
)
2 changes: 2 additions & 0 deletions backend/app/schemas/kind.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ class TeamSpec(BaseModel):

members: List[TeamMember]
collaborationModel: str # pipeline、route、coordinate、collaborate
icon: Optional[str] = None # Lucide icon name (e.g., "Users", "Bot", "Zap")
isRecommended: bool = False # Whether this team is recommended


class TeamStatus(Status):
Expand Down
8 changes: 8 additions & 0 deletions backend/app/schemas/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class TeamBase(BaseModel):
bots: List[BotInfo]
workflow: Optional[dict[str, Any]] = None
is_active: bool = True
icon: Optional[str] = None # Lucide icon name (e.g., "Users", "Bot", "Zap")
is_recommended: bool = False # Whether this team is recommended


class TeamCreate(TeamBase):
Expand All @@ -57,6 +59,8 @@ class TeamUpdate(BaseModel):
bots: Optional[List[BotInfo]] = None
workflow: Optional[dict[str, Any]] = None
is_active: Optional[bool] = None
icon: Optional[str] = None # Lucide icon name
is_recommended: Optional[bool] = None # Whether this team is recommended


class TeamInDB(TeamBase):
Expand All @@ -69,6 +73,7 @@ class TeamInDB(TeamBase):
user: Optional[dict[str, Any]] = None
share_status: int = 0 # 0-private, 1-sharing, 2-shared from others
agent_type: Optional[str] = None # agno, claude, dify, etc.
is_favorited: bool = False # Whether current user has favorited this team

class Config:
from_attributes = True
Expand All @@ -86,6 +91,9 @@ class TeamDetail(BaseModel):
updated_at: datetime
user: Optional[UserInDB] = None
share_status: int = 0 # 0-private, 1-sharing, 2-shared from others
icon: Optional[str] = None # Lucide icon name
is_recommended: bool = False # Whether this team is recommended
is_favorited: bool = False # Whether current user has favorited this team

class Config:
from_attributes = True
Expand Down
17 changes: 16 additions & 1 deletion backend/app/services/adapters/team_kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ def create_with_user(
# Create Team JSON
team_json = {
"kind": "Team",
"spec": {"members": members, "collaborationModel": collaboration_model},
"spec": {
"members": members,
"collaborationModel": collaboration_model,
"icon": getattr(obj_in, "icon", None),
"isRecommended": getattr(obj_in, "is_recommended", False),
},
"status": {"state": "Available"},
"metadata": {"name": obj_in.name, "namespace": "default"},
"apiVersion": "agent.wecode.io/v1",
Expand Down Expand Up @@ -511,6 +516,14 @@ def update_with_user(

team_crd.spec.collaborationModel = collaboration_model

# Handle icon update
if "icon" in update_data:
team_crd.spec.icon = update_data["icon"]

# Handle is_recommended update
if "is_recommended" in update_data:
team_crd.spec.isRecommended = update_data["is_recommended"]

Comment on lines +519 to +526
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid persisting None into spec.isRecommended

TeamUpdate.is_recommended is Optional[bool], but Team.spec.isRecommended is effectively a boolean flag. With the current code, a client sending "is_recommended": null will write None into the spec, which can break assumptions that this is always a boolean.

Consider only updating when a concrete boolean is provided:

-    # Handle is_recommended update
-    if "is_recommended" in update_data:
-        team_crd.spec.isRecommended = update_data["is_recommended"]
+    # Handle is_recommended update (ignore explicit nulls)
+    if "is_recommended" in update_data and update_data["is_recommended"] is not None:
+        team_crd.spec.isRecommended = update_data["is_recommended"]

You can decide separately whether you want a way to “reset” this flag (e.g., treat None as False explicitly).

🤖 Prompt for AI Agents
In backend/app/services/adapters/team_kinds.py around lines 519-526, the code
unconditionally assigns update_data["is_recommended"] to
team_crd.spec.isRecommended which allows None to be persisted; change the logic
to only update team_crd.spec.isRecommended when update_data["is_recommended"] is
a concrete boolean (i.e., check "is_recommended" in update_data and
update_data["is_recommended"] is not None or
isinstance(update_data["is_recommended"], bool)) so None values are ignored (or,
if you prefer a reset behavior, explicitly set to False when the value is None).

# Save the updated team CRD
team.json = team_crd.model_dump(mode="json")
team.updated_at = datetime.now()
Expand Down Expand Up @@ -886,6 +899,8 @@ def _convert_to_team_dict(
"created_at": team.created_at,
"updated_at": team.updated_at,
"agent_type": agent_type, # Add agent_type field
"icon": team_crd.spec.icon, # Lucide icon name
"is_recommended": team_crd.spec.isRecommended, # Whether this team is recommended
}

def _get_bot_summary(self, bot: Kind, db: Session, user_id: int) -> Dict[str, Any]:
Expand Down
91 changes: 91 additions & 0 deletions backend/app/services/team_favorite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

from typing import Any, Dict, List, Set

from fastapi import HTTPException
from sqlalchemy.orm import Session

from app.models.kind import Kind
from app.models.user_team_favorite import UserTeamFavorite


class TeamFavoriteService:
"""Service for team favorite operations"""

def add_favorite(
self, db: Session, *, team_id: int, user_id: int
) -> Dict[str, Any]:
"""Add a team to user's favorites"""
# Check if team exists
team = (
db.query(Kind)
.filter(Kind.id == team_id, Kind.kind == "Team", Kind.is_active == True)
.first()
)

if not team:
raise HTTPException(status_code=404, detail="Team not found")

# Check if already favorited
existing = (
db.query(UserTeamFavorite)
.filter(
UserTeamFavorite.user_id == user_id, UserTeamFavorite.team_id == team_id
)
.first()
)

if existing:
return {"message": "Team already in favorites", "is_favorited": True}

# Create favorite record
favorite = UserTeamFavorite(user_id=user_id, team_id=team_id)
db.add(favorite)
db.commit()

return {"message": "Team added to favorites", "is_favorited": True}
Comment on lines +40 to +48
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for UserTeamFavorite model and foreign key definitions
echo "=== Searching for UserTeamFavorite model ==="
fd -t py -i "*favorite*" | head -20

echo -e "\n=== Searching for ForeignKey references in Python files ==="
rg -n "ForeignKey|foreign_key" --type py -i "team_id|user_team_favorite" -A 2 -B 2

echo -e "\n=== Searching for model class definitions ==="
rg -n "class UserTeamFavorite\|class.*Favorite" --type py -A 10

Repository: wecode-ai/Wegent

Length of output: 466


🏁 Script executed:

#!/bin/bash
# Search for the file under review
echo "=== Looking for team_favorite.py ==="
find . -name "team_favorite.py" -type f

echo -e "\n=== Looking for model files with 'favorite' in name ==="
find . -name "*favorite*.py" -type f

echo -e "\n=== Searching for UserTeamFavorite class definition ==="
rg -n "class UserTeamFavorite" --type python -A 15

echo -e "\n=== Searching for ForeignKey on team_id ==="
rg -n "team_id.*ForeignKey\|ForeignKey.*team_id" --type python -B 2 -A 2

echo -e "\n=== General search for ForeignKey usage ==="
rg -n "ForeignKey" --type python -B 1 -A 1 | head -50

Repository: wecode-ai/Wegent

Length of output: 146


🏁 Script executed:

#!/bin/bash
# Read the file under review
echo "=== Reading backend/app/services/team_favorite.py ==="
cat -n backend/app/services/team_favorite.py

echo -e "\n=== List files in backend/app ==="
ls -la backend/app/

echo -e "\n=== Look for database model files ==="
find backend/app -name "*.py" -path "*/models/*" -o -name "*model*.py" | head -10

Repository: wecode-ai/Wegent

Length of output: 4557


🏁 Script executed:

#!/bin/bash
# Read the UserTeamFavorite model
echo "=== Reading backend/app/models/user_team_favorite.py ==="
cat -n backend/app/models/user_team_favorite.py

echo -e "\n=== Look for migration or schema files ==="
find backend -name "*migration*" -o -name "*alembic*" -o -name "*schema*" | head -20

echo -e "\n=== Check db directory ==="
ls -la backend/app/db/

Repository: wecode-ai/Wegent

Length of output: 1531


🏁 Script executed:

#!/bin/bash
# Read the migration file
echo "=== Reading Alembic migration file ==="
cat -n backend/alembic/versions/0c086b93f8b9_initial_migration.py

Repository: wecode-ai/Wegent

Length of output: 6472


🏁 Script executed:

#!/bin/bash
# List all migration files
echo "=== All migration files ==="
ls -la backend/alembic/versions/

echo -e "\n=== Search for user_team_favorites in all migration files ==="
grep -r "user_team_favorites" backend/alembic/

echo -e "\n=== Search for user_team_favorites in all Python files ==="
grep -r "user_team_favorites" backend/app/ --include="*.py" | grep -i "create\|table\|migration"

Repository: wecode-ai/Wegent

Length of output: 2408


🏁 Script executed:

#!/bin/bash
# Read the migration file for user_team_favorites
cat -n backend/alembic/versions/2a3b4c5d6e7f_add_user_team_favorites_table.py

Repository: wecode-ai/Wegent

Length of output: 2060


No foreign key constraint exists on user_team_favorites.team_id—the review concern is valid.

The migration file (backend/alembic/versions/2a3b4c5d6e7f_add_user_team_favorites_table.py) creates the table with only indexes and a unique constraint on (user_id, team_id), but no ForeignKey constraint on team_id. This confirms the race condition scenario: a team could be deleted after the existence check at lines 22–26 but before the commit at line 46, resulting in an orphaned favorite record pointing to a non-existent team.

Adding a foreign key constraint to enforce referential integrity would prevent this edge case.

🤖 Prompt for AI Agents
In backend/app/services/team_favorite.py lines 40–48 and related migration, add
a DB-level foreign key on user_team_favorites.team_id referencing teams.id to
enforce referential integrity and prevent orphaned favorites: update the Alembic
migration (or create a new revision) to ALTER TABLE user_team_favorites ADD
CONSTRAINT fk_user_team_favorites_team_id FOREIGN KEY (team_id) REFERENCES
teams(id) (optionally WITH ON DELETE CASCADE); also update the SQLAlchemy model
to declare team_id = Column(Integer, ForeignKey("teams.id")) and, to handle
existing deployments, add a new migration that either backfills/removes orphan
rows before adding the constraint or uses the appropriate ALTER ... NOT VALID /
VALIDATE pattern; finally, in the service wrap the db.add/db.commit in a
try/except catching IntegrityError to rollback and return a clear error if the
referenced team was concurrently deleted.


def remove_favorite(
self, db: Session, *, team_id: int, user_id: int
) -> Dict[str, Any]:
"""Remove a team from user's favorites"""
favorite = (
db.query(UserTeamFavorite)
.filter(
UserTeamFavorite.user_id == user_id, UserTeamFavorite.team_id == team_id
)
.first()
)

if not favorite:
return {"message": "Team not in favorites", "is_favorited": False}

db.delete(favorite)
db.commit()

return {"message": "Team removed from favorites", "is_favorited": False}

def get_user_favorite_team_ids(self, db: Session, *, user_id: int) -> Set[int]:
"""Get set of team IDs that user has favorited"""
favorites = (
db.query(UserTeamFavorite.team_id)
.filter(UserTeamFavorite.user_id == user_id)
.all()
)
return {f.team_id for f in favorites}

def is_team_favorited(self, db: Session, *, team_id: int, user_id: int) -> bool:
"""Check if a team is in user's favorites"""
favorite = (
db.query(UserTeamFavorite)
.filter(
UserTeamFavorite.user_id == user_id, UserTeamFavorite.team_id == team_id
)
.first()
)
return favorite is not None


team_favorite_service = TeamFavoriteService()
Loading
Loading