feat: add admin endpoints for chatbot tools
This commit is contained in:
parent
0c5d0f957f
commit
4b75ffd70c
3 changed files with 499 additions and 1 deletions
|
|
@ -68,6 +68,74 @@ class DeleteResponse(BaseModel, ModelMixin):
|
|||
thread_id: str = Field(..., description="Deleted thread ID")
|
||||
|
||||
|
||||
# Tool Management Models
|
||||
class ToolInfo(BaseModel, ModelMixin):
|
||||
"""Information about a chatbot tool"""
|
||||
|
||||
id: str = Field(..., description="Tool UUID")
|
||||
tool_id: str = Field(
|
||||
..., description="Tool identifier (e.g., 'shared.tavily_search')"
|
||||
)
|
||||
name: str = Field(..., description="Tool function name")
|
||||
label: str = Field(..., description="Display label for the tool")
|
||||
category: str = Field(..., description="Tool category (shared or customer)")
|
||||
description: str = Field(..., description="Tool description")
|
||||
is_active: bool = Field(..., description="Whether the tool is active")
|
||||
date_created: float = Field(..., description="Creation timestamp")
|
||||
date_updated: float = Field(..., description="Last update timestamp")
|
||||
|
||||
|
||||
class ToolListResponse(BaseModel, ModelMixin):
|
||||
"""Response model for listing all tools"""
|
||||
|
||||
tools: List[ToolInfo] = Field(..., description="List of available tools")
|
||||
|
||||
|
||||
class GrantToolRequest(BaseModel, ModelMixin):
|
||||
"""Request model for granting a tool to a user"""
|
||||
|
||||
user_id: str = Field(..., description="User ID to grant the tool to")
|
||||
tool_id: str = Field(..., description="Tool UUID from tools table")
|
||||
|
||||
|
||||
class GrantToolResponse(BaseModel, ModelMixin):
|
||||
"""Response model after granting a tool"""
|
||||
|
||||
message: str = Field(..., description="Confirmation message")
|
||||
user_id: str = Field(..., description="User ID")
|
||||
tool_id: str = Field(..., description="Tool UUID")
|
||||
|
||||
|
||||
class RevokeToolRequest(BaseModel, ModelMixin):
|
||||
"""Request model for revoking a tool from a user"""
|
||||
|
||||
user_id: str = Field(..., description="User ID to revoke the tool from")
|
||||
tool_id: str = Field(..., description="Tool UUID from tools table")
|
||||
|
||||
|
||||
class RevokeToolResponse(BaseModel, ModelMixin):
|
||||
"""Response model after revoking a tool"""
|
||||
|
||||
message: str = Field(..., description="Confirmation message")
|
||||
user_id: str = Field(..., description="User ID")
|
||||
tool_id: str = Field(..., description="Tool UUID")
|
||||
|
||||
|
||||
class UpdateToolRequest(BaseModel, ModelMixin):
|
||||
"""Request model for updating a tool's label and description"""
|
||||
|
||||
label: Optional[str] = Field(None, description="New label for the tool")
|
||||
description: Optional[str] = Field(None, description="New description for the tool")
|
||||
|
||||
|
||||
class UpdateToolResponse(BaseModel, ModelMixin):
|
||||
"""Response model after updating a tool"""
|
||||
|
||||
message: str = Field(..., description="Confirmation message")
|
||||
tool_id: str = Field(..., description="Tool UUID")
|
||||
updated_fields: List[str] = Field(..., description="List of updated field names")
|
||||
|
||||
|
||||
# Register model labels for internationalization
|
||||
register_model_labels(
|
||||
"MessageItem",
|
||||
|
|
|
|||
|
|
@ -683,3 +683,210 @@ async def delete_thread_for_user(
|
|||
)
|
||||
|
||||
logger.info(f"Successfully deleted thread {thread_id} for user {user.id}")
|
||||
|
||||
|
||||
# Tool Management Functions
|
||||
|
||||
|
||||
async def get_all_tools(*, session: AsyncSession) -> List[dict]:
|
||||
"""Get all tools from the database.
|
||||
|
||||
Args:
|
||||
session: The database session for querying.
|
||||
|
||||
Returns:
|
||||
List of tool dictionaries with all tool information.
|
||||
"""
|
||||
from modules.features.chatBot.database import Tool
|
||||
|
||||
logger.info("Fetching all tools from database")
|
||||
|
||||
stmt = select(Tool).order_by(Tool.category, Tool.name)
|
||||
result = await session.execute(stmt)
|
||||
tools = result.scalars().all()
|
||||
|
||||
tool_list = []
|
||||
for tool in tools:
|
||||
tool_dict = {
|
||||
"id": str(tool.id),
|
||||
"tool_id": tool.tool_id,
|
||||
"name": tool.name,
|
||||
"label": tool.label,
|
||||
"category": tool.category,
|
||||
"description": tool.description,
|
||||
"is_active": tool.is_active,
|
||||
"date_created": tool.date_created.timestamp(),
|
||||
"date_updated": tool.date_updated.timestamp(),
|
||||
}
|
||||
tool_list.append(tool_dict)
|
||||
|
||||
logger.info(f"Retrieved {len(tool_list)} tools from database")
|
||||
return tool_list
|
||||
|
||||
|
||||
async def grant_tool_to_user(
|
||||
*, user_id: str, tool_id: str, session: AsyncSession
|
||||
) -> None:
|
||||
"""Grant a tool to a user.
|
||||
|
||||
Args:
|
||||
user_id: The user ID to grant the tool to.
|
||||
tool_id: The tool UUID from the tools table.
|
||||
session: The database session for querying/updating.
|
||||
|
||||
Raises:
|
||||
ValueError: If the tool doesn't exist, is not active, or user already has the tool.
|
||||
"""
|
||||
from modules.features.chatBot.database import Tool, UserToolMapping
|
||||
import uuid
|
||||
|
||||
logger.info(f"Granting tool {tool_id} to user {user_id}")
|
||||
|
||||
# Convert tool_id string to UUID
|
||||
try:
|
||||
tool_uuid = uuid.UUID(tool_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid tool ID format: {tool_id}")
|
||||
|
||||
# Check if tool exists and is active
|
||||
stmt = select(Tool).where(Tool.id == tool_uuid)
|
||||
result = await session.execute(stmt)
|
||||
tool = result.scalar_one_or_none()
|
||||
|
||||
if tool is None:
|
||||
raise ValueError(f"Tool with ID {tool_id} does not exist")
|
||||
|
||||
if not tool.is_active:
|
||||
raise ValueError(
|
||||
f"Cannot grant inactive tool '{tool.label}' (tool_id: {tool.tool_id}). "
|
||||
f"Please activate the tool first before granting it to users."
|
||||
)
|
||||
|
||||
# Check if user already has this tool
|
||||
stmt = select(UserToolMapping).where(
|
||||
UserToolMapping.user_id == user_id, UserToolMapping.tool_id == tool_uuid
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
existing_mapping = result.scalar_one_or_none()
|
||||
|
||||
if existing_mapping is not None:
|
||||
raise ValueError(
|
||||
f"User {user_id} already has access to tool '{tool.label}' (tool_id: {tool.tool_id})"
|
||||
)
|
||||
|
||||
# Create new mapping
|
||||
new_mapping = UserToolMapping(
|
||||
user_id=user_id,
|
||||
tool_id=tool_uuid,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
session.add(new_mapping)
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"Successfully granted tool {tool_id} ({tool.label}) to user {user_id}")
|
||||
|
||||
|
||||
async def revoke_tool_from_user(
|
||||
*, user_id: str, tool_id: str, session: AsyncSession
|
||||
) -> None:
|
||||
"""Revoke a tool from a user by deleting the mapping.
|
||||
|
||||
Args:
|
||||
user_id: The user ID to revoke the tool from.
|
||||
tool_id: The tool UUID from the tools table.
|
||||
session: The database session for deleting.
|
||||
|
||||
Raises:
|
||||
ValueError: If the mapping doesn't exist.
|
||||
"""
|
||||
from modules.features.chatBot.database import UserToolMapping
|
||||
import uuid
|
||||
|
||||
logger.info(f"Revoking tool {tool_id} from user {user_id}")
|
||||
|
||||
# Convert tool_id string to UUID
|
||||
try:
|
||||
tool_uuid = uuid.UUID(tool_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid tool ID format: {tool_id}")
|
||||
|
||||
# Delete the mapping
|
||||
stmt = delete(UserToolMapping).where(
|
||||
UserToolMapping.user_id == user_id, UserToolMapping.tool_id == tool_uuid
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
# Check if any rows were deleted
|
||||
if result.rowcount == 0:
|
||||
raise ValueError(
|
||||
f"User {user_id} does not have access to tool {tool_id}, or the mapping does not exist"
|
||||
)
|
||||
|
||||
logger.info(f"Successfully revoked tool {tool_id} from user {user_id}")
|
||||
|
||||
|
||||
async def update_tool(
|
||||
*,
|
||||
tool_id: str,
|
||||
label: Optional[str],
|
||||
description: Optional[str],
|
||||
session: AsyncSession,
|
||||
) -> List[str]:
|
||||
"""Update a tool's label and/or description.
|
||||
|
||||
Args:
|
||||
tool_id: The tool UUID to update.
|
||||
label: Optional new label for the tool.
|
||||
description: Optional new description for the tool.
|
||||
session: The database session for updating.
|
||||
|
||||
Returns:
|
||||
List of updated field names.
|
||||
|
||||
Raises:
|
||||
ValueError: If the tool doesn't exist or no fields provided to update.
|
||||
"""
|
||||
from modules.features.chatBot.database import Tool
|
||||
import uuid
|
||||
|
||||
logger.info(f"Updating tool {tool_id}")
|
||||
|
||||
# Validate that at least one field is provided
|
||||
if label is None and description is None:
|
||||
raise ValueError("At least one field (label or description) must be provided")
|
||||
|
||||
# Convert tool_id string to UUID
|
||||
try:
|
||||
tool_uuid = uuid.UUID(tool_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid tool ID format: {tool_id}")
|
||||
|
||||
# Check if tool exists
|
||||
stmt = select(Tool).where(Tool.id == tool_uuid)
|
||||
result = await session.execute(stmt)
|
||||
tool = result.scalar_one_or_none()
|
||||
|
||||
if tool is None:
|
||||
raise ValueError(f"Tool with ID {tool_id} does not exist")
|
||||
|
||||
# Build update values
|
||||
update_values = {"date_updated": datetime.now(timezone.utc)}
|
||||
updated_fields = []
|
||||
|
||||
if label is not None:
|
||||
update_values["label"] = label
|
||||
updated_fields.append("label")
|
||||
|
||||
if description is not None:
|
||||
update_values["description"] = description
|
||||
updated_fields.append("description")
|
||||
|
||||
# Update the tool
|
||||
stmt = update(Tool).where(Tool.id == tool_uuid).values(**update_values)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"Successfully updated tool {tool_id}, fields: {updated_fields}")
|
||||
return updated_fields
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from modules.features.chatBot.database import get_async_db_session
|
|||
from modules.features.chatBot.service import (
|
||||
get_or_create_thread_for_user,
|
||||
)
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.datamodels.datamodelUam import User, UserPrivilege
|
||||
from modules.datamodels.datamodelChatbot import (
|
||||
ChatMessageRequest,
|
||||
MessageItem,
|
||||
|
|
@ -22,6 +22,14 @@ from modules.datamodels.datamodelChatbot import (
|
|||
ThreadDetail,
|
||||
RenameThreadRequest,
|
||||
DeleteResponse,
|
||||
ToolListResponse,
|
||||
ToolInfo,
|
||||
GrantToolRequest,
|
||||
GrantToolResponse,
|
||||
RevokeToolRequest,
|
||||
RevokeToolResponse,
|
||||
UpdateToolRequest,
|
||||
UpdateToolResponse,
|
||||
)
|
||||
from modules.security.auth import getCurrentUser, limiter
|
||||
from modules.features.chatBot import service as chat_service
|
||||
|
|
@ -311,3 +319,218 @@ async def delete_thread(
|
|||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete thread: {type(e).__name__}: {str(e) or 'No error message provided'}",
|
||||
)
|
||||
|
||||
|
||||
# Tool Management Endpoints
|
||||
|
||||
|
||||
@router.get("/tools", response_model=ToolListResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def get_all_tools(
|
||||
*,
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser),
|
||||
session: AsyncSession = Depends(get_async_db_session),
|
||||
) -> ToolListResponse:
|
||||
"""
|
||||
Get all available chatbot tools.
|
||||
Only accessible to system administrators.
|
||||
"""
|
||||
try:
|
||||
# Check SYSADMIN permission
|
||||
if currentUser.privilege != UserPrivilege.SYSADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only system administrators can view tools",
|
||||
)
|
||||
|
||||
# Get all tools from service
|
||||
tools_data = await chat_service.get_all_tools(session=session)
|
||||
|
||||
# Convert to ToolInfo objects
|
||||
tools = [ToolInfo(**tool) for tool in tools_data]
|
||||
|
||||
logger.info(f"User {currentUser.id} retrieved {len(tools)} tools")
|
||||
|
||||
return ToolListResponse(tools=tools)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error retrieving tools: {type(e).__name__}: {str(e)}", exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to retrieve tools: {type(e).__name__}: {str(e) or 'No error message provided'}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tools/grant", response_model=GrantToolResponse)
|
||||
@limiter.limit("10/minute")
|
||||
async def grant_tool_to_user(
|
||||
*,
|
||||
request: Request,
|
||||
grant_request: GrantToolRequest,
|
||||
currentUser: User = Depends(getCurrentUser),
|
||||
session: AsyncSession = Depends(get_async_db_session),
|
||||
) -> GrantToolResponse:
|
||||
"""
|
||||
Grant a tool to a user.
|
||||
Only accessible to system administrators.
|
||||
"""
|
||||
try:
|
||||
# Check SYSADMIN permission
|
||||
if currentUser.privilege != UserPrivilege.SYSADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only system administrators can grant tools",
|
||||
)
|
||||
|
||||
# Grant the tool
|
||||
await chat_service.grant_tool_to_user(
|
||||
user_id=grant_request.user_id,
|
||||
tool_id=grant_request.tool_id,
|
||||
session=session,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"User {currentUser.id} granted tool {grant_request.tool_id} to user {grant_request.user_id}"
|
||||
)
|
||||
|
||||
return GrantToolResponse(
|
||||
message=f"Tool successfully granted to user {grant_request.user_id}",
|
||||
user_id=grant_request.user_id,
|
||||
tool_id=grant_request.tool_id,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation error: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e) or "Invalid request",
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error granting tool: {type(e).__name__}: {str(e)}", exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to grant tool: {type(e).__name__}: {str(e) or 'No error message provided'}",
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tools/revoke", response_model=RevokeToolResponse)
|
||||
@limiter.limit("10/minute")
|
||||
async def revoke_tool_from_user(
|
||||
*,
|
||||
request: Request,
|
||||
revoke_request: RevokeToolRequest,
|
||||
currentUser: User = Depends(getCurrentUser),
|
||||
session: AsyncSession = Depends(get_async_db_session),
|
||||
) -> RevokeToolResponse:
|
||||
"""
|
||||
Revoke a tool from a user.
|
||||
Only accessible to system administrators.
|
||||
"""
|
||||
try:
|
||||
# Check SYSADMIN permission
|
||||
if currentUser.privilege != UserPrivilege.SYSADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only system administrators can revoke tools",
|
||||
)
|
||||
|
||||
# Revoke the tool
|
||||
await chat_service.revoke_tool_from_user(
|
||||
user_id=revoke_request.user_id,
|
||||
tool_id=revoke_request.tool_id,
|
||||
session=session,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"User {currentUser.id} revoked tool {revoke_request.tool_id} from user {revoke_request.user_id}"
|
||||
)
|
||||
|
||||
return RevokeToolResponse(
|
||||
message=f"Tool successfully revoked from user {revoke_request.user_id}",
|
||||
user_id=revoke_request.user_id,
|
||||
tool_id=revoke_request.tool_id,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation error: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e) or "Invalid request",
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error revoking tool: {type(e).__name__}: {str(e)}", exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to revoke tool: {type(e).__name__}: {str(e) or 'No error message provided'}",
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/tools/{tool_id}", response_model=UpdateToolResponse)
|
||||
@limiter.limit("10/minute")
|
||||
async def update_tool(
|
||||
*,
|
||||
request: Request,
|
||||
tool_id: str,
|
||||
update_request: UpdateToolRequest,
|
||||
currentUser: User = Depends(getCurrentUser),
|
||||
session: AsyncSession = Depends(get_async_db_session),
|
||||
) -> UpdateToolResponse:
|
||||
"""
|
||||
Update a tool's label and/or description.
|
||||
Only accessible to system administrators.
|
||||
"""
|
||||
try:
|
||||
# Check SYSADMIN permission
|
||||
if currentUser.privilege != UserPrivilege.SYSADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only system administrators can update tools",
|
||||
)
|
||||
|
||||
# Update the tool
|
||||
updated_fields = await chat_service.update_tool(
|
||||
tool_id=tool_id,
|
||||
label=update_request.label,
|
||||
description=update_request.description,
|
||||
session=session,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"User {currentUser.id} updated tool {tool_id}, fields: {updated_fields}"
|
||||
)
|
||||
|
||||
return UpdateToolResponse(
|
||||
message="Tool successfully updated",
|
||||
tool_id=tool_id,
|
||||
updated_fields=updated_fields,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation error: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e) or "Invalid request",
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error updating tool: {type(e).__name__}: {str(e)}", exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update tool: {type(e).__name__}: {str(e) or 'No error message provided'}",
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue