use gobby_core::ai::generation::{
GenerationTier, profile_for_tier, resolve_direct_generation_target,
};
use gobby_core::ai::resolve_route_observed;
use gobby_core::ai_context::{AiConfigSource, AiContext, AiContextOptions};
use gobby_core::config::{
AiCapability, AiRouting, EmbeddingConfig, FalkorConfig, QdrantConfig, resolve_embedding_config,
resolve_falkordb_config, resolve_qdrant_config,
};
use crate::WikiError;
use crate::search::semantic::{
GobbyQdrantBackend, GobbySemanticBackend, OpenAiEmbeddingBackend, SemanticEmbedding,
};
#[derive(Debug, Clone)]
pub(crate) struct RuntimeServices {
pub(crate) postgres_configured: bool,
pub(crate) falkor: Option<FalkorConfig>,
pub(crate) qdrant: Option<QdrantConfig>,
pub(crate) embedding: Option<EmbeddingConfig>,
pub(crate) semantic_embedding: Option<SemanticEmbedding>,
}
impl RuntimeServices {
pub(crate) fn detached() -> Self {
Self {
postgres_configured: false,
falkor: None,
qdrant: None,
embedding: None,
semantic_embedding: None,
}
}
pub(crate) fn shared_code_graph_available(&self) -> bool {
self.falkor.is_some()
}
pub(crate) fn semantic_available(&self) -> bool {
self.qdrant.is_some() && self.semantic_embedding.is_some()
}
pub(crate) fn semantic_backend(
&self,
) -> Option<GobbySemanticBackend<OpenAiEmbeddingBackend, GobbyQdrantBackend>> {
let embedding = self.semantic_embedding.clone()?;
let qdrant = self.qdrant.clone()?;
Some(GobbySemanticBackend::new(
Some(embedding),
Some(qdrant),
OpenAiEmbeddingBackend::new(),
GobbyQdrantBackend,
))
}
}
pub(crate) fn probe_runtime_services(command: &'static str) -> Result<RuntimeServices, WikiError> {
let Some(database_url) = crate::support::env::database_url_for(command)? else {
return Ok(RuntimeServices::detached());
};
let mut conn = gobby_core::postgres::connect_readonly(&database_url).map_err(|error| {
WikiError::Config {
detail: format!("failed to connect to PostgreSQL for {command}: {error}"),
}
})?;
let home = gobby_home(command)?;
let primary = crate::support::search::PostgresConfigSource { conn: &mut conn };
let mut source =
AiConfigSource::with_primary_from_gobby_home(primary, &home).map_err(|error| {
WikiError::Config {
detail: format!("failed to resolve runtime config for {command}: {error}"),
}
})?;
let falkor = resolve_falkordb_config(&mut source);
let qdrant =
resolve_qdrant_config(&mut source).filter(crate::support::config::qdrant_config_has_url);
let embedding = resolve_embedding_config(&mut source);
let semantic_embedding = {
let context = AiContext::resolve(None, &mut source);
resolve_semantic_embedding(&context, &mut source)
};
Ok(RuntimeServices {
postgres_configured: true,
falkor,
qdrant,
embedding,
semantic_embedding,
})
}
pub(crate) fn text_generation_available(
command: &'static str,
requested: AiRouting,
tier: GenerationTier,
) -> bool {
if matches!(requested, AiRouting::Off) {
return false;
}
let Ok(mut source) = crate::support::config::hub_ai_config_source(command) else {
return false;
};
let context = AiContext::resolve_with_options(
None,
&mut source,
AiContextOptions {
no_ai: false,
forced_routing: Some(requested),
},
);
let observed = resolve_route_observed(&context, AiCapability::TextGenerate);
match observed.route {
AiRouting::Daemon => true,
AiRouting::Direct => {
resolve_direct_generation_target(&mut source, &profile_for_tier(tier, None))
.api_base()
.is_some()
}
_ => false,
}
}
pub(crate) fn resolve_semantic_embedding(
context: &AiContext,
source: &mut impl gobby_core::config::ConfigSource,
) -> Option<SemanticEmbedding> {
match effective_embedding_route(context) {
AiRouting::Off => None,
AiRouting::Daemon => {
#[cfg(feature = "ai")]
{
Some(SemanticEmbedding::Daemon(Box::new(context.clone())))
}
#[cfg(not(feature = "ai"))]
{
None
}
}
AiRouting::Direct => resolve_embedding_config(source).map(SemanticEmbedding::Direct),
AiRouting::Auto => {
#[cfg(feature = "ai")]
{
Some(SemanticEmbedding::Daemon(Box::new(context.clone())))
}
#[cfg(not(feature = "ai"))]
{
resolve_embedding_config(source).map(SemanticEmbedding::Direct)
}
}
}
}
fn effective_embedding_route(context: &AiContext) -> AiRouting {
#[cfg(feature = "ai")]
{
gobby_core::ai::effective_route(context, AiCapability::Embed)
}
#[cfg(not(feature = "ai"))]
{
match context.binding(AiCapability::Embed).routing {
AiRouting::Off => AiRouting::Off,
AiRouting::Direct => AiRouting::Direct,
AiRouting::Daemon => {
eprintln!(
"warning: gwiki was built without ai support; daemon-backed embeddings are disabled"
);
AiRouting::Off
}
AiRouting::Auto => {
eprintln!(
"warning: gwiki was built without ai support; auto embedding route cannot use the daemon"
);
AiRouting::Auto
}
}
}
}
fn gobby_home(command: &'static str) -> Result<std::path::PathBuf, WikiError> {
gobby_core::gobby_home().map_err(|error| WikiError::Config {
detail: format!("failed to resolve Gobby home for {command}: {error}"),
})
}