titan-synapse 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CONTRIBUTING.md +187 -0
  2. package/Cargo.lock +3976 -0
  3. package/Cargo.toml +10 -0
  4. package/LICENSE +190 -0
  5. package/PROGRESS.md +151 -0
  6. package/README.md +514 -0
  7. package/TEST_LOG.md +220 -0
  8. package/config/default.yaml +36 -0
  9. package/crates/synapse/Cargo.toml +70 -0
  10. package/crates/synapse/src/cli/bench.rs +44 -0
  11. package/crates/synapse/src/cli/eval.rs +395 -0
  12. package/crates/synapse/src/cli/export.rs +45 -0
  13. package/crates/synapse/src/cli/hub.rs +179 -0
  14. package/crates/synapse/src/cli/import.rs +35 -0
  15. package/crates/synapse/src/cli/learn.rs +53 -0
  16. package/crates/synapse/src/cli/mod.rs +10 -0
  17. package/crates/synapse/src/cli/models.rs +36 -0
  18. package/crates/synapse/src/cli/pull.rs +60 -0
  19. package/crates/synapse/src/cli/status.rs +52 -0
  20. package/crates/synapse/src/cli/train.rs +99 -0
  21. package/crates/synapse/src/config.rs +220 -0
  22. package/crates/synapse/src/dashboard.rs +281 -0
  23. package/crates/synapse/src/format/manifest.rs +57 -0
  24. package/crates/synapse/src/format/mod.rs +4 -0
  25. package/crates/synapse/src/format/packer.rs +213 -0
  26. package/crates/synapse/src/inference/engine.rs +361 -0
  27. package/crates/synapse/src/inference/kv_cache.rs +97 -0
  28. package/crates/synapse/src/inference/lora.rs +166 -0
  29. package/crates/synapse/src/inference/mod.rs +9 -0
  30. package/crates/synapse/src/inference/model.rs +167 -0
  31. package/crates/synapse/src/inference/sampler.rs +133 -0
  32. package/crates/synapse/src/inference/speculative.rs +153 -0
  33. package/crates/synapse/src/learn/cloud_fallback.rs +186 -0
  34. package/crates/synapse/src/learn/engine.rs +109 -0
  35. package/crates/synapse/src/learn/mod.rs +5 -0
  36. package/crates/synapse/src/main.rs +185 -0
  37. package/crates/synapse/src/memory/extractor.rs +201 -0
  38. package/crates/synapse/src/memory/graph.rs +332 -0
  39. package/crates/synapse/src/memory/hallucination.rs +259 -0
  40. package/crates/synapse/src/memory/mod.rs +7 -0
  41. package/crates/synapse/src/openai.rs +232 -0
  42. package/crates/synapse/src/server.rs +166 -0
  43. package/crates/synapse/src/streaming.rs +80 -0
  44. package/crates/synapse/src/swarm/coordinator.rs +198 -0
  45. package/crates/synapse/src/swarm/mod.rs +8 -0
  46. package/crates/synapse/src/swarm/orchestrator.rs +225 -0
  47. package/crates/synapse/src/swarm/pool.rs +64 -0
  48. package/crates/synapse/src/swarm/spawner.rs +199 -0
  49. package/crates/synapse/src/swarm/synthesizer.rs +26 -0
  50. package/crates/synapse/src/vram/manager.rs +67 -0
  51. package/crates/synapse/src/vram/mod.rs +3 -0
  52. package/docker-compose.yml +19 -0
  53. package/install.sh +311 -0
  54. package/package.json +36 -0
  55. package/python/Dockerfile.learn +18 -0
  56. package/python/requirements.txt +11 -0
  57. package/python/synapse_learn/__init__.py +0 -0
  58. package/python/synapse_learn/datasets.py +233 -0
  59. package/python/synapse_learn/real_eval.py +616 -0
  60. package/python/synapse_learn/server.py +431 -0
  61. package/python/synapse_learn/train_base.py +672 -0
  62. package/python/synapse_learn/train_specialists.py +787 -0
@@ -0,0 +1,53 @@
1
+ use anyhow::Result;
2
+ use colored::Colorize;
3
+ use crate::config::SynapseConfig;
4
+ use crate::learn::LearningEngine;
5
+
6
+ pub async fn status(config: &SynapseConfig) -> Result<()> {
7
+ println!("{}", "Learning Engine Status".bold().cyan());
8
+ println!("{}", "═".repeat(50));
9
+
10
+ let engine = LearningEngine::new(&config.learning.sidecar_url, config.learning.enabled);
11
+
12
+ match engine.status().await {
13
+ Ok(status) => {
14
+ println!(" {} {}", "Pairs collected:".bold(), status.pairs_collected);
15
+ println!(" {} {}", "Training queue:".bold(), status.training_queue);
16
+ println!(" {} {}", "Adapters created:".bold(), status.adapters_created);
17
+ if let Some(last) = status.last_trained {
18
+ println!(" {} {}", "Last trained:".bold(), last);
19
+ }
20
+ }
21
+ Err(e) => {
22
+ println!(" {} Learning sidecar not reachable: {e}", "⚠".yellow());
23
+ println!(" Start it with: {}", "docker compose up synapse-learn".yellow());
24
+ }
25
+ }
26
+
27
+ Ok(())
28
+ }
29
+
30
+ pub async fn train_now(config: &SynapseConfig) -> Result<()> {
31
+ println!("{}", "Triggering training...".bold().cyan());
32
+
33
+ let engine = LearningEngine::new(&config.learning.sidecar_url, config.learning.enabled);
34
+
35
+ let request = crate::learn::engine::TrainRequest {
36
+ specialist: "general".into(),
37
+ base_model: config.base_model.clone(),
38
+ };
39
+
40
+ match engine.train_now(request).await {
41
+ Ok(result) => {
42
+ println!(" {} Training complete!", "✓".green());
43
+ println!(" Adapter: {}", result.adapter_path);
44
+ println!(" Loss: {:.4}", result.loss);
45
+ println!(" Pairs used: {}", result.pairs_used);
46
+ }
47
+ Err(e) => {
48
+ println!(" {} Training failed: {e}", "✗".red());
49
+ }
50
+ }
51
+
52
+ Ok(())
53
+ }
@@ -0,0 +1,10 @@
1
+ pub mod status;
2
+ pub mod models;
3
+ pub mod pull;
4
+ pub mod export;
5
+ pub mod import;
6
+ pub mod learn;
7
+ pub mod bench;
8
+ pub mod eval;
9
+ pub mod hub;
10
+ pub mod train;
@@ -0,0 +1,36 @@
1
+ use anyhow::Result;
2
+ use colored::Colorize;
3
+ use crate::config::SynapseConfig;
4
+
5
+ pub async fn run(config: &SynapseConfig) -> Result<()> {
6
+ println!("{}", "Available Models".bold().cyan());
7
+ println!("{}", "═".repeat(50));
8
+
9
+ // Scan models directory
10
+ if !config.models_dir.exists() {
11
+ println!(" No models found. Use {} to download one.", "synapse pull <model>".yellow());
12
+ return Ok(());
13
+ }
14
+
15
+ let mut found = false;
16
+ for entry in std::fs::read_dir(&config.models_dir)? {
17
+ let entry = entry?;
18
+ let path = entry.path();
19
+ if path.is_file() {
20
+ let name = path.file_name().unwrap_or_default().to_string_lossy();
21
+ let size_mb = std::fs::metadata(&path)?.len() / (1024 * 1024);
22
+ println!(" {} ({} MB)", name.green(), size_mb);
23
+ found = true;
24
+ }
25
+ }
26
+
27
+ if !found {
28
+ println!(" No models found. Use {} to download one.", "synapse pull <model>".yellow());
29
+ println!("\n Available models:");
30
+ println!(" {} — Coordinator (0.5 GB)", "qwen3-0.6b".yellow());
31
+ println!(" {} — Specialist base (2.1 GB)", "qwen3-3b".yellow());
32
+ println!(" {} — Generalist (4.5 GB)", "qwen3-7b".yellow());
33
+ }
34
+
35
+ Ok(())
36
+ }
@@ -0,0 +1,60 @@
1
+ use anyhow::Result;
2
+ use colored::Colorize;
3
+ use crate::config::SynapseConfig;
4
+
5
+ /// Known model mappings to HuggingFace repos
6
+ const MODEL_REGISTRY: &[(&str, &str, &str)] = &[
7
+ ("qwen3-0.6b", "Qwen/Qwen3-0.6B-GGUF", "qwen3-0.6b-q4_k_m.gguf"),
8
+ ("qwen3-3b", "Qwen/Qwen3-4B-GGUF", "qwen3-4b-q4_k_m.gguf"),
9
+ ("qwen3-7b", "Qwen/Qwen3-8B-GGUF", "qwen3-8b-q4_k_m.gguf"),
10
+ ];
11
+
12
+ pub async fn run(config: &SynapseConfig, model: &str) -> Result<()> {
13
+ let (name, repo, filename) = MODEL_REGISTRY.iter()
14
+ .find(|(n, _, _)| *n == model)
15
+ .ok_or_else(|| anyhow::anyhow!(
16
+ "Unknown model '{model}'. Available: {}",
17
+ MODEL_REGISTRY.iter().map(|(n, _, _)| *n).collect::<Vec<_>>().join(", ")
18
+ ))?;
19
+
20
+ let output_path = config.models_dir.join(filename);
21
+ if output_path.exists() {
22
+ println!(" {} {} already downloaded", "✓".green(), name);
23
+ return Ok(());
24
+ }
25
+
26
+ std::fs::create_dir_all(&config.models_dir)?;
27
+
28
+ println!("{} {} from {}", "Pulling".bold().cyan(), name, repo);
29
+ println!(" Downloading to {}", output_path.display());
30
+
31
+ // Use huggingface-cli if available, otherwise curl
32
+ let hf_cli = tokio::process::Command::new("huggingface-cli")
33
+ .args(["download", repo, filename, "--local-dir", &config.models_dir.to_string_lossy()])
34
+ .output()
35
+ .await;
36
+
37
+ match hf_cli {
38
+ Ok(out) if out.status.success() => {
39
+ println!(" {} Downloaded {}", "✓".green(), name);
40
+ }
41
+ _ => {
42
+ // Fallback to direct URL download
43
+ let url = format!("https://huggingface.co/{repo}/resolve/main/{filename}");
44
+ println!(" Using direct download: {url}");
45
+
46
+ let output = tokio::process::Command::new("curl")
47
+ .args(["-L", "-o", &output_path.to_string_lossy(), &url, "--progress-bar"])
48
+ .status()
49
+ .await?;
50
+
51
+ if output.success() {
52
+ println!(" {} Downloaded {}", "✓".green(), name);
53
+ } else {
54
+ anyhow::bail!("Failed to download model. Try manually:\n huggingface-cli download {repo} {filename}");
55
+ }
56
+ }
57
+ }
58
+
59
+ Ok(())
60
+ }
@@ -0,0 +1,52 @@
1
+ use anyhow::Result;
2
+ use colored::Colorize;
3
+ use crate::config::SynapseConfig;
4
+ use crate::vram::VramManager;
5
+
6
+ pub async fn run(config: &SynapseConfig) -> Result<()> {
7
+ println!("{}", "TITAN Synapse Status".bold().cyan());
8
+ println!("{}", "═".repeat(50));
9
+
10
+ // Version
11
+ println!(" {} {}", "Version:".bold(), env!("CARGO_PKG_VERSION"));
12
+ println!(" {} {}", "Data dir:".bold(), config.data_dir.display());
13
+
14
+ // GPU info
15
+ println!("\n{}", "GPU".bold().yellow());
16
+ match VramManager::gpu_info().await {
17
+ Ok(info) => {
18
+ println!(" {} {}", "Name:".bold(), info.name);
19
+ println!(" {} {} MB / {} MB ({:.1}% used)",
20
+ "VRAM:".bold(),
21
+ info.vram_used_mb, info.vram_total_mb,
22
+ (info.vram_used_mb as f32 / info.vram_total_mb.max(1) as f32) * 100.0
23
+ );
24
+ println!(" {} {:.1}%", "GPU Util:".bold(), info.utilization_percent);
25
+ if let Some(temp) = info.temperature_c {
26
+ println!(" {} {}°C", "Temp:".bold(), temp);
27
+ }
28
+ }
29
+ Err(e) => println!(" {} {e}", "Error:".red()),
30
+ }
31
+
32
+ // Models
33
+ println!("\n{}", "Configuration".bold().yellow());
34
+ println!(" {} {}", "Coordinator:".bold(), config.coordinator_model);
35
+ println!(" {} {}", "Base model:".bold(), config.base_model);
36
+ println!(" {} {}", "Specialists:".bold(), config.specialists.len());
37
+ for spec in &config.specialists {
38
+ println!(" {} [{}]",
39
+ format!("• {}", spec.name).green(),
40
+ spec.capabilities.join(", ")
41
+ );
42
+ }
43
+
44
+ // Learning
45
+ println!("\n{}", "Learning".bold().yellow());
46
+ println!(" {} {}", "Enabled:".bold(),
47
+ if config.learning.enabled { "yes".green() } else { "no".red() }
48
+ );
49
+ println!(" {} {}", "Sidecar:".bold(), config.learning.sidecar_url);
50
+
51
+ Ok(())
52
+ }
@@ -0,0 +1,99 @@
1
+ use anyhow::Result;
2
+ use colored::Colorize;
3
+ use crate::config::SynapseConfig;
4
+ use std::process::Command;
5
+
6
+ /// Train our own Synapse model — not someone else's, OURS.
7
+ ///
8
+ /// Pipeline:
9
+ /// 1. Generate training data (swarm routing + honesty + public datasets + user prefs)
10
+ /// 2. SFT (Supervised Fine-Tuning) with QLoRA on the base architecture
11
+ /// 3. DPO (Direct Preference Optimization) using collected preference pairs
12
+ /// 4. Export to GGUF for the Synapse inference engine
13
+ ///
14
+ /// The base architecture (Qwen2.5-3B) is Apache 2.0 licensed.
15
+ /// Once we fine-tune it, the result is OUR model: synapse-3b.
16
+ pub async fn run(config: &SynapseConfig, stage: &str, base_model: &str, output: &str) -> Result<()> {
17
+ println!("{}", "╔══════════════════════════════════════════════════╗".bold().purple());
18
+ println!("{}", "║ TITAN SYNAPSE — Model Training Pipeline ║".bold().purple());
19
+ println!("{}", "║ Building OUR model. Not theirs. OURS. ║".bold().purple());
20
+ println!("{}", "╚══════════════════════════════════════════════════╝".bold().purple());
21
+ println!();
22
+
23
+ println!(" {} {}", "Stage:".bold(), stage.cyan());
24
+ println!(" {} {}", "Base Architecture:".bold(), base_model);
25
+ println!(" {} {}", "Output Model:".bold(), output.green().bold());
26
+ println!(" {} {}", "License:".bold(), "Apache 2.0 (our model, our weights)");
27
+ println!();
28
+
29
+ // Check if Python training script exists
30
+ let train_script = config.data_dir
31
+ .parent().unwrap_or(&config.data_dir)
32
+ .join("python/synapse_learn/train_base.py");
33
+
34
+ // Also check relative to current directory
35
+ let script_paths = [
36
+ std::path::PathBuf::from("python/synapse_learn/train_base.py"),
37
+ train_script.clone(),
38
+ config.data_dir.join("train_base.py"),
39
+ ];
40
+
41
+ let script = script_paths.iter().find(|p| p.exists());
42
+
43
+ if let Some(script_path) = script {
44
+ println!(" {} Found training script: {}", "✓".green(), script_path.display());
45
+ println!();
46
+
47
+ // Run the Python training pipeline
48
+ println!(" {} Starting training...", "⚡".yellow());
49
+ println!(" {}", "This will take a while. Go grab coffee. Or three.".dimmed());
50
+ println!();
51
+
52
+ let result = Command::new("python")
53
+ .args([
54
+ script_path.to_str().unwrap(),
55
+ "--stage", stage,
56
+ "--base-model", base_model,
57
+ "--output", output,
58
+ ])
59
+ .env("SYNAPSE_DATA_DIR", config.data_dir.to_str().unwrap())
60
+ .status();
61
+
62
+ match result {
63
+ Ok(status) if status.success() => {
64
+ println!();
65
+ println!(" {}", "═".repeat(50).purple());
66
+ println!(" {} {}", "✓".green().bold(), "Model training complete!".bold().green());
67
+ println!();
68
+ println!(" Your model: {}", format!("{}.gguf", output).cyan().bold());
69
+ println!(" Location: {}", config.models_dir.display());
70
+ println!();
71
+ println!(" {} Start using it:", "→".bold());
72
+ println!(" {} synapse up", "$".dimmed());
73
+ println!(" Then open {} in your browser", "http://localhost:6900".cyan());
74
+ println!();
75
+ println!(" {}", "This model is YOURS. Train it more. Make it smarter.".bold());
76
+ println!(" {}", "Every conversation it has makes it better.".dimmed());
77
+ }
78
+ Ok(status) => {
79
+ println!(" {} Training exited with code: {:?}", "⚠".yellow(), status.code());
80
+ }
81
+ Err(e) => {
82
+ println!(" {} Failed to run training: {e}", "✗".red());
83
+ println!();
84
+ println!(" {} Install Python dependencies:", "→".bold());
85
+ println!(" pip install torch transformers peft trl bitsandbytes datasets");
86
+ }
87
+ }
88
+ } else {
89
+ // No Python script found — print manual instructions
90
+ println!(" {} Training script not found at expected paths.", "⚠".yellow());
91
+ println!();
92
+ println!(" {} Manual training:", "→".bold());
93
+ println!(" cd python/synapse_learn");
94
+ println!(" pip install torch transformers peft trl bitsandbytes datasets");
95
+ println!(" python train_base.py --stage {} --base-model {} --output {}", stage, base_model, output);
96
+ }
97
+
98
+ Ok(())
99
+ }
@@ -0,0 +1,220 @@
1
+ use anyhow::Result;
2
+ use serde::{Deserialize, Serialize};
3
+ use std::path::PathBuf;
4
+
5
+ #[derive(Debug, Clone, Serialize, Deserialize)]
6
+ pub struct SynapseConfig {
7
+ /// Server port
8
+ #[serde(default = "default_port")]
9
+ pub port: u16,
10
+
11
+ /// Data directory (~/.synapse)
12
+ #[serde(default = "default_data_dir")]
13
+ pub data_dir: PathBuf,
14
+
15
+ /// Models directory
16
+ #[serde(default = "default_models_dir")]
17
+ pub models_dir: PathBuf,
18
+
19
+ /// Adapters directory
20
+ #[serde(default = "default_adapters_dir")]
21
+ pub adapters_dir: PathBuf,
22
+
23
+ /// Coordinator model name
24
+ #[serde(default = "default_coordinator")]
25
+ pub coordinator_model: String,
26
+
27
+ /// Default specialist base model
28
+ #[serde(default = "default_base_model")]
29
+ pub base_model: String,
30
+
31
+ /// Max VRAM budget in MB (0 = auto-detect)
32
+ #[serde(default)]
33
+ pub max_vram_mb: u64,
34
+
35
+ /// Cloud fallback configuration
36
+ #[serde(default)]
37
+ pub cloud: CloudConfig,
38
+
39
+ /// Learning engine configuration
40
+ #[serde(default)]
41
+ pub learning: LearningConfig,
42
+
43
+ /// Specialist definitions
44
+ #[serde(default)]
45
+ pub specialists: Vec<SpecialistConfig>,
46
+ }
47
+
48
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
49
+ pub struct CloudConfig {
50
+ /// Cloud API base URL (OpenAI-compatible)
51
+ pub api_base: Option<String>,
52
+ /// API key
53
+ pub api_key: Option<String>,
54
+ /// Model to use for cloud fallback
55
+ pub model: Option<String>,
56
+ /// Enable cloud fallback
57
+ #[serde(default)]
58
+ pub enabled: bool,
59
+ }
60
+
61
+ #[derive(Debug, Clone, Serialize, Deserialize)]
62
+ pub struct LearningConfig {
63
+ /// Enable continuous learning
64
+ #[serde(default = "default_true")]
65
+ pub enabled: bool,
66
+ /// Min preference pairs before training
67
+ #[serde(default = "default_min_pairs")]
68
+ pub min_pairs_before_training: u32,
69
+ /// Learning sidecar URL
70
+ #[serde(default = "default_learn_url")]
71
+ pub sidecar_url: String,
72
+ /// Self-evaluation threshold (1-5, below this = negative example)
73
+ #[serde(default = "default_eval_threshold")]
74
+ pub eval_threshold: f32,
75
+ }
76
+
77
+ #[derive(Debug, Clone, Serialize, Deserialize)]
78
+ pub struct SpecialistConfig {
79
+ /// Specialist name
80
+ pub name: String,
81
+ /// Capabilities this specialist handles
82
+ pub capabilities: Vec<String>,
83
+ /// Base model (overrides global)
84
+ pub base_model: Option<String>,
85
+ /// LoRA adapter path
86
+ pub adapter: Option<String>,
87
+ /// System prompt
88
+ pub system_prompt: Option<String>,
89
+ /// Priority (higher = preferred)
90
+ #[serde(default = "default_priority")]
91
+ pub priority: u32,
92
+ }
93
+
94
+ fn default_port() -> u16 { 6900 }
95
+ fn default_data_dir() -> PathBuf {
96
+ dirs::home_dir().unwrap_or_default().join(".synapse")
97
+ }
98
+ fn default_models_dir() -> PathBuf {
99
+ default_data_dir().join("models")
100
+ }
101
+ fn default_adapters_dir() -> PathBuf {
102
+ default_data_dir().join("adapters")
103
+ }
104
+ fn default_coordinator() -> String { "qwen3-0.6b".into() }
105
+ fn default_base_model() -> String { "qwen3-3b".into() }
106
+ fn default_true() -> bool { true }
107
+ fn default_min_pairs() -> u32 { 10 }
108
+ fn default_learn_url() -> String { "http://localhost:8090".into() }
109
+ fn default_eval_threshold() -> f32 { 3.0 }
110
+ fn default_priority() -> u32 { 50 }
111
+
112
+ impl Default for LearningConfig {
113
+ fn default() -> Self {
114
+ Self {
115
+ enabled: true,
116
+ min_pairs_before_training: 10,
117
+ sidecar_url: default_learn_url(),
118
+ eval_threshold: 3.0,
119
+ }
120
+ }
121
+ }
122
+
123
+ impl SynapseConfig {
124
+ pub fn load(path: Option<&str>) -> Result<Self> {
125
+ let config_path = match path {
126
+ Some(p) => PathBuf::from(p),
127
+ None => default_data_dir().join("config.yaml"),
128
+ };
129
+
130
+ if config_path.exists() {
131
+ let content = std::fs::read_to_string(&config_path)?;
132
+ let config: SynapseConfig = serde_yaml::from_str(&content)?;
133
+ Ok(config)
134
+ } else {
135
+ let config = SynapseConfig::default();
136
+ // Ensure data directories exist
137
+ std::fs::create_dir_all(&config.data_dir)?;
138
+ std::fs::create_dir_all(&config.models_dir)?;
139
+ std::fs::create_dir_all(&config.adapters_dir)?;
140
+ // Write default config
141
+ let yaml = serde_yaml::to_string(&config)?;
142
+ std::fs::write(&config_path, yaml)?;
143
+ tracing::info!("Created default config at {}", config_path.display());
144
+ Ok(config)
145
+ }
146
+ }
147
+ }
148
+
149
+ impl Default for SynapseConfig {
150
+ fn default() -> Self {
151
+ Self {
152
+ port: default_port(),
153
+ data_dir: default_data_dir(),
154
+ models_dir: default_models_dir(),
155
+ adapters_dir: default_adapters_dir(),
156
+ coordinator_model: default_coordinator(),
157
+ base_model: default_base_model(),
158
+ max_vram_mb: 0,
159
+ cloud: CloudConfig::default(),
160
+ learning: LearningConfig::default(),
161
+ specialists: vec![
162
+ SpecialistConfig {
163
+ name: "general".into(),
164
+ capabilities: vec!["general".into(), "chat".into()],
165
+ base_model: None,
166
+ adapter: None,
167
+ system_prompt: Some("You are a helpful AI assistant.".into()),
168
+ priority: 50,
169
+ },
170
+ SpecialistConfig {
171
+ name: "python_expert".into(),
172
+ capabilities: vec!["python".into(), "debugging".into(), "testing".into()],
173
+ base_model: None,
174
+ adapter: None,
175
+ system_prompt: Some("You are an expert Python developer.".into()),
176
+ priority: 60,
177
+ },
178
+ SpecialistConfig {
179
+ name: "sql_expert".into(),
180
+ capabilities: vec!["sql".into(), "database".into(), "query".into()],
181
+ base_model: None,
182
+ adapter: None,
183
+ system_prompt: Some("You are an expert database engineer.".into()),
184
+ priority: 60,
185
+ },
186
+ ],
187
+ }
188
+ }
189
+ }
190
+
191
+ #[cfg(test)]
192
+ mod tests {
193
+ use super::*;
194
+
195
+ #[test]
196
+ fn test_default_config() {
197
+ let config = SynapseConfig::default();
198
+ assert_eq!(config.port, 6900);
199
+ assert_eq!(config.coordinator_model, "qwen3-0.6b");
200
+ assert_eq!(config.specialists.len(), 3);
201
+ }
202
+
203
+ #[test]
204
+ fn test_config_serialization() {
205
+ let config = SynapseConfig::default();
206
+ let yaml = serde_yaml::to_string(&config).unwrap();
207
+ let parsed: SynapseConfig = serde_yaml::from_str(&yaml).unwrap();
208
+ assert_eq!(parsed.port, config.port);
209
+ }
210
+
211
+ #[test]
212
+ fn test_load_missing_config() {
213
+ // Should create default when file doesn't exist
214
+ let tmp = tempfile::tempdir().unwrap();
215
+ let path = tmp.path().join("nonexistent.yaml");
216
+ // This would try to create dirs, so just test default
217
+ let config = SynapseConfig::default();
218
+ assert_eq!(config.base_model, "qwen3-3b");
219
+ }
220
+ }