Skip to content
Open
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: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ build/

# Accidental output files

# Data files (optional - uncomment if you don't want to track conversation data)
# data/conversations/*.json
# Data files - user data and conversations (sensitive)
data/users.json
data/conversations/*.json
20 changes: 20 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Azure OpenAI Configuration
# Set your Azure OpenAI endpoint here
AZURE_ENDPOINT=https://your-azure-openai-endpoint.openai.azure.com/

# Azure AI Projects endpoint (for web-search agents)
# Uncomment and set if using web search features
# AZURE_AI_PROJECT_ENDPOINT=https://your-ai-project.azure.com

# Azure Storage Configuration (optional - local file storage is used by default)
# Uncomment to use Azure Blob Storage instead of local files
# AZURE_STORAGE_ACCOUNT_NAME=your-storage-account
# AZURE_STORAGE_CONTAINER_NAME=conversations

# Authentication Configuration
# IMPORTANT: Change these values in production!
# Default admin credentials will be created on first run
JWT_SECRET=change-this-to-a-secure-random-string

# Note: User management is now handled via the UI (admin can add/delete users)
# Default admin user (admin/changeme) is created automatically on first run
2 changes: 1 addition & 1 deletion backend/DxO_web_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


async def stage1_lead_research(user_query: str, user_instruction: str = None) -> Dict[str, Any]:
research_prompt = f"""You are a Lead Research Agent specializing in breadth-first research.\n You should always use the web search to generate your responses and ground it storongly with the latest facts.\nYour task is to conduct comprehensive, wide-ranging research on the following question.\n\nQuestion: {user_query}\n\nProvide a thorough, well-organized research report that covers the breadth of the topic:"""
research_prompt = f"""You are a Lead Research Agent specializing in breadth-first research.\nYour task is to conduct comprehensive, wide-ranging research on the following question.\n\nQuestion: {user_query}\n\nProvide a thorough, well-organized research report that covers the breadth of the topic:"""
if user_instruction:
research_prompt += f"\n\nAdditional User Instruction:\n{user_instruction}"

Expand Down
204 changes: 204 additions & 0 deletions backend/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""Simple JWT Authentication for the application."""

import os
import json
import hashlib
import jwt
from datetime import datetime, timedelta
from functools import wraps
from pathlib import Path
from fastapi import HTTPException, Depends, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional, List, Dict, Any

# Configuration from environment variables
JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key-change-in-production")
JWT_ALGORITHM = "HS256"
JWT_EXPIRATION_HOURS = 24

# Users file path
USERS_FILE = Path(__file__).parent.parent / "data" / "users.json"


def _hash_password(password: str) -> str:
"""Hash a password using SHA256."""
return hashlib.sha256(password.encode()).hexdigest()


def _ensure_users_file():
"""Ensure the users file exists with a default admin user."""
USERS_FILE.parent.mkdir(parents=True, exist_ok=True)

if not USERS_FILE.exists():
# Create default admin user
default_users = {
"admin": {
"password_hash": _hash_password("changeme"),
"is_admin": True,
"created_at": datetime.utcnow().isoformat()
}
}
with open(USERS_FILE, 'w') as f:
json.dump(default_users, f, indent=2)


def _load_users() -> Dict[str, Any]:
"""Load users from file."""
_ensure_users_file()
with open(USERS_FILE, 'r') as f:
return json.load(f)


def _save_users(users: Dict[str, Any]):
"""Save users to file."""
_ensure_users_file()
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=2)


def get_all_users() -> List[Dict[str, Any]]:
"""Get list of all users (without password hashes)."""
users = _load_users()
return [
{
"username": username,
"is_admin": data.get("is_admin", False),
"created_at": data.get("created_at", "")
}
for username, data in users.items()
]


def add_user(username: str, password: str, is_admin: bool = False) -> bool:
"""Add a new user. Returns False if user already exists."""
users = _load_users()

if username in users:
return False

users[username] = {
"password_hash": _hash_password(password),
"is_admin": is_admin,
"created_at": datetime.utcnow().isoformat()
}

_save_users(users)
return True


def delete_user(username: str) -> bool:
"""Delete a user. Returns False if user doesn't exist or is the last admin."""
users = _load_users()

if username not in users:
return False

# Don't allow deleting the last admin
if users[username].get("is_admin", False):
admin_count = sum(1 for u in users.values() if u.get("is_admin", False))
if admin_count <= 1:
return False

del users[username]
_save_users(users)
return True


def is_user_admin(username: str) -> bool:
"""Check if a user is an admin."""
users = _load_users()
return users.get(username, {}).get("is_admin", False)


class LoginRequest(BaseModel):
username: str
password: str


class LoginResponse(BaseModel):
token: str
expires_in: int # seconds


class TokenPayload(BaseModel):
sub: str # username
exp: int # expiration timestamp


security = HTTPBearer(auto_error=False)


def create_token(username: str) -> tuple[str, int]:
"""Create a JWT token for the given username."""
expires_delta = timedelta(hours=JWT_EXPIRATION_HOURS)
expires_at = datetime.utcnow() + expires_delta

payload = {
"sub": username,
"exp": expires_at,
"iat": datetime.utcnow()
}

token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
expires_in = int(expires_delta.total_seconds())

return token, expires_in


def verify_token(token: str) -> Optional[str]:
"""Verify a JWT token and return the username if valid."""
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
username = payload.get("sub")
return username
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None


def authenticate_user(username: str, password: str) -> bool:
"""Check if username and password match stored credentials."""
users = _load_users()

if username not in users:
return False

stored_hash = users[username].get("password_hash", "")
return stored_hash == _hash_password(password)


async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> str:
"""Dependency to get current authenticated user from JWT token."""
if credentials is None:
raise HTTPException(
status_code=401,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}
)

token = credentials.credentials
username = verify_token(token)

if username is None:
raise HTTPException(
status_code=401,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"}
)

return username


async def optional_auth(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> Optional[str]:
"""Optional auth - returns username if authenticated, None otherwise."""
if credentials is None:
return None

token = credentials.credentials
return verify_token(token)
22 changes: 11 additions & 11 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,28 @@
# Azure OpenAI Model Deployments
# All available council models
ALL_COUNCIL_MODELS = [
"grok-4",
"grok-3",
"gpt-4.1",
"DeepSeek-V3.2",
"Mistral-Large-3",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-mini"
]

# Default council models (for backward compatibility)
COUNCIL_MODELS = [
"grok-4",
"gpt-4.1-mini",
"gpt-4.1",
"DeepSeek-V3.2"
"gpt-4.1"
]

# Chairman model - synthesizes final response (best model)
CHAIRMAN_MODEL = "gpt-5.2"
CHAIRMAN_MODEL = "gpt-5"

# DxO (Decision by Experts) agents
LEAD_RESEARCH_MODEL = "gpt-5.2" # Breadth-first research
CRITIC_MODEL = "grok-4" # Critiques research
DOMAIN_EXPERT_MODEL = "DeepSeek-V3.2" # Domain expertise
AGGREGATOR_MODEL = "gpt-5.2" # Synthesizes final response (best model)
LEAD_RESEARCH_MODEL = "gpt-5" # Breadth-first research
CRITIC_MODEL = "grok-3" # Critiques research
DOMAIN_EXPERT_MODEL = "gpt-4.1" # Domain expertise
AGGREGATOR_MODEL = "gpt-5" # Synthesizes final response (best model)

# DxO Web Search (Agent-based) identifiers (Azure AI Project agents)
# These are used when running the DxO flow against Azure AI "Agents" (web-search grounded)
Expand All @@ -53,7 +53,7 @@
AZURE_STORAGE_CONTAINER_NAME = os.getenv("AZURE_STORAGE_CONTAINER_NAME", "conversations")

# Super Chat aggregator (for parallel mode)
SUPER_AGGREGATOR_MODEL = "gpt-5.2" # Aggregates Council and DxO results
SUPER_AGGREGATOR_MODEL = "gpt-5" # Aggregates Council and DxO results

# Fallback configurations
FALLBACK_AGENT = "gpt-4-1-mini-agent" # Fallback agent when primary agent fails
Expand Down
36 changes: 23 additions & 13 deletions backend/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,35 @@
import asyncio
import time
from typing import List, Dict, Any, Optional
from openai import AsyncOpenAI
from .config import AZURE_ENDPOINT, AZURE_API_KEY, FALLBACK_LLM
from openai import AsyncAzureOpenAI
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from .config import AZURE_ENDPOINT, FALLBACK_LLM


# Initialize async Azure OpenAI client
_async_client = None
# Singleton async Azure OpenAI client
_async_client: Optional[AsyncAzureOpenAI] = None


def get_async_client() -> AsyncOpenAI:
"""Get or create the async Azure OpenAI client instance."""
def get_async_client() -> AsyncAzureOpenAI:
"""Get or create the async Azure OpenAI client instance using Entra ID authentication."""
global _async_client
if _async_client is None:
t0 = time.time()
print(f"[CLIENT] Creating AsyncOpenAI client...", flush=True)
_async_client = AsyncOpenAI(
api_key=AZURE_API_KEY,
base_url=AZURE_ENDPOINT,
timeout=300.0, # 5 minute timeout for large queries
max_retries=2, # Retry on transient failures
print(f"[CLIENT] Creating AsyncAzureOpenAI client with Entra ID auth...", flush=True)

# Extract base URL without the /openai/deployments path
azure_endpoint = AZURE_ENDPOINT.split("/openai/")[0] if "/openai/" in AZURE_ENDPOINT else AZURE_ENDPOINT

# Use DefaultAzureCredential with token provider for Azure OpenAI
_async_client = AsyncAzureOpenAI(
azure_ad_token_provider=get_bearer_token_provider(
DefaultAzureCredential(),
"https://cognitiveservices.azure.com/.default"
),
azure_endpoint=azure_endpoint,
api_version="2024-10-21",
timeout=300.0,
max_retries=2,
)
print(f"[CLIENT] Client created in {time.time() - t0:.3f}s", flush=True)
return _async_client
Expand All @@ -32,7 +42,7 @@ async def warm_up_client():
Pre-initialize the client and make a minimal API call to establish connection.
Call this at app startup to eliminate cold start delays.
"""
print("[WARMUP] Initializing AsyncOpenAI client...", flush=True)
print("[WARMUP] Initializing AsyncAzureOpenAI client...", flush=True)

client = get_async_client()

Expand Down
Loading