Ubuntu commited on
Commit ·
ea44208
1
Parent(s): 35c59b8
mongo integration
Browse files- Dockerfile +24 -7
- main.py +165 -28
- mongo_client.py +371 -0
- pyproject.toml +2 -0
Dockerfile
CHANGED
|
@@ -1,10 +1,21 @@
|
|
| 1 |
-
# Hugging Face Spaces (Docker SDK) - FastAPI + Uvicorn + Redis
|
| 2 |
# Docs: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 3 |
|
| 4 |
FROM python:3.12-slim
|
| 5 |
|
| 6 |
-
# Install
|
| 7 |
-
RUN apt-get update && apt-get install -y redis-server &&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# Create a non-root user matching HF Spaces expectations
|
| 10 |
RUN useradd -m -u 1000 user
|
|
@@ -18,6 +29,8 @@ ENV REDIS_HOST="127.0.0.1"
|
|
| 18 |
ENV REDIS_PORT="6379"
|
| 19 |
ENV REDIS_DB="0"
|
| 20 |
ENV CACHE_TTL="300"
|
|
|
|
|
|
|
| 21 |
WORKDIR /app
|
| 22 |
|
| 23 |
# Install uv (dependency manager)
|
|
@@ -34,12 +47,16 @@ RUN uv venv /app/.venv \
|
|
| 34 |
COPY --chown=user . /app
|
| 35 |
|
| 36 |
# Ensure data directories exist and are writable
|
| 37 |
-
RUN mkdir -p /app/chroma_db /app/data /app/redis_data
|
|
|
|
| 38 |
|
| 39 |
# Seed the database (consistent data in every container)
|
| 40 |
RUN uv run python seed_db.py
|
| 41 |
|
| 42 |
-
EXPOSE 7860 6379
|
| 43 |
|
| 44 |
-
# Start Redis and the app
|
| 45 |
-
CMD ["sh", "-c", "
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces (Docker SDK) - FastAPI + Uvicorn + Redis + MongoDB
|
| 2 |
# Docs: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 3 |
|
| 4 |
FROM python:3.12-slim
|
| 5 |
|
| 6 |
+
# Install dependencies
|
| 7 |
+
RUN apt-get update && apt-get install -y redis-server gnupg && \
|
| 8 |
+
rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Install MongoDB using the official approach with --allow-unauthenticated for build
|
| 11 |
+
RUN apt-get update && \
|
| 12 |
+
GNUPGHOME=/tmp gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 9B3B7D436F8E4A3426B64D3D610C8B9831C4F6FC && \
|
| 13 |
+
GNUPGHOME=/tmp gpg --export 9B3B7D436F8E4A3426B64D3D610C8B9831C4F6FC > /usr/share/keyrings/mongodb-archive-keyring.gpg && \
|
| 14 |
+
echo "deb [ signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/7.0 main" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list && \
|
| 15 |
+
apt-get update && \
|
| 16 |
+
apt-get install -y --allow-unauthenticated mongodb-org || \
|
| 17 |
+
(wget -qO- https://repo.mongodb.org/apt/debian/pool/mongodb-org-7.0-7.0.14/mongodb-org-server_7.0.14_amd64.deb -O /tmp/mongo.deb && dpkg -i /tmp/mongo.deb) || \
|
| 18 |
+
echo "MongoDB install attempted"
|
| 19 |
|
| 20 |
# Create a non-root user matching HF Spaces expectations
|
| 21 |
RUN useradd -m -u 1000 user
|
|
|
|
| 29 |
ENV REDIS_PORT="6379"
|
| 30 |
ENV REDIS_DB="0"
|
| 31 |
ENV CACHE_TTL="300"
|
| 32 |
+
ENV MONGO_URI="mongodb://127.0.0.1:27017"
|
| 33 |
+
ENV MONGO_DB_NAME="music_memories"
|
| 34 |
WORKDIR /app
|
| 35 |
|
| 36 |
# Install uv (dependency manager)
|
|
|
|
| 47 |
COPY --chown=user . /app
|
| 48 |
|
| 49 |
# Ensure data directories exist and are writable
|
| 50 |
+
RUN mkdir -p /app/chroma_db /app/data /app/redis_data /app/mongo_data && \
|
| 51 |
+
chown -R user:user /app/chroma_db /app/data /app/redis_data /app/mongo_data
|
| 52 |
|
| 53 |
# Seed the database (consistent data in every container)
|
| 54 |
RUN uv run python seed_db.py
|
| 55 |
|
| 56 |
+
EXPOSE 7860 6379 27017
|
| 57 |
|
| 58 |
+
# Start Redis, MongoDB and the app
|
| 59 |
+
CMD ["sh", "-c", "\
|
| 60 |
+
redis-server --daemonize yes --dir /app/redis_data --appendonly yes && \
|
| 61 |
+
(mongod --dbpath /app/mongo_data --bind_ip 127.0.0.1 --fork --logpath /app/mongo_data/mongod.log 2>/dev/null || echo 'MongoDB not available') && \
|
| 62 |
+
uvicorn main:app --host 0.0.0.0 --port 7860"]
|
main.py
CHANGED
|
@@ -35,22 +35,44 @@ from redis_client import (
|
|
| 35 |
get_cached_user, cache_user, invalidate_user_cache,
|
| 36 |
get_cached_playlist, cache_playlist, invalidate_playlist_cache,
|
| 37 |
get_cached_list, cache_list, invalidate_list_cache, cache_delete_pattern,
|
| 38 |
-
store_play_event, get_user_play_history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
)
|
| 40 |
-
import asyncio
|
| 41 |
|
| 42 |
app = FastAPI()
|
| 43 |
|
| 44 |
|
| 45 |
@asynccontextmanager
|
| 46 |
async def lifespan(app: FastAPI):
|
| 47 |
-
"""Application lifespan manager for Redis and database initialization."""
|
| 48 |
# Startup
|
| 49 |
init_database()
|
| 50 |
await init_redis()
|
|
|
|
| 51 |
yield
|
| 52 |
# Shutdown
|
| 53 |
await close_redis()
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
app.router.lifespan_context = lifespan
|
|
@@ -63,9 +85,11 @@ def greet_json():
|
|
| 63 |
|
| 64 |
@app.get("/health")
|
| 65 |
async def health_check():
|
| 66 |
-
"""Health check endpoint with Redis status."""
|
| 67 |
from redis_client import get_redis_client
|
| 68 |
redis_status = "disconnected"
|
|
|
|
|
|
|
| 69 |
try:
|
| 70 |
redis_client = get_redis_client()
|
| 71 |
if redis_client:
|
|
@@ -74,9 +98,18 @@ async def health_check():
|
|
| 74 |
except Exception:
|
| 75 |
redis_status = "error"
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return {
|
| 78 |
"status": "healthy",
|
| 79 |
"redis": redis_status,
|
|
|
|
| 80 |
"database": "sqlite"
|
| 81 |
}
|
| 82 |
|
|
@@ -99,19 +132,23 @@ async def list_songs():
|
|
| 99 |
|
| 100 |
@app.get("/songs/{song_id}")
|
| 101 |
async def get_song(song_id: int):
|
| 102 |
-
"""Get a song by ID."""
|
| 103 |
# Try cache first
|
| 104 |
song = await get_cached_song(song_id)
|
| 105 |
if song:
|
| 106 |
-
|
|
|
|
|
|
|
| 107 |
|
| 108 |
# Fetch from database
|
| 109 |
song = get_song_by_id(song_id)
|
| 110 |
if not song:
|
| 111 |
raise HTTPException(status_code=404, detail="Song not found")
|
| 112 |
|
|
|
|
|
|
|
| 113 |
await cache_song(song_id, song)
|
| 114 |
-
return {**song, "source": "database"}
|
| 115 |
|
| 116 |
|
| 117 |
@app.post("/songs")
|
|
@@ -124,6 +161,8 @@ async def create_song(title: str, artist: str, album: str = None, duration: int
|
|
| 124 |
add_song_vibe(song["id"], title, artist, lyrics)
|
| 125 |
# Invalidate songs list cache
|
| 126 |
await invalidate_list_cache("all_songs")
|
|
|
|
|
|
|
| 127 |
return {"status": "success", "song": song}
|
| 128 |
|
| 129 |
|
|
@@ -135,6 +174,8 @@ async def delete_song_endpoint(song_id: int):
|
|
| 135 |
remove_song_vibe(song_id)
|
| 136 |
await invalidate_song_cache(song_id)
|
| 137 |
await invalidate_list_cache("all_songs")
|
|
|
|
|
|
|
| 138 |
return {"status": "success", "deleted_id": song_id}
|
| 139 |
|
| 140 |
|
|
@@ -176,6 +217,9 @@ async def create_user(name: str):
|
|
| 176 |
"""Add a new user."""
|
| 177 |
user = add_user(name)
|
| 178 |
await invalidate_list_cache("all_users")
|
|
|
|
|
|
|
|
|
|
| 179 |
return {"status": "success", "user": user}
|
| 180 |
|
| 181 |
|
|
@@ -203,6 +247,8 @@ async def create_playlist(name: str, vibe_code: str = None, mood_description: st
|
|
| 203 |
if mood_description:
|
| 204 |
add_playlist_journey(playlist["id"], name, mood_description)
|
| 205 |
await invalidate_list_cache("all_playlists")
|
|
|
|
|
|
|
| 206 |
return {"status": "success", "playlist": playlist}
|
| 207 |
|
| 208 |
|
|
@@ -214,6 +260,8 @@ async def delete_playlist_endpoint(playlist_id: int):
|
|
| 214 |
remove_playlist_journey(playlist_id)
|
| 215 |
await invalidate_playlist_cache(playlist_id)
|
| 216 |
await invalidate_list_cache("all_playlists")
|
|
|
|
|
|
|
| 217 |
return {"status": "success", "deleted_id": playlist_id}
|
| 218 |
|
| 219 |
|
|
@@ -260,6 +308,9 @@ async def create_memory(user_id: int, description: str, date: str = None, song_i
|
|
| 260 |
add_memory_vibe(memory["id"], user_id, description)
|
| 261 |
await invalidate_list_cache("all_memories")
|
| 262 |
await invalidate_list_cache(f"user_memories:{user_id}")
|
|
|
|
|
|
|
|
|
|
| 263 |
return {"status": "success", "memory": memory}
|
| 264 |
|
| 265 |
|
|
@@ -271,6 +322,8 @@ async def delete_memory_endpoint(memory_id: int):
|
|
| 271 |
remove_memory_vibe(memory_id)
|
| 272 |
await cache_delete_pattern("list:user_memories:*")
|
| 273 |
await invalidate_list_cache("all_memories")
|
|
|
|
|
|
|
| 274 |
return {"status": "success", "deleted_id": memory_id}
|
| 275 |
|
| 276 |
|
|
@@ -297,6 +350,8 @@ async def create_context(weather: str = None, time_of_day: str = None, location_
|
|
| 297 |
# Add to semantic search index
|
| 298 |
add_context_vibe(context["id"], weather or "", time_of_day or "", location_type or "")
|
| 299 |
await invalidate_list_cache("all_contexts")
|
|
|
|
|
|
|
| 300 |
return {"status": "success", "context": context}
|
| 301 |
|
| 302 |
|
|
@@ -307,66 +362,148 @@ async def delete_context_endpoint(context_id: int):
|
|
| 307 |
raise HTTPException(status_code=404, detail="Context not found")
|
| 308 |
remove_context_vibe(context_id)
|
| 309 |
await invalidate_list_cache("all_contexts")
|
|
|
|
|
|
|
| 310 |
return {"status": "success", "deleted_id": context_id}
|
| 311 |
|
| 312 |
|
| 313 |
-
# ============== PLAY HISTORY ==============
|
| 314 |
|
| 315 |
@app.get("/history")
|
| 316 |
async def list_history(user_id: int = None, limit: int = 50):
|
| 317 |
-
"""Get play history."""
|
| 318 |
if user_id:
|
| 319 |
-
# Try
|
| 320 |
-
history = await
|
| 321 |
if not history:
|
| 322 |
-
# Fallback to
|
| 323 |
-
history =
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
else:
|
| 326 |
-
# Try
|
| 327 |
-
history = await
|
| 328 |
if not history:
|
| 329 |
-
# Fallback to
|
| 330 |
-
history =
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
|
| 334 |
@app.post("/history")
|
| 335 |
-
async def create_history(user_id: int, song_id: int, context_id: int = None):
|
| 336 |
-
"""Add a play history entry."""
|
| 337 |
-
#
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
sqlite_history = add_play_history(user_id, song_id, context_id)
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
|
| 343 |
# ============== SEMANTIC SEARCH ==============
|
| 344 |
|
| 345 |
@app.get("/search/songs")
|
| 346 |
-
def search_songs(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 347 |
"""Semantic search for songs by vibe/lyrics."""
|
| 348 |
results = search_song_vibes(q, n_results=n)
|
|
|
|
|
|
|
|
|
|
| 349 |
return {"query": q, "results": results}
|
| 350 |
|
| 351 |
|
| 352 |
@app.get("/search/memories")
|
| 353 |
-
def search_memories(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 354 |
"""Semantic search for memories."""
|
| 355 |
results = search_memory_vibes(q, n_results=n)
|
|
|
|
|
|
|
|
|
|
| 356 |
return {"query": q, "results": results}
|
| 357 |
|
| 358 |
|
| 359 |
@app.get("/search/contexts")
|
| 360 |
-
def search_contexts(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 361 |
"""Semantic search for contexts."""
|
| 362 |
results = search_context_vibes(q, n_results=n)
|
|
|
|
|
|
|
|
|
|
| 363 |
return {"query": q, "results": results}
|
| 364 |
|
| 365 |
|
| 366 |
@app.get("/search/playlists")
|
| 367 |
-
def search_playlists(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 368 |
"""Semantic search for playlists by mood."""
|
| 369 |
results = search_playlist_journeys(q, n_results=n)
|
|
|
|
|
|
|
|
|
|
| 370 |
return {"query": q, "results": results}
|
| 371 |
|
| 372 |
|
|
|
|
| 35 |
get_cached_user, cache_user, invalidate_user_cache,
|
| 36 |
get_cached_playlist, cache_playlist, invalidate_playlist_cache,
|
| 37 |
get_cached_list, cache_list, invalidate_list_cache, cache_delete_pattern,
|
| 38 |
+
store_play_event, get_user_play_history as get_redis_play_history,
|
| 39 |
+
get_global_play_history as get_redis_global_history,
|
| 40 |
+
)
|
| 41 |
+
from mongo_client import (
|
| 42 |
+
init_mongodb,
|
| 43 |
+
close_mongodb,
|
| 44 |
+
get_mongo_db,
|
| 45 |
+
# Play history
|
| 46 |
+
store_play_history_mongo,
|
| 47 |
+
get_user_play_history_mongo,
|
| 48 |
+
get_global_play_history_mongo,
|
| 49 |
+
get_top_songs_mongo,
|
| 50 |
+
get_song_play_count_mongo,
|
| 51 |
+
# Activity logs
|
| 52 |
+
log_user_activity,
|
| 53 |
+
get_user_activity_mongo,
|
| 54 |
+
# Analytics
|
| 55 |
+
track_analytics_event,
|
| 56 |
+
get_analytics_summary,
|
| 57 |
+
# Search history
|
| 58 |
+
log_search,
|
| 59 |
+
get_popular_searches,
|
| 60 |
)
|
|
|
|
| 61 |
|
| 62 |
app = FastAPI()
|
| 63 |
|
| 64 |
|
| 65 |
@asynccontextmanager
|
| 66 |
async def lifespan(app: FastAPI):
|
| 67 |
+
"""Application lifespan manager for Redis, MongoDB and database initialization."""
|
| 68 |
# Startup
|
| 69 |
init_database()
|
| 70 |
await init_redis()
|
| 71 |
+
await init_mongodb()
|
| 72 |
yield
|
| 73 |
# Shutdown
|
| 74 |
await close_redis()
|
| 75 |
+
await close_mongodb()
|
| 76 |
|
| 77 |
|
| 78 |
app.router.lifespan_context = lifespan
|
|
|
|
| 85 |
|
| 86 |
@app.get("/health")
|
| 87 |
async def health_check():
|
| 88 |
+
"""Health check endpoint with Redis and MongoDB status."""
|
| 89 |
from redis_client import get_redis_client
|
| 90 |
redis_status = "disconnected"
|
| 91 |
+
mongo_status = "disconnected"
|
| 92 |
+
|
| 93 |
try:
|
| 94 |
redis_client = get_redis_client()
|
| 95 |
if redis_client:
|
|
|
|
| 98 |
except Exception:
|
| 99 |
redis_status = "error"
|
| 100 |
|
| 101 |
+
try:
|
| 102 |
+
mongo_db = get_mongo_db()
|
| 103 |
+
if mongo_db:
|
| 104 |
+
await mongo_db.command('ping')
|
| 105 |
+
mongo_status = "connected"
|
| 106 |
+
except Exception:
|
| 107 |
+
mongo_status = "error"
|
| 108 |
+
|
| 109 |
return {
|
| 110 |
"status": "healthy",
|
| 111 |
"redis": redis_status,
|
| 112 |
+
"mongodb": mongo_status,
|
| 113 |
"database": "sqlite"
|
| 114 |
}
|
| 115 |
|
|
|
|
| 132 |
|
| 133 |
@app.get("/songs/{song_id}")
|
| 134 |
async def get_song(song_id: int):
|
| 135 |
+
"""Get a song by ID with play count from MongoDB."""
|
| 136 |
# Try cache first
|
| 137 |
song = await get_cached_song(song_id)
|
| 138 |
if song:
|
| 139 |
+
# Add play count from MongoDB
|
| 140 |
+
play_count = await get_song_play_count_mongo(song_id)
|
| 141 |
+
return {**song, "source": "cache", "play_count": play_count}
|
| 142 |
|
| 143 |
# Fetch from database
|
| 144 |
song = get_song_by_id(song_id)
|
| 145 |
if not song:
|
| 146 |
raise HTTPException(status_code=404, detail="Song not found")
|
| 147 |
|
| 148 |
+
# Add play count from MongoDB
|
| 149 |
+
play_count = await get_song_play_count_mongo(song_id)
|
| 150 |
await cache_song(song_id, song)
|
| 151 |
+
return {**song, "source": "database", "play_count": play_count}
|
| 152 |
|
| 153 |
|
| 154 |
@app.post("/songs")
|
|
|
|
| 161 |
add_song_vibe(song["id"], title, artist, lyrics)
|
| 162 |
# Invalidate songs list cache
|
| 163 |
await invalidate_list_cache("all_songs")
|
| 164 |
+
# Log activity to MongoDB
|
| 165 |
+
await track_analytics_event("song_created", {"song_id": song["id"], "title": title})
|
| 166 |
return {"status": "success", "song": song}
|
| 167 |
|
| 168 |
|
|
|
|
| 174 |
remove_song_vibe(song_id)
|
| 175 |
await invalidate_song_cache(song_id)
|
| 176 |
await invalidate_list_cache("all_songs")
|
| 177 |
+
# Log activity to MongoDB
|
| 178 |
+
await track_analytics_event("song_deleted", {"song_id": song_id})
|
| 179 |
return {"status": "success", "deleted_id": song_id}
|
| 180 |
|
| 181 |
|
|
|
|
| 217 |
"""Add a new user."""
|
| 218 |
user = add_user(name)
|
| 219 |
await invalidate_list_cache("all_users")
|
| 220 |
+
# Log activity to MongoDB
|
| 221 |
+
await log_user_activity(user["id"], "user_registered", {"name": name})
|
| 222 |
+
await track_analytics_event("user_signup", {"user_id": user["id"]})
|
| 223 |
return {"status": "success", "user": user}
|
| 224 |
|
| 225 |
|
|
|
|
| 247 |
if mood_description:
|
| 248 |
add_playlist_journey(playlist["id"], name, mood_description)
|
| 249 |
await invalidate_list_cache("all_playlists")
|
| 250 |
+
# Log activity to MongoDB
|
| 251 |
+
await track_analytics_event("playlist_created", {"playlist_id": playlist["id"], "name": name})
|
| 252 |
return {"status": "success", "playlist": playlist}
|
| 253 |
|
| 254 |
|
|
|
|
| 260 |
remove_playlist_journey(playlist_id)
|
| 261 |
await invalidate_playlist_cache(playlist_id)
|
| 262 |
await invalidate_list_cache("all_playlists")
|
| 263 |
+
# Log activity to MongoDB
|
| 264 |
+
await track_analytics_event("playlist_deleted", {"playlist_id": playlist_id})
|
| 265 |
return {"status": "success", "deleted_id": playlist_id}
|
| 266 |
|
| 267 |
|
|
|
|
| 308 |
add_memory_vibe(memory["id"], user_id, description)
|
| 309 |
await invalidate_list_cache("all_memories")
|
| 310 |
await invalidate_list_cache(f"user_memories:{user_id}")
|
| 311 |
+
# Log activity to MongoDB
|
| 312 |
+
await log_user_activity(user_id, "memory_created", {"memory_id": memory["id"]})
|
| 313 |
+
await track_analytics_event("memory_added", {"memory_id": memory["id"], "user_id": user_id})
|
| 314 |
return {"status": "success", "memory": memory}
|
| 315 |
|
| 316 |
|
|
|
|
| 322 |
remove_memory_vibe(memory_id)
|
| 323 |
await cache_delete_pattern("list:user_memories:*")
|
| 324 |
await invalidate_list_cache("all_memories")
|
| 325 |
+
# Log activity to MongoDB
|
| 326 |
+
await track_analytics_event("memory_deleted", {"memory_id": memory_id})
|
| 327 |
return {"status": "success", "deleted_id": memory_id}
|
| 328 |
|
| 329 |
|
|
|
|
| 350 |
# Add to semantic search index
|
| 351 |
add_context_vibe(context["id"], weather or "", time_of_day or "", location_type or "")
|
| 352 |
await invalidate_list_cache("all_contexts")
|
| 353 |
+
# Log activity to MongoDB
|
| 354 |
+
await track_analytics_event("context_created", {"context_id": context["id"]})
|
| 355 |
return {"status": "success", "context": context}
|
| 356 |
|
| 357 |
|
|
|
|
| 362 |
raise HTTPException(status_code=404, detail="Context not found")
|
| 363 |
remove_context_vibe(context_id)
|
| 364 |
await invalidate_list_cache("all_contexts")
|
| 365 |
+
# Log activity to MongoDB
|
| 366 |
+
await track_analytics_event("context_deleted", {"context_id": context_id})
|
| 367 |
return {"status": "success", "deleted_id": context_id}
|
| 368 |
|
| 369 |
|
| 370 |
+
# ============== PLAY HISTORY (MongoDB Primary) ==============
|
| 371 |
|
| 372 |
@app.get("/history")
|
| 373 |
async def list_history(user_id: int = None, limit: int = 50):
|
| 374 |
+
"""Get play history from MongoDB (primary storage)."""
|
| 375 |
if user_id:
|
| 376 |
+
# Try MongoDB first (primary storage)
|
| 377 |
+
history = await get_user_play_history_mongo(user_id, limit)
|
| 378 |
if not history:
|
| 379 |
+
# Fallback to Redis
|
| 380 |
+
history = await get_redis_play_history(user_id, limit)
|
| 381 |
+
if not history:
|
| 382 |
+
# Fallback to SQLite
|
| 383 |
+
history = get_play_history(user_id, limit)
|
| 384 |
+
return {"history": history, "source": "sqlite"}
|
| 385 |
+
return {"history": history, "source": "redis"}
|
| 386 |
+
return {"history": history, "source": "mongodb"}
|
| 387 |
else:
|
| 388 |
+
# Try MongoDB first (primary storage)
|
| 389 |
+
history = await get_global_play_history_mongo(limit)
|
| 390 |
if not history:
|
| 391 |
+
# Fallback to Redis
|
| 392 |
+
history = await get_redis_global_history(limit)
|
| 393 |
+
if not history:
|
| 394 |
+
# Fallback to SQLite
|
| 395 |
+
history = get_play_history(None, limit)
|
| 396 |
+
return {"history": history, "source": "sqlite"}
|
| 397 |
+
return {"history": history, "source": "redis"}
|
| 398 |
+
return {"history": history, "source": "mongodb"}
|
| 399 |
|
| 400 |
|
| 401 |
@app.post("/history")
|
| 402 |
+
async def create_history(user_id: int, song_id: int, context_id: int = None, duration_seconds: int = None):
|
| 403 |
+
"""Add a play history entry to MongoDB (primary), Redis (cache), and SQLite (backup)."""
|
| 404 |
+
# Get song info for MongoDB storage
|
| 405 |
+
song = get_song_by_id(song_id)
|
| 406 |
+
|
| 407 |
+
# Store in MongoDB (primary storage for analytics)
|
| 408 |
+
mongo_result = await store_play_history_mongo(
|
| 409 |
+
user_id=user_id,
|
| 410 |
+
song_id=song_id,
|
| 411 |
+
song_title=song.get("title") if song else None,
|
| 412 |
+
song_artist=song.get("artist") if song else None,
|
| 413 |
+
context_id=context_id,
|
| 414 |
+
duration_seconds=duration_seconds
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
# Store in Redis (fast cache)
|
| 418 |
+
await store_play_event(user_id, song_id, context_id)
|
| 419 |
+
|
| 420 |
+
# Store in SQLite (backup)
|
| 421 |
sqlite_history = add_play_history(user_id, song_id, context_id)
|
| 422 |
+
|
| 423 |
+
# Log analytics event
|
| 424 |
+
await track_analytics_event("song_played", {
|
| 425 |
+
"user_id": user_id,
|
| 426 |
+
"song_id": song_id,
|
| 427 |
+
"context_id": context_id
|
| 428 |
+
})
|
| 429 |
+
|
| 430 |
+
return {
|
| 431 |
+
"status": "success",
|
| 432 |
+
"history": sqlite_history,
|
| 433 |
+
"mongodb_id": mongo_result.get("id") if mongo_result else None,
|
| 434 |
+
"redis_stored": True
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
# ============== ANALYTICS ENDPOINTS (MongoDB) ==============
|
| 439 |
+
|
| 440 |
+
@app.get("/analytics/summary")
|
| 441 |
+
async def get_analytics(hours: int = Query(24, description="Hours to look back")):
|
| 442 |
+
"""Get analytics summary from MongoDB."""
|
| 443 |
+
summary = await get_analytics_summary(hours=hours)
|
| 444 |
+
return summary
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
@app.get("/analytics/top-songs")
|
| 448 |
+
async def get_top_songs(limit: int = Query(10, description="Number of results")):
|
| 449 |
+
"""Get top played songs from MongoDB."""
|
| 450 |
+
top_songs = await get_top_songs_mongo(limit)
|
| 451 |
+
return {"top_songs": top_songs}
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
@app.get("/analytics/popular-searches")
|
| 455 |
+
async def get_popular_searches_endpoint(hours: int = Query(24, description="Hours to look back"), limit: int = Query(10)):
|
| 456 |
+
"""Get popular search queries from MongoDB."""
|
| 457 |
+
searches = await get_popular_searches(hours=hours, limit=limit)
|
| 458 |
+
return {"popular_searches": searches}
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
@app.get("/users/{user_id}/activity")
|
| 462 |
+
async def get_user_activity(user_id: int, limit: int = Query(50), action_type: str = None):
|
| 463 |
+
"""Get user activity log from MongoDB."""
|
| 464 |
+
activity = await get_user_activity_mongo(user_id, limit, action_type)
|
| 465 |
+
return {"activity": activity}
|
| 466 |
|
| 467 |
|
| 468 |
# ============== SEMANTIC SEARCH ==============
|
| 469 |
|
| 470 |
@app.get("/search/songs")
|
| 471 |
+
async def search_songs(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 472 |
"""Semantic search for songs by vibe/lyrics."""
|
| 473 |
results = search_song_vibes(q, n_results=n)
|
| 474 |
+
# Log search to MongoDB
|
| 475 |
+
await log_search(search_type="songs", query=q, results_count=len(results))
|
| 476 |
+
await track_analytics_event("search_performed", {"search_type": "songs", "query": q, "results": len(results)})
|
| 477 |
return {"query": q, "results": results}
|
| 478 |
|
| 479 |
|
| 480 |
@app.get("/search/memories")
|
| 481 |
+
async def search_memories(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 482 |
"""Semantic search for memories."""
|
| 483 |
results = search_memory_vibes(q, n_results=n)
|
| 484 |
+
# Log search to MongoDB
|
| 485 |
+
await log_search(search_type="memories", query=q, results_count=len(results))
|
| 486 |
+
await track_analytics_event("search_performed", {"search_type": "memories", "query": q, "results": len(results)})
|
| 487 |
return {"query": q, "results": results}
|
| 488 |
|
| 489 |
|
| 490 |
@app.get("/search/contexts")
|
| 491 |
+
async def search_contexts(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 492 |
"""Semantic search for contexts."""
|
| 493 |
results = search_context_vibes(q, n_results=n)
|
| 494 |
+
# Log search to MongoDB
|
| 495 |
+
await log_search(search_type="contexts", query=q, results_count=len(results))
|
| 496 |
+
await track_analytics_event("search_performed", {"search_type": "contexts", "query": q, "results": len(results)})
|
| 497 |
return {"query": q, "results": results}
|
| 498 |
|
| 499 |
|
| 500 |
@app.get("/search/playlists")
|
| 501 |
+
async def search_playlists(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 502 |
"""Semantic search for playlists by mood."""
|
| 503 |
results = search_playlist_journeys(q, n_results=n)
|
| 504 |
+
# Log search to MongoDB
|
| 505 |
+
await log_search(search_type="playlists", query=q, results_count=len(results))
|
| 506 |
+
await track_analytics_event("search_performed", {"search_type": "playlists", "query": q, "results": len(results)})
|
| 507 |
return {"query": q, "results": results}
|
| 508 |
|
| 509 |
|
mongo_client.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MongoDB client module for play history, activity logs, and analytics."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Optional, List, Dict, Any
|
| 6 |
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
| 7 |
+
from pymongo import ASCENDING, DESCENDING
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# MongoDB configuration from environment variables
|
| 11 |
+
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017")
|
| 12 |
+
MONGO_DB_NAME = os.getenv("MONGO_DB_NAME", "music_memories")
|
| 13 |
+
|
| 14 |
+
# Global MongoDB client and database instances
|
| 15 |
+
_mongo_client: Optional[AsyncIOMotorClient] = None
|
| 16 |
+
_mongo_db: Optional[AsyncIOMotorDatabase] = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_mongo_client() -> Optional[AsyncIOMotorClient]:
|
| 20 |
+
"""Get the MongoDB client instance."""
|
| 21 |
+
return _mongo_client
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_mongo_db() -> Optional[AsyncIOMotorDatabase]:
|
| 25 |
+
"""Get the MongoDB database instance."""
|
| 26 |
+
return _mongo_db
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
async def init_mongodb() -> None:
|
| 30 |
+
"""Initialize MongoDB connection."""
|
| 31 |
+
global _mongo_client, _mongo_db
|
| 32 |
+
try:
|
| 33 |
+
_mongo_client = AsyncIOMotorClient(MONGO_URI)
|
| 34 |
+
_mongo_db = _mongo_client[MONGO_DB_NAME]
|
| 35 |
+
|
| 36 |
+
# Test connection
|
| 37 |
+
await _mongo_client.admin.command('ping')
|
| 38 |
+
print(f"✓ Connected to MongoDB at {MONGO_URI}")
|
| 39 |
+
|
| 40 |
+
# Create indexes for play_history collection
|
| 41 |
+
await _mongo_db.play_history.create_index([("user_id", ASCENDING), ("played_at", DESCENDING)])
|
| 42 |
+
await _mongo_db.play_history.create_index([("song_id", ASCENDING)])
|
| 43 |
+
await _mongo_db.play_history.create_index([("played_at", DESCENDING)])
|
| 44 |
+
|
| 45 |
+
# Create indexes for activity_logs collection
|
| 46 |
+
await _mongo_db.activity_logs.create_index([("user_id", ASCENDING), ("timestamp", DESCENDING)])
|
| 47 |
+
await _mongo_db.activity_logs.create_index([("action_type", ASCENDING)])
|
| 48 |
+
await _mongo_db.activity_logs.create_index([("timestamp", DESCENDING)])
|
| 49 |
+
|
| 50 |
+
# Create indexes for analytics collection
|
| 51 |
+
await _mongo_db.analytics.create_index([("event_type", ASCENDING), ("timestamp", DESCENDING)])
|
| 52 |
+
await _mongo_db.analytics.create_index([("timestamp", DESCENDING)])
|
| 53 |
+
|
| 54 |
+
print("✓ MongoDB indexes created")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"⚠ MongoDB connection failed: {e}")
|
| 57 |
+
print("⚠ Running without MongoDB (using Redis/SQLite fallback)")
|
| 58 |
+
_mongo_client = None
|
| 59 |
+
_mongo_db = None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
async def close_mongodb() -> None:
|
| 63 |
+
"""Close MongoDB connection."""
|
| 64 |
+
global _mongo_client, _mongo_db
|
| 65 |
+
if _mongo_client:
|
| 66 |
+
_mongo_client.close()
|
| 67 |
+
_mongo_client = None
|
| 68 |
+
_mongo_db = None
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ============== Play History (MongoDB) ==============
|
| 72 |
+
|
| 73 |
+
async def store_play_history_mongo(
|
| 74 |
+
user_id: int,
|
| 75 |
+
song_id: int,
|
| 76 |
+
song_title: str = None,
|
| 77 |
+
song_artist: str = None,
|
| 78 |
+
context_id: int = None,
|
| 79 |
+
duration_seconds: int = None,
|
| 80 |
+
completed: bool = True
|
| 81 |
+
) -> Dict[str, Any]:
|
| 82 |
+
"""Store a play history event in MongoDB."""
|
| 83 |
+
if _mongo_db is None:
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
document = {
|
| 87 |
+
"user_id": user_id,
|
| 88 |
+
"song_id": song_id,
|
| 89 |
+
"song_title": song_title,
|
| 90 |
+
"song_artist": song_artist,
|
| 91 |
+
"context_id": context_id,
|
| 92 |
+
"duration_seconds": duration_seconds,
|
| 93 |
+
"completed": completed,
|
| 94 |
+
"played_at": datetime.utcnow(),
|
| 95 |
+
"created_at": datetime.utcnow()
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
result = await _mongo_db.play_history.insert_one(document)
|
| 99 |
+
return {
|
| 100 |
+
"id": str(result.inserted_id),
|
| 101 |
+
**document
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
async def get_user_play_history_mongo(
|
| 106 |
+
user_id: int,
|
| 107 |
+
limit: int = 50
|
| 108 |
+
) -> List[Dict[str, Any]]:
|
| 109 |
+
"""Get play history for a user from MongoDB."""
|
| 110 |
+
if _mongo_db is None:
|
| 111 |
+
return []
|
| 112 |
+
|
| 113 |
+
cursor = _mongo_db.play_history.find(
|
| 114 |
+
{"user_id": user_id}
|
| 115 |
+
).sort("played_at", DESCENDING).limit(limit)
|
| 116 |
+
|
| 117 |
+
results = []
|
| 118 |
+
async for doc in cursor:
|
| 119 |
+
results.append({
|
| 120 |
+
"id": str(doc["_id"]),
|
| 121 |
+
"user_id": doc["user_id"],
|
| 122 |
+
"song_id": doc["song_id"],
|
| 123 |
+
"song_title": doc.get("song_title"),
|
| 124 |
+
"song_artist": doc.get("song_artist"),
|
| 125 |
+
"context_id": doc.get("context_id"),
|
| 126 |
+
"duration_seconds": doc.get("duration_seconds"),
|
| 127 |
+
"completed": doc.get("completed", True),
|
| 128 |
+
"played_at": doc["played_at"].isoformat() if doc.get("played_at") else None
|
| 129 |
+
})
|
| 130 |
+
|
| 131 |
+
return results
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
async def get_global_play_history_mongo(
|
| 135 |
+
limit: int = 50
|
| 136 |
+
) -> List[Dict[str, Any]]:
|
| 137 |
+
"""Get global play history from MongoDB."""
|
| 138 |
+
if _mongo_db is None:
|
| 139 |
+
return []
|
| 140 |
+
|
| 141 |
+
cursor = _mongo_db.play_history.find({}).sort("played_at", DESCENDING).limit(limit)
|
| 142 |
+
|
| 143 |
+
results = []
|
| 144 |
+
async for doc in cursor:
|
| 145 |
+
results.append({
|
| 146 |
+
"id": str(doc["_id"]),
|
| 147 |
+
"user_id": doc["user_id"],
|
| 148 |
+
"song_id": doc["song_id"],
|
| 149 |
+
"song_title": doc.get("song_title"),
|
| 150 |
+
"song_artist": doc.get("song_artist"),
|
| 151 |
+
"context_id": doc.get("context_id"),
|
| 152 |
+
"duration_seconds": doc.get("duration_seconds"),
|
| 153 |
+
"completed": doc.get("completed", True),
|
| 154 |
+
"played_at": doc["played_at"].isoformat() if doc.get("played_at") else None
|
| 155 |
+
})
|
| 156 |
+
|
| 157 |
+
return results
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
async def get_song_play_count_mongo(song_id: int) -> int:
|
| 161 |
+
"""Get total play count for a song."""
|
| 162 |
+
if _mongo_db is None:
|
| 163 |
+
return 0
|
| 164 |
+
|
| 165 |
+
count = await _mongo_db.play_history.count_documents({"song_id": song_id})
|
| 166 |
+
return count
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
async def get_top_songs_mongo(limit: int = 10) -> List[Dict[str, Any]]:
|
| 170 |
+
"""Get top played songs from MongoDB analytics."""
|
| 171 |
+
if _mongo_db is None:
|
| 172 |
+
return []
|
| 173 |
+
|
| 174 |
+
pipeline = [
|
| 175 |
+
{"$group": {
|
| 176 |
+
"_id": "$song_id",
|
| 177 |
+
"play_count": {"$sum": 1},
|
| 178 |
+
"song_title": {"$first": "$song_title"},
|
| 179 |
+
"song_artist": {"$first": "$song_artist"}
|
| 180 |
+
}},
|
| 181 |
+
{"$sort": {"play_count": -1}},
|
| 182 |
+
{"$limit": limit}
|
| 183 |
+
]
|
| 184 |
+
|
| 185 |
+
results = []
|
| 186 |
+
async for doc in _mongo_db.play_history.aggregate(pipeline):
|
| 187 |
+
results.append({
|
| 188 |
+
"song_id": doc["_id"],
|
| 189 |
+
"song_title": doc.get("song_title"),
|
| 190 |
+
"song_artist": doc.get("song_artist"),
|
| 191 |
+
"play_count": doc["play_count"]
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
return results
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# ============== Activity Logs (MongoDB) ==============
|
| 198 |
+
|
| 199 |
+
async def log_user_activity(
|
| 200 |
+
user_id: int,
|
| 201 |
+
action_type: str,
|
| 202 |
+
details: Dict[str, Any] = None
|
| 203 |
+
) -> Dict[str, Any]:
|
| 204 |
+
"""Log a user activity event."""
|
| 205 |
+
if _mongo_db is None:
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
document = {
|
| 209 |
+
"user_id": user_id,
|
| 210 |
+
"action_type": action_type,
|
| 211 |
+
"details": details or {},
|
| 212 |
+
"timestamp": datetime.utcnow()
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
result = await _mongo_db.activity_logs.insert_one(document)
|
| 216 |
+
return {
|
| 217 |
+
"id": str(result.inserted_id),
|
| 218 |
+
**document
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
async def get_user_activity_mongo(
|
| 223 |
+
user_id: int,
|
| 224 |
+
limit: int = 50,
|
| 225 |
+
action_type: str = None
|
| 226 |
+
) -> List[Dict[str, Any]]:
|
| 227 |
+
"""Get activity logs for a user."""
|
| 228 |
+
if _mongo_db is None:
|
| 229 |
+
return []
|
| 230 |
+
|
| 231 |
+
query = {"user_id": user_id}
|
| 232 |
+
if action_type:
|
| 233 |
+
query["action_type"] = action_type
|
| 234 |
+
|
| 235 |
+
cursor = _mongo_db.activity_logs.find(query).sort("timestamp", DESCENDING).limit(limit)
|
| 236 |
+
|
| 237 |
+
results = []
|
| 238 |
+
async for doc in cursor:
|
| 239 |
+
results.append({
|
| 240 |
+
"id": str(doc["_id"]),
|
| 241 |
+
"user_id": doc["user_id"],
|
| 242 |
+
"action_type": doc["action_type"],
|
| 243 |
+
"details": doc.get("details", {}),
|
| 244 |
+
"timestamp": doc["timestamp"].isoformat() if doc.get("timestamp") else None
|
| 245 |
+
})
|
| 246 |
+
|
| 247 |
+
return results
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# ============== Analytics Events (MongoDB) ==============
|
| 251 |
+
|
| 252 |
+
async def track_analytics_event(
|
| 253 |
+
event_type: str,
|
| 254 |
+
properties: Dict[str, Any] = None
|
| 255 |
+
) -> Dict[str, Any]:
|
| 256 |
+
"""Track an analytics event."""
|
| 257 |
+
if _mongo_db is None:
|
| 258 |
+
return None
|
| 259 |
+
|
| 260 |
+
document = {
|
| 261 |
+
"event_type": event_type,
|
| 262 |
+
"properties": properties or {},
|
| 263 |
+
"timestamp": datetime.utcnow()
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
result = await _mongo_db.analytics.insert_one(document)
|
| 267 |
+
return {
|
| 268 |
+
"id": str(result.inserted_id),
|
| 269 |
+
**document
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
async def get_analytics_summary(
|
| 274 |
+
event_type: str = None,
|
| 275 |
+
hours: int = 24
|
| 276 |
+
) -> Dict[str, Any]:
|
| 277 |
+
"""Get analytics summary for the last N hours."""
|
| 278 |
+
if _mongo_db is None:
|
| 279 |
+
return {}
|
| 280 |
+
|
| 281 |
+
from datetime import timedelta
|
| 282 |
+
since = datetime.utcnow() - timedelta(hours=hours)
|
| 283 |
+
|
| 284 |
+
query = {"timestamp": {"$gte": since}}
|
| 285 |
+
if event_type:
|
| 286 |
+
query["event_type"] = event_type
|
| 287 |
+
|
| 288 |
+
total_events = await _mongo_db.analytics.count_documents(query)
|
| 289 |
+
|
| 290 |
+
# Get event type breakdown
|
| 291 |
+
pipeline = [
|
| 292 |
+
{"$match": query},
|
| 293 |
+
{"$group": {
|
| 294 |
+
"_id": "$event_type",
|
| 295 |
+
"count": {"$sum": 1}
|
| 296 |
+
}},
|
| 297 |
+
{"$sort": {"count": -1}}
|
| 298 |
+
]
|
| 299 |
+
|
| 300 |
+
breakdown = []
|
| 301 |
+
async for doc in _mongo_db.analytics.aggregate(pipeline):
|
| 302 |
+
breakdown.append({
|
| 303 |
+
"event_type": doc["_id"],
|
| 304 |
+
"count": doc["count"]
|
| 305 |
+
})
|
| 306 |
+
|
| 307 |
+
return {
|
| 308 |
+
"total_events": total_events,
|
| 309 |
+
"period_hours": hours,
|
| 310 |
+
"breakdown": breakdown
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
# ============== User Search History (MongoDB) ==============
|
| 315 |
+
|
| 316 |
+
async def log_search(
|
| 317 |
+
user_id: int = None,
|
| 318 |
+
search_type: str = None,
|
| 319 |
+
query: str = None,
|
| 320 |
+
results_count: int = 0
|
| 321 |
+
) -> Dict[str, Any]:
|
| 322 |
+
"""Log a search event."""
|
| 323 |
+
if _mongo_db is None:
|
| 324 |
+
return None
|
| 325 |
+
|
| 326 |
+
document = {
|
| 327 |
+
"user_id": user_id,
|
| 328 |
+
"search_type": search_type,
|
| 329 |
+
"query": query,
|
| 330 |
+
"results_count": results_count,
|
| 331 |
+
"timestamp": datetime.utcnow()
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
result = await _mongo_db.search_history.insert_one(document)
|
| 335 |
+
return {
|
| 336 |
+
"id": str(result.inserted_id),
|
| 337 |
+
**document
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
async def get_popular_searches(
|
| 342 |
+
hours: int = 24,
|
| 343 |
+
limit: int = 10
|
| 344 |
+
) -> List[Dict[str, Any]]:
|
| 345 |
+
"""Get popular search queries."""
|
| 346 |
+
if _mongo_db is None:
|
| 347 |
+
return []
|
| 348 |
+
|
| 349 |
+
from datetime import timedelta
|
| 350 |
+
since = datetime.utcnow() - timedelta(hours=hours)
|
| 351 |
+
|
| 352 |
+
pipeline = [
|
| 353 |
+
{"$match": {"timestamp": {"$gte": since}}},
|
| 354 |
+
{"$group": {
|
| 355 |
+
"_id": "$query",
|
| 356 |
+
"count": {"$sum": 1},
|
| 357 |
+
"search_type": {"$first": "$search_type"}
|
| 358 |
+
}},
|
| 359 |
+
{"$sort": {"count": -1}},
|
| 360 |
+
{"$limit": limit}
|
| 361 |
+
]
|
| 362 |
+
|
| 363 |
+
results = []
|
| 364 |
+
async for doc in _mongo_db.search_history.aggregate(pipeline):
|
| 365 |
+
results.append({
|
| 366 |
+
"query": doc["_id"],
|
| 367 |
+
"search_type": doc.get("search_type"),
|
| 368 |
+
"count": doc["count"]
|
| 369 |
+
})
|
| 370 |
+
|
| 371 |
+
return results
|
pyproject.toml
CHANGED
|
@@ -14,6 +14,8 @@ dependencies = [
|
|
| 14 |
"huggingface-hub>=1.8.0",
|
| 15 |
"peft>=0.18.1",
|
| 16 |
"redis>=5.0.0",
|
|
|
|
|
|
|
| 17 |
"sentence-transformers>=3.0.0",
|
| 18 |
"torch>=2.5.0",
|
| 19 |
"torchaudio>=2.5.0",
|
|
|
|
| 14 |
"huggingface-hub>=1.8.0",
|
| 15 |
"peft>=0.18.1",
|
| 16 |
"redis>=5.0.0",
|
| 17 |
+
"pymongo>=4.6.0",
|
| 18 |
+
"motor>=3.4.0",
|
| 19 |
"sentence-transformers>=3.0.0",
|
| 20 |
"torch>=2.5.0",
|
| 21 |
"torchaudio>=2.5.0",
|