Updated the chat UI and backend so MiniCPM thinking renders
Browse files- assets/app.js +79 -12
- assets/markdown.js +31 -0
- assets/server.css +86 -7
- tests/test_research_findings.py +21 -1
- ui/agent/graph/nodes/consolidator.py +3 -2
- ui/agent/graph/nodes/helpers.py +27 -0
- ui/agent/graph/respond.py +12 -2
- ui/agent/graph/state.py +1 -0
- ui/agent/streaming.py +18 -4
assets/app.js
CHANGED
|
@@ -2,7 +2,8 @@ import { gradioPredict, gradioStream } from "/assets/gradio_api.js?v=2";
|
|
| 2 |
import {
|
| 3 |
ensureMarkdownTools,
|
| 4 |
renderMarkdownInto,
|
| 5 |
-
|
|
|
|
| 6 |
|
| 7 |
const CHAT_SESSIONS_KEY = "borderless-chat-sessions";
|
| 8 |
|
|
@@ -530,23 +531,77 @@ function shouldRenderMarkdown(message, isTool) {
|
|
| 530 |
return !isTool;
|
| 531 |
}
|
| 532 |
|
| 533 |
-
async function
|
| 534 |
-
const
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
|
| 537 |
-
|
| 538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
return;
|
| 540 |
}
|
| 541 |
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
}
|
| 548 |
}
|
| 549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
function appendToolLogSection(parent, label, value) {
|
| 551 |
const section = document.createElement("div");
|
| 552 |
section.className = "tool-log-section";
|
|
@@ -648,9 +703,19 @@ async function renderPlanMessage(node, message, metadata) {
|
|
| 648 |
}
|
| 649 |
|
| 650 |
async function renderToolMessage(node, message, metadata, options = {}) {
|
| 651 |
-
const isThinking =
|
|
|
|
| 652 |
if (isThinking) {
|
| 653 |
node.classList.add("thinking");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
}
|
| 655 |
|
| 656 |
if (metadata.display === "plan") {
|
|
@@ -806,6 +871,7 @@ async function renderResearchMessage(parent, message, task) {
|
|
| 806 |
await renderToolMessage(node, message, metadata, { compact: true });
|
| 807 |
} else {
|
| 808 |
const body = document.createElement("div");
|
|
|
|
| 809 |
await renderMessageBody(body, message, false);
|
| 810 |
node.appendChild(body);
|
| 811 |
}
|
|
@@ -966,6 +1032,7 @@ async function renderMessages() {
|
|
| 966 |
}
|
| 967 |
} else {
|
| 968 |
const body = document.createElement("div");
|
|
|
|
| 969 |
await renderMessageBody(body, message, isTool);
|
| 970 |
node.appendChild(body);
|
| 971 |
}
|
|
|
|
| 2 |
import {
|
| 3 |
ensureMarkdownTools,
|
| 4 |
renderMarkdownInto,
|
| 5 |
+
splitThinkingContent,
|
| 6 |
+
} from "/assets/markdown.js?v=2";
|
| 7 |
|
| 8 |
const CHAT_SESSIONS_KEY = "borderless-chat-sessions";
|
| 9 |
|
|
|
|
| 531 |
return !isTool;
|
| 532 |
}
|
| 533 |
|
| 534 |
+
async function renderThinkingBlock(parent, thinkingText, options = {}) {
|
| 535 |
+
const text = String(thinkingText || "").trim();
|
| 536 |
+
if (!text) {
|
| 537 |
+
return null;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
const wrapper = document.createElement("div");
|
| 541 |
+
wrapper.className = "thinking-block-wrap";
|
| 542 |
+
|
| 543 |
+
if (options.label) {
|
| 544 |
+
const label = document.createElement("div");
|
| 545 |
+
label.className = "thinking-block-label";
|
| 546 |
+
label.textContent = options.label;
|
| 547 |
+
wrapper.appendChild(label);
|
| 548 |
+
}
|
| 549 |
|
| 550 |
+
const block = document.createElement("div");
|
| 551 |
+
block.className = "thinking-block markdown-body";
|
| 552 |
+
await renderMarkdownInto(block, text);
|
| 553 |
+
wrapper.appendChild(block);
|
| 554 |
+
parent.appendChild(wrapper);
|
| 555 |
+
return wrapper;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
async function renderAssistantContent(parent, message, isTool) {
|
| 559 |
+
const metadata = message.metadata || {};
|
| 560 |
+
const { thinking, answer } = splitThinkingContent(message.content || "", {
|
| 561 |
+
thinking: metadata.thinking,
|
| 562 |
+
});
|
| 563 |
+
|
| 564 |
+
parent.replaceChildren();
|
| 565 |
+
|
| 566 |
+
if (thinking) {
|
| 567 |
+
await renderThinkingBlock(parent, thinking, {
|
| 568 |
+
label: metadata.display === "thinking" ? null : "Thinking",
|
| 569 |
+
});
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
if (answer) {
|
| 573 |
+
const body = document.createElement("div");
|
| 574 |
+
body.className = "chat-message-body";
|
| 575 |
+
if (shouldRenderMarkdown(message, isTool)) {
|
| 576 |
+
body.classList.add("markdown-body");
|
| 577 |
+
try {
|
| 578 |
+
await renderMarkdownInto(body, answer);
|
| 579 |
+
} catch {
|
| 580 |
+
body.textContent = answer;
|
| 581 |
+
}
|
| 582 |
+
} else {
|
| 583 |
+
body.textContent = answer;
|
| 584 |
+
}
|
| 585 |
+
parent.appendChild(body);
|
| 586 |
return;
|
| 587 |
}
|
| 588 |
|
| 589 |
+
if (thinking && metadata.display === "thinking") {
|
| 590 |
+
return;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
if (!thinking && !answer) {
|
| 594 |
+
const body = document.createElement("div");
|
| 595 |
+
body.className = "chat-message-body";
|
| 596 |
+
body.textContent = message.content || "";
|
| 597 |
+
parent.appendChild(body);
|
| 598 |
}
|
| 599 |
}
|
| 600 |
|
| 601 |
+
async function renderMessageBody(body, message, isTool) {
|
| 602 |
+
await renderAssistantContent(body, message, isTool);
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
function appendToolLogSection(parent, label, value) {
|
| 606 |
const section = document.createElement("div");
|
| 607 |
section.className = "tool-log-section";
|
|
|
|
| 703 |
}
|
| 704 |
|
| 705 |
async function renderToolMessage(node, message, metadata, options = {}) {
|
| 706 |
+
const isThinking =
|
| 707 |
+
metadata.display === "thinking" || metadata.title === "Thinking";
|
| 708 |
if (isThinking) {
|
| 709 |
node.classList.add("thinking");
|
| 710 |
+
const header = document.createElement("div");
|
| 711 |
+
header.className = "thinking-message-header";
|
| 712 |
+
header.textContent = metadata.title || "Thinking";
|
| 713 |
+
node.appendChild(header);
|
| 714 |
+
const panel = document.createElement("div");
|
| 715 |
+
panel.className = "thinking-message";
|
| 716 |
+
await renderAssistantContent(panel, message, true);
|
| 717 |
+
node.appendChild(panel);
|
| 718 |
+
return;
|
| 719 |
}
|
| 720 |
|
| 721 |
if (metadata.display === "plan") {
|
|
|
|
| 871 |
await renderToolMessage(node, message, metadata, { compact: true });
|
| 872 |
} else {
|
| 873 |
const body = document.createElement("div");
|
| 874 |
+
body.className = "assistant-message-content";
|
| 875 |
await renderMessageBody(body, message, false);
|
| 876 |
node.appendChild(body);
|
| 877 |
}
|
|
|
|
| 1032 |
}
|
| 1033 |
} else {
|
| 1034 |
const body = document.createElement("div");
|
| 1035 |
+
body.className = "assistant-message-content";
|
| 1036 |
await renderMessageBody(body, message, isTool);
|
| 1037 |
node.appendChild(body);
|
| 1038 |
}
|
assets/markdown.js
CHANGED
|
@@ -50,3 +50,34 @@ export function plainTextSummary(content, limit = 280) {
|
|
| 50 |
}
|
| 51 |
return `${text.slice(0, limit - 1)}…`;
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
return `${text.slice(0, limit - 1)}…`;
|
| 52 |
}
|
| 53 |
+
|
| 54 |
+
const THINK_OPEN = "<" + "think" + ">";
|
| 55 |
+
const THINK_CLOSE = "</" + "think" + ">";
|
| 56 |
+
|
| 57 |
+
export function splitThinkingContent(fullText, options = {}) {
|
| 58 |
+
const metaThinking = String(options.thinking || "").trim();
|
| 59 |
+
let text = String(fullText || "")
|
| 60 |
+
.replace(/<\|redacted_im_end\|>/g, "")
|
| 61 |
+
.replace(/<think>/g, THINK_OPEN)
|
| 62 |
+
.replace(/<\/redacted_thinking>/g, THINK_CLOSE);
|
| 63 |
+
|
| 64 |
+
let thinking = metaThinking;
|
| 65 |
+
let answer = text.trim();
|
| 66 |
+
const openIdx = text.indexOf(THINK_OPEN);
|
| 67 |
+
const closeIdx = text.indexOf(THINK_CLOSE);
|
| 68 |
+
|
| 69 |
+
if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {
|
| 70 |
+
const extracted = text.slice(openIdx + THINK_OPEN.length, closeIdx).trim();
|
| 71 |
+
thinking = thinking ? `${thinking}\n\n${extracted}` : extracted;
|
| 72 |
+
answer = `${text.slice(0, openIdx)}${text.slice(closeIdx + THINK_CLOSE.length)}`.trim();
|
| 73 |
+
} else if (openIdx !== -1 && closeIdx === -1) {
|
| 74 |
+
const extracted = text.slice(openIdx + THINK_OPEN.length).trim();
|
| 75 |
+
thinking = thinking ? `${thinking}\n\n${extracted}` : extracted;
|
| 76 |
+
answer = text.slice(0, openIdx).trim();
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
thinking: thinking.trim(),
|
| 81 |
+
answer: answer.trim(),
|
| 82 |
+
};
|
| 83 |
+
}
|
assets/server.css
CHANGED
|
@@ -267,10 +267,93 @@ button:disabled {
|
|
| 267 |
}
|
| 268 |
|
| 269 |
.chat-message.tool.thinking {
|
| 270 |
-
background: rgba(
|
| 271 |
-
border-color: rgba(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
font-style: italic;
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
}
|
| 275 |
|
| 276 |
.chat-message.tool details.tool-log {
|
|
@@ -284,10 +367,6 @@ button:disabled {
|
|
| 284 |
list-style-position: outside;
|
| 285 |
}
|
| 286 |
|
| 287 |
-
.chat-message.tool.thinking details.tool-log > summary {
|
| 288 |
-
color: #cbd5e1;
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
.chat-message.tool .tool-summary-text {
|
| 292 |
margin-top: 6px;
|
| 293 |
white-space: pre-wrap;
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
.chat-message.tool.thinking {
|
| 270 |
+
background: rgba(79, 70, 229, 0.08);
|
| 271 |
+
border-color: rgba(99, 102, 241, 0.28);
|
| 272 |
+
font-style: normal;
|
| 273 |
+
color: #e5e7eb;
|
| 274 |
+
padding: 0;
|
| 275 |
+
overflow: hidden;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.thinking-message-header {
|
| 279 |
+
display: flex;
|
| 280 |
+
align-items: center;
|
| 281 |
+
gap: 8px;
|
| 282 |
+
padding: 10px 12px 0;
|
| 283 |
+
font-size: 0.72rem;
|
| 284 |
+
font-weight: 700;
|
| 285 |
+
letter-spacing: 0.12em;
|
| 286 |
+
text-transform: uppercase;
|
| 287 |
+
color: #a5b4fc;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.thinking-message-header::before {
|
| 291 |
+
content: "";
|
| 292 |
+
width: 6px;
|
| 293 |
+
height: 6px;
|
| 294 |
+
border-radius: 50%;
|
| 295 |
+
background: #818cf8;
|
| 296 |
+
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.thinking-message {
|
| 300 |
+
padding: 8px 12px 12px;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.thinking-block-wrap {
|
| 304 |
+
margin-bottom: 10px;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.thinking-block-wrap:last-child {
|
| 308 |
+
margin-bottom: 0;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.thinking-block-label {
|
| 312 |
+
margin-bottom: 6px;
|
| 313 |
+
font-size: 0.72rem;
|
| 314 |
+
font-weight: 700;
|
| 315 |
+
letter-spacing: 0.1em;
|
| 316 |
+
text-transform: uppercase;
|
| 317 |
+
color: #a5b4fc;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.thinking-block {
|
| 321 |
+
background: rgba(79, 70, 229, 0.12);
|
| 322 |
+
border-left: 3px solid #6366f1;
|
| 323 |
+
border-radius: 4px 12px 12px 4px;
|
| 324 |
+
padding: 12px 14px;
|
| 325 |
+
font-size: 0.92rem;
|
| 326 |
+
color: #c7d2fe;
|
| 327 |
font-style: italic;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.thinking-block.markdown-body {
|
| 331 |
+
white-space: normal;
|
| 332 |
+
font-style: italic;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.thinking-block.markdown-body p,
|
| 336 |
+
.thinking-block.markdown-body li {
|
| 337 |
+
color: #c7d2fe;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.thinking-block.markdown-body code {
|
| 341 |
+
color: #e0e7ff;
|
| 342 |
+
background: rgba(15, 23, 42, 0.35);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.assistant-message-content .thinking-block-wrap + .chat-message-body {
|
| 346 |
+
margin-top: 4px;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.chat-message.assistant .assistant-message-content {
|
| 350 |
+
display: flex;
|
| 351 |
+
flex-direction: column;
|
| 352 |
+
gap: 0;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.chat-message.tool.thinking details.tool-log > summary {
|
| 356 |
+
color: #cbd5e1;
|
| 357 |
}
|
| 358 |
|
| 359 |
.chat-message.tool details.tool-log {
|
|
|
|
| 367 |
list-style-position: outside;
|
| 368 |
}
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
.chat-message.tool .tool-summary-text {
|
| 371 |
margin-top: 6px;
|
| 372 |
white-space: pre-wrap;
|
tests/test_research_findings.py
CHANGED
|
@@ -11,7 +11,11 @@ os.environ.setdefault("BORDERLESS_PRELOAD_MODEL", "0")
|
|
| 11 |
|
| 12 |
from langchain_core.messages import AIMessage, ToolMessage
|
| 13 |
|
| 14 |
-
from ui.agent.graph.nodes.helpers import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
from ui.agent.synthesis import synthesize_finding_from_tool_messages
|
| 16 |
|
| 17 |
|
|
@@ -52,6 +56,22 @@ class ExtractAssistantTextTests(unittest.TestCase):
|
|
| 52 |
additional_kwargs={"reasoning_content": "Hidden reasoning"},
|
| 53 |
)
|
| 54 |
self.assertEqual(extract_assistant_text(message), "Visible answer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
class ResearchToolCallsTests(unittest.TestCase):
|
|
|
|
| 11 |
|
| 12 |
from langchain_core.messages import AIMessage, ToolMessage
|
| 13 |
|
| 14 |
+
from ui.agent.graph.nodes.helpers import (
|
| 15 |
+
extract_assistant_text,
|
| 16 |
+
extract_thinking_text,
|
| 17 |
+
research_tool_calls,
|
| 18 |
+
)
|
| 19 |
from ui.agent.synthesis import synthesize_finding_from_tool_messages
|
| 20 |
|
| 21 |
|
|
|
|
| 56 |
additional_kwargs={"reasoning_content": "Hidden reasoning"},
|
| 57 |
)
|
| 58 |
self.assertEqual(extract_assistant_text(message), "Visible answer")
|
| 59 |
+
self.assertEqual(extract_thinking_text(message), "Hidden reasoning")
|
| 60 |
+
|
| 61 |
+
def test_extract_thinking_text_from_think_tags(self) -> None:
|
| 62 |
+
open_tag = "<" + "think" + ">"
|
| 63 |
+
close_tag = "</" + "think" + ">"
|
| 64 |
+
message = AIMessage(
|
| 65 |
+
content=(
|
| 66 |
+
f"{open_tag}Compare Canada and Germany pathways.{close_tag}\n\n"
|
| 67 |
+
"## Recommended Countries\n- Canada"
|
| 68 |
+
)
|
| 69 |
+
)
|
| 70 |
+
self.assertIn("Canada", extract_assistant_text(message))
|
| 71 |
+
self.assertEqual(
|
| 72 |
+
extract_thinking_text(message),
|
| 73 |
+
"Compare Canada and Germany pathways.",
|
| 74 |
+
)
|
| 75 |
|
| 76 |
|
| 77 |
class ResearchToolCallsTests(unittest.TestCase):
|
ui/agent/graph/nodes/consolidator.py
CHANGED
|
@@ -10,7 +10,7 @@ from ..llm import build_llm
|
|
| 10 |
from ..state import AgentState
|
| 11 |
from ...synthesis import build_structured_final_answer
|
| 12 |
from .config import CONSOLIDATOR_MAX_TOKENS, FINDING_SUMMARY_LIMIT
|
| 13 |
-
from .helpers import extract_assistant_text, format_todo_label
|
| 14 |
from .prompts import CONSOLIDATOR_SYSTEM_PROMPT
|
| 15 |
|
| 16 |
|
|
@@ -43,6 +43,7 @@ def consolidator_node(state: AgentState, config: RunnableConfig) -> dict[str, An
|
|
| 43 |
]
|
| 44 |
response = llm.invoke(messages)
|
| 45 |
answer = extract_assistant_text(response)
|
|
|
|
| 46 |
if not answer:
|
| 47 |
answer = build_structured_final_answer(
|
| 48 |
profile_summary=str(state.get("profile_summary") or "").strip(),
|
|
@@ -53,4 +54,4 @@ def consolidator_node(state: AgentState, config: RunnableConfig) -> dict[str, An
|
|
| 53 |
"directly from the parallel country research notes below."
|
| 54 |
),
|
| 55 |
)
|
| 56 |
-
return {"final_answer": answer}
|
|
|
|
| 10 |
from ..state import AgentState
|
| 11 |
from ...synthesis import build_structured_final_answer
|
| 12 |
from .config import CONSOLIDATOR_MAX_TOKENS, FINDING_SUMMARY_LIMIT
|
| 13 |
+
from .helpers import extract_assistant_text, extract_thinking_text, format_todo_label
|
| 14 |
from .prompts import CONSOLIDATOR_SYSTEM_PROMPT
|
| 15 |
|
| 16 |
|
|
|
|
| 43 |
]
|
| 44 |
response = llm.invoke(messages)
|
| 45 |
answer = extract_assistant_text(response)
|
| 46 |
+
thinking = extract_thinking_text(response)
|
| 47 |
if not answer:
|
| 48 |
answer = build_structured_final_answer(
|
| 49 |
profile_summary=str(state.get("profile_summary") or "").strip(),
|
|
|
|
| 54 |
"directly from the parallel country research notes below."
|
| 55 |
),
|
| 56 |
)
|
| 57 |
+
return {"final_answer": answer, "consolidator_thinking": thinking}
|
ui/agent/graph/nodes/helpers.py
CHANGED
|
@@ -447,6 +447,33 @@ def extract_assistant_text(message: AIMessage) -> str:
|
|
| 447 |
return ""
|
| 448 |
|
| 449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
def research_tool_calls(
|
| 451 |
response: AIMessage,
|
| 452 |
) -> list[tuple[str, dict[str, Any], str]]:
|
|
|
|
| 447 |
return ""
|
| 448 |
|
| 449 |
|
| 450 |
+
def extract_thinking_text(message: AIMessage) -> str:
|
| 451 |
+
"""Extract model reasoning / thinking text separate from the user-facing answer."""
|
| 452 |
+
content_text, reasoning_text = assistant_text_sources(message)
|
| 453 |
+
answer = extract_assistant_text(message)
|
| 454 |
+
parts: list[str] = []
|
| 455 |
+
|
| 456 |
+
for text in (reasoning_text, content_text):
|
| 457 |
+
if not text.strip():
|
| 458 |
+
continue
|
| 459 |
+
inner_parts = _THINK_INNER_PATTERN.findall(text)
|
| 460 |
+
if inner_parts:
|
| 461 |
+
parts.extend(part.strip() for part in inner_parts if part.strip())
|
| 462 |
+
continue
|
| 463 |
+
outside = _THINK_BLOCK_PATTERN.sub("", text).strip()
|
| 464 |
+
candidate = outside or text.strip()
|
| 465 |
+
if candidate and candidate != answer:
|
| 466 |
+
parts.append(candidate)
|
| 467 |
+
|
| 468 |
+
deduped: list[str] = []
|
| 469 |
+
seen: set[str] = set()
|
| 470 |
+
for part in parts:
|
| 471 |
+
if part not in seen:
|
| 472 |
+
seen.add(part)
|
| 473 |
+
deduped.append(part)
|
| 474 |
+
return "\n\n".join(deduped).strip()
|
| 475 |
+
|
| 476 |
+
|
| 477 |
def research_tool_calls(
|
| 478 |
response: AIMessage,
|
| 479 |
) -> list[tuple[str, dict[str, Any], str]]:
|
ui/agent/graph/respond.py
CHANGED
|
@@ -78,11 +78,17 @@ class _UiState:
|
|
| 78 |
def handle(self, event: dict[str, Any]) -> bool:
|
| 79 |
kind = event.get("type")
|
| 80 |
if kind == "thinking":
|
|
|
|
| 81 |
self.ui_messages.append(
|
| 82 |
ChatMessage(
|
| 83 |
role="assistant",
|
| 84 |
-
content=
|
| 85 |
-
metadata={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
)
|
| 87 |
)
|
| 88 |
return True
|
|
@@ -317,4 +323,8 @@ def respond_with_graph(
|
|
| 317 |
ui_state.ui_messages,
|
| 318 |
final_answer,
|
| 319 |
ui_state.globe_state,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
)
|
|
|
|
| 78 |
def handle(self, event: dict[str, Any]) -> bool:
|
| 79 |
kind = event.get("type")
|
| 80 |
if kind == "thinking":
|
| 81 |
+
thinking_text = str(event.get("text") or "").strip()
|
| 82 |
self.ui_messages.append(
|
| 83 |
ChatMessage(
|
| 84 |
role="assistant",
|
| 85 |
+
content=thinking_text,
|
| 86 |
+
metadata={
|
| 87 |
+
"title": "Thinking",
|
| 88 |
+
"status": "done",
|
| 89 |
+
"display": "thinking",
|
| 90 |
+
"thinking": thinking_text,
|
| 91 |
+
},
|
| 92 |
)
|
| 93 |
)
|
| 94 |
return True
|
|
|
|
| 323 |
ui_state.ui_messages,
|
| 324 |
final_answer,
|
| 325 |
ui_state.globe_state,
|
| 326 |
+
assistant_metadata={
|
| 327 |
+
"markdown": True,
|
| 328 |
+
"thinking": str(last_state.get("consolidator_thinking") or "").strip(),
|
| 329 |
+
},
|
| 330 |
)
|
ui/agent/graph/state.py
CHANGED
|
@@ -37,6 +37,7 @@ class AgentState(TypedDict, total=False):
|
|
| 37 |
todos: list[TodoItem]
|
| 38 |
findings: Annotated[list[Finding], operator.add]
|
| 39 |
final_answer: str
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
class ResearchTask(TypedDict):
|
|
|
|
| 37 |
todos: list[TodoItem]
|
| 38 |
findings: Annotated[list[Finding], operator.add]
|
| 39 |
final_answer: str
|
| 40 |
+
consolidator_thinking: str
|
| 41 |
|
| 42 |
|
| 43 |
class ResearchTask(TypedDict):
|
ui/agent/streaming.py
CHANGED
|
@@ -29,16 +29,23 @@ def yield_streaming_messages(
|
|
| 29 |
ui_messages: list[ChatMessage],
|
| 30 |
text: str,
|
| 31 |
globe_state: dict[str, Any],
|
|
|
|
|
|
|
| 32 |
):
|
| 33 |
messages = list(ui_messages)
|
|
|
|
| 34 |
if not text:
|
| 35 |
-
messages.append(ChatMessage(role="assistant", content=""))
|
| 36 |
yield messages, globe_state
|
| 37 |
return
|
| 38 |
|
| 39 |
-
messages.append(ChatMessage(role="assistant", content=""))
|
| 40 |
for end in _chunk_end_indices(text):
|
| 41 |
-
messages[-1] = ChatMessage(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
yield messages, globe_state
|
| 43 |
|
| 44 |
|
|
@@ -46,8 +53,15 @@ def yield_response(
|
|
| 46 |
ui_messages: list[ChatMessage],
|
| 47 |
text: str,
|
| 48 |
globe_state: dict[str, Any],
|
|
|
|
|
|
|
| 49 |
):
|
| 50 |
if ui_messages:
|
| 51 |
-
yield from yield_streaming_messages(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
else:
|
| 53 |
yield from yield_streaming_string(text, globe_state)
|
|
|
|
| 29 |
ui_messages: list[ChatMessage],
|
| 30 |
text: str,
|
| 31 |
globe_state: dict[str, Any],
|
| 32 |
+
*,
|
| 33 |
+
assistant_metadata: dict[str, Any] | None = None,
|
| 34 |
):
|
| 35 |
messages = list(ui_messages)
|
| 36 |
+
metadata = dict(assistant_metadata or {})
|
| 37 |
if not text:
|
| 38 |
+
messages.append(ChatMessage(role="assistant", content="", metadata=metadata or None))
|
| 39 |
yield messages, globe_state
|
| 40 |
return
|
| 41 |
|
| 42 |
+
messages.append(ChatMessage(role="assistant", content="", metadata=metadata or None))
|
| 43 |
for end in _chunk_end_indices(text):
|
| 44 |
+
messages[-1] = ChatMessage(
|
| 45 |
+
role="assistant",
|
| 46 |
+
content=text[:end],
|
| 47 |
+
metadata=metadata or None,
|
| 48 |
+
)
|
| 49 |
yield messages, globe_state
|
| 50 |
|
| 51 |
|
|
|
|
| 53 |
ui_messages: list[ChatMessage],
|
| 54 |
text: str,
|
| 55 |
globe_state: dict[str, Any],
|
| 56 |
+
*,
|
| 57 |
+
assistant_metadata: dict[str, Any] | None = None,
|
| 58 |
):
|
| 59 |
if ui_messages:
|
| 60 |
+
yield from yield_streaming_messages(
|
| 61 |
+
ui_messages,
|
| 62 |
+
text,
|
| 63 |
+
globe_state,
|
| 64 |
+
assistant_metadata=assistant_metadata,
|
| 65 |
+
)
|
| 66 |
else:
|
| 67 |
yield from yield_streaming_string(text, globe_state)
|