fix: stream real agent reasoning
This commit is contained in:
+123
-16
@@ -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`.
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user