Skip to main content

typesec_agent/interop/
langchain.rs

1//! LangChain dialect: `AIMessage.tool_calls` in, error `ToolMessage`s out.
2//! The same shapes serve LangGraph nodes, which pass LangChain messages.
3
4use serde_json::{Value, json};
5
6use super::call::{GuardedToolCall, InteropError, ToolCallRequest};
7use super::wire;
8
9/// Dialect name used in error messages and the Python bindings.
10pub const DIALECT: &str = "langchain";
11
12/// Parse tool calls from an `AIMessage` (`{"tool_calls": [...]}`) or a bare
13/// array of LangChain `ToolCall` dicts (`{"name", "args", "id"}`).
14pub fn parse_tool_calls(payload: &Value) -> Result<Vec<ToolCallRequest>, InteropError> {
15    wire::call_array(payload, "tool_calls", DIALECT)?
16        .iter()
17        .map(parse_call)
18        .collect()
19}
20
21fn parse_call(item: &Value) -> Result<ToolCallRequest, InteropError> {
22    let name = wire::required_str(item, "name", DIALECT)?;
23    let arguments = wire::parse_arguments(item.get("args"), DIALECT, name)?;
24    let mut request = ToolCallRequest::new(name, arguments);
25    if let Some(id) = wire::optional_str(item, "id") {
26        request = request.with_call_id(id);
27    }
28    Ok(request)
29}
30
31/// Filter a list of serialized tool definitions (dicts with a `name`) down
32/// to the definitions `keep` accepts.
33pub fn filter_tools(tools: &Value, keep: &dyn Fn(&str) -> bool) -> Value {
34    wire::filter_tool_array(tools, |item| wire::optional_str(item, "name"), keep)
35}
36
37/// Render a denied/undecided call as an error `ToolMessage` dict
38/// (`status: "error"`), so graphs/chains feed the refusal back to the model.
39/// Returns `None` for allowed calls.
40pub fn denial(call: &GuardedToolCall) -> Option<Value> {
41    call.denial_message().map(|content| {
42        json!({
43            "type": "tool",
44            "tool_call_id": call.request.call_id.clone().unwrap_or_default(),
45            "content": content,
46            "status": "error",
47        })
48    })
49}