Skip to content

Commit 9d8f590

Browse files
committed
feat: support pipe filters for per-message content truncation in task templates
Add |middletruncate:n, |start:n, and |end:n pipe filters to the {{MESSAGES}} template variable, enabling per-message character truncation for task models (title, tags, follow-up, etc.). Example: {{MESSAGES:END:2|middletruncate:500}} This optimizes task model prompt size for conversations with very long messages (e.g. pasted documents), reducing latency for local models and API costs. Closes open-webui#21499
1 parent defeddf commit 9d8f590

1 file changed

Lines changed: 105 additions & 21 deletions

File tree

backend/open_webui/utils/task.py

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,41 +142,125 @@ def replacement_function(match):
142142
return template
143143

144144

145+
def truncate_content(content: str, max_chars: int, mode: str = "middletruncate") -> str:
146+
"""Truncate a string to max_chars using the specified mode.
147+
148+
Modes:
149+
- middletruncate: keep beginning and end, join with '...'
150+
- start: keep first max_chars characters
151+
- end: keep last max_chars characters
152+
"""
153+
if not content or len(content) <= max_chars:
154+
return content
155+
156+
if mode == "start":
157+
return content[:max_chars]
158+
elif mode == "end":
159+
return content[-max_chars:]
160+
else: # middletruncate
161+
half = max_chars // 2
162+
return f"{content[:half]}...{content[-(max_chars - half):]}"
163+
164+
165+
def apply_content_filter(
166+
messages: list[dict], filter_str: str
167+
) -> list[dict]:
168+
"""Apply a content filter to each message's content.
169+
170+
filter_str is like 'middletruncate:500', 'start:200', or 'end:200'.
171+
Returns a new list with truncated content (original messages are not mutated).
172+
"""
173+
parts = filter_str.split(":")
174+
if len(parts) != 2:
175+
return messages
176+
177+
mode = parts[0].lower()
178+
try:
179+
max_chars = int(parts[1])
180+
except ValueError:
181+
return messages
182+
183+
if mode not in ("middletruncate", "start", "end"):
184+
return messages
185+
186+
result = []
187+
for msg in messages:
188+
new_msg = dict(msg)
189+
if isinstance(new_msg.get("content"), str):
190+
new_msg["content"] = truncate_content(new_msg["content"], max_chars, mode)
191+
elif isinstance(new_msg.get("content"), list):
192+
new_content = []
193+
for item in new_msg["content"]:
194+
if isinstance(item, dict) and item.get("type") == "text":
195+
new_item = dict(item)
196+
new_item["text"] = truncate_content(
197+
item.get("text", ""), max_chars, mode
198+
)
199+
new_content.append(new_item)
200+
else:
201+
new_content.append(item)
202+
new_msg["content"] = new_content
203+
result.append(new_msg)
204+
return result
205+
206+
145207
def replace_messages_variable(
146208
template: str, messages: Optional[list[dict]] = None
147209
) -> str:
148210
def replacement_function(match):
149-
full_match = match.group(0)
150-
start_length = match.group(1)
151-
end_length = match.group(2)
152-
middle_length = match.group(3)
211+
# Groups: (1) filter for bare MESSAGES
212+
# (2) START count, (3) filter for START
213+
# (4) END count, (5) filter for END
214+
# (6) MIDDLE count,(7) filter for MIDDLE
215+
bare_filter = match.group(1)
216+
start_length = match.group(2)
217+
start_filter = match.group(3)
218+
end_length = match.group(4)
219+
end_filter = match.group(5)
220+
middle_length = match.group(6)
221+
middle_filter = match.group(7)
222+
153223
# If messages is None, handle it as an empty list
154224
if messages is None:
155225
return ""
156226

157-
# Process messages based on the number of messages required
158-
if full_match == "{{MESSAGES}}":
159-
return get_messages_content(messages)
160-
elif start_length is not None:
161-
return get_messages_content(messages[: int(start_length)])
227+
# Select messages based on the variant
228+
if start_length is not None:
229+
selected = messages[: int(start_length)]
230+
content_filter = start_filter
162231
elif end_length is not None:
163-
return get_messages_content(messages[-int(end_length) :])
232+
selected = messages[-int(end_length) :]
233+
content_filter = end_filter
164234
elif middle_length is not None:
165235
mid = int(middle_length)
166-
167236
if len(messages) <= mid:
168-
return get_messages_content(messages)
169-
# Handle middle truncation: split to get start and end portions of the messages list
170-
half = mid // 2
171-
start_msgs = messages[:half]
172-
end_msgs = messages[-half:] if mid % 2 == 0 else messages[-(half + 1) :]
173-
formatted_start = get_messages_content(start_msgs)
174-
formatted_end = get_messages_content(end_msgs)
175-
return f"{formatted_start}\n{formatted_end}"
176-
return ""
237+
selected = messages
238+
else:
239+
half = mid // 2
240+
start_msgs = messages[:half]
241+
end_msgs = (
242+
messages[-half:] if mid % 2 == 0 else messages[-(half + 1) :]
243+
)
244+
selected = start_msgs + end_msgs
245+
content_filter = middle_filter
246+
else:
247+
# Bare {{MESSAGES}} or {{MESSAGES|filter}}
248+
selected = messages
249+
content_filter = bare_filter
250+
251+
# Apply content filter if present
252+
if content_filter:
253+
selected = apply_content_filter(selected, content_filter)
254+
255+
return get_messages_content(selected)
177256

178257
template = re.sub(
179-
r"{{MESSAGES}}|{{MESSAGES:START:(\d+)}}|{{MESSAGES:END:(\d+)}}|{{MESSAGES:MIDDLETRUNCATE:(\d+)}}",
258+
r"(?:"
259+
r"\{\{MESSAGES(?:\|(\w+:\d+))?\}\}"
260+
r"|\{\{MESSAGES:START:(\d+)(?:\|(\w+:\d+))?\}\}"
261+
r"|\{\{MESSAGES:END:(\d+)(?:\|(\w+:\d+))?\}\}"
262+
r"|\{\{MESSAGES:MIDDLETRUNCATE:(\d+)(?:\|(\w+:\d+))?\}\}"
263+
r")",
180264
replacement_function,
181265
template,
182266
)

0 commit comments

Comments
 (0)