use lc_core::language_models::{BaseChatModel, LLMResult};
use lc_core::runnables::RunnableConfig;
use lc_core::tools::ToolDefinition;
use lc_schema::Message;
use serde_json::Value;
use crate::retry::{retry_chat, RetryConfig};
#[derive(Debug, Clone)]
pub(crate) struct StructuredChatResult {
pub content: String,
pub tool_args: Option<Value>,
}
pub(crate) async fn chat_structured<M>(
llm: &M,
tool: Option<ToolDefinition>,
messages: Vec<Message>,
config: Option<RunnableConfig>,
retry: &RetryConfig,
) -> Result<StructuredChatResult, M::Error>
where
M: BaseChatModel + ?Sized,
{
let result = match tool {
Some(tool) => match llm.bind_tools(vec![tool]) {
Some(bound) => retry_chat(bound.as_ref(), messages, config, retry).await?,
None => retry_chat(llm, messages, config, retry).await?,
},
None => retry_chat(llm, messages, config, retry).await?,
};
Ok(extract_structured(&result))
}
pub(crate) fn extract_structured(result: &LLMResult) -> StructuredChatResult {
let tool_args = result
.tool_calls
.as_ref()
.and_then(|calls| calls.first())
.and_then(|call| call.parse_arguments::<Value>().ok());
StructuredChatResult {
content: result.content.clone(),
tool_args,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_structured_no_tool_calls() {
let result = LLMResult {
content: "text answer".to_string(),
model: "mock".to_string(),
token_usage: None,
tool_calls: None,
thinking_content: None,
};
let structured = extract_structured(&result);
assert_eq!(structured.content, "text answer");
assert!(structured.tool_args.is_none());
}
#[test]
fn test_extract_structured_with_tool_call() {
use lc_core::tools::ToolCall;
let result = LLMResult {
content: String::new(),
model: "mock".to_string(),
token_usage: None,
tool_calls: Some(vec![ToolCall::new(
"call_1",
"generate_plan",
r#"{"steps": ["a", "b"]}"#.to_string(),
)]),
thinking_content: None,
};
let structured = extract_structured(&result);
assert!(structured.tool_args.is_some());
let steps = structured.tool_args.unwrap();
assert_eq!(steps["steps"][0], "a");
}
#[test]
fn test_extract_structured_invalid_tool_args() {
use lc_core::tools::ToolCall;
let result = LLMResult {
content: "fallback text".to_string(),
model: "mock".to_string(),
token_usage: None,
tool_calls: Some(vec![ToolCall::new("call_2", "f", "not-json".to_string())]),
thinking_content: None,
};
let structured = extract_structured(&result);
assert!(structured.tool_args.is_none());
assert_eq!(structured.content, "fallback text");
}
}