use lc_core::language_models::{BaseChatModel, LLMResult};
use lc_core::tools::ToolDefinition;
use lc_schema::Message;
use serde_json::Value;
#[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>,
) -> Result<StructuredChatResult, String>
where
M: BaseChatModel + ?Sized,
{
let result = match tool {
Some(tool) => match llm.bind_tools(vec![tool]) {
Some(bound) => bound
.chat(messages, None)
.await
.map_err(|e| e.to_string())?,
None => llm.chat(messages, None).await.map_err(|e| e.to_string())?,
},
None => llm.chat(messages, None).await.map_err(|e| e.to_string())?,
};
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::*;
use lc_core::tools::ToolCall;
#[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() {
let result = LLMResult {
content: String::new(),
model: "mock".to_string(),
token_usage: None,
tool_calls: Some(vec![ToolCall::new(
"call_1",
"extract_entities_relations",
r#"{"entities": [], "relations": []}"#.to_string(),
)]),
thinking_content: None,
};
let structured = extract_structured(&result);
assert!(structured.tool_args.is_some());
assert!(structured.tool_args.unwrap()["entities"].is_array());
}
#[test]
fn test_extract_structured_invalid_tool_args() {
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");
}
}