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#[derive(Deserialize, Debug, Clone)]
11pub struct ConceptNetEdge {
12 #[serde(rename = "@id")]
14 pub id: String,
15
16 #[serde(rename = "@type")]
18 pub edge_type: String,
19
20 pub rel: ConceptNetRelation,
22
23 pub start: ConceptNetNode,
25
26 pub end: ConceptNetNode,
28
29 pub weight: f32,
31
32 #[serde(rename = "surfaceText")]
34 pub surface_text: Option<String>,
35}
36
37#[derive(Deserialize, Debug, Clone)]
39pub struct ConceptNetRelation {
40 #[serde(rename = "@id")]
42 pub id: String,
43
44 #[serde(rename = "@type")]
46 pub rel_type: String,
47
48 pub label: String,
50}
51
52#[derive(Deserialize, Debug, Clone)]
54pub struct ConceptNetNode {
55 #[serde(rename = "@id")]
57 pub id: String,
58
59 #[serde(rename = "@type")]
61 pub node_type: String,
62
63 pub label: String,
65
66 pub language: String,
68
69 pub term: String,
71}
72
73#[derive(Deserialize, Debug, Clone)]
75pub struct ConceptNetResponse {
76 #[serde(rename = "@id")]
78 pub id: String,
79
80 pub edges: Vec<ConceptNetEdge>,
82}
83
84#[derive(Clone)]
86pub struct ConceptNetClient {
87 client: Client,
88 base_url: String,
89}
90
91impl ConceptNetClient {
92 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 pub async fn get_related(&self, uri: &str) -> Result<ConceptNetResponse> {
122 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 }