Skip to content

Commit a359262

Browse files
committed
refac
1 parent d59b933 commit a359262

3 files changed

Lines changed: 138 additions & 146 deletions

File tree

backend/open_webui/models/notes.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,22 @@ async def search_notes(
161161
if filter:
162162
query_key = filter.get('query')
163163
if query_key:
164-
# Normalize search by removing hyphens and spaces (e.g., "todo" matches "to-do" and "to do")
165-
normalized_query = query_key.replace('-', '').replace(' ', '')
166-
stmt = stmt.filter(
167-
or_(
168-
func.replace(func.replace(Note.title, '-', ''), ' ', '').ilike(f'%{normalized_query}%'),
169-
func.replace(
170-
func.replace(cast(Note.data['content']['md'], Text), '-', ''),
171-
' ',
172-
'',
173-
).ilike(f'%{normalized_query}%'),
164+
# Split query into individual words and normalize each
165+
# (strip hyphens so "todo" matches "to-do").
166+
# All words must match somewhere in title OR content (AND semantics).
167+
search_words = query_key.split()
168+
normalized_words = [w.replace('-', '') for w in search_words if w.replace('-', '')]
169+
for word in normalized_words:
170+
stmt = stmt.filter(
171+
or_(
172+
func.replace(func.replace(Note.title, '-', ''), ' ', '').ilike(f'%{word}%'),
173+
func.replace(
174+
func.replace(cast(Note.data['content']['md'], Text), '-', ''),
175+
' ',
176+
'',
177+
).ilike(f'%{word}%'),
178+
)
174179
)
175-
)
176180

177181
view_option = filter.get('view_option')
178182
if view_option == 'created':

backend/open_webui/tools/builtin.py

Lines changed: 120 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -760,14 +760,26 @@ async def search_notes(
760760
content_snippet = ''
761761
if note.data and note.data.get('content', {}).get('md'):
762762
md_content = note.data['content']['md']
763-
lower_content = md_content.lower()
764-
lower_query = query.lower()
765-
idx = lower_content.find(lower_query)
766-
if idx != -1:
767-
start = max(0, idx - 50)
768-
end = min(len(md_content), idx + len(query) + 100)
763+
content_lower = md_content.lower()
764+
765+
# Find the first matching word to center the snippet around.
766+
search_words = query.lower().split()
767+
match_pos = -1
768+
match_len = len(query)
769+
for word in search_words:
770+
found_pos = content_lower.find(word)
771+
if found_pos != -1:
772+
match_pos = found_pos
773+
match_len = len(word)
774+
break
775+
776+
if match_pos != -1:
777+
snippet_start = max(0, match_pos - 50)
778+
snippet_end = min(len(md_content), match_pos + match_len + 100)
769779
content_snippet = (
770-
('...' if start > 0 else '') + md_content[start:end] + ('...' if end < len(md_content) else '')
780+
('...' if snippet_start > 0 else '')
781+
+ md_content[snippet_start:snippet_end]
782+
+ ('...' if snippet_end < len(md_content) else '')
771783
)
772784
else:
773785
content_snippet = md_content[:150] + ('...' if len(md_content) > 150 else '')
@@ -2337,162 +2349,137 @@ async def view_skill(
23372349

23382350
class TaskItem(BaseModel):
23392351
id: Optional[str] = Field(None, description='Unique identifier for the task. Auto-generated if omitted.')
2340-
content: Optional[str] = Field(None, description='Task description. Aliases: title, name, description.')
2352+
content: str = Field(..., description='Task description.')
23412353
status: Literal['pending', 'in_progress', 'completed', 'cancelled'] = Field('pending', description='Task status.')
23422354

23432355

2344-
async def tasks(
2345-
tasks: Optional[list[TaskItem]] = None,
2346-
overwrite: bool = True,
2356+
def _task_summary(all_tasks: list[dict]) -> dict:
2357+
"""Build summary counts for a task list."""
2358+
pending = sum(1 for t in all_tasks if t['status'] == 'pending')
2359+
in_progress = sum(1 for t in all_tasks if t['status'] == 'in_progress')
2360+
completed = sum(1 for t in all_tasks if t['status'] == 'completed')
2361+
cancelled = sum(1 for t in all_tasks if t['status'] == 'cancelled')
2362+
return {
2363+
'total': len(all_tasks),
2364+
'pending': pending,
2365+
'in_progress': in_progress,
2366+
'completed': completed,
2367+
'cancelled': cancelled,
2368+
}
2369+
2370+
2371+
async def _emit_tasks(event_emitter, all_tasks: list[dict]):
2372+
"""Persist task state to the UI."""
2373+
if event_emitter:
2374+
await event_emitter(
2375+
{
2376+
'type': 'chat:message:tasks',
2377+
'data': {
2378+
'tasks': all_tasks,
2379+
},
2380+
}
2381+
)
2382+
2383+
2384+
async def create_tasks(
2385+
tasks: list[TaskItem],
23472386
__chat_id__: str = None,
23482387
__message_id__: str = None,
23492388
__event_emitter__: callable = None,
23502389
__request__: Request = None,
23512390
__user__: dict = None,
23522391
) -> str:
23532392
"""
2354-
Track progress on multi-step work by maintaining a task checklist.
2355-
Use this whenever a request involves multiple steps or could take
2356-
significant effort. Call to set the full list, then call again
2357-
with overwrite=false after completing each task to mark it
2358-
completed. Do not leave tasks in_progress when the work is done.
2359-
Each task has an id, content, and status (pending, in_progress,
2360-
completed, cancelled).
2393+
Create a task checklist to track progress on multi-step work.
2394+
Call this once at the start to define all steps, then use
2395+
update_task to mark each task as you complete it.
23612396
2362-
:param tasks: Optional list of task items. Each item: id (string), content (string, required for new tasks), status (pending|in_progress|completed|cancelled). Leave empty to fetch without modifying.
2363-
:param overwrite: If true (default), replaces the entire task list. If false, updates/adds tasks by id while keeping existing ones.
2397+
:param tasks: List of task items. Each item: content (string, required), status (pending|in_progress|completed|cancelled, default pending), id (optional, auto-generated).
23642398
:return: JSON with the full task list and summary counts
23652399
"""
23662400
if __chat_id__ is None:
23672401
return json.dumps({'error': 'Chat context not available'})
23682402

23692403
try:
2370-
2371-
def _to_dict(task) -> dict:
2372-
"""Convert TaskItem or dict to plain dict."""
2404+
all_tasks = []
2405+
for idx, task in enumerate(tasks):
23732406
if hasattr(task, 'model_dump'):
23742407
d = task.model_dump(exclude_none=True)
2375-
# Include any extra fields the model sent
2376-
if hasattr(task, 'model_extra') and task.model_extra:
2377-
d.update(task.model_extra)
2378-
return d
2379-
return dict(task) if not isinstance(task, dict) else task
2380-
2381-
def _resolve_content(d: dict) -> str:
2382-
"""Accept content, title, name, or description as the task text."""
2383-
for key in ('content', 'title', 'name', 'description'):
2384-
val = str(d.get(key, '')).strip()
2385-
if val:
2386-
return val
2387-
return ''
2388-
2389-
def _resolve_id(d: dict, idx: int) -> str:
2390-
"""Use provided id, or auto-generate from index."""
2391-
item_id = str(d.get('id', '') or '').strip()
2392-
return item_id if item_id else str(idx + 1)
2393-
2394-
if tasks is None:
2395-
# Read-only - return current list
2396-
all_tasks = await Chats.get_chat_tasks_by_id(__chat_id__)
2397-
elif overwrite:
2398-
# Full replacement - validate and write
2399-
all_tasks = []
2400-
for idx, task in enumerate(tasks):
2401-
d = _to_dict(task)
2402-
item_id = _resolve_id(d, idx)
2403-
content = _resolve_content(d)
2404-
if not content:
2405-
continue
2408+
elif isinstance(task, dict):
2409+
d = task
2410+
else:
2411+
d = dict(task)
24062412

2407-
status = str(d.get('status', 'pending')).strip().lower()
2408-
if status not in VALID_TASK_STATUSES:
2409-
status = 'pending'
2413+
content = str(d.get('content', '')).strip()
2414+
if not content:
2415+
continue
24102416

2411-
all_tasks.append(
2412-
{
2413-
'id': item_id,
2414-
'content': content,
2415-
'status': status,
2416-
}
2417-
)
2418-
else:
2419-
# Partial update - merge by id
2420-
existing_tasks = await Chats.get_chat_tasks_by_id(__chat_id__)
2421-
existing_by_id = {t['id']: t for t in existing_tasks}
2422-
2423-
seen_ids = set()
2424-
for idx, task in enumerate(tasks):
2425-
d = _to_dict(task)
2426-
item_id = _resolve_id(d, len(existing_tasks) + idx)
2427-
2428-
seen_ids.add(item_id)
2429-
2430-
if item_id in existing_by_id:
2431-
resolved = _resolve_content(d)
2432-
if resolved:
2433-
existing_by_id[item_id]['content'] = resolved
2434-
status = str(d.get('status', '')).strip().lower()
2435-
if status and status in VALID_TASK_STATUSES:
2436-
existing_by_id[item_id]['status'] = status
2437-
else:
2438-
content = _resolve_content(d)
2439-
if not content:
2440-
continue
2417+
item_id = str(d.get('id', '') or '').strip() or str(idx + 1)
2418+
status = str(d.get('status', 'pending')).strip().lower()
2419+
if status not in VALID_TASK_STATUSES:
2420+
status = 'pending'
24412421

2442-
status = str(d.get('status', 'pending')).strip().lower()
2443-
if status not in VALID_TASK_STATUSES:
2444-
status = 'pending'
2422+
all_tasks.append({'id': item_id, 'content': content, 'status': status})
24452423

2446-
existing_by_id[item_id] = {
2447-
'id': item_id,
2448-
'content': content,
2449-
'status': status,
2450-
}
2424+
await Chats.update_chat_tasks_by_id(__chat_id__, all_tasks)
2425+
await _emit_tasks(__event_emitter__, all_tasks)
24512426

2452-
# Preserve order of existing, append new
2453-
all_tasks = []
2454-
for t in existing_tasks:
2455-
if t['id'] in existing_by_id:
2456-
all_tasks.append(existing_by_id[t['id']])
2457-
for item_id in seen_ids:
2458-
if not any(t['id'] == item_id for t in existing_tasks):
2459-
all_tasks.append(existing_by_id[item_id])
2460-
2461-
# Persist to DB and emit (skip for read-only)
2462-
if tasks is not None:
2463-
await Chats.update_chat_tasks_by_id(__chat_id__, all_tasks)
2464-
2465-
if __event_emitter__:
2466-
await __event_emitter__(
2467-
{
2468-
'type': 'chat:message:tasks',
2469-
'data': {
2470-
'tasks': all_tasks,
2471-
},
2472-
}
2473-
)
2427+
return json.dumps(
2428+
{'tasks': all_tasks, 'summary': _task_summary(all_tasks)},
2429+
ensure_ascii=False,
2430+
)
2431+
except Exception as e:
2432+
log.exception(f'tasks error: {e}')
2433+
return json.dumps({'error': str(e)})
24742434

2475-
# Build summary counts
2476-
pending = sum(1 for t in all_tasks if t['status'] == 'pending')
2477-
in_progress = sum(1 for t in all_tasks if t['status'] == 'in_progress')
2478-
completed = sum(1 for t in all_tasks if t['status'] == 'completed')
2479-
cancelled = sum(1 for t in all_tasks if t['status'] == 'cancelled')
2435+
2436+
async def update_task(
2437+
id: str,
2438+
status: str = 'completed',
2439+
__chat_id__: str = None,
2440+
__message_id__: str = None,
2441+
__event_emitter__: callable = None,
2442+
__request__: Request = None,
2443+
__user__: dict = None,
2444+
) -> str:
2445+
"""
2446+
Mark a single task as completed, in_progress, pending, or cancelled.
2447+
Call this after finishing each step. You MUST call this for every
2448+
task, including the very last one.
2449+
2450+
:param id: The task ID to update
2451+
:param status: New status: completed, in_progress, pending, or cancelled (default: completed)
2452+
:return: JSON with the updated task list and summary counts
2453+
"""
2454+
if __chat_id__ is None:
2455+
return json.dumps({'error': 'Chat context not available'})
2456+
2457+
try:
2458+
status = status.strip().lower()
2459+
if status not in VALID_TASK_STATUSES:
2460+
return json.dumps({'error': f'Invalid status: {status}. Must be one of: {", ".join(sorted(VALID_TASK_STATUSES))}'})
2461+
2462+
all_tasks = await Chats.get_chat_tasks_by_id(__chat_id__)
2463+
2464+
found = False
2465+
for task in all_tasks:
2466+
if task['id'] == id:
2467+
task['status'] = status
2468+
found = True
2469+
break
2470+
2471+
if not found:
2472+
return json.dumps({'error': f'Task with id "{id}" not found'})
2473+
2474+
await Chats.update_chat_tasks_by_id(__chat_id__, all_tasks)
2475+
await _emit_tasks(__event_emitter__, all_tasks)
24802476

24812477
return json.dumps(
2482-
{
2483-
'tasks': all_tasks,
2484-
'summary': {
2485-
'total': len(all_tasks),
2486-
'pending': pending,
2487-
'in_progress': in_progress,
2488-
'completed': completed,
2489-
'cancelled': cancelled,
2490-
},
2491-
},
2478+
{'tasks': all_tasks, 'summary': _task_summary(all_tasks)},
24922479
ensure_ascii=False,
24932480
)
24942481
except Exception as e:
2495-
log.exception(f'tasks error: {e}')
2482+
log.exception(f'update_task_status error: {e}')
24962483
return json.dumps({'error': str(e)})
24972484

24982485

backend/open_webui/utils/tools.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
view_file,
8787
view_knowledge_file,
8888
view_skill,
89-
tasks,
89+
create_tasks,
90+
update_task,
9091
create_automation,
9192
update_automation,
9293
list_automations,
@@ -543,7 +544,7 @@ async def has_user_permission(feature_key: str) -> bool:
543544

544545
# Task management - break down complex work into trackable steps
545546
if is_builtin_tool_enabled('tasks'):
546-
builtin_functions.append(tasks)
547+
builtin_functions.extend([create_tasks, update_task])
547548

548549
# Automation tools - create and manage scheduled automations from chat
549550
if is_builtin_tool_enabled('automations') and await has_user_permission('automations'):

0 commit comments

Comments
 (0)