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>,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum StructuredError {
#[error("{0}")]
Msg(String),
}
pub(crate) async fn chat_structured<M>(
llm: &M,
tool: Option<ToolDefinition>,
messages: Vec<Message>,
) -> Result<StructuredChatResult, StructuredError>
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| StructuredError::Msg(e.to_string()))?,
None => llm
.chat(messages, None)
.await
.map_err(|e| StructuredError::Msg(e.to_string()))?,
},
None => llm
.chat(messages, None)
.await
.map_err(|e| StructuredError::Msg(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::builder("call_1")
.name("extract_entities_relations")
.arguments(r#"{"entities": [], "relations": []}"#.to_string())
.build()]),
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::builder("call_2")
.name("f")
.arguments("not-json".to_string())
.build()]),
thinking_content: None,
};
let structured = extract_structured(&result);
assert!(structured.tool_args.is_none());
assert_eq!(structured.content, "fallback text");
}
}