1use serde_json::{json, Value};
6
7use crate::http::HttpError;
8use crate::CoreError;
9
10use super::client::SynthClient;
11
12const GRAPH_EVOLVE_ENDPOINT: &str = "/api/graph-evolve/jobs";
13const GRAPHGEN_LEGACY_ENDPOINT: &str = "/api/graphgen/jobs";
14const GRAPHGEN_COMPLETIONS_ENDPOINT: &str = "/api/graphgen/graph/completions";
15const GRAPHGEN_RECORD_ENDPOINT: &str = "/api/graphgen/graph/record";
16
17pub struct GraphEvolveClient<'a> {
19 client: &'a SynthClient,
20}
21
22impl<'a> GraphEvolveClient<'a> {
23 pub(crate) fn new(client: &'a SynthClient) -> Self {
24 Self { client }
25 }
26
27 async fn get_with_fallback(
28 &self,
29 primary: &str,
30 legacy: Option<&str>,
31 params: Option<&[(&str, &str)]>,
32 ) -> Result<Value, CoreError> {
33 match self.client.http.get_json(primary, params).await {
34 Ok(value) => Ok(value),
35 Err(err) => {
36 if err.status() == Some(404) {
37 if let Some(legacy) = legacy {
38 return self
39 .client
40 .http
41 .get_json(legacy, params)
42 .await
43 .map_err(map_http_error);
44 }
45 }
46 Err(map_http_error(err))
47 }
48 }
49 }
50
51 async fn post_with_fallback(
52 &self,
53 primary: &str,
54 legacy: Option<&str>,
55 payload: &Value,
56 ) -> Result<Value, CoreError> {
57 match self.client.http.post_json(primary, payload).await {
58 Ok(value) => Ok(value),
59 Err(err) => {
60 if err.status() == Some(404) {
61 if let Some(legacy) = legacy {
62 return self
63 .client
64 .http
65 .post_json(legacy, payload)
66 .await
67 .map_err(map_http_error);
68 }
69 }
70 Err(map_http_error(err))
71 }
72 }
73 }
74
75 async fn get_bytes_with_fallback(
76 &self,
77 primary: &str,
78 legacy: Option<&str>,
79 ) -> Result<Vec<u8>, CoreError> {
80 match self.client.http.get_bytes(primary, None).await {
81 Ok(bytes) => Ok(bytes),
82 Err(err) => {
83 if err.status() == Some(404) {
84 if let Some(legacy) = legacy {
85 return self
86 .client
87 .http
88 .get_bytes(legacy, None)
89 .await
90 .map_err(map_http_error);
91 }
92 }
93 Err(map_http_error(err))
94 }
95 }
96 }
97
98 pub async fn submit_job(&self, payload: Value) -> Result<Value, CoreError> {
100 self.post_with_fallback(
101 GRAPH_EVOLVE_ENDPOINT,
102 Some(GRAPHGEN_LEGACY_ENDPOINT),
103 &payload,
104 )
105 .await
106 }
107
108 pub async fn get_status(&self, job_id: &str) -> Result<Value, CoreError> {
110 let primary = format!("{}/{}", GRAPH_EVOLVE_ENDPOINT, job_id);
111 let legacy = format!("{}/{}", GRAPHGEN_LEGACY_ENDPOINT, job_id);
112 self.get_with_fallback(&primary, Some(&legacy), None).await
113 }
114
115 pub async fn start_job(&self, job_id: &str) -> Result<Value, CoreError> {
117 let primary = format!("{}/{}/start", GRAPH_EVOLVE_ENDPOINT, job_id);
118 let legacy = format!("{}/{}/start", GRAPHGEN_LEGACY_ENDPOINT, job_id);
119 self.post_with_fallback(&primary, Some(&legacy), &Value::Null)
120 .await
121 }
122
123 pub async fn get_events(
125 &self,
126 job_id: &str,
127 since_seq: i64,
128 limit: i64,
129 ) -> Result<Value, CoreError> {
130 let primary = format!("{}/{}/events", GRAPH_EVOLVE_ENDPOINT, job_id);
131 let legacy = format!("{}/{}/events", GRAPHGEN_LEGACY_ENDPOINT, job_id);
132 let since = since_seq.to_string();
133 let limit_s = limit.to_string();
134 let params = [("since_seq", since.as_str()), ("limit", limit_s.as_str())];
135 self.get_with_fallback(&primary, Some(&legacy), Some(¶ms))
136 .await
137 }
138
139 pub async fn get_metrics(&self, job_id: &str, query_string: &str) -> Result<Value, CoreError> {
141 let query = query_string.trim_start_matches('?');
142 let primary = if query.is_empty() {
143 format!("{}/{}/metrics", GRAPH_EVOLVE_ENDPOINT, job_id)
144 } else {
145 format!("{}/{}/metrics?{}", GRAPH_EVOLVE_ENDPOINT, job_id, query)
146 };
147 let legacy = if query.is_empty() {
148 format!("{}/{}/metrics", GRAPHGEN_LEGACY_ENDPOINT, job_id)
149 } else {
150 format!("{}/{}/metrics?{}", GRAPHGEN_LEGACY_ENDPOINT, job_id, query)
151 };
152 self.get_with_fallback(&primary, Some(&legacy), None).await
153 }
154
155 pub async fn download_prompt(&self, job_id: &str) -> Result<Value, CoreError> {
157 let primary = format!("{}/{}/download", GRAPH_EVOLVE_ENDPOINT, job_id);
158 let legacy = format!("{}/{}/download", GRAPHGEN_LEGACY_ENDPOINT, job_id);
159 self.get_with_fallback(&primary, Some(&legacy), None).await
160 }
161
162 pub async fn download_graph_txt(&self, job_id: &str) -> Result<String, CoreError> {
164 let primary = format!("{}/{}/graph.txt", GRAPH_EVOLVE_ENDPOINT, job_id);
165 let legacy = format!("{}/{}/graph.txt", GRAPHGEN_LEGACY_ENDPOINT, job_id);
166 let bytes = self
167 .get_bytes_with_fallback(&primary, Some(&legacy))
168 .await?;
169 Ok(String::from_utf8_lossy(&bytes).to_string())
170 }
171
172 pub async fn run_inference(&self, payload: Value) -> Result<Value, CoreError> {
174 self.client
175 .http
176 .post_json(GRAPHGEN_COMPLETIONS_ENDPOINT, &payload)
177 .await
178 .map_err(map_http_error)
179 }
180
181 pub async fn get_graph_record(&self, payload: Value) -> Result<Value, CoreError> {
183 self.client
184 .http
185 .post_json(GRAPHGEN_RECORD_ENDPOINT, &payload)
186 .await
187 .map_err(map_http_error)
188 }
189
190 pub async fn cancel_job(&self, job_id: &str, payload: Value) -> Result<Value, CoreError> {
192 let path = format!("/api/jobs/{}/cancel", job_id);
193 self.client
194 .http
195 .post_json(&path, &payload)
196 .await
197 .map_err(map_http_error)
198 }
199
200 pub async fn query_workflow_state(&self, job_id: &str) -> Result<Value, CoreError> {
202 let path = format!("/api/jobs/{}/workflow-state", job_id);
203 match self.client.http.get_json(&path, None).await {
204 Ok(value) => Ok(value),
205 Err(err) => {
206 let status = err
207 .status()
208 .map(|s| s.to_string())
209 .unwrap_or_else(|| "unknown".to_string());
210 Ok(json!({
211 "job_id": job_id,
212 "workflow_state": Value::Null,
213 "error": format!("HTTP {}: {}", status, err),
214 }))
215 }
216 }
217 }
218}
219
220fn map_http_error(e: HttpError) -> CoreError {
222 match e {
223 HttpError::Response(detail) => {
224 if detail.status == 401 || detail.status == 403 {
225 CoreError::Authentication(format!("authentication failed: {}", detail))
226 } else if detail.status == 429 {
227 CoreError::UsageLimit(crate::UsageLimitInfo::from_http_429(
228 "graph_evolve",
229 &detail,
230 ))
231 } else {
232 CoreError::HttpResponse(crate::HttpErrorInfo {
233 status: detail.status,
234 url: detail.url,
235 message: detail.message,
236 body_snippet: detail.body_snippet,
237 })
238 }
239 }
240 HttpError::Request(e) => CoreError::Http(e),
241 _ => CoreError::Internal(format!("{}", e)),
242 }
243}