1use std::sync::Arc;
5
6use tokio_util::sync::CancellationToken;
7
8use crate::events::{EventSink, Reporter};
9use crate::llm::LlmClient;
10use crate::team::TeamSettings;
11use crate::worktree::{Credentials, NoCredentials};
12
13#[derive(Debug, Clone)]
16pub struct Settings {
17 pub llm_model: String,
18 pub llm_base_url: Option<String>,
19 pub embed_model: String,
20 pub embed: bool,
21 pub enrich: bool,
22 pub flows: bool,
23 pub guide: bool,
24 pub max_enrich: usize,
25 pub max_flows: usize,
26 pub llm_concurrency: usize,
27 pub prices: Option<(f64, f64)>,
29 pub format: String,
31 pub scip: crate::scip::Mode,
32 pub auto_index: bool,
33 pub auto_index_llm: bool,
34 pub auto_index_every: std::time::Duration,
36}
37
38pub const AUTO_INDEX_MINUTES: usize = 5;
40pub const MAX_AUTO_INDEX_MINUTES: usize = 1440;
41
42impl Settings {
43 pub fn resolve(team: &TeamSettings, env: impl Fn(&str) -> Option<String>) -> Settings {
45 let env = |k: &str| env(k).filter(|v| !v.is_empty());
46 Settings {
47 llm_model: team
48 .llm_model
49 .clone()
50 .or_else(|| env("COMINDLY_LLM_MODEL"))
51 .unwrap_or_else(|| crate::llm::DEFAULT_MODEL.to_string()),
52 llm_base_url: team
53 .llm_base_url
54 .clone()
55 .or_else(|| env("COMINDLY_LLM_BASE_URL")),
56 embed_model: team
57 .embed_model
58 .clone()
59 .or_else(|| env("COMINDLY_EMBED_MODEL"))
60 .unwrap_or_else(|| crate::embed::DEFAULT_MODEL.to_string()),
61 embed: team.embed.unwrap_or(true),
62 enrich: team.enrich.unwrap_or(true),
63 flows: team.flows.unwrap_or(true),
64 guide: team.guide.unwrap_or(true),
65 max_enrich: team.max_enrich.unwrap_or(usize::MAX),
66 max_flows: team.max_flows.unwrap_or(usize::MAX),
67 llm_concurrency: team.llm_concurrency.unwrap_or(8).clamp(1, 64),
68 prices: (team.llm_price_in.is_some() || team.llm_price_out.is_some()).then(|| {
69 (
70 team.llm_price_in.unwrap_or(0.0),
71 team.llm_price_out.unwrap_or(0.0),
72 )
73 }),
74 format: env("COMINDLY_FORMAT")
75 .or_else(|| team.format.clone())
76 .unwrap_or_else(|| "md".to_string()),
77 scip: env("COMINDLY_SCIP")
78 .or_else(|| team.scip.clone())
79 .and_then(|s| crate::scip::Mode::parse(&s))
80 .unwrap_or(crate::scip::Mode::Auto),
81 auto_index: team.auto_index.unwrap_or(false),
82 auto_index_llm: team.auto_index_llm.unwrap_or(false),
83 auto_index_every: std::time::Duration::from_secs(
84 60 * team
85 .auto_index_minutes
86 .unwrap_or(AUTO_INDEX_MINUTES)
87 .clamp(1, MAX_AUTO_INDEX_MINUTES) as u64,
88 ),
89 }
90 }
91}
92
93#[derive(Clone)]
95pub struct IndexContext {
96 pub root: String,
98 pub settings: Settings,
99 pub llm: Option<Arc<LlmClient>>,
101 pub git: Arc<dyn Credentials>,
103 pub report: Reporter,
104 pub cancel: CancellationToken,
105 pub cache: std::path::PathBuf,
107}
108
109impl IndexContext {
110 pub fn new(
111 root: impl Into<String>,
112 settings: Settings,
113 events: Arc<dyn EventSink>,
114 cache: std::path::PathBuf,
115 ) -> Self {
116 IndexContext {
117 root: root.into(),
118 settings,
119 llm: None,
120 git: Arc::new(NoCredentials),
121 report: Reporter::new(events),
122 cancel: CancellationToken::new(),
123 cache,
124 }
125 }
126
127 pub fn with_llm(mut self, llm: Option<LlmClient>) -> Self {
128 self.llm = llm.map(Arc::new);
129 self
130 }
131
132 pub fn with_git(mut self, git: Arc<dyn Credentials>) -> Self {
133 self.git = git;
134 self
135 }
136
137 pub fn graph(&self) -> String {
139 format!("{}/_graph", self.root.trim_end_matches('/'))
140 }
141
142 pub fn check(&self) -> anyhow::Result<()> {
144 anyhow::ensure!(!self.cancel.is_cancelled(), Cancelled);
145 Ok(())
146 }
147}
148
149#[derive(Debug, Clone, Copy)]
151pub struct Cancelled;
152
153impl std::fmt::Display for Cancelled {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.write_str("cancelled")
156 }
157}
158
159impl std::error::Error for Cancelled {}