refac
This commit is contained in:
@@ -412,6 +412,23 @@ class ChatTable:
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_internal_chat_by_note_id(
|
||||
self, note_id: str, user_id: str, db: AsyncSession | None = None
|
||||
) -> ChatModel | None:
|
||||
async with get_async_db_context(db) as session:
|
||||
result = await session.execute(
|
||||
select(Chat)
|
||||
.where(
|
||||
Chat.user_id == user_id,
|
||||
Chat.meta['internal'].as_boolean().is_(True),
|
||||
Chat.meta['type'].as_string() == 'note',
|
||||
Chat.meta['note_id'].as_string() == note_id,
|
||||
)
|
||||
.order_by(Chat.created_at.asc())
|
||||
)
|
||||
chat = result.scalars().first()
|
||||
return ChatModel.model_validate(chat) if chat else None
|
||||
|
||||
def _chat_import_form_to_chat_model(self, user_id: str, form_data: ChatImportForm) -> ChatModel:
|
||||
id = str(uuid.uuid4())
|
||||
chat = ChatModel(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
|
||||
from open_webui.config import (
|
||||
@@ -12,6 +13,7 @@ from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.events import EVENTS, publish_event
|
||||
from open_webui.internal.db import get_async_session
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.chats import ChatForm, ChatResponse, Chats
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.notes import (
|
||||
@@ -45,6 +47,39 @@ def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optiona
|
||||
return {'content': {'md': md[:max_length]}}
|
||||
|
||||
|
||||
def _note_chat_system_prompt(note_id: str) -> str:
|
||||
return (
|
||||
f'You are chatting with note {note_id}. Use view_note with this note id to read the current note. '
|
||||
'For edits, use replace_note_content for whole-note changes or replace_note_text '
|
||||
'for targeted exact text replacement.'
|
||||
)
|
||||
|
||||
|
||||
async def _normalize_note_chat_payload(chat: ChatResponse, note_id: str, db: AsyncSession) -> ChatResponse:
|
||||
payload = {**(chat.chat or {})}
|
||||
params = {**(payload.get('params') or {})}
|
||||
changed = False
|
||||
|
||||
if params.pop('note_id', None) is not None:
|
||||
changed = True
|
||||
|
||||
system = _note_chat_system_prompt(note_id)
|
||||
if params.get('system') != system:
|
||||
params['system'] = system
|
||||
changed = True
|
||||
|
||||
if payload.pop('system', None) is not None:
|
||||
changed = True
|
||||
|
||||
payload['params'] = params
|
||||
if changed:
|
||||
updated_chat = await Chats.update_chat_by_id(chat.id, payload, db=db, touch=False)
|
||||
if updated_chat:
|
||||
return updated_chat
|
||||
|
||||
return chat
|
||||
|
||||
|
||||
############################
|
||||
# GetNotes
|
||||
############################
|
||||
@@ -303,6 +338,69 @@ async def get_note_by_id(
|
||||
)
|
||||
|
||||
|
||||
@router.get('/{id}/chat', response_model=ChatResponse)
|
||||
async def get_note_chat_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
log.info('[note-chat] get-or-create requested note_id=%s user_id=%s', id, user.id)
|
||||
if user.role != 'admin' and not await has_permission(
|
||||
user.id, 'features.notes', await Config.get('user.permissions'), db=db
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
note = await 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)
|
||||
|
||||
if user.role != 'admin' and (
|
||||
user.id != note.user_id
|
||||
and not await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='note',
|
||||
resource_id=note.id,
|
||||
permission='read',
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||
|
||||
chat = await Chats.get_internal_chat_by_note_id(note.id, user.id, db=db)
|
||||
if chat:
|
||||
log.info('[note-chat] reusing hidden chat note_id=%s chat_id=%s user_id=%s', note.id, chat.id, user.id)
|
||||
return await _normalize_note_chat_payload(chat, note.id, db)
|
||||
|
||||
meta = {'internal': True, 'type': 'note', 'note_id': note.id}
|
||||
chat_id = str(uuid4())
|
||||
chat = await Chats.insert_new_chat(
|
||||
chat_id,
|
||||
user.id,
|
||||
ChatForm(
|
||||
chat={
|
||||
'id': chat_id,
|
||||
'title': 'Chat',
|
||||
'models': [''],
|
||||
'params': {'system': _note_chat_system_prompt(note.id)},
|
||||
'history': {'messages': {}, 'currentId': None},
|
||||
'messages': [],
|
||||
'tags': [],
|
||||
}
|
||||
),
|
||||
db=db,
|
||||
internal_meta=meta,
|
||||
)
|
||||
if not chat:
|
||||
log.error('[note-chat] failed creating hidden chat note_id=%s user_id=%s', note.id, user.id)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
|
||||
log.info('[note-chat] created hidden chat note_id=%s chat_id=%s user_id=%s', note.id, chat.id, user.id)
|
||||
return chat
|
||||
|
||||
|
||||
############################
|
||||
# UpdateNoteById
|
||||
############################
|
||||
|
||||
@@ -49,6 +49,8 @@ from open_webui.routers.memories import (
|
||||
add_memory as _add_memory,
|
||||
)
|
||||
from open_webui.routers.retrieval import search_web as _search_web
|
||||
from open_webui.events import EVENTS, publish_event
|
||||
from open_webui.socket.main import sio
|
||||
from open_webui.utils.sanitize import sanitize_code
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -56,6 +58,33 @@ log = logging.getLogger(__name__)
|
||||
MAX_KNOWLEDGE_BASE_SEARCH_ITEMS = 10_000
|
||||
|
||||
|
||||
async def _has_write_access_to_note(note, user_id: str) -> bool:
|
||||
if note.user_id == user_id:
|
||||
return True
|
||||
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
|
||||
user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)]
|
||||
return await AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type='note',
|
||||
resource_id=note.id,
|
||||
permission='write',
|
||||
user_group_ids=set(user_group_ids),
|
||||
)
|
||||
|
||||
|
||||
async def _emit_note_updated(request: Request, user: dict, note) -> None:
|
||||
await sio.emit('note-events', note.model_dump(), to=f'note:{note.id}')
|
||||
await publish_event(
|
||||
request,
|
||||
EVENTS.NOTE_UPDATED,
|
||||
actor=user,
|
||||
subject_id=note.id,
|
||||
data={'title': note.title},
|
||||
)
|
||||
|
||||
|
||||
async def _has_read_access_to_file(
|
||||
file,
|
||||
user_id: str,
|
||||
@@ -1155,23 +1184,21 @@ async def replace_note_content(
|
||||
if not note:
|
||||
return json.dumps({'error': 'Note not found'})
|
||||
|
||||
# Check write permission
|
||||
user_id = __user__.get('id')
|
||||
user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)]
|
||||
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
|
||||
if note.user_id != user_id and not await AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type='note',
|
||||
resource_id=note.id,
|
||||
permission='write',
|
||||
user_group_ids=set(user_group_ids),
|
||||
):
|
||||
if not await _has_write_access_to_note(note, user_id):
|
||||
return json.dumps({'error': 'Write access denied'})
|
||||
|
||||
# Build update form
|
||||
update_data = {'data': {'content': {'md': content}}}
|
||||
update_data = {
|
||||
'data': {
|
||||
**(note.data or {}),
|
||||
'content': {
|
||||
**((note.data or {}).get('content') or {}),
|
||||
'json': None,
|
||||
'html': '',
|
||||
'md': content,
|
||||
},
|
||||
}
|
||||
}
|
||||
if title:
|
||||
update_data['title'] = title
|
||||
|
||||
@@ -1181,6 +1208,8 @@ async def replace_note_content(
|
||||
if not updated_note:
|
||||
return json.dumps({'error': 'Failed to update note'})
|
||||
|
||||
await _emit_note_updated(__request__, __user__, updated_note)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'success',
|
||||
@@ -1195,6 +1224,88 @@ async def replace_note_content(
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
async def replace_note_text(
|
||||
note_id: str,
|
||||
target_text: str,
|
||||
replacement_text: str,
|
||||
title: Optional[str] = None,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
) -> str:
|
||||
"""
|
||||
Replace an exact text span in a note when it occurs exactly once.
|
||||
|
||||
:param note_id: The ID of the note to update
|
||||
:param target_text: Exact text to replace
|
||||
:param replacement_text: Replacement text
|
||||
:param title: Optional new title for the note
|
||||
:return: JSON with success status or a clear match-count error
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({'error': 'Request context not available'})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({'error': 'User context not available'})
|
||||
|
||||
try:
|
||||
from open_webui.models.notes import NoteUpdateForm
|
||||
|
||||
note = await Notes.get_note_by_id(note_id)
|
||||
if not note:
|
||||
return json.dumps({'error': 'Note not found'})
|
||||
|
||||
user_id = __user__.get('id')
|
||||
if not await _has_write_access_to_note(note, user_id):
|
||||
return json.dumps({'error': 'Write access denied'})
|
||||
|
||||
if target_text == '':
|
||||
return json.dumps({'error': 'target_text must not be empty'})
|
||||
|
||||
content = ((note.data or {}).get('content') or {}).get('md') or ''
|
||||
match_count = content.count(target_text)
|
||||
if match_count != 1:
|
||||
return json.dumps(
|
||||
{
|
||||
'error': 'target_text must occur exactly once',
|
||||
'match_count': match_count,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
update_data = {
|
||||
'data': {
|
||||
**(note.data or {}),
|
||||
'content': {
|
||||
**((note.data or {}).get('content') or {}),
|
||||
'json': None,
|
||||
'html': '',
|
||||
'md': content.replace(target_text, replacement_text, 1),
|
||||
},
|
||||
}
|
||||
}
|
||||
if title:
|
||||
update_data['title'] = title
|
||||
|
||||
updated_note = await Notes.update_note_by_id(note_id, NoteUpdateForm(**update_data))
|
||||
if not updated_note:
|
||||
return json.dumps({'error': 'Failed to update note'})
|
||||
|
||||
await _emit_note_updated(__request__, __user__, updated_note)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'success',
|
||||
'id': updated_note.id,
|
||||
'title': updated_note.title,
|
||||
'updated_at': updated_note.updated_at,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'replace_note_text error: {e}')
|
||||
return json.dumps({'error': str(e)})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CHATS TOOLS
|
||||
# =============================================================================
|
||||
|
||||
@@ -1881,6 +1881,7 @@ def apply_params_to_form_data(form_data, model):
|
||||
'reasoning_tags': list,
|
||||
'compact_token_threshold': int,
|
||||
'system': str,
|
||||
'note_id': str,
|
||||
}
|
||||
|
||||
for key in list(params.keys()):
|
||||
|
||||
@@ -72,6 +72,7 @@ from open_webui.tools.builtin import (
|
||||
read_memory_path,
|
||||
replace_memory_content,
|
||||
replace_note_content,
|
||||
replace_note_text,
|
||||
search_calendar_events,
|
||||
search_channel_messages,
|
||||
search_channels,
|
||||
@@ -635,7 +636,7 @@ async def get_builtin_tools(
|
||||
|
||||
# Notes tools - search, view, create, and update user's notes
|
||||
if is_builtin_tool_enabled('notes') and config.get('notes.enable') and await has_user_permission('notes'):
|
||||
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content])
|
||||
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content, replace_note_text])
|
||||
|
||||
# Channels tools - search channels and messages
|
||||
if is_builtin_tool_enabled('channels') and config.get('channels.enable') and await has_user_permission('channels'):
|
||||
|
||||
Reference in New Issue
Block a user