- Implemented `conversation-store` for managing conversations and messages. - Created `file-preview-store` to handle file preview state. - Added `sidebar-store` for sidebar visibility management. - Developed `theme-store` for theme persistence and management. - Introduced `kb-selection-store` for managing active knowledge base selections with persistence. chore: define API and chat types - Added types for API responses, authentication, chat messages, conversations, and projects. - Defined interfaces for various entities including users, sessions, and message ratings. build: configure TypeScript and testing setup - Set up `tsconfig.json` for TypeScript configuration. - Created `vitest.config.ts` for testing configuration with Vitest. - Added `vitest.setup.ts` for global test setup including mocks for Next.js router and media queries. - Configured Vercel deployment settings in `vercel.json`.
10 KiB
Architecture Guide
This project follows a Repository + Service layered architecture. Every feature — users, conversations, files, RAG documents, sync sources — uses the same pattern: Models → Schemas → Repositories → Services → Endpoints.
Request Flow
HTTP Request → API Route → Service → Repository → Database
↓
Response ← Service ← Repository ←
Routes never contain direct database calls. All data access goes through services, which in turn delegate to repositories.
Directory Structure (backend/app/)
| Directory / File | Purpose |
|---|---|
api/routes/v1/ |
HTTP endpoints, request validation, auth |
api/deps.py |
Dependency injection (db session, current user) |
services/ |
Business logic, orchestration |
↳ user.py |
User CRUD, profile updates |
↳ conversation.py |
Conversation & message management |
↳ message_rating.py |
Message rating CRUD, statistics, export |
↳ file_upload.py |
Chat file upload handling |
↳ file_storage.py |
File storage abstraction (local / S3) |
↳ rag_document.py |
RAG document lifecycle |
↳ rag_sync.py |
Remote-source sync orchestration |
↳ sync_source.py |
Sync-source CRUD |
repositories/ |
Data access layer, database queries |
↳ user.py |
User queries |
↳ conversation.py |
Conversation queries |
↳ chat_file.py |
Chat file queries |
↳ message_rating.py |
Message rating queries |
↳ rag_document.py |
RAG document queries |
↳ sync_log.py |
Sync log queries |
↳ sync_source.py |
Sync source queries |
schemas/ |
Pydantic request/response models |
↳ user.py |
User schemas |
↳ conversation.py |
Conversation & message schemas |
↳ file.py |
File upload schemas |
↳ message_rating.py |
Message rating schemas |
↳ rag.py |
RAG query/response schemas |
↳ sync_source.py |
Sync source schemas |
db/models/ |
SQLAlchemy / MongoDB models |
↳ user.py |
User model |
↳ conversation.py |
Conversation & message models |
↳ chat_file.py |
Chat file model |
↳ message_rating.py |
Message rating model |
↳ webhook.py |
Webhook model |
↳ rag_document.py |
RAG document model |
↳ sync_log.py |
Sync log model |
↳ sync_source.py |
Sync source model |
core/config.py |
Settings via pydantic-settings |
core/security.py |
JWT / API key utilities |
agents/ |
AI agents and tools |
rag/ |
RAG module (embeddings, vector store, retrieval) |
rag/connectors/ |
Sync connectors (Google Drive, S3) |
commands/ |
Django-style CLI commands |
worker/ |
Background task definitions |
Layer Responsibilities
API Routes (api/routes/v1/)
- HTTP request/response handling
- Input validation via Pydantic schemas
- Authentication and authorization checks
- Never contains direct DB calls — always delegates to a service
Services (services/)
- Business logic and validation
- Orchestrates one or more repository calls
- Raises domain exceptions (
NotFoundError,AlreadyExistsError, etc.) - Manages transaction boundaries
Repositories (repositories/)
- Database operations only
- No business logic
- Uses
db.flush()notcommit()(the dependency-injected session manages transactions) - Returns domain models
Schemas (schemas/)
- Separate
Create,Update, andResponsemodels per entity Responseschemas usemodel_config = ConfigDict(from_attributes=True)for ORM conversion
Models (db/models/)
- SQLAlchemy (or MongoDB) model definitions
- Relationships, indexes, and column defaults live here
RAG Connectors (rag/connectors/)
- Pluggable sync adapters that implement
BaseSyncConnector - Each connector provides
list_files()anddownload_file() - Registered in
CONNECTOR_REGISTRYfor discovery at runtime
Key Files
- Entry point:
app/main.py - Configuration:
app/core/config.py - Dependencies:
app/api/deps.py - Auth utilities:
app/core/security.py - Exception handlers:
app/api/exception_handlers.py
Authentication & Authorization
Authentication Methods
The project supports two authentication methods, both always available:
-
JWT (JSON Web Tokens) -- Used by the frontend and API clients.
- Login via
POST /api/v1/auth/loginreturnsaccess_token+refresh_token. - Access tokens expire after
ACCESS_TOKEN_EXPIRE_MINUTES(default 30 min). - Refresh tokens expire after
REFRESH_TOKEN_EXPIRE_MINUTES(default 7 days). - The frontend stores tokens as HTTP-only cookies.
- WebSocket auth passes the JWT as a query parameter (
?token=<jwt>) or cookie.
- Login via
-
API Key -- Used for server-to-server and programmatic access.
- Passed via the
X-API-Keyheader (configurable viaAPI_KEY_HEADER). - A single shared key set via the
API_KEYenvironment variable. - Uses constant-time comparison (
secrets.compare_digest) to prevent timing attacks.
- Passed via the
Roles
Two roles are defined in UserRole (see app/db/models/user.py):
| Role | Value | Description |
|---|---|---|
| ADMIN | "admin" |
Full system access, can manage users, RAG, webhooks, exports |
| USER | "user" |
Standard access: chat, profile, search |
Role hierarchy: ADMIN has access to everything. The has_role() method on the
User model returns True for any role if the user is an admin.
How RoleChecker Works
RoleChecker is a callable FastAPI dependency class in app/api/deps.py:
class RoleChecker:
def __init__(self, required_role: UserRole) -> None:
self.required_role = required_role
async def __call__(self, user: User = Depends(get_current_user)) -> User:
if not user.has_role(self.required_role):
raise AuthorizationError(...)
return user
Use it in routes:
# Any authenticated user
@router.get("/profile")
async def profile(current_user: CurrentUser): ...
# Admin only
@router.get("/all-users")
async def list_users(current_user: CurrentAdmin): ...
The type aliases are:
CurrentUser=Annotated[User, Depends(get_current_user)]-- any authenticated userCurrentAdmin=Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]-- admin role requiredCurrentSuperuser=Annotated[User, Depends(get_current_active_superuser)]-- legacy alias for admin
IDOR Protection
Conversation and file endpoints enforce ownership at the service layer:
- Conversations pass
user_id=current_user.idto the service, which filters queries by owner. - File downloads verify
chat_file.user_id == current_user.idbefore returning the file. - This prevents users from accessing resources belonging to other users.
For full endpoint-level permissions, see docs/permissions.md.
File Processing in Chat
When a user uploads a file in the chat interface, the following pipeline executes:
Upload (POST /files/upload)
-> Validate (MIME type + size)
-> Classify (image / pdf / docx / text)
-> Parse (extract text content)
-> Store (save to media/{user_id}/)
-> Record (create ChatFile in DB)
-> Link (attach to message when sent)
Supported File Types
| Category | Extensions | Processing |
|---|---|---|
| Images | JPEG, PNG, WebP, GIF | Stored as-is, sent to LLM as binary for vision |
| Text extracted via configured parser | ||
| Documents | .docx | Text extracted via python-docx |
| Text | .txt, .md | UTF-8 decoded directly |
Parser Selection
PDFs are parsed using PyMuPDF (fast, local, no API key needed).
Storage
Files are saved to media/{user_id}/ via FileStorageService. The ChatFile
model stores the storage_path, filename, mime_type, size, file_type,
and parsed_content (extracted text). Only the file owner can access their files.
Size Limits
Maximum upload size is controlled by MAX_UPLOAD_SIZE_MB (default 50MB).
RAG System
Architecture Overview
The RAG (Retrieval Augmented Generation) system provides a knowledge base that the AI agent can search during conversations. It is composed of:
Documents -> Parse -> Chunk -> Embed -> Vector Store
|
User Query -> Embed -> Search -> Rerank? -> Results -> Agent Prompt
Key Principle: RAG is Global
Collections are shared across ALL users. There is no per-user document isolation. This means:
- Any authenticated user can search any collection.
- Only admins can create/delete collections, upload documents, configure sync sources, and view sync logs.
- The knowledge base serves as an organization-wide shared resource.
Components
| Component | File | Purpose |
|---|---|---|
DocumentProcessor |
rag/documents.py |
Parses files into text (PDF, DOCX, TXT, images) |
IngestionService |
rag/ingestion.py |
Orchestrates parse -> chunk -> embed -> store |
RetrievalService |
rag/retrieval.py |
Handles search queries with filtering and scoring |
EmbeddingService |
rag/embeddings.py |
Generates embeddings via configured provider |
BaseVectorStore |
rag/vectorstore.py |
Abstract interface for vector database operations |
MilvusVectorStore |
rag/vectorstore.py |
Milvus implementation |
Ingestion Pipeline
Documents can be ingested via:
- CLI --
uv run ai_agent cmd rag-ingest <path> - API --
POST /api/v1/rag/collections/{name}/ingest(admin only, file upload) - Sync Sources -- Configured connectors (Google Drive, S3) that pull documents on a schedule or on-demand.
Each ingested document gets:
- Parsed into text (with parser selected by
PDF_PARSERenv var) - Split into chunks (configurable
RAG_CHUNK_SIZE/RAG_CHUNK_OVERLAP) - Embedded via the configured embedding provider
- Stored in the vector database
- Tracked in SQL via
RAGDocumentmodel with status (processing,done,error)
Sync Modes
| Mode | Behavior |
|---|---|
full |
Replace all documents (re-ingest everything) |
new_only |
Add new files, re-ingest files whose content hash changed, skip unchanged |
update_only |
Only re-ingest changed files, skip new files entirely |
Sync Connectors
Remote document sources use pluggable connectors in rag/connectors/. Each
connector implements BaseSyncConnector with list_files() and download_file()
methods. See docs/patterns.md for how to add a new connector.