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()
|
||||
}
|
||||
|
||||
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 {
|
||||
let mime = attachment
|
||||
.mime_type
|
||||
@@ -939,22 +1010,7 @@ async fn chat_handler(
|
||||
continue;
|
||||
};
|
||||
|
||||
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 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 let Some(reasoning) = extract_reasoning_delta(&parsed) {
|
||||
if !send_sse(
|
||||
&tx,
|
||||
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
|
||||
.get("content")
|
||||
.and_then(Value::as_str)
|
||||
@@ -1266,6 +1330,49 @@ mod tests {
|
||||
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:
|
||||
/// no env → local; whitespace/empty env → still local (treated as unset);
|
||||
/// real env → fallback_remote with trimmed `upstreamBase`.
|
||||
|
||||
@@ -1349,22 +1349,16 @@ export default function ComfyUIAgent() {
|
||||
const renderReasoning = (message: AgentMessage | StreamAccumulator, isStreaming = false) => {
|
||||
const processed = processThinkTags(message.content || '', message.reasoningContent || '');
|
||||
const hasReasoning = processed.reasoningContent.trim().length > 0;
|
||||
if (!hasReasoning && !isStreaming) return null;
|
||||
if (!hasReasoning) return null;
|
||||
return (
|
||||
<details open={isStreaming} style={reasoningBoxStyle}>
|
||||
<summary style={reasoningSummaryStyle}>
|
||||
思考过程
|
||||
</summary>
|
||||
{hasReasoning ? (
|
||||
<div
|
||||
style={reasoningContentStyle}
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(processed.reasoningContent) }}
|
||||
/>
|
||||
) : (
|
||||
<div style={reasoningContentStyle}>
|
||||
正在分析你的需求、拆解角色、场景、镜头与后续工作流步骤...
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={reasoningContentStyle}
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(processed.reasoningContent) }}
|
||||
/>
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user