llm_brain/
conceptnet.rs

1use reqwest::{Client, Url};
2use serde::Deserialize;
3
4use crate::config::ConceptNetConfig;
5use crate::error::{LLMBrainError, Result};
6
7const DEFAULT_CONCEPTNET_URL: &str = "https://api.conceptnet.io";
8
9/// A `ConceptNet` edge representing a relation between two nodes
10#[derive(Deserialize, Debug, Clone)]
11pub struct ConceptNetEdge {
12    /// Edge unique identifier
13    #[serde(rename = "@id")]
14    pub id: String,
15
16    /// Edge type (typically "Edge")
17    #[serde(rename = "@type")]
18    pub edge_type: String,
19
20    /// The relation between the start and end nodes
21    pub rel: ConceptNetRelation,
22
23    /// The source node
24    pub start: ConceptNetNode,
25
26    /// The target node
27    pub end: ConceptNetNode,
28
29    /// Weight/confidence score of the edge
30    pub weight: f32,
31
32    /// Human-readable description of the relation
33    #[serde(rename = "surfaceText")]
34    pub surface_text: Option<String>,
35}
36
37/// A `ConceptNet` relation type
38#[derive(Deserialize, Debug, Clone)]
39pub struct ConceptNetRelation {
40    /// Relation identifier (e.g., "/r/RelatedTo")
41    #[serde(rename = "@id")]
42    pub id: String,
43
44    /// Type identifier (typically "Relation")
45    #[serde(rename = "@type")]
46    pub rel_type: String,
47
48    /// Human-readable label (e.g., "Related To")
49    pub label: String,
50}
51
52/// A `ConceptNet` node representing a concept
53#[derive(Deserialize, Debug, Clone)]
54pub struct ConceptNetNode {
55    /// Node identifier (e.g., "/c/en/example")
56    #[serde(rename = "@id")]
57    pub id: String,
58
59    /// Node type (typically "Node")
60    #[serde(rename = "@type")]
61    pub node_type: String,
62
63    /// Human-readable label (e.g., "example")
64    pub label: String,
65
66    /// Language code (e.g., "en")
67    pub language: String,
68
69    /// Term identifier
70    pub term: String,
71}
72
73/// Response from a `ConceptNet` API query
74#[derive(Deserialize, Debug, Clone)]
75pub struct ConceptNetResponse {
76    /// The queried URI
77    #[serde(rename = "@id")]
78    pub id: String,
79
80    /// List of edges related to the query
81    pub edges: Vec<ConceptNetEdge>,
82}
83
84/// Client for interacting with the `ConceptNet` API
85#[derive(Clone)]
86pub struct ConceptNetClient {
87    client: Client,
88    base_url: String,
89}
90
91impl ConceptNetClient {
92    /// Creates a new `ConceptNetClient`.
93    ///
94    /// Uses the provided configuration or falls back to default values.
95    pub fn new(config: Option<&ConceptNetConfig>) -> Result<Self> {
96        let base_url = config
97            .and_then(|c| c.base_url.as_deref())
98            .unwrap_or(DEFAULT_CONCEPTNET_URL)
99            .trim_end_matches('/')
100            .to_owned();
101
102        let client = Client::builder()
103            .user_agent(format!("LLMBrainBot/{} (Rust)", env!("CARGO_PKG_VERSION")))
104            .build()
105            .map_err(|e| {
106                LLMBrainError::InitializationError(format!("Failed to build reqwest client: {e}"))
107            })?;
108
109        Ok(Self { client, base_url })
110    }
111
112    /// Fetches edges related to a `ConceptNet` URI.
113    ///
114    /// # Arguments
115    ///
116    /// * `uri` - A `ConceptNet` URI (e.g., "/c/en/knowledge") or term (e.g.,
117    ///   "knowledge")
118    ///
119    /// If `uri` doesn't start with a slash, it will be wrapped as an English
120    /// term (/c/en/{uri}).
121    pub async fn get_related(&self, uri: &str) -> Result<ConceptNetResponse> {
122        // Ensure the URI starts with a slash if it doesn't
123        let path = if uri.starts_with('/') {
124            uri
125        } else {
126            &format!("/c/en/{uri}")
127        };
128
129        let url_str = format!("{}{}", self.base_url, path);
130        let url = Url::parse(&url_str).map_err(|e| {
131            LLMBrainError::ApiError(format!("Invalid ConceptNet URL '{url_str}': {e}"))
132        })?;
133
134        let response = self
135            .client
136            .get(url)
137            .query(&[("limit", "50")])
138            .send()
139            .await
140            .map_err(|e| LLMBrainError::ApiError(format!("ConceptNet request failed: {e}")))?;
141
142        if !response.status().is_success() {
143            return Err(LLMBrainError::ApiError(format!(
144                "ConceptNet API request failed with status {}: {}",
145                response.status(),
146                response
147                    .text()
148                    .await
149                    .unwrap_or_else(|_| "Failed to get error body".to_owned())
150            )));
151        }
152
153        let data: ConceptNetResponse = response.json().await.map_err(|e| {
154            LLMBrainError::ApiError(format!("Failed to parse ConceptNet JSON response: {e}"))
155        })?;
156
157        Ok(data)
158    }
159
160    // Add other methods like lookup_term, search, etc. as needed
161}