feat: Enhance idea retrieval to include the latest generation and support user-specific ideas not tied to a project, while also improving asset storage uniqueness and adjusting generation cancellation timeout.
This commit is contained in:
2
aiws.py
2
aiws.py
@@ -130,7 +130,7 @@ async def start_scheduler(service: GenerationService):
|
|||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Scheduler error: {e}")
|
logger.error(f"Scheduler error: {e}")
|
||||||
await asyncio.sleep(600) # Check every 10 minutes
|
await asyncio.sleep(60) # Check every 10 minutes
|
||||||
|
|
||||||
# --- LIFESPAN (Запуск FastAPI + Bot) ---
|
# --- LIFESPAN (Запуск FastAPI + Bot) ---
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
Binary file not shown.
@@ -6,34 +6,30 @@ from api.service.idea_service import IdeaService
|
|||||||
from api.service.generation_service import GenerationService
|
from api.service.generation_service import GenerationService
|
||||||
from models.Idea import Idea
|
from models.Idea import Idea
|
||||||
from api.models.GenerationRequest import GenerationResponse, GenerationsResponse
|
from api.models.GenerationRequest import GenerationResponse, GenerationsResponse
|
||||||
from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest
|
from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest, IdeaResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/ideas", tags=["ideas"])
|
router = APIRouter(prefix="/api/ideas", tags=["ideas"])
|
||||||
|
|
||||||
@router.post("", response_model=Idea)
|
@router.post("", response_model=Idea)
|
||||||
async def create_idea(
|
async def create_idea(
|
||||||
request: IdeaCreateRequest,
|
request: IdeaCreateRequest,
|
||||||
project_id: str = Depends(get_project_id),
|
project_id: Optional[str] = Depends(get_project_id),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
idea_service: IdeaService = Depends(get_idea_service)
|
idea_service: IdeaService = Depends(get_idea_service)
|
||||||
):
|
):
|
||||||
if not project_id and not request.project_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Project ID header is required")
|
|
||||||
|
|
||||||
pid = project_id or request.project_id
|
pid = project_id or request.project_id
|
||||||
|
|
||||||
return await idea_service.create_idea(request.name, request.description, pid, str(current_user["_id"]))
|
return await idea_service.create_idea(request.name, request.description, pid, str(current_user["_id"]))
|
||||||
|
|
||||||
@router.get("", response_model=List[Idea])
|
@router.get("", response_model=List[IdeaResponse])
|
||||||
async def get_ideas(
|
async def get_ideas(
|
||||||
project_id: str = Depends(get_project_id),
|
project_id: Optional[str] = Depends(get_project_id),
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
idea_service: IdeaService = Depends(get_idea_service)
|
idea_service: IdeaService = Depends(get_idea_service)
|
||||||
):
|
):
|
||||||
if not project_id:
|
return await idea_service.get_ideas(project_id, str(current_user["_id"]), limit, offset)
|
||||||
raise HTTPException(status_code=400, detail="Project ID header is required")
|
|
||||||
return await idea_service.get_ideas(project_id, limit, offset)
|
|
||||||
|
|
||||||
@router.get("/{idea_id}", response_model=Idea)
|
@router.get("/{idea_id}", response_model=Idea)
|
||||||
async def get_idea(
|
async def get_idea(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from models.Idea import Idea
|
||||||
|
from api.models.GenerationRequest import GenerationResponse
|
||||||
|
|
||||||
class IdeaCreateRequest(BaseModel):
|
class IdeaCreateRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@@ -9,3 +11,6 @@ class IdeaCreateRequest(BaseModel):
|
|||||||
class IdeaUpdateRequest(BaseModel):
|
class IdeaUpdateRequest(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class IdeaResponse(Idea):
|
||||||
|
last_generation: Optional[GenerationResponse] = None
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -7,14 +7,14 @@ class IdeaService:
|
|||||||
def __init__(self, dao: DAO):
|
def __init__(self, dao: DAO):
|
||||||
self.dao = dao
|
self.dao = dao
|
||||||
|
|
||||||
async def create_idea(self, name: str, description: Optional[str], project_id: str, user_id: str) -> Idea:
|
async def create_idea(self, name: str, description: Optional[str], project_id: Optional[str], user_id: str) -> Idea:
|
||||||
idea = Idea(name=name, description=description, project_id=project_id, created_by=user_id)
|
idea = Idea(name=name, description=description, project_id=project_id, created_by=user_id)
|
||||||
idea_id = await self.dao.ideas.create_idea(idea)
|
idea_id = await self.dao.ideas.create_idea(idea)
|
||||||
idea.id = idea_id
|
idea.id = idea_id
|
||||||
return idea
|
return idea
|
||||||
|
|
||||||
async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]:
|
async def get_ideas(self, project_id: Optional[str], user_id: str, limit: int = 20, offset: int = 0) -> List[dict]:
|
||||||
return await self.dao.ideas.get_ideas(project_id, limit, offset)
|
return await self.dao.ideas.get_ideas(project_id, user_id, limit, offset)
|
||||||
|
|
||||||
async def get_idea(self, idea_id: str) -> Optional[Idea]:
|
async def get_idea(self, idea_id: str) -> Optional[Idea]:
|
||||||
return await self.dao.ideas.get_idea(idea_id)
|
return await self.dao.ideas.get_idea(idea_id)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Idea(BaseModel):
|
|||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
name: str = "New Idea"
|
name: str = "New Idea"
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
project_id: str
|
project_id: Optional[str] = None
|
||||||
created_by: str # User ID
|
created_by: str # User ID
|
||||||
is_deleted: bool = False
|
is_deleted: bool = False
|
||||||
created_at: datetime = Field(default_factory=datetime.now)
|
created_at: datetime = Field(default_factory=datetime.now)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import logging
|
import logging
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
from uuid import uuid4
|
||||||
from motor.motor_asyncio import AsyncIOMotorClient
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
from models.Asset import Asset
|
from models.Asset import Asset
|
||||||
@@ -19,7 +20,8 @@ class AssetsRepo:
|
|||||||
# Main data
|
# Main data
|
||||||
if asset.data:
|
if asset.data:
|
||||||
ts = int(asset.created_at.timestamp())
|
ts = int(asset.created_at.timestamp())
|
||||||
object_name = f"{asset.type.value}/{ts}_{asset.name}"
|
uid = uuid4().hex[:8]
|
||||||
|
object_name = f"{asset.type.value}/{ts}_{uid}_{asset.name}"
|
||||||
|
|
||||||
uploaded = await self.s3.upload_file(object_name, asset.data)
|
uploaded = await self.s3.upload_file(object_name, asset.data)
|
||||||
if uploaded:
|
if uploaded:
|
||||||
@@ -32,7 +34,8 @@ class AssetsRepo:
|
|||||||
# Thumbnail
|
# Thumbnail
|
||||||
if asset.thumbnail:
|
if asset.thumbnail:
|
||||||
ts = int(asset.created_at.timestamp())
|
ts = int(asset.created_at.timestamp())
|
||||||
thumb_name = f"{asset.type.value}/thumbs/{ts}_{asset.name}_thumb.jpg"
|
uid = uuid4().hex[:8]
|
||||||
|
thumb_name = f"{asset.type.value}/thumbs/{ts}_{uid}_{asset.name}_thumb.jpg"
|
||||||
|
|
||||||
uploaded_thumb = await self.s3.upload_file(thumb_name, asset.thumbnail)
|
uploaded_thumb = await self.s3.upload_file(thumb_name, asset.thumbnail)
|
||||||
if uploaded_thumb:
|
if uploaded_thumb:
|
||||||
@@ -134,7 +137,8 @@ class AssetsRepo:
|
|||||||
if self.s3:
|
if self.s3:
|
||||||
if asset.data:
|
if asset.data:
|
||||||
ts = int(asset.created_at.timestamp())
|
ts = int(asset.created_at.timestamp())
|
||||||
object_name = f"{asset.type.value}/{ts}_{asset.name}"
|
uid = uuid4().hex[:8]
|
||||||
|
object_name = f"{asset.type.value}/{ts}_{uid}_{asset.name}"
|
||||||
if await self.s3.upload_file(object_name, asset.data):
|
if await self.s3.upload_file(object_name, asset.data):
|
||||||
asset.minio_object_name = object_name
|
asset.minio_object_name = object_name
|
||||||
asset.minio_bucket = self.s3.bucket_name
|
asset.minio_bucket = self.s3.bucket_name
|
||||||
@@ -142,7 +146,8 @@ class AssetsRepo:
|
|||||||
|
|
||||||
if asset.thumbnail:
|
if asset.thumbnail:
|
||||||
ts = int(asset.created_at.timestamp())
|
ts = int(asset.created_at.timestamp())
|
||||||
thumb_name = f"{asset.type.value}/thumbs/{ts}_{asset.name}_thumb.jpg"
|
uid = uuid4().hex[:8]
|
||||||
|
thumb_name = f"{asset.type.value}/thumbs/{ts}_{uid}_{asset.name}_thumb.jpg"
|
||||||
if await self.s3.upload_file(thumb_name, asset.thumbnail):
|
if await self.s3.upload_file(thumb_name, asset.thumbnail):
|
||||||
asset.minio_thumbnail_object_name = thumb_name
|
asset.minio_thumbnail_object_name = thumb_name
|
||||||
asset.thumbnail = None
|
asset.thumbnail = None
|
||||||
@@ -216,7 +221,8 @@ class AssetsRepo:
|
|||||||
created_at = doc.get("created_at")
|
created_at = doc.get("created_at")
|
||||||
ts = int(created_at.timestamp()) if created_at else 0
|
ts = int(created_at.timestamp()) if created_at else 0
|
||||||
|
|
||||||
object_name = f"{type_}/{ts}_{asset_id}_{name}"
|
uid = uuid4().hex[:8]
|
||||||
|
object_name = f"{type_}/{ts}_{uid}_{asset_id}_{name}"
|
||||||
if await self.s3.upload_file(object_name, data):
|
if await self.s3.upload_file(object_name, data):
|
||||||
await self.collection.update_one(
|
await self.collection.update_one(
|
||||||
{"_id": asset_id},
|
{"_id": asset_id},
|
||||||
@@ -243,7 +249,8 @@ class AssetsRepo:
|
|||||||
created_at = doc.get("created_at")
|
created_at = doc.get("created_at")
|
||||||
ts = int(created_at.timestamp()) if created_at else 0
|
ts = int(created_at.timestamp()) if created_at else 0
|
||||||
|
|
||||||
thumb_name = f"{type_}/thumbs/{ts}_{asset_id}_{name}_thumb.jpg"
|
uid = uuid4().hex[:8]
|
||||||
|
thumb_name = f"{type_}/thumbs/{ts}_{uid}_{asset_id}_{name}_thumb.jpg"
|
||||||
if await self.s3.upload_file(thumb_name, thumb):
|
if await self.s3.upload_file(thumb_name, thumb):
|
||||||
await self.collection.update_one(
|
await self.collection.update_one(
|
||||||
{"_id": asset_id},
|
{"_id": asset_id},
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class GenerationRepo:
|
|||||||
generations.append(Generation(**generation))
|
generations.append(Generation(**generation))
|
||||||
return generations
|
return generations
|
||||||
|
|
||||||
async def cancel_stale_generations(self, timeout_minutes: int = 60) -> int:
|
async def cancel_stale_generations(self, timeout_minutes: int = 5) -> int:
|
||||||
cutoff_time = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
|
cutoff_time = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
|
||||||
res = await self.collection.update_many(
|
res = await self.collection.update_many(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,14 +20,43 @@ class IdeaRepo:
|
|||||||
return Idea(**res)
|
return Idea(**res)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]:
|
async def get_ideas(self, project_id: Optional[str], user_id: str, limit: int = 20, offset: int = 0) -> List[dict]:
|
||||||
filter = {"project_id": project_id, "is_deleted": False}
|
if project_id:
|
||||||
res = await self.collection.find(filter).sort("updated_at", -1).skip(offset).limit(limit).to_list(None)
|
match_stage = {"project_id": project_id, "is_deleted": False}
|
||||||
ideas = []
|
else:
|
||||||
for doc in res:
|
match_stage = {"created_by": user_id, "project_id": None, "is_deleted": False}
|
||||||
doc["id"] = str(doc.pop("_id"))
|
|
||||||
ideas.append(Idea(**doc))
|
pipeline = [
|
||||||
return ideas
|
{"$match": match_stage},
|
||||||
|
{"$sort": {"updated_at": -1}},
|
||||||
|
{"$skip": offset},
|
||||||
|
{"$limit": limit},
|
||||||
|
# Add string id field for lookup
|
||||||
|
{"$addFields": {"str_id": {"$toString": "$_id"}}},
|
||||||
|
# Lookup generations
|
||||||
|
{
|
||||||
|
"$lookup": {
|
||||||
|
"from": "generations",
|
||||||
|
"let": {"idea_id": "$str_id"},
|
||||||
|
"pipeline": [
|
||||||
|
{"$match": {"$expr": {"$eq": ["$idea_id", "$$idea_id"]}}},
|
||||||
|
{"$sort": {"created_at": -1}}, # Ensure we get the latest
|
||||||
|
{"$limit": 1}
|
||||||
|
],
|
||||||
|
"as": "generations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# Unwind generations array (preserve ideas without generations)
|
||||||
|
{"$unwind": {"path": "$generations", "preserveNullAndEmptyArrays": True}},
|
||||||
|
# Rename for clarity
|
||||||
|
{"$addFields": {
|
||||||
|
"last_generation": "$generations",
|
||||||
|
"id": "$str_id"
|
||||||
|
}},
|
||||||
|
{"$project": {"generations": 0, "str_id": 0, "_id": 0}}
|
||||||
|
]
|
||||||
|
|
||||||
|
return await self.collection.aggregate(pipeline).to_list(None)
|
||||||
|
|
||||||
async def delete_idea(self, idea_id: str) -> bool:
|
async def delete_idea(self, idea_id: str) -> bool:
|
||||||
if not ObjectId.is_valid(idea_id):
|
if not ObjectId.is_valid(idea_id):
|
||||||
|
|||||||
Reference in New Issue
Block a user