refac
This commit is contained in:
@@ -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)
|
||||
|
||||
+370
-454
File diff suppressed because it is too large
Load Diff
+280
-386
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+218
-364
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
|
||||
@@ -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
@@ -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,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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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}'},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)},
|
||||
)
|
||||
|
||||
@@ -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
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user