use super::config::GraphRagConfig;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::Duration;
use tracing::{debug, info, warn};
#[derive(Debug, Serialize)]
struct IndexRequest {
repo_path: String,
repo_url: Option<String>,
branch: String,
incremental: bool,
}
#[derive(Debug, Deserialize)]
pub struct IndexResponse {
pub job_id: String,
pub status: String,
pub message: String,
}
#[derive(Debug, Deserialize)]
pub struct QueryResponse {
pub patterns: Vec<CodePattern>,
pub related_symbols: Vec<RelatedSymbol>,
pub summary: String,
}
#[derive(Debug, Deserialize)]
pub struct CodePattern {
pub pattern_type: String,
pub name: String,
pub file_path: String,
pub line_start: i32,
pub line_end: i32,
pub confidence: f32,
pub description: String,
pub code_snippet: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct RelatedSymbol {
pub name: String,
pub kind: String,
pub file_path: String,
pub line: i32,
pub relationship: String,
pub distance: i32,
}
pub struct GraphRagClient {
config: GraphRagConfig,
client: reqwest::Client,
}
impl GraphRagClient {
pub fn new() -> Result<Self> {
Self::with_config(GraphRagConfig::load())
}
pub fn with_config(config: GraphRagConfig) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.context("Failed to create HTTP client")?;
Ok(Self { config, client })
}
pub async fn is_available(&self) -> bool {
if !self.config.enabled {
return false;
}
let url = format!("{}/health", self.config.api_url);
match self.client.get(&url).send().await {
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
pub async fn index_repository(
&self,
repo_path: &Path,
repo_url: Option<&str>,
branch: Option<&str>,
) -> Result<IndexResponse> {
if !self.config.enabled {
return Ok(IndexResponse {
job_id: "disabled".to_string(),
status: "skipped".to_string(),
message: "GraphRAG indexing is disabled".to_string(),
});
}
let abs_path = repo_path
.canonicalize()
.unwrap_or_else(|_| repo_path.to_path_buf());
let translated_path = self.config.translate_path(&abs_path.to_string_lossy());
debug!(
"Path translation: {} -> {}",
abs_path.display(),
translated_path
);
let request = IndexRequest {
repo_path: translated_path,
repo_url: repo_url.map(|s| s.to_string()),
branch: branch.unwrap_or("main").to_string(),
incremental: self.config.incremental,
};
let url = format!("{}/index", self.config.api_url);
debug!("Sending index request to {}: {:?}", url, request);
let mut req = self.client.post(&url).json(&request);
if let Some(ref key) = self.config.api_key {
req = req.bearer_auth(key);
}
let response = req.send().await.context("Failed to send index request")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!("Index request failed ({}): {}", status, body);
}
let result: IndexResponse = response
.json()
.await
.context("Failed to parse index response")?;
info!(
"GraphRAG indexing queued: job_id={}, status={}",
result.job_id, result.status
);
if !self.config.async_index {
self.wait_for_completion(&result.job_id).await?;
}
Ok(result)
}
async fn wait_for_completion(&self, job_id: &str) -> Result<IndexResponse> {
let url = format!("{}/index/{}", self.config.api_url, job_id);
let max_attempts = 60;
for attempt in 0..max_attempts {
tokio::time::sleep(Duration::from_secs(5)).await;
let mut req = self.client.get(&url);
if let Some(ref key) = self.config.api_key {
req = req.bearer_auth(key);
}
let response = req.send().await?;
let status: IndexResponse = response.json().await?;
match status.status.as_str() {
"completed" => {
info!("GraphRAG indexing completed: {}", status.message);
return Ok(status);
}
"failed" => {
anyhow::bail!("Indexing failed: {}", status.message);
}
_ => {
debug!(
"Indexing in progress (attempt {}/{}): {}",
attempt + 1,
max_attempts,
status.status
);
}
}
}
anyhow::bail!("Indexing timed out after {} attempts", max_attempts)
}
pub async fn query(
&self,
repo_id: &str,
query: &str,
max_results: usize,
) -> Result<QueryResponse> {
#[derive(Serialize)]
struct QueryRequest<'a> {
repo_id: &'a str,
query: &'a str,
max_results: usize,
include_code: bool,
}
let url = format!("{}/query", self.config.api_url);
let request = QueryRequest {
repo_id,
query,
max_results,
include_code: true,
};
let mut req = self.client.post(&url).json(&request);
if let Some(ref key) = self.config.api_key {
req = req.bearer_auth(key);
}
let response = req.send().await.context("Failed to send query request")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!("Query failed ({}): {}", status, body);
}
response
.json()
.await
.context("Failed to parse query response")
}
pub async fn get_call_graph(
&self,
repo_id: &str,
symbol_name: &str,
depth: usize,
) -> Result<serde_json::Value> {
#[derive(Serialize)]
struct CallGraphRequest<'a> {
repo_id: &'a str,
symbol_name: &'a str,
depth: usize,
direction: &'a str,
}
let url = format!("{}/callgraph", self.config.api_url);
let request = CallGraphRequest {
repo_id,
symbol_name,
depth,
direction: "both",
};
let mut req = self.client.post(&url).json(&request);
if let Some(ref key) = self.config.api_key {
req = req.bearer_auth(key);
}
let response = req
.send()
.await
.context("Failed to send callgraph request")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!("Call graph request failed ({}): {}", status, body);
}
response
.json()
.await
.context("Failed to parse callgraph response")
}
}
impl Default for GraphRagClient {
fn default() -> Self {
Self::new().expect("Failed to create default GraphRAG client")
}
}
pub async fn trigger_indexing(repo_path: &Path, repo_url: Option<&str>, branch: Option<&str>) {
let config = GraphRagConfig::load();
if !config.enabled {
debug!("GraphRAG indexing disabled, skipping");
return;
}
match GraphRagClient::with_config(config) {
Ok(client) => {
if !client.is_available().await {
debug!("GraphRAG service not available, skipping indexing");
return;
}
match client.index_repository(repo_path, repo_url, branch).await {
Ok(response) => {
info!("GraphRAG: {} (job: {})", response.message, response.job_id);
}
Err(e) => {
warn!("GraphRAG indexing failed: {}", e);
}
}
}
Err(e) => {
warn!("Failed to create GraphRAG client: {}", e);
}
}
}
pub fn trigger_indexing_sync(repo_path: &Path, repo_url: Option<&str>, branch: Option<&str>) {
let path = repo_path.to_path_buf();
let url = repo_url.map(|s| s.to_string());
let br = branch.map(|s| s.to_string());
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
trigger_indexing(&path, url.as_deref(), br.as_deref()).await;
});
} else {
if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
rt.block_on(async {
trigger_indexing(&path, url.as_deref(), br.as_deref()).await;
});
}
}
}