mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-27 19:25:15 +02:00
Merge remote-tracking branch 'upstream/dev' into pr-611
This commit is contained in:
commit
6f330e7b8d
92 changed files with 5331 additions and 6029 deletions
|
|
@ -11,10 +11,10 @@ from .google_calendar_add_connector_route import (
|
|||
from .google_gmail_add_connector_route import (
|
||||
router as google_gmail_add_connector_router,
|
||||
)
|
||||
from .llm_config_routes import router as llm_config_router
|
||||
from .logs_routes import router as logs_router
|
||||
from .luma_add_connector_route import router as luma_add_connector_router
|
||||
from .new_chat_routes import router as new_chat_router
|
||||
from .new_llm_config_routes import router as new_llm_config_router
|
||||
from .notes_routes import router as notes_router
|
||||
from .podcasts_routes import router as podcasts_router
|
||||
from .rbac_routes import router as rbac_router
|
||||
|
|
@ -35,5 +35,5 @@ router.include_router(google_calendar_add_connector_router)
|
|||
router.include_router(google_gmail_add_connector_router)
|
||||
router.include_router(airtable_add_connector_router)
|
||||
router.include_router(luma_add_connector_router)
|
||||
router.include_router(llm_config_router)
|
||||
router.include_router(new_llm_config_router) # LLM configs with prompt configuration
|
||||
router.include_router(logs_router)
|
||||
|
|
|
|||
|
|
@ -1,576 +0,0 @@
|
|||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
LLMConfig,
|
||||
Permission,
|
||||
SearchSpace,
|
||||
User,
|
||||
get_async_session,
|
||||
)
|
||||
from app.schemas import LLMConfigCreate, LLMConfigRead, LLMConfigUpdate
|
||||
from app.services.llm_service import validate_llm_config
|
||||
from app.users import current_active_user
|
||||
from app.utils.rbac import check_permission
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMPreferencesUpdate(BaseModel):
|
||||
"""Schema for updating search space LLM preferences"""
|
||||
|
||||
long_context_llm_id: int | None = None
|
||||
fast_llm_id: int | None = None
|
||||
strategic_llm_id: int | None = None
|
||||
|
||||
|
||||
class LLMPreferencesRead(BaseModel):
|
||||
"""Schema for reading search space LLM preferences"""
|
||||
|
||||
long_context_llm_id: int | None = None
|
||||
fast_llm_id: int | None = None
|
||||
strategic_llm_id: int | None = None
|
||||
long_context_llm: LLMConfigRead | None = None
|
||||
fast_llm: LLMConfigRead | None = None
|
||||
strategic_llm: LLMConfigRead | None = None
|
||||
|
||||
|
||||
class GlobalLLMConfigRead(BaseModel):
|
||||
"""Schema for reading global LLM configs (without API key)"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
provider: str
|
||||
custom_provider: str | None = None
|
||||
model_name: str
|
||||
api_base: str | None = None
|
||||
language: str | None = None
|
||||
litellm_params: dict | None = None
|
||||
is_global: bool = True
|
||||
|
||||
|
||||
# Global LLM Config endpoints
|
||||
|
||||
|
||||
@router.get("/global-llm-configs", response_model=list[GlobalLLMConfigRead])
|
||||
async def get_global_llm_configs(
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all available global LLM configurations.
|
||||
These are pre-configured by the system administrator and available to all users.
|
||||
API keys are not exposed through this endpoint.
|
||||
"""
|
||||
try:
|
||||
global_configs = config.GLOBAL_LLM_CONFIGS
|
||||
|
||||
# Remove API keys from response
|
||||
safe_configs = []
|
||||
for cfg in global_configs:
|
||||
safe_config = {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_base": cfg.get("api_base"),
|
||||
"language": cfg.get("language"),
|
||||
"litellm_params": cfg.get("litellm_params", {}),
|
||||
"is_global": True,
|
||||
}
|
||||
safe_configs.append(safe_config)
|
||||
|
||||
return safe_configs
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch global LLM configs: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/llm-configs", response_model=LLMConfigRead)
|
||||
async def create_llm_config(
|
||||
llm_config: LLMConfigCreate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Create a new LLM configuration for a search space.
|
||||
Requires LLM_CONFIGS_CREATE permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to create LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_CREATE.value,
|
||||
"You don't have permission to create LLM configurations in this search space",
|
||||
)
|
||||
|
||||
# Validate the LLM configuration by making a test API call
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=llm_config.provider.value,
|
||||
model_name=llm_config.model_name,
|
||||
api_key=llm_config.api_key,
|
||||
api_base=llm_config.api_base,
|
||||
custom_provider=llm_config.custom_provider,
|
||||
litellm_params=llm_config.litellm_params,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
db_llm_config = LLMConfig(**llm_config.model_dump())
|
||||
session.add(db_llm_config)
|
||||
await session.commit()
|
||||
await session.refresh(db_llm_config)
|
||||
return db_llm_config
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/llm-configs", response_model=list[LLMConfigRead])
|
||||
async def read_llm_configs(
|
||||
search_space_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 200,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all LLM configurations for a search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to read LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(LLMConfig)
|
||||
.filter(LLMConfig.search_space_id == search_space_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch LLM configurations: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/llm-configs/{llm_config_id}", response_model=LLMConfigRead)
|
||||
async def read_llm_config(
|
||||
llm_config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get a specific LLM configuration by ID.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Get the LLM config
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == llm_config_id)
|
||||
)
|
||||
llm_config = result.scalars().first()
|
||||
|
||||
if not llm_config:
|
||||
raise HTTPException(status_code=404, detail="LLM configuration not found")
|
||||
|
||||
# Verify user has permission to read LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
return llm_config
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/llm-configs/{llm_config_id}", response_model=LLMConfigRead)
|
||||
async def update_llm_config(
|
||||
llm_config_id: int,
|
||||
llm_config_update: LLMConfigUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update an existing LLM configuration.
|
||||
Requires LLM_CONFIGS_UPDATE permission.
|
||||
"""
|
||||
try:
|
||||
# Get the LLM config
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == llm_config_id)
|
||||
)
|
||||
db_llm_config = result.scalars().first()
|
||||
|
||||
if not db_llm_config:
|
||||
raise HTTPException(status_code=404, detail="LLM configuration not found")
|
||||
|
||||
# Verify user has permission to update LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
db_llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_UPDATE.value,
|
||||
"You don't have permission to update LLM configurations in this search space",
|
||||
)
|
||||
|
||||
update_data = llm_config_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Apply updates to a temporary copy for validation
|
||||
temp_config = {
|
||||
"provider": update_data.get("provider", db_llm_config.provider.value),
|
||||
"model_name": update_data.get("model_name", db_llm_config.model_name),
|
||||
"api_key": update_data.get("api_key", db_llm_config.api_key),
|
||||
"api_base": update_data.get("api_base", db_llm_config.api_base),
|
||||
"custom_provider": update_data.get(
|
||||
"custom_provider", db_llm_config.custom_provider
|
||||
),
|
||||
"litellm_params": update_data.get(
|
||||
"litellm_params", db_llm_config.litellm_params
|
||||
),
|
||||
}
|
||||
|
||||
# Validate the updated configuration
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=temp_config["provider"],
|
||||
model_name=temp_config["model_name"],
|
||||
api_key=temp_config["api_key"],
|
||||
api_base=temp_config["api_base"],
|
||||
custom_provider=temp_config["custom_provider"],
|
||||
litellm_params=temp_config["litellm_params"],
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
# Apply updates to the database object
|
||||
for key, value in update_data.items():
|
||||
setattr(db_llm_config, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(db_llm_config)
|
||||
return db_llm_config
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.delete("/llm-configs/{llm_config_id}", response_model=dict)
|
||||
async def delete_llm_config(
|
||||
llm_config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Delete an LLM configuration.
|
||||
Requires LLM_CONFIGS_DELETE permission.
|
||||
"""
|
||||
try:
|
||||
# Get the LLM config
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == llm_config_id)
|
||||
)
|
||||
db_llm_config = result.scalars().first()
|
||||
|
||||
if not db_llm_config:
|
||||
raise HTTPException(status_code=404, detail="LLM configuration not found")
|
||||
|
||||
# Verify user has permission to delete LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
db_llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_DELETE.value,
|
||||
"You don't have permission to delete LLM configurations in this search space",
|
||||
)
|
||||
|
||||
await session.delete(db_llm_config)
|
||||
await session.commit()
|
||||
return {"message": "LLM configuration deleted successfully"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
# Search Space LLM Preferences endpoints
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def get_llm_preferences(
|
||||
search_space_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get the LLM preferences for a specific search space.
|
||||
LLM preferences are shared by all members of the search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to read LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM preferences in this search space",
|
||||
)
|
||||
|
||||
# Get the search space
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Helper function to get config (global or custom)
|
||||
async def get_config_for_id(config_id):
|
||||
if config_id is None:
|
||||
return None
|
||||
|
||||
# Check if it's a global config (negative ID)
|
||||
if config_id < 0:
|
||||
for cfg in config.GLOBAL_LLM_CONFIGS:
|
||||
if cfg.get("id") == config_id:
|
||||
# Return as LLMConfigRead-compatible dict
|
||||
return {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_key": "***GLOBAL***", # Don't expose the actual key
|
||||
"api_base": cfg.get("api_base"),
|
||||
"language": cfg.get("language"),
|
||||
"litellm_params": cfg.get("litellm_params"),
|
||||
"created_at": None,
|
||||
"search_space_id": search_space_id,
|
||||
}
|
||||
return None
|
||||
|
||||
# It's a custom config, fetch from database
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == config_id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
# Get the configs (from DB for custom, or constructed for global)
|
||||
long_context_llm = await get_config_for_id(search_space.long_context_llm_id)
|
||||
fast_llm = await get_config_for_id(search_space.fast_llm_id)
|
||||
strategic_llm = await get_config_for_id(search_space.strategic_llm_id)
|
||||
|
||||
return {
|
||||
"long_context_llm_id": search_space.long_context_llm_id,
|
||||
"fast_llm_id": search_space.fast_llm_id,
|
||||
"strategic_llm_id": search_space.strategic_llm_id,
|
||||
"long_context_llm": long_context_llm,
|
||||
"fast_llm": fast_llm,
|
||||
"strategic_llm": strategic_llm,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def update_llm_preferences(
|
||||
search_space_id: int,
|
||||
preferences: LLMPreferencesUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update the LLM preferences for a specific search space.
|
||||
LLM preferences are shared by all members of the search space.
|
||||
Requires SETTINGS_UPDATE permission (only users with settings access can change).
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to update settings (not just LLM configs)
|
||||
# This ensures only users with settings access can change shared LLM preferences
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.SETTINGS_UPDATE.value,
|
||||
"You don't have permission to update LLM preferences in this search space",
|
||||
)
|
||||
|
||||
# Get the search space
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Validate that all provided LLM config IDs belong to the search space
|
||||
update_data = preferences.model_dump(exclude_unset=True)
|
||||
|
||||
# Store language from configs to validate consistency
|
||||
languages = set()
|
||||
|
||||
for _key, llm_config_id in update_data.items():
|
||||
if llm_config_id is not None:
|
||||
# Check if this is a global config (negative ID)
|
||||
if llm_config_id < 0:
|
||||
# Validate global config exists
|
||||
global_config = None
|
||||
for cfg in config.GLOBAL_LLM_CONFIGS:
|
||||
if cfg.get("id") == llm_config_id:
|
||||
global_config = cfg
|
||||
break
|
||||
|
||||
if not global_config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Global LLM configuration {llm_config_id} not found",
|
||||
)
|
||||
|
||||
# Collect language for consistency check (if explicitly set)
|
||||
lang = global_config.get("language")
|
||||
if lang and lang.strip(): # Only add non-empty languages
|
||||
languages.add(lang.strip())
|
||||
else:
|
||||
# Verify the LLM config belongs to the search space (custom config)
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(
|
||||
LLMConfig.id == llm_config_id,
|
||||
LLMConfig.search_space_id == search_space_id,
|
||||
)
|
||||
)
|
||||
llm_config = result.scalars().first()
|
||||
if not llm_config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"LLM configuration {llm_config_id} not found in this search space",
|
||||
)
|
||||
|
||||
# Collect language for consistency check (if explicitly set)
|
||||
if llm_config.language and llm_config.language.strip():
|
||||
languages.add(llm_config.language.strip())
|
||||
|
||||
# Language consistency check - only warn if there are multiple explicit languages
|
||||
# Allow mixing configs with and without language settings
|
||||
if len(languages) > 1:
|
||||
# Log warning but allow the operation
|
||||
logger.warning(
|
||||
f"Multiple languages detected in LLM selection for search_space {search_space_id}: {languages}. "
|
||||
"This may affect response quality."
|
||||
)
|
||||
|
||||
# Update search space LLM preferences
|
||||
for key, value in update_data.items():
|
||||
setattr(search_space, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(search_space)
|
||||
|
||||
# Helper function to get config (global or custom)
|
||||
async def get_config_for_id(config_id):
|
||||
if config_id is None:
|
||||
return None
|
||||
|
||||
# Check if it's a global config (negative ID)
|
||||
if config_id < 0:
|
||||
for cfg in config.GLOBAL_LLM_CONFIGS:
|
||||
if cfg.get("id") == config_id:
|
||||
# Return as LLMConfigRead-compatible dict
|
||||
return {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_key": "***GLOBAL***", # Don't expose the actual key
|
||||
"api_base": cfg.get("api_base"),
|
||||
"language": cfg.get("language"),
|
||||
"litellm_params": cfg.get("litellm_params"),
|
||||
"created_at": None,
|
||||
"search_space_id": search_space_id,
|
||||
}
|
||||
return None
|
||||
|
||||
# It's a custom config, fetch from database
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == config_id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
# Get the configs (from DB for custom, or constructed for global)
|
||||
long_context_llm = await get_config_for_id(search_space.long_context_llm_id)
|
||||
fast_llm = await get_config_for_id(search_space.fast_llm_id)
|
||||
strategic_llm = await get_config_for_id(search_space.strategic_llm_id)
|
||||
|
||||
# Return updated preferences
|
||||
return {
|
||||
"long_context_llm_id": search_space.long_context_llm_id,
|
||||
"fast_llm_id": search_space.fast_llm_id,
|
||||
"strategic_llm_id": search_space.strategic_llm_id,
|
||||
"long_context_llm": long_context_llm,
|
||||
"fast_llm": fast_llm,
|
||||
"strategic_llm": strategic_llm,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
|
@ -685,8 +685,16 @@ async def handle_new_chat(
|
|||
)
|
||||
search_space = search_space_result.scalars().first()
|
||||
|
||||
# TODO: Add new llm config arch then complete this
|
||||
llm_config_id = -4
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Use agent_llm_id from search space for chat operations
|
||||
# Positive IDs load from NewLLMConfig database table
|
||||
# Negative IDs load from YAML global configs
|
||||
# Falls back to -1 (first global config) if not configured
|
||||
llm_config_id = (
|
||||
search_space.agent_llm_id if search_space.agent_llm_id is not None else -1
|
||||
)
|
||||
|
||||
# Return streaming response
|
||||
return StreamingResponse(
|
||||
|
|
@ -696,7 +704,6 @@ async def handle_new_chat(
|
|||
chat_id=request.chat_id,
|
||||
session=session,
|
||||
llm_config_id=llm_config_id,
|
||||
messages=request.messages,
|
||||
attachments=request.attachments,
|
||||
mentioned_document_ids=request.mentioned_document_ids,
|
||||
),
|
||||
|
|
|
|||
376
surfsense_backend/app/routes/new_llm_config_routes.py
Normal file
376
surfsense_backend/app/routes/new_llm_config_routes.py
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
"""
|
||||
API routes for NewLLMConfig CRUD operations.
|
||||
|
||||
NewLLMConfig combines LLM model settings with prompt configuration:
|
||||
- LLM provider, model, API key, etc.
|
||||
- Configurable system instructions
|
||||
- Citation toggle
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.system_prompt import get_default_system_instructions
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
NewLLMConfig,
|
||||
Permission,
|
||||
User,
|
||||
get_async_session,
|
||||
)
|
||||
from app.schemas import (
|
||||
DefaultSystemInstructionsResponse,
|
||||
GlobalNewLLMConfigRead,
|
||||
NewLLMConfigCreate,
|
||||
NewLLMConfigRead,
|
||||
NewLLMConfigUpdate,
|
||||
)
|
||||
from app.services.llm_service import validate_llm_config
|
||||
from app.users import current_active_user
|
||||
from app.utils.rbac import check_permission
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Global Configs Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/global-new-llm-configs", response_model=list[GlobalNewLLMConfigRead])
|
||||
async def get_global_new_llm_configs(
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all available global NewLLMConfig configurations.
|
||||
These are pre-configured by the system administrator and available to all users.
|
||||
API keys are not exposed through this endpoint.
|
||||
|
||||
Global configs have negative IDs to distinguish from user-created configs.
|
||||
"""
|
||||
try:
|
||||
global_configs = config.GLOBAL_LLM_CONFIGS
|
||||
|
||||
# Transform to new structure, hiding API keys
|
||||
safe_configs = []
|
||||
for cfg in global_configs:
|
||||
safe_config = {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"description": cfg.get("description"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_base": cfg.get("api_base") or None,
|
||||
"litellm_params": cfg.get("litellm_params", {}),
|
||||
# New prompt configuration fields
|
||||
"system_instructions": cfg.get("system_instructions", ""),
|
||||
"use_default_system_instructions": cfg.get(
|
||||
"use_default_system_instructions", True
|
||||
),
|
||||
"citations_enabled": cfg.get("citations_enabled", True),
|
||||
"is_global": True,
|
||||
}
|
||||
safe_configs.append(safe_config)
|
||||
|
||||
return safe_configs
|
||||
except Exception as e:
|
||||
logger.exception("Failed to fetch global NewLLMConfigs")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch global configurations: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CRUD Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post("/new-llm-configs", response_model=NewLLMConfigRead)
|
||||
async def create_new_llm_config(
|
||||
config_data: NewLLMConfigCreate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Create a new NewLLMConfig for a search space.
|
||||
Requires LLM_CONFIGS_CREATE permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config_data.search_space_id,
|
||||
Permission.LLM_CONFIGS_CREATE.value,
|
||||
"You don't have permission to create LLM configurations in this search space",
|
||||
)
|
||||
|
||||
# Validate the LLM configuration by making a test API call
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=config_data.provider.value,
|
||||
model_name=config_data.model_name,
|
||||
api_key=config_data.api_key,
|
||||
api_base=config_data.api_base,
|
||||
custom_provider=config_data.custom_provider,
|
||||
litellm_params=config_data.litellm_params,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
# Create the config
|
||||
db_config = NewLLMConfig(**config_data.model_dump())
|
||||
session.add(db_config)
|
||||
await session.commit()
|
||||
await session.refresh(db_config)
|
||||
|
||||
return db_config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to create NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/new-llm-configs", response_model=list[NewLLMConfigRead])
|
||||
async def list_new_llm_configs(
|
||||
search_space_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all NewLLMConfigs for a search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig)
|
||||
.filter(NewLLMConfig.search_space_id == search_space_id)
|
||||
.order_by(NewLLMConfig.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
return result.scalars().all()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Failed to list NewLLMConfigs")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch configurations: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get(
|
||||
"/new-llm-configs/default-system-instructions",
|
||||
response_model=DefaultSystemInstructionsResponse,
|
||||
)
|
||||
async def get_default_system_instructions_endpoint(
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get the default SURFSENSE_SYSTEM_INSTRUCTIONS template.
|
||||
Useful for pre-populating the UI when creating a new configuration.
|
||||
"""
|
||||
return DefaultSystemInstructionsResponse(
|
||||
default_system_instructions=get_default_system_instructions()
|
||||
)
|
||||
|
||||
|
||||
@router.get("/new-llm-configs/{config_id}", response_model=NewLLMConfigRead)
|
||||
async def get_new_llm_config(
|
||||
config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get a specific NewLLMConfig by ID.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config.search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/new-llm-configs/{config_id}", response_model=NewLLMConfigRead)
|
||||
async def update_new_llm_config(
|
||||
config_id: int,
|
||||
update_data: NewLLMConfigUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update an existing NewLLMConfig.
|
||||
Requires LLM_CONFIGS_UPDATE permission.
|
||||
"""
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config.search_space_id,
|
||||
Permission.LLM_CONFIGS_UPDATE.value,
|
||||
"You don't have permission to update LLM configurations in this search space",
|
||||
)
|
||||
|
||||
update_dict = update_data.model_dump(exclude_unset=True)
|
||||
|
||||
# If updating LLM settings, validate them
|
||||
if any(
|
||||
key in update_dict
|
||||
for key in [
|
||||
"provider",
|
||||
"model_name",
|
||||
"api_key",
|
||||
"api_base",
|
||||
"custom_provider",
|
||||
"litellm_params",
|
||||
]
|
||||
):
|
||||
# Build the validation config from existing + updates
|
||||
validation_config = {
|
||||
"provider": update_dict.get("provider", config.provider).value
|
||||
if hasattr(update_dict.get("provider", config.provider), "value")
|
||||
else update_dict.get("provider", config.provider.value),
|
||||
"model_name": update_dict.get("model_name", config.model_name),
|
||||
"api_key": update_dict.get("api_key", config.api_key),
|
||||
"api_base": update_dict.get("api_base", config.api_base),
|
||||
"custom_provider": update_dict.get(
|
||||
"custom_provider", config.custom_provider
|
||||
),
|
||||
"litellm_params": update_dict.get(
|
||||
"litellm_params", config.litellm_params
|
||||
),
|
||||
}
|
||||
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=validation_config["provider"],
|
||||
model_name=validation_config["model_name"],
|
||||
api_key=validation_config["api_key"],
|
||||
api_base=validation_config["api_base"],
|
||||
custom_provider=validation_config["custom_provider"],
|
||||
litellm_params=validation_config["litellm_params"],
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
# Apply updates
|
||||
for key, value in update_dict.items():
|
||||
setattr(config, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(config)
|
||||
|
||||
return config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to update NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.delete("/new-llm-configs/{config_id}", response_model=dict)
|
||||
async def delete_new_llm_config(
|
||||
config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Delete a NewLLMConfig.
|
||||
Requires LLM_CONFIGS_DELETE permission.
|
||||
"""
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config.search_space_id,
|
||||
Permission.LLM_CONFIGS_DELETE.value,
|
||||
"You don't have permission to delete LLM configurations in this search space",
|
||||
)
|
||||
|
||||
await session.delete(config)
|
||||
await session.commit()
|
||||
|
||||
return {"message": "Configuration deleted successfully", "id": config_id}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to delete NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete configuration: {e!s}"
|
||||
) from e
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
NewLLMConfig,
|
||||
Permission,
|
||||
SearchSpace,
|
||||
SearchSpaceMembership,
|
||||
|
|
@ -17,6 +17,8 @@ from app.db import (
|
|||
get_default_roles_config,
|
||||
)
|
||||
from app.schemas import (
|
||||
LLMPreferencesRead,
|
||||
LLMPreferencesUpdate,
|
||||
SearchSpaceCreate,
|
||||
SearchSpaceRead,
|
||||
SearchSpaceUpdate,
|
||||
|
|
@ -184,37 +186,6 @@ async def read_search_spaces(
|
|||
) from e
|
||||
|
||||
|
||||
@router.get("/searchspaces/prompts/community")
|
||||
async def get_community_prompts():
|
||||
"""
|
||||
Get community-curated prompts for SearchSpace System Instructions.
|
||||
This endpoint does not require authentication as it serves public prompts.
|
||||
"""
|
||||
try:
|
||||
# Get the path to the prompts YAML file
|
||||
prompts_file = (
|
||||
Path(__file__).parent.parent
|
||||
/ "prompts"
|
||||
/ "public_search_space_prompts.yaml"
|
||||
)
|
||||
|
||||
if not prompts_file.exists():
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Community prompts file not found"
|
||||
)
|
||||
|
||||
with open(prompts_file, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
return data.get("prompts", [])
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to load community prompts: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/searchspaces/{search_space_id}", response_model=SearchSpaceRead)
|
||||
async def read_search_space(
|
||||
search_space_id: int,
|
||||
|
|
@ -329,3 +300,184 @@ async def delete_search_space(
|
|||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete search space: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LLM Preferences Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def _get_llm_config_by_id(
|
||||
session: AsyncSession, config_id: int | None
|
||||
) -> dict | None:
|
||||
"""
|
||||
Get an LLM config by ID as a dictionary. Returns database config for positive IDs,
|
||||
global config for negative IDs, or None if ID is None.
|
||||
"""
|
||||
if config_id is None:
|
||||
return None
|
||||
|
||||
if config_id < 0:
|
||||
# Global config - find from YAML
|
||||
global_configs = config.GLOBAL_LLM_CONFIGS
|
||||
for cfg in global_configs:
|
||||
if cfg.get("id") == config_id:
|
||||
return {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"description": cfg.get("description"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_base": cfg.get("api_base"),
|
||||
"litellm_params": cfg.get("litellm_params", {}),
|
||||
"system_instructions": cfg.get("system_instructions", ""),
|
||||
"use_default_system_instructions": cfg.get(
|
||||
"use_default_system_instructions", True
|
||||
),
|
||||
"citations_enabled": cfg.get("citations_enabled", True),
|
||||
"is_global": True,
|
||||
}
|
||||
return None
|
||||
else:
|
||||
# Database config - convert to dict
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
db_config = result.scalars().first()
|
||||
if db_config:
|
||||
return {
|
||||
"id": db_config.id,
|
||||
"name": db_config.name,
|
||||
"description": db_config.description,
|
||||
"provider": db_config.provider.value if db_config.provider else None,
|
||||
"custom_provider": db_config.custom_provider,
|
||||
"model_name": db_config.model_name,
|
||||
"api_key": db_config.api_key,
|
||||
"api_base": db_config.api_base,
|
||||
"litellm_params": db_config.litellm_params or {},
|
||||
"system_instructions": db_config.system_instructions or "",
|
||||
"use_default_system_instructions": db_config.use_default_system_instructions,
|
||||
"citations_enabled": db_config.citations_enabled,
|
||||
"created_at": db_config.created_at.isoformat()
|
||||
if db_config.created_at
|
||||
else None,
|
||||
"search_space_id": db_config.search_space_id,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def get_llm_preferences(
|
||||
search_space_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get LLM preferences (role assignments) for a search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Check permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM preferences",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Get full config objects for each role
|
||||
agent_llm = await _get_llm_config_by_id(session, search_space.agent_llm_id)
|
||||
document_summary_llm = await _get_llm_config_by_id(
|
||||
session, search_space.document_summary_llm_id
|
||||
)
|
||||
|
||||
return LLMPreferencesRead(
|
||||
agent_llm_id=search_space.agent_llm_id,
|
||||
document_summary_llm_id=search_space.document_summary_llm_id,
|
||||
agent_llm=agent_llm,
|
||||
document_summary_llm=document_summary_llm,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get LLM preferences")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to get LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def update_llm_preferences(
|
||||
search_space_id: int,
|
||||
preferences: LLMPreferencesUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update LLM preferences (role assignments) for a search space.
|
||||
Requires LLM_CONFIGS_UPDATE permission.
|
||||
"""
|
||||
try:
|
||||
# Check permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_UPDATE.value,
|
||||
"You don't have permission to update LLM preferences",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Update preferences
|
||||
update_data = preferences.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(search_space, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(search_space)
|
||||
|
||||
# Get full config objects for response
|
||||
agent_llm = await _get_llm_config_by_id(session, search_space.agent_llm_id)
|
||||
document_summary_llm = await _get_llm_config_by_id(
|
||||
session, search_space.document_summary_llm_id
|
||||
)
|
||||
|
||||
return LLMPreferencesRead(
|
||||
agent_llm_id=search_space.agent_llm_id,
|
||||
document_summary_llm_id=search_space.document_summary_llm_id,
|
||||
agent_llm=agent_llm,
|
||||
document_summary_llm=document_summary_llm,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to update LLM preferences")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue