agent_chain_core/
sys_info.rs

1//! Print information about the system and agent-chain packages for debugging purposes.
2//!
3//! This module provides utilities for printing system and package information,
4//! useful for debugging and support purposes. It is a Rust adaptation of
5//! langchain_core's sys_info.py module.
6
7use crate::env::VERSION;
8use rustc_version_runtime::version;
9use std::borrow::Cow;
10use std::collections::HashMap;
11use std::io::{self, Write};
12
13/// Information about a package/crate.
14#[derive(Debug, Clone)]
15pub struct PackageInfo {
16    /// The package name.
17    pub name: Cow<'static, str>,
18    /// The package version, if available.
19    pub version: Option<&'static str>,
20}
21
22impl PackageInfo {
23    /// Create a new PackageInfo with a static name.
24    pub fn new_static(name: &'static str, version: Option<&'static str>) -> Self {
25        Self {
26            name: Cow::Borrowed(name),
27            version,
28        }
29    }
30
31    /// Create a new PackageInfo with an owned name.
32    pub fn new_owned(name: String, version: Option<&'static str>) -> Self {
33        Self {
34            name: Cow::Owned(name),
35            version,
36        }
37    }
38}
39
40/// System information for debugging.
41#[derive(Debug, Clone)]
42pub struct SystemInfo {
43    /// Operating system name (e.g., "linux", "macos", "windows").
44    pub os: &'static str,
45    /// Operating system family (e.g., "unix", "windows").
46    pub os_family: &'static str,
47    /// CPU architecture (e.g., "x86_64", "aarch64").
48    pub arch: &'static str,
49    /// Rust version used to compile.
50    pub rust_version: String,
51}
52
53impl SystemInfo {
54    /// Get the current system information.
55    pub fn current() -> Self {
56        Self {
57            os: std::env::consts::OS,
58            os_family: std::env::consts::FAMILY,
59            arch: std::env::consts::ARCH,
60            rust_version: version().to_string(),
61        }
62    }
63}
64
65/// Get sub-dependencies that are not in the main package list.
66///
67/// In Rust, dependencies are determined at compile time, so this function
68/// returns a predefined list of common dependencies used by the agent-chain crates.
69fn get_sub_deps(packages: &[Cow<'static, str>]) -> Vec<PackageInfo> {
70    // Common dependencies used by agent-chain crates
71    let all_deps = [
72        ("async-trait", option_env!("DEP_ASYNC_TRAIT_VERSION")),
73        ("futures", option_env!("DEP_FUTURES_VERSION")),
74        ("reqwest", option_env!("DEP_REQWEST_VERSION")),
75        ("serde", option_env!("DEP_SERDE_VERSION")),
76        ("serde_json", option_env!("DEP_SERDE_JSON_VERSION")),
77        ("tokio", option_env!("DEP_TOKIO_VERSION")),
78        ("tracing", option_env!("DEP_TRACING_VERSION")),
79    ];
80
81    let package_set: std::collections::HashSet<&str> =
82        packages.iter().map(|s| s.as_ref()).collect();
83
84    all_deps
85        .iter()
86        .filter(|(name, _)| !package_set.contains(name))
87        .map(|(name, version)| PackageInfo::new_static(name, *version))
88        .collect()
89}
90
91/// Get information about agent-chain packages.
92///
93/// Returns a list of package information for all agent-chain related crates.
94pub fn get_package_info() -> Vec<PackageInfo> {
95    // Core packages in priority order (matching Python's order_by)
96    let mut packages = vec![
97        PackageInfo::new_static("agent-chain-core", Some(VERSION)),
98        PackageInfo::new_static("agent-chain", option_env!("DEP_AGENT_CHAIN_VERSION")),
99        PackageInfo::new_static("agent-graph", option_env!("DEP_AGENT_GRAPH_VERSION")),
100        PackageInfo::new_static(
101            "agent-chain-macros",
102            option_env!("DEP_AGENT_CHAIN_MACROS_VERSION"),
103        ),
104        PackageInfo::new_static(
105            "agent-graph-macros",
106            option_env!("DEP_AGENT_GRAPH_MACROS_VERSION"),
107        ),
108    ];
109
110    // Sort remaining packages alphabetically (excluding the first one which is agent-chain-core)
111    if packages.len() > 1 {
112        packages[1..].sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
113    }
114
115    packages
116}
117
118/// Get all system and package information as structured data.
119///
120/// Returns a tuple of (system_info, packages, sub_dependencies).
121pub fn get_sys_info(additional_pkgs: &[&str]) -> (SystemInfo, Vec<PackageInfo>, Vec<PackageInfo>) {
122    let system_info = SystemInfo::current();
123    let mut packages = get_package_info();
124
125    // Add additional packages
126    for pkg in additional_pkgs {
127        packages.push(PackageInfo::new_owned((*pkg).to_string(), None));
128    }
129
130    let package_names: Vec<Cow<'static, str>> = packages.iter().map(|p| p.name.clone()).collect();
131    let sub_deps = get_sub_deps(&package_names);
132
133    (system_info, packages, sub_deps)
134}
135
136/// Print information about the environment for debugging purposes.
137///
138/// This function prints system information, package versions, and dependencies
139/// to stdout. It is useful for debugging and support purposes.
140///
141/// # Arguments
142///
143/// * `additional_pkgs` - Additional package names to include in the output.
144///
145/// # Example
146///
147/// ```
148/// use agent_chain_core::sys_info::print_sys_info;
149///
150/// print_sys_info(&[]);
151/// ```
152pub fn print_sys_info(additional_pkgs: &[&str]) {
153    let mut stdout = io::stdout();
154    print_sys_info_to(&mut stdout, additional_pkgs);
155}
156
157/// Print system information to a specific writer.
158///
159/// This is useful for testing or redirecting output.
160pub fn print_sys_info_to<W: Write>(writer: &mut W, additional_pkgs: &[&str]) {
161    let (system_info, packages, sub_deps) = get_sys_info(additional_pkgs);
162
163    // Print system information
164    writeln!(writer).ok();
165    writeln!(writer, "System Information").ok();
166    writeln!(writer, "------------------").ok();
167    writeln!(writer, "> OS:  {}", system_info.os).ok();
168    writeln!(writer, "> OS Family:  {}", system_info.os_family).ok();
169    writeln!(writer, "> Architecture:  {}", system_info.arch).ok();
170    writeln!(writer, "> Rust Version:  {}", system_info.rust_version).ok();
171
172    // Print package information
173    writeln!(writer).ok();
174    writeln!(writer, "Package Information").ok();
175    writeln!(writer, "-------------------").ok();
176
177    let mut not_installed: Vec<&str> = Vec::new();
178
179    for pkg in &packages {
180        match pkg.version {
181            Some(version) => {
182                writeln!(writer, "> {}: {}", pkg.name, version).ok();
183            }
184            None => {
185                not_installed.push(pkg.name.as_ref());
186            }
187        }
188    }
189
190    // Print packages without version information
191    if !not_installed.is_empty() {
192        writeln!(writer).ok();
193        writeln!(writer, "Optional packages not installed").ok();
194        writeln!(writer, "-------------------------------").ok();
195        for pkg in not_installed {
196            writeln!(writer, "> {}", pkg).ok();
197        }
198    }
199
200    // Print sub-dependencies
201    let deps_with_version: Vec<_> = sub_deps.iter().filter(|d| d.version.is_some()).collect();
202
203    if !deps_with_version.is_empty() {
204        writeln!(writer).ok();
205        writeln!(writer, "Other Dependencies").ok();
206        writeln!(writer, "------------------").ok();
207
208        for dep in deps_with_version {
209            if let Some(version) = dep.version {
210                writeln!(writer, "> {}: {}", dep.name, version).ok();
211            }
212        }
213    }
214}
215
216/// Get system information as a HashMap (for compatibility with env module).
217pub fn get_sys_info_map() -> HashMap<String, String> {
218    let system_info = SystemInfo::current();
219    let packages = get_package_info();
220
221    let mut map = HashMap::new();
222    map.insert("os".to_string(), system_info.os.to_string());
223    map.insert("os_family".to_string(), system_info.os_family.to_string());
224    map.insert("arch".to_string(), system_info.arch.to_string());
225    map.insert(
226        "rust_version".to_string(),
227        system_info.rust_version.to_string(),
228    );
229
230    for pkg in packages {
231        if let Some(version) = pkg.version {
232            map.insert(
233                format!("{}_version", pkg.name.replace('-', "_")),
234                version.to_string(),
235            );
236        }
237    }
238
239    map
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_system_info_current() {
248        let info = SystemInfo::current();
249
250        // These should not be empty
251        assert!(!info.os.is_empty());
252        assert!(!info.os_family.is_empty());
253        assert!(!info.arch.is_empty());
254        assert!(!info.rust_version.is_empty());
255    }
256
257    #[test]
258    fn test_get_package_info() {
259        let packages = get_package_info();
260
261        // Should have at least agent-chain-core
262        assert!(!packages.is_empty());
263
264        // First package should be agent-chain-core
265        assert_eq!(packages[0].name, "agent-chain-core");
266        assert!(packages[0].version.is_some());
267    }
268
269    #[test]
270    fn test_get_sys_info() {
271        let (system_info, packages, _sub_deps) = get_sys_info(&[]);
272
273        assert!(!system_info.os.is_empty());
274        assert!(!packages.is_empty());
275    }
276
277    #[test]
278    fn test_get_sys_info_with_additional_pkgs() {
279        let (_, packages, _) = get_sys_info(&["custom-package"]);
280
281        // Should include the additional package
282        let has_custom = packages.iter().any(|p| p.name == "custom-package");
283        assert!(has_custom);
284    }
285
286    #[test]
287    fn test_print_sys_info_to_buffer() {
288        let mut buffer = Vec::new();
289        print_sys_info_to(&mut buffer, &[]);
290
291        let output = String::from_utf8(buffer).unwrap();
292
293        // Should contain expected sections
294        assert!(output.contains("System Information"));
295        assert!(output.contains("Package Information"));
296        assert!(output.contains("agent-chain-core"));
297    }
298
299    #[test]
300    fn test_get_sys_info_map() {
301        let map = get_sys_info_map();
302
303        assert!(map.contains_key("os"));
304        assert!(map.contains_key("rust_version"));
305        assert!(map.contains_key("agent_chain_core_version"));
306    }
307}