mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-18 20:53:16 +08:00
fix(chat): persist/split <thinking>/<thought> blocks like stream UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7ea02e66ca
commit
46ebfeb54f
3 changed files with 36 additions and 3 deletions
|
|
@ -1547,10 +1547,12 @@ function hydrateMermaidIn(root) {
|
||||||
|
|
||||||
const RE_REDACTED_THINKING = new RegExp("<redacted_thinking>\\s*([\\s\\S]*?)\\s*</redacted_thinking>", "i");
|
const RE_REDACTED_THINKING = new RegExp("<redacted_thinking>\\s*([\\s\\S]*?)\\s*</redacted_thinking>", "i");
|
||||||
const RE_THINK_TAG = new RegExp("<think\\s*>\\s*([\\s\\S]*?)\\s*</think\\s*>", "i");
|
const RE_THINK_TAG = new RegExp("<think\\s*>\\s*([\\s\\S]*?)\\s*</think\\s*>", "i");
|
||||||
|
const RE_THINKING_TAG = new RegExp("<thinking\\s*>\\s*([\\s\\S]*?)\\s*</thinking\\s*>", "i");
|
||||||
|
const RE_THOUGHT_TAG = new RegExp("<thought\\s*>\\s*([\\s\\S]*?)\\s*</thought\\s*>", "i");
|
||||||
|
|
||||||
function _findEarliestReasoningBlock(remaining) {
|
function _findEarliestReasoningBlock(remaining) {
|
||||||
let best = null;
|
let best = null;
|
||||||
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG]) {
|
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG, RE_THINKING_TAG, RE_THOUGHT_TAG]) {
|
||||||
const m = re.exec(remaining);
|
const m = re.exec(remaining);
|
||||||
re.lastIndex = 0;
|
re.lastIndex = 0;
|
||||||
if (!m) continue;
|
if (!m) continue;
|
||||||
|
|
@ -1657,7 +1659,7 @@ function extractThinkingFromText(text) {
|
||||||
const raw = String(text || "");
|
const raw = String(text || "");
|
||||||
if (!raw) return "";
|
if (!raw) return "";
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG]) {
|
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG, RE_THINKING_TAG, RE_THOUGHT_TAG]) {
|
||||||
const rr = new RegExp(re.source, "gi");
|
const rr = new RegExp(re.source, "gi");
|
||||||
for (const m of raw.matchAll(rr)) {
|
for (const m of raw.matchAll(rr)) {
|
||||||
const inner = String(m[1] || "").trim();
|
const inner = String(m[1] || "").trim();
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ _DIRECT_LOOP_OC_STAGE: dict[str, str] = {
|
||||||
"tool_result_context_guard": "tool_context_guard",
|
"tool_result_context_guard": "tool_context_guard",
|
||||||
"tool_pairing_guard": "tool_pairing_guard",
|
"tool_pairing_guard": "tool_pairing_guard",
|
||||||
}
|
}
|
||||||
_THINK_BLOCK_RE = re.compile(r"<(think|redacted_thinking)>\s*(.*?)\s*</\1>\s*", flags=re.IGNORECASE | re.DOTALL)
|
_THINK_BLOCK_RE = re.compile(
|
||||||
|
r"<\s*(?:antml:)?(think|thinking|thought|redacted_thinking)\s*>\s*(.*?)\s*</\s*(?:antml:)?\1\s*>",
|
||||||
|
flags=re.IGNORECASE | re.DOTALL,
|
||||||
|
)
|
||||||
_DSML_INVOKE_NAME_RE = re.compile(r"invoke\s+name\s*=\s*['\"]([^'\"\s>]+)['\"]", flags=re.IGNORECASE)
|
_DSML_INVOKE_NAME_RE = re.compile(r"invoke\s+name\s*=\s*['\"]([^'\"\s>]+)['\"]", flags=re.IGNORECASE)
|
||||||
_JSON_TOOL_NAME_RE = re.compile(r"['\"]name['\"]\s*:\s*['\"]([^'\"\s]{1,120})['\"]", flags=re.IGNORECASE)
|
_JSON_TOOL_NAME_RE = re.compile(r"['\"]name['\"]\s*:\s*['\"]([^'\"\s]{1,120})['\"]", flags=re.IGNORECASE)
|
||||||
_TOOL_WIRE_CACHE_LOCK = threading.Lock()
|
_TOOL_WIRE_CACHE_LOCK = threading.Lock()
|
||||||
|
|
|
||||||
28
tests/test_direct_loop_reasoning_split.py
Normal file
28
tests/test_direct_loop_reasoning_split.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from oclaw.runtime.direct_loop import _split_reasoning_and_body
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_reasoning_extracts_thinking_tags() -> None:
|
||||||
|
chunks, body = _split_reasoning_and_body("<thinking>\ninner\n</thinking>\nvisible")
|
||||||
|
assert chunks == ["inner"]
|
||||||
|
assert body == "visible"
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_reasoning_extracts_thought_tags() -> None:
|
||||||
|
chunks, body = _split_reasoning_and_body("<thought>x</thought>after")
|
||||||
|
assert chunks == ["x"]
|
||||||
|
assert body == "after"
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_reasoning_explicit_when_no_tags() -> None:
|
||||||
|
chunks, body = _split_reasoning_and_body("body only", explicit_reasoning=" rc ")
|
||||||
|
assert chunks == ["rc"]
|
||||||
|
assert body == "body only"
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_reasoning_antml_optional_prefix() -> None:
|
||||||
|
raw = "<antml:thinking>\nZ\n</antml:thinking>\nok"
|
||||||
|
chunks, body = _split_reasoning_and_body(raw)
|
||||||
|
assert chunks == ["Z"]
|
||||||
|
assert body == "ok"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue