Skip to content
Closed
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
12 changes: 11 additions & 1 deletion openrag/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
from api.routers.admin.cluster import router as actors_router
from api.routers.admin.indexing import router as indexer_router
from api.routers.admin.jobs import router as queue_router
from api.routers.admin.model_endpoints import router as model_endpoints_router
from api.routers.admin.monitoring import router as monitoring_router
from api.routers.admin.partitions import router as partition_router
from api.routers.admin.presets import router as presets_router
from api.routers.admin.tools import router as tools_router
from api.routers.admin.users import router as users_router
from api.routers.admin.workspaces import router as workspaces_router
Expand Down Expand Up @@ -102,12 +104,16 @@


class Tags(Enum):
"""OpenAPI tag labels used by mounted routers."""

VDB = "VectorDB operations"
INDEXER = "Indexer"
SEARCH = "Semantic Search"
OPENAI = "OpenAI Compatible API"
EXTRACT = "Document extracts"
PARTITION = "Partitions & files"
MODEL_ENDPOINTS = "Model Endpoints"
PRESETS = "Presets"
QUEUE = "Queue management"
ACTORS = "Ray Actors"
USERS = "User management"
Expand Down Expand Up @@ -219,6 +225,7 @@ async def lifespan(app: FastAPI):


def custom_openapi():
"""Build the OpenAPI schema with global bearer authentication metadata."""
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
Expand Down Expand Up @@ -297,6 +304,7 @@ def root_redirect():

@app.get("/config", summary="Get current configuration", tags=["Configuration"], dependencies=[Depends(require_admin)])
def get_config():
"""Return the loaded application settings for admins."""
return settings


Expand All @@ -310,6 +318,8 @@ def get_config():
app.include_router(extract_router, prefix="/extract", tags=[Tags.EXTRACT])
app.include_router(search_router, prefix="/search", tags=[Tags.SEARCH])
app.include_router(partition_router, prefix="/partition", tags=[Tags.PARTITION])
app.include_router(model_endpoints_router, prefix="/model-endpoints", tags=[Tags.MODEL_ENDPOINTS])
app.include_router(presets_router, prefix="/presets", tags=[Tags.PRESETS])
app.include_router(queue_router, prefix="/queue", tags=[Tags.QUEUE])
app.include_router(actors_router, prefix="/actors", tags=[Tags.ACTORS])
app.include_router(users_router, prefix="/users", tags=[Tags.USERS])
Expand Down Expand Up @@ -338,7 +348,7 @@ def get_config():
@serve.deployment(num_replicas=settings.ray.serve.num_replicas)
@serve.ingress(app)
class OpenRagAPI:
pass
"""Ray Serve deployment wrapper for the FastAPI app."""

serve.start(http_options={"host": settings.ray.serve.host, "port": settings.ray.serve.port})
if WITH_CHAINLIT_UI:
Expand Down
97 changes: 97 additions & 0 deletions openrag/api/routers/admin/model_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Admin routes for the Phase 14 model endpoint registry.

The router is intentionally transport-only: auth, request validation and
response shaping live here, while endpoint persistence and validation are
delegated to the service resolved from the DI container.
"""

from api.dependencies.auth import require_admin
from api.schemas.admin.model_endpoint_schemas import (
CreateModelEndpointRequest,
ModelEndpointResponse,
ModelEndpointType,
UpdateModelEndpointRequest,
ValidateEndpointResponse,
)
from di.providers import get_model_endpoint_service
from fastapi import APIRouter, Depends, Response, status

router = APIRouter(dependencies=[Depends(require_admin)])


@router.post(
"/",
response_model=ModelEndpointResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_model_endpoint(
body: CreateModelEndpointRequest,
service=Depends(get_model_endpoint_service),
):
"""Register a named inference endpoint."""
return await service.create_model_endpoint(body.model_dump())


@router.get("/", response_model=list[ModelEndpointResponse])
async def list_model_endpoints(
model_type: ModelEndpointType | None = None,
service=Depends(get_model_endpoint_service),
):
"""List registered inference endpoints, optionally filtered by type."""
return await service.list_model_endpoints(model_type=model_type)


@router.get("/{model_type}/{name}", response_model=ModelEndpointResponse)
async def get_model_endpoint(
model_type: ModelEndpointType,
name: str,
service=Depends(get_model_endpoint_service),
):
"""Return one registered inference endpoint."""
return await service.get_model_endpoint(name=name, model_type=model_type)


@router.put("/{model_type}/{name}", response_model=ModelEndpointResponse)
async def update_model_endpoint(
model_type: ModelEndpointType,
name: str,
body: UpdateModelEndpointRequest,
service=Depends(get_model_endpoint_service),
):
"""Update a registered inference endpoint."""
return await service.update_model_endpoint(
name=name,
model_type=model_type,
**body.model_dump(exclude_unset=True),
)


@router.delete("/{model_type}/{name}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_model_endpoint(
model_type: ModelEndpointType,
name: str,
service=Depends(get_model_endpoint_service),
):
"""Delete a registered inference endpoint."""
await service.delete_model_endpoint(name=name, model_type=model_type)
return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.post("/{model_type}/{name}/set-default", response_model=ModelEndpointResponse)
async def set_default_model_endpoint(
model_type: ModelEndpointType,
name: str,
service=Depends(get_model_endpoint_service),
):
"""Promote a registered endpoint to the default for its type."""
return await service.set_default(model_type=model_type, name=name)


@router.post("/{model_type}/{name}/validate", response_model=ValidateEndpointResponse)
async def validate_model_endpoint(
model_type: ModelEndpointType,
name: str,
service=Depends(get_model_endpoint_service),
):
"""Probe a registered endpoint for reachability and model availability."""
return await service.validate_endpoint(name=name, model_type=model_type)
85 changes: 85 additions & 0 deletions openrag/api/routers/admin/partitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require_partition_owner,
require_partition_viewer,
)
from api.schemas.admin.partition_schemas import PartitionDetailResponse, UpdatePartitionRequest
from core.utils.logging import get_logger
from di.providers import get_partition_service
from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response, status
Expand All @@ -29,9 +30,21 @@


def _quote_param_value(s: str) -> str:
"""Percent-encode a path parameter value for URL generation."""
return quote(s, safe="")


def _require_service_method(service, method_name: str):
"""Return a service method or fail clearly when a phased method is absent."""
method = getattr(service, method_name, None)
if not callable(method):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail=f"{method_name} is not available.",
)
return method


@router.get(
"/",
description="""List all accessible partitions.
Expand All @@ -49,6 +62,7 @@ async def list_existant_partitions(
partitions=Depends(partitions_with_details),
service=Depends(get_partition_service),
):
"""List partitions visible to the current user."""
if len(partitions) == 1 and partitions[0]["partition"] == "all":
partitions = await service.list_partitions()
logger.debug("Returned list of existing partitions.", partition_count=len(partitions))
Expand Down Expand Up @@ -77,6 +91,7 @@ async def delete_partition(
partition_owner=Depends(require_partition_owner),
service=Depends(get_partition_service),
):
"""Delete a partition owned by the current user."""
await service.delete_partition(partition)
return Response(status_code=status.HTTP_204_NO_CONTENT)

Expand Down Expand Up @@ -107,9 +122,11 @@ async def list_files(
partition_viewer=Depends(require_partition_viewer),
service=Depends(get_partition_service),
):
"""List files stored in a partition."""
file_dicts = await service.list_files(partition, limit)

def process_file(file_dict):
"""Add a canonical file-detail link to one file row."""
return {
"link": str(
request.url_for(
Expand Down Expand Up @@ -153,6 +170,7 @@ async def get_file(
partition_viewer=Depends(require_partition_viewer),
service=Depends(get_partition_service),
):
"""Return metadata and chunk links for one file in a partition."""
if not await service.file_exists(file_id, partition):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
Expand Down Expand Up @@ -196,6 +214,7 @@ async def list_all_chunks(
partition_viewer=Depends(require_partition_viewer),
service=Depends(get_partition_service),
):
"""List all chunks in a partition."""
items = await service.list_all_chunks(partition=partition, include_embedding=include_embedding)
chunks = [
{
Expand Down Expand Up @@ -232,6 +251,7 @@ async def create_partition(
partition: str,
service=Depends(get_partition_service),
):
"""Create a new partition owned by the current user."""
if await service.partition_exists(partition):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
Expand All @@ -242,6 +262,69 @@ async def create_partition(
return Response(status_code=status.HTTP_201_CREATED)


@router.patch(
"/{partition}",
response_model=PartitionDetailResponse,
description="""Update Phase 14 preset assignments for a partition.

**Parameters:**
- `partition`: The partition name

**Body:**
Accepts partition config fields such as:
- `description`
- `embedder`
- `indexation_preset`
- `retrieval_preset`
- `chat_history_depth`
- `chat_llm`

**Permissions:**
- Requires partition owner role

**Response:**
Returns the updated resolved partition configuration.
""",
)
async def update_partition_config(
partition: str,
body: UpdatePartitionRequest,
partition_owner=Depends(require_partition_owner),
service=Depends(get_partition_service),
):
"""Update Phase 14 preset references for a partition."""
method = _require_service_method(service, "update_partition_config")
return await method(
partition=partition,
**body.model_dump(exclude_unset=True),
)


@router.get(
"/{partition}/config",
response_model=PartitionDetailResponse,
description="""Return the resolved Phase 14 pipeline config for a partition.

**Parameters:**
- `partition`: The partition name

**Permissions:**
- Requires partition viewer role or higher

**Response:**
Returns partition metadata, preset references, and resolved indexation/retrieval pipeline configs.
""",
)
async def get_partition_config(
partition: str,
partition_viewer=Depends(require_partition_viewer),
service=Depends(get_partition_service),
):
"""Return the resolved Phase 14 config for a partition."""
method = _require_service_method(service, "get_partition_config")
return await method(partition=partition)


@router.get(
"/{partition}/users",
description="""List all users with access to a partition.
Expand Down Expand Up @@ -401,6 +484,7 @@ async def get_related_files(
partition_viewer=Depends(require_partition_viewer),
service=Depends(get_partition_service),
):
"""Return files sharing a relationship identifier."""
files = await service.get_related_files(partition=partition, relationship_id=relationship_id)
return JSONResponse(status_code=status.HTTP_200_OK, content={"files": files})

Expand Down Expand Up @@ -438,6 +522,7 @@ async def get_file_ancestors(
partition_viewer=Depends(require_partition_viewer),
service=Depends(get_partition_service),
):
"""Return the ancestor path for one file."""
if not await service.file_exists(file_id, partition):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
Expand Down
Loading
Loading