use std::collections::HashMap;
use std::sync::Arc;
use crate::core::{AnalyzerRegistry, FactStore, TrustySearchClient};
use crate::embedder::{BowEmbedder, Embedder};
use crate::types::KgGraph;
use axum::{
http::StatusCode,
response::{IntoResponse, Json, Response},
};
use tokio::sync::{broadcast, RwLock};
#[derive(Clone, Debug, serde::Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AnalyzerEvent {
AnalysisStarted {
index_id: String,
},
AnalysisCompleted {
index_id: String,
chunk_count: usize,
duration_ms: u64,
},
FactUpserted {
subject: String,
predicate: String,
},
FactDeleted {
id: String,
},
ScipIngested {
index_id: String,
symbols_ingested: usize,
},
}
pub const DEFAULT_PORT: u16 = 7879;
#[derive(Clone)]
pub struct AnalyzerAppState {
pub search: TrustySearchClient,
pub facts: FactStore,
pub registry: Arc<AnalyzerRegistry>,
pub embedder: Arc<dyn Embedder>,
pub scip_overlays: Arc<RwLock<HashMap<String, KgGraph>>>,
pub events: broadcast::Sender<AnalyzerEvent>,
pub webhook_secret: Option<String>,
pub api_key: Option<String>,
pub llm_model: String,
}
impl AnalyzerAppState {
pub fn new(search: TrustySearchClient, facts: FactStore) -> Self {
let (events_tx, _) = broadcast::channel(128);
Self {
search,
facts,
registry: Arc::new(AnalyzerRegistry::default_registry()),
embedder: Arc::new(BowEmbedder::default()),
scip_overlays: Arc::new(RwLock::new(HashMap::new())),
events: events_tx,
webhook_secret: None,
api_key: std::env::var("OPENROUTER_API_KEY").ok(),
llm_model: std::env::var("TRUSTY_LLM_MODEL")
.unwrap_or_else(|_| "openai/gpt-4o-mini".to_string()),
}
}
pub fn with_registry(
search: TrustySearchClient,
facts: FactStore,
registry: Arc<AnalyzerRegistry>,
) -> Self {
let (events_tx, _) = broadcast::channel(128);
Self {
search,
facts,
registry,
embedder: Arc::new(BowEmbedder::default()),
scip_overlays: Arc::new(RwLock::new(HashMap::new())),
events: events_tx,
webhook_secret: None,
api_key: std::env::var("OPENROUTER_API_KEY").ok(),
llm_model: std::env::var("TRUSTY_LLM_MODEL")
.unwrap_or_else(|_| "openai/gpt-4o-mini".to_string()),
}
}
pub fn with_api_key(mut self, key: Option<String>) -> Self {
self.api_key = key;
self
}
pub fn with_llm_model(mut self, model: impl Into<String>) -> Self {
self.llm_model = model.into();
self
}
pub fn with_webhook_secret(mut self, secret: Option<String>) -> Self {
self.webhook_secret = secret;
self
}
pub fn with_embedder(mut self, embedder: Arc<dyn Embedder>) -> Self {
self.embedder = embedder;
self
}
pub fn emit(&self, event: AnalyzerEvent) {
let _ = self.events.send(event);
}
}
pub(crate) struct ApiError {
pub status: StatusCode,
pub message: String,
}
impl ApiError {
pub fn bad_request(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: msg.into(),
}
}
#[allow(dead_code)]
pub fn not_found(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::NOT_FOUND,
message: msg.into(),
}
}
pub fn internal(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: msg.into(),
}
}
pub fn bad_gateway(msg: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
message: msg.into(),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(
self.status,
Json(serde_json::json!({ "error": self.message })),
)
.into_response()
}
}
pub(crate) async fn fetch_chunks(
state: &AnalyzerAppState,
id: &str,
) -> Result<Vec<crate::types::CodeChunk>, ApiError> {
state.search.get_chunks(id).await.map_err(|e| {
tracing::warn!("get_chunks({id}) failed: {e:#}");
ApiError::bad_gateway(format!("get_chunks({id}): {e:#}"))
})
}