| from sqlalchemy import select, desc | |
| from ..database.db import session_pool | |
| from ..model.paper import Paper | |
| from ..model.arxiv_catalog import ArxivCatalog | |
| from ..schema.arxiv import ArxivEntry | |
| from ..controller.catalog_controller import get_feed | |
| from ..core.recommendation_engine.recommender import ( | |
| get_feed_recommendations, | |
| rerank_results, | |
| search_catalog, | |
| ) | |
| from ..core.logger import SingletonLogger | |
| logger = SingletonLogger().get_logger() | |
| MAX_SEED_PAPERS = 5 | |
| def _qdrant_results_to_entries(items: list[dict]) -> list[ArxivEntry]: | |
| entries: list[ArxivEntry] = [] | |
| for item in items: | |
| authors_str = item.get("authors") or "" | |
| authors = [a.strip() for a in authors_str.split(";") if a.strip()] if authors_str else [] | |
| categories_str = item.get("categories") or "" | |
| categories = categories_str.split() if categories_str else [] | |
| entries.append( | |
| ArxivEntry( | |
| id=str(item.get("arxiv_id") or ""), | |
| arxiv_id=item.get("arxiv_id"), | |
| title=item.get("title") or "Untitled", | |
| abstract=item.get("abstract") or "", | |
| authors=authors, | |
| categories=categories, | |
| primary_category=item.get("primary_category"), | |
| published=item.get("published_date"), | |
| updated=None, | |
| pdf_url=item.get("pdf_url"), | |
| paper_url=item.get("paper_url"), | |
| ) | |
| ) | |
| return entries | |
| async def search_catalog_papers( | |
| query: str, | |
| start: int = 0, | |
| limit: int = 20, | |
| ) -> list[ArxivEntry]: | |
| try: | |
| results = await search_catalog(query=query, offset=start, limit=limit) | |
| return _qdrant_results_to_entries(results) | |
| except Exception as exc: | |
| logger.error("Catalog search failed: %s", exc) | |
| raise | |
| async def get_smart_feed( | |
| user_id: int, | |
| topics: list[str], | |
| start: int = 0, | |
| limit: int = 24, | |
| ) -> list[ArxivEntry]: | |
| async with session_pool() as session: | |
| saved_result = await session.execute( | |
| select(Paper) | |
| .where(Paper.user_id == user_id, Paper.arxiv_id.isnot(None)) | |
| .order_by(desc(Paper.created_at)) | |
| ) | |
| saved_papers = saved_result.scalars().all() | |
| if not saved_papers: | |
| return await get_feed(topics=topics, start=start, limit=limit) | |
| saved_arxiv_ids = [p.arxiv_id for p in saved_papers if p.arxiv_id] | |
| seed_papers = saved_papers[:MAX_SEED_PAPERS] | |
| query_texts = [ | |
| f"{p.title} {(p.abstract or '')[:200]}" for p in seed_papers | |
| ] | |
| async with session_pool() as session: | |
| cat_result = await session.execute( | |
| select(ArxivCatalog.primary_category) | |
| .where(ArxivCatalog.arxiv_id.in_(saved_arxiv_ids)) | |
| .distinct() | |
| ) | |
| user_saved_categories = [row[0] for row in cat_result.all() if row[0]] | |
| try: | |
| results = await get_feed_recommendations( | |
| query_texts=query_texts, | |
| category_filter=topics, | |
| exclude_ids=saved_arxiv_ids, | |
| offset=start, | |
| limit=limit, | |
| ) | |
| reranked = rerank_results(results, user_saved_categories)[:limit] | |
| return _qdrant_results_to_entries(reranked) | |
| except Exception as exc: | |
| logger.warning("Qdrant unavailable, falling back to catalog feed: %s", exc) | |
| return await get_feed(topics=topics, start=start, limit=limit) | |