fix: stream real agent reasoning

This commit is contained in:
boos
2026-06-06 20:26:31 +08:00
parent 44dc89a0dd
commit 6cd0a8caca
2 changed files with 128 additions and 27 deletions
+123 -16
View File
@@ -683,6 +683,77 @@ fn send_sse(tx: &UnboundedSender<Result<Event, Infallible>>, payload: Value) ->
.is_ok() .is_ok()
} }
fn string_field(value: &Value, keys: &[&str]) -> Option<String> {
keys.iter()
.find_map(|key| value.get(*key).and_then(Value::as_str))
.map(ToOwned::to_owned)
.filter(|text| !text.is_empty())
}
fn extract_reasoning_delta(parsed: &Value) -> Option<String> {
if let Some(delta) = parsed
.get("choices")
.and_then(Value::as_array)
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("delta"))
{
if let Some(text) = string_field(
delta,
&[
"reasoning_content",
"reasoning",
"thinking",
"thinking_delta",
"reasoning_delta",
],
) {
return Some(text);
}
if let Some(text) = delta
.get("reasoning")
.and_then(|value| value.get("content").or_else(|| value.get("text")))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.filter(|text| !text.is_empty())
{
return Some(text);
}
}
let event_type = parsed
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
match event_type {
"response.reasoning.delta"
| "response.reasoning_text.delta"
| "response.reasoning_summary_text.delta" => {
string_field(parsed, &["delta", "text", "content"])
}
"content_block_delta" => {
let delta = parsed.get("delta")?;
match delta
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"thinking_delta" => string_field(delta, &["thinking", "text", "delta"]),
"signature_delta" => None,
other if other.contains("thinking") || other.contains("reasoning") => {
string_field(delta, &["thinking", "text", "content", "delta"])
}
_ => None,
}
}
other if other.contains("reasoning") || other.contains("thinking") => string_field(
parsed,
&["delta", "text", "content", "thinking", "reasoning"],
),
_ => None,
}
}
fn attachment_summary(attachment: &AgentAttachment) -> String { fn attachment_summary(attachment: &AgentAttachment) -> String {
let mime = attachment let mime = attachment
.mime_type .mime_type
@@ -939,22 +1010,7 @@ async fn chat_handler(
continue; continue;
}; };
let delta = parsed if let Some(reasoning) = extract_reasoning_delta(&parsed) {
.get("choices")
.and_then(Value::as_array)
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("delta"))
.cloned()
.unwrap_or(Value::Null);
let reasoning = delta
.get("reasoning_content")
.or_else(|| delta.get("reasoning"))
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
if !reasoning.is_empty() {
if !send_sse( if !send_sse(
&tx, &tx,
json!({ json!({
@@ -966,6 +1022,14 @@ async fn chat_handler(
} }
} }
let delta = parsed
.get("choices")
.and_then(Value::as_array)
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("delta"))
.cloned()
.unwrap_or(Value::Null);
let content = delta let content = delta
.get("content") .get("content")
.and_then(Value::as_str) .and_then(Value::as_str)
@@ -1266,6 +1330,49 @@ mod tests {
result.expect("status_handler returned error").0 result.expect("status_handler returned error").0
} }
#[test]
fn extracts_chat_reasoning_delta() {
let parsed = json!({
"choices": [{
"delta": {
"reasoning_content": "正在拆解镜头..."
}
}]
});
assert_eq!(
extract_reasoning_delta(&parsed).as_deref(),
Some("正在拆解镜头...")
);
}
#[test]
fn extracts_responses_reasoning_delta() {
let parsed = json!({
"type": "response.reasoning_summary_text.delta",
"delta": "先分析角色和场景。"
});
assert_eq!(
extract_reasoning_delta(&parsed).as_deref(),
Some("先分析角色和场景。")
);
}
#[test]
fn extracts_anthropic_thinking_delta() {
let parsed = json!({
"type": "content_block_delta",
"index": 0,
"delta": {
"type": "thinking_delta",
"thinking": "用户需要一个视频分镜。"
}
});
assert_eq!(
extract_reasoning_delta(&parsed).as_deref(),
Some("用户需要一个视频分镜。")
);
}
/// 3.5 — `/api/comfyui-agent/status` reflects the env state in one shot: /// 3.5 — `/api/comfyui-agent/status` reflects the env state in one shot:
/// no env → local; whitespace/empty env → still local (treated as unset); /// no env → local; whitespace/empty env → still local (treated as unset);
/// real env → fallback_remote with trimmed `upstreamBase`. /// real env → fallback_remote with trimmed `upstreamBase`.
+1 -7
View File
@@ -1349,22 +1349,16 @@ export default function ComfyUIAgent() {
const renderReasoning = (message: AgentMessage | StreamAccumulator, isStreaming = false) => { const renderReasoning = (message: AgentMessage | StreamAccumulator, isStreaming = false) => {
const processed = processThinkTags(message.content || '', message.reasoningContent || ''); const processed = processThinkTags(message.content || '', message.reasoningContent || '');
const hasReasoning = processed.reasoningContent.trim().length > 0; const hasReasoning = processed.reasoningContent.trim().length > 0;
if (!hasReasoning && !isStreaming) return null; if (!hasReasoning) return null;
return ( return (
<details open={isStreaming} style={reasoningBoxStyle}> <details open={isStreaming} style={reasoningBoxStyle}>
<summary style={reasoningSummaryStyle}> <summary style={reasoningSummaryStyle}>
思考过程 思考过程
</summary> </summary>
{hasReasoning ? (
<div <div
style={reasoningContentStyle} style={reasoningContentStyle}
dangerouslySetInnerHTML={{ __html: renderMarkdown(processed.reasoningContent) }} dangerouslySetInnerHTML={{ __html: renderMarkdown(processed.reasoningContent) }}
/> />
) : (
<div style={reasoningContentStyle}>
正在分析你的需求、拆解角色、场景、镜头与后续工作流步骤...
</div>
)}
</details> </details>
); );
}; };