fixes
This commit is contained in:
33
.context.md
Normal file
33
.context.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Project Context: AI Char Bot
|
||||
|
||||
## Overview
|
||||
Python backend project using FastAPI and MongoDB (Motor).
|
||||
Root: `/Users/xds/develop/py projects/ai-char-bot`
|
||||
|
||||
## Architecture
|
||||
- **API Layer**: `api/endpoints` (FastAPI routers).
|
||||
- **Service Layer**: `api/service` (Business logic).
|
||||
- **Data Layer**: `repos` (DAOs and Repositories).
|
||||
- **Models**: `models` (Domain models) and `api/models` (Request/Response DTOs).
|
||||
- **Adapters**: `adapters` (External services like S3, Google Gemini).
|
||||
|
||||
## Coding Standards & Preferences
|
||||
- **Type Hinting**: Use `Type | None` instead of `Optional[Type]` (Python 3.10+ style).
|
||||
- **Async/Await**: Extensive use of `asyncio` and asynchronous DB drivers.
|
||||
- **Error Handling**:
|
||||
- Repositories should return `None` if an entity is not found (e.g., `toggle_like`).
|
||||
- Services/Routers handle `HTTPException`.
|
||||
|
||||
## Key Features & Implementation Details
|
||||
- **Generations**:
|
||||
- Managed by `GenerationService` and `GenerationRepo`.
|
||||
- `toggle_like` returns `bool | None` (True=Liked, False=Unliked, None=Not Found).
|
||||
- `get_generations` requires `current_user_id` to correctly calculate `is_liked`.
|
||||
- **Ideas**:
|
||||
- Managed by `IdeaService` and `IdeaRepo`.
|
||||
- Can have linked generations.
|
||||
- When fetching generations for an idea, ensure `current_user_id` is passed to `GenerationService`.
|
||||
|
||||
## Recent Changes
|
||||
- Refactored `toggle_like` to handle non-existent generations and return `bool | None`.
|
||||
- Updated `IdeaRouter` to pass `current_user_id` when fetching generations to ensure `is_liked` flag is correct.
|
||||
33
.gemini/AGENTS.md
Normal file
33
.gemini/AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Project Context: AI Char Bot
|
||||
|
||||
## Overview
|
||||
Python backend project using FastAPI and MongoDB (Motor).
|
||||
Root: `/Users/xds/develop/py projects/ai-char-bot`
|
||||
|
||||
## Architecture
|
||||
- **API Layer**: `api/endpoints` (FastAPI routers).
|
||||
- **Service Layer**: `api/service` (Business logic).
|
||||
- **Data Layer**: `repos` (DAOs and Repositories).
|
||||
- **Models**: `models` (Domain models) and `api/models` (Request/Response DTOs).
|
||||
- **Adapters**: `adapters` (External services like S3, Google Gemini).
|
||||
|
||||
## Coding Standards & Preferences
|
||||
- **Type Hinting**: Use `Type | None` instead of `Optional[Type]` (Python 3.10+ style).
|
||||
- **Async/Await**: Extensive use of `asyncio` and asynchronous DB drivers.
|
||||
- **Error Handling**:
|
||||
- Repositories should return `None` if an entity is not found (e.g., `toggle_like`).
|
||||
- Services/Routers handle `HTTPException`.
|
||||
|
||||
## Key Features & Implementation Details
|
||||
- **Generations**:
|
||||
- Managed by `GenerationService` and `GenerationRepo`.
|
||||
- `toggle_like` returns `bool | None` (True=Liked, False=Unliked, None=Not Found).
|
||||
- `get_generations` requires `current_user_id` to correctly calculate `is_liked`.
|
||||
- **Ideas**:
|
||||
- Managed by `IdeaService` and `IdeaRepo`.
|
||||
- Can have linked generations.
|
||||
- When fetching generations for an idea, ensure `current_user_id` is passed to `GenerationService`.
|
||||
|
||||
## Recent Changes
|
||||
- Refactored `toggle_like` to handle non-existent generations and return `bool | None`.
|
||||
- Updated `IdeaRouter` to pass `current_user_id` when fetching generations to ensure `is_liked` flag is correct.
|
||||
@@ -66,6 +66,7 @@ async def get_generations(
|
||||
character_id: Optional[str] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
only_liked: bool = False,
|
||||
generation_service: GenerationService = Depends(get_generation_service),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
project_id: Optional[str] = Depends(get_project_id),
|
||||
@@ -75,13 +76,16 @@ async def get_generations(
|
||||
|
||||
# If project_id is set, we don't filter by user to show all project-wide generations
|
||||
created_by_filter = None if project_id else str(current_user["_id"])
|
||||
only_liked_by = str(current_user["_id"]) if only_liked else None
|
||||
|
||||
return await generation_service.get_generations(
|
||||
character_id=character_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
created_by=created_by_filter,
|
||||
project_id=project_id
|
||||
project_id=project_id,
|
||||
only_liked_by=only_liked_by,
|
||||
current_user_id=str(current_user["_id"])
|
||||
)
|
||||
|
||||
|
||||
@@ -150,7 +154,7 @@ async def get_generation_group(
|
||||
generation_service: GenerationService = Depends(get_generation_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
return await generation_service.get_generations_by_group(group_id)
|
||||
return await generation_service.get_generations_by_group(group_id, current_user_id=str(current_user["_id"]))
|
||||
|
||||
|
||||
@router.get("/{generation_id}", response_model=GenerationResponse)
|
||||
@@ -159,7 +163,7 @@ async def get_generation(
|
||||
generation_service: GenerationService = Depends(get_generation_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> GenerationResponse:
|
||||
gen = await generation_service.get_generation(generation_id)
|
||||
gen = await generation_service.get_generation(generation_id, current_user_id=str(current_user["_id"]))
|
||||
if not gen:
|
||||
raise HTTPException(status_code=404, detail="Generation not found")
|
||||
|
||||
@@ -176,6 +180,18 @@ async def get_generation(
|
||||
return gen
|
||||
|
||||
|
||||
@router.post("/{generation_id}/like", response_model=dict)
|
||||
async def toggle_like(
|
||||
generation_id: str,
|
||||
generation_service: GenerationService = Depends(get_generation_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
is_liked = await generation_service.toggle_like(generation_id, str(current_user["_id"]))
|
||||
if is_liked is None:
|
||||
raise HTTPException(status_code=404, detail="Generation not found")
|
||||
return {"is_liked": is_liked}
|
||||
|
||||
|
||||
@router.post("/import", response_model=GenerationResponse)
|
||||
async def import_external_generation(
|
||||
request: Request,
|
||||
|
||||
@@ -68,18 +68,10 @@ async def get_idea_generations(
|
||||
idea_id: str,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
generation_service: GenerationService = Depends(get_generation_service)
|
||||
generation_service: GenerationService = Depends(get_generation_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
# Depending on how generation service implements filtering by idea_id.
|
||||
# We might need to update generation_service to support getting by idea_id directly
|
||||
# or ensure generic get_generations supports it.
|
||||
# Looking at generation_router.py, get_generations doesn't have idea_id arg?
|
||||
# Let's check generation_service.get_generations signature again.
|
||||
# It has: (character_id, limit, offset, user_id, project_id). NO IDEA_ID.
|
||||
# I need to update GenerationService.get_generations too!
|
||||
|
||||
# For now, let's assume I will update it.
|
||||
return await generation_service.get_generations(idea_id=idea_id, limit=limit, offset=offset)
|
||||
return await generation_service.get_generations(idea_id=idea_id, limit=limit, offset=offset, current_user_id=str(current_user["_id"]))
|
||||
|
||||
@router.post("/{idea_id}/generations/{generation_id}")
|
||||
async def add_generation_to_idea(
|
||||
|
||||
@@ -50,6 +50,8 @@ class GenerationResponse(BaseModel):
|
||||
created_by: Optional[str] = None
|
||||
generation_group_id: Optional[str] = None
|
||||
idea_id: Optional[str] = None
|
||||
likes_count: int = 0
|
||||
is_liked: bool = False
|
||||
created_at: datetime = datetime.now(UTC)
|
||||
updated_at: datetime = datetime.now(UTC)
|
||||
|
||||
|
||||
@@ -100,29 +100,40 @@ class GenerationService:
|
||||
return await asyncio.to_thread(self.gemini.generate_text, prompt=technical_prompt, images_list=images)
|
||||
|
||||
async def get_generations(self, **kwargs) -> GenerationsResponse:
|
||||
current_user_id = kwargs.pop('current_user_id', None)
|
||||
generations = await self.dao.generations.get_generations(**kwargs)
|
||||
total_count = await self.dao.generations.count_generations(
|
||||
character_id=kwargs.get('character_id'),
|
||||
created_by=kwargs.get('created_by'),
|
||||
project_id=kwargs.get('project_id'),
|
||||
idea_id=kwargs.get('idea_id')
|
||||
idea_id=kwargs.get('idea_id'),
|
||||
only_liked_by=kwargs.get('only_liked_by')
|
||||
)
|
||||
return GenerationsResponse(
|
||||
generations=[GenerationResponse(**gen.model_dump()) for gen in generations],
|
||||
generations=[self._map_to_response(gen, current_user_id) for gen in generations],
|
||||
total_count=total_count
|
||||
)
|
||||
|
||||
async def get_generation(self, generation_id: str) -> Optional[GenerationResponse]:
|
||||
async def get_generation(self, generation_id: str, current_user_id: Optional[str] = None) -> Optional[GenerationResponse]:
|
||||
gen = await self.dao.generations.get_generation(generation_id)
|
||||
return GenerationResponse(**gen.model_dump()) if gen else None
|
||||
return self._map_to_response(gen, current_user_id) if gen else None
|
||||
|
||||
async def get_generations_by_group(self, group_id: str) -> GenerationGroupResponse:
|
||||
async def toggle_like(self, generation_id: str, user_id: str) -> bool | None:
|
||||
return await self.dao.generations.toggle_like(generation_id, user_id)
|
||||
|
||||
async def get_generations_by_group(self, group_id: str, current_user_id: Optional[str] = None) -> GenerationGroupResponse:
|
||||
generations = await self.dao.generations.get_generations_by_group(group_id)
|
||||
return GenerationGroupResponse(
|
||||
generation_group_id=group_id,
|
||||
generations=[GenerationResponse(**gen.model_dump()) for gen in generations]
|
||||
generations=[self._map_to_response(gen, current_user_id) for gen in generations]
|
||||
)
|
||||
|
||||
def _map_to_response(self, gen: Generation, current_user_id: Optional[str] = None) -> GenerationResponse:
|
||||
res = GenerationResponse(**gen.model_dump())
|
||||
res.likes_count = len(gen.liked_by) if gen.liked_by else 0
|
||||
res.is_liked = current_user_id in gen.liked_by if current_user_id and gen.liked_by else False
|
||||
return res
|
||||
|
||||
async def get_running_generations(self, user_id: Optional[str] = None, project_id: Optional[str] = None) -> List[Generation]:
|
||||
return await self.dao.generations.get_generations(status=GenerationStatus.RUNNING, created_by=user_id, project_id=project_id)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class Generation(BaseModel):
|
||||
created_by: Optional[str] = None # Stores User ID (Telegram ID or Web User ObjectId)
|
||||
project_id: Optional[str] = None
|
||||
idea_id: Optional[str] = None
|
||||
liked_by: List[str] = Field(default_factory=list)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ class GenerationRepo:
|
||||
return Generation(**res)
|
||||
|
||||
async def get_generations(self, character_id: Optional[str] = None, status: Optional[GenerationStatus] = None,
|
||||
limit: int = 10, offset: int = 0, created_by: Optional[str] = None, project_id: Optional[str] = None, idea_id: Optional[str] = None) -> List[Generation]:
|
||||
limit: int = 10, offset: int = 0, created_by: Optional[str] = None, project_id: Optional[str] = None,
|
||||
idea_id: Optional[str] = None, only_liked_by: Optional[str] = None) -> List[Generation]:
|
||||
|
||||
filter: dict[str, Any] = {"is_deleted": False}
|
||||
if character_id is not None:
|
||||
@@ -43,6 +44,8 @@ class GenerationRepo:
|
||||
filter["project_id"] = project_id
|
||||
if idea_id is not None:
|
||||
filter["idea_id"] = idea_id
|
||||
if only_liked_by is not None:
|
||||
filter["liked_by"] = only_liked_by
|
||||
|
||||
# If fetching for an idea, sort by created_at ascending (cronological)
|
||||
# Otherwise typically descending (newest first)
|
||||
@@ -57,7 +60,8 @@ class GenerationRepo:
|
||||
return generations
|
||||
|
||||
async def count_generations(self, character_id: Optional[str] = None, status: Optional[GenerationStatus] = None,
|
||||
album_id: Optional[str] = None, created_by: Optional[str] = None, project_id: Optional[str] = None, idea_id: Optional[str] = None) -> int:
|
||||
album_id: Optional[str] = None, created_by: Optional[str] = None, project_id: Optional[str] = None,
|
||||
idea_id: Optional[str] = None, only_liked_by: Optional[str] = None) -> int:
|
||||
args = {}
|
||||
if character_id is not None:
|
||||
args["linked_character_id"] = character_id
|
||||
@@ -73,6 +77,8 @@ class GenerationRepo:
|
||||
args["idea_id"] = idea_id
|
||||
if album_id is not None:
|
||||
args["album_id"] = album_id
|
||||
if only_liked_by is not None:
|
||||
args["liked_by"] = only_liked_by
|
||||
return await self.collection.count_documents(args)
|
||||
|
||||
async def get_generations_by_ids(self, generation_ids: List[str]) -> List[Generation]:
|
||||
@@ -94,6 +100,37 @@ class GenerationRepo:
|
||||
async def update_generation(self, generation: Generation, ):
|
||||
res = await self.collection.update_one({"_id": ObjectId(generation.id)}, {"$set": generation.model_dump()})
|
||||
|
||||
async def toggle_like(self, generation_id: str, user_id: str) -> bool | None:
|
||||
"""
|
||||
Toggles like for a user on a generation.
|
||||
Returns True if liked, False if unliked, None if generation not found.
|
||||
"""
|
||||
if not ObjectId.is_valid(generation_id):
|
||||
return None
|
||||
|
||||
oid = ObjectId(generation_id)
|
||||
|
||||
# Check if generation exists
|
||||
gen = await self.collection.find_one({"_id": oid}, {"liked_by": 1})
|
||||
|
||||
if not gen:
|
||||
return None
|
||||
|
||||
if user_id in gen.get("liked_by", []):
|
||||
# Unlike
|
||||
await self.collection.update_one(
|
||||
{"_id": oid},
|
||||
{"$pull": {"liked_by": user_id}}
|
||||
)
|
||||
return False
|
||||
else:
|
||||
# Like
|
||||
await self.collection.update_one(
|
||||
{"_id": oid},
|
||||
{"$addToSet": {"liked_by": user_id}}
|
||||
)
|
||||
return True
|
||||
|
||||
async def get_usage_stats(self, created_by: Optional[str] = None, project_id: Optional[str] = None) -> dict:
|
||||
"""
|
||||
Calculates usage statistics (runs, tokens, cost) using MongoDB aggregation.
|
||||
|
||||
Reference in New Issue
Block a user