Skip to content

Commit 3dc87af

Browse files
committed
Merge branch 'main' into feature/frontend-pdfchat
# Conflicts: # backend/app/llm/openai/provider.py # backend/app/main.py # backend/app/routes/paper_routes.py # backend/app/services/paper_service.py # frontend/src/stores/chat.ts
2 parents 5fc13a9 + 7470db3 commit 3dc87af

53 files changed

Lines changed: 1977 additions & 658 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ backend/ingestion/data
1919
backend/inquiro-env/
2020

2121
# Claude Code
22-
.claude
22+
.claude/
2323

2424
# Evaluation data and results
2525
backend/evaluation_data/

CLAUDE.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Inquiro is an AI-powered research discovery platform that matches user research interests with relevant scientific papers using semantic ranking. It uses SPECTER2 embeddings for paper similarity and pgvector for vector search.
8+
9+
## Development Commands
10+
11+
### Database
12+
```bash
13+
docker compose up -d # Start PostgreSQL with pgvector
14+
```
15+
16+
### Backend (from /backend directory)
17+
```bash
18+
pip install -r requirements.txt
19+
pip install -r requirements-dev.txt # Includes ruff and pre-commit
20+
pre-commit install # Setup git hooks for linting
21+
uvicorn app.main:app --reload # Start dev server on :8000
22+
```
23+
24+
Requires `dev.env` file (copy from `.env.example`).
25+
26+
### Frontend (from /frontend directory)
27+
```bash
28+
npm install
29+
npm run dev # Start Vite dev server on :5173
30+
npm run lint # ESLint check
31+
npm run lint:fix # ESLint fix
32+
npm run format # Prettier
33+
npm run build # Production build
34+
```
35+
36+
### API Client Generation
37+
```bash
38+
cd openapi
39+
python generate.py # Regenerates frontend/src/api/ from OpenAPI spec
40+
```
41+
Requires Docker. Generates TypeScript Axios client from FastAPI OpenAPI schema.
42+
43+
## Architecture
44+
45+
### Backend (FastAPI + SQLAlchemy async)
46+
- `app/main.py` - FastAPI app entry point, registers all routers
47+
- `app/core/config.py` - Settings via pydantic-settings (loads from `dev.env`)
48+
- `app/core/database.py` - Async SQLAlchemy engine, session factory, Base model
49+
- `app/routes/` - API endpoint handlers (auth, users, papers, projects, search)
50+
- `app/services/` - Business logic layer
51+
- `app/repositories/` - Database access layer
52+
- `app/models/` - SQLAlchemy ORM models
53+
- `app/schemas/` - Pydantic DTOs (*_dto.py)
54+
- `app/llm/embeddings/specter2.py` - SPECTER2 embeddings for semantic paper search
55+
- `app/llm/openai/` - OpenAI integration for paper summaries
56+
- `ingestion/` - Scripts for ingesting papers from arXiv
57+
58+
Database: PostgreSQL 18 with pgvector extension for vector similarity search.
59+
60+
### Frontend (Vue 3 + TypeScript)
61+
- Uses Composition API with `<script setup>`
62+
- `src/api/` - Auto-generated Axios client from OpenAPI (do not edit manually)
63+
- `src/services/` - Service wrappers around API client
64+
- `src/stores/` - Pinia stores with persistence (auth uses sessionStorage)
65+
- `src/components/` - Atomic design: atoms, molecules, organisms, templates
66+
- `src/pages/` - Route-level page components
67+
- `src/composables/` - Reusable composition functions (useTheme, useScrollToTop)
68+
- UI: Vuetify 3 + Tailwind CSS + Lucide icons
69+
70+
### API Communication
71+
Frontend API types are generated from backend OpenAPI schema. When backend endpoints change:
72+
1. Update backend routes/schemas
73+
2. Run `python openapi/generate.py` to regenerate frontend types
74+
75+
## Code Style
76+
77+
### Backend
78+
- Ruff for linting and formatting (configured in pyproject.toml)
79+
- Line length: 100 characters
80+
- Pre-commit hooks run ruff on commit
81+
82+
### Frontend
83+
- ESLint + Prettier
84+
- Husky + lint-staged for pre-commit hooks

backend/.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
repos:
2-
- repo: https://github.com/astral-sh/ruff-pre-commit
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
33
rev: 'v0.5.0' # Use a recent ruff version
44
hooks:
5-
- id: ruff
6-
args: [--fix, --exit-non-zero-on-fix]
7-
- id: ruff-format
5+
- id: ruff
6+
args: [ --fix, --exit-non-zero-on-fix ]
7+
- id: ruff-format

backend/app/constants/database_constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ class PaperType(Enum):
1818
WORKSHOP = "WORKSHOP"
1919
THESIS = "THESIS"
2020
OTHER = "OTHER"
21+
22+
23+
class PaperContentStatus(Enum):
24+
"""Lifecycle status of paper content parsing and storage."""
25+
PENDING = "PENDING"
26+
PROCESSING = "PROCESSING"
27+
SUCCEEDED = "SUCCEEDED"
28+
FAILED = "FAILED"

backend/app/core/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class Settings(BaseSettings):
2929
# --- OpenAI ---
3030
OPENAI_API_KEY: Optional[str] = None
3131

32+
# --- Docling PDF Conversion ---
33+
DOCLING_WORKERS: int = 2
34+
DOCLING_TIMEOUT_SECONDS: int = 300
35+
DOCLING_MAX_RETRIES: int = 3
36+
DOCLING_RETRY_BASE_DELAY: int = 30 # seconds, doubles each attempt
37+
3238
model_config = SettingsConfigDict(
3339
env_file=os.getenv("ENV_FILE", "dev.env"),
3440
extra="ignore",

backend/app/core/database.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async def init_db() -> None:
3636
"app.models.project",
3737
"app.models.paper",
3838
"app.models.project_paper",
39+
"app.models.paper_content",
3940
):
4041
import_module(module)
4142

backend/app/core/deps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def get_specter2_proximity_embedder() -> Specter2Embedder:
1515
@lru_cache(maxsize=1)
1616
def get_specter2_query_embedder() -> Specter2Embedder:
1717
"""Return a shared Specter2Embedder instance, initialized once."""
18-
return Specter2Embedder(model="allenai/specter2_adhoc_query")
18+
return Specter2Embedder(adapter="allenai/specter2_adhoc_query")
1919

2020

2121
@lru_cache(maxsize=1)

backend/app/core/security.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta]
1919

2020
to_encode = data.copy()
2121
expire = datetime.now(timezone.utc) + (
22-
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
22+
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
2323
)
2424
to_encode.update({"exp": expire})
2525
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=ALGORITHM)
@@ -30,7 +30,7 @@ def create_refresh_token(data: Dict[str, Any], expires_delta: Optional[timedelta
3030

3131
to_encode = data.copy()
3232
expire = datetime.now(timezone.utc) + (
33-
expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
33+
expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
3434
)
3535
to_encode.update({"exp": expire, "type": "refresh"})
3636
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=ALGORITHM)

backend/app/llm/embeddings/specter2.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from typing import List, Optional
33

44
import torch
5-
from transformers import AutoAdapterModel, AutoTokenizer
5+
from adapters import AutoAdapterModel
6+
from transformers import AutoTokenizer
67

78
logger = logging.getLogger(__name__)
89

@@ -20,31 +21,46 @@ def build_specter2_text(title: str, abstract: str, tokenizer: AutoTokenizer) ->
2021
class Specter2Embedder:
2122
"""Wrapper around the SPECTER2 retrieval (proximity) model."""
2223

23-
def __init__(self, model: str = "allenai/specter2", device: Optional[str] = None) -> None:
24+
def __init__(
25+
self, adapter: str = "allenai/specter2_base", device: Optional[str] = None
26+
) -> None:
2427
"""Load SPECTER2 tokenizer, base model, and proximity adapter."""
28+
2529
if device is None:
2630
device = "cuda" if torch.cuda.is_available() else "cpu"
2731

2832
self.device = torch.device(device)
29-
logger.info("Loading SPECTER2 base + proximity adapter on %s...", self.device)
33+
self.adapter = adapter
3034

31-
# Load tokenizer + base model
32-
self.tokenizer = AutoTokenizer.from_pretrained("allenai/specter2_base")
35+
# Tokenizer (shared across all variants)
36+
self.tokenizer = AutoTokenizer.from_pretrained(
37+
"allenai/specter2_base",
38+
local_files_only=False,
39+
trust_remote_code=False,
40+
)
3341

34-
self.model = AutoAdapterModel.from_pretrained("allenai/specter2_base")
42+
# Adapter-aware model
43+
self.model = AutoAdapterModel.from_pretrained(
44+
"allenai/specter2_base",
45+
local_files_only=False,
46+
trust_remote_code=False,
47+
)
3548

36-
# Load retrieval adapter (proximity)
49+
# Load and activate adapter
3750
self.model.load_adapter(
38-
model,
39-
load_as="specter2",
40-
set_active=True,
51+
adapter,
4152
source="hf",
53+
set_active=True,
4254
)
4355

4456
self.model.to(self.device)
4557
self.model.eval()
4658

47-
logger.info("SPECTER2 model + adapter loaded successfully.")
59+
logger.info(
60+
"Loaded SPECTER2 model with adapter '%s' on device '%s'",
61+
adapter,
62+
self.device,
63+
)
4864

4965
def embed_batch(self, texts: List[str]) -> List[Optional[List[float]]]:
5066
"""

backend/app/llm/evaluation/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ python -m app.llm.evaluation.dataset_generator \
3434
Note: The default output is `evaluation_data/dataset.json` (directories are created automatically).
3535

3636
This will create the dataset file with test cases containing:
37+
3738
- `ground_truth_keywords`: The original keywords
3839
- `user_input`: Generated natural language query
3940
- `metadata`: Generation info
@@ -58,6 +59,7 @@ python -m app.llm.evaluation.prompt_evaluator \
5859
Note: The default output is `evaluation_results/results.json` (directories are created automatically).
5960

6061
The results will include:
62+
6163
- `mean_jaccard`: Average Jaccard similarity score
6264
- `std_jaccard`: Standard deviation
6365
- `min_jaccard` / `max_jaccard`: Score range
@@ -66,6 +68,7 @@ The results will include:
6668
## Metrics
6769

6870
**Jaccard Similarity**: Measures overlap between extracted and ground truth keywords
71+
6972
- Formula: `|A ∩ B| / |A ∪ B|`
7073
- Range: 0.0 (no overlap) to 1.0 (perfect match)
7174
- Keywords are normalized (lowercase, stripped) before comparison
@@ -127,6 +130,7 @@ python -m app.llm.evaluation.evaluate_all_prompts \
127130
```
128131

129132
This will:
133+
130134
- Evaluate all prompts matching the pattern (default: `prompt_*.txt`)
131135
- Save individual results for each prompt
132136
- Generate a comparison report ranking prompts by mean Jaccard score

0 commit comments

Comments
 (0)