refac
This commit is contained in:
@@ -73,6 +73,7 @@ from open_webui.models.models import Models
|
|||||||
from open_webui.retrieval.utils import get_sources_from_items
|
from open_webui.retrieval.utils import get_sources_from_items
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.utils.sanitize import strip_markdown_code_fences
|
||||||
from open_webui.utils.chat import generate_chat_completion
|
from open_webui.utils.chat import generate_chat_completion
|
||||||
from open_webui.utils.task import (
|
from open_webui.utils.task import (
|
||||||
get_task_model_id,
|
get_task_model_id,
|
||||||
@@ -1408,12 +1409,12 @@ async def convert_url_images_to_base64(form_data):
|
|||||||
def process_messages_with_output(messages: list[dict]) -> list[dict]:
|
def process_messages_with_output(messages: list[dict]) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Process messages with OR-aligned output items for LLM consumption.
|
Process messages with OR-aligned output items for LLM consumption.
|
||||||
|
|
||||||
For assistant messages with 'output' field, produces properly formatted
|
For assistant messages with 'output' field, produces properly formatted
|
||||||
OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM.
|
OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM.
|
||||||
"""
|
"""
|
||||||
processed = []
|
processed = []
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message.get("role") == "assistant" and message.get("output"):
|
if message.get("role") == "assistant" and message.get("output"):
|
||||||
# Use output items for clean OpenAI-format messages
|
# Use output items for clean OpenAI-format messages
|
||||||
@@ -1421,11 +1422,11 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]:
|
|||||||
if output_messages:
|
if output_messages:
|
||||||
processed.extend(output_messages)
|
processed.extend(output_messages)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Strip 'output' field before adding (LLM shouldn't see it)
|
# Strip 'output' field before adding (LLM shouldn't see it)
|
||||||
clean_message = {k: v for k, v in message.items() if k != "output"}
|
clean_message = {k: v for k, v in message.items() if k != "output"}
|
||||||
processed.append(clean_message)
|
processed.append(clean_message)
|
||||||
|
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
|
|
||||||
@@ -1841,9 +1842,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
# Inject builtin tools for native function calling based on enabled features and model capability
|
# Inject builtin tools for native function calling based on enabled features and model capability
|
||||||
# Check if builtin_tools capability is enabled for this model (defaults to True if not specified)
|
# Check if builtin_tools capability is enabled for this model (defaults to True if not specified)
|
||||||
builtin_tools_enabled = (
|
builtin_tools_enabled = (
|
||||||
(model.get("info", {}).get("meta", {}).get("capabilities") or {})
|
model.get("info", {}).get("meta", {}).get("capabilities") or {}
|
||||||
.get("builtin_tools", True)
|
).get("builtin_tools", True)
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
metadata.get("params", {}).get("function_calling") == "native"
|
metadata.get("params", {}).get("function_calling") == "native"
|
||||||
and builtin_tools_enabled
|
and builtin_tools_enabled
|
||||||
@@ -1887,9 +1887,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
|
|
||||||
# Check if file context extraction is enabled for this model (default True)
|
# Check if file context extraction is enabled for this model (default True)
|
||||||
file_context_enabled = (
|
file_context_enabled = (
|
||||||
(model.get("info", {}).get("meta", {}).get("capabilities") or {})
|
model.get("info", {}).get("meta", {}).get("capabilities") or {}
|
||||||
.get("file_context", True)
|
).get("file_context", True)
|
||||||
)
|
|
||||||
|
|
||||||
if file_context_enabled:
|
if file_context_enabled:
|
||||||
try:
|
try:
|
||||||
@@ -2238,7 +2237,7 @@ async def process_chat_response(
|
|||||||
)
|
)
|
||||||
|
|
||||||
title = Chats.get_chat_title_by_id(metadata["chat_id"])
|
title = Chats.get_chat_title_by_id(metadata["chat_id"])
|
||||||
|
|
||||||
# Use output from backend if provided (OR-compliant backends)
|
# Use output from backend if provided (OR-compliant backends)
|
||||||
response_output = response_data.get("output")
|
response_output = response_data.get("output")
|
||||||
|
|
||||||
@@ -2248,7 +2247,11 @@ async def process_chat_response(
|
|||||||
"data": {
|
"data": {
|
||||||
"done": True,
|
"done": True,
|
||||||
"content": content,
|
"content": content,
|
||||||
**({"output": response_output} if response_output else {}),
|
**(
|
||||||
|
{"output": response_output}
|
||||||
|
if response_output
|
||||||
|
else {}
|
||||||
|
),
|
||||||
"title": title,
|
"title": title,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2261,7 +2264,11 @@ async def process_chat_response(
|
|||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": content,
|
"content": content,
|
||||||
**({"output": response_output} if response_output else {}),
|
**(
|
||||||
|
{"output": response_output}
|
||||||
|
if response_output
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2521,33 +2528,33 @@ async def process_chat_response(
|
|||||||
For LLM consumption, use convert_output_to_messages() instead.
|
For LLM consumption, use convert_output_to_messages() instead.
|
||||||
"""
|
"""
|
||||||
content = ""
|
content = ""
|
||||||
|
|
||||||
# First pass: collect function_call_output items by call_id for lookup
|
# First pass: collect function_call_output items by call_id for lookup
|
||||||
tool_outputs = {}
|
tool_outputs = {}
|
||||||
for item in output:
|
for item in output:
|
||||||
if item.get("type") == "function_call_output":
|
if item.get("type") == "function_call_output":
|
||||||
tool_outputs[item.get("call_id")] = item
|
tool_outputs[item.get("call_id")] = item
|
||||||
|
|
||||||
# Second pass: render items in order
|
# Second pass: render items in order
|
||||||
for item in output:
|
for item in output:
|
||||||
item_type = item.get("type", "")
|
item_type = item.get("type", "")
|
||||||
|
|
||||||
if item_type == "message":
|
if item_type == "message":
|
||||||
for content_part in item.get("content", []):
|
for content_part in item.get("content", []):
|
||||||
if content_part.get("type") == "output_text":
|
if content_part.get("type") == "output_text":
|
||||||
text = content_part.get("text", "").strip()
|
text = content_part.get("text", "").strip()
|
||||||
if text:
|
if text:
|
||||||
content = f"{content}{text}\n"
|
content = f"{content}{text}\n"
|
||||||
|
|
||||||
elif item_type == "function_call":
|
elif item_type == "function_call":
|
||||||
# Render tool call inline with its result (if available)
|
# Render tool call inline with its result (if available)
|
||||||
if content and not content.endswith("\n"):
|
if content and not content.endswith("\n"):
|
||||||
content += "\n"
|
content += "\n"
|
||||||
|
|
||||||
call_id = item.get("call_id", "")
|
call_id = item.get("call_id", "")
|
||||||
name = item.get("name", "")
|
name = item.get("name", "")
|
||||||
arguments = item.get("arguments", "")
|
arguments = item.get("arguments", "")
|
||||||
|
|
||||||
result_item = tool_outputs.get(call_id)
|
result_item = tool_outputs.get(call_id)
|
||||||
if result_item:
|
if result_item:
|
||||||
result_text = ""
|
result_text = ""
|
||||||
@@ -2556,59 +2563,65 @@ async def process_chat_response(
|
|||||||
result_text += out.get("text", "")
|
result_text += out.get("text", "")
|
||||||
files = result_item.get("files")
|
files = result_item.get("files")
|
||||||
embeds = result_item.get("embeds", "")
|
embeds = result_item.get("embeds", "")
|
||||||
|
|
||||||
content += f'<details type="tool_calls" done="true" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}" result="{html.escape(json.dumps(result_text, ensure_ascii=False))}" files="{html.escape(json.dumps(files)) if files else ""}" embeds="{html.escape(json.dumps(embeds))}">\n<summary>Tool Executed</summary>\n</details>\n'
|
content += f'<details type="tool_calls" done="true" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}" result="{html.escape(json.dumps(result_text, ensure_ascii=False))}" files="{html.escape(json.dumps(files)) if files else ""}" embeds="{html.escape(json.dumps(embeds))}">\n<summary>Tool Executed</summary>\n</details>\n'
|
||||||
else:
|
else:
|
||||||
content += f'<details type="tool_calls" done="false" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
content += f'<details type="tool_calls" done="false" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
||||||
|
|
||||||
elif item_type == "function_call_output":
|
elif item_type == "function_call_output":
|
||||||
# Already handled inline with function_call above
|
# Already handled inline with function_call above
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif item_type == "reasoning":
|
elif item_type == "reasoning":
|
||||||
reasoning_content = ""
|
reasoning_content = ""
|
||||||
for content_part in item.get("content", []):
|
for content_part in item.get("content", []):
|
||||||
if content_part.get("type") == "output_text":
|
if content_part.get("type") == "output_text":
|
||||||
reasoning_content = content_part.get("text", "").strip()
|
reasoning_content = content_part.get("text", "").strip()
|
||||||
|
|
||||||
duration = item.get("duration")
|
duration = item.get("duration")
|
||||||
status = item.get("status", "in_progress")
|
status = item.get("status", "in_progress")
|
||||||
|
|
||||||
if content and not content.endswith("\n"):
|
if content and not content.endswith("\n"):
|
||||||
content += "\n"
|
content += "\n"
|
||||||
|
|
||||||
display = html.escape("\n".join(
|
display = html.escape(
|
||||||
(f"> {line}" if not line.startswith(">") else line)
|
"\n".join(
|
||||||
for line in reasoning_content.splitlines()
|
(f"> {line}" if not line.startswith(">") else line)
|
||||||
))
|
for line in reasoning_content.splitlines()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if status == "completed" or duration is not None:
|
if status == "completed" or duration is not None:
|
||||||
content = f'{content}<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n'
|
content = f'{content}<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n'
|
||||||
else:
|
else:
|
||||||
content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n'
|
content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n'
|
||||||
|
|
||||||
elif item_type == "open_webui:code_interpreter":
|
elif item_type == "open_webui:code_interpreter":
|
||||||
code = item.get("code", "")
|
code = item.get("code", "")
|
||||||
output_val = item.get("output")
|
output_val = item.get("output")
|
||||||
lang = item.get("lang", "")
|
lang = item.get("lang", "")
|
||||||
|
|
||||||
content_stripped, original_whitespace = split_content_and_whitespace(content)
|
content_stripped, original_whitespace = (
|
||||||
|
split_content_and_whitespace(content)
|
||||||
|
)
|
||||||
if is_opening_code_block(content_stripped):
|
if is_opening_code_block(content_stripped):
|
||||||
content = content_stripped.rstrip("`").rstrip() + original_whitespace
|
content = (
|
||||||
|
content_stripped.rstrip("`").rstrip()
|
||||||
|
+ original_whitespace
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
content = content_stripped + original_whitespace
|
content = content_stripped + original_whitespace
|
||||||
|
|
||||||
if content and not content.endswith("\n"):
|
if content and not content.endswith("\n"):
|
||||||
content += "\n"
|
content += "\n"
|
||||||
|
|
||||||
if output_val:
|
if output_val:
|
||||||
output_escaped = html.escape(json.dumps(output_val))
|
output_escaped = html.escape(json.dumps(output_val))
|
||||||
content = f'{content}<details type="code_interpreter" done="true" output="{output_escaped}">\n<summary>Analyzed</summary>\n```{lang}\n{code}\n```\n</details>\n'
|
content = f'{content}<details type="code_interpreter" done="true" output="{output_escaped}">\n<summary>Analyzed</summary>\n```{lang}\n{code}\n```\n</details>\n'
|
||||||
else:
|
else:
|
||||||
content = f'{content}<details type="code_interpreter" done="false">\n<summary>Analyzing...</summary>\n```{lang}\n{code}\n```\n</details>\n'
|
content = f'{content}<details type="code_interpreter" done="false">\n<summary>Analyzing...</summary>\n```{lang}\n{code}\n```\n</details>\n'
|
||||||
|
|
||||||
return content.strip()
|
|
||||||
|
|
||||||
|
return content.strip()
|
||||||
|
|
||||||
def convert_content_blocks_to_messages(content_blocks, raw=False):
|
def convert_content_blocks_to_messages(content_blocks, raw=False):
|
||||||
messages = []
|
messages = []
|
||||||
@@ -2668,13 +2681,17 @@ async def process_chat_response(
|
|||||||
if block_type == "text":
|
if block_type == "text":
|
||||||
text_content = block.get("content", "").strip()
|
text_content = block.get("content", "").strip()
|
||||||
if text_content:
|
if text_content:
|
||||||
output_items.append({
|
output_items.append(
|
||||||
"type": "message",
|
{
|
||||||
"id": block_id or next_id("msg"),
|
"type": "message",
|
||||||
"status": "completed",
|
"id": block_id or next_id("msg"),
|
||||||
"role": "assistant",
|
"status": "completed",
|
||||||
"content": [{"type": "output_text", "text": text_content}],
|
"role": "assistant",
|
||||||
})
|
"content": [
|
||||||
|
{"type": "output_text", "text": text_content}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
elif block_type == "tool_calls":
|
elif block_type == "tool_calls":
|
||||||
tool_calls = block.get("content", [])
|
tool_calls = block.get("content", [])
|
||||||
@@ -2684,50 +2701,86 @@ async def process_chat_response(
|
|||||||
for tool_call in tool_calls:
|
for tool_call in tool_calls:
|
||||||
call_id = tool_call.get("id", "")
|
call_id = tool_call.get("id", "")
|
||||||
func = tool_call.get("function", {})
|
func = tool_call.get("function", {})
|
||||||
output_items.append({
|
output_items.append(
|
||||||
"type": "function_call",
|
{
|
||||||
"id": call_id or next_id("fc"), # Use call_id as item id if available
|
"type": "function_call",
|
||||||
"call_id": call_id,
|
"id": call_id
|
||||||
"name": func.get("name", ""),
|
or next_id(
|
||||||
"arguments": func.get("arguments", "{}"),
|
"fc"
|
||||||
"status": "completed" if results else "in_progress",
|
), # Use call_id as item id if available
|
||||||
})
|
"call_id": call_id,
|
||||||
|
"name": func.get("name", ""),
|
||||||
|
"arguments": func.get("arguments", "{}"),
|
||||||
|
"status": "completed" if results else "in_progress",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Emit function_call_output items
|
# Emit function_call_output items
|
||||||
for result in results:
|
for result in results:
|
||||||
output_items.append({
|
output_items.append(
|
||||||
"type": "function_call_output",
|
{
|
||||||
"id": result.get("id") or next_id("fco"),
|
"type": "function_call_output",
|
||||||
"call_id": result.get("tool_call_id", ""),
|
"id": result.get("id") or next_id("fco"),
|
||||||
"output": [{"type": "input_text", "text": result.get("content", "")}],
|
"call_id": result.get("tool_call_id", ""),
|
||||||
"status": "completed",
|
"output": [
|
||||||
**({"files": result.get("files")} if result.get("files") else {}),
|
{
|
||||||
**({"embeds": result.get("embeds")} if result.get("embeds") else {}),
|
"type": "input_text",
|
||||||
})
|
"text": result.get("content", ""),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "completed",
|
||||||
|
**(
|
||||||
|
{"files": result.get("files")}
|
||||||
|
if result.get("files")
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
**(
|
||||||
|
{"embeds": result.get("embeds")}
|
||||||
|
if result.get("embeds")
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
elif block_type == "reasoning":
|
elif block_type == "reasoning":
|
||||||
reasoning_content = block.get("content", "").strip()
|
reasoning_content = block.get("content", "").strip()
|
||||||
duration = block.get("duration")
|
duration = block.get("duration")
|
||||||
output_items.append({
|
output_items.append(
|
||||||
"type": "reasoning",
|
{
|
||||||
"id": block_id or next_id("r"),
|
"type": "reasoning",
|
||||||
"status": "completed" if duration is not None else "in_progress",
|
"id": block_id or next_id("r"),
|
||||||
"content": [{"type": "output_text", "text": reasoning_content}] if reasoning_content else None,
|
"status": (
|
||||||
"summary": None,
|
"completed"
|
||||||
})
|
if duration is not None
|
||||||
|
else "in_progress"
|
||||||
|
),
|
||||||
|
"content": (
|
||||||
|
[{"type": "output_text", "text": reasoning_content}]
|
||||||
|
if reasoning_content
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"summary": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
elif block_type == "code_interpreter":
|
elif block_type == "code_interpreter":
|
||||||
code = block.get("content", "")
|
code = block.get("content", "")
|
||||||
output_val = block.get("output")
|
output_val = block.get("output")
|
||||||
attrs = block.get("attributes", {})
|
attrs = block.get("attributes", {})
|
||||||
output_items.append({
|
output_items.append(
|
||||||
"type": "open_webui:code_interpreter",
|
{
|
||||||
"id": block_id or next_id("ci"),
|
"type": "open_webui:code_interpreter",
|
||||||
"status": "completed" if output_val is not None else "in_progress",
|
"id": block_id or next_id("ci"),
|
||||||
"lang": attrs.get("lang", ""),
|
"status": (
|
||||||
"code": code,
|
"completed"
|
||||||
"output": output_val,
|
if output_val is not None
|
||||||
})
|
else "in_progress"
|
||||||
|
),
|
||||||
|
"lang": attrs.get("lang", ""),
|
||||||
|
"code": code,
|
||||||
|
"output": output_val,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return output_items
|
return output_items
|
||||||
|
|
||||||
@@ -2934,14 +2987,16 @@ async def process_chat_response(
|
|||||||
output = existing_output
|
output = existing_output
|
||||||
else:
|
else:
|
||||||
# Always create an initial message item (even if content is empty)
|
# Always create an initial message item (even if content is empty)
|
||||||
output = [{
|
output = [
|
||||||
"type": "message",
|
{
|
||||||
"id": output_id("msg"),
|
"type": "message",
|
||||||
"status": "in_progress",
|
"id": output_id("msg"),
|
||||||
"role": "assistant",
|
"status": "in_progress",
|
||||||
"content": [{"type": "output_text", "text": content}],
|
"role": "assistant",
|
||||||
}]
|
"content": [{"type": "output_text", "text": content}],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
# Keep content_blocks for backward compatibility during transition
|
# Keep content_blocks for backward compatibility during transition
|
||||||
content_blocks = [
|
content_blocks = [
|
||||||
{
|
{
|
||||||
@@ -3354,7 +3409,9 @@ async def process_chat_response(
|
|||||||
|
|
||||||
if ENABLE_REALTIME_CHAT_SAVE:
|
if ENABLE_REALTIME_CHAT_SAVE:
|
||||||
# Save message in the database
|
# Save message in the database
|
||||||
output = convert_content_blocks_to_output(content_blocks)
|
output = convert_content_blocks_to_output(
|
||||||
|
content_blocks
|
||||||
|
)
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata["chat_id"],
|
metadata["chat_id"],
|
||||||
metadata["message_id"],
|
metadata["message_id"],
|
||||||
@@ -3692,6 +3749,9 @@ async def process_chat_response(
|
|||||||
try:
|
try:
|
||||||
if content_blocks[-1]["attributes"].get("type") == "code":
|
if content_blocks[-1]["attributes"].get("type") == "code":
|
||||||
code = content_blocks[-1]["content"]
|
code = content_blocks[-1]["content"]
|
||||||
|
# Strip markdown fences if model included them
|
||||||
|
code = strip_markdown_code_fences(code)
|
||||||
|
|
||||||
if CODE_INTERPRETER_BLOCKED_MODULES:
|
if CODE_INTERPRETER_BLOCKED_MODULES:
|
||||||
blocking_code = textwrap.dedent(
|
blocking_code = textwrap.dedent(
|
||||||
f"""
|
f"""
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def strip_markdown_code_fences(code: str) -> str:
|
||||||
|
"""
|
||||||
|
Strip markdown code fences if present.
|
||||||
|
|
||||||
|
This is a defensive, non-breaking change — if the code doesn't
|
||||||
|
contain fences, it passes through unchanged.
|
||||||
|
|
||||||
|
Handles patterns like:
|
||||||
|
- ```python
|
||||||
|
- ```py
|
||||||
|
- ```
|
||||||
|
"""
|
||||||
|
code = code.strip()
|
||||||
|
# Remove opening fence (```python, ```py, ``` etc.)
|
||||||
|
code = re.sub(r"^```\w*\n?", "", code)
|
||||||
|
# Remove closing fence
|
||||||
|
code = re.sub(r"\n?```\s*$", "", code)
|
||||||
|
return code.strip()
|
||||||
Reference in New Issue
Block a user