This commit is contained in:
Timothy Jaeryang Baek
2026-03-17 17:58:01 -05:00
parent fcf7208352
commit de3317e26b
220 changed files with 17200 additions and 22836 deletions
+57 -75
View File
@@ -53,18 +53,16 @@ class UserAnalyticsResponse(BaseModel):
####################
@router.get("/models", response_model=ModelAnalyticsResponse)
@router.get('/models', response_model=ModelAnalyticsResponse)
async def get_model_analytics(
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'),
end_date: Optional[int] = Query(None, description='End timestamp (epoch)'),
group_id: Optional[str] = Query(None, description='Filter by user group ID'),
user=Depends(get_admin_user),
db: Session = Depends(get_session),
):
"""Get message counts per model."""
counts = ChatMessages.get_message_count_by_model(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
counts = ChatMessages.get_message_count_by_model(start_date=start_date, end_date=end_date, group_id=group_id, db=db)
models = [
ModelAnalyticsEntry(model_id=model_id, count=count)
for model_id, count in sorted(counts.items(), key=lambda x: -x[1])
@@ -72,27 +70,23 @@ async def get_model_analytics(
return ModelAnalyticsResponse(models=models)
@router.get("/users", response_model=UserAnalyticsResponse)
@router.get('/users', response_model=UserAnalyticsResponse)
async def get_user_analytics(
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
limit: int = Query(50, description="Max users to return"),
start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'),
end_date: Optional[int] = Query(None, description='End timestamp (epoch)'),
group_id: Optional[str] = Query(None, description='Filter by user group ID'),
limit: int = Query(50, description='Max users to return'),
user=Depends(get_admin_user),
db: Session = Depends(get_session),
):
"""Get message counts and token usage per user with user info."""
counts = ChatMessages.get_message_count_by_user(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
counts = ChatMessages.get_message_count_by_user(start_date=start_date, end_date=end_date, group_id=group_id, db=db)
token_usage = ChatMessages.get_token_usage_by_user(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
# Get user info for top users
top_user_ids = [
uid for uid, _ in sorted(counts.items(), key=lambda x: -x[1])[:limit]
]
top_user_ids = [uid for uid, _ in sorted(counts.items(), key=lambda x: -x[1])[:limit]]
user_info = {u.id: u for u in Users.get_users_by_user_ids(top_user_ids, db=db)}
users = []
@@ -105,22 +99,22 @@ async def get_user_analytics(
name=u.name if u else None,
email=u.email if u else None,
count=counts[user_id],
input_tokens=tokens.get("input_tokens", 0),
output_tokens=tokens.get("output_tokens", 0),
total_tokens=tokens.get("total_tokens", 0),
input_tokens=tokens.get('input_tokens', 0),
output_tokens=tokens.get('output_tokens', 0),
total_tokens=tokens.get('total_tokens', 0),
)
)
return UserAnalyticsResponse(users=users)
@router.get("/messages", response_model=list[ChatMessageModel])
@router.get('/messages', response_model=list[ChatMessageModel])
async def get_messages(
model_id: Optional[str] = Query(None, description="Filter by model ID"),
user_id: Optional[str] = Query(None, description="Filter by user ID"),
chat_id: Optional[str] = Query(None, description="Filter by chat ID"),
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
model_id: Optional[str] = Query(None, description='Filter by model ID'),
user_id: Optional[str] = Query(None, description='Filter by user ID'),
chat_id: Optional[str] = Query(None, description='Filter by chat ID'),
start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'),
end_date: Optional[int] = Query(None, description='End timestamp (epoch)'),
skip: int = Query(0),
limit: int = Query(50, le=100),
user=Depends(get_admin_user),
@@ -139,9 +133,7 @@ async def get_messages(
db=db,
)
elif user_id:
return ChatMessages.get_messages_by_user_id(
user_id=user_id, skip=skip, limit=limit, db=db
)
return ChatMessages.get_messages_by_user_id(user_id=user_id, skip=skip, limit=limit, db=db)
else:
# Return empty if no filter specified
return []
@@ -154,11 +146,11 @@ class SummaryResponse(BaseModel):
total_users: int
@router.get("/summary", response_model=SummaryResponse)
@router.get('/summary', response_model=SummaryResponse)
async def get_summary(
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'),
end_date: Optional[int] = Query(None, description='End timestamp (epoch)'),
group_id: Optional[str] = Query(None, description='Filter by user group ID'),
user=Depends(get_admin_user),
db: Session = Depends(get_session),
):
@@ -190,29 +182,24 @@ class DailyStatsResponse(BaseModel):
data: list[DailyStatsEntry]
@router.get("/daily", response_model=DailyStatsResponse)
@router.get('/daily', response_model=DailyStatsResponse)
async def get_daily_stats(
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
granularity: str = Query("daily", description="Granularity: 'hourly' or 'daily'"),
start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'),
end_date: Optional[int] = Query(None, description='End timestamp (epoch)'),
group_id: Optional[str] = Query(None, description='Filter by user group ID'),
granularity: str = Query('daily', description="Granularity: 'hourly' or 'daily'"),
user=Depends(get_admin_user),
db: Session = Depends(get_session),
):
"""Get message counts grouped by model for time-series chart."""
if granularity == "hourly":
counts = ChatMessages.get_hourly_message_counts_by_model(
start_date=start_date, end_date=end_date, db=db
)
if granularity == 'hourly':
counts = ChatMessages.get_hourly_message_counts_by_model(start_date=start_date, end_date=end_date, db=db)
else:
counts = ChatMessages.get_daily_message_counts_by_model(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
return DailyStatsResponse(
data=[
DailyStatsEntry(date=date, models=models)
for date, models in sorted(counts.items())
]
data=[DailyStatsEntry(date=date, models=models) for date, models in sorted(counts.items())]
)
@@ -231,22 +218,20 @@ class TokenUsageResponse(BaseModel):
total_tokens: int
@router.get("/tokens", response_model=TokenUsageResponse)
@router.get('/tokens', response_model=TokenUsageResponse)
async def get_token_usage(
start_date: Optional[int] = Query(None),
end_date: Optional[int] = Query(None),
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
group_id: Optional[str] = Query(None, description='Filter by user group ID'),
user=Depends(get_admin_user),
db: Session = Depends(get_session),
):
"""Get token usage aggregated by model."""
usage = ChatMessages.get_token_usage_by_model(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
usage = ChatMessages.get_token_usage_by_model(start_date=start_date, end_date=end_date, group_id=group_id, db=db)
models = [
TokenUsageEntry(model_id=model_id, **data)
for model_id, data in sorted(usage.items(), key=lambda x: -x[1]["total_tokens"])
for model_id, data in sorted(usage.items(), key=lambda x: -x[1]['total_tokens'])
]
total_input = sum(m.input_tokens for m in models)
@@ -278,7 +263,7 @@ class ModelChatsResponse(BaseModel):
total: int
@router.get("/models/{model_id:path}/chats", response_model=ModelChatsResponse)
@router.get('/models/{model_id:path}/chats', response_model=ModelChatsResponse)
async def get_model_chats(
model_id: str,
start_date: Optional[int] = Query(None),
@@ -311,7 +296,7 @@ async def get_model_chats(
continue
# Get user_id from first user message
first_user_msg = next((m for m in messages if m.role == "user"), None)
first_user_msg = next((m for m in messages if m.role == 'user'), None)
user_id = first_user_msg.user_id if first_user_msg else None
# Extract first message content as preview
@@ -321,8 +306,8 @@ async def get_model_chats(
if isinstance(content, str):
first_message = content[:200]
elif isinstance(content, list):
text_parts = [b.get("text", "") for b in content if isinstance(b, dict)]
first_message = " ".join(text_parts)[:200]
text_parts = [b.get('text', '') for b in content if isinstance(b, dict)]
first_message = ' '.join(text_parts)[:200]
# Get user info
user_name = None
@@ -367,10 +352,10 @@ class ModelOverviewResponse(BaseModel):
tags: list[TagEntry]
@router.get("/models/{model_id:path}/overview", response_model=ModelOverviewResponse)
@router.get('/models/{model_id:path}/overview', response_model=ModelOverviewResponse)
async def get_model_overview(
model_id: str,
days: int = Query(30, description="Number of days of history (0 for all)"),
days: int = Query(30, description='Number of days of history (0 for all)'),
user=Depends(get_admin_user),
db: Session = Depends(get_session),
):
@@ -387,7 +372,7 @@ async def get_model_overview(
)
# Get feedback history per day
history_counts: dict[str, dict] = defaultdict(lambda: {"won": 0, "lost": 0})
history_counts: dict[str, dict] = defaultdict(lambda: {'won': 0, 'lost': 0})
# Calculate start date for history
now = datetime.now()
@@ -398,19 +383,19 @@ async def get_model_overview(
for chat_id in chat_ids:
feedbacks = Feedbacks.get_feedbacks_by_chat_id(chat_id, db=db)
for fb in feedbacks:
if fb.data and "rating" in fb.data:
rating = fb.data["rating"]
if fb.data and 'rating' in fb.data:
rating = fb.data['rating']
fb_date = datetime.fromtimestamp(fb.created_at)
# Filter by date range
if start_dt and fb_date < start_dt:
continue
date_str = fb_date.strftime("%Y-%m-%d")
date_str = fb_date.strftime('%Y-%m-%d')
if rating == 1:
history_counts[date_str]["won"] += 1
history_counts[date_str]['won'] += 1
elif rating == -1:
history_counts[date_str]["lost"] += 1
history_counts[date_str]['lost'] += 1
# Fill in missing days
history = []
@@ -421,18 +406,18 @@ async def get_model_overview(
elif history_counts:
# Find earliest date
min_date = min(history_counts.keys())
current = datetime.strptime(min_date, "%Y-%m-%d")
current = datetime.strptime(min_date, '%Y-%m-%d')
else:
current = now
while current <= end_dt:
date_str = current.strftime("%Y-%m-%d")
counts = history_counts.get(date_str, {"won": 0, "lost": 0})
date_str = current.strftime('%Y-%m-%d')
counts = history_counts.get(date_str, {'won': 0, 'lost': 0})
history.append(
HistoryEntry(
date=date_str,
won=counts["won"],
lost=counts["lost"],
won=counts['won'],
lost=counts['lost'],
)
)
current += timedelta(days=1)
@@ -442,13 +427,10 @@ async def get_model_overview(
for chat_id in chat_ids:
chat = Chats.get_chat_by_id(chat_id, db=db)
if chat and chat.meta:
for tag in chat.meta.get("tags", []):
for tag in chat.meta.get('tags', []):
tag_counts[tag] += 1
# Sort by count and take top 10
tags = [
TagEntry(tag=tag, count=count)
for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1])[:10]
]
tags = [TagEntry(tag=tag, count=count) for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1])[:10]]
return ModelOverviewResponse(history=history, tags=tags)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+143 -192
View File
@@ -44,7 +44,7 @@ class ImportConfigForm(BaseModel):
config: dict
@router.post("/import", response_model=dict)
@router.post('/import', response_model=dict)
async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user)):
save_config(form_data.config)
return get_config()
@@ -55,7 +55,7 @@ async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user
############################
@router.get("/export", response_model=dict)
@router.get('/export', response_model=dict)
async def export_config(user=Depends(get_admin_user)):
return get_config()
@@ -70,30 +70,26 @@ class ConnectionsConfigForm(BaseModel):
ENABLE_BASE_MODELS_CACHE: bool
@router.get("/connections", response_model=ConnectionsConfigForm)
@router.get('/connections', response_model=ConnectionsConfigForm)
async def get_connections_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE,
}
@router.post("/connections", response_model=ConnectionsConfigForm)
@router.post('/connections', response_model=ConnectionsConfigForm)
async def set_connections_config(
request: Request,
form_data: ConnectionsConfigForm,
user=Depends(get_admin_user),
):
request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
form_data.ENABLE_DIRECT_CONNECTIONS
)
request.app.state.config.ENABLE_BASE_MODELS_CACHE = (
form_data.ENABLE_BASE_MODELS_CACHE
)
request.app.state.config.ENABLE_DIRECT_CONNECTIONS = form_data.ENABLE_DIRECT_CONNECTIONS
request.app.state.config.ENABLE_BASE_MODELS_CACHE = form_data.ENABLE_BASE_MODELS_CACHE
return {
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE,
}
@@ -103,7 +99,7 @@ class OAuthClientRegistrationForm(BaseModel):
client_name: Optional[str] = None
@router.post("/oauth/clients/register")
@router.post('/oauth/clients/register')
async def register_oauth_client(
request: Request,
form_data: OAuthClientRegistrationForm,
@@ -113,24 +109,20 @@ async def register_oauth_client(
try:
oauth_client_id = form_data.client_id
if type:
oauth_client_id = f"{type}:{form_data.client_id}"
oauth_client_id = f'{type}:{form_data.client_id}'
oauth_client_info = (
await get_oauth_client_info_with_dynamic_client_registration(
request, oauth_client_id, form_data.url
)
oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration(
request, oauth_client_id, form_data.url
)
return {
"status": True,
"oauth_client_info": encrypt_data(
oauth_client_info.model_dump(mode="json")
),
'status': True,
'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')),
}
except Exception as e:
log.debug(f"Failed to register OAuth client: {e}")
log.debug(f'Failed to register OAuth client: {e}')
raise HTTPException(
status_code=400,
detail=f"Failed to register OAuth client",
detail=f'Failed to register OAuth client',
)
@@ -142,40 +134,40 @@ async def register_oauth_client(
class ToolServerConnection(BaseModel):
url: str
path: str
type: Optional[str] = "openapi" # openapi, mcp
type: Optional[str] = 'openapi' # openapi, mcp
auth_type: Optional[str]
headers: Optional[dict | str] = None
key: Optional[str]
config: Optional[dict]
model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra='allow')
class ToolServersConfigForm(BaseModel):
TOOL_SERVER_CONNECTIONS: list[ToolServerConnection]
@router.get("/tool_servers", response_model=ToolServersConfigForm)
@router.get('/tool_servers', response_model=ToolServersConfigForm)
async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)):
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers", response_model=ToolServersConfigForm)
@router.post('/tool_servers', response_model=ToolServersConfigForm)
async def set_tool_servers_config(
request: Request,
form_data: ToolServersConfigForm,
user=Depends(get_admin_user),
):
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
server_type = connection.get("type", "openapi")
auth_type = connection.get("auth_type", "none")
server_type = connection.get('type', 'openapi')
auth_type = connection.get('auth_type', 'none')
if auth_type == "oauth_2.1":
if auth_type == 'oauth_2.1':
# Remove existing OAuth clients for tool servers
server_id = connection.get("info", {}).get("id")
client_key = f"{server_type}:{server_id}"
server_id = connection.get('info', {}).get('id')
client_key = f'{server_type}:{server_id}'
try:
request.app.state.oauth_client_manager.remove_client(client_key)
@@ -190,42 +182,40 @@ async def set_tool_servers_config(
await set_tool_servers(request)
for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
server_type = connection.get("type", "openapi")
if server_type == "mcp":
server_id = connection.get("info", {}).get("id")
auth_type = connection.get("auth_type", "none")
server_type = connection.get('type', 'openapi')
if server_type == 'mcp':
server_id = connection.get('info', {}).get('id')
auth_type = connection.get('auth_type', 'none')
if auth_type == "oauth_2.1" and server_id:
if auth_type == 'oauth_2.1' and server_id:
try:
oauth_client_info = connection.get("info", {}).get(
"oauth_client_info", ""
)
oauth_client_info = connection.get('info', {}).get('oauth_client_info', '')
oauth_client_info = decrypt_data(oauth_client_info)
request.app.state.oauth_client_manager.add_client(
f"{server_type}:{server_id}",
f'{server_type}:{server_id}',
OAuthClientInformationFull(**oauth_client_info),
)
except Exception as e:
log.debug(f"Failed to add OAuth client for MCP tool server: {e}")
log.debug(f'Failed to add OAuth client for MCP tool server: {e}')
continue
return {
"TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS,
'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS,
}
class TerminalServerConnection(BaseModel):
id: Optional[str] = ""
name: Optional[str] = ""
id: Optional[str] = ''
name: Optional[str] = ''
enabled: Optional[bool] = True
url: str
path: Optional[str] = "/openapi.json"
path: Optional[str] = '/openapi.json'
key: Optional[str] = ""
auth_type: Optional[str] = "bearer"
key: Optional[str] = ''
auth_type: Optional[str] = 'bearer'
config: Optional[dict] = None
@@ -234,21 +224,21 @@ class TerminalServerConnection(BaseModel):
policy_id: Optional[str] = None
policy: Optional[dict] = None # cached policy data
model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra='allow')
class TerminalServersConfigForm(BaseModel):
TERMINAL_SERVER_CONNECTIONS: list[TerminalServerConnection]
@router.get("/terminal_servers")
@router.get('/terminal_servers')
async def get_terminal_servers_config(request: Request, user=Depends(get_admin_user)):
return {
"TERMINAL_SERVER_CONNECTIONS": request.app.state.config.TERMINAL_SERVER_CONNECTIONS,
'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS,
}
@router.post("/terminal_servers")
@router.post('/terminal_servers')
async def set_terminal_servers_config(
request: Request,
form_data: TerminalServersConfigForm,
@@ -261,57 +251,45 @@ async def set_terminal_servers_config(
await set_terminal_servers(request)
return {
"TERMINAL_SERVER_CONNECTIONS": request.app.state.config.TERMINAL_SERVER_CONNECTIONS,
'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS,
}
@router.post("/tool_servers/verify")
async def verify_tool_servers_config(
request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)
):
@router.post('/tool_servers/verify')
async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)):
"""
Verify the connection to the tool server.
"""
try:
if form_data.type == "mcp":
if form_data.auth_type == "oauth_2.1":
if form_data.type == 'mcp':
if form_data.auth_type == 'oauth_2.1':
discovery_urls = await get_discovery_urls(form_data.url)
for discovery_url in discovery_urls:
log.debug(
f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}"
)
log.debug(f'Trying to fetch OAuth 2.1 discovery document from {discovery_url}')
async with aiohttp.ClientSession(
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
) as session:
async with session.get(
discovery_url
) as oauth_server_metadata_response:
async with session.get(discovery_url) as oauth_server_metadata_response:
if oauth_server_metadata_response.status == 200:
try:
oauth_server_metadata = (
OAuthMetadata.model_validate(
await oauth_server_metadata_response.json()
)
oauth_server_metadata = OAuthMetadata.model_validate(
await oauth_server_metadata_response.json()
)
return {
"status": True,
"oauth_server_metadata": oauth_server_metadata.model_dump(
mode="json"
),
'status': True,
'oauth_server_metadata': oauth_server_metadata.model_dump(mode='json'),
}
except Exception as e:
log.info(
f"Failed to parse OAuth 2.1 discovery document: {e}"
)
log.info(f'Failed to parse OAuth 2.1 discovery document: {e}')
raise HTTPException(
status_code=400,
detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_url}",
detail=f'Failed to parse OAuth 2.1 discovery document from {discovery_url}',
)
raise HTTPException(
status_code=400,
detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls}",
detail=f'Failed to fetch OAuth 2.1 discovery document from {discovery_urls}',
)
else:
try:
@@ -319,25 +297,25 @@ async def verify_tool_servers_config(
headers = None
token = None
if form_data.auth_type == "bearer":
if form_data.auth_type == 'bearer':
token = form_data.key
elif form_data.auth_type == "session":
elif form_data.auth_type == 'session':
token = request.state.token.credentials
elif form_data.auth_type == "system_oauth":
elif form_data.auth_type == 'system_oauth':
oauth_token = None
try:
if request.cookies.get("oauth_session_id", None):
if request.cookies.get('oauth_session_id', None):
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
user.id,
request.cookies.get("oauth_session_id", None),
request.cookies.get('oauth_session_id', None),
)
if oauth_token:
token = oauth_token.get("access_token", "")
token = oauth_token.get('access_token', '')
except Exception as e:
pass
if token:
headers = {"Authorization": f"Bearer {token}"}
headers = {'Authorization': f'Bearer {token}'}
if form_data.headers and isinstance(form_data.headers, dict):
if headers is None:
@@ -347,14 +325,14 @@ async def verify_tool_servers_config(
await client.connect(form_data.url, headers=headers)
specs = await client.list_tool_specs()
return {
"status": True,
"specs": specs,
'status': True,
'specs': specs,
}
except Exception as e:
log.debug(f"Failed to create MCP client: {e}")
log.debug(f'Failed to create MCP client: {e}')
raise HTTPException(
status_code=400,
detail=f"Failed to create MCP client",
detail=f'Failed to create MCP client',
)
finally:
if client:
@@ -362,28 +340,26 @@ async def verify_tool_servers_config(
else: # openapi
token = None
headers = None
if form_data.auth_type == "bearer":
if form_data.auth_type == 'bearer':
token = form_data.key
elif form_data.auth_type == "session":
elif form_data.auth_type == 'session':
token = request.state.token.credentials
elif form_data.auth_type == "system_oauth":
elif form_data.auth_type == 'system_oauth':
try:
if request.cookies.get("oauth_session_id", None):
oauth_token = (
await request.app.state.oauth_manager.get_oauth_token(
user.id,
request.cookies.get("oauth_session_id", None),
)
if request.cookies.get('oauth_session_id', None):
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
user.id,
request.cookies.get('oauth_session_id', None),
)
if oauth_token:
token = oauth_token.get("access_token", "")
token = oauth_token.get('access_token', '')
except Exception as e:
pass
if token:
headers = {"Authorization": f"Bearer {token}"}
headers = {'Authorization': f'Bearer {token}'}
if form_data.headers and isinstance(form_data.headers, dict):
if headers is None:
@@ -395,10 +371,10 @@ async def verify_tool_servers_config(
except HTTPException as e:
raise e
except Exception as e:
log.debug(f"Failed to connect to the tool server: {e}")
log.debug(f'Failed to connect to the tool server: {e}')
raise HTTPException(
status_code=400,
detail=f"Failed to connect to the tool server",
detail=f'Failed to connect to the tool server',
)
@@ -423,91 +399,68 @@ class CodeInterpreterConfigForm(BaseModel):
CODE_INTERPRETER_JUPYTER_TIMEOUT: Optional[int]
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
@router.get('/code_execution', response_model=CodeInterpreterConfigForm)
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
"CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
"CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
"CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION,
'CODE_EXECUTION_ENGINE': request.app.state.config.CODE_EXECUTION_ENGINE,
'CODE_EXECUTION_JUPYTER_URL': request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
'CODE_EXECUTION_JUPYTER_AUTH': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
'CODE_EXECUTION_JUPYTER_TIMEOUT': request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER,
'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE,
'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
'CODE_INTERPRETER_JUPYTER_URL': request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
'CODE_INTERPRETER_JUPYTER_AUTH': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
'CODE_INTERPRETER_JUPYTER_TIMEOUT': request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
}
@router.post("/code_execution", response_model=CodeInterpreterConfigForm)
@router.post('/code_execution', response_model=CodeInterpreterConfigForm)
async def set_code_execution_config(
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
):
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
form_data.CODE_EXECUTION_JUPYTER_URL
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = (
form_data.CODE_EXECUTION_JUPYTER_AUTH
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
)
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = (
form_data.CODE_EXECUTION_JUPYTER_TIMEOUT
)
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = form_data.CODE_EXECUTION_JUPYTER_URL
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = form_data.CODE_EXECUTION_JUPYTER_AUTH
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = form_data.CODE_EXECUTION_JUPYTER_TIMEOUT
request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
form_data.CODE_INTERPRETER_PROMPT_TEMPLATE
)
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = form_data.CODE_INTERPRETER_PROMPT_TEMPLATE
request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = (
form_data.CODE_INTERPRETER_JUPYTER_URL
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = form_data.CODE_INTERPRETER_JUPYTER_URL
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = form_data.CODE_INTERPRETER_JUPYTER_AUTH
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = (
form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT
)
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT
return {
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
"CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
"CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
"CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION,
'CODE_EXECUTION_ENGINE': request.app.state.config.CODE_EXECUTION_ENGINE,
'CODE_EXECUTION_JUPYTER_URL': request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
'CODE_EXECUTION_JUPYTER_AUTH': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
'CODE_EXECUTION_JUPYTER_TIMEOUT': request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER,
'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE,
'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
'CODE_INTERPRETER_JUPYTER_URL': request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
'CODE_INTERPRETER_JUPYTER_AUTH': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
'CODE_INTERPRETER_JUPYTER_TIMEOUT': request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
}
@@ -522,32 +475,30 @@ class ModelsConfigForm(BaseModel):
DEFAULT_MODEL_PARAMS: Optional[dict] = None
@router.get("/models", response_model=ModelsConfigForm)
@router.get('/models', response_model=ModelsConfigForm)
async def get_models_config(request: Request, user=Depends(get_admin_user)):
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"DEFAULT_PINNED_MODELS": request.app.state.config.DEFAULT_PINNED_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
"DEFAULT_MODEL_METADATA": request.app.state.config.DEFAULT_MODEL_METADATA,
"DEFAULT_MODEL_PARAMS": request.app.state.config.DEFAULT_MODEL_PARAMS,
'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS,
'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS,
'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST,
'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA,
'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS,
}
@router.post("/models", response_model=ModelsConfigForm)
async def set_models_config(
request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)
):
@router.post('/models', response_model=ModelsConfigForm)
async def set_models_config(request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)):
request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS
request.app.state.config.DEFAULT_PINNED_MODELS = form_data.DEFAULT_PINNED_MODELS
request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST
request.app.state.config.DEFAULT_MODEL_METADATA = form_data.DEFAULT_MODEL_METADATA
request.app.state.config.DEFAULT_MODEL_PARAMS = form_data.DEFAULT_MODEL_PARAMS
return {
"DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS,
"DEFAULT_PINNED_MODELS": request.app.state.config.DEFAULT_PINNED_MODELS,
"MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST,
"DEFAULT_MODEL_METADATA": request.app.state.config.DEFAULT_MODEL_METADATA,
"DEFAULT_MODEL_PARAMS": request.app.state.config.DEFAULT_MODEL_PARAMS,
'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS,
'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS,
'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST,
'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA,
'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS,
}
@@ -560,14 +511,14 @@ class SetDefaultSuggestionsForm(BaseModel):
suggestions: list[PromptSuggestion]
@router.post("/suggestions", response_model=list[PromptSuggestion])
@router.post('/suggestions', response_model=list[PromptSuggestion])
async def set_default_suggestions(
request: Request,
form_data: SetDefaultSuggestionsForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data['suggestions']
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
@@ -580,18 +531,18 @@ class SetBannersForm(BaseModel):
banners: list[BannerModel]
@router.post("/banners", response_model=list[BannerModel])
@router.post('/banners', response_model=list[BannerModel])
async def set_banners(
request: Request,
form_data: SetBannersForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.BANNERS = data["banners"]
request.app.state.config.BANNERS = data['banners']
return request.app.state.config.BANNERS
@router.get("/banners", response_model=list[BannerModel])
@router.get('/banners', response_model=list[BannerModel])
async def get_banners(
request: Request,
user=Depends(get_verified_user),
+68 -123
View File
@@ -51,9 +51,7 @@ router = APIRouter()
import os
EMBEDDING_MODEL_NAME = os.environ.get(
"AUXILIARY_EMBEDDING_MODEL", "TaylorAI/bge-micro-v2"
)
EMBEDDING_MODEL_NAME = os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2')
_embedding_model = None
@@ -65,13 +63,11 @@ def _get_embedding_model():
_embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME)
except Exception as e:
log.error(f"Embedding model load failed: {e}")
log.error(f'Embedding model load failed: {e}')
return _embedding_model
def _calculate_elo(
feedbacks: list[LeaderboardFeedbackData], similarities: dict = None
) -> dict:
def _calculate_elo(feedbacks: list[LeaderboardFeedbackData], similarities: dict = None) -> dict:
"""
Calculate Elo ratings for models based on user feedback.
@@ -90,35 +86,33 @@ def _calculate_elo(
def get_or_create_stats(model_id):
if model_id not in model_stats:
model_stats[model_id] = {"rating": 1000.0, "won": 0, "lost": 0}
model_stats[model_id] = {'rating': 1000.0, 'won': 0, 'lost': 0}
return model_stats[model_id]
for feedback in feedbacks:
data = feedback.data or {}
winner_id = data.get("model_id")
rating_value = str(data.get("rating", ""))
if not winner_id or rating_value not in ("1", "-1"):
winner_id = data.get('model_id')
rating_value = str(data.get('rating', ''))
if not winner_id or rating_value not in ('1', '-1'):
continue
won = rating_value == "1"
won = rating_value == '1'
weight = similarities.get(feedback.id, 1.0) if similarities else 1.0
for opponent_id in data.get("sibling_model_ids") or []:
for opponent_id in data.get('sibling_model_ids') or []:
winner = get_or_create_stats(winner_id)
opponent = get_or_create_stats(opponent_id)
expected = 1 / (1 + 10 ** ((opponent["rating"] - winner["rating"]) / 400))
expected = 1 / (1 + 10 ** ((opponent['rating'] - winner['rating']) / 400))
winner["rating"] += K_FACTOR * ((1 if won else 0) - expected) * weight
opponent["rating"] += (
K_FACTOR * ((0 if won else 1) - (1 - expected)) * weight
)
winner['rating'] += K_FACTOR * ((1 if won else 0) - expected) * weight
opponent['rating'] += K_FACTOR * ((0 if won else 1) - (1 - expected)) * weight
if won:
winner["won"] += 1
opponent["lost"] += 1
winner['won'] += 1
opponent['lost'] += 1
else:
winner["lost"] += 1
opponent["won"] += 1
winner['lost'] += 1
opponent['won'] += 1
return model_stats
@@ -139,16 +133,13 @@ def _get_top_tags(feedbacks: list[LeaderboardFeedbackData], limit: int = 5) -> d
for feedback in feedbacks:
data = feedback.data or {}
model_id = data.get("model_id")
model_id = data.get('model_id')
if model_id:
for tag in data.get("tags", []):
for tag in data.get('tags', []):
tag_counts[model_id][tag] += 1
return {
model_id: [
{"tag": tag, "count": count}
for tag, count in sorted(tags.items(), key=lambda x: -x[1])[:limit]
]
model_id: [{'tag': tag, 'count': count} for tag, count in sorted(tags.items(), key=lambda x: -x[1])[:limit]]
for model_id, tags in tag_counts.items()
}
@@ -172,14 +163,7 @@ def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str)
if not embedding_model:
return {}
all_tags = list(
{
tag
for feedback in feedbacks
if feedback.data
for tag in feedback.data.get("tags", [])
}
)
all_tags = list({tag for feedback in feedbacks if feedback.data for tag in feedback.data.get('tags', [])})
if not all_tags:
return {}
@@ -187,23 +171,18 @@ def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str)
tag_embeddings = embedding_model.encode(all_tags)
query_embedding = embedding_model.encode([query])[0]
except Exception as e:
log.error(f"Embedding error: {e}")
log.error(f'Embedding error: {e}')
return {}
# Vectorized cosine similarity
tag_norms = np.linalg.norm(tag_embeddings, axis=1)
query_norm = np.linalg.norm(query_embedding)
similarities = np.dot(tag_embeddings, query_embedding) / (
tag_norms * query_norm + 1e-9
)
similarities = np.dot(tag_embeddings, query_embedding) / (tag_norms * query_norm + 1e-9)
tag_similarity_map = dict(zip(all_tags, similarities.tolist()))
return {
feedback.id: max(
(
tag_similarity_map.get(tag, 0)
for tag in (feedback.data or {}).get("tags", [])
),
(tag_similarity_map.get(tag, 0) for tag in (feedback.data or {}).get('tags', [])),
default=0,
)
for feedback in feedbacks
@@ -223,7 +202,7 @@ class LeaderboardResponse(BaseModel):
entries: list[LeaderboardEntry]
@router.get("/leaderboard", response_model=LeaderboardResponse)
@router.get('/leaderboard', response_model=LeaderboardResponse)
async def get_leaderboard(
query: Optional[str] = None,
user=Depends(get_admin_user),
@@ -234,9 +213,7 @@ async def get_leaderboard(
similarities = None
if query and query.strip():
similarities = await run_in_threadpool(
_compute_similarities, feedbacks, query.strip()
)
similarities = await run_in_threadpool(_compute_similarities, feedbacks, query.strip())
elo_stats = _calculate_elo(feedbacks, similarities)
tags_by_model = _get_top_tags(feedbacks)
@@ -245,10 +222,10 @@ async def get_leaderboard(
[
LeaderboardEntry(
model_id=mid,
rating=round(s["rating"]),
won=s["won"],
lost=s["lost"],
count=s["won"] + s["lost"],
rating=round(s['rating']),
won=s['won'],
lost=s['lost'],
count=s['won'] + s['lost'],
top_tags=tags_by_model.get(mid, []),
)
for mid, s in elo_stats.items()
@@ -260,7 +237,7 @@ async def get_leaderboard(
return LeaderboardResponse(entries=entries)
@router.get("/leaderboard/{model_id}/history", response_model=ModelHistoryResponse)
@router.get('/leaderboard/{model_id}/history', response_model=ModelHistoryResponse)
async def get_model_history(
model_id: str,
days: int = 30,
@@ -268,9 +245,7 @@ async def get_model_history(
db: Session = Depends(get_session),
):
"""Get daily win/loss history for a specific model."""
history = Feedbacks.get_model_evaluation_history(
model_id=model_id, days=days, db=db
)
history = Feedbacks.get_model_evaluation_history(model_id=model_id, days=days, db=db)
return ModelHistoryResponse(model_id=model_id, history=history)
@@ -279,11 +254,11 @@ async def get_model_history(
############################
@router.get("/config")
@router.get('/config')
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
"ENABLE_EVALUATION_ARENA_MODELS": request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS,
"EVALUATION_ARENA_MODELS": request.app.state.config.EVALUATION_ARENA_MODELS,
'ENABLE_EVALUATION_ARENA_MODELS': request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS,
'EVALUATION_ARENA_MODELS': request.app.state.config.EVALUATION_ARENA_MODELS,
}
@@ -297,7 +272,7 @@ class UpdateConfigForm(BaseModel):
EVALUATION_ARENA_MODELS: Optional[list[dict]] = None
@router.post("/config")
@router.post('/config')
async def update_config(
request: Request,
form_data: UpdateConfigForm,
@@ -309,54 +284,42 @@ async def update_config(
if form_data.EVALUATION_ARENA_MODELS is not None:
config.EVALUATION_ARENA_MODELS = form_data.EVALUATION_ARENA_MODELS
return {
"ENABLE_EVALUATION_ARENA_MODELS": config.ENABLE_EVALUATION_ARENA_MODELS,
"EVALUATION_ARENA_MODELS": config.EVALUATION_ARENA_MODELS,
'ENABLE_EVALUATION_ARENA_MODELS': config.ENABLE_EVALUATION_ARENA_MODELS,
'EVALUATION_ARENA_MODELS': config.EVALUATION_ARENA_MODELS,
}
@router.get("/feedbacks/all", response_model=list[FeedbackResponse])
async def get_all_feedbacks(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/feedbacks/all', response_model=list[FeedbackResponse])
async def get_all_feedbacks(user=Depends(get_admin_user), db: Session = Depends(get_session)):
feedbacks = Feedbacks.get_all_feedbacks(db=db)
return feedbacks
@router.get("/feedbacks/all/ids", response_model=list[FeedbackIdResponse])
async def get_all_feedback_ids(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/feedbacks/all/ids', response_model=list[FeedbackIdResponse])
async def get_all_feedback_ids(user=Depends(get_admin_user), db: Session = Depends(get_session)):
return Feedbacks.get_all_feedback_ids(db=db)
@router.delete("/feedbacks/all")
async def delete_all_feedbacks(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.delete('/feedbacks/all')
async def delete_all_feedbacks(user=Depends(get_admin_user), db: Session = Depends(get_session)):
success = Feedbacks.delete_all_feedbacks(db=db)
return success
@router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
async def export_all_feedbacks(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/feedbacks/all/export', response_model=list[FeedbackModel])
async def export_all_feedbacks(user=Depends(get_admin_user), db: Session = Depends(get_session)):
feedbacks = Feedbacks.get_all_feedbacks(db=db)
return feedbacks
@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
async def get_feedbacks(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/feedbacks/user', response_model=list[FeedbackUserResponse])
async def get_feedbacks(user=Depends(get_verified_user), db: Session = Depends(get_session)):
feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id, db=db)
return feedbacks
@router.delete("/feedbacks", response_model=bool)
async def delete_feedbacks(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.delete('/feedbacks', response_model=bool)
async def delete_feedbacks(user=Depends(get_verified_user), db: Session = Depends(get_session)):
success = Feedbacks.delete_feedbacks_by_user_id(user.id, db=db)
return success
@@ -364,7 +327,7 @@ async def delete_feedbacks(
PAGE_ITEM_COUNT = 30
@router.get("/feedbacks/list", response_model=FeedbackListResponse)
@router.get('/feedbacks/list', response_model=FeedbackListResponse)
async def get_feedbacks(
order_by: Optional[str] = None,
direction: Optional[str] = None,
@@ -379,24 +342,22 @@ async def get_feedbacks(
filter = {}
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit, db=db)
return result
@router.post("/feedback", response_model=FeedbackModel)
@router.post('/feedback', response_model=FeedbackModel)
async def create_feedback(
request: Request,
form_data: FeedbackForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
feedback = Feedbacks.insert_new_feedback(
user_id=user.id, form_data=form_data, db=db
)
feedback = Feedbacks.insert_new_feedback(user_id=user.id, form_data=form_data, db=db)
if not feedback:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -406,61 +367,45 @@ async def create_feedback(
return feedback
@router.get("/feedback/{id}", response_model=FeedbackModel)
async def get_feedback_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
if user.role == "admin":
@router.get('/feedback/{id}', response_model=FeedbackModel)
async def get_feedback_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == 'admin':
feedback = Feedbacks.get_feedback_by_id(id=id, db=db)
else:
feedback = Feedbacks.get_feedback_by_id_and_user_id(
id=id, user_id=user.id, db=db
)
feedback = Feedbacks.get_feedback_by_id_and_user_id(id=id, user_id=user.id, db=db)
if not feedback:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
return feedback
@router.post("/feedback/{id}", response_model=FeedbackModel)
@router.post('/feedback/{id}', response_model=FeedbackModel)
async def update_feedback_by_id(
id: str,
form_data: FeedbackForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role == "admin":
if user.role == 'admin':
feedback = Feedbacks.update_feedback_by_id(id=id, form_data=form_data, db=db)
else:
feedback = Feedbacks.update_feedback_by_id_and_user_id(
id=id, user_id=user.id, form_data=form_data, db=db
)
feedback = Feedbacks.update_feedback_by_id_and_user_id(id=id, user_id=user.id, form_data=form_data, db=db)
if not feedback:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
return feedback
@router.delete("/feedback/{id}")
async def delete_feedback_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
if user.role == "admin":
@router.delete('/feedback/{id}')
async def delete_feedback_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == 'admin':
success = Feedbacks.delete_feedback_by_id(id=id, db=db)
else:
success = Feedbacks.delete_feedback_by_id_and_user_id(
id=id, user_id=user.id, db=db
)
success = Feedbacks.delete_feedback_by_id_and_user_id(id=id, user_id=user.id, db=db)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
return success
+125 -217
View File
@@ -73,14 +73,14 @@ def _is_text_file(file_path: str, chunk_size: int = 8192) -> bool:
"""
try:
resolved = Storage.get_file(file_path)
with open(resolved, "rb") as f:
with open(resolved, 'rb') as f:
chunk = f.read(chunk_size)
if not chunk:
return False
# Null bytes are a strong indicator of binary content
if b"\x00" in chunk:
if b'\x00' in chunk:
return False
chunk.decode("utf-8")
chunk.decode('utf-8')
return True
except (UnicodeDecodeError, Exception):
return False
@@ -100,31 +100,25 @@ def process_uploaded_file(
content_type = file.content_type
# Detect mis-labeled text files (e.g. .ts → video/mp2t)
if content_type and content_type.startswith(("image/", "video/")):
if content_type and content_type.startswith(('image/', 'video/')):
if _is_text_file(file_path):
content_type = "text/plain"
content_type = 'text/plain'
if content_type:
stt_supported_content_types = getattr(
request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
)
stt_supported_content_types = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', [])
if strict_match_mime_type(stt_supported_content_types, content_type):
file_path_processed = Storage.get_file(file_path)
result = transcribe(
request, file_path_processed, file_metadata, user
)
result = transcribe(request, file_path_processed, file_metadata, user)
process_file(
request,
ProcessFileForm(
file_id=file_item.id, content=result.get("text", "")
),
ProcessFileForm(file_id=file_item.id, content=result.get('text', '')),
user=user,
db=db_session,
)
elif (not content_type.startswith(("image/", "video/"))) or (
request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external"
elif (not content_type.startswith(('image/', 'video/'))) or (
request.app.state.config.CONTENT_EXTRACTION_ENGINE == 'external'
):
process_file(
request,
@@ -133,13 +127,9 @@ def process_uploaded_file(
db=db_session,
)
else:
raise Exception(
f"File type {content_type} is not supported for processing"
)
raise Exception(f'File type {content_type} is not supported for processing')
else:
log.info(
f"File type {file.content_type} is not provided, but trying to process anyway"
)
log.info(f'File type {file.content_type} is not provided, but trying to process anyway')
process_file(
request,
ProcessFileForm(file_id=file_item.id),
@@ -148,12 +138,12 @@ def process_uploaded_file(
)
except Exception as e:
log.error(f"Error processing file: {file_item.id}")
log.error(f'Error processing file: {file_item.id}')
Files.update_file_data_by_id(
file_item.id,
{
"status": "failed",
"error": str(e.detail) if hasattr(e, "detail") else str(e),
'status': 'failed',
'error': str(e.detail) if hasattr(e, 'detail') else str(e),
},
db=db_session,
)
@@ -165,7 +155,7 @@ def process_uploaded_file(
_process_handler(db_session)
@router.post("/", response_model=FileModelResponse)
@router.post('/', response_model=FileModelResponse)
def upload_file(
request: Request,
background_tasks: BackgroundTasks,
@@ -198,7 +188,7 @@ def upload_file_handler(
background_tasks: Optional[BackgroundTasks] = None,
db: Optional[Session] = None,
):
log.info(f"file.content_type: {file.content_type} {process}")
log.info(f'file.content_type: {file.content_type} {process}')
if isinstance(metadata, str):
try:
@@ -206,7 +196,7 @@ def upload_file_handler(
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Invalid metadata format"),
detail=ERROR_MESSAGES.DEFAULT('Invalid metadata format'),
)
file_metadata = metadata if metadata else {}
@@ -216,7 +206,7 @@ def upload_file_handler(
file_extension = os.path.splitext(filename)[1]
# Remove the leading dot from the file extension
file_extension = file_extension[1:] if file_extension else ""
file_extension = file_extension[1:] if file_extension else ''
if process and request.app.state.config.ALLOWED_FILE_EXTENSIONS:
request.app.state.config.ALLOWED_FILE_EXTENSIONS = [
@@ -226,23 +216,21 @@ def upload_file_handler(
if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(
f"File type {file_extension} is not allowed"
),
detail=ERROR_MESSAGES.DEFAULT(f'File type {file_extension} is not allowed'),
)
# replace filename with uuid
id = str(uuid.uuid4())
name = filename
filename = f"{id}_{filename}"
filename = f'{id}_{filename}'
contents, file_path = Storage.upload_file(
file.file,
filename,
{
"OpenWebUI-User-Email": user.email,
"OpenWebUI-User-Id": user.id,
"OpenWebUI-User-Name": user.name,
"OpenWebUI-File-Id": id,
'OpenWebUI-User-Email': user.email,
'OpenWebUI-User-Id': user.id,
'OpenWebUI-User-Name': user.name,
'OpenWebUI-File-Id': id,
},
)
@@ -250,35 +238,27 @@ def upload_file_handler(
user.id,
FileForm(
**{
"id": id,
"filename": name,
"path": file_path,
"data": {
**({"status": "pending"} if process else {}),
'id': id,
'filename': name,
'path': file_path,
'data': {
**({'status': 'pending'} if process else {}),
},
"meta": {
"name": name,
"content_type": (
file.content_type
if isinstance(file.content_type, str)
else None
),
"size": len(contents),
"data": file_metadata,
'meta': {
'name': name,
'content_type': (file.content_type if isinstance(file.content_type, str) else None),
'size': len(contents),
'data': file_metadata,
},
}
),
db=db,
)
if "channel_id" in file_metadata:
channel = Channels.get_channel_by_id_and_user_id(
file_metadata["channel_id"], user.id, db=db
)
if 'channel_id' in file_metadata:
channel = Channels.get_channel_by_id_and_user_id(file_metadata['channel_id'], user.id, db=db)
if channel:
Channels.add_file_to_channel_by_id(
channel.id, file_item.id, user.id, db=db
)
Channels.add_file_to_channel_by_id(channel.id, file_item.id, user.id, db=db)
if process:
if background_tasks and process_in_background:
@@ -291,7 +271,7 @@ def upload_file_handler(
file_metadata,
user,
)
return {"status": True, **file_item.model_dump()}
return {'status': True, **file_item.model_dump()}
else:
process_uploaded_file(
request,
@@ -302,14 +282,14 @@ def upload_file_handler(
user,
db=db,
)
return {"status": True, **file_item.model_dump()}
return {'status': True, **file_item.model_dump()}
else:
if file_item:
return file_item
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
detail=ERROR_MESSAGES.DEFAULT('Error uploading file'),
)
except HTTPException as e:
@@ -318,7 +298,7 @@ def upload_file_handler(
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
detail=ERROR_MESSAGES.DEFAULT('Error uploading file'),
)
@@ -330,26 +310,22 @@ def upload_file_handler(
PAGE_SIZE = 50
@router.get("/", response_model=FileListResponse)
@router.get('/', response_model=FileListResponse)
async def list_files(
user=Depends(get_verified_user),
page: int = Query(1, ge=1, description="Page number (1-indexed)"),
page: int = Query(1, ge=1, description='Page number (1-indexed)'),
content: bool = Query(True),
db: Session = Depends(get_session),
):
skip = (page - 1) * PAGE_SIZE
user_id = (
None if (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) else user.id
)
user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id
result = Files.get_file_list(
user_id=user_id, skip=skip, limit=PAGE_SIZE, db=db
)
result = Files.get_file_list(user_id=user_id, skip=skip, limit=PAGE_SIZE, db=db)
if not content:
for file in result.items:
if file.data and "content" in file.data:
del file.data["content"]
if file.data and 'content' in file.data:
del file.data['content']
return result
@@ -359,17 +335,15 @@ async def list_files(
############################
@router.get("/search", response_model=list[FileModelResponse])
@router.get('/search', response_model=list[FileModelResponse])
async def search_files(
filename: str = Query(
...,
description="Filename pattern to search for. Supports wildcards such as '*.txt'",
),
content: bool = Query(True),
skip: int = Query(0, ge=0, description="Number of files to skip"),
limit: int = Query(
100, ge=1, le=1000, description="Maximum number of files to return"
),
skip: int = Query(0, ge=0, description='Number of files to skip'),
limit: int = Query(100, ge=1, le=1000, description='Maximum number of files to return'),
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
@@ -378,9 +352,7 @@ async def search_files(
Uses SQL-based filtering with pagination for better performance.
"""
# Determine user_id: null for admin with bypass (search all), user.id otherwise
user_id = (
None if (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) else user.id
)
user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id
# Use optimized database query with pagination
files = Files.search_files(
@@ -394,13 +366,13 @@ async def search_files(
if not files:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No files found matching the pattern.",
detail='No files found matching the pattern.',
)
if not content:
for file in files:
if file.data and "content" in file.data:
del file.data["content"]
if file.data and 'content' in file.data:
del file.data['content']
return files
@@ -410,10 +382,8 @@ async def search_files(
############################
@router.delete("/all")
async def delete_all_files(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.delete('/all')
async def delete_all_files(user=Depends(get_admin_user), db: Session = Depends(get_session)):
result = Files.delete_all_files(db=db)
if result:
try:
@@ -421,16 +391,16 @@ async def delete_all_files(
VECTOR_DB_CLIENT.reset()
except Exception as e:
log.exception(e)
log.error("Error deleting files")
log.error('Error deleting files')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
detail=ERROR_MESSAGES.DEFAULT('Error deleting files'),
)
return {"message": "All files deleted successfully"}
return {'message': 'All files deleted successfully'}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
detail=ERROR_MESSAGES.DEFAULT('Error deleting files'),
)
@@ -439,10 +409,8 @@ async def delete_all_files(
############################
@router.get("/{id}", response_model=Optional[FileModel])
async def get_file_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{id}', response_model=Optional[FileModel])
async def get_file_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
file = Files.get_file_by_id(id, db=db)
if not file:
@@ -451,11 +419,7 @@ async def get_file_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'read', user, db=db):
return file
else:
raise HTTPException(
@@ -464,7 +428,7 @@ async def get_file_by_id(
)
@router.get("/{id}/process/status")
@router.get('/{id}/process/status')
async def get_file_process_status(
id: str,
stream: bool = Query(False),
@@ -479,11 +443,7 @@ async def get_file_process_status(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'read', user, db=db):
if stream:
MAX_FILE_PROCESSING_DURATION = 3600 * 2
@@ -494,32 +454,32 @@ async def get_file_process_status(
for _ in range(MAX_FILE_PROCESSING_DURATION):
file_item = Files.get_file_by_id(file_id) # Creates own session
if file_item:
data = file_item.model_dump().get("data", {})
status = data.get("status")
data = file_item.model_dump().get('data', {})
status = data.get('status')
if status:
event = {"status": status}
if status == "failed":
event["error"] = data.get("error")
event = {'status': status}
if status == 'failed':
event['error'] = data.get('error')
yield f"data: {json.dumps(event)}\n\n"
if status in ("completed", "failed"):
yield f'data: {json.dumps(event)}\n\n'
if status in ('completed', 'failed'):
break
else:
# Legacy
break
else:
yield f"data: {json.dumps({'status': 'not_found'})}\n\n"
yield f'data: {json.dumps({"status": "not_found"})}\n\n'
break
await asyncio.sleep(1)
return StreamingResponse(
event_stream(file.id),
media_type="text/event-stream",
media_type='text/event-stream',
)
else:
return {"status": file.data.get("status", "pending")}
return {'status': file.data.get('status', 'pending')}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -532,10 +492,8 @@ async def get_file_process_status(
############################
@router.get("/{id}/data/content")
async def get_file_data_content_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{id}/data/content')
async def get_file_data_content_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
file = Files.get_file_by_id(id, db=db)
if not file:
@@ -544,12 +502,8 @@ async def get_file_data_content_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user, db=db)
):
return {"content": file.data.get("content", "")}
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'read', user, db=db):
return {'content': file.data.get('content', '')}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -566,7 +520,7 @@ class ContentForm(BaseModel):
content: str
@router.post("/{id}/data/content/update")
@router.post('/{id}/data/content/update')
def update_file_data_content_by_id(
request: Request,
id: str,
@@ -582,11 +536,7 @@ def update_file_data_content_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "write", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'write', user, db=db):
try:
process_file(
request,
@@ -597,7 +547,7 @@ def update_file_data_content_by_id(
file = Files.get_file_by_id(id=id, db=db)
except Exception as e:
log.exception(e)
log.error(f"Error processing file: {file.id}")
log.error(f'Error processing file: {file.id}')
# Propagate content change to all knowledge collections referencing
# this file. Without this the old embeddings remain in the knowledge
@@ -606,9 +556,7 @@ def update_file_data_content_by_id(
for knowledge in knowledges:
try:
# Remove old embeddings for this file from the KB collection
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": id}
)
VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': id})
# Re-add from the now-updated file-{file_id} collection
process_file(
request,
@@ -617,12 +565,9 @@ def update_file_data_content_by_id(
db=db,
)
except Exception as e:
log.warning(
f"Failed to update knowledge {knowledge.id} after "
f"content change for file {id}: {e}"
)
log.warning(f'Failed to update knowledge {knowledge.id} after content change for file {id}: {e}')
return {"content": file.data.get("content", "")}
return {'content': file.data.get('content', '')}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -635,7 +580,7 @@ def update_file_data_content_by_id(
############################
@router.get("/{id}/content")
@router.get('/{id}/content')
async def get_file_content_by_id(
id: str,
user=Depends(get_verified_user),
@@ -650,11 +595,7 @@ async def get_file_content_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'read', user, db=db):
try:
file_path = Storage.get_file(file.path)
file_path = Path(file_path)
@@ -662,30 +603,22 @@ async def get_file_content_by_id(
# Check if the file already exists in the cache
if file_path.is_file():
# Handle Unicode filenames
filename = file.meta.get("name", file.filename)
filename = file.meta.get('name', file.filename)
encoded_filename = quote(filename) # RFC5987 encoding
content_type = file.meta.get("content_type")
filename = file.meta.get("name", file.filename)
content_type = file.meta.get('content_type')
filename = file.meta.get('name', file.filename)
encoded_filename = quote(filename)
headers = {}
if attachment:
headers["Content-Disposition"] = (
f"attachment; filename*=UTF-8''{encoded_filename}"
)
headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{encoded_filename}"
else:
if content_type == "application/pdf" or filename.lower().endswith(
".pdf"
):
headers["Content-Disposition"] = (
f"inline; filename*=UTF-8''{encoded_filename}"
)
content_type = "application/pdf"
elif content_type != "text/plain":
headers["Content-Disposition"] = (
f"attachment; filename*=UTF-8''{encoded_filename}"
)
if content_type == 'application/pdf' or filename.lower().endswith('.pdf'):
headers['Content-Disposition'] = f"inline; filename*=UTF-8''{encoded_filename}"
content_type = 'application/pdf'
elif content_type != 'text/plain':
headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{encoded_filename}"
return FileResponse(file_path, headers=headers, media_type=content_type)
@@ -698,10 +631,10 @@ async def get_file_content_by_id(
raise e
except Exception as e:
log.exception(e)
log.error("Error getting file content")
log.error('Error getting file content')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error getting file content"),
detail=ERROR_MESSAGES.DEFAULT('Error getting file content'),
)
else:
raise HTTPException(
@@ -710,10 +643,8 @@ async def get_file_content_by_id(
)
@router.get("/{id}/content/html")
async def get_html_file_content_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{id}/content/html')
async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
file = Files.get_file_by_id(id, db=db)
if not file:
@@ -723,24 +654,20 @@ async def get_html_file_content_by_id(
)
file_user = Users.get_user_by_id(file.user_id, db=db)
if not file_user.role == "admin":
if not file_user.role == 'admin':
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'read', user, db=db):
try:
file_path = Storage.get_file(file.path)
file_path = Path(file_path)
# Check if the file already exists in the cache
if file_path.is_file():
log.info(f"file_path: {file_path}")
log.info(f'file_path: {file_path}')
return FileResponse(file_path)
else:
raise HTTPException(
@@ -751,10 +678,10 @@ async def get_html_file_content_by_id(
raise e
except Exception as e:
log.exception(e)
log.error("Error getting file content")
log.error('Error getting file content')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error getting file content"),
detail=ERROR_MESSAGES.DEFAULT('Error getting file content'),
)
else:
raise HTTPException(
@@ -763,10 +690,8 @@ async def get_html_file_content_by_id(
)
@router.get("/{id}/content/{file_name}")
async def get_file_content_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{id}/content/{file_name}')
async def get_file_content_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
file = Files.get_file_by_id(id, db=db)
if not file:
@@ -775,19 +700,13 @@ async def get_file_content_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "read", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'read', user, db=db):
file_path = file.path
# Handle Unicode filenames
filename = file.meta.get("name", file.filename)
filename = file.meta.get('name', file.filename)
encoded_filename = quote(filename) # RFC5987 encoding
headers = {
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
}
headers = {'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}"}
if file_path:
file_path = Storage.get_file(file_path)
@@ -803,16 +722,16 @@ async def get_file_content_by_id(
)
else:
# File path doesn’t exist, return the content as .txt if possible
file_content = file.content.get("content", "")
file_content = file.content.get('content', '')
file_name = file.filename
# Create a generator that encodes the file content
def generator():
yield file_content.encode("utf-8")
yield file_content.encode('utf-8')
return StreamingResponse(
generator(),
media_type="text/plain",
media_type='text/plain',
headers=headers,
)
else:
@@ -827,10 +746,8 @@ async def get_file_content_by_id(
############################
@router.delete("/{id}")
async def delete_file_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.delete('/{id}')
async def delete_file_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
file = Files.get_file_by_id(id, db=db)
if not file:
@@ -839,12 +756,7 @@ async def delete_file_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"
or has_access_to_file(id, "write", user, db=db)
):
if file.user_id == user.id or user.role == 'admin' or has_access_to_file(id, 'write', user, db=db):
# Clean up KB associations and embeddings before deleting
knowledges = Knowledges.get_knowledges_by_file_id(id, db=db)
for knowledge in knowledges:
@@ -852,33 +764,29 @@ async def delete_file_by_id(
Knowledges.remove_file_from_knowledge_by_id(knowledge.id, id, db=db)
# Clean KB embeddings (same logic as /knowledge/{id}/file/remove)
try:
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": id}
)
VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': id})
if file.hash:
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"hash": file.hash}
)
VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'hash': file.hash})
except Exception as e:
log.debug(f"KB embedding cleanup for {knowledge.id}: {e}")
log.debug(f'KB embedding cleanup for {knowledge.id}: {e}')
result = Files.delete_file_by_id(id, db=db)
if result:
try:
Storage.delete_file(file.path)
VECTOR_DB_CLIENT.delete(collection_name=f"file-{id}")
VECTOR_DB_CLIENT.delete(collection_name=f'file-{id}')
except Exception as e:
log.exception(e)
log.error("Error deleting files")
log.error('Error deleting files')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
detail=ERROR_MESSAGES.DEFAULT('Error deleting files'),
)
return {"message": "File deleted successfully"}
return {'message': 'File deleted successfully'}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting file"),
detail=ERROR_MESSAGES.DEFAULT('Error deleting file'),
)
else:
raise HTTPException(
+44 -76
View File
@@ -44,7 +44,7 @@ router = APIRouter()
############################
@router.get("/", response_model=list[FolderNameIdResponse])
@router.get('/', response_model=list[FolderNameIdResponse])
async def get_folders(
request: Request,
user=Depends(get_verified_user),
@@ -56,9 +56,9 @@ async def get_folders(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if user.role != "admin" and not has_permission(
if user.role != 'admin' and not has_permission(
user.id,
"features.folders",
'features.folders',
request.app.state.config.USER_PERMISSIONS,
db=db,
):
@@ -72,35 +72,24 @@ async def get_folders(
# Verify folder data integrity
folder_list = []
for folder in folders:
if folder.parent_id and not Folders.get_folder_by_id_and_user_id(
folder.parent_id, user.id, db=db
):
folder = Folders.update_folder_parent_id_by_id_and_user_id(
folder.id, user.id, None, db=db
)
if folder.parent_id and not Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db):
folder = Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db)
if folder.data:
if "files" in folder.data:
if 'files' in folder.data:
valid_files = []
for file in folder.data["files"]:
if file.get("type") == "file":
if Files.check_access_by_user_id(
file.get("id"), user.id, "read", db=db
):
for file in folder.data['files']:
if file.get('type') == 'file':
if Files.check_access_by_user_id(file.get('id'), user.id, 'read', db=db):
valid_files.append(file)
elif file.get("type") == "collection":
if Knowledges.check_access_by_user_id(
file.get("id"), user.id, "read", db=db
):
elif file.get('type') == 'collection':
if Knowledges.check_access_by_user_id(file.get('id'), user.id, 'read', db=db):
valid_files.append(file)
else:
valid_files.append(file)
folder.data["files"] = valid_files
Folders.update_folder_by_id_and_user_id(
folder.id, user.id, FolderUpdateForm(data=folder.data), db=db
)
folder.data['files'] = valid_files
Folders.update_folder_by_id_and_user_id(folder.id, user.id, FolderUpdateForm(data=folder.data), db=db)
folder_list.append(FolderNameIdResponse(**folder.model_dump()))
@@ -112,33 +101,29 @@ async def get_folders(
############################
@router.post("/")
@router.post('/')
def create_folder(
form_data: FolderForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
form_data.parent_id, user.id, form_data.name, db=db
)
folder = Folders.get_folder_by_parent_id_and_user_id_and_name(form_data.parent_id, user.id, form_data.name, db=db)
if folder:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
)
try:
folder = Folders.insert_new_folder(
user.id, form_data, form_data.parent_id, db=db
)
folder = Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db)
return folder
except Exception as e:
log.exception(e)
log.error("Error creating folder")
log.error('Error creating folder')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating folder"),
detail=ERROR_MESSAGES.DEFAULT('Error creating folder'),
)
@@ -147,10 +132,8 @@ def create_folder(
############################
@router.get("/{id}", response_model=Optional[FolderModel])
async def get_folder_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{id}', response_model=Optional[FolderModel])
async def get_folder_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
if folder:
return folder
@@ -166,7 +149,7 @@ async def get_folder_by_id(
############################
@router.post("/{id}/update")
@router.post('/{id}/update')
async def update_folder_name_by_id(
id: str,
form_data: FolderUpdateForm,
@@ -175,7 +158,6 @@ async def update_folder_name_by_id(
):
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
if folder:
if form_data.name is not None:
# Check if folder with same name exists
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
@@ -184,20 +166,18 @@ async def update_folder_name_by_id(
if existing_folder and existing_folder.id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
)
try:
folder = Folders.update_folder_by_id_and_user_id(
id, user.id, form_data, db=db
)
folder = Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db)
return folder
except Exception as e:
log.exception(e)
log.error(f"Error updating folder: {id}")
log.error(f'Error updating folder: {id}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
)
else:
raise HTTPException(
@@ -215,7 +195,7 @@ class FolderParentIdForm(BaseModel):
parent_id: Optional[str] = None
@router.post("/{id}/update/parent")
@router.post('/{id}/update/parent')
async def update_folder_parent_id_by_id(
id: str,
form_data: FolderParentIdForm,
@@ -231,20 +211,18 @@ async def update_folder_parent_id_by_id(
if existing_folder:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Folder already exists"),
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
)
try:
folder = Folders.update_folder_parent_id_by_id_and_user_id(
id, user.id, form_data.parent_id, db=db
)
folder = Folders.update_folder_parent_id_by_id_and_user_id(id, user.id, form_data.parent_id, db=db)
return folder
except Exception as e:
log.exception(e)
log.error(f"Error updating folder: {id}")
log.error(f'Error updating folder: {id}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
)
else:
raise HTTPException(
@@ -262,7 +240,7 @@ class FolderIsExpandedForm(BaseModel):
is_expanded: bool
@router.post("/{id}/update/expanded")
@router.post('/{id}/update/expanded')
async def update_folder_is_expanded_by_id(
id: str,
form_data: FolderIsExpandedForm,
@@ -272,16 +250,14 @@ async def update_folder_is_expanded_by_id(
folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
if folder:
try:
folder = Folders.update_folder_is_expanded_by_id_and_user_id(
id, user.id, form_data.is_expanded, db=db
)
folder = Folders.update_folder_is_expanded_by_id_and_user_id(id, user.id, form_data.is_expanded, db=db)
return folder
except Exception as e:
log.exception(e)
log.error(f"Error updating folder: {id}")
log.error(f'Error updating folder: {id}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating folder"),
detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
)
else:
raise HTTPException(
@@ -295,7 +271,7 @@ async def update_folder_is_expanded_by_id(
############################
@router.delete("/{id}")
@router.delete('/{id}')
async def delete_folder_by_id(
request: Request,
id: str,
@@ -305,9 +281,9 @@ async def delete_folder_by_id(
):
if Chats.count_chats_by_folder_id_and_user_id(id, user.id, db=db):
chat_delete_permission = has_permission(
user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db
)
if user.role != "admin" and not chat_delete_permission:
if user.role != 'admin' and not chat_delete_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -319,33 +295,25 @@ async def delete_folder_by_id(
folder = folders.pop()
if folder:
try:
folder_ids = Folders.delete_folder_by_id_and_user_id(
folder.id, user.id, db=db
)
folder_ids = Folders.delete_folder_by_id_and_user_id(folder.id, user.id, db=db)
for folder_id in folder_ids:
if delete_contents:
Chats.delete_chats_by_user_id_and_folder_id(
user.id, folder_id, db=db
)
Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id, db=db)
else:
Chats.move_chats_by_user_id_and_folder_id(
user.id, folder_id, None, db=db
)
Chats.move_chats_by_user_id_and_folder_id(user.id, folder_id, None, db=db)
return True
except Exception as e:
log.exception(e)
log.error(f"Error deleting folder: {id}")
log.error(f'Error deleting folder: {id}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"),
detail=ERROR_MESSAGES.DEFAULT('Error deleting folder'),
)
finally:
# Get all subfolders
subfolders = Folders.get_folders_by_parent_id_and_user_id(
folder.id, user.id, db=db
)
subfolders = Folders.get_folders_by_parent_id_and_user_id(folder.id, user.id, db=db)
folders.extend(subfolders)
else:
+73 -124
View File
@@ -39,17 +39,13 @@ router = APIRouter()
############################
@router.get("/", response_model=list[FunctionResponse])
async def get_functions(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/', response_model=list[FunctionResponse])
async def get_functions(user=Depends(get_verified_user), db: Session = Depends(get_session)):
return Functions.get_functions(db=db)
@router.get("/list", response_model=list[FunctionUserResponse])
async def get_function_list(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/list', response_model=list[FunctionUserResponse])
async def get_function_list(user=Depends(get_admin_user), db: Session = Depends(get_session)):
return Functions.get_function_list(db=db)
@@ -58,7 +54,7 @@ async def get_function_list(
############################
@router.get("/export", response_model=list[FunctionModel | FunctionWithValvesModel])
@router.get('/export', response_model=list[FunctionModel | FunctionWithValvesModel])
async def get_functions(
include_valves: bool = False,
user=Depends(get_admin_user),
@@ -78,70 +74,59 @@ class LoadUrlForm(BaseModel):
def github_url_to_raw_url(url: str) -> str:
# Handle 'tree' (folder) URLs (add main.py at the end)
m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url)
m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url)
if m1:
org, repo, branch, path = m1.groups()
return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py"
return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py'
# Handle 'blob' (file) URLs
m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url)
m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url)
if m2:
org, repo, branch, path = m2.groups()
return (
f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}"
)
return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}'
# No match; return as-is
return url
@router.post("/load/url", response_model=Optional[dict])
async def load_function_from_url(
request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)
):
@router.post('/load/url', response_model=Optional[dict])
async def load_function_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)):
# NOTE: This is NOT a SSRF vulnerability:
# This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use,
# and does NOT accept untrusted user input. Access is enforced by authentication.
url = str(form_data.url)
if not url:
raise HTTPException(status_code=400, detail="Please enter a valid URL")
raise HTTPException(status_code=400, detail='Please enter a valid URL')
url = github_url_to_raw_url(url)
url_parts = url.rstrip("/").split("/")
url_parts = url.rstrip('/').split('/')
file_name = url_parts[-1]
function_name = (
file_name[:-3]
if (
file_name.endswith(".py")
and (not file_name.startswith(("main.py", "index.py", "__init__.py")))
)
else url_parts[-2] if len(url_parts) > 1 else "function"
if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py'))))
else url_parts[-2]
if len(url_parts) > 1
else 'function'
)
try:
async with aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
) as session:
async with session.get(
url, headers={"Content-Type": "application/json"}
) as resp:
async with session.get(url, headers={'Content-Type': 'application/json'}) as resp:
if resp.status != 200:
raise HTTPException(
status_code=resp.status, detail="Failed to fetch the function"
)
raise HTTPException(status_code=resp.status, detail='Failed to fetch the function')
data = await resp.text()
if not data:
raise HTTPException(
status_code=400, detail="No data received from the URL"
)
raise HTTPException(status_code=400, detail='No data received from the URL')
return {
"name": function_name,
"content": data,
'name': function_name,
'content': data,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error importing function: {e}")
raise HTTPException(status_code=500, detail=f'Error importing function: {e}')
############################
@@ -153,7 +138,7 @@ class SyncFunctionsForm(BaseModel):
functions: list[FunctionWithValvesModel] = []
@router.post("/sync", response_model=list[FunctionWithValvesModel])
@router.post('/sync', response_model=list[FunctionWithValvesModel])
async def sync_functions(
request: Request,
form_data: SyncFunctionsForm,
@@ -168,21 +153,17 @@ async def sync_functions(
content=function.content,
)
if hasattr(function_module, "Valves") and function.valves:
if hasattr(function_module, 'Valves') and function.valves:
Valves = function_module.Valves
try:
Valves(
**{k: v for k, v in function.valves.items() if v is not None}
)
Valves(**{k: v for k, v in function.valves.items() if v is not None})
except Exception as e:
log.exception(
f"Error validating valves for function {function.id}: {e}"
)
log.exception(f'Error validating valves for function {function.id}: {e}')
raise e
return Functions.sync_functions(user.id, form_data.functions, db=db)
except Exception as e:
log.exception(f"Failed to load a function: {e}")
log.exception(f'Failed to load a function: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -194,7 +175,7 @@ async def sync_functions(
############################
@router.post("/create", response_model=Optional[FunctionResponse])
@router.post('/create', response_model=Optional[FunctionResponse])
async def create_new_function(
request: Request,
form_data: FunctionForm,
@@ -204,7 +185,7 @@ async def create_new_function(
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only alphanumeric characters and underscores are allowed in the id",
detail='Only alphanumeric characters and underscores are allowed in the id',
)
form_data.id = form_data.id.lower()
@@ -222,27 +203,23 @@ async def create_new_function(
FUNCTIONS = request.app.state.FUNCTIONS
FUNCTIONS[form_data.id] = function_module
function = Functions.insert_new_function(
user.id, function_type, form_data, db=db
)
function = Functions.insert_new_function(user.id, function_type, form_data, db=db)
function_cache_dir = CACHE_DIR / "functions" / form_data.id
function_cache_dir = CACHE_DIR / 'functions' / form_data.id
function_cache_dir.mkdir(parents=True, exist_ok=True)
if function_type == "filter" and getattr(function_module, "toggle", None):
Functions.update_function_metadata_by_id(
form_data.id, {"toggle": True}, db=db
)
if function_type == 'filter' and getattr(function_module, 'toggle', None):
Functions.update_function_metadata_by_id(form_data.id, {'toggle': True}, db=db)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
detail=ERROR_MESSAGES.DEFAULT('Error creating function'),
)
except Exception as e:
log.exception(f"Failed to create a new function: {e}")
log.exception(f'Failed to create a new function: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -259,10 +236,8 @@ async def create_new_function(
############################
@router.get("/id/{id}", response_model=Optional[FunctionModel])
async def get_function_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}', response_model=Optional[FunctionModel])
async def get_function_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
function = Functions.get_function_by_id(id, db=db)
if function:
@@ -279,22 +254,18 @@ async def get_function_by_id(
############################
@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel])
async def toggle_function_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.post('/id/{id}/toggle', response_model=Optional[FunctionModel])
async def toggle_function_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
function = Functions.get_function_by_id(id, db=db)
if function:
function = Functions.update_function_by_id(
id, {"is_active": not function.is_active}, db=db
)
function = Functions.update_function_by_id(id, {'is_active': not function.is_active}, db=db)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
detail=ERROR_MESSAGES.DEFAULT('Error updating function'),
)
else:
raise HTTPException(
@@ -308,22 +279,18 @@ async def toggle_function_by_id(
############################
@router.post("/id/{id}/toggle/global", response_model=Optional[FunctionModel])
async def toggle_global_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.post('/id/{id}/toggle/global', response_model=Optional[FunctionModel])
async def toggle_global_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
function = Functions.get_function_by_id(id, db=db)
if function:
function = Functions.update_function_by_id(
id, {"is_global": not function.is_global}, db=db
)
function = Functions.update_function_by_id(id, {'is_global': not function.is_global}, db=db)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
detail=ERROR_MESSAGES.DEFAULT('Error updating function'),
)
else:
raise HTTPException(
@@ -337,7 +304,7 @@ async def toggle_global_by_id(
############################
@router.post("/id/{id}/update", response_model=Optional[FunctionModel])
@router.post('/id/{id}/update', response_model=Optional[FunctionModel])
async def update_function_by_id(
request: Request,
id: str,
@@ -347,28 +314,26 @@ async def update_function_by_id(
):
try:
form_data.content = replace_imports(form_data.content)
function_module, function_type, frontmatter = load_function_module_by_id(
id, content=form_data.content
)
function_module, function_type, frontmatter = load_function_module_by_id(id, content=form_data.content)
form_data.meta.manifest = frontmatter
FUNCTIONS = request.app.state.FUNCTIONS
FUNCTIONS[id] = function_module
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
updated = {**form_data.model_dump(exclude={'id'}), 'type': function_type}
log.debug(updated)
function = Functions.update_function_by_id(id, updated, db=db)
if function_type == "filter" and getattr(function_module, "toggle", None):
Functions.update_function_metadata_by_id(id, {"toggle": True}, db=db)
if function_type == 'filter' and getattr(function_module, 'toggle', None):
Functions.update_function_metadata_by_id(id, {'toggle': True}, db=db)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
detail=ERROR_MESSAGES.DEFAULT('Error updating function'),
)
except Exception as e:
@@ -383,7 +348,7 @@ async def update_function_by_id(
############################
@router.delete("/id/{id}/delete", response_model=bool)
@router.delete('/id/{id}/delete', response_model=bool)
async def delete_function_by_id(
request: Request,
id: str,
@@ -405,10 +370,8 @@ async def delete_function_by_id(
############################
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_function_valves_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}/valves', response_model=Optional[dict])
async def get_function_valves_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
function = Functions.get_function_by_id(id, db=db)
if function:
try:
@@ -431,7 +394,7 @@ async def get_function_valves_by_id(
############################
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
@router.get('/id/{id}/valves/spec', response_model=Optional[dict])
async def get_function_valves_spec_by_id(
request: Request,
id: str,
@@ -440,11 +403,9 @@ async def get_function_valves_spec_by_id(
):
function = Functions.get_function_by_id(id, db=db)
if function:
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
function_module, function_type, frontmatter = get_function_module_from_cache(request, id)
if hasattr(function_module, "Valves"):
if hasattr(function_module, 'Valves'):
Valves = function_module.Valves
schema = Valves.schema()
# Resolve dynamic options for select dropdowns
@@ -463,7 +424,7 @@ async def get_function_valves_spec_by_id(
############################
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
@router.post('/id/{id}/valves/update', response_model=Optional[dict])
async def update_function_valves_by_id(
request: Request,
id: str,
@@ -473,11 +434,9 @@ async def update_function_valves_by_id(
):
function = Functions.get_function_by_id(id, db=db)
if function:
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
function_module, function_type, frontmatter = get_function_module_from_cache(request, id)
if hasattr(function_module, "Valves"):
if hasattr(function_module, 'Valves'):
Valves = function_module.Valves
try:
@@ -488,7 +447,7 @@ async def update_function_valves_by_id(
Functions.update_function_valves_by_id(id, valves_dict, db=db)
return valves_dict
except Exception as e:
log.exception(f"Error updating function values by id {id}: {e}")
log.exception(f'Error updating function values by id {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -511,16 +470,12 @@ async def update_function_valves_by_id(
############################
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_function_user_valves_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}/valves/user', response_model=Optional[dict])
async def get_function_user_valves_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
function = Functions.get_function_by_id(id, db=db)
if function:
try:
user_valves = Functions.get_user_valves_by_id_and_user_id(
id, user.id, db=db
)
user_valves = Functions.get_user_valves_by_id_and_user_id(id, user.id, db=db)
return user_valves
except Exception as e:
raise HTTPException(
@@ -534,7 +489,7 @@ async def get_function_user_valves_by_id(
)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
@router.get('/id/{id}/valves/user/spec', response_model=Optional[dict])
async def get_function_user_valves_spec_by_id(
request: Request,
id: str,
@@ -543,11 +498,9 @@ async def get_function_user_valves_spec_by_id(
):
function = Functions.get_function_by_id(id, db=db)
if function:
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
function_module, function_type, frontmatter = get_function_module_from_cache(request, id)
if hasattr(function_module, "UserValves"):
if hasattr(function_module, 'UserValves'):
UserValves = function_module.UserValves
schema = UserValves.schema()
# Resolve dynamic options for select dropdowns
@@ -561,7 +514,7 @@ async def get_function_user_valves_spec_by_id(
)
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
@router.post('/id/{id}/valves/user/update', response_model=Optional[dict])
async def update_function_user_valves_by_id(
request: Request,
id: str,
@@ -572,23 +525,19 @@ async def update_function_user_valves_by_id(
function = Functions.get_function_by_id(id, db=db)
if function:
function_module, function_type, frontmatter = get_function_module_from_cache(
request, id
)
function_module, function_type, frontmatter = get_function_module_from_cache(request, id)
if hasattr(function_module, "UserValves"):
if hasattr(function_module, 'UserValves'):
UserValves = function_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
user_valves = UserValves(**form_data)
user_valves_dict = user_valves.model_dump(exclude_unset=True)
Functions.update_user_valves_by_id_and_user_id(
id, user.id, user_valves_dict, db=db
)
Functions.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db)
return user_valves_dict
except Exception as e:
log.exception(f"Error updating function user valves by id {id}: {e}")
log.exception(f'Error updating function user valves by id {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
+29 -40
View File
@@ -31,20 +31,19 @@ router = APIRouter()
############################
@router.get("/", response_model=list[GroupResponse])
@router.get('/', response_model=list[GroupResponse])
async def get_groups(
share: Optional[bool] = None,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
filter = {}
# Admins can share to all groups regardless of share setting
if user.role != "admin":
filter["member_id"] = user.id
if user.role != 'admin':
filter['member_id'] = user.id
if share is not None:
filter["share"] = share
filter['share'] = share
groups = Groups.get_groups(filter=filter, db=db)
@@ -56,7 +55,7 @@ async def get_groups(
############################
@router.post("/create", response_model=Optional[GroupResponse])
@router.post('/create', response_model=Optional[GroupResponse])
async def create_new_group(
form_data: GroupForm,
user=Depends(get_admin_user),
@@ -72,10 +71,10 @@ async def create_new_group(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
detail=ERROR_MESSAGES.DEFAULT('Error creating group'),
)
except Exception as e:
log.exception(f"Error creating a new group: {e}")
log.exception(f'Error creating a new group: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -87,10 +86,8 @@ async def create_new_group(
############################
@router.get("/id/{id}", response_model=Optional[GroupResponse])
async def get_group_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}', response_model=Optional[GroupResponse])
async def get_group_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
group = Groups.get_group_by_id(id, db=db)
if group:
return GroupResponse(
@@ -104,10 +101,8 @@ async def get_group_by_id(
)
@router.get("/id/{id}/info", response_model=Optional[GroupInfoResponse])
async def get_group_info_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}/info', response_model=Optional[GroupInfoResponse])
async def get_group_info_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
group = Groups.get_group_by_id(id, db=db)
if group:
return GroupInfoResponse(
@@ -131,10 +126,8 @@ class GroupExportResponse(GroupResponse):
pass
@router.get("/id/{id}/export", response_model=Optional[GroupExportResponse])
async def export_group_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}/export', response_model=Optional[GroupExportResponse])
async def export_group_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
group = Groups.get_group_by_id(id, db=db)
if group:
return GroupExportResponse(
@@ -154,15 +147,13 @@ async def export_group_by_id(
############################
@router.post("/id/{id}/users", response_model=list[UserInfoResponse])
async def get_users_in_group(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.post('/id/{id}/users', response_model=list[UserInfoResponse])
async def get_users_in_group(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
try:
users = Users.get_users_by_group_id(id, db=db)
return users
except Exception as e:
log.exception(f"Error adding users to group {id}: {e}")
log.exception(f'Error adding users to group {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -174,7 +165,7 @@ async def get_users_in_group(
############################
@router.post("/id/{id}/update", response_model=Optional[GroupResponse])
@router.post('/id/{id}/update', response_model=Optional[GroupResponse])
async def update_group_by_id(
id: str,
form_data: GroupUpdateForm,
@@ -191,10 +182,10 @@ async def update_group_by_id(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
detail=ERROR_MESSAGES.DEFAULT('Error updating group'),
)
except Exception as e:
log.exception(f"Error updating group {id}: {e}")
log.exception(f'Error updating group {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -206,7 +197,7 @@ async def update_group_by_id(
############################
@router.post("/id/{id}/users/add", response_model=Optional[GroupResponse])
@router.post('/id/{id}/users/add', response_model=Optional[GroupResponse])
async def add_user_to_group(
id: str,
form_data: UserIdsForm,
@@ -226,17 +217,17 @@ async def add_user_to_group(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error adding users to group"),
detail=ERROR_MESSAGES.DEFAULT('Error adding users to group'),
)
except Exception as e:
log.exception(f"Error adding users to group {id}: {e}")
log.exception(f'Error adding users to group {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@router.post("/id/{id}/users/remove", response_model=Optional[GroupResponse])
@router.post('/id/{id}/users/remove', response_model=Optional[GroupResponse])
async def remove_users_from_group(
id: str,
form_data: UserIdsForm,
@@ -253,10 +244,10 @@ async def remove_users_from_group(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error removing users from group"),
detail=ERROR_MESSAGES.DEFAULT('Error removing users from group'),
)
except Exception as e:
log.exception(f"Error removing users from group {id}: {e}")
log.exception(f'Error removing users from group {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -268,10 +259,8 @@ async def remove_users_from_group(
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_group_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.delete('/id/{id}/delete', response_model=bool)
async def delete_group_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
try:
result = Groups.delete_group_by_id(id, db=db)
if result:
@@ -279,10 +268,10 @@ async def delete_group_by_id(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
detail=ERROR_MESSAGES.DEFAULT('Error deleting group'),
)
except Exception as e:
log.exception(f"Error deleting group {id}: {e}")
log.exception(f'Error deleting group {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
File diff suppressed because it is too large Load Diff
+137 -174
View File
@@ -50,7 +50,7 @@ PAGE_ITEM_COUNT = 30
# Knowledge Base Embedding
############################
KNOWLEDGE_BASES_COLLECTION = "knowledge-bases"
KNOWLEDGE_BASES_COLLECTION = 'knowledge-bases'
async def embed_knowledge_base_metadata(
@@ -61,24 +61,24 @@ async def embed_knowledge_base_metadata(
) -> bool:
"""Generate and store embedding for knowledge base."""
try:
content = f"{name}\n\n{description}" if description else name
content = f'{name}\n\n{description}' if description else name
embedding = await request.app.state.EMBEDDING_FUNCTION(content)
VECTOR_DB_CLIENT.upsert(
collection_name=KNOWLEDGE_BASES_COLLECTION,
items=[
{
"id": knowledge_base_id,
"text": content,
"vector": embedding,
"metadata": {
"knowledge_base_id": knowledge_base_id,
'id': knowledge_base_id,
'text': content,
'vector': embedding,
'metadata': {
'knowledge_base_id': knowledge_base_id,
},
}
],
)
return True
except Exception as e:
log.error(f"Failed to embed knowledge base {knowledge_base_id}: {e}")
log.error(f'Failed to embed knowledge base {knowledge_base_id}: {e}')
return False
@@ -91,7 +91,7 @@ def remove_knowledge_base_metadata_embedding(knowledge_base_id: str) -> bool:
)
return True
except Exception as e:
log.debug(f"Failed to remove embedding for {knowledge_base_id}: {e}")
log.debug(f'Failed to remove embedding for {knowledge_base_id}: {e}')
return False
@@ -104,7 +104,7 @@ class KnowledgeAccessListResponse(BaseModel):
total: int
@router.get("/", response_model=KnowledgeAccessListResponse)
@router.get('/', response_model=KnowledgeAccessListResponse)
async def get_knowledge_bases(
page: Optional[int] = 1,
user=Depends(get_verified_user),
@@ -118,23 +118,21 @@ async def get_knowledge_bases(
groups = Groups.get_groups_by_member_id(user.id, db=db)
user_group_ids = {group.id for group in groups}
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL:
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
result = Knowledges.search_knowledge_bases(
user.id, filter=filter, skip=skip, limit=limit, db=db
)
result = Knowledges.search_knowledge_bases(user.id, filter=filter, skip=skip, limit=limit, db=db)
# Batch-fetch writable knowledge IDs in a single query instead of N has_access calls
knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items]
writable_knowledge_base_ids = AccessGrants.get_accessible_resource_ids(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_ids=knowledge_base_ids,
permission="write",
permission='write',
user_group_ids=user_group_ids,
db=db,
)
@@ -145,7 +143,7 @@ async def get_knowledge_bases(
**knowledge_base.model_dump(),
write_access=(
user.id == knowledge_base.user_id
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or knowledge_base.id in writable_knowledge_base_ids
),
)
@@ -155,7 +153,7 @@ async def get_knowledge_bases(
)
@router.get("/search", response_model=KnowledgeAccessListResponse)
@router.get('/search', response_model=KnowledgeAccessListResponse)
async def search_knowledge_bases(
query: Optional[str] = None,
view_option: Optional[str] = None,
@@ -169,30 +167,28 @@ async def search_knowledge_bases(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if view_option:
filter["view_option"] = view_option
filter['view_option'] = view_option
groups = Groups.get_groups_by_member_id(user.id, db=db)
user_group_ids = {group.id for group in groups}
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL:
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
result = Knowledges.search_knowledge_bases(
user.id, filter=filter, skip=skip, limit=limit, db=db
)
result = Knowledges.search_knowledge_bases(user.id, filter=filter, skip=skip, limit=limit, db=db)
# Batch-fetch writable knowledge IDs in a single query instead of N has_access calls
knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items]
writable_knowledge_base_ids = AccessGrants.get_accessible_resource_ids(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_ids=knowledge_base_ids,
permission="write",
permission='write',
user_group_ids=user_group_ids,
db=db,
)
@@ -203,7 +199,7 @@ async def search_knowledge_bases(
**knowledge_base.model_dump(),
write_access=(
user.id == knowledge_base.user_id
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or knowledge_base.id in writable_knowledge_base_ids
),
)
@@ -213,7 +209,7 @@ async def search_knowledge_bases(
)
@router.get("/search/files", response_model=KnowledgeFileListResponse)
@router.get('/search/files', response_model=KnowledgeFileListResponse)
async def search_knowledge_files(
query: Optional[str] = None,
page: Optional[int] = 1,
@@ -226,17 +222,15 @@ async def search_knowledge_files(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
groups = Groups.get_groups_by_member_id(user.id, db=db)
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
return Knowledges.search_knowledge_files(
filter=filter, skip=skip, limit=limit, db=db
)
return Knowledges.search_knowledge_files(filter=filter, skip=skip, limit=limit, db=db)
############################
@@ -244,7 +238,7 @@ async def search_knowledge_files(
############################
@router.post("/create", response_model=Optional[KnowledgeResponse])
@router.post('/create', response_model=Optional[KnowledgeResponse])
async def create_new_knowledge(
request: Request,
form_data: KnowledgeForm,
@@ -254,8 +248,8 @@ async def create_new_knowledge(
# Database operations (has_permission, filter_allowed_access_grants, insert_new_knowledge) manage their own sessions.
# This prevents holding a connection during embed_knowledge_base_metadata()
# which makes external embedding API calls (1-5+ seconds).
if user.role != "admin" and not has_permission(
user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS
if user.role != 'admin' and not has_permission(
user.id, 'workspace.knowledge', request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -267,7 +261,7 @@ async def create_new_knowledge(
user.id,
user.role,
form_data.access_grants,
"sharing.public_knowledge",
'sharing.public_knowledge',
)
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
@@ -293,13 +287,13 @@ async def create_new_knowledge(
############################
@router.post("/reindex", response_model=bool)
@router.post('/reindex', response_model=bool)
async def reindex_knowledge_files(
request: Request,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin":
if user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
@@ -307,18 +301,16 @@ async def reindex_knowledge_files(
knowledge_bases = Knowledges.get_knowledge_bases(db=db)
log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
log.info(f'Starting reindexing for {len(knowledge_bases)} knowledge bases')
for knowledge_base in knowledge_bases:
try:
files = Knowledges.get_files_by_id(knowledge_base.id, db=db)
try:
if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
VECTOR_DB_CLIENT.delete_collection(
collection_name=knowledge_base.id
)
VECTOR_DB_CLIENT.delete_collection(collection_name=knowledge_base.id)
except Exception as e:
log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}")
log.error(f'Error deleting collection {knowledge_base.id}: {str(e)}')
continue # Skip, don't raise
failed_files = []
@@ -327,32 +319,26 @@ async def reindex_knowledge_files(
await run_in_threadpool(
process_file,
request,
ProcessFileForm(
file_id=file.id, collection_name=knowledge_base.id
),
ProcessFileForm(file_id=file.id, collection_name=knowledge_base.id),
user=user,
db=db,
)
except Exception as e:
log.error(
f"Error processing file {file.filename} (ID: {file.id}): {str(e)}"
)
failed_files.append({"file_id": file.id, "error": str(e)})
log.error(f'Error processing file {file.filename} (ID: {file.id}): {str(e)}')
failed_files.append({'file_id': file.id, 'error': str(e)})
continue
except Exception as e:
log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}")
log.error(f'Error processing knowledge base {knowledge_base.id}: {str(e)}')
# Don't raise, just continue
continue
if failed_files:
log.warning(
f"Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}"
)
log.warning(f'Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}')
for failed in failed_files:
log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}")
log.warning(f'File ID: {failed["file_id"]}, Error: {failed["error"]}')
log.info(f"Reindexing completed.")
log.info(f'Reindexing completed.')
return True
@@ -361,7 +347,7 @@ async def reindex_knowledge_files(
############################
@router.post("/metadata/reindex", response_model=dict)
@router.post('/metadata/reindex', response_model=dict)
async def reindex_knowledge_base_metadata_embeddings(
request: Request,
user=Depends(get_admin_user),
@@ -374,15 +360,15 @@ async def reindex_knowledge_base_metadata_embeddings(
this entire operation would exhaust the connection pool.
"""
knowledge_bases = Knowledges.get_knowledge_bases()
log.info(f"Reindexing embeddings for {len(knowledge_bases)} knowledge bases")
log.info(f'Reindexing embeddings for {len(knowledge_bases)} knowledge bases')
success_count = 0
for kb in knowledge_bases:
if await embed_knowledge_base_metadata(request, kb.id, kb.name, kb.description):
success_count += 1
log.info(f"Embedding reindex complete: {success_count}/{len(knowledge_bases)}")
return {"total": len(knowledge_bases), "success": success_count}
log.info(f'Embedding reindex complete: {success_count}/{len(knowledge_bases)}')
return {'total': len(knowledge_bases), 'success': success_count}
############################
@@ -395,35 +381,32 @@ class KnowledgeFilesResponse(KnowledgeResponse):
write_access: Optional[bool] = False
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
async def get_knowledge_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{id}', response_model=Optional[KnowledgeFilesResponse])
async def get_knowledge_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
if knowledge:
if (
user.role == "admin"
user.role == 'admin'
or knowledge.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="read",
permission='read',
db=db,
)
):
return KnowledgeFilesResponse(
**knowledge.model_dump(),
write_access=(
user.id == knowledge.user_id
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
),
@@ -445,7 +428,7 @@ async def get_knowledge_by_id(
############################
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
@router.post('/{id}/update', response_model=Optional[KnowledgeFilesResponse])
async def update_knowledge_by_id(
request: Request,
id: str,
@@ -467,11 +450,11 @@ async def update_knowledge_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -483,7 +466,7 @@ async def update_knowledge_by_id(
user.id,
user.role,
form_data.access_grants,
"sharing.public_knowledge",
'sharing.public_knowledge',
)
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
@@ -515,7 +498,7 @@ class KnowledgeAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post("/{id}/access/update", response_model=Optional[KnowledgeFilesResponse])
@router.post('/{id}/access/update', response_model=Optional[KnowledgeFilesResponse])
async def update_knowledge_access_by_id(
request: Request,
id: str,
@@ -534,12 +517,12 @@ async def update_knowledge_access_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -551,10 +534,10 @@ async def update_knowledge_access_by_id(
user.id,
user.role,
form_data.access_grants,
"sharing.public_knowledge",
'sharing.public_knowledge',
)
AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db)
return KnowledgeFilesResponse(
**Knowledges.get_knowledge_by_id(id=id, db=db).model_dump(),
@@ -567,7 +550,7 @@ async def update_knowledge_access_by_id(
############################
@router.get("/{id}/files", response_model=KnowledgeFileListResponse)
@router.get('/{id}/files', response_model=KnowledgeFileListResponse)
async def get_knowledge_files_by_id(
id: str,
query: Optional[str] = None,
@@ -578,7 +561,6 @@ async def get_knowledge_files_by_id(
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
if not knowledge:
raise HTTPException(
@@ -587,13 +569,13 @@ async def get_knowledge_files_by_id(
)
if not (
user.role == "admin"
user.role == 'admin'
or knowledge.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="read",
permission='read',
db=db,
)
):
@@ -609,17 +591,15 @@ async def get_knowledge_files_by_id(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if view_option:
filter["view_option"] = view_option
filter['view_option'] = view_option
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
return Knowledges.search_files_by_id(
id, user.id, filter=filter, skip=skip, limit=limit, db=db
)
return Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db)
############################
@@ -631,7 +611,7 @@ class KnowledgeFileIdForm(BaseModel):
file_id: str
@router.post("/{id}/file/add", response_model=Optional[KnowledgeFilesResponse])
@router.post('/{id}/file/add', response_model=Optional[KnowledgeFilesResponse])
def add_file_to_knowledge_by_id(
request: Request,
id: str,
@@ -650,12 +630,12 @@ def add_file_to_knowledge_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -684,9 +664,7 @@ def add_file_to_knowledge_by_id(
)
# Add file to knowledge base
Knowledges.add_file_to_knowledge_by_id(
knowledge_id=id, file_id=form_data.file_id, user_id=user.id, db=db
)
Knowledges.add_file_to_knowledge_by_id(knowledge_id=id, file_id=form_data.file_id, user_id=user.id, db=db)
except Exception as e:
log.debug(e)
raise HTTPException(
@@ -706,7 +684,7 @@ def add_file_to_knowledge_by_id(
)
@router.post("/{id}/file/update", response_model=Optional[KnowledgeFilesResponse])
@router.post('/{id}/file/update', response_model=Optional[KnowledgeFilesResponse])
def update_file_from_knowledge_by_id(
request: Request,
id: str,
@@ -725,14 +703,13 @@ def update_file_from_knowledge_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -753,9 +730,7 @@ def update_file_from_knowledge_by_id(
)
# Remove content from the vector database
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
)
VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': form_data.file_id})
# Add content to the vector database
try:
@@ -788,7 +763,7 @@ def update_file_from_knowledge_by_id(
############################
@router.post("/{id}/file/remove", response_model=Optional[KnowledgeFilesResponse])
@router.post('/{id}/file/remove', response_model=Optional[KnowledgeFilesResponse])
def remove_file_from_knowledge_by_id(
id: str,
form_data: KnowledgeFileIdForm,
@@ -807,12 +782,12 @@ def remove_file_from_knowledge_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -833,32 +808,30 @@ def remove_file_from_knowledge_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
Knowledges.remove_file_from_knowledge_by_id(
knowledge_id=id, file_id=form_data.file_id, db=db
)
Knowledges.remove_file_from_knowledge_by_id(knowledge_id=id, file_id=form_data.file_id, db=db)
# Remove content from the vector database
try:
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"file_id": form_data.file_id}
collection_name=knowledge.id, filter={'file_id': form_data.file_id}
) # Remove by file_id first
VECTOR_DB_CLIENT.delete(
collection_name=knowledge.id, filter={"hash": file.hash}
collection_name=knowledge.id, filter={'hash': file.hash}
) # Remove by hash as well in case of duplicates
except Exception as e:
log.debug("This was most likely caused by bypassing embedding processing")
log.debug('This was most likely caused by bypassing embedding processing')
log.debug(e)
pass
if delete_file:
try:
# Remove the file's collection from vector database
file_collection = f"file-{form_data.file_id}"
file_collection = f'file-{form_data.file_id}'
if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection):
VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection)
except Exception as e:
log.debug("This was most likely caused by bypassing embedding processing")
log.debug('This was most likely caused by bypassing embedding processing')
log.debug(e)
pass
@@ -882,10 +855,8 @@ def remove_file_from_knowledge_by_id(
############################
@router.delete("/{id}/delete", response_model=bool)
async def delete_knowledge_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.delete('/{id}/delete', response_model=bool)
async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
if not knowledge:
raise HTTPException(
@@ -897,34 +868,34 @@ async def delete_knowledge_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
log.info(f"Deleting knowledge base: {id} (name: {knowledge.name})")
log.info(f'Deleting knowledge base: {id} (name: {knowledge.name})')
# Get all models
models = Models.get_all_models(db=db)
log.info(f"Found {len(models)} models to check for knowledge base {id}")
log.info(f'Found {len(models)} models to check for knowledge base {id}')
# Update models that reference this knowledge base
for model in models:
if model.meta and hasattr(model.meta, "knowledge"):
if model.meta and hasattr(model.meta, 'knowledge'):
knowledge_list = model.meta.knowledge or []
# Filter out the deleted knowledge base
updated_knowledge = [k for k in knowledge_list if k.get("id") != id]
updated_knowledge = [k for k in knowledge_list if k.get('id') != id]
# If the knowledge list changed, update the model
if len(updated_knowledge) != len(knowledge_list):
log.info(f"Updating model {model.id} to remove knowledge base {id}")
log.info(f'Updating model {model.id} to remove knowledge base {id}')
model.meta.knowledge = updated_knowledge
# Create a ModelForm for the update
model_form = ModelForm(
@@ -957,10 +928,8 @@ async def delete_knowledge_by_id(
############################
@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse])
async def reset_knowledge_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.post('/{id}/reset', response_model=Optional[KnowledgeResponse])
async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
knowledge = Knowledges.get_knowledge_by_id(id=id, db=db)
if not knowledge:
raise HTTPException(
@@ -972,12 +941,12 @@ async def reset_knowledge_by_id(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -999,7 +968,7 @@ async def reset_knowledge_by_id(
############################
@router.post("/{id}/files/batch/add", response_model=Optional[KnowledgeFilesResponse])
@router.post('/{id}/files/batch/add', response_model=Optional[KnowledgeFilesResponse])
async def add_files_to_knowledge_batch(
request: Request,
id: str,
@@ -1021,12 +990,12 @@ async def add_files_to_knowledge_batch(
knowledge.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="knowledge",
resource_type='knowledge',
resource_id=knowledge.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -1034,7 +1003,7 @@ async def add_files_to_knowledge_batch(
)
# Batch-fetch all files to avoid N+1 queries
log.info(f"files/batch/add - {len(form_data)} files")
log.info(f'files/batch/add - {len(form_data)} files')
file_ids = [form.file_id for form in form_data]
files = Files.get_files_by_ids(file_ids, db=db)
@@ -1044,7 +1013,7 @@ async def add_files_to_knowledge_batch(
if missing_ids:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File {missing_ids[0]} not found",
detail=f'File {missing_ids[0]} not found',
)
# Process files
@@ -1056,27 +1025,23 @@ async def add_files_to_knowledge_batch(
db=db,
)
except Exception as e:
log.error(
f"add_files_to_knowledge_batch: Exception occurred: {e}", exc_info=True
)
log.error(f'add_files_to_knowledge_batch: Exception occurred: {e}', exc_info=True)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# Only add files that were successfully processed
successful_file_ids = [r.file_id for r in result.results if r.status == "completed"]
successful_file_ids = [r.file_id for r in result.results if r.status == 'completed']
for file_id in successful_file_ids:
Knowledges.add_file_to_knowledge_by_id(
knowledge_id=id, file_id=file_id, user_id=user.id, db=db
)
Knowledges.add_file_to_knowledge_by_id(knowledge_id=id, file_id=file_id, user_id=user.id, db=db)
# If there were any errors, include them in the response
if result.errors:
error_details = [f"{err.file_id}: {err.error}" for err in result.errors]
error_details = [f'{err.file_id}: {err.error}' for err in result.errors]
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db),
warnings={
"message": "Some files failed to process",
"errors": error_details,
'message': 'Some files failed to process',
'errors': error_details,
},
)
@@ -1091,10 +1056,8 @@ async def add_files_to_knowledge_batch(
############################
@router.get("/{id}/export")
async def export_knowledge_by_id(
id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/{id}/export')
async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
"""
Export a knowledge base as a zip file containing .txt files.
Admin only.
@@ -1111,24 +1074,24 @@ async def export_knowledge_by_id(
# Create zip file in memory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for file in files:
content = file.data.get("content", "") if file.data else ""
content = file.data.get('content', '') if file.data else ''
if content:
# Use original filename with .txt extension
filename = file.filename
if not filename.endswith(".txt"):
filename = f"{filename}.txt"
if not filename.endswith('.txt'):
filename = f'{filename}.txt'
zf.writestr(filename, content)
zip_buffer.seek(0)
# Sanitize knowledge name for filename
safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in knowledge.name)
zip_filename = f"{safe_name}.zip"
safe_name = ''.join(c if c.isalnum() or c in ' -_' else '_' for c in knowledge.name)
zip_filename = f'{safe_name}.zip'
return StreamingResponse(
zip_buffer,
media_type="application/zip",
headers={"Content-Disposition": f"attachment; filename={zip_filename}"},
media_type='application/zip',
headers={'Content-Disposition': f'attachment; filename={zip_filename}'},
)
+41 -62
View File
@@ -23,7 +23,7 @@ router = APIRouter()
############################
@router.get("/", response_model=list[MemoryModel])
@router.get('/', response_model=list[MemoryModel])
async def get_memories(
request: Request,
user=Depends(get_verified_user),
@@ -35,9 +35,7 @@ async def get_memories(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -59,7 +57,7 @@ class MemoryUpdateModel(BaseModel):
content: Optional[str] = None
@router.post("/add", response_model=Optional[MemoryModel])
@router.post('/add', response_model=Optional[MemoryModel])
async def add_memory(
request: Request,
form_data: AddMemoryForm,
@@ -75,9 +73,7 @@ async def add_memory(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -88,13 +84,13 @@ async def add_memory(
vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user)
VECTOR_DB_CLIENT.upsert(
collection_name=f"user-memory-{user.id}",
collection_name=f'user-memory-{user.id}',
items=[
{
"id": memory.id,
"text": memory.content,
"vector": vector,
"metadata": {"created_at": memory.created_at},
'id': memory.id,
'text': memory.content,
'vector': vector,
'metadata': {'created_at': memory.created_at},
}
],
)
@@ -112,7 +108,7 @@ class QueryMemoryForm(BaseModel):
k: Optional[int] = 1
@router.post("/query")
@router.post('/query')
async def query_memory(
request: Request,
form_data: QueryMemoryForm,
@@ -128,9 +124,7 @@ async def query_memory(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -138,12 +132,12 @@ async def query_memory(
memories = Memories.get_memories_by_user_id(user.id)
if not memories:
raise HTTPException(status_code=404, detail="No memories found for user")
raise HTTPException(status_code=404, detail='No memories found for user')
vector = await request.app.state.EMBEDDING_FUNCTION(form_data.content, user=user)
results = VECTOR_DB_CLIENT.search(
collection_name=f"user-memory-{user.id}",
collection_name=f'user-memory-{user.id}',
vectors=[vector],
limit=form_data.k,
)
@@ -154,7 +148,7 @@ async def query_memory(
############################
# ResetMemoryFromVectorDB
############################
@router.post("/reset", response_model=bool)
@router.post('/reset', response_model=bool)
async def reset_memory_from_vector_db(
request: Request,
user=Depends(get_verified_user),
@@ -173,36 +167,31 @@ async def reset_memory_from_vector_db(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}')
memories = Memories.get_memories_by_user_id(user.id)
# Generate vectors in parallel
vectors = await asyncio.gather(
*[
request.app.state.EMBEDDING_FUNCTION(memory.content, user=user)
for memory in memories
]
*[request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) for memory in memories]
)
VECTOR_DB_CLIENT.upsert(
collection_name=f"user-memory-{user.id}",
collection_name=f'user-memory-{user.id}',
items=[
{
"id": memory.id,
"text": memory.content,
"vector": vectors[idx],
"metadata": {
"created_at": memory.created_at,
"updated_at": memory.updated_at,
'id': memory.id,
'text': memory.content,
'vector': vectors[idx],
'metadata': {
'created_at': memory.created_at,
'updated_at': memory.updated_at,
},
}
for idx, memory in enumerate(memories)
@@ -217,7 +206,7 @@ async def reset_memory_from_vector_db(
############################
@router.delete("/delete/user", response_model=bool)
@router.delete('/delete/user', response_model=bool)
async def delete_memory_by_user_id(
request: Request,
user=Depends(get_verified_user),
@@ -229,9 +218,7 @@ async def delete_memory_by_user_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -241,7 +228,7 @@ async def delete_memory_by_user_id(
if result:
try:
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}')
except Exception as e:
log.error(e)
return True
@@ -254,7 +241,7 @@ async def delete_memory_by_user_id(
############################
@router.post("/{memory_id}/update", response_model=Optional[MemoryModel])
@router.post('/{memory_id}/update', response_model=Optional[MemoryModel])
async def update_memory_by_id(
memory_id: str,
request: Request,
@@ -271,33 +258,29 @@ async def update_memory_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
memory = Memories.update_memory_by_id_and_user_id(
memory_id, user.id, form_data.content
)
memory = Memories.update_memory_by_id_and_user_id(memory_id, user.id, form_data.content)
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
raise HTTPException(status_code=404, detail='Memory not found')
if form_data.content is not None:
vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user)
VECTOR_DB_CLIENT.upsert(
collection_name=f"user-memory-{user.id}",
collection_name=f'user-memory-{user.id}',
items=[
{
"id": memory.id,
"text": memory.content,
"vector": vector,
"metadata": {
"created_at": memory.created_at,
"updated_at": memory.updated_at,
'id': memory.id,
'text': memory.content,
'vector': vector,
'metadata': {
'created_at': memory.created_at,
'updated_at': memory.updated_at,
},
}
],
@@ -311,7 +294,7 @@ async def update_memory_by_id(
############################
@router.delete("/{memory_id}", response_model=bool)
@router.delete('/{memory_id}', response_model=bool)
async def delete_memory_by_id(
memory_id: str,
request: Request,
@@ -324,9 +307,7 @@ async def delete_memory_by_id(
detail=ERROR_MESSAGES.NOT_FOUND,
)
if not has_permission(
user.id, "features.memories", request.app.state.config.USER_PERMISSIONS
):
if not has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -335,9 +316,7 @@ async def delete_memory_by_id(
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db)
if result:
VECTOR_DB_CLIENT.delete(
collection_name=f"user-memory-{user.id}", ids=[memory_id]
)
VECTOR_DB_CLIENT.delete(collection_name=f'user-memory-{user.id}', ids=[memory_id])
return True
return False
+84 -108
View File
@@ -55,9 +55,7 @@ def is_valid_model_id(model_id: str) -> bool:
PAGE_ITEM_COUNT = 30
@router.get(
"/list", response_model=ModelAccessListResponse
) # do NOT use "/" as path, conflicts with main.py
@router.get('/list', response_model=ModelAccessListResponse) # do NOT use "/" as path, conflicts with main.py
async def get_models(
query: Optional[str] = None,
view_option: Optional[str] = None,
@@ -68,7 +66,6 @@ async def get_models(
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
limit = PAGE_ITEM_COUNT
page = max(1, page)
@@ -76,25 +73,25 @@ async def get_models(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if view_option:
filter["view_option"] = view_option
filter['view_option'] = view_option
if tag:
filter["tag"] = tag
filter['tag'] = tag
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
# Pre-fetch user group IDs once - used for both filter and write_access check
groups = Groups.get_groups_by_member_id(user.id, db=db)
user_group_ids = {group.id for group in groups}
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL:
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db)
@@ -102,9 +99,9 @@ async def get_models(
model_ids = [model.id for model in result.items]
writable_model_ids = AccessGrants.get_accessible_resource_ids(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_ids=model_ids,
permission="write",
permission='write',
user_group_ids=user_group_ids,
db=db,
)
@@ -114,7 +111,7 @@ async def get_models(
ModelAccessResponse(
**model.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == model.user_id
or model.id in writable_model_ids
),
@@ -130,10 +127,8 @@ async def get_models(
###########################
@router.get("/base", response_model=list[ModelResponse])
async def get_base_models(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/base', response_model=list[ModelResponse])
async def get_base_models(user=Depends(get_admin_user), db: Session = Depends(get_session)):
return Models.get_base_models(db=db)
@@ -142,11 +137,9 @@ async def get_base_models(
###########################
@router.get("/tags", response_model=list[str])
async def get_model_tags(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
@router.get('/tags', response_model=list[str])
async def get_model_tags(user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
models = Models.get_models(db=db)
else:
models = Models.get_models_by_user_id(user.id, db=db)
@@ -155,8 +148,8 @@ async def get_model_tags(
for model in models:
if model.meta:
meta = model.meta.model_dump()
for tag in meta.get("tags", []):
tags_set.add((tag.get("name")))
for tag in meta.get('tags', []):
tags_set.add((tag.get('name')))
tags = [tag for tag in tags_set]
tags.sort()
@@ -168,15 +161,15 @@ async def get_model_tags(
############################
@router.post("/create", response_model=Optional[ModelModel])
@router.post('/create', response_model=Optional[ModelModel])
async def create_new_model(
request: Request,
form_data: ModelForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "workspace.models", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'workspace.models', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -212,15 +205,15 @@ async def create_new_model(
############################
@router.get("/export", response_model=list[ModelModel])
@router.get('/export', response_model=list[ModelModel])
async def export_models(
request: Request,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
if user.role != 'admin' and not has_permission(
user.id,
"workspace.models_export",
'workspace.models_export',
request.app.state.config.USER_PERMISSIONS,
db=db,
):
@@ -229,7 +222,7 @@ async def export_models(
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
return Models.get_models(db=db)
else:
return Models.get_models_by_user_id(user.id, db=db)
@@ -244,16 +237,16 @@ class ModelsImportForm(BaseModel):
models: list[dict]
@router.post("/import", response_model=bool)
@router.post('/import', response_model=bool)
async def import_models(
request: Request,
user=Depends(get_verified_user),
form_data: ModelsImportForm = (...),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
if user.role != 'admin' and not has_permission(
user.id,
"workspace.models_import",
'workspace.models_import',
request.app.state.config.USER_PERMISSIONS,
db=db,
):
@@ -266,43 +259,36 @@ async def import_models(
if isinstance(data, list):
# Batch-fetch all existing models in one query to avoid N+1
model_ids = [
model_data.get("id")
model_data.get('id')
for model_data in data
if model_data.get("id") and is_valid_model_id(model_data.get("id"))
if model_data.get('id') and is_valid_model_id(model_data.get('id'))
]
existing_models = {
model.id: model
for model in (
Models.get_models_by_ids(model_ids, db=db) if model_ids else []
)
model.id: model for model in (Models.get_models_by_ids(model_ids, db=db) if model_ids else [])
}
for model_data in data:
# Here, you can add logic to validate model_data if needed
model_id = model_data.get("id")
model_id = model_data.get('id')
if model_id and is_valid_model_id(model_id):
existing_model = existing_models.get(model_id)
if existing_model:
# Update existing model
model_data["meta"] = model_data.get("meta", {})
model_data["params"] = model_data.get("params", {})
model_data['meta'] = model_data.get('meta', {})
model_data['params'] = model_data.get('params', {})
updated_model = ModelForm(
**{**existing_model.model_dump(), **model_data}
)
updated_model = ModelForm(**{**existing_model.model_dump(), **model_data})
Models.update_model_by_id(model_id, updated_model, db=db)
else:
# Insert new model
model_data["meta"] = model_data.get("meta", {})
model_data["params"] = model_data.get("params", {})
model_data['meta'] = model_data.get('meta', {})
model_data['params'] = model_data.get('params', {})
new_model = ModelForm(**model_data)
Models.insert_new_model(
user_id=user.id, form_data=new_model, db=db
)
Models.insert_new_model(user_id=user.id, form_data=new_model, db=db)
return True
else:
raise HTTPException(status_code=400, detail="Invalid JSON format")
raise HTTPException(status_code=400, detail='Invalid JSON format')
except Exception as e:
log.exception(e)
raise HTTPException(status_code=500, detail=str(e))
@@ -317,7 +303,7 @@ class SyncModelsForm(BaseModel):
models: list[ModelModel] = []
@router.post("/sync", response_model=list[ModelModel])
@router.post('/sync', response_model=list[ModelModel])
async def sync_models(
request: Request,
form_data: SyncModelsForm,
@@ -337,33 +323,31 @@ class ModelIdForm(BaseModel):
# Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id
@router.get("/model", response_model=Optional[ModelAccessResponse])
async def get_model_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/model', response_model=Optional[ModelAccessResponse])
async def get_model_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
model = Models.get_model_by_id(id, db=db)
if model:
if (
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or model.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_id=model.id,
permission="read",
permission='read',
db=db,
)
):
return ModelAccessResponse(
**model.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == model.user_id
or AccessGrants.has_access(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_id=model.id,
permission="write",
permission='write',
db=db,
)
),
@@ -385,7 +369,7 @@ async def get_model_by_id(
###########################
@router.get("/model/profile/image")
@router.get('/model/profile/image')
def get_model_profile_image(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
@@ -393,21 +377,21 @@ def get_model_profile_image(id: str, user=Depends(get_verified_user)):
etag = f'"{model.updated_at}"' if model.updated_at else None
if model.meta.profile_image_url:
if model.meta.profile_image_url.startswith("http"):
if model.meta.profile_image_url.startswith('http'):
return Response(
status_code=status.HTTP_302_FOUND,
headers={"Location": model.meta.profile_image_url},
headers={'Location': model.meta.profile_image_url},
)
elif model.meta.profile_image_url.startswith("data:image"):
elif model.meta.profile_image_url.startswith('data:image'):
try:
header, base64_data = model.meta.profile_image_url.split(",", 1)
header, base64_data = model.meta.profile_image_url.split(',', 1)
image_data = base64.b64decode(base64_data)
image_buffer = io.BytesIO(image_data)
media_type = header.split(";")[0].lstrip("data:")
media_type = header.split(';')[0].lstrip('data:')
headers = {"Content-Disposition": "inline"}
headers = {'Content-Disposition': 'inline'}
if etag:
headers["ETag"] = etag
headers['ETag'] = etag
return StreamingResponse(
image_buffer,
@@ -417,9 +401,9 @@ def get_model_profile_image(id: str, user=Depends(get_verified_user)):
except Exception as e:
pass
return FileResponse(f"{STATIC_DIR}/favicon.png")
return FileResponse(f'{STATIC_DIR}/favicon.png')
else:
return FileResponse(f"{STATIC_DIR}/favicon.png")
return FileResponse(f'{STATIC_DIR}/favicon.png')
############################
@@ -427,20 +411,18 @@ def get_model_profile_image(id: str, user=Depends(get_verified_user)):
############################
@router.post("/model/toggle", response_model=Optional[ModelResponse])
async def toggle_model_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.post('/model/toggle', response_model=Optional[ModelResponse])
async def toggle_model_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
model = Models.get_model_by_id(id, db=db)
if model:
if (
user.role == "admin"
user.role == 'admin'
or model.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_id=model.id,
permission="write",
permission='write',
db=db,
)
):
@@ -451,7 +433,7 @@ async def toggle_model_by_id(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
detail=ERROR_MESSAGES.DEFAULT('Error updating function'),
)
else:
raise HTTPException(
@@ -470,7 +452,7 @@ async def toggle_model_by_id(
############################
@router.post("/model/update", response_model=Optional[ModelModel])
@router.post('/model/update', response_model=Optional[ModelModel])
async def update_model_by_id(
form_data: ModelForm,
user=Depends(get_verified_user),
@@ -487,21 +469,19 @@ async def update_model_by_id(
model.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_id=model.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
model = Models.update_model_by_id(
form_data.id, ModelForm(**form_data.model_dump()), db=db
)
model = Models.update_model_by_id(form_data.id, ModelForm(**form_data.model_dump()), db=db)
return model
@@ -516,7 +496,7 @@ class ModelAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post("/model/access/update", response_model=Optional[ModelModel])
@router.post('/model/access/update', response_model=Optional[ModelModel])
async def update_model_access_by_id(
request: Request,
form_data: ModelAccessGrantsForm,
@@ -528,7 +508,7 @@ async def update_model_access_by_id(
# Non-preset models (e.g. direct Ollama/OpenAI models) may not have a DB
# entry yet. Create a minimal one so access grants can be stored.
if not model:
if user.role != "admin":
if user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -546,19 +526,19 @@ async def update_model_access_by_id(
if not model:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT("Error creating model entry"),
detail=ERROR_MESSAGES.DEFAULT('Error creating model entry'),
)
if (
model.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_id=model.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -570,12 +550,10 @@ async def update_model_access_by_id(
user.id,
user.role,
form_data.access_grants,
"sharing.public_models",
'sharing.public_models',
)
AccessGrants.set_access_grants(
"model", form_data.id, form_data.access_grants, db=db
)
AccessGrants.set_access_grants('model', form_data.id, form_data.access_grants, db=db)
return Models.get_model_by_id(form_data.id, db=db)
@@ -585,7 +563,7 @@ async def update_model_access_by_id(
############################
@router.post("/model/delete", response_model=bool)
@router.post('/model/delete', response_model=bool)
async def delete_model_by_id(
form_data: ModelIdForm,
user=Depends(get_verified_user),
@@ -599,13 +577,13 @@ async def delete_model_by_id(
)
if (
user.role != "admin"
user.role != 'admin'
and model.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="model",
resource_type='model',
resource_id=model.id,
permission="write",
permission='write',
db=db,
)
):
@@ -618,9 +596,7 @@ async def delete_model_by_id(
return result
@router.delete("/delete/all", response_model=bool)
async def delete_all_models(
user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.delete('/delete/all', response_model=bool)
async def delete_all_models(user=Depends(get_admin_user), db: Session = Depends(get_session)):
result = Models.delete_all_models(db=db)
return result
+65 -87
View File
@@ -44,8 +44,8 @@ router = APIRouter()
def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optional[dict]:
if not data:
return data
md = (data.get("content") or {}).get("md") or ""
return {"content": {"md": md[:max_length]}}
md = (data.get('content') or {}).get('md') or ''
return {'content': {'md': md[:max_length]}}
############################
@@ -62,15 +62,15 @@ class NoteItemResponse(BaseModel):
user: Optional[UserResponse] = None
@router.get("/", response_model=list[NoteItemResponse])
@router.get('/', response_model=list[NoteItemResponse])
async def get_notes(
request: Request,
page: Optional[int] = None,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -83,7 +83,7 @@ async def get_notes(
limit = 60
skip = (page - 1) * limit
notes = Notes.get_notes_by_user_id(user.id, "read", skip=skip, limit=limit, db=db)
notes = Notes.get_notes_by_user_id(user.id, 'read', skip=skip, limit=limit, db=db)
if not notes:
return []
@@ -94,8 +94,8 @@ async def get_notes(
NoteUserResponse(
**{
**note.model_dump(),
"data": _truncate_note_data(note.data),
"user": UserResponse(**users[note.user_id].model_dump()),
'data': _truncate_note_data(note.data),
'user': UserResponse(**users[note.user_id].model_dump()),
}
)
for note in notes
@@ -103,7 +103,7 @@ async def get_notes(
]
@router.get("/search", response_model=NoteListResponse)
@router.get('/search', response_model=NoteListResponse)
async def search_notes(
request: Request,
query: Optional[str] = None,
@@ -115,8 +115,8 @@ async def search_notes(
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -131,22 +131,22 @@ async def search_notes(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if view_option:
filter["view_option"] = view_option
filter['view_option'] = view_option
if permission:
filter["permission"] = permission
filter['permission'] = permission
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL:
groups = Groups.get_groups_by_member_id(user.id, db=db)
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
result = Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db)
for note in result.items:
@@ -159,15 +159,15 @@ async def search_notes(
############################
@router.post("/create", response_model=Optional[NoteModel])
@router.post('/create', response_model=Optional[NoteModel])
async def create_new_note(
request: Request,
form_data: NoteForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -179,9 +179,7 @@ async def create_new_note(
return note
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
@@ -193,15 +191,15 @@ class NoteResponse(NoteModel):
write_access: bool = False
@router.get("/{id}", response_model=Optional[NoteResponse])
@router.get('/{id}', response_model=Optional[NoteResponse])
async def get_note_by_id(
request: Request,
id: str,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -210,34 +208,30 @@ async def get_note_by_id(
note = Notes.get_note_by_id(id, db=db)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
if user.role != "admin" and (
if user.role != 'admin' and (
user.id != note.user_id
and (
not AccessGrants.has_access(
user_id=user.id,
resource_type="note",
resource_type='note',
resource_id=note.id,
permission="read",
permission='read',
db=db,
)
)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
write_access = (
user.role == "admin"
user.role == 'admin'
or (user.id == note.user_id)
or AccessGrants.has_access(
user_id=user.id,
resource_type="note",
resource_type='note',
resource_id=note.id,
permission="write",
permission='write',
db=db,
)
or has_public_read_access_grant(note.access_grants)
@@ -251,7 +245,7 @@ async def get_note_by_id(
############################
@router.post("/{id}/update", response_model=Optional[NoteModel])
@router.post('/{id}/update', response_model=Optional[NoteModel])
async def update_note_by_id(
request: Request,
id: str,
@@ -259,8 +253,8 @@ async def update_note_by_id(
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -269,47 +263,41 @@ async def update_note_by_id(
note = Notes.get_note_by_id(id, db=db)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
if user.role != "admin" and (
if user.role != 'admin' and (
user.id != note.user_id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="note",
resource_type='note',
resource_id=note.id,
permission="write",
permission='write',
db=db,
)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
form_data.access_grants = filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
user.id,
user.role,
form_data.access_grants,
"sharing.public_notes",
'sharing.public_notes',
db=db,
)
try:
note = Notes.update_note_by_id(id, form_data, db=db)
await sio.emit(
"note-events",
'note-events',
note.model_dump(),
to=f"note:{note.id}",
to=f'note:{note.id}',
)
return note
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
############################
@@ -321,7 +309,7 @@ class NoteAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post("/{id}/access/update", response_model=Optional[NoteModel])
@router.post('/{id}/access/update', response_model=Optional[NoteModel])
async def update_note_access_by_id(
request: Request,
id: str,
@@ -329,8 +317,8 @@ async def update_note_access_by_id(
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -339,33 +327,29 @@ async def update_note_access_by_id(
note = Notes.get_note_by_id(id, db=db)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
if user.role != "admin" and (
if user.role != 'admin' and (
user.id != note.user_id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="note",
resource_type='note',
resource_id=note.id,
permission="write",
permission='write',
db=db,
)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
form_data.access_grants = filter_allowed_access_grants(
request.app.state.config.USER_PERMISSIONS,
user.id,
user.role,
form_data.access_grants,
"sharing.public_notes",
'sharing.public_notes',
)
AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db)
return Notes.get_note_by_id(id, db=db)
@@ -375,15 +359,15 @@ async def update_note_access_by_id(
############################
@router.delete("/{id}/delete", response_model=bool)
@router.delete('/{id}/delete', response_model=bool)
async def delete_note_by_id(
request: Request,
id: str,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -392,29 +376,23 @@ async def delete_note_by_id(
note = Notes.get_note_by_id(id, db=db)
if not note:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
if user.role != "admin" and (
if user.role != 'admin' and (
user.id != note.user_id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="note",
resource_type='note',
resource_id=note.id,
permission="write",
permission='write',
db=db,
)
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
try:
note = Notes.delete_note_by_id(id, db=db)
return True
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+103 -136
View File
@@ -40,33 +40,30 @@ def get_sorted_filters(model_id, models):
filters = [
model
for model in models.values()
if "pipeline" in model
and "type" in model["pipeline"]
and model["pipeline"]["type"] == "filter"
if 'pipeline' in model
and 'type' in model['pipeline']
and model['pipeline']['type'] == 'filter'
and (
model["pipeline"]["pipelines"] == ["*"]
or any(
model_id == target_model_id
for target_model_id in model["pipeline"]["pipelines"]
)
model['pipeline']['pipelines'] == ['*']
or any(model_id == target_model_id for target_model_id in model['pipeline']['pipelines'])
)
]
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
sorted_filters = sorted(filters, key=lambda x: x['pipeline']['priority'])
return sorted_filters
async def process_pipeline_inlet_filter(request, payload, user, models):
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
model_id = payload["model"]
user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role}
model_id = payload['model']
sorted_filters = get_sorted_filters(model_id, models)
model = models[model_id]
if "pipeline" in model:
if 'pipeline' in model:
sorted_filters.append(model)
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
urlIdx = filter.get('urlIdx')
try:
urlIdx = int(urlIdx)
@@ -79,15 +76,15 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
if not key:
continue
headers = {"Authorization": f"Bearer {key}"}
headers = {'Authorization': f'Bearer {key}'}
request_data = {
"user": user,
"body": payload,
'user': user,
'body': payload,
}
try:
async with session.post(
f"{url}/{filter['id']}/filter/inlet",
f'{url}/{filter["id"]}/filter/inlet',
headers=headers,
json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -95,31 +92,27 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
response.raise_for_status()
payload = await response.json()
except aiohttp.ClientResponseError as e:
res = (
await response.json()
if response.content_type == "application/json"
else {}
)
if "detail" in res:
raise Exception(response.status, res["detail"])
res = await response.json() if response.content_type == 'application/json' else {}
if 'detail' in res:
raise Exception(response.status, res['detail'])
except Exception as e:
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
return payload
async def process_pipeline_outlet_filter(request, payload, user, models):
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
model_id = payload["model"]
user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role}
model_id = payload['model']
sorted_filters = get_sorted_filters(model_id, models)
model = models[model_id]
if "pipeline" in model:
if 'pipeline' in model:
sorted_filters = [model] + sorted_filters
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
urlIdx = filter.get('urlIdx')
try:
urlIdx = int(urlIdx)
@@ -132,15 +125,15 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
if not key:
continue
headers = {"Authorization": f"Bearer {key}"}
headers = {'Authorization': f'Bearer {key}'}
request_data = {
"user": user,
"body": payload,
'user': user,
'body': payload,
}
try:
async with session.post(
f"{url}/{filter['id']}/filter/outlet",
f'{url}/{filter["id"]}/filter/outlet',
headers=headers,
json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -149,17 +142,13 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
payload = await response.json()
except aiohttp.ClientResponseError as e:
try:
res = (
await response.json()
if "application/json" in response.content_type
else {}
)
if "detail" in res:
res = await response.json() if 'application/json' in response.content_type else {}
if 'detail' in res:
raise Exception(response.status, res)
except Exception:
pass
except Exception as e:
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
return payload
@@ -173,72 +162,68 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
router = APIRouter()
@router.get("/list")
@router.get('/list')
async def get_pipelines_list(request: Request, user=Depends(get_admin_user)):
responses = await get_all_models_responses(request, user)
log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}")
log.debug(f'get_pipelines_list: get_openai_models_responses returned {responses}')
urlIdxs = [
idx
for idx, response in enumerate(responses)
if response is not None and "pipelines" in response
]
urlIdxs = [idx for idx, response in enumerate(responses) if response is not None and 'pipelines' in response]
return {
"data": [
'data': [
{
"url": request.app.state.config.OPENAI_API_BASE_URLS[urlIdx],
"idx": urlIdx,
'url': request.app.state.config.OPENAI_API_BASE_URLS[urlIdx],
'idx': urlIdx,
}
for urlIdx in urlIdxs
]
}
@router.post("/upload")
@router.post('/upload')
async def upload_pipeline(
request: Request,
urlIdx: int = Form(...),
file: UploadFile = File(...),
user=Depends(get_admin_user),
):
log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
log.info(f'upload_pipeline: urlIdx={urlIdx}, filename={file.filename}')
filename = os.path.basename(file.filename)
# Check if the uploaded file is a python file
if not (filename and filename.endswith(".py")):
if not (filename and filename.endswith('.py')):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only Python (.py) files are allowed.",
detail='Only Python (.py) files are allowed.',
)
upload_folder = f"{CACHE_DIR}/pipelines"
upload_folder = f'{CACHE_DIR}/pipelines'
os.makedirs(upload_folder, exist_ok=True)
file_path = os.path.join(upload_folder, filename)
response = None
try:
# Save the uploaded file
with open(file_path, "wb") as buffer:
with open(file_path, 'wb') as buffer:
shutil.copyfileobj(file.file, buffer)
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
headers = {'Authorization': f'Bearer {key}'}
async with aiohttp.ClientSession(trust_env=True) as session:
with open(file_path, "rb") as f:
with open(file_path, 'rb') as f:
form_data = aiohttp.FormData()
form_data.add_field(
"file",
'file',
f,
filename=filename,
content_type="application/octet-stream",
content_type='application/octet-stream',
)
async with session.post(
f"{url}/pipelines/upload",
f'{url}/pipelines/upload',
headers=headers,
data=form_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
@@ -249,7 +234,7 @@ async def upload_pipeline(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
status_code = status.HTTP_404_NOT_FOUND
@@ -257,14 +242,14 @@ async def upload_pipeline(
status_code = response.status
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=status_code,
detail=detail if detail else "Pipeline not found",
detail=detail if detail else 'Pipeline not found',
)
finally:
# Ensure the file is deleted after the upload is completed or on failure
@@ -277,10 +262,8 @@ class AddPipelineForm(BaseModel):
urlIdx: int
@router.post("/add")
async def add_pipeline(
request: Request, form_data: AddPipelineForm, user=Depends(get_admin_user)
):
@router.post('/add')
async def add_pipeline(request: Request, form_data: AddPipelineForm, user=Depends(get_admin_user)):
response = None
try:
urlIdx = form_data.urlIdx
@@ -290,9 +273,9 @@ async def add_pipeline(
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(
f"{url}/pipelines/add",
headers={"Authorization": f"Bearer {key}"},
json={"url": form_data.url},
f'{url}/pipelines/add',
headers={'Authorization': f'Bearer {key}'},
json={'url': form_data.url},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
response.raise_for_status()
@@ -301,22 +284,20 @@ async def add_pipeline(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
if response is not None:
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=(
response.status if response is not None else status.HTTP_404_NOT_FOUND
),
detail=detail if detail else "Pipeline not found",
status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND),
detail=detail if detail else 'Pipeline not found',
)
@@ -325,10 +306,8 @@ class DeletePipelineForm(BaseModel):
urlIdx: int
@router.delete("/delete")
async def delete_pipeline(
request: Request, form_data: DeletePipelineForm, user=Depends(get_admin_user)
):
@router.delete('/delete')
async def delete_pipeline(request: Request, form_data: DeletePipelineForm, user=Depends(get_admin_user)):
response = None
try:
urlIdx = form_data.urlIdx
@@ -338,9 +317,9 @@ async def delete_pipeline(
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.delete(
f"{url}/pipelines/delete",
headers={"Authorization": f"Bearer {key}"},
json={"id": form_data.id},
f'{url}/pipelines/delete',
headers={'Authorization': f'Bearer {key}'},
json={'id': form_data.id},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
response.raise_for_status()
@@ -349,29 +328,25 @@ async def delete_pipeline(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
if response is not None:
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=(
response.status if response is not None else status.HTTP_404_NOT_FOUND
),
detail=detail if detail else "Pipeline not found",
status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND),
detail=detail if detail else 'Pipeline not found',
)
@router.get("/")
async def get_pipelines(
request: Request, urlIdx: Optional[int] = None, user=Depends(get_admin_user)
):
@router.get('/')
async def get_pipelines(request: Request, urlIdx: Optional[int] = None, user=Depends(get_admin_user)):
response = None
try:
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -379,8 +354,8 @@ async def get_pipelines(
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
f"{url}/pipelines",
headers={"Authorization": f"Bearer {key}"},
f'{url}/pipelines',
headers={'Authorization': f'Bearer {key}'},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
response.raise_for_status()
@@ -389,26 +364,24 @@ async def get_pipelines(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
if response is not None:
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=(
response.status if response is not None else status.HTTP_404_NOT_FOUND
),
detail=detail if detail else "Pipeline not found",
status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND),
detail=detail if detail else 'Pipeline not found',
)
@router.get("/{pipeline_id}/valves")
@router.get('/{pipeline_id}/valves')
async def get_pipeline_valves(
request: Request,
urlIdx: Optional[int],
@@ -422,8 +395,8 @@ async def get_pipeline_valves(
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
f"{url}/{pipeline_id}/valves",
headers={"Authorization": f"Bearer {key}"},
f'{url}/{pipeline_id}/valves',
headers={'Authorization': f'Bearer {key}'},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
response.raise_for_status()
@@ -432,26 +405,24 @@ async def get_pipeline_valves(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
if response is not None:
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=(
response.status if response is not None else status.HTTP_404_NOT_FOUND
),
detail=detail if detail else "Pipeline not found",
status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND),
detail=detail if detail else 'Pipeline not found',
)
@router.get("/{pipeline_id}/valves/spec")
@router.get('/{pipeline_id}/valves/spec')
async def get_pipeline_valves_spec(
request: Request,
urlIdx: Optional[int],
@@ -465,8 +436,8 @@ async def get_pipeline_valves_spec(
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
f"{url}/{pipeline_id}/valves/spec",
headers={"Authorization": f"Bearer {key}"},
f'{url}/{pipeline_id}/valves/spec',
headers={'Authorization': f'Bearer {key}'},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
response.raise_for_status()
@@ -475,26 +446,24 @@ async def get_pipeline_valves_spec(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
if response is not None:
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=(
response.status if response is not None else status.HTTP_404_NOT_FOUND
),
detail=detail if detail else "Pipeline not found",
status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND),
detail=detail if detail else 'Pipeline not found',
)
@router.post("/{pipeline_id}/valves/update")
@router.post('/{pipeline_id}/valves/update')
async def update_pipeline_valves(
request: Request,
urlIdx: Optional[int],
@@ -509,8 +478,8 @@ async def update_pipeline_valves(
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(
f"{url}/{pipeline_id}/valves/update",
headers={"Authorization": f"Bearer {key}"},
f'{url}/{pipeline_id}/valves/update',
headers={'Authorization': f'Bearer {key}'},
json={**form_data},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
@@ -520,21 +489,19 @@ async def update_pipeline_valves(
return {**data}
except Exception as e:
# Handle connection error here
log.exception(f"Connection error: {e}")
log.exception(f'Connection error: {e}')
detail = None
if response is not None:
try:
res = await response.json()
if "detail" in res:
detail = res["detail"]
if 'detail' in res:
detail = res['detail']
except Exception:
pass
raise HTTPException(
status_code=(
response.status if response is not None else status.HTTP_404_NOT_FOUND
),
detail=detail if detail else "Pipeline not found",
status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND),
detail=detail if detail else 'Pipeline not found',
)
+90 -110
View File
@@ -45,26 +45,22 @@ PAGE_ITEM_COUNT = 30
############################
@router.get("/", response_model=list[PromptModel])
async def get_prompts(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
@router.get('/', response_model=list[PromptModel])
async def get_prompts(user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
prompts = Prompts.get_prompts(db=db)
else:
prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db)
prompts = Prompts.get_prompts_by_user_id(user.id, 'read', db=db)
return prompts
@router.get("/tags", response_model=list[str])
async def get_prompt_tags(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
@router.get('/tags', response_model=list[str])
async def get_prompt_tags(user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
return Prompts.get_tags(db=db)
else:
prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db)
prompts = Prompts.get_prompts_by_user_id(user.id, 'read', db=db)
tags = set()
for prompt in prompts:
if prompt.tags:
@@ -72,7 +68,7 @@ async def get_prompt_tags(
return sorted(list(tags))
@router.get("/list", response_model=PromptAccessListResponse)
@router.get('/list', response_model=PromptAccessListResponse)
async def get_prompt_list(
query: Optional[str] = None,
view_option: Optional[str] = None,
@@ -90,37 +86,35 @@ async def get_prompt_list(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if view_option:
filter["view_option"] = view_option
filter['view_option'] = view_option
if tag:
filter["tag"] = tag
filter['tag'] = tag
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
# Pre-fetch user group IDs once - used for both filter and write_access check
groups = Groups.get_groups_by_member_id(user.id, db=db)
user_group_ids = {group.id for group in groups}
if not (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL):
if not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL):
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
result = Prompts.search_prompts(
user.id, filter=filter, skip=skip, limit=limit, db=db
)
result = Prompts.search_prompts(user.id, filter=filter, skip=skip, limit=limit, db=db)
# Batch-fetch writable prompt IDs in a single query instead of N has_access calls
prompt_ids = [prompt.id for prompt in result.items]
writable_prompt_ids = AccessGrants.get_accessible_resource_ids(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_ids=prompt_ids,
permission="write",
permission='write',
user_group_ids=user_group_ids,
db=db,
)
@@ -130,7 +124,7 @@ async def get_prompt_list(
PromptAccessResponse(
**prompt.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == prompt.user_id
or prompt.id in writable_prompt_ids
),
@@ -146,23 +140,23 @@ async def get_prompt_list(
############################
@router.post("/create", response_model=Optional[PromptModel])
@router.post('/create', response_model=Optional[PromptModel])
async def create_new_prompt(
request: Request,
form_data: PromptForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not (
if user.role != 'admin' and not (
has_permission(
user.id,
"workspace.prompts",
'workspace.prompts',
request.app.state.config.USER_PERMISSIONS,
db=db,
)
or has_permission(
user.id,
"workspace.prompts_import",
'workspace.prompts_import',
request.app.state.config.USER_PERMISSIONS,
db=db,
)
@@ -193,34 +187,32 @@ async def create_new_prompt(
############################
@router.get("/command/{command}", response_model=Optional[PromptAccessResponse])
async def get_prompt_by_command(
command: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/command/{command}', response_model=Optional[PromptAccessResponse])
async def get_prompt_by_command(command: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
prompt = Prompts.get_prompt_by_command(command, db=db)
if prompt:
if (
user.role == "admin"
user.role == 'admin'
or prompt.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="read",
permission='read',
db=db,
)
):
return PromptAccessResponse(
**prompt.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == prompt.user_id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
),
@@ -237,34 +229,32 @@ async def get_prompt_by_command(
############################
@router.get("/id/{prompt_id}", response_model=Optional[PromptAccessResponse])
async def get_prompt_by_id(
prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{prompt_id}', response_model=Optional[PromptAccessResponse])
async def get_prompt_by_id(prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
prompt = Prompts.get_prompt_by_id(prompt_id, db=db)
if prompt:
if (
user.role == "admin"
user.role == 'admin'
or prompt.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="read",
permission='read',
db=db,
)
):
return PromptAccessResponse(
**prompt.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == prompt.user_id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
),
@@ -281,7 +271,7 @@ async def get_prompt_by_id(
############################
@router.post("/id/{prompt_id}/update", response_model=Optional[PromptModel])
@router.post('/id/{prompt_id}/update', response_model=Optional[PromptModel])
async def update_prompt_by_id(
prompt_id: str,
form_data: PromptForm,
@@ -301,12 +291,12 @@ async def update_prompt_by_id(
prompt.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -338,7 +328,7 @@ async def update_prompt_by_id(
############################
@router.post("/id/{prompt_id}/update/meta", response_model=Optional[PromptModel])
@router.post('/id/{prompt_id}/update/meta', response_model=Optional[PromptModel])
async def update_prompt_metadata(
prompt_id: str,
form_data: PromptMetadataForm,
@@ -358,12 +348,12 @@ async def update_prompt_metadata(
prompt.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -379,9 +369,7 @@ async def update_prompt_metadata(
detail=f"Command '/{form_data.command}' is already in use",
)
updated_prompt = Prompts.update_prompt_metadata(
prompt.id, form_data.name, form_data.command, form_data.tags, db=db
)
updated_prompt = Prompts.update_prompt_metadata(prompt.id, form_data.name, form_data.command, form_data.tags, db=db)
if updated_prompt:
return updated_prompt
else:
@@ -391,7 +379,7 @@ async def update_prompt_metadata(
)
@router.post("/id/{prompt_id}/update/version", response_model=Optional[PromptModel])
@router.post('/id/{prompt_id}/update/version', response_model=Optional[PromptModel])
async def set_prompt_version(
prompt_id: str,
form_data: PromptVersionUpdateForm,
@@ -409,21 +397,19 @@ async def set_prompt_version(
prompt.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
updated_prompt = Prompts.update_prompt_version(
prompt.id, form_data.version_id, db=db
)
updated_prompt = Prompts.update_prompt_version(prompt.id, form_data.version_id, db=db)
if updated_prompt:
return updated_prompt
else:
@@ -442,7 +428,7 @@ class PromptAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post("/id/{prompt_id}/access/update", response_model=Optional[PromptModel])
@router.post('/id/{prompt_id}/access/update', response_model=Optional[PromptModel])
async def update_prompt_access_by_id(
request: Request,
prompt_id: str,
@@ -461,12 +447,12 @@ async def update_prompt_access_by_id(
prompt.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -478,10 +464,10 @@ async def update_prompt_access_by_id(
user.id,
user.role,
form_data.access_grants,
"sharing.public_prompts",
'sharing.public_prompts',
)
AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db)
return Prompts.get_prompt_by_id(prompt_id, db=db)
@@ -491,10 +477,8 @@ async def update_prompt_access_by_id(
############################
@router.post("/id/{prompt_id}/toggle", response_model=Optional[PromptModel])
async def toggle_prompt_active(
prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.post('/id/{prompt_id}/toggle', response_model=Optional[PromptModel])
async def toggle_prompt_active(prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
prompt = Prompts.get_prompt_by_id(prompt_id, db=db)
if not prompt:
@@ -507,12 +491,12 @@ async def toggle_prompt_active(
prompt.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -533,10 +517,8 @@ async def toggle_prompt_active(
############################
@router.delete("/id/{prompt_id}/delete", response_model=bool)
async def delete_prompt_by_id(
prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.delete('/id/{prompt_id}/delete', response_model=bool)
async def delete_prompt_by_id(prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
prompt = Prompts.get_prompt_by_id(prompt_id, db=db)
if not prompt:
@@ -549,12 +531,12 @@ async def delete_prompt_by_id(
prompt.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -570,7 +552,7 @@ async def delete_prompt_by_id(
############################
@router.get("/id/{prompt_id}/history", response_model=list[PromptHistoryResponse])
@router.get('/id/{prompt_id}/history', response_model=list[PromptHistoryResponse])
async def get_prompt_history(
prompt_id: str,
page: int = 0,
@@ -590,13 +572,13 @@ async def get_prompt_history(
# Check read access
if not (
user.role == "admin"
user.role == 'admin'
or prompt.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="read",
permission='read',
db=db,
)
):
@@ -605,13 +587,11 @@ async def get_prompt_history(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
history = PromptHistories.get_history_by_prompt_id(
prompt.id, limit=PAGE_SIZE, offset=page * PAGE_SIZE, db=db
)
history = PromptHistories.get_history_by_prompt_id(prompt.id, limit=PAGE_SIZE, offset=page * PAGE_SIZE, db=db)
return history
@router.get("/id/{prompt_id}/history/{history_id}", response_model=PromptHistoryModel)
@router.get('/id/{prompt_id}/history/{history_id}', response_model=PromptHistoryModel)
async def get_prompt_history_entry(
prompt_id: str,
history_id: str,
@@ -629,13 +609,13 @@ async def get_prompt_history_entry(
# Check read access
if not (
user.role == "admin"
user.role == 'admin'
or prompt.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="read",
permission='read',
db=db,
)
):
@@ -654,7 +634,7 @@ async def get_prompt_history_entry(
return history_entry
@router.delete("/id/{prompt_id}/history/{history_id}", response_model=bool)
@router.delete('/id/{prompt_id}/history/{history_id}', response_model=bool)
async def delete_prompt_history_entry(
prompt_id: str,
history_id: str,
@@ -672,13 +652,13 @@ async def delete_prompt_history_entry(
# Check write access
if not (
user.role == "admin"
user.role == 'admin'
or prompt.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="write",
permission='write',
db=db,
)
):
@@ -691,7 +671,7 @@ async def delete_prompt_history_entry(
if prompt.version_id == history_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete the active production version",
detail='Cannot delete the active production version',
)
success = PromptHistories.delete_history_entry(history_id, db=db)
@@ -704,7 +684,7 @@ async def delete_prompt_history_entry(
return success
@router.get("/id/{prompt_id}/history/diff")
@router.get('/id/{prompt_id}/history/diff')
async def get_prompt_diff(
prompt_id: str,
from_id: str,
@@ -723,13 +703,13 @@ async def get_prompt_diff(
# Check read access
if not (
user.role == "admin"
user.role == 'admin'
or prompt.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="prompt",
resource_type='prompt',
resource_id=prompt.id,
permission="read",
permission='read',
db=db,
)
):
@@ -742,7 +722,7 @@ async def get_prompt_diff(
if not diff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="One or both history entries not found",
detail='One or both history entries not found',
)
return diff
File diff suppressed because it is too large Load Diff
+179 -203
View File
@@ -37,32 +37,32 @@ log = logging.getLogger(__name__)
router = APIRouter()
# SCIM 2.0 Schema URIs
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
SCIM_LIST_RESPONSE_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error"
SCIM_USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User'
SCIM_GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group'
SCIM_LIST_RESPONSE_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
SCIM_ERROR_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error'
# SCIM Resource Types
SCIM_RESOURCE_TYPE_USER = "User"
SCIM_RESOURCE_TYPE_GROUP = "Group"
SCIM_RESOURCE_TYPE_USER = 'User'
SCIM_RESOURCE_TYPE_GROUP = 'Group'
def scim_error(status_code: int, detail: str, scim_type: Optional[str] = None):
"""Create a SCIM-compliant error response"""
error_body = {
"schemas": [SCIM_ERROR_SCHEMA],
"status": str(status_code),
"detail": detail,
'schemas': [SCIM_ERROR_SCHEMA],
'status': str(status_code),
'detail': detail,
}
if scim_type:
error_body["scimType"] = scim_type
error_body['scimType'] = scim_type
elif status_code == 404:
error_body["scimType"] = "invalidValue"
error_body['scimType'] = 'invalidValue'
elif status_code == 409:
error_body["scimType"] = "uniqueness"
error_body['scimType'] = 'uniqueness'
elif status_code == 400:
error_body["scimType"] = "invalidSyntax"
error_body['scimType'] = 'invalidSyntax'
return JSONResponse(status_code=status_code, content=error_body)
@@ -101,7 +101,7 @@ class SCIMEmail(BaseModel):
"""SCIM Email"""
value: str
type: Optional[str] = "work"
type: Optional[str] = 'work'
primary: bool = True
display: Optional[str] = None
@@ -110,7 +110,7 @@ class SCIMPhoto(BaseModel):
"""SCIM Photo"""
value: str
type: Optional[str] = "photo"
type: Optional[str] = 'photo'
primary: bool = True
display: Optional[str] = None
@@ -119,8 +119,8 @@ class SCIMGroupMember(BaseModel):
"""SCIM Group Member"""
value: str # User ID
ref: Optional[str] = Field(None, alias="$ref")
type: Optional[str] = "User"
ref: Optional[str] = Field(None, alias='$ref')
type: Optional[str] = 'User'
display: Optional[str] = None
@@ -227,13 +227,11 @@ class SCIMPatchOperation(BaseModel):
class SCIMPatchRequest(BaseModel):
"""SCIM Patch Request"""
schemas: List[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
schemas: List[str] = ['urn:ietf:params:scim:api:messages:2.0:PatchOp']
Operations: List[SCIMPatchOperation]
def get_scim_auth(
request: Request, authorization: Optional[str] = Header(None)
) -> bool:
def get_scim_auth(request: Request, authorization: Optional[str] = Header(None)) -> bool:
"""
Verify SCIM authentication
Checks for SCIM-specific bearer token configured in the system
@@ -241,8 +239,8 @@ def get_scim_auth(
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
headers={"WWW-Authenticate": "Bearer"},
detail='Authorization header required',
headers={'WWW-Authenticate': 'Bearer'},
)
try:
@@ -250,42 +248,40 @@ def get_scim_auth(
if len(parts) != 2:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authorization format. Expected: Bearer <token>",
detail='Invalid authorization format. Expected: Bearer <token>',
)
scheme, token = parts
if scheme.lower() != "bearer":
if scheme.lower() != 'bearer':
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication scheme",
detail='Invalid authentication scheme',
)
# Check if SCIM is enabled
enable_scim = getattr(request.app.state, "ENABLE_SCIM", False)
log.info(
f"SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}"
)
enable_scim = getattr(request.app.state, 'ENABLE_SCIM', False)
log.info(f'SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}')
# Handle both PersistentConfig and direct value
if hasattr(enable_scim, "value"):
if hasattr(enable_scim, 'value'):
enable_scim = enable_scim.value
if not enable_scim:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="SCIM is not enabled",
detail='SCIM is not enabled',
)
# Verify the SCIM token
scim_token = getattr(request.app.state, "SCIM_TOKEN", None)
scim_token = getattr(request.app.state, 'SCIM_TOKEN', None)
# Handle both PersistentConfig and direct value
if hasattr(scim_token, "value"):
if hasattr(scim_token, 'value'):
scim_token = scim_token.value
log.debug(f"SCIM token configured: {bool(scim_token)}")
log.debug(f'SCIM token configured: {bool(scim_token)}')
if not scim_token or token != scim_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid SCIM token",
detail='Invalid SCIM token',
)
return True
@@ -293,13 +289,13 @@ def get_scim_auth(
# Re-raise HTTP exceptions as-is
raise
except Exception as e:
log.error(f"SCIM authentication error: {e}")
log.error(f'SCIM authentication error: {e}')
import traceback
log.error(f"Traceback: {traceback.format_exc()}")
log.error(f'Traceback: {traceback.format_exc()}')
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication failed",
detail='Authentication failed',
)
@@ -311,8 +307,8 @@ def get_external_id(user: UserModel) -> Optional[str]:
if not user.scim:
return None
for provider_data in user.scim.values():
if isinstance(provider_data, dict) and "external_id" in provider_data:
return provider_data["external_id"]
if isinstance(provider_data, dict) and 'external_id' in provider_data:
return provider_data['external_id']
return None
@@ -324,7 +320,7 @@ def get_scim_provider() -> str:
if not SCIM_AUTH_PROVIDER:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled",
detail='SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled',
)
return SCIM_AUTH_PROVIDER
@@ -343,18 +339,18 @@ def find_user_by_external_id(external_id: str, db=None) -> Optional[UserModel]:
def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
"""Convert internal User model to SCIM User"""
# Parse display name into name components
name_parts = user.name.split(" ", 1) if user.name else ["", ""]
given_name = name_parts[0] if name_parts else ""
family_name = name_parts[1] if len(name_parts) > 1 else ""
name_parts = user.name.split(' ', 1) if user.name else ['', '']
given_name = name_parts[0] if name_parts else ''
family_name = name_parts[1] if len(name_parts) > 1 else ''
# Get user's groups
user_groups = Groups.get_groups_by_member_id(user.id, db=db)
groups = [
{
"value": group.id,
"display": group.name,
"$ref": f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
"type": "direct",
'value': group.id,
'display': group.name,
'$ref': f'{request.base_url}api/v1/scim/v2/Groups/{group.id}',
'type': 'direct',
}
for group in user_groups
]
@@ -370,22 +366,14 @@ def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
),
displayName=user.name,
emails=[SCIMEmail(value=user.email)],
active=user.role != "pending",
photos=(
[SCIMPhoto(value=user.profile_image_url)]
if user.profile_image_url
else None
),
active=user.role != 'pending',
photos=([SCIMPhoto(value=user.profile_image_url)] if user.profile_image_url else None),
groups=groups if groups else None,
meta=SCIMMeta(
resourceType=SCIM_RESOURCE_TYPE_USER,
created=datetime.fromtimestamp(
user.created_at, tz=timezone.utc
).isoformat(),
lastModified=datetime.fromtimestamp(
user.updated_at, tz=timezone.utc
).isoformat(),
location=f"{request.base_url}api/v1/scim/v2/Users/{user.id}",
created=datetime.fromtimestamp(user.created_at, tz=timezone.utc).isoformat(),
lastModified=datetime.fromtimestamp(user.updated_at, tz=timezone.utc).isoformat(),
location=f'{request.base_url}api/v1/scim/v2/Users/{user.id}',
),
)
@@ -399,7 +387,7 @@ def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup:
members = [
SCIMGroupMember(
value=user.id,
ref=f"{request.base_url}api/v1/scim/v2/Users/{user.id}",
ref=f'{request.base_url}api/v1/scim/v2/Users/{user.id}',
display=user.name,
)
for user in users
@@ -411,108 +399,104 @@ def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup:
members=members,
meta=SCIMMeta(
resourceType=SCIM_RESOURCE_TYPE_GROUP,
created=datetime.fromtimestamp(
group.created_at, tz=timezone.utc
).isoformat(),
lastModified=datetime.fromtimestamp(
group.updated_at, tz=timezone.utc
).isoformat(),
location=f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
created=datetime.fromtimestamp(group.created_at, tz=timezone.utc).isoformat(),
lastModified=datetime.fromtimestamp(group.updated_at, tz=timezone.utc).isoformat(),
location=f'{request.base_url}api/v1/scim/v2/Groups/{group.id}',
),
)
# SCIM Service Provider Config
@router.get("/ServiceProviderConfig")
@router.get('/ServiceProviderConfig')
async def get_service_provider_config():
"""Get SCIM Service Provider Configuration"""
return {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"patch": {"supported": True},
"bulk": {"supported": False, "maxOperations": 1000, "maxPayloadSize": 1048576},
"filter": {"supported": True, "maxResults": 200},
"changePassword": {"supported": False},
"sort": {"supported": False},
"etag": {"supported": False},
"authenticationSchemes": [
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
'patch': {'supported': True},
'bulk': {'supported': False, 'maxOperations': 1000, 'maxPayloadSize': 1048576},
'filter': {'supported': True, 'maxResults': 200},
'changePassword': {'supported': False},
'sort': {'supported': False},
'etag': {'supported': False},
'authenticationSchemes': [
{
"type": "oauthbearertoken",
"name": "OAuth Bearer Token",
"description": "Authentication using OAuth 2.0 Bearer Token",
'type': 'oauthbearertoken',
'name': 'OAuth Bearer Token',
'description': 'Authentication using OAuth 2.0 Bearer Token',
}
],
}
# SCIM Resource Types
@router.get("/ResourceTypes")
@router.get('/ResourceTypes')
async def get_resource_types(request: Request):
"""Get SCIM Resource Types"""
return [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
"id": "User",
"name": "User",
"endpoint": "/Users",
"schema": SCIM_USER_SCHEMA,
"meta": {
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/User",
"resourceType": "ResourceType",
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
'id': 'User',
'name': 'User',
'endpoint': '/Users',
'schema': SCIM_USER_SCHEMA,
'meta': {
'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/User',
'resourceType': 'ResourceType',
},
},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
"id": "Group",
"name": "Group",
"endpoint": "/Groups",
"schema": SCIM_GROUP_SCHEMA,
"meta": {
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/Group",
"resourceType": "ResourceType",
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
'id': 'Group',
'name': 'Group',
'endpoint': '/Groups',
'schema': SCIM_GROUP_SCHEMA,
'meta': {
'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/Group',
'resourceType': 'ResourceType',
},
},
]
# SCIM Schemas
@router.get("/Schemas")
@router.get('/Schemas')
async def get_schemas():
"""Get SCIM Schemas"""
return [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
"id": SCIM_USER_SCHEMA,
"name": "User",
"description": "User Account",
"attributes": [
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
'id': SCIM_USER_SCHEMA,
'name': 'User',
'description': 'User Account',
'attributes': [
{
"name": "userName",
"type": "string",
"required": True,
"uniqueness": "server",
'name': 'userName',
'type': 'string',
'required': True,
'uniqueness': 'server',
},
{"name": "displayName", "type": "string", "required": True},
{'name': 'displayName', 'type': 'string', 'required': True},
{
"name": "emails",
"type": "complex",
"multiValued": True,
"required": True,
'name': 'emails',
'type': 'complex',
'multiValued': True,
'required': True,
},
{"name": "active", "type": "boolean", "required": False},
{'name': 'active', 'type': 'boolean', 'required': False},
],
},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
"id": SCIM_GROUP_SCHEMA,
"name": "Group",
"description": "Group",
"attributes": [
{"name": "displayName", "type": "string", "required": True},
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
'id': SCIM_GROUP_SCHEMA,
'name': 'Group',
'description': 'Group',
'attributes': [
{'name': 'displayName', 'type': 'string', 'required': True},
{
"name": "members",
"type": "complex",
"multiValued": True,
"required": False,
'name': 'members',
'type': 'complex',
'multiValued': True,
'required': False,
},
],
},
@@ -520,7 +504,7 @@ async def get_schemas():
# Users endpoints
@router.get("/Users", response_model=SCIMListResponse)
@router.get('/Users', response_model=SCIMListResponse)
async def get_users(
request: Request,
startIndex: int = Query(1),
@@ -540,24 +524,24 @@ async def get_users(
# Get users from database
if filter:
# Simple filter parsing - supports userName eq, externalId eq
if "userName eq" in filter:
if 'userName eq' in filter:
email = filter.split('"')[1]
user = Users.get_user_by_email(email, db=db)
users_list = [user] if user else []
total = 1 if user else 0
elif "externalId eq" in filter:
elif 'externalId eq' in filter:
external_id = filter.split('"')[1]
user = find_user_by_external_id(external_id, db=db)
users_list = [user] if user else []
total = 1 if user else 0
else:
response = Users.get_users(skip=skip, limit=limit, db=db)
users_list = response["users"]
total = response["total"]
users_list = response['users']
total = response['total']
else:
response = Users.get_users(skip=skip, limit=limit, db=db)
users_list = response["users"]
total = response["total"]
users_list = response['users']
total = response['total']
# Convert to SCIM format
scim_users = [user_to_scim(user, request, db=db) for user in users_list]
@@ -570,7 +554,7 @@ async def get_users(
)
@router.get("/Users/{user_id}", response_model=SCIMUser)
@router.get('/Users/{user_id}', response_model=SCIMUser)
async def get_user(
user_id: str,
request: Request,
@@ -580,14 +564,12 @@ async def get_user(
"""Get SCIM User by ID"""
user = Users.get_user_by_id(user_id, db=db)
if not user:
return scim_error(
status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found"
)
return scim_error(status_code=status.HTTP_404_NOT_FOUND, detail=f'User {user_id} not found')
return user_to_scim(user, request, db=db)
@router.post("/Users", response_model=SCIMUser, status_code=status.HTTP_201_CREATED)
@router.post('/Users', response_model=SCIMUser, status_code=status.HTTP_201_CREATED)
async def create_user(
request: Request,
user_data: SCIMUserCreateRequest,
@@ -601,7 +583,7 @@ async def create_user(
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"User with externalId {user_data.externalId} already exists",
detail=f'User with externalId {user_data.externalId} already exists',
)
# Determine primary email (lowercased per RFC 5321)
@@ -617,7 +599,7 @@ async def create_user(
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"User with email {email} already exists",
detail=f'User with email {email} already exists',
)
# Create user
@@ -629,10 +611,10 @@ async def create_user(
if user_data.name.formatted:
name = user_data.name.formatted
elif user_data.name.givenName or user_data.name.familyName:
name = f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip()
name = f'{user_data.name.givenName or ""} {user_data.name.familyName or ""}'.strip()
# Get profile image if provided
profile_image = "/user.png"
profile_image = '/user.png'
if user_data.photos and len(user_data.photos) > 0:
profile_image = user_data.photos[0].value
@@ -641,14 +623,14 @@ async def create_user(
name=name,
email=email,
profile_image_url=profile_image,
role="user" if user_data.active else "pending",
role='user' if user_data.active else 'pending',
db=db,
)
if not new_user:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user",
detail='Failed to create user',
)
# Store externalId in the scim field
@@ -660,7 +642,7 @@ async def create_user(
return user_to_scim(new_user, request, db=db)
@router.put("/Users/{user_id}", response_model=SCIMUser)
@router.put('/Users/{user_id}', response_model=SCIMUser)
async def update_user(
user_id: str,
request: Request,
@@ -673,39 +655,37 @@ async def update_user(
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} not found",
detail=f'User {user_id} not found',
)
# Build update dict
update_data = {}
if user_data.userName:
update_data["email"] = user_data.userName
update_data['email'] = user_data.userName
if user_data.displayName:
update_data["name"] = user_data.displayName
update_data['name'] = user_data.displayName
elif user_data.name:
if user_data.name.formatted:
update_data["name"] = user_data.name.formatted
update_data['name'] = user_data.name.formatted
elif user_data.name.givenName or user_data.name.familyName:
update_data["name"] = (
f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip()
)
update_data['name'] = f'{user_data.name.givenName or ""} {user_data.name.familyName or ""}'.strip()
if user_data.emails and len(user_data.emails) > 0:
update_data["email"] = user_data.emails[0].value
update_data['email'] = user_data.emails[0].value
if user_data.active is not None:
update_data["role"] = "user" if user_data.active else "pending"
update_data['role'] = 'user' if user_data.active else 'pending'
if user_data.photos and len(user_data.photos) > 0:
update_data["profile_image_url"] = user_data.photos[0].value
update_data['profile_image_url'] = user_data.photos[0].value
updated_user = Users.update_user_by_id(user_id, update_data, db=db)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user",
detail='Failed to update user',
)
# Update externalId in the scim field
@@ -717,7 +697,7 @@ async def update_user(
return user_to_scim(updated_user, request, db=db)
@router.patch("/Users/{user_id}", response_model=SCIMUser)
@router.patch('/Users/{user_id}', response_model=SCIMUser)
async def patch_user(
user_id: str,
request: Request,
@@ -730,7 +710,7 @@ async def patch_user(
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} not found",
detail=f'User {user_id} not found',
)
update_data = {}
@@ -740,18 +720,18 @@ async def patch_user(
path = operation.path
value = operation.value
if op == "replace":
if path == "active":
update_data["role"] = "user" if value else "pending"
elif path == "userName":
update_data["email"] = value
elif path == "displayName":
update_data["name"] = value
elif path == "emails[primary eq true].value":
update_data["email"] = value
elif path == "name.formatted":
update_data["name"] = value
elif path == "externalId":
if op == 'replace':
if path == 'active':
update_data['role'] = 'user' if value else 'pending'
elif path == 'userName':
update_data['email'] = value
elif path == 'displayName':
update_data['name'] = value
elif path == 'emails[primary eq true].value':
update_data['email'] = value
elif path == 'name.formatted':
update_data['name'] = value
elif path == 'externalId':
provider = get_scim_provider()
Users.update_user_scim_by_id(user_id, provider, value, db=db)
@@ -761,7 +741,7 @@ async def patch_user(
if not updated_user:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user",
detail='Failed to update user',
)
else:
updated_user = user
@@ -769,7 +749,7 @@ async def patch_user(
return user_to_scim(updated_user, request, db=db)
@router.delete("/Users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete('/Users/{user_id}', status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: str,
request: Request,
@@ -781,21 +761,21 @@ async def delete_user(
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} not found",
detail=f'User {user_id} not found',
)
success = Users.delete_user_by_id(user_id, db=db)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete user",
detail='Failed to delete user',
)
return None
# Groups endpoints
@router.get("/Groups", response_model=SCIMListResponse)
@router.get('/Groups', response_model=SCIMListResponse)
async def get_groups(
request: Request,
startIndex: int = Query(1),
@@ -830,7 +810,7 @@ async def get_groups(
)
@router.get("/Groups/{group_id}", response_model=SCIMGroup)
@router.get('/Groups/{group_id}', response_model=SCIMGroup)
async def get_group(
group_id: str,
request: Request,
@@ -842,13 +822,13 @@ async def get_group(
if not group:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Group {group_id} not found",
detail=f'Group {group_id} not found',
)
return group_to_scim(group, request, db=db)
@router.post("/Groups", response_model=SCIMGroup, status_code=status.HTTP_201_CREATED)
@router.post('/Groups', response_model=SCIMGroup, status_code=status.HTTP_201_CREATED)
async def create_group(
request: Request,
group_data: SCIMGroupCreateRequest,
@@ -867,7 +847,7 @@ async def create_group(
form = GroupForm(
name=group_data.displayName,
description="",
description='',
)
# Need to get the creating user's ID - we'll use the first admin
@@ -875,14 +855,14 @@ async def create_group(
if not admin_user:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="No admin user found",
detail='No admin user found',
)
new_group = Groups.insert_new_group(admin_user.id, form, db=db)
if not new_group:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create group",
detail='Failed to create group',
)
# Add members if provided
@@ -902,7 +882,7 @@ async def create_group(
return group_to_scim(new_group, request, db=db)
@router.put("/Groups/{group_id}", response_model=SCIMGroup)
@router.put('/Groups/{group_id}', response_model=SCIMGroup)
async def update_group(
group_id: str,
request: Request,
@@ -915,7 +895,7 @@ async def update_group(
if not group:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Group {group_id} not found",
detail=f'Group {group_id} not found',
)
# Build update form
@@ -936,13 +916,13 @@ async def update_group(
if not updated_group:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update group",
detail='Failed to update group',
)
return group_to_scim(updated_group, request, db=db)
@router.patch("/Groups/{group_id}", response_model=SCIMGroup)
@router.patch('/Groups/{group_id}', response_model=SCIMGroup)
async def patch_group(
group_id: str,
request: Request,
@@ -955,7 +935,7 @@ async def patch_group(
if not group:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Group {group_id} not found",
detail=f'Group {group_id} not found',
)
from open_webui.models.groups import GroupUpdateForm
@@ -970,26 +950,22 @@ async def patch_group(
path = operation.path
value = operation.value
if op == "replace":
if path == "displayName":
if op == 'replace':
if path == 'displayName':
update_form.name = value
elif path == "members":
elif path == 'members':
# Replace all members
Groups.set_group_user_ids_by_id(
group_id, [member["value"] for member in value], db=db
)
Groups.set_group_user_ids_by_id(group_id, [member['value'] for member in value], db=db)
elif op == "add":
if path == "members":
elif op == 'add':
if path == 'members':
# Add members
if isinstance(value, list):
for member in value:
if isinstance(member, dict) and "value" in member:
Groups.add_users_to_group(
group_id, [member["value"]], db=db
)
elif op == "remove":
if path and path.startswith("members[value eq"):
if isinstance(member, dict) and 'value' in member:
Groups.add_users_to_group(group_id, [member['value']], db=db)
elif op == 'remove':
if path and path.startswith('members[value eq'):
# Remove specific member
member_id = path.split('"')[1]
Groups.remove_users_from_group(group_id, [member_id], db=db)
@@ -999,13 +975,13 @@ async def patch_group(
if not updated_group:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update group",
detail='Failed to update group',
)
return group_to_scim(updated_group, request, db=db)
@router.delete("/Groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete('/Groups/{group_id}', status_code=status.HTTP_204_NO_CONTENT)
async def delete_group(
group_id: str,
request: Request,
@@ -1017,14 +993,14 @@ async def delete_group(
if not group:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Group {group_id} not found",
detail=f'Group {group_id} not found',
)
success = Groups.delete_group_by_id(group_id, db=db)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete group",
detail='Failed to delete group',
)
return None
+55 -61
View File
@@ -36,18 +36,16 @@ router = APIRouter()
############################
@router.get("/", response_model=list[SkillUserResponse])
@router.get('/', response_model=list[SkillUserResponse])
async def get_skills(
request: Request,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
skills = Skills.get_skills(db=db)
else:
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
}
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
all_skills = Skills.get_skills(db=db)
skills = [
skill
@@ -55,9 +53,9 @@ async def get_skills(
if skill.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="read",
permission='read',
user_group_ids=user_group_ids,
db=db,
)
@@ -71,7 +69,7 @@ async def get_skills(
############################
@router.get("/list", response_model=SkillAccessListResponse)
@router.get('/list', response_model=SkillAccessListResponse)
async def get_skill_list(
query: Optional[str] = None,
view_option: Optional[str] = None,
@@ -86,16 +84,16 @@ async def get_skill_list(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if view_option:
filter["view_option"] = view_option
filter['view_option'] = view_option
if not (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL):
if not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL):
groups = Groups.get_groups_by_member_id(user.id, db=db)
if groups:
filter["group_ids"] = [group.id for group in groups]
filter['group_ids'] = [group.id for group in groups]
filter["user_id"] = user.id
filter['user_id'] = user.id
result = Skills.search_skills(user.id, filter=filter, skip=skip, limit=limit, db=db)
@@ -104,13 +102,13 @@ async def get_skill_list(
SkillAccessResponse(
**skill.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == skill.user_id
or AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="write",
permission='write',
db=db,
)
),
@@ -126,15 +124,15 @@ async def get_skill_list(
############################
@router.get("/export", response_model=list[SkillModel])
@router.get('/export', response_model=list[SkillModel])
async def export_skills(
request: Request,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
if user.role != 'admin' and not has_permission(
user.id,
"workspace.skills",
'workspace.skills',
request.app.state.config.USER_PERMISSIONS,
db=db,
):
@@ -143,10 +141,10 @@ async def export_skills(
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
return Skills.get_skills(db=db)
else:
return Skills.get_skills_by_user_id(user.id, "read", db=db)
return Skills.get_skills_by_user_id(user.id, 'read', db=db)
############################
@@ -154,22 +152,22 @@ async def export_skills(
############################
@router.post("/create", response_model=Optional[SkillResponse])
@router.post('/create', response_model=Optional[SkillResponse])
async def create_new_skill(
request: Request,
form_data: SkillForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
user.id, "workspace.skills", request.app.state.config.USER_PERMISSIONS, db=db
if user.role != 'admin' and not has_permission(
user.id, 'workspace.skills', request.app.state.config.USER_PERMISSIONS, db=db
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
form_data.id = form_data.id.lower().replace(" ", "-")
form_data.id = form_data.id.lower().replace(' ', '-')
existing = Skills.get_skill_by_id(form_data.id, db=db)
if existing is not None:
@@ -185,10 +183,10 @@ async def create_new_skill(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating skill"),
detail=ERROR_MESSAGES.DEFAULT('Error creating skill'),
)
except Exception as e:
log.exception(f"Failed to create skill: {e}")
log.exception(f'Failed to create skill: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -200,34 +198,32 @@ async def create_new_skill(
############################
@router.get("/id/{id}", response_model=Optional[SkillAccessResponse])
async def get_skill_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}', response_model=Optional[SkillAccessResponse])
async def get_skill_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
skill = Skills.get_skill_by_id(id, db=db)
if skill:
if (
user.role == "admin"
user.role == 'admin'
or skill.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="read",
permission='read',
db=db,
)
):
return SkillAccessResponse(
**skill.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == skill.user_id
or AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="write",
permission='write',
db=db,
)
),
@@ -249,7 +245,7 @@ async def get_skill_by_id(
############################
@router.post("/id/{id}/update", response_model=Optional[SkillModel])
@router.post('/id/{id}/update', response_model=Optional[SkillModel])
async def update_skill_by_id(
request: Request,
id: str,
@@ -268,12 +264,12 @@ async def update_skill_by_id(
skill.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -282,7 +278,7 @@ async def update_skill_by_id(
try:
updated = {
**form_data.model_dump(exclude={"id"}),
**form_data.model_dump(exclude={'id'}),
}
skill = Skills.update_skill_by_id(id, updated, db=db)
@@ -292,7 +288,7 @@ async def update_skill_by_id(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating skill"),
detail=ERROR_MESSAGES.DEFAULT('Error updating skill'),
)
except Exception as e:
raise HTTPException(
@@ -310,7 +306,7 @@ class SkillAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post("/id/{id}/access/update", response_model=Optional[SkillModel])
@router.post('/id/{id}/access/update', response_model=Optional[SkillModel])
async def update_skill_access_by_id(
request: Request,
id: str,
@@ -329,12 +325,12 @@ async def update_skill_access_by_id(
skill.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -346,10 +342,10 @@ async def update_skill_access_by_id(
user.id,
user.role,
form_data.access_grants,
"sharing.public_skills",
'sharing.public_skills',
)
AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db)
AccessGrants.set_access_grants('skill', id, form_data.access_grants, db=db)
return Skills.get_skill_by_id(id, db=db)
@@ -359,20 +355,18 @@ async def update_skill_access_by_id(
############################
@router.post("/id/{id}/toggle", response_model=Optional[SkillModel])
async def toggle_skill_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.post('/id/{id}/toggle', response_model=Optional[SkillModel])
async def toggle_skill_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
skill = Skills.get_skill_by_id(id, db=db)
if skill:
if (
user.role == "admin"
user.role == 'admin'
or skill.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="write",
permission='write',
db=db,
)
):
@@ -383,7 +377,7 @@ async def toggle_skill_by_id(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error toggling skill"),
detail=ERROR_MESSAGES.DEFAULT('Error toggling skill'),
)
else:
raise HTTPException(
@@ -402,7 +396,7 @@ async def toggle_skill_by_id(
############################
@router.delete("/id/{id}/delete", response_model=bool)
@router.delete('/id/{id}/delete', response_model=bool)
async def delete_skill_by_id(
request: Request,
id: str,
@@ -420,12 +414,12 @@ async def delete_skill_by_id(
skill.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="skill",
resource_type='skill',
resource_id=skill.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
+218 -283
View File
@@ -52,36 +52,34 @@ class ActiveChatsForm(BaseModel):
chat_ids: list[str]
@router.post("/active/chats")
async def check_active_chats(
request: Request, form_data: ActiveChatsForm, user=Depends(get_verified_user)
):
@router.post('/active/chats')
async def check_active_chats(request: Request, form_data: ActiveChatsForm, user=Depends(get_verified_user)):
"""Check which chat IDs have active tasks."""
from open_webui.tasks import get_active_chat_ids
active = await get_active_chat_ids(request.app.state.redis, form_data.chat_ids)
return {"active_chat_ids": active}
return {'active_chat_ids': active}
@router.get("/config")
@router.get('/config')
async def get_task_config(request: Request, user=Depends(get_verified_user)):
return {
"TASK_MODEL": request.app.state.config.TASK_MODEL,
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
"FOLLOW_UP_GENERATION_PROMPT_TEMPLATE": request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
"ENABLE_FOLLOW_UP_GENERATION": request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
"ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
"QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
"VOICE_MODE_PROMPT_TEMPLATE": request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE,
'TASK_MODEL': request.app.state.config.TASK_MODEL,
'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL,
'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION,
'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION,
'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE,
}
@@ -104,100 +102,73 @@ class TaskConfigForm(BaseModel):
VOICE_MODE_PROMPT_TEMPLATE: Optional[str]
@router.post("/config/update")
async def update_task_config(
request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user)
):
@router.post('/config/update')
async def update_task_config(request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user)):
request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION
request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = form_data.TITLE_GENERATION_PROMPT_TEMPLATE
request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = (
form_data.ENABLE_FOLLOW_UP_GENERATION
)
request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = (
form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = form_data.ENABLE_FOLLOW_UP_GENERATION
request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = (
form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = (
form_data.ENABLE_AUTOCOMPLETE_GENERATION
)
request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = form_data.ENABLE_AUTOCOMPLETE_GENERATION
request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = (
form_data.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
)
request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = (
form_data.TAGS_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = form_data.TAGS_GENERATION_PROMPT_TEMPLATE
request.app.state.config.ENABLE_TAGS_GENERATION = form_data.ENABLE_TAGS_GENERATION
request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION = (
form_data.ENABLE_SEARCH_QUERY_GENERATION
)
request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = (
form_data.ENABLE_RETRIEVAL_QUERY_GENERATION
)
request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION = form_data.ENABLE_SEARCH_QUERY_GENERATION
request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = form_data.ENABLE_RETRIEVAL_QUERY_GENERATION
request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = (
form_data.QUERY_GENERATION_PROMPT_TEMPLATE
)
request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
)
request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = form_data.QUERY_GENERATION_PROMPT_TEMPLATE
request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = (
form_data.VOICE_MODE_PROMPT_TEMPLATE
)
request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = form_data.VOICE_MODE_PROMPT_TEMPLATE
return {
"TASK_MODEL": request.app.state.config.TASK_MODEL,
"TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
"ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
"TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
"IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
"ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
"TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
"ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
"ENABLE_FOLLOW_UP_GENERATION": request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
"FOLLOW_UP_GENERATION_PROMPT_TEMPLATE": request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
"ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
"ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
"QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
"VOICE_MODE_PROMPT_TEMPLATE": request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE,
'TASK_MODEL': request.app.state.config.TASK_MODEL,
'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL,
'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION,
'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION,
'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION,
'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE,
'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE,
}
@router.post("/title/completions")
async def generate_title(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
@router.post('/title/completions')
async def generate_title(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_TITLE_GENERATION:
return JSONResponse(
status_code=status.HTTP_200_OK,
content={"detail": "Title generation is disabled"},
content={'detail': 'Title generation is disabled'},
)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -209,37 +180,33 @@ async def generate_title(
models,
)
log.debug(
f"generating chat title using model {task_model_id} for user {user.email} "
)
log.debug(f'generating chat title using model {task_model_id} for user {user.email} ')
if request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
if request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
content = title_generation_template(template, form_data["messages"], user)
content = title_generation_template(template, form_data['messages'], user)
max_tokens = (
models[task_model_id].get("info", {}).get("params", {}).get("max_tokens", 1000)
)
max_tokens = models[task_model_id].get('info', {}).get('params', {}).get('max_tokens', 1000)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
**(
{"max_tokens": max_tokens}
if models[task_model_id].get("owned_by") == "ollama"
{'max_tokens': max_tokens}
if models[task_model_id].get('owned_by') == 'ollama'
else {
"max_completion_tokens": max_tokens,
'max_completion_tokens': max_tokens,
}
),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.TITLE_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.TITLE_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -252,36 +219,33 @@ async def generate_title(
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error("Exception occurred", exc_info=True)
log.error('Exception occurred', exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "An internal error has occurred."},
content={'detail': 'An internal error has occurred.'},
)
@router.post("/follow_up/completions")
async def generate_follow_ups(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
@router.post('/follow_up/completions')
async def generate_follow_ups(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_FOLLOW_UP_GENERATION:
return JSONResponse(
status_code=status.HTTP_200_OK,
content={"detail": "Follow-up generation is disabled"},
content={'detail': 'Follow-up generation is disabled'},
)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -293,26 +257,24 @@ async def generate_follow_ups(
models,
)
log.debug(
f"generating chat title using model {task_model_id} for user {user.email} "
)
log.debug(f'generating chat title using model {task_model_id} for user {user.email} ')
if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != "":
if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE
content = follow_up_generation_template(template, form_data["messages"], user)
content = follow_up_generation_template(template, form_data['messages'], user)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.FOLLOW_UP_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.FOLLOW_UP_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -325,36 +287,33 @@ async def generate_follow_ups(
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error("Exception occurred", exc_info=True)
log.error('Exception occurred', exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "An internal error has occurred."},
content={'detail': 'An internal error has occurred.'},
)
@router.post("/tags/completions")
async def generate_chat_tags(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
@router.post('/tags/completions')
async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_TAGS_GENERATION:
return JSONResponse(
status_code=status.HTTP_200_OK,
content={"detail": "Tags generation is disabled"},
content={'detail': 'Tags generation is disabled'},
)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -366,26 +325,24 @@ async def generate_chat_tags(
models,
)
log.debug(
f"generating chat tags using model {task_model_id} for user {user.email} "
)
log.debug(f'generating chat tags using model {task_model_id} for user {user.email} ')
if request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "":
if request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE
content = tags_generation_template(template, form_data["messages"], user)
content = tags_generation_template(template, form_data['messages'], user)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.TAGS_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.TAGS_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -398,29 +355,27 @@ async def generate_chat_tags(
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error(f"Error generating chat completion: {e}")
log.error(f'Error generating chat completion: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "An internal error has occurred."},
content={'detail': 'An internal error has occurred.'},
)
@router.post("/image_prompt/completions")
async def generate_image_prompt(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
@router.post('/image_prompt/completions')
async def generate_image_prompt(request: Request, form_data: dict, user=Depends(get_verified_user)):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -432,26 +387,24 @@ async def generate_image_prompt(
models,
)
log.debug(
f"generating image prompt using model {task_model_id} for user {user.email} "
)
log.debug(f'generating image prompt using model {task_model_id} for user {user.email} ')
if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != "":
if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != '':
template = request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
content = image_prompt_generation_template(template, form_data["messages"], user)
content = image_prompt_generation_template(template, form_data['messages'], user)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.IMAGE_PROMPT_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.IMAGE_PROMPT_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -464,48 +417,45 @@ async def generate_image_prompt(
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error("Exception occurred", exc_info=True)
log.error('Exception occurred', exc_info=True)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "An internal error has occurred."},
content={'detail': 'An internal error has occurred.'},
)
@router.post("/queries/completions")
async def generate_queries(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
type = form_data.get("type")
if type == "web_search":
@router.post('/queries/completions')
async def generate_queries(request: Request, form_data: dict, user=Depends(get_verified_user)):
type = form_data.get('type')
if type == 'web_search':
if not request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Search query generation is disabled",
detail=f'Search query generation is disabled',
)
elif type == "retrieval":
elif type == 'retrieval':
if not request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Query generation is disabled",
detail=f'Query generation is disabled',
)
if getattr(request.state, "cached_queries", None):
log.info(f"Reusing cached queries: {request.state.cached_queries}")
if getattr(request.state, 'cached_queries', None):
log.info(f'Reusing cached queries: {request.state.cached_queries}')
return request.state.cached_queries
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -517,26 +467,24 @@ async def generate_queries(
models,
)
log.debug(
f"generating {type} queries using model {task_model_id} for user {user.email}"
)
log.debug(f'generating {type} queries using model {task_model_id} for user {user.email}')
if (request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != "":
if (request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != '':
template = request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE
content = query_generation_template(template, form_data["messages"], user)
content = query_generation_template(template, form_data['messages'], user)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.QUERY_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.QUERY_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -551,46 +499,41 @@ async def generate_queries(
except Exception as e:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
content={'detail': str(e)},
)
@router.post("/auto/completions")
async def generate_autocompletion(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
@router.post('/auto/completions')
async def generate_autocompletion(request: Request, form_data: dict, user=Depends(get_verified_user)):
if not request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Autocompletion generation is disabled",
detail=f'Autocompletion generation is disabled',
)
type = form_data.get("type")
prompt = form_data.get("prompt")
messages = form_data.get("messages")
type = form_data.get('type')
prompt = form_data.get('prompt')
messages = form_data.get('messages')
if request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH > 0:
if (
len(prompt)
> request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
):
if len(prompt) > request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Input prompt exceeds maximum length of {request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}",
detail=f'Input prompt exceeds maximum length of {request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}',
)
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -602,11 +545,9 @@ async def generate_autocompletion(
models,
)
log.debug(
f"generating autocompletion using model {task_model_id} for user {user.email}"
)
log.debug(f'generating autocompletion using model {task_model_id} for user {user.email}')
if (request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != "":
if (request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != '':
template = request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
else:
template = DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
@@ -614,14 +555,14 @@ async def generate_autocompletion(
content = autocomplete_generation_template(template, prompt, messages, type, user)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.AUTOCOMPLETE_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.AUTOCOMPLETE_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -634,30 +575,27 @@ async def generate_autocompletion(
try:
return await generate_chat_completion(request, form_data=payload, user=user)
except Exception as e:
log.error(f"Error generating chat completion: {e}")
log.error(f'Error generating chat completion: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "An internal error has occurred."},
content={'detail': 'An internal error has occurred.'},
)
@router.post("/emoji/completions")
async def generate_emoji(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
@router.post('/emoji/completions')
async def generate_emoji(request: Request, form_data: dict, user=Depends(get_verified_user)):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
# Check if the user has a custom task model
@@ -669,28 +607,28 @@ async def generate_emoji(
models,
)
log.debug(f"generating emoji using model {task_model_id} for user {user.email} ")
log.debug(f'generating emoji using model {task_model_id} for user {user.email} ')
template = DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE
content = emoji_generation_template(template, form_data["prompt"], user)
content = emoji_generation_template(template, form_data['prompt'], user)
payload = {
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
'model': task_model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': False,
**(
{"max_tokens": 4}
if models[task_model_id].get("owned_by") == "ollama"
{'max_tokens': 4}
if models[task_model_id].get('owned_by') == 'ollama'
else {
"max_completion_tokens": 4,
'max_completion_tokens': 4,
}
),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.EMOJI_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'task': str(TASKS.EMOJI_GENERATION),
'task_body': form_data,
'chat_id': form_data.get('chat_id', None),
},
}
@@ -705,47 +643,44 @@ async def generate_emoji(
except Exception as e:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
content={'detail': str(e)},
)
@router.post("/moa/completions")
async def generate_moa_response(
request: Request, form_data: dict, user=Depends(get_verified_user)
):
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
@router.post('/moa/completions')
async def generate_moa_response(request: Request, form_data: dict, user=Depends(get_verified_user)):
if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'):
models = {
request.state.model["id"]: request.state.model,
request.state.model['id']: request.state.model,
}
else:
models = request.app.state.MODELS
model_id = form_data["model"]
model_id = form_data['model']
if model_id not in models:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Model not found",
detail='Model not found',
)
template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE
content = moa_response_generation_template(
template,
form_data["prompt"],
form_data["responses"],
form_data['prompt'],
form_data['responses'],
)
payload = {
"model": model_id,
"messages": [{"role": "user", "content": content}],
"stream": form_data.get("stream", False),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"chat_id": form_data.get("chat_id", None),
"task": str(TASKS.MOA_RESPONSE_GENERATION),
"task_body": form_data,
'model': model_id,
'messages': [{'role': 'user', 'content': content}],
'stream': form_data.get('stream', False),
'metadata': {
**(request.state.metadata if hasattr(request.state, 'metadata') else {}),
'chat_id': form_data.get('chat_id', None),
'task': str(TASKS.MOA_RESPONSE_GENERATION),
'task_body': form_data,
},
}
@@ -760,5 +695,5 @@ async def generate_moa_response(
except Exception as e:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
content={'detail': str(e)},
)
+67 -78
View File
@@ -23,10 +23,8 @@ log = logging.getLogger(__name__)
router = APIRouter()
STREAMING_CONTENT_TYPES = ("application/octet-stream", "image/", "application/pdf")
STRIPPED_RESPONSE_HEADERS = frozenset(
("transfer-encoding", "connection", "content-encoding", "content-length")
)
STREAMING_CONTENT_TYPES = ('application/octet-stream', 'image/', 'application/pdf')
STRIPPED_RESPONSE_HEADERS = frozenset(('transfer-encoding', 'connection', 'content-encoding', 'content-length'))
def _sanitize_proxy_path(path: str) -> str | None:
@@ -37,14 +35,14 @@ def _sanitize_proxy_path(path: str) -> str | None:
decoded = unquote(path)
normalized = posixpath.normpath(decoded)
# Remove any leading slashes that would reset the base
cleaned = normalized.lstrip("/")
cleaned = normalized.lstrip('/')
# Reject if normpath resolved to parent traversal or current-dir only
if cleaned.startswith("..") or cleaned == ".":
if cleaned.startswith('..') or cleaned == '.':
return None
return cleaned
@router.get("/")
@router.get('/')
async def list_terminal_servers(request: Request, user=Depends(get_verified_user)):
"""Return terminal servers the authenticated user has access to."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
@@ -52,20 +50,19 @@ async def list_terminal_servers(request: Request, user=Depends(get_verified_user
return [
{
"id": connection.get("id", ""),
"url": connection.get("url", ""),
"name": connection.get("name", ""),
'id': connection.get('id', ''),
'url': connection.get('url', ''),
'name': connection.get('name', ''),
}
for connection in connections
if connection.get("enabled", True)
and has_connection_access(user, connection, user_group_ids)
if connection.get('enabled', True) and has_connection_access(user, connection, user_group_ids)
]
PROXY_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
PROXY_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
@router.api_route("/{server_id}/{path:path}", methods=PROXY_METHODS)
@router.api_route('/{server_id}/{path:path}', methods=PROXY_METHODS)
async def proxy_terminal(
server_id: str,
path: str,
@@ -74,56 +71,52 @@ async def proxy_terminal(
):
"""Proxy a request to the admin terminal server identified by *server_id*."""
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connection = next((c for c in connections if c.get("id") == server_id), None)
connection = next((c for c in connections if c.get('id') == server_id), None)
if connection is None:
return JSONResponse(
{"error": f"Terminal server '{server_id}' not found"}, status_code=404
)
return JSONResponse({'error': f"Terminal server '{server_id}' not found"}, status_code=404)
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
if not has_connection_access(user, connection, user_group_ids):
return JSONResponse({"error": "Access denied"}, status_code=403)
return JSONResponse({'error': 'Access denied'}, status_code=403)
base_url = (connection.get("url") or "").rstrip("/")
base_url = (connection.get('url') or '').rstrip('/')
if not base_url:
return JSONResponse(
{"error": "Terminal server URL not configured"}, status_code=503
)
return JSONResponse({'error': 'Terminal server URL not configured'}, status_code=503)
safe_path = _sanitize_proxy_path(path)
if safe_path is None:
return JSONResponse({"error": "Invalid path"}, status_code=400)
return JSONResponse({'error': 'Invalid path'}, status_code=400)
target_url = f"{base_url}/{safe_path}"
target_url = f'{base_url}/{safe_path}'
# Route through orchestrator policy endpoint if policy_id is set
policy_id = connection.get("policy_id")
policy_id = connection.get('policy_id')
if policy_id:
target_url = f"{base_url}/p/{policy_id}/{safe_path}"
target_url = f'{base_url}/p/{policy_id}/{safe_path}'
if request.query_params:
target_url += f"?{request.query_params}"
target_url += f'?{request.query_params}'
headers = {"X-User-Id": user.id}
headers = {'X-User-Id': user.id}
cookies = {}
auth_type = connection.get("auth_type", "bearer")
auth_type = connection.get('auth_type', 'bearer')
if auth_type == "bearer":
headers["Authorization"] = f"Bearer {connection.get('key', '')}"
elif auth_type == "session":
if auth_type == 'bearer':
headers['Authorization'] = f'Bearer {connection.get("key", "")}'
elif auth_type == 'session':
cookies = request.cookies
headers["Authorization"] = f"Bearer {request.state.token.credentials}"
elif auth_type == "system_oauth":
headers['Authorization'] = f'Bearer {request.state.token.credentials}'
elif auth_type == 'system_oauth':
cookies = request.cookies
oauth_token = request.headers.get("x-oauth-access-token", "")
oauth_token = request.headers.get('x-oauth-access-token', '')
if oauth_token:
headers["Authorization"] = f"Bearer {oauth_token}"
headers['Authorization'] = f'Bearer {oauth_token}'
# auth_type == "none": no Authorization header
content_type = request.headers.get("content-type")
content_type = request.headers.get('content-type')
if content_type:
headers["Content-Type"] = content_type
headers['Content-Type'] = content_type
body = await request.body()
session = aiohttp.ClientSession(
@@ -140,7 +133,7 @@ async def proxy_terminal(
data=body or None,
)
upstream_content_type = upstream_response.headers.get("content-type", "")
upstream_content_type = upstream_response.headers.get('content-type', '')
filtered_headers = {
key: value
for key, value in upstream_response.headers.items()
@@ -167,16 +160,12 @@ async def proxy_terminal(
await upstream_response.release()
await session.close()
return Response(
content=response_body, status_code=status_code, headers=filtered_headers
)
return Response(content=response_body, status_code=status_code, headers=filtered_headers)
except Exception as error:
await session.close()
log.exception("Terminal proxy error: %s", error)
return JSONResponse(
{"error": f"Terminal proxy error: {error}"}, status_code=502
)
log.exception('Terminal proxy error: %s', error)
return JSONResponse({'error': f'Terminal proxy error: {error}'}, status_code=502)
# ---------------------------------------------------------------------------
@@ -201,42 +190,42 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
try:
raw = await asyncio.wait_for(ws.receive_text(), timeout=10.0)
payload = json.loads(raw)
if payload.get("type") != "auth":
await ws.close(code=4001, reason="Expected auth message")
if payload.get('type') != 'auth':
await ws.close(code=4001, reason='Expected auth message')
return None
token = payload.get("token", "")
token = payload.get('token', '')
data = decode_token(token)
if data is None or "id" not in data:
await ws.close(code=4001, reason="Invalid token")
if data is None or 'id' not in data:
await ws.close(code=4001, reason='Invalid token')
return None
user = Users.get_user_by_id(data["id"])
user = Users.get_user_by_id(data['id'])
if user is None:
await ws.close(code=4001, reason="User not found")
await ws.close(code=4001, reason='User not found')
return None
except (asyncio.TimeoutError, json.JSONDecodeError):
await ws.close(code=4001, reason="Auth timeout or invalid payload")
await ws.close(code=4001, reason='Auth timeout or invalid payload')
return None
except Exception:
await ws.close(code=4001, reason="Invalid token")
await ws.close(code=4001, reason='Invalid token')
return None
# Resolve terminal server
connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
connection = next((c for c in connections if c.get("id") == server_id), None)
connection = next((c for c in connections if c.get('id') == server_id), None)
if connection is None:
await ws.close(code=4004, reason="Terminal server not found")
await ws.close(code=4004, reason='Terminal server not found')
return None
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
if not has_connection_access(user, connection, user_group_ids):
await ws.close(code=4003, reason="Access denied")
await ws.close(code=4003, reason='Access denied')
return None
return user, connection
@router.websocket("/{server_id}/api/terminals/{session_id}")
@router.websocket('/{server_id}/api/terminals/{session_id}')
async def ws_terminal(
ws: WebSocket,
server_id: str,
@@ -255,28 +244,28 @@ async def ws_terminal(
return
user, connection = result
base_url = (connection.get("url") or "").rstrip("/")
base_url = (connection.get('url') or '').rstrip('/')
if not base_url:
await ws.close(code=4003, reason="Terminal server URL not configured")
await ws.close(code=4003, reason='Terminal server URL not configured')
return
# Build upstream WebSocket URL (no token in URL)
ws_base = base_url.replace("https://", "wss://").replace("http://", "ws://")
ws_base = base_url.replace('https://', 'wss://').replace('http://', 'ws://')
# Route through orchestrator policy endpoint if policy_id is set
policy_id = connection.get("policy_id")
policy_id = connection.get('policy_id')
upstream_params = {}
# For orchestrator-backed servers, pass user_id
upstream_params["user_id"] = user.id
upstream_params['user_id'] = user.id
import urllib.parse
if policy_id:
upstream_url = f"{ws_base}/p/{policy_id}/api/terminals/{session_id}"
upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}'
else:
upstream_url = f"{ws_base}/api/terminals/{session_id}"
upstream_url = f'{ws_base}/api/terminals/{session_id}'
if upstream_params:
upstream_url += f"?{urllib.parse.urlencode(upstream_params)}"
upstream_url += f'?{urllib.parse.urlencode(upstream_params)}'
session = aiohttp.ClientSession()
try:
@@ -285,22 +274,22 @@ async def ws_terminal(
import json as _json
# First-message auth to upstream terminal server
auth_type = connection.get("auth_type", "bearer")
if auth_type == "bearer":
key = connection.get("key", "")
await upstream.send_str(_json.dumps({"type": "auth", "token": key}))
auth_type = connection.get('auth_type', 'bearer')
if auth_type == 'bearer':
key = connection.get('key', '')
await upstream.send_str(_json.dumps({'type': 'auth', 'token': key}))
async def _client_to_upstream():
"""Forward client → upstream."""
try:
while True:
msg = await ws.receive()
if msg["type"] == "websocket.disconnect":
if msg['type'] == 'websocket.disconnect':
break
elif "bytes" in msg and msg["bytes"]:
await upstream.send_bytes(msg["bytes"])
elif "text" in msg and msg["text"]:
await upstream.send_str(msg["text"])
elif 'bytes' in msg and msg['bytes']:
await upstream.send_bytes(msg['bytes'])
elif 'text' in msg and msg['text']:
await upstream.send_str(msg['text'])
except Exception:
pass
@@ -326,7 +315,7 @@ async def ws_terminal(
return_exceptions=True,
)
except Exception as e:
log.exception("Terminal WebSocket proxy error: %s", e)
log.exception('Terminal WebSocket proxy error: %s', e)
finally:
await session.close()
try:
+131 -181
View File
@@ -59,7 +59,7 @@ def get_tool_module(request, tool_id, load_from_db=True):
############################
@router.get("/", response_model=list[ToolUserResponse])
@router.get('/', response_model=list[ToolUserResponse])
async def get_tools(
request: Request,
user=Depends(get_verified_user),
@@ -69,18 +69,12 @@ async def get_tools(
# Local Tools
for tool in Tools.get_tools(defer_content=True, db=db):
tool_module = (
request.app.state.TOOLS.get(tool.id)
if hasattr(request.app.state, "TOOLS")
else None
)
tool_module = request.app.state.TOOLS.get(tool.id) if hasattr(request.app.state, 'TOOLS') else None
tools.append(
ToolUserResponse(
**{
**tool.model_dump(),
"has_user_valves": (
hasattr(tool_module, "UserValves") if tool_module else False
),
'has_user_valves': (hasattr(tool_module, 'UserValves') if tool_module else False),
}
)
)
@@ -88,94 +82,82 @@ async def get_tools(
# OpenAPI Tool Servers
server_access_grants = {}
for server in await get_tool_servers(request):
server_idx = server.get("idx", 0)
server_idx = server.get('idx', 0)
connections = request.app.state.config.TOOL_SERVER_CONNECTIONS
if server_idx >= len(connections):
log.warning(
f"Tool server index {server_idx} out of range "
f"(have {len(connections)} connections), skipping server {server.get('id')}"
f'Tool server index {server_idx} out of range '
f'(have {len(connections)} connections), skipping server {server.get("id")}'
)
continue
connection = connections[server_idx]
server_config = connection.get("config", {})
server_config = connection.get('config', {})
server_id = f"server:{server.get('id')}"
server_access_grants[server_id] = server_config.get("access_grants", [])
server_id = f'server:{server.get("id")}'
server_access_grants[server_id] = server_config.get('access_grants', [])
tools.append(
ToolUserResponse(
**{
"id": server_id,
"user_id": server_id,
"name": server.get("openapi", {})
.get("info", {})
.get("title", "Tool Server"),
"meta": {
"description": server.get("openapi", {})
.get("info", {})
.get("description", ""),
'id': server_id,
'user_id': server_id,
'name': server.get('openapi', {}).get('info', {}).get('title', 'Tool Server'),
'meta': {
'description': server.get('openapi', {}).get('info', {}).get('description', ''),
},
"updated_at": int(time.time()),
"created_at": int(time.time()),
'updated_at': int(time.time()),
'created_at': int(time.time()),
}
)
)
# MCP Tool Servers
for server in request.app.state.config.TOOL_SERVER_CONNECTIONS:
if server.get("type", "openapi") == "mcp" and server.get("config", {}).get(
"enable"
):
server_id = server.get("info", {}).get("id")
auth_type = server.get("auth_type", "none")
if server.get('type', 'openapi') == 'mcp' and server.get('config', {}).get('enable'):
server_id = server.get('info', {}).get('id')
auth_type = server.get('auth_type', 'none')
session_token = None
if auth_type == "oauth_2.1":
splits = server_id.split(":")
if auth_type == 'oauth_2.1':
splits = server_id.split(':')
server_id = splits[-1] if len(splits) > 1 else server_id
session_token = (
await request.app.state.oauth_client_manager.get_oauth_token(
user.id, f"mcp:{server_id}"
)
session_token = await request.app.state.oauth_client_manager.get_oauth_token(
user.id, f'mcp:{server_id}'
)
server_config = server.get("config", {})
server_config = server.get('config', {})
tool_id = f"server:mcp:{server.get('info', {}).get('id')}"
server_access_grants[tool_id] = server_config.get("access_grants", [])
tool_id = f'server:mcp:{server.get("info", {}).get("id")}'
server_access_grants[tool_id] = server_config.get('access_grants', [])
tools.append(
ToolUserResponse(
**{
"id": tool_id,
"user_id": tool_id,
"name": server.get("info", {}).get("name", "MCP Tool Server"),
"meta": {
"description": server.get("info", {}).get(
"description", ""
),
'id': tool_id,
'user_id': tool_id,
'name': server.get('info', {}).get('name', 'MCP Tool Server'),
'meta': {
'description': server.get('info', {}).get('description', ''),
},
"updated_at": int(time.time()),
"created_at": int(time.time()),
'updated_at': int(time.time()),
'created_at': int(time.time()),
**(
{
"authenticated": session_token is not None,
'authenticated': session_token is not None,
}
if auth_type == "oauth_2.1"
if auth_type == 'oauth_2.1'
else {}
),
}
)
)
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
# Admin can see all tools
return tools
else:
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
}
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
tools = [
tool
for tool in tools
@@ -183,17 +165,17 @@ async def get_tools(
or (
has_access(
user.id,
"read",
'read',
server_access_grants.get(str(tool.id), []),
user_group_ids,
db=db,
)
if str(tool.id).startswith("server:")
if str(tool.id).startswith('server:')
else AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tool.id,
permission="read",
permission='read',
user_group_ids=user_group_ids,
db=db,
)
@@ -207,34 +189,25 @@ async def get_tools(
############################
@router.get("/list", response_model=list[ToolAccessResponse])
async def get_tool_list(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
@router.get('/list', response_model=list[ToolAccessResponse])
async def get_tool_list(user=Depends(get_verified_user), db: Session = Depends(get_session)):
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
tools = Tools.get_tools(defer_content=True, db=db)
else:
tools = Tools.get_tools_by_user_id(user.id, "read", defer_content=True, db=db)
tools = Tools.get_tools_by_user_id(user.id, 'read', defer_content=True, db=db)
user_group_ids = {
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
}
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)}
result = []
for tool in tools:
has_write = (
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == tool.user_id
or any(
g.permission == "write"
g.permission == 'write'
and (
(
g.principal_type == "user"
and (g.principal_id == user.id or g.principal_id == "*")
)
or (
g.principal_type == "group" and g.principal_id in user_group_ids
)
(g.principal_type == 'user' and (g.principal_id == user.id or g.principal_id == '*'))
or (g.principal_type == 'group' and g.principal_id in user_group_ids)
)
for g in tool.access_grants
)
@@ -259,70 +232,59 @@ class LoadUrlForm(BaseModel):
def github_url_to_raw_url(url: str) -> str:
# Handle 'tree' (folder) URLs (add main.py at the end)
m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url)
m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url)
if m1:
org, repo, branch, path = m1.groups()
return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py"
return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py'
# Handle 'blob' (file) URLs
m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url)
m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url)
if m2:
org, repo, branch, path = m2.groups()
return (
f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}"
)
return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}'
# No match; return as-is
return url
@router.post("/load/url", response_model=Optional[dict])
async def load_tool_from_url(
request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)
):
@router.post('/load/url', response_model=Optional[dict])
async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)):
# NOTE: This is NOT a SSRF vulnerability:
# This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use,
# and does NOT accept untrusted user input. Access is enforced by authentication.
url = str(form_data.url)
if not url:
raise HTTPException(status_code=400, detail="Please enter a valid URL")
raise HTTPException(status_code=400, detail='Please enter a valid URL')
url = github_url_to_raw_url(url)
url_parts = url.rstrip("/").split("/")
url_parts = url.rstrip('/').split('/')
file_name = url_parts[-1]
tool_name = (
file_name[:-3]
if (
file_name.endswith(".py")
and (not file_name.startswith(("main.py", "index.py", "__init__.py")))
)
else url_parts[-2] if len(url_parts) > 1 else "function"
if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py'))))
else url_parts[-2]
if len(url_parts) > 1
else 'function'
)
try:
async with aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
) as session:
async with session.get(
url, headers={"Content-Type": "application/json"}
) as resp:
async with session.get(url, headers={'Content-Type': 'application/json'}) as resp:
if resp.status != 200:
raise HTTPException(
status_code=resp.status, detail="Failed to fetch the tool"
)
raise HTTPException(status_code=resp.status, detail='Failed to fetch the tool')
data = await resp.text()
if not data:
raise HTTPException(
status_code=400, detail="No data received from the URL"
)
raise HTTPException(status_code=400, detail='No data received from the URL')
return {
"name": tool_name,
"content": data,
'name': tool_name,
'content': data,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error importing tool: {e}")
raise HTTPException(status_code=500, detail=f'Error importing tool: {e}')
############################
@@ -330,15 +292,15 @@ async def load_tool_from_url(
############################
@router.get("/export", response_model=list[ToolModel])
@router.get('/export', response_model=list[ToolModel])
async def export_tools(
request: Request,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not has_permission(
if user.role != 'admin' and not has_permission(
user.id,
"workspace.tools_export",
'workspace.tools_export',
request.app.state.config.USER_PERMISSIONS,
db=db,
):
@@ -347,10 +309,10 @@ async def export_tools(
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL:
return Tools.get_tools(db=db)
else:
return Tools.get_tools_by_user_id(user.id, "read", db=db)
return Tools.get_tools_by_user_id(user.id, 'read', db=db)
############################
@@ -358,20 +320,18 @@ async def export_tools(
############################
@router.post("/create", response_model=Optional[ToolResponse])
@router.post('/create', response_model=Optional[ToolResponse])
async def create_new_tools(
request: Request,
form_data: ToolForm,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
if user.role != "admin" and not (
has_permission(
user.id, "workspace.tools", request.app.state.config.USER_PERMISSIONS, db=db
)
if user.role != 'admin' and not (
has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db)
or has_permission(
user.id,
"workspace.tools_import",
'workspace.tools_import',
request.app.state.config.USER_PERMISSIONS,
db=db,
)
@@ -384,7 +344,7 @@ async def create_new_tools(
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only alphanumeric characters and underscores are allowed in the id",
detail='Only alphanumeric characters and underscores are allowed in the id',
)
form_data.id = form_data.id.lower()
@@ -393,9 +353,7 @@ async def create_new_tools(
if tools is None:
try:
form_data.content = replace_imports(form_data.content)
tool_module, frontmatter = load_tool_module_by_id(
form_data.id, content=form_data.content
)
tool_module, frontmatter = load_tool_module_by_id(form_data.id, content=form_data.content)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
@@ -404,7 +362,7 @@ async def create_new_tools(
specs = get_tool_specs(TOOLS[form_data.id])
tools = Tools.insert_new_tool(user.id, form_data, specs, db=db)
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
tool_cache_dir = CACHE_DIR / 'tools' / form_data.id
tool_cache_dir.mkdir(parents=True, exist_ok=True)
if tools:
@@ -412,10 +370,10 @@ async def create_new_tools(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
detail=ERROR_MESSAGES.DEFAULT('Error creating tools'),
)
except Exception as e:
log.exception(f"Failed to load the tool by id {form_data.id}: {e}")
log.exception(f'Failed to load the tool by id {form_data.id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -432,34 +390,32 @@ async def create_new_tools(
############################
@router.get("/id/{id}", response_model=Optional[ToolAccessResponse])
async def get_tools_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}', response_model=Optional[ToolAccessResponse])
async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
tools = Tools.get_tool_by_id(id, db=db)
if tools:
if (
user.role == "admin"
user.role == 'admin'
or tools.user_id == user.id
or AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="read",
permission='read',
db=db,
)
):
return ToolAccessResponse(
**tools.model_dump(),
write_access=(
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
or user.id == tools.user_id
or AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
),
@@ -481,7 +437,7 @@ async def get_tools_by_id(
############################
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
@router.post('/id/{id}/update', response_model=Optional[ToolModel])
async def update_tools_by_id(
request: Request,
id: str,
@@ -501,12 +457,12 @@ async def update_tools_by_id(
tools.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -524,8 +480,8 @@ async def update_tools_by_id(
specs = get_tool_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
"specs": specs,
**form_data.model_dump(exclude={'id'}),
'specs': specs,
}
log.debug(updated)
@@ -536,7 +492,7 @@ async def update_tools_by_id(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating tools"),
detail=ERROR_MESSAGES.DEFAULT('Error updating tools'),
)
except Exception as e:
@@ -555,7 +511,7 @@ class ToolAccessGrantsForm(BaseModel):
access_grants: list[dict]
@router.post("/id/{id}/access/update", response_model=Optional[ToolModel])
@router.post('/id/{id}/access/update', response_model=Optional[ToolModel])
async def update_tool_access_by_id(
request: Request,
id: str,
@@ -574,12 +530,12 @@ async def update_tool_access_by_id(
tools.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -591,10 +547,10 @@ async def update_tool_access_by_id(
user.id,
user.role,
form_data.access_grants,
"sharing.public_tools",
'sharing.public_tools',
)
AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db)
AccessGrants.set_access_grants('tool', id, form_data.access_grants, db=db)
return Tools.get_tool_by_id(id, db=db)
@@ -604,7 +560,7 @@ async def update_tool_access_by_id(
############################
@router.delete("/id/{id}/delete", response_model=bool)
@router.delete('/id/{id}/delete', response_model=bool)
async def delete_tools_by_id(
request: Request,
id: str,
@@ -622,12 +578,12 @@ async def delete_tools_by_id(
tools.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -648,10 +604,8 @@ async def delete_tools_by_id(
############################
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_tools_valves_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}/valves', response_model=Optional[dict])
async def get_tools_valves_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
tools = Tools.get_tool_by_id(id, db=db)
if not tools:
raise HTTPException(
@@ -663,12 +617,12 @@ async def get_tools_valves_by_id(
tools.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -690,7 +644,7 @@ async def get_tools_valves_by_id(
############################
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
@router.get('/id/{id}/valves/spec', response_model=Optional[dict])
async def get_tools_valves_spec_by_id(
request: Request,
id: str,
@@ -708,12 +662,12 @@ async def get_tools_valves_spec_by_id(
tools.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -726,7 +680,7 @@ async def get_tools_valves_spec_by_id(
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "Valves"):
if hasattr(tools_module, 'Valves'):
Valves = tools_module.Valves
schema = Valves.schema()
# Resolve dynamic options for select dropdowns
@@ -740,7 +694,7 @@ async def get_tools_valves_spec_by_id(
############################
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
@router.post('/id/{id}/valves/update', response_model=Optional[dict])
async def update_tools_valves_by_id(
request: Request,
id: str,
@@ -759,12 +713,12 @@ async def update_tools_valves_by_id(
tools.user_id != user.id
and not AccessGrants.has_access(
user_id=user.id,
resource_type="tool",
resource_type='tool',
resource_id=tools.id,
permission="write",
permission='write',
db=db,
)
and user.role != "admin"
and user.role != 'admin'
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -777,7 +731,7 @@ async def update_tools_valves_by_id(
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if not hasattr(tools_module, "Valves"):
if not hasattr(tools_module, 'Valves'):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
@@ -791,7 +745,7 @@ async def update_tools_valves_by_id(
Tools.update_tool_valves_by_id(id, valves_dict, db=db)
return valves_dict
except Exception as e:
log.exception(f"Failed to update tool valves by id {id}: {e}")
log.exception(f'Failed to update tool valves by id {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
@@ -803,10 +757,8 @@ async def update_tools_valves_by_id(
############################
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_tools_user_valves_by_id(
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/id/{id}/valves/user', response_model=Optional[dict])
async def get_tools_user_valves_by_id(id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
tools = Tools.get_tool_by_id(id, db=db)
if tools:
try:
@@ -824,7 +776,7 @@ async def get_tools_user_valves_by_id(
)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
@router.get('/id/{id}/valves/user/spec', response_model=Optional[dict])
async def get_tools_user_valves_spec_by_id(
request: Request,
id: str,
@@ -839,7 +791,7 @@ async def get_tools_user_valves_spec_by_id(
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
if hasattr(tools_module, 'UserValves'):
UserValves = tools_module.UserValves
schema = UserValves.schema()
# Resolve dynamic options for select dropdowns
@@ -853,7 +805,7 @@ async def get_tools_user_valves_spec_by_id(
)
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
@router.post('/id/{id}/valves/user/update', response_model=Optional[dict])
async def update_tools_user_valves_by_id(
request: Request,
id: str,
@@ -870,19 +822,17 @@ async def update_tools_user_valves_by_id(
tools_module, _ = load_tool_module_by_id(id)
request.app.state.TOOLS[id] = tools_module
if hasattr(tools_module, "UserValves"):
if hasattr(tools_module, 'UserValves'):
UserValves = tools_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
user_valves = UserValves(**form_data)
user_valves_dict = user_valves.model_dump(exclude_unset=True)
Tools.update_user_valves_by_id_and_user_id(
id, user.id, user_valves_dict, db=db
)
Tools.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db)
return user_valves_dict
except Exception as e:
log.exception(f"Failed to update user valves by id {id}: {e}")
log.exception(f'Failed to update user valves by id {id}: {e}')
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
+79 -113
View File
@@ -54,7 +54,7 @@ router = APIRouter()
PAGE_ITEM_COUNT = 30
@router.get("/", response_model=UserGroupIdsListResponse)
@router.get('/', response_model=UserGroupIdsListResponse)
async def get_users(
query: Optional[str] = None,
order_by: Optional[str] = None,
@@ -70,38 +70,38 @@ async def get_users(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
filter["direction"] = direction
filter['direction'] = direction
result = Users.get_users(filter=filter, skip=skip, limit=limit, db=db)
users = result["users"]
total = result["total"]
users = result['users']
total = result['total']
# Fetch groups for all users in a single query to avoid N+1
user_ids = [user.id for user in users]
user_groups = Groups.get_groups_by_member_ids(user_ids, db=db)
return {
"users": [
'users': [
UserGroupIdsModel(
**{
**user.model_dump(),
"group_ids": [group.id for group in user_groups.get(user.id, [])],
'group_ids': [group.id for group in user_groups.get(user.id, [])],
}
)
for user in users
],
"total": total,
'total': total,
}
@router.get("/all", response_model=UserInfoListResponse)
@router.get('/all', response_model=UserInfoListResponse)
async def get_all_users(
user=Depends(get_admin_user),
db: Session = Depends(get_session),
@@ -109,7 +109,7 @@ async def get_all_users(
return Users.get_users(db=db)
@router.get("/search", response_model=UserInfoListResponse)
@router.get('/search', response_model=UserInfoListResponse)
async def search_users(
query: Optional[str] = None,
order_by: Optional[str] = None,
@@ -125,11 +125,11 @@ async def search_users(
filter = {}
if query:
filter["query"] = query
filter['query'] = query
if order_by:
filter["order_by"] = order_by
filter['order_by'] = order_by
if direction:
filter["direction"] = direction
filter['direction'] = direction
return Users.get_users(filter=filter, skip=skip, limit=limit, db=db)
@@ -139,10 +139,8 @@ async def search_users(
############################
@router.get("/groups")
async def get_user_groups(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/groups')
async def get_user_groups(user=Depends(get_verified_user), db: Session = Depends(get_session)):
return Groups.get_groups_by_member_id(user.id, db=db)
@@ -151,15 +149,13 @@ async def get_user_groups(
############################
@router.get("/permissions")
@router.get('/permissions')
async def get_user_permissisions(
request: Request,
user=Depends(get_verified_user),
db: Session = Depends(get_session),
):
user_permissions = get_permissions(
user.id, request.app.state.config.USER_PERMISSIONS, db=db
)
user_permissions = get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db)
return user_permissions
@@ -249,34 +245,20 @@ class UserPermissions(BaseModel):
settings: SettingsPermissions
@router.get("/default/permissions", response_model=UserPermissions)
@router.get('/default/permissions', response_model=UserPermissions)
async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)):
return {
"workspace": WorkspacePermissions(
**request.app.state.config.USER_PERMISSIONS.get("workspace", {})
),
"sharing": SharingPermissions(
**request.app.state.config.USER_PERMISSIONS.get("sharing", {})
),
"access_grants": AccessGrantsPermissions(
**request.app.state.config.USER_PERMISSIONS.get("access_grants", {})
),
"chat": ChatPermissions(
**request.app.state.config.USER_PERMISSIONS.get("chat", {})
),
"features": FeaturesPermissions(
**request.app.state.config.USER_PERMISSIONS.get("features", {})
),
"settings": SettingsPermissions(
**request.app.state.config.USER_PERMISSIONS.get("settings", {})
),
'workspace': WorkspacePermissions(**request.app.state.config.USER_PERMISSIONS.get('workspace', {})),
'sharing': SharingPermissions(**request.app.state.config.USER_PERMISSIONS.get('sharing', {})),
'access_grants': AccessGrantsPermissions(**request.app.state.config.USER_PERMISSIONS.get('access_grants', {})),
'chat': ChatPermissions(**request.app.state.config.USER_PERMISSIONS.get('chat', {})),
'features': FeaturesPermissions(**request.app.state.config.USER_PERMISSIONS.get('features', {})),
'settings': SettingsPermissions(**request.app.state.config.USER_PERMISSIONS.get('settings', {})),
}
@router.post("/default/permissions")
async def update_default_user_permissions(
request: Request, form_data: UserPermissions, user=Depends(get_admin_user)
):
@router.post('/default/permissions')
async def update_default_user_permissions(request: Request, form_data: UserPermissions, user=Depends(get_admin_user)):
request.app.state.config.USER_PERMISSIONS = form_data.model_dump()
return request.app.state.config.USER_PERMISSIONS
@@ -286,10 +268,8 @@ async def update_default_user_permissions(
############################
@router.get("/user/settings", response_model=Optional[UserSettings])
async def get_user_settings_by_session_user(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/user/settings', response_model=Optional[UserSettings])
async def get_user_settings_by_session_user(user=Depends(get_verified_user), db: Session = Depends(get_session)):
user = Users.get_user_by_id(user.id, db=db)
if user:
return user.settings
@@ -305,7 +285,7 @@ async def get_user_settings_by_session_user(
############################
@router.post("/user/settings/update", response_model=UserSettings)
@router.post('/user/settings/update', response_model=UserSettings)
async def update_user_settings_by_session_user(
request: Request,
form_data: UserSettings,
@@ -313,19 +293,19 @@ async def update_user_settings_by_session_user(
db: Session = Depends(get_session),
):
updated_user_settings = form_data.model_dump()
ui_settings = updated_user_settings.get("ui")
ui_settings = updated_user_settings.get('ui')
if (
user.role != "admin"
user.role != 'admin'
and ui_settings is not None
and "toolServers" in ui_settings.keys()
and 'toolServers' in ui_settings.keys()
and not has_permission(
user.id,
"features.direct_tool_servers",
'features.direct_tool_servers',
request.app.state.config.USER_PERMISSIONS,
)
):
# If the user is not an admin and does not have permission to use tool servers, remove the key
updated_user_settings["ui"].pop("toolServers", None)
updated_user_settings['ui'].pop('toolServers', None)
user = Users.update_user_settings_by_id(user.id, updated_user_settings, db=db)
if user:
@@ -342,7 +322,7 @@ async def update_user_settings_by_session_user(
############################
@router.get("/user/status")
@router.get('/user/status')
async def get_user_status_by_session_user(
request: Request,
user=Depends(get_verified_user),
@@ -368,7 +348,7 @@ async def get_user_status_by_session_user(
############################
@router.post("/user/status/update")
@router.post('/user/status/update')
async def update_user_status_by_session_user(
request: Request,
form_data: UserStatus,
@@ -396,10 +376,8 @@ async def update_user_status_by_session_user(
############################
@router.get("/user/info", response_model=Optional[dict])
async def get_user_info_by_session_user(
user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/user/info', response_model=Optional[dict])
async def get_user_info_by_session_user(user=Depends(get_verified_user), db: Session = Depends(get_session)):
user = Users.get_user_by_id(user.id, db=db)
if user:
return user.info
@@ -415,7 +393,7 @@ async def get_user_info_by_session_user(
############################
@router.post("/user/info/update", response_model=Optional[dict])
@router.post('/user/info/update', response_model=Optional[dict])
async def update_user_info_by_session_user(
form_data: dict, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@@ -424,9 +402,7 @@ async def update_user_info_by_session_user(
if user.info is None:
user.info = {}
user = Users.update_user_by_id(
user.id, {"info": {**user.info, **form_data}}, db=db
)
user = Users.update_user_by_id(user.id, {'info': {**user.info, **form_data}}, db=db)
if user:
return user.info
else:
@@ -452,17 +428,15 @@ class UserActiveResponse(UserStatus):
groups: Optional[list] = []
is_active: bool
model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra='allow')
@router.get("/{user_id}", response_model=UserActiveResponse)
async def get_user_by_id(
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/{user_id}', response_model=UserActiveResponse)
async def get_user_by_id(user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
# Check if user_id is a shared chat
# If it is, get the user_id from the chat
if user_id.startswith("shared-"):
chat_id = user_id.replace("shared-", "")
if user_id.startswith('shared-'):
chat_id = user_id.replace('shared-', '')
chat = Chats.get_chat_by_id(chat_id)
if chat:
user_id = chat.user_id
@@ -478,8 +452,8 @@ async def get_user_by_id(
return UserActiveResponse(
**{
**user.model_dump(),
"groups": [{"id": group.id, "name": group.name} for group in groups],
"is_active": Users.is_user_active(user_id, db=db),
'groups': [{'id': group.id, 'name': group.name} for group in groups],
'is_active': Users.is_user_active(user_id, db=db),
}
)
else:
@@ -489,18 +463,16 @@ async def get_user_by_id(
)
@router.get("/{user_id}/info", response_model=UserInfoResponse)
async def get_user_info_by_id(
user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
@router.get('/{user_id}/info', response_model=UserInfoResponse)
async def get_user_info_by_id(user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
user = Users.get_user_by_id(user_id, db=db)
if user:
groups = Groups.get_groups_by_member_id(user_id, db=db)
return UserInfoResponse(
**{
**user.model_dump(),
"groups": [{"id": group.id, "name": group.name} for group in groups],
"is_active": Users.is_user_active(user_id, db=db),
'groups': [{'id': group.id, 'name': group.name} for group in groups],
'is_active': Users.is_user_active(user_id, db=db),
}
)
else:
@@ -510,10 +482,8 @@ async def get_user_info_by_id(
)
@router.get("/{user_id}/oauth/sessions")
async def get_user_oauth_sessions_by_id(
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/{user_id}/oauth/sessions')
async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
sessions = OAuthSessions.get_sessions_by_user_id(user_id, db=db)
if sessions and len(sessions) > 0:
return sessions
@@ -529,32 +499,32 @@ async def get_user_oauth_sessions_by_id(
############################
@router.get("/{user_id}/profile/image")
@router.get('/{user_id}/profile/image')
def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_user)):
user = Users.get_user_by_id(user_id)
if user:
if user.profile_image_url:
# check if it's url or base64
if user.profile_image_url.startswith("http"):
if user.profile_image_url.startswith('http'):
return Response(
status_code=status.HTTP_302_FOUND,
headers={"Location": user.profile_image_url},
headers={'Location': user.profile_image_url},
)
elif user.profile_image_url.startswith("data:image"):
elif user.profile_image_url.startswith('data:image'):
try:
header, base64_data = user.profile_image_url.split(",", 1)
header, base64_data = user.profile_image_url.split(',', 1)
image_data = base64.b64decode(base64_data)
image_buffer = io.BytesIO(image_data)
media_type = header.split(";")[0].lstrip("data:")
media_type = header.split(';')[0].lstrip('data:')
return StreamingResponse(
image_buffer,
media_type=media_type,
headers={"Content-Disposition": "inline"},
headers={'Content-Disposition': 'inline'},
)
except Exception as e:
pass
return FileResponse(f"{STATIC_DIR}/user.png")
return FileResponse(f'{STATIC_DIR}/user.png')
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -567,12 +537,12 @@ def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_user)):
############################
@router.get("/{user_id}/active", response_model=dict)
@router.get('/{user_id}/active', response_model=dict)
async def get_user_active_status_by_id(
user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
):
return {
"active": Users.is_user_active(user_id, db=db),
'active': Users.is_user_active(user_id, db=db),
}
@@ -581,7 +551,7 @@ async def get_user_active_status_by_id(
############################
@router.post("/{user_id}/update", response_model=Optional[UserModel])
@router.post('/{user_id}/update', response_model=Optional[UserModel])
async def update_user_by_id(
user_id: str,
form_data: UserUpdateForm,
@@ -600,7 +570,7 @@ async def update_user_by_id(
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
if form_data.role != "admin":
if form_data.role != 'admin':
# If the primary admin is trying to change their own role, prevent it
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -608,10 +578,10 @@ async def update_user_by_id(
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
log.error(f'Error checking primary admin status: {e}')
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
detail='Could not verify primary admin status.',
)
user = Users.get_user_by_id(user_id, db=db)
@@ -638,10 +608,10 @@ async def update_user_by_id(
updated_user = Users.update_user_by_id(
user_id,
{
"role": form_data.role,
"name": form_data.name,
"email": form_data.email.lower(),
"profile_image_url": form_data.profile_image_url,
'role': form_data.role,
'name': form_data.name,
'email': form_data.email.lower(),
'profile_image_url': form_data.profile_image_url,
},
db=db,
)
@@ -665,10 +635,8 @@ async def update_user_by_id(
############################
@router.delete("/{user_id}", response_model=bool)
async def delete_user_by_id(
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.delete('/{user_id}', response_model=bool)
async def delete_user_by_id(user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
# Prevent deletion of the primary admin user
try:
first_user = Users.get_first_user(db=db)
@@ -678,10 +646,10 @@ async def delete_user_by_id(
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
log.error(f'Error checking primary admin status: {e}')
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
detail='Could not verify primary admin status.',
)
if user.id != user_id:
@@ -707,8 +675,6 @@ async def delete_user_by_id(
############################
@router.get("/{user_id}/groups")
async def get_user_groups_by_id(
user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)
):
@router.get('/{user_id}/groups')
async def get_user_groups_by_id(user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session)):
return Groups.get_groups_by_member_id(user_id, db=db)
+22 -28
View File
@@ -20,7 +20,7 @@ log = logging.getLogger(__name__)
router = APIRouter()
@router.get("/gravatar")
@router.get('/gravatar')
async def get_gravatar(email: str, user=Depends(get_verified_user)):
return get_gravatar_url(email)
@@ -29,33 +29,31 @@ class CodeForm(BaseModel):
code: str
@router.post("/code/format")
@router.post('/code/format')
async def format_code(form_data: CodeForm, user=Depends(get_admin_user)):
try:
formatted_code = black.format_str(form_data.code, mode=black.Mode())
return {"code": formatted_code}
return {'code': formatted_code}
except black.NothingChanged:
return {"code": form_data.code}
return {'code': form_data.code}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/code/execute")
async def execute_code(
request: Request, form_data: CodeForm, user=Depends(get_verified_user)
):
if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter":
@router.post('/code/execute')
async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_verified_user)):
if request.app.state.config.CODE_EXECUTION_ENGINE == 'jupyter':
output = await execute_code_jupyter(
request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
form_data.code,
(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token"
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'token'
else None
),
(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password"
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'password'
else None
),
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
@@ -65,7 +63,7 @@ async def execute_code(
else:
raise HTTPException(
status_code=400,
detail="Code execution engine not supported",
detail='Code execution engine not supported',
)
@@ -73,11 +71,9 @@ class MarkdownForm(BaseModel):
md: str
@router.post("/markdown")
async def get_html_from_markdown(
form_data: MarkdownForm, user=Depends(get_verified_user)
):
return {"html": markdown.markdown(form_data.md)}
@router.post('/markdown')
async def get_html_from_markdown(form_data: MarkdownForm, user=Depends(get_verified_user)):
return {'html': markdown.markdown(form_data.md)}
class ChatForm(BaseModel):
@@ -85,24 +81,22 @@ class ChatForm(BaseModel):
messages: list[dict]
@router.post("/pdf")
async def download_chat_as_pdf(
form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)
):
@router.post('/pdf')
async def download_chat_as_pdf(form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)):
try:
pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": "attachment;filename=chat.pdf"},
media_type='application/pdf',
headers={'Content-Disposition': 'attachment;filename=chat.pdf'},
)
except Exception as e:
log.exception(f"Error generating PDF: {e}")
log.exception(f'Error generating PDF: {e}')
raise HTTPException(status_code=400, detail=str(e))
@router.get("/db/download")
@router.get('/db/download')
async def download_db(user=Depends(get_admin_user)):
if not ENABLE_ADMIN_EXPORT:
raise HTTPException(
@@ -111,13 +105,13 @@ async def download_db(user=Depends(get_admin_user)):
)
from open_webui.internal.db import engine
if engine.name != "sqlite":
if engine.name != 'sqlite':
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
)
return FileResponse(
engine.url.database,
media_type="application/octet-stream",
filename="webui.db",
media_type='application/octet-stream',
filename='webui.db',
)