feat: reasoning_content in API for reasoning models (#6322)
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
This commit is contained in:
@@ -21,8 +21,9 @@ use goose::config::declarative_providers::{
|
|||||||
};
|
};
|
||||||
use goose::conversation::message::{
|
use goose::conversation::message::{
|
||||||
ActionRequired, ActionRequiredData, FrontendToolRequest, Message, MessageContent,
|
ActionRequired, ActionRequiredData, FrontendToolRequest, Message, MessageContent,
|
||||||
MessageMetadata, RedactedThinkingContent, SystemNotificationContent, SystemNotificationType,
|
MessageMetadata, ReasoningContent, RedactedThinkingContent, SystemNotificationContent,
|
||||||
ThinkingContent, TokenState, ToolConfirmationRequest, ToolRequest, ToolResponse,
|
SystemNotificationType, ThinkingContent, TokenState, ToolConfirmationRequest, ToolRequest,
|
||||||
|
ToolResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::routes::recipe_utils::RecipeManifest;
|
use crate::routes::recipe_utils::RecipeManifest;
|
||||||
@@ -480,6 +481,7 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
ActionRequiredData,
|
ActionRequiredData,
|
||||||
ThinkingContent,
|
ThinkingContent,
|
||||||
RedactedThinkingContent,
|
RedactedThinkingContent,
|
||||||
|
ReasoningContent,
|
||||||
FrontendToolRequest,
|
FrontendToolRequest,
|
||||||
ResourceContentsSchema,
|
ResourceContentsSchema,
|
||||||
SystemNotificationType,
|
SystemNotificationType,
|
||||||
|
|||||||
@@ -336,19 +336,19 @@ fn format_message_for_compacting(msg: &Message) -> String {
|
|||||||
let content_parts: Vec<String> = msg
|
let content_parts: Vec<String> = msg
|
||||||
.content
|
.content
|
||||||
.iter()
|
.iter()
|
||||||
.map(|content| match content {
|
.filter_map(|content| match content {
|
||||||
MessageContent::Text(text) => text.text.clone(),
|
MessageContent::Text(text) => Some(text.text.clone()),
|
||||||
MessageContent::Image(img) => format!("[image: {}]", img.mime_type),
|
MessageContent::Image(img) => Some(format!("[image: {}]", img.mime_type)),
|
||||||
MessageContent::ToolRequest(req) => {
|
MessageContent::ToolRequest(req) => {
|
||||||
if let Ok(call) = &req.tool_call {
|
if let Ok(call) = &req.tool_call {
|
||||||
format!(
|
Some(format!(
|
||||||
"tool_request({}): {}",
|
"tool_request({}): {}",
|
||||||
call.name,
|
call.name,
|
||||||
serde_json::to_string(&call.arguments)
|
serde_json::to_string(&call.arguments)
|
||||||
.unwrap_or_else(|_| "<<invalid json>>".to_string())
|
.unwrap_or_else(|_| "<<invalid json>>".to_string())
|
||||||
)
|
))
|
||||||
} else {
|
} else {
|
||||||
"tool_request: [error]".to_string()
|
Some("tool_request: [error]".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageContent::ToolResponse(res) => {
|
MessageContent::ToolResponse(res) => {
|
||||||
@@ -362,40 +362,41 @@ fn format_message_for_compacting(msg: &Message) -> String {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !text_items.is_empty() {
|
if !text_items.is_empty() {
|
||||||
format!("tool_response: {}", text_items.join("\n"))
|
Some(format!("tool_response: {}", text_items.join("\n")))
|
||||||
} else {
|
} else {
|
||||||
"tool_response: [non-text content]".to_string()
|
Some("tool_response: [non-text content]".to_string())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"tool_response: [error]".to_string()
|
Some("tool_response: [error]".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageContent::ToolConfirmationRequest(req) => {
|
MessageContent::ToolConfirmationRequest(req) => {
|
||||||
format!("tool_confirmation_request: {}", req.tool_name)
|
Some(format!("tool_confirmation_request: {}", req.tool_name))
|
||||||
}
|
}
|
||||||
MessageContent::ActionRequired(action) => match &action.data {
|
MessageContent::ActionRequired(action) => match &action.data {
|
||||||
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
|
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
|
||||||
format!("action_required(tool_confirmation): {}", tool_name)
|
Some(format!("action_required(tool_confirmation): {}", tool_name))
|
||||||
}
|
}
|
||||||
ActionRequiredData::Elicitation { message, .. } => {
|
ActionRequiredData::Elicitation { message, .. } => {
|
||||||
format!("action_required(elicitation): {}", message)
|
Some(format!("action_required(elicitation): {}", message))
|
||||||
}
|
}
|
||||||
ActionRequiredData::ElicitationResponse { id, .. } => {
|
ActionRequiredData::ElicitationResponse { id, .. } => {
|
||||||
format!("action_required(elicitation_response): {}", id)
|
Some(format!("action_required(elicitation_response): {}", id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MessageContent::FrontendToolRequest(req) => {
|
MessageContent::FrontendToolRequest(req) => {
|
||||||
if let Ok(call) = &req.tool_call {
|
if let Ok(call) = &req.tool_call {
|
||||||
format!("frontend_tool_request: {}", call.name)
|
Some(format!("frontend_tool_request: {}", call.name))
|
||||||
} else {
|
} else {
|
||||||
"frontend_tool_request: [error]".to_string()
|
Some("frontend_tool_request: [error]".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageContent::Thinking(_) => "thinking".to_string(),
|
MessageContent::Thinking(_) => None,
|
||||||
MessageContent::RedactedThinking(_) => "redacted_thinking".to_string(),
|
MessageContent::RedactedThinking(_) => None,
|
||||||
MessageContent::SystemNotification(notification) => {
|
MessageContent::SystemNotification(notification) => {
|
||||||
format!("system_notification: {}", notification.msg)
|
Some(format!("system_notification: {}", notification.msg))
|
||||||
}
|
}
|
||||||
|
MessageContent::Reasoning(_) => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ pub struct SystemNotificationContent {
|
|||||||
pub data: Option<serde_json::Value>,
|
pub data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ReasoningContent {
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
|
||||||
/// Content passed inside a message, which can be both simple content and tool content
|
/// Content passed inside a message, which can be both simple content and tool content
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
@@ -189,6 +194,7 @@ pub enum MessageContent {
|
|||||||
Thinking(ThinkingContent),
|
Thinking(ThinkingContent),
|
||||||
RedactedThinking(RedactedThinkingContent),
|
RedactedThinking(RedactedThinkingContent),
|
||||||
SystemNotification(SystemNotificationContent),
|
SystemNotification(SystemNotificationContent),
|
||||||
|
Reasoning(ReasoningContent),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MessageContent {
|
impl fmt::Display for MessageContent {
|
||||||
@@ -230,6 +236,7 @@ impl fmt::Display for MessageContent {
|
|||||||
MessageContent::SystemNotification(r) => {
|
MessageContent::SystemNotification(r) => {
|
||||||
write!(f, "[SystemNotification: {}]", r.msg)
|
write!(f, "[SystemNotification: {}]", r.msg)
|
||||||
}
|
}
|
||||||
|
MessageContent::Reasoning(r) => write!(f, "[Reasoning: {}]", r.text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -443,6 +450,10 @@ impl MessageContent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reasoning<S: Into<String>>(text: S) -> Self {
|
||||||
|
MessageContent::Reasoning(ReasoningContent { text: text.into() })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn as_system_notification(&self) -> Option<&SystemNotificationContent> {
|
pub fn as_system_notification(&self) -> Option<&SystemNotificationContent> {
|
||||||
if let MessageContent::SystemNotification(ref notification) = self {
|
if let MessageContent::SystemNotification(ref notification) = self {
|
||||||
Some(notification)
|
Some(notification)
|
||||||
@@ -514,6 +525,14 @@ impl MessageContent {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the reasoning content if this is a ReasoningContent variant
|
||||||
|
pub fn as_reasoning(&self) -> Option<&ReasoningContent> {
|
||||||
|
match self {
|
||||||
|
MessageContent::Reasoning(reasoning) => Some(reasoning),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Content> for MessageContent {
|
impl From<Content> for MessageContent {
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MessageContent::Reasoning(_reasoning) => {
|
||||||
|
// Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek)
|
||||||
|
// Anthropic doesn't use this format, so skip it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
|
|||||||
.build()?,
|
.build()?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
MessageContent::Reasoning(_reasoning) => {
|
||||||
|
// Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek)
|
||||||
|
// Bedrock doesn't use this format, so skip
|
||||||
|
bedrock::ContentBlock::Text("".to_string())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,10 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
|
|||||||
MessageContent::SystemNotification(_)
|
MessageContent::SystemNotification(_)
|
||||||
| MessageContent::ToolConfirmationRequest(_)
|
| MessageContent::ToolConfirmationRequest(_)
|
||||||
| MessageContent::ActionRequired(_) => {}
|
| MessageContent::ActionRequired(_) => {}
|
||||||
|
MessageContent::Reasoning(_reasoning) => {
|
||||||
|
// Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek)
|
||||||
|
// Databricks doesn't use this format, so skip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ struct Delta {
|
|||||||
role: Option<String>,
|
role: Option<String>,
|
||||||
tool_calls: Option<Vec<DeltaToolCall>>,
|
tool_calls: Option<Vec<DeltaToolCall>>,
|
||||||
reasoning_details: Option<Vec<Value>>,
|
reasoning_details: Option<Vec<Value>>,
|
||||||
|
reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -80,6 +81,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
|||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let mut content_array = Vec::new();
|
let mut content_array = Vec::new();
|
||||||
let mut text_array = Vec::new();
|
let mut text_array = Vec::new();
|
||||||
|
let mut reasoning_text: Option<String> = None;
|
||||||
|
|
||||||
for content in &message.content {
|
for content in &message.content {
|
||||||
match content {
|
match content {
|
||||||
@@ -112,6 +114,9 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
|||||||
MessageContent::SystemNotification(_) => {
|
MessageContent::SystemNotification(_) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
MessageContent::Reasoning(r) => {
|
||||||
|
reasoning_text = Some(r.text.clone());
|
||||||
|
}
|
||||||
MessageContent::ToolRequest(request) => match &request.tool_call {
|
MessageContent::ToolRequest(request) => match &request.tool_call {
|
||||||
Ok(tool_call) => {
|
Ok(tool_call) => {
|
||||||
let sanitized_name = sanitize_function_name(&tool_call.name);
|
let sanitized_name = sanitize_function_name(&tool_call.name);
|
||||||
@@ -277,6 +282,17 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
|
|||||||
converted["content"] = json!(null);
|
converted["content"] = json!(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepSeek requires reasoning_content field when tool_calls are present
|
||||||
|
// Set it to the captured reasoning text, or empty string if not present
|
||||||
|
if converted.get("tool_calls").is_some() {
|
||||||
|
let reasoning = reasoning_text.unwrap_or_default();
|
||||||
|
converted["reasoning_content"] = json!(reasoning);
|
||||||
|
} else if let Some(reasoning) = reasoning_text {
|
||||||
|
if !reasoning.is_empty() {
|
||||||
|
converted["reasoning_content"] = json!(reasoning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if converted.get("content").is_some() || converted.get("tool_calls").is_some() {
|
if converted.get("content").is_some() || converted.get("tool_calls").is_some() {
|
||||||
output.insert(0, converted);
|
output.insert(0, converted);
|
||||||
}
|
}
|
||||||
@@ -330,6 +346,15 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
|
|||||||
|
|
||||||
let mut content = Vec::new();
|
let mut content = Vec::new();
|
||||||
|
|
||||||
|
// Capture reasoning_content if present (for DeepSeek reasoning models)
|
||||||
|
if let Some(reasoning_content) = original.get("reasoning_content") {
|
||||||
|
if let Some(reasoning_str) = reasoning_content.as_str() {
|
||||||
|
if !reasoning_str.is_empty() {
|
||||||
|
content.push(MessageContent::reasoning(reasoning_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(text) = original.get("content") {
|
if let Some(text) = original.get("content") {
|
||||||
if let Some(text_str) = text.as_str() {
|
if let Some(text_str) = text.as_str() {
|
||||||
content.push(MessageContent::text(text_str));
|
content.push(MessageContent::text(text_str));
|
||||||
@@ -678,23 +703,44 @@ where
|
|||||||
Some(msg),
|
Some(msg),
|
||||||
usage,
|
usage,
|
||||||
)
|
)
|
||||||
} else if chunk.choices[0].delta.content.is_some() {
|
} else if chunk.choices[0].delta.content.is_some() || chunk.choices[0].delta.reasoning_content.is_some() {
|
||||||
let text = chunk.choices[0].delta.content.as_ref().unwrap();
|
let mut content = Vec::new();
|
||||||
let mut msg = Message::new(
|
|
||||||
Role::Assistant,
|
|
||||||
chrono::Utc::now().timestamp(),
|
|
||||||
vec![MessageContent::text(text)],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add ID if present
|
if let Some(reasoning) = &chunk.choices[0].delta.reasoning_content {
|
||||||
if let Some(id) = chunk.id {
|
if !reasoning.is_empty() {
|
||||||
msg = msg.with_id(id);
|
content.push(MessageContent::reasoning(reasoning));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yield (
|
if let Some(text) = &chunk.choices[0].delta.content {
|
||||||
Some(msg),
|
if !text.is_empty() {
|
||||||
usage,
|
content.push(MessageContent::text(text));
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !content.is_empty() {
|
||||||
|
let mut msg = Message::new(
|
||||||
|
Role::Assistant,
|
||||||
|
chrono::Utc::now().timestamp(),
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add ID if present
|
||||||
|
if let Some(id) = chunk.id {
|
||||||
|
msg = msg.with_id(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield (
|
||||||
|
Some(msg),
|
||||||
|
if chunk.choices[0].finish_reason.is_some() {
|
||||||
|
usage
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else if usage.is_some() {
|
||||||
|
yield (None, usage)
|
||||||
|
}
|
||||||
} else if usage.is_some() {
|
} else if usage.is_some() {
|
||||||
yield (None, usage)
|
yield (None, usage)
|
||||||
}
|
}
|
||||||
@@ -1834,4 +1880,82 @@ data: [DONE]"#;
|
|||||||
|
|
||||||
panic!("Expected tool call message with nested extra_content metadata");
|
panic!("Expected tool call message with nested extra_content metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_to_message_with_reasoning_content() -> anyhow::Result<()> {
|
||||||
|
// Test capturing reasoning_content from DeepSeek reasoning models
|
||||||
|
let response = json!({
|
||||||
|
"choices": [{
|
||||||
|
"role": "assistant",
|
||||||
|
"message": {
|
||||||
|
"reasoning_content": "Let me think about this step by step...",
|
||||||
|
"content": "The answer is 9.11 is greater than 9.8"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 10,
|
||||||
|
"output_tokens": 25,
|
||||||
|
"total_tokens": 35
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = response_to_message(&response)?;
|
||||||
|
assert_eq!(message.content.len(), 2);
|
||||||
|
|
||||||
|
// First should be reasoning content
|
||||||
|
if let MessageContent::Reasoning(reasoning) = &message.content[0] {
|
||||||
|
assert_eq!(reasoning.text, "Let me think about this step by step...");
|
||||||
|
} else {
|
||||||
|
panic!("Expected Reasoning content");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second should be text content
|
||||||
|
if let MessageContent::Text(text) = &message.content[1] {
|
||||||
|
assert_eq!(text.text, "The answer is 9.11 is greater than 9.8");
|
||||||
|
} else {
|
||||||
|
panic!("Expected Text content");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_messages_with_reasoning_content() -> anyhow::Result<()> {
|
||||||
|
// Test that reasoning_content is properly included in formatted messages
|
||||||
|
let mut message = Message::assistant()
|
||||||
|
.with_content(MessageContent::reasoning("Thinking through the problem..."))
|
||||||
|
.with_text("The result is 42");
|
||||||
|
|
||||||
|
// Add a tool call to test that reasoning_content works with tool calls
|
||||||
|
message = message.with_tool_request(
|
||||||
|
"tool1",
|
||||||
|
Ok(rmcp::model::CallToolRequestParams {
|
||||||
|
meta: None,
|
||||||
|
task: None,
|
||||||
|
name: "test_tool".into(),
|
||||||
|
arguments: Some(rmcp::object!({"param": "value"})),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let spec = format_messages(&[message], &ImageFormat::OpenAi);
|
||||||
|
|
||||||
|
assert_eq!(spec.len(), 1);
|
||||||
|
assert_eq!(spec[0]["role"], "assistant");
|
||||||
|
|
||||||
|
// Should have reasoning_content field
|
||||||
|
assert!(spec[0].get("reasoning_content").is_some());
|
||||||
|
assert_eq!(
|
||||||
|
spec[0]["reasoning_content"],
|
||||||
|
"Thinking through the problem..."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should have content
|
||||||
|
assert_eq!(spec[0]["content"], "The result is 42");
|
||||||
|
|
||||||
|
// Should have tool_calls
|
||||||
|
assert!(spec[0]["tool_calls"].is_array());
|
||||||
|
assert_eq!(spec[0]["tool_calls"][0]["function"]["name"], "test_tool");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
|||||||
MessageContent::FrontendToolRequest(_tool_request) => {
|
MessageContent::FrontendToolRequest(_tool_request) => {
|
||||||
// Skip frontend tool requests
|
// Skip frontend tool requests
|
||||||
}
|
}
|
||||||
|
MessageContent::Reasoning(_reasoning) => {
|
||||||
|
// Reasoning content is for OpenAI-compatible APIs (e.g., DeepSeek)
|
||||||
|
// Snowflake doesn't use this format, so skip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5042,6 +5042,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/ReasoningContent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"reasoning"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Content passed inside a message, which can be both simple content and tool content",
|
"description": "Content passed inside a message, which can be both simple content and tool content",
|
||||||
@@ -5724,6 +5745,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ReasoningContent": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Recipe": {
|
"Recipe": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -564,6 +564,8 @@ export type MessageContent = (TextContent & {
|
|||||||
type: 'redactedThinking';
|
type: 'redactedThinking';
|
||||||
}) | (SystemNotificationContent & {
|
}) | (SystemNotificationContent & {
|
||||||
type: 'systemNotification';
|
type: 'systemNotification';
|
||||||
|
}) | (ReasoningContent & {
|
||||||
|
type: 'reasoning';
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MessageEvent = {
|
export type MessageEvent = {
|
||||||
@@ -833,6 +835,10 @@ export type ReadResourceResponse = {
|
|||||||
uri: string;
|
uri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReasoningContent = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Recipe = {
|
export type Recipe = {
|
||||||
activities?: Array<string> | null;
|
activities?: Array<string> | null;
|
||||||
author?: Author | null;
|
author?: Author | null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import MarkdownContent from './MarkdownContent';
|
|||||||
import ToolCallWithResponse from './ToolCallWithResponse';
|
import ToolCallWithResponse from './ToolCallWithResponse';
|
||||||
import {
|
import {
|
||||||
getTextAndImageContent,
|
getTextAndImageContent,
|
||||||
|
getReasoningContent,
|
||||||
getToolRequests,
|
getToolRequests,
|
||||||
getToolResponses,
|
getToolResponses,
|
||||||
getToolConfirmationContent,
|
getToolConfirmationContent,
|
||||||
@@ -47,6 +48,7 @@ export default function GooseMessage({
|
|||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
let { textContent, imagePaths } = getTextAndImageContent(message);
|
let { textContent, imagePaths } = getTextAndImageContent(message);
|
||||||
|
const reasoningContent = getReasoningContent(message);
|
||||||
|
|
||||||
const splitChainOfThought = (text: string): { displayText: string; cotText: string | null } => {
|
const splitChainOfThought = (text: string): { displayText: string; cotText: string | null } => {
|
||||||
const regex = /<think>([\s\S]*?)<\/think>/i;
|
const regex = /<think>([\s\S]*?)<\/think>/i;
|
||||||
@@ -129,6 +131,17 @@ export default function GooseMessage({
|
|||||||
return (
|
return (
|
||||||
<div className="goose-message flex w-[90%] justify-start min-w-0">
|
<div className="goose-message flex w-[90%] justify-start min-w-0">
|
||||||
<div className="flex flex-col w-full min-w-0">
|
<div className="flex flex-col w-full min-w-0">
|
||||||
|
{reasoningContent && (
|
||||||
|
<details className="mb-2">
|
||||||
|
<summary className="cursor-pointer text-xs text-textSubtle select-none">
|
||||||
|
Show reasoning
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 text-sm">
|
||||||
|
<MarkdownContent content={reasoningContent} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
{cotText && (
|
{cotText && (
|
||||||
<details className="bg-background-muted border border-border-default rounded p-2 mb-2">
|
<details className="bg-background-muted border border-border-default rounded p-2 mb-2">
|
||||||
<summary className="cursor-pointer text-sm text-text-muted select-none">
|
<summary className="cursor-pointer text-sm text-text-muted select-none">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ToolCallWithResponse from '../ToolCallWithResponse';
|
|||||||
import ImagePreview from '../ImagePreview';
|
import ImagePreview from '../ImagePreview';
|
||||||
import {
|
import {
|
||||||
getTextAndImageContent,
|
getTextAndImageContent,
|
||||||
|
getReasoningContent,
|
||||||
ToolRequestMessageContent,
|
ToolRequestMessageContent,
|
||||||
ToolResponseMessageContent,
|
ToolResponseMessageContent,
|
||||||
} from '../../types/message';
|
} from '../../types/message';
|
||||||
@@ -82,6 +83,7 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
|
|||||||
messages
|
messages
|
||||||
.map((message, index) => {
|
.map((message, index) => {
|
||||||
const { textContent, imagePaths } = getTextAndImageContent(message);
|
const { textContent, imagePaths } = getTextAndImageContent(message);
|
||||||
|
const reasoningContent = getReasoningContent(message);
|
||||||
|
|
||||||
// Get tool requests from the message
|
// Get tool requests from the message
|
||||||
const toolRequests = message.content
|
const toolRequests = message.content
|
||||||
@@ -119,6 +121,19 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
|
{/* Reasoning content */}
|
||||||
|
{reasoningContent && (
|
||||||
|
<details className="mb-2">
|
||||||
|
<summary className="cursor-pointer text-xs text-textSubtle select-none">
|
||||||
|
Show reasoning
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 text-sm">
|
||||||
|
<MarkdownContent content={reasoningContent} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Text content */}
|
||||||
{textContent && (
|
{textContent && (
|
||||||
<div
|
<div
|
||||||
className={`${toolRequests.length > 0 || imagePaths.length > 0 ? 'mb-4' : ''}`}
|
className={`${toolRequests.length > 0 || imagePaths.length > 0 ? 'mb-4' : ''}`}
|
||||||
|
|||||||
@@ -97,6 +97,18 @@ export function getTextAndImageContent(message: Message): {
|
|||||||
return { textContent, imagePaths };
|
return { textContent, imagePaths };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReasoningContent(message: Message): string | null {
|
||||||
|
const reasoningContents = message.content
|
||||||
|
.filter((content) => content.type === 'reasoning')
|
||||||
|
.map((content) => {
|
||||||
|
if ('text' in content) return content.text;
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.filter((text) => text.length > 0);
|
||||||
|
|
||||||
|
return reasoningContents.length > 0 ? reasoningContents.join('') : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {
|
export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {
|
||||||
return message.content.filter(
|
return message.content.filter(
|
||||||
(content): content is ToolRequest & { type: 'toolRequest' } => content.type === 'toolRequest'
|
(content): content is ToolRequest & { type: 'toolRequest' } => content.type === 'toolRequest'
|
||||||
|
|||||||
Reference in New Issue
Block a user