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.
- package/CONTRIBUTING.md +187 -0
- package/Cargo.lock +3976 -0
- package/Cargo.toml +10 -0
- package/LICENSE +190 -0
- package/PROGRESS.md +151 -0
- package/README.md +514 -0
- package/TEST_LOG.md +220 -0
- package/config/default.yaml +36 -0
- package/crates/synapse/Cargo.toml +70 -0
- package/crates/synapse/src/cli/bench.rs +44 -0
- package/crates/synapse/src/cli/eval.rs +395 -0
- package/crates/synapse/src/cli/export.rs +45 -0
- package/crates/synapse/src/cli/hub.rs +179 -0
- package/crates/synapse/src/cli/import.rs +35 -0
- package/crates/synapse/src/cli/learn.rs +53 -0
- package/crates/synapse/src/cli/mod.rs +10 -0
- package/crates/synapse/src/cli/models.rs +36 -0
- package/crates/synapse/src/cli/pull.rs +60 -0
- package/crates/synapse/src/cli/status.rs +52 -0
- package/crates/synapse/src/cli/train.rs +99 -0
- package/crates/synapse/src/config.rs +220 -0
- package/crates/synapse/src/dashboard.rs +281 -0
- package/crates/synapse/src/format/manifest.rs +57 -0
- package/crates/synapse/src/format/mod.rs +4 -0
- package/crates/synapse/src/format/packer.rs +213 -0
- package/crates/synapse/src/inference/engine.rs +361 -0
- package/crates/synapse/src/inference/kv_cache.rs +97 -0
- package/crates/synapse/src/inference/lora.rs +166 -0
- package/crates/synapse/src/inference/mod.rs +9 -0
- package/crates/synapse/src/inference/model.rs +167 -0
- package/crates/synapse/src/inference/sampler.rs +133 -0
- package/crates/synapse/src/inference/speculative.rs +153 -0
- package/crates/synapse/src/learn/cloud_fallback.rs +186 -0
- package/crates/synapse/src/learn/engine.rs +109 -0
- package/crates/synapse/src/learn/mod.rs +5 -0
- package/crates/synapse/src/main.rs +185 -0
- package/crates/synapse/src/memory/extractor.rs +201 -0
- package/crates/synapse/src/memory/graph.rs +332 -0
- package/crates/synapse/src/memory/hallucination.rs +259 -0
- package/crates/synapse/src/memory/mod.rs +7 -0
- package/crates/synapse/src/openai.rs +232 -0
- package/crates/synapse/src/server.rs +166 -0
- package/crates/synapse/src/streaming.rs +80 -0
- package/crates/synapse/src/swarm/coordinator.rs +198 -0
- package/crates/synapse/src/swarm/mod.rs +8 -0
- package/crates/synapse/src/swarm/orchestrator.rs +225 -0
- package/crates/synapse/src/swarm/pool.rs +64 -0
- package/crates/synapse/src/swarm/spawner.rs +199 -0
- package/crates/synapse/src/swarm/synthesizer.rs +26 -0
- package/crates/synapse/src/vram/manager.rs +67 -0
- package/crates/synapse/src/vram/mod.rs +3 -0
- package/docker-compose.yml +19 -0
- package/install.sh +311 -0
- package/package.json +36 -0
- package/python/Dockerfile.learn +18 -0
- package/python/requirements.txt +11 -0
- package/python/synapse_learn/__init__.py +0 -0
- package/python/synapse_learn/datasets.py +233 -0
- package/python/synapse_learn/real_eval.py +616 -0
- package/python/synapse_learn/server.py +431 -0
- package/python/synapse_learn/train_base.py +672 -0
- 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,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
|
+
}
|