chore: format
This commit is contained in:
@@ -91,41 +91,41 @@ def upgrade():
|
||||
original_chat_id = row.user_id.replace('shared-', '', 1)
|
||||
|
||||
# Verify original chat still exists
|
||||
original = conn.execute(
|
||||
sa.select(chat_t.c.user_id).where(chat_t.c.id == original_chat_id)
|
||||
).fetchone()
|
||||
original = conn.execute(sa.select(chat_t.c.user_id).where(chat_t.c.id == original_chat_id)).fetchone()
|
||||
|
||||
if not original:
|
||||
continue
|
||||
|
||||
# Insert snapshot into shared_chat
|
||||
conn.execute(shared_chat_t.insert().values(
|
||||
id=share_token,
|
||||
chat_id=original_chat_id,
|
||||
user_id=original.user_id,
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
))
|
||||
conn.execute(
|
||||
shared_chat_t.insert().values(
|
||||
id=share_token,
|
||||
chat_id=original_chat_id,
|
||||
user_id=original.user_id,
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
)
|
||||
|
||||
# Create user:*:read grant for backward compat
|
||||
conn.execute(access_grant_t.insert().values(
|
||||
id=str(uuid.uuid4()),
|
||||
resource_type='shared_chat',
|
||||
resource_id=original_chat_id,
|
||||
principal_type='user',
|
||||
principal_id='*',
|
||||
permission='read',
|
||||
created_at=row.created_at or int(time.time()),
|
||||
))
|
||||
conn.execute(
|
||||
access_grant_t.insert().values(
|
||||
id=str(uuid.uuid4()),
|
||||
resource_type='shared_chat',
|
||||
resource_id=original_chat_id,
|
||||
principal_type='user',
|
||||
principal_id='*',
|
||||
permission='read',
|
||||
created_at=row.created_at or int(time.time()),
|
||||
)
|
||||
)
|
||||
|
||||
# 3. Clean up old phantom rows
|
||||
conn.execute(
|
||||
chat_message_t.delete().where(
|
||||
chat_message_t.c.chat_id.in_(
|
||||
sa.select(chat_t.c.id).where(chat_t.c.user_id.like('shared-%'))
|
||||
)
|
||||
chat_message_t.c.chat_id.in_(sa.select(chat_t.c.id).where(chat_t.c.user_id.like('shared-%')))
|
||||
)
|
||||
)
|
||||
conn.execute(chat_t.delete().where(chat_t.c.user_id.like('shared-%')))
|
||||
@@ -147,18 +147,18 @@ def downgrade():
|
||||
).fetchall()
|
||||
|
||||
for row in shared_rows:
|
||||
conn.execute(chat_t.insert().values(
|
||||
id=row.id,
|
||||
user_id=f'shared-{row.chat_id}',
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
archived=False,
|
||||
meta={},
|
||||
))
|
||||
conn.execute(
|
||||
chat_t.insert().values(
|
||||
id=row.id,
|
||||
user_id=f'shared-{row.chat_id}',
|
||||
title=row.title,
|
||||
chat=row.chat,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
archived=False,
|
||||
meta={},
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
access_grant_t.delete().where(access_grant_t.c.resource_type == 'shared_chat')
|
||||
)
|
||||
conn.execute(access_grant_t.delete().where(access_grant_t.c.resource_type == 'shared_chat'))
|
||||
op.drop_table('shared_chat')
|
||||
|
||||
@@ -723,9 +723,7 @@ class ChatTable:
|
||||
"""Delegate to SharedChats for listing shared chats by user."""
|
||||
from open_webui.models.shared_chats import SharedChats
|
||||
|
||||
return await SharedChats.get_by_user_id(
|
||||
user_id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
return await SharedChats.get_by_user_id(user_id, filter=filter, skip=skip, limit=limit, db=db)
|
||||
|
||||
async def get_chat_list_by_user_id(
|
||||
self,
|
||||
|
||||
@@ -299,15 +299,17 @@ class PromptsTable:
|
||||
|
||||
if dialect_name == 'sqlite':
|
||||
tag_clause = text(
|
||||
"EXISTS (SELECT 1 FROM json_each(prompt.tags) t WHERE LOWER(t.value) = :tag_val)"
|
||||
'EXISTS (SELECT 1 FROM json_each(prompt.tags) t WHERE LOWER(t.value) = :tag_val)'
|
||||
)
|
||||
elif dialect_name == 'postgresql':
|
||||
tag_clause = text(
|
||||
"EXISTS (SELECT 1 FROM json_array_elements_text(prompt.tags) t WHERE LOWER(t) = :tag_val)"
|
||||
'EXISTS (SELECT 1 FROM json_array_elements_text(prompt.tags) t WHERE LOWER(t) = :tag_val)'
|
||||
)
|
||||
else:
|
||||
# Fallback: LIKE on serialised JSON text (ASCII-safe only)
|
||||
tag_clause = func.lower(cast(Prompt.tags, String)).like(f'%{json.dumps(tag_lower, ensure_ascii=False)}%')
|
||||
tag_clause = func.lower(cast(Prompt.tags, String)).like(
|
||||
f'%{json.dumps(tag_lower, ensure_ascii=False)}%'
|
||||
)
|
||||
tag_lower = None
|
||||
|
||||
if tag_lower is not None:
|
||||
|
||||
@@ -60,9 +60,7 @@ class SharedChatResponse(BaseModel):
|
||||
|
||||
|
||||
class SharedChatsTable:
|
||||
async def create(
|
||||
self, chat_id: str, user_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
async def create(self, chat_id: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]:
|
||||
"""
|
||||
Create a snapshot of the chat for link sharing.
|
||||
Returns the SharedChatModel with the share token as its id.
|
||||
@@ -92,9 +90,7 @@ class SharedChatsTable:
|
||||
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
|
||||
async def update(
|
||||
self, share_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
async def update(self, share_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]:
|
||||
"""
|
||||
Re-snapshot: update the shared chat with the current state of the original chat.
|
||||
"""
|
||||
@@ -117,9 +113,7 @@ class SharedChatsTable:
|
||||
await db.refresh(shared_chat)
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
|
||||
async def get_by_id(
|
||||
self, share_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
async def get_by_id(self, share_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]:
|
||||
"""Get a shared chat by its share token."""
|
||||
async with get_async_db_context(db) as db:
|
||||
shared_chat = await db.get(SharedChat, share_id)
|
||||
@@ -127,16 +121,11 @@ class SharedChatsTable:
|
||||
return SharedChatModel.model_validate(shared_chat)
|
||||
return None
|
||||
|
||||
async def get_by_chat_id(
|
||||
self, chat_id: str, db: Optional[AsyncSession] = None
|
||||
) -> Optional[SharedChatModel]:
|
||||
async def get_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]:
|
||||
"""Get the shared chat for a given original chat. Returns the most recent one."""
|
||||
async with get_async_db_context(db) as db:
|
||||
result = await db.execute(
|
||||
select(SharedChat)
|
||||
.filter_by(chat_id=chat_id)
|
||||
.order_by(SharedChat.updated_at.desc())
|
||||
.limit(1)
|
||||
select(SharedChat).filter_by(chat_id=chat_id).order_by(SharedChat.updated_at.desc()).limit(1)
|
||||
)
|
||||
shared_chat = result.scalars().first()
|
||||
if shared_chat:
|
||||
@@ -194,9 +183,7 @@ class SharedChatsTable:
|
||||
for sc in result.scalars().all()
|
||||
]
|
||||
|
||||
async def delete_by_id(
|
||||
self, share_id: str, db: Optional[AsyncSession] = None
|
||||
) -> bool:
|
||||
async def delete_by_id(self, share_id: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
"""Delete a shared chat by its share token."""
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
@@ -206,9 +193,7 @@ class SharedChatsTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def delete_by_chat_id(
|
||||
self, chat_id: str, db: Optional[AsyncSession] = None
|
||||
) -> bool:
|
||||
async def delete_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool:
|
||||
"""Delete all shared chats for a given original chat."""
|
||||
try:
|
||||
async with get_async_db_context(db) as db:
|
||||
|
||||
@@ -959,7 +959,7 @@ async def filter_accessible_collections(
|
||||
# System meta-collection — never exposed to non-admins.
|
||||
continue
|
||||
elif name.startswith('file-'):
|
||||
file_id = name[len('file-'):]
|
||||
file_id = name[len('file-') :]
|
||||
if await has_access_to_file(file_id=file_id, access_type=access_type, user=user):
|
||||
validated.add(name)
|
||||
elif name.startswith('user-memory-'):
|
||||
|
||||
@@ -150,15 +150,8 @@ async def query_memory(
|
||||
# same RELEVANCE_THRESHOLD used by RAG ensures only genuinely matching
|
||||
# memories are surfaced (distances are normalised to 0→1, higher is
|
||||
# better).
|
||||
relevance_threshold = getattr(
|
||||
request.app.state.config, 'RELEVANCE_THRESHOLD', 0.0
|
||||
)
|
||||
if (
|
||||
results
|
||||
and relevance_threshold > 0.0
|
||||
and results.distances
|
||||
and results.distances[0]
|
||||
):
|
||||
relevance_threshold = getattr(request.app.state.config, 'RELEVANCE_THRESHOLD', 0.0)
|
||||
if results and relevance_threshold > 0.0 and results.distances and results.distances[0]:
|
||||
from open_webui.retrieval.vector.main import SearchResult
|
||||
|
||||
filtered_ids = []
|
||||
|
||||
@@ -2365,7 +2365,6 @@ async def _validate_collection_access(collection_names: list[str], user, access_
|
||||
)
|
||||
|
||||
|
||||
|
||||
class QueryDocForm(BaseModel):
|
||||
collection_name: str
|
||||
query: str
|
||||
|
||||
@@ -339,11 +339,8 @@ async def check_model_access(
|
||||
raise HTTPException(status_code=403, detail='Model not found')
|
||||
|
||||
# Enforce access on chained base models
|
||||
if not await has_base_model_access(
|
||||
user.id, model_info, user_group_ids=user_group_ids
|
||||
):
|
||||
if not await has_base_model_access(user.id, model_info, user_group_ids=user_group_ids):
|
||||
raise HTTPException(status_code=403, detail='Model not found')
|
||||
else:
|
||||
if user.role != 'admin':
|
||||
raise HTTPException(status_code=403, detail='Model not found')
|
||||
|
||||
|
||||
@@ -452,6 +452,50 @@ def serialize_output(output: list) -> str:
|
||||
# Already handled inline with function_call above
|
||||
pass
|
||||
|
||||
elif item_type in ('web_search_call', 'file_search_call', 'computer_call'):
|
||||
# OpenAI Responses API built-in server-side tool output items.
|
||||
# These are emitted when the model uses native tools (web_search,
|
||||
# file_search, computer_use) through the Responses API. Render as
|
||||
# collapsible tool call blocks matching the function_call pattern.
|
||||
if content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
|
||||
call_id = item.get('id', '')
|
||||
status = item.get('status', 'in_progress')
|
||||
|
||||
# Derive a human-readable display name
|
||||
display_names = {
|
||||
'web_search_call': 'Web Search',
|
||||
'file_search_call': 'File Search',
|
||||
'computer_call': 'Computer Use',
|
||||
}
|
||||
display_name = display_names.get(item_type, item_type)
|
||||
|
||||
# Extract a summary of what the tool did for the details body
|
||||
summary_text = ''
|
||||
if item_type == 'web_search_call':
|
||||
action = item.get('action', {})
|
||||
if isinstance(action, dict):
|
||||
query = action.get('query', '')
|
||||
if query:
|
||||
summary_text = f'Query: {query}'
|
||||
elif item_type == 'file_search_call':
|
||||
queries = item.get('queries', [])
|
||||
if queries:
|
||||
summary_text = f'Queries: {", ".join(str(q) for q in queries)}'
|
||||
elif item_type == 'computer_call':
|
||||
action = item.get('action', {})
|
||||
if isinstance(action, dict):
|
||||
action_type = action.get('type', '')
|
||||
if action_type:
|
||||
summary_text = f'Action: {action_type}'
|
||||
|
||||
done = status == 'completed' or idx != len(output) - 1
|
||||
if done:
|
||||
content += f'<details type="tool_calls" done="true" id="{call_id}" name="{html.escape(display_name)}" arguments="">\n<summary>Tool Executed</summary>\n{html.escape(summary_text)}\n</details>\n'
|
||||
else:
|
||||
content += f'<details type="tool_calls" done="false" id="{call_id}" name="{html.escape(display_name)}" arguments="">\n<summary>Executing...</summary>\n</details>\n'
|
||||
|
||||
elif item_type == 'reasoning':
|
||||
reasoning_content = ''
|
||||
# Check for 'summary' (new structure) or 'content' (legacy/fallback)
|
||||
@@ -2619,9 +2663,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
|
||||
# Resolve terminal tools if terminal_id is set (outside tool_ids check
|
||||
# so system terminals work even when no other tools are selected)
|
||||
terminal_capability = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get(
|
||||
'terminal', True
|
||||
)
|
||||
terminal_capability = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('terminal', True)
|
||||
if terminal_id and terminal_capability:
|
||||
try:
|
||||
terminal_result = await get_terminal_tools(
|
||||
@@ -3110,11 +3152,13 @@ async def outlet_filter_handler(ctx):
|
||||
|
||||
# Append the full assistant message (content, output, usage, etc.)
|
||||
if assistant_message:
|
||||
message_list.append({
|
||||
'id': message_id,
|
||||
'role': 'assistant',
|
||||
**assistant_message,
|
||||
})
|
||||
message_list.append(
|
||||
{
|
||||
'id': message_id,
|
||||
'role': 'assistant',
|
||||
**assistant_message,
|
||||
}
|
||||
)
|
||||
else:
|
||||
messages_map = await Chats.get_messages_map_by_chat_id(chat_id)
|
||||
if not messages_map:
|
||||
@@ -4221,7 +4265,6 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
reasoning_item['status'] = 'completed'
|
||||
|
||||
|
||||
if response_tool_calls:
|
||||
tool_calls.append(_split_tool_calls(response_tool_calls))
|
||||
|
||||
|
||||
@@ -413,7 +413,6 @@ async def check_model_access(user, model, db=None):
|
||||
raise Exception('Model not found')
|
||||
|
||||
|
||||
|
||||
async def get_filtered_models(models, user, db=None):
|
||||
# Filter out models that the user does not have access to
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user