1use std::sync::atomic::{AtomicBool, Ordering};
4
5use crate::config::Config;
6use crate::error::{Error, Result};
7use crate::ffi::{self, AgentHandle};
8use crate::init;
9use crate::result::AgentResult;
10
11const DEFAULT_TIMEOUT_MS: u64 = 60_000;
13
14pub struct GopherAgent {
16 handle: AgentHandle,
17 disposed: AtomicBool,
18}
19
20unsafe impl Send for GopherAgent {}
22unsafe impl Sync for GopherAgent {}
23
24impl GopherAgent {
25 pub fn create(config: Config) -> Result<Self> {
27 init()?;
28
29 let handle = if config.has_api_key() {
30 ffi::agent_create_by_api_key(
31 config.provider(),
32 config.model(),
33 config.api_key().unwrap_or(""),
34 )
35 } else if config.has_server_config() {
36 ffi::agent_create_by_json(
37 config.provider(),
38 config.model(),
39 config.server_config().unwrap_or(""),
40 )
41 } else {
42 return Err(Error::config(
43 "Either API key or server config must be provided",
44 ));
45 };
46
47 if handle.is_null() {
48 let err_msg = ffi::get_last_error();
49 ffi::clear_error();
50 let msg = if err_msg.is_empty() {
51 "Failed to create agent".to_string()
52 } else {
53 err_msg
54 };
55 return Err(Error::agent(msg));
56 }
57
58 Ok(GopherAgent {
59 handle,
60 disposed: AtomicBool::new(false),
61 })
62 }
63
64 pub fn create_with_api_key(provider: &str, model: &str, api_key: &str) -> Result<Self> {
66 let config = crate::ConfigBuilder::new()
67 .with_provider(provider)
68 .with_model(model)
69 .with_api_key(api_key)
70 .build();
71 Self::create(config)
72 }
73
74 pub fn create_with_server_config(
76 provider: &str,
77 model: &str,
78 server_config: &str,
79 ) -> Result<Self> {
80 let config = crate::ConfigBuilder::new()
81 .with_provider(provider)
82 .with_model(model)
83 .with_server_config(server_config)
84 .build();
85 Self::create(config)
86 }
87
88 pub fn run(&self, query: &str) -> Result<String> {
90 self.run_with_timeout(query, DEFAULT_TIMEOUT_MS)
91 }
92
93 pub fn run_with_timeout(&self, query: &str, timeout_ms: u64) -> Result<String> {
95 self.ensure_not_disposed()?;
96
97 let response = ffi::agent_run(self.handle, query, timeout_ms);
98 if response.is_empty() {
99 Ok(format!("No response for query: \"{}\"", query))
100 } else {
101 Ok(response)
102 }
103 }
104
105 pub fn run_detailed(&self, query: &str) -> AgentResult {
107 self.run_detailed_with_timeout(query, DEFAULT_TIMEOUT_MS)
108 }
109
110 pub fn run_detailed_with_timeout(&self, query: &str, timeout_ms: u64) -> AgentResult {
112 match self.run_with_timeout(query, timeout_ms) {
113 Ok(response) => AgentResult::success(response),
114 Err(Error::Timeout(msg)) => AgentResult::timeout(msg),
115 Err(e) => AgentResult::error(e.to_string()),
116 }
117 }
118
119 pub fn is_disposed(&self) -> bool {
121 self.disposed.load(Ordering::SeqCst)
122 }
123
124 fn ensure_not_disposed(&self) -> Result<()> {
126 if self.is_disposed() {
127 Err(Error::Disposed)
128 } else {
129 Ok(())
130 }
131 }
132
133 fn dispose(&self) {
135 if self
136 .disposed
137 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
138 .is_ok()
139 && !self.handle.is_null()
140 {
141 ffi::agent_release(self.handle);
142 }
143 }
144}
145
146impl Drop for GopherAgent {
147 fn drop(&mut self) {
148 self.dispose();
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 const TEST_SERVER_CONFIG: &str = r#"{
157 "succeeded": true,
158 "code": 200000000,
159 "message": "success",
160 "data": {
161 "servers": [
162 {
163 "version": "2025-01-09",
164 "serverId": "1",
165 "name": "test-server",
166 "transport": "http_sse",
167 "config": {"url": "http://127.0.0.1:9999/mcp", "headers": {}},
168 "connectTimeout": 5000,
169 "requestTimeout": 30000
170 }
171 ]
172 }
173 }"#;
174
175 fn skip_if_native_library_not_available() -> bool {
176 !ffi::is_available()
177 }
178
179 #[test]
180 fn test_create_with_empty_config() {
181 if skip_if_native_library_not_available() {
182 return;
183 }
184
185 let config = crate::ConfigBuilder::new().build();
186 let result = GopherAgent::create(config);
187 assert!(result.is_err());
188 }
189
190 #[test]
191 fn test_create_with_server_config() {
192 if skip_if_native_library_not_available() {
193 return;
194 }
195
196 let result = GopherAgent::create_with_server_config(
197 "AnthropicProvider",
198 "claude-3-haiku-20240307",
199 TEST_SERVER_CONFIG,
200 );
201
202 if let Ok(agent) = result {
204 assert!(!agent.is_disposed());
205 }
206 }
207
208 #[test]
209 fn test_disposed_after_drop() {
210 if skip_if_native_library_not_available() {
211 return;
212 }
213
214 let result = GopherAgent::create_with_server_config(
215 "AnthropicProvider",
216 "claude-3-haiku-20240307",
217 TEST_SERVER_CONFIG,
218 );
219
220 if let Ok(agent) = result {
221 assert!(!agent.is_disposed());
222 drop(agent);
223 }
225 }
226}