Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ ALPHATRION_CLICKHOUSE_ENABLE_BATCH=true
ALPHATRION_ENABLE_PROMETHEUS=false
ALPHATRION_PROMETHEUS_PUSHGATEWAY_URL=localhost:9091
ALPHATRION_PROMETHEUS_JOB_NAME=alphatrion

# Authentication configurations
# Set to true to enable JWT authentication, false to use direct headers (x-user-id, x-org-id)
ALPHATRION_ENABLE_AUTH=true
ALPHATRION_JWT_SECRET=your_jwt_secret_key_here
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</p>

<h3 align="center">
Open, modular framework to build and optimize GenAI applications
⚒️ The observability platform for agentic systems.
</h3>

[![stability-alpha](https://img.shields.io/badge/stability-alpha-f4d03f.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#alpha)
Expand All @@ -25,11 +25,15 @@ Open, modular framework to build and optimize GenAI applications
- **🤖 Model Distribution** - Analyze request patterns and usage across different AI models
- **📦 Artifact Management** - Store and version execution results, checkpoints, and model outputs
- **🎯 Interactive Dashboard** - Modern web UI for exploring experiments, metrics, and traces
- **🔐 Secure Authentication** - JWT-based authentication with user profiles and multi-team support
- **👥 Multi-User Support** - Collaborative workspace with organization and team management
- **🔌 Easy Integration** - Simple Python API with async/await support

## Core Concepts

- **Team** - Top-level organizational unit for user collaboration
- **Organization** - Top-level entity for grouping teams and users
- **Team** - Collaborative workspace for organizing experiments and runs
- **User** - Individual account with secure authentication and team memberships
- **Experiment** - Logical grouping of runs with shared purpose, organized by labels
- **Run** - Individual execution instance with configuration and metrics

Expand All @@ -56,12 +60,10 @@ make up
# Wait for services to be ready, then run migrations
make migrate

# Initialize your team and user
alphatrion init # Use -h for custom options
# Initialize your organization, team, and user account
alphatrion init
```

Save the generated user ID — you'll need it to track experiments.

**Optional Tools:**
- pgAdmin: `http://localhost:8081` (alphatrion@inftyai.com / alphatr1on)
- Registry UI: `http://localhost:80`
Expand Down Expand Up @@ -96,7 +98,7 @@ alphatrion server
alphatrion dashboard
```

Access the dashboard at `http://127.0.0.1:5173` to explore experiments, visualize metrics, and analyze traces.
Access the dashboard at `http://127.0.0.1:5173` and **log in with your email and password** to explore experiments, visualize metrics, and analyze traces.

![dashboard](./site/images/dashboard.png)

Expand All @@ -106,13 +108,6 @@ AlphaTrion automatically captures distributed tracing data for all LLM calls, in

![tracing](./site/images/trace.png)

### 6. Other APIs

- **log_params**: Track hyperparameters and configuration settings
- **log_metrics**: Record performance metrics and visualize trends
- **log_artifacts**: Store and manage files, checkpoints, and model outputs


### Cleanup

```bash
Expand Down
3 changes: 3 additions & 0 deletions alphatrion/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@

# Runtime related envs
ROOT_PATH = "ALPHATRION_ROOT_PATH"

ENABLE_AUTH = "ALPHATRION_ENABLE_AUTH"
JWT_SECRET = "ALPHATRION_JWT_SECRET"
90 changes: 90 additions & 0 deletions alphatrion/server/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Authentication utilities for JWT-based authentication."""

import os
from datetime import UTC, datetime, timedelta
from typing import Any

import bcrypt
from jose import JWTError, jwt

from alphatrion import envs

# JWT settings
SECRET_KEY = os.getenv(envs.JWT_SECRET, "your-secret-key-change-this-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_DAYS = 7


def hash_password(password: str) -> str:
"""Hash a password using bcrypt.

Args:
password: Plain text password

Returns:
Bcrypt hash of the password
"""
# Convert password to bytes
password_bytes = password.encode("utf-8")
# Generate salt and hash
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
# Return as string
return hashed.decode("utf-8")


def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a bcrypt hash.

Args:
plain_password: Plain text password to verify
hashed_password: Bcrypt hash to verify against

Returns:
True if password matches, False otherwise
"""
try:
password_bytes = plain_password.encode("utf-8")
hashed_bytes = hashed_password.encode("utf-8")
return bcrypt.checkpw(password_bytes, hashed_bytes)
except Exception:
return False


def create_access_token(
data: dict[str, Any], expires_delta: timedelta | None = None
) -> str:
"""Create a JWT access token.

Args:
data: Dictionary containing claims to encode in the token
expires_delta: Optional token expiration time

Returns:
Encoded JWT token string
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.now(UTC) + expires_delta
else:
expire = datetime.now(UTC) + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)

to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


def decode_access_token(token: str) -> dict[str, Any] | None:
"""Decode and validate a JWT access token.

Args:
token: JWT token string

Returns:
Dictionary containing the decoded claims, or None if invalid
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
141 changes: 140 additions & 1 deletion alphatrion/server/cmd/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@
# ruff: noqa: B904

import logging
import uuid
from importlib.metadata import version

from fastapi import FastAPI, Request
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from strawberry.fastapi import GraphQLRouter

from alphatrion.server.auth import (
create_access_token,
decode_access_token,
hash_password,
verify_password,
)
from alphatrion.server.graphql.context import get_context
from alphatrion.server.graphql.schema import schema
from alphatrion.storage import runtime

# Configure logging
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -117,3 +126,133 @@ def health_check():
@app.get("/version")
def get_version():
return {"version": version("alphatrion"), "status": "ok"}


# Auth endpoints
class LoginRequest(BaseModel):
email: str
password: str


class LoginResponse(BaseModel):
access_token: str
token_type: str
user: dict


@app.post("/api/auth/login", response_model=LoginResponse)
async def login(credentials: LoginRequest):
"""Authenticate user and return JWT token with user information."""
try:
metadb = runtime.storage_runtime().metadb

# Find user by email
user = metadb.get_user_by_email(email=credentials.email)

if not user:
raise HTTPException(status_code=401, detail="Invalid email or password")

# Verify password
if not verify_password(credentials.password, user.password_hash):
raise HTTPException(status_code=401, detail="Invalid email or password")

# Get user's teams
team_members = metadb.get_team_members_by_user_id(user_id=user.uuid)
teams = []
for member in team_members:
team = metadb.get_team(team_id=member.team_id)
if team:
teams.append(
{
"id": str(team.uuid),
"name": team.name,
"description": team.description,
}
)

# Create JWT token with user claims
# Note: team_id is NOT included - users can belong to multiple teams
# Team selection is handled in the UI
token_data = {
"sub": str(user.uuid), # subject = user_id
"user_id": str(user.uuid),
"org_id": str(user.org_id),
"email": user.email,
}

access_token = create_access_token(data=token_data)

# Return token and user info
return {
"access_token": access_token,
"token_type": "bearer",
"user": {
"id": str(user.uuid),
"name": user.name,
"email": user.email,
"avatarUrl": user.avatar_url,
"meta": user.meta,
"createdAt": user.created_at.isoformat(),
"updatedAt": user.updated_at.isoformat(),
"teams": teams,
},
}

except HTTPException:
raise
except Exception as e:
logger.error(f"Login failed: {e}")
raise HTTPException(status_code=500, detail="Internal server error")


class ChangePasswordRequest(BaseModel):
current_password: str
new_password: str


@app.post("/api/auth/change-password")
async def change_password(request: Request, password_data: ChangePasswordRequest):
"""Change user's password."""
try:
# Extract token from Authorization header
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=401, detail="Missing or invalid authorization header"
)

token = auth_header.replace("Bearer ", "")

# Decode token to get user_id
payload = decode_access_token(token)
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")

user_id = payload.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token payload")

metadb = runtime.storage_runtime().metadb

# Get user from database
user = metadb.get_user(user_id=uuid.UUID(user_id))
if not user:
raise HTTPException(status_code=404, detail="User not found")

# Verify current password
if not verify_password(password_data.current_password, user.password_hash):
raise HTTPException(status_code=401, detail="Current password is incorrect")

# Hash new password
new_password_hash = hash_password(password_data.new_password)

# Update password in database
metadb.update_user(user_id=uuid.UUID(user_id), password_hash=new_password_hash)

return {"message": "Password changed successfully"}

except HTTPException:
raise
except Exception as e:
logger.error(f"Password change failed: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
Loading
Loading