1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Core configuration for the Kowalski system
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
/// Ollama configuration
pub ollama: OllamaConfig,
/// Chat configuration
pub chat: ChatConfig,
/// Memory configuration
pub memory: MemoryConfig,
/// Maximum number of memories to retrieve from working memory
pub working_memory_retrieval_limit: usize,
/// Maximum number of memories to retrieve from episodic memory
pub episodic_memory_retrieval_limit: usize,
/// Maximum number of memories to retrieve from semantic memory
pub semantic_memory_retrieval_limit: usize,
/// LLM configuration (new)
#[serde(default)]
pub llm: LLMConfig,
/// MCP configuration
#[serde(default)]
pub mcp: McpConfig,
/// Additional configurations from other agents
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
/// Configuration for generic LLM settings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMConfig {
/// The provider to use: `ollama` (local) or `openai` (Chat Completions API — OpenAI or compatible).
pub provider: String,
/// API key for `openai` provider (OpenAI, Groq, etc.). Many servers omit this; use `""` in TOML if needed.
pub openai_api_key: Option<String>,
/// Base URL for OpenAI-compatible Chat Completions (e.g. `https://api.openai.com/v1`, or
/// `http://127.0.0.1:1234/v1` for LM Studio). If unset, the official OpenAI API base is used.
#[serde(default)]
pub openai_api_base: Option<String>,
}
impl Default for LLMConfig {
fn default() -> Self {
Self {
provider: "ollama".to_string(),
openai_api_key: std::env::var("OPENAI_API_KEY").ok(),
openai_api_base: None,
}
}
}
/// Configuration for Ollama integration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OllamaConfig {
/// The host where Ollama is running
pub host: String,
/// The port where Ollama is running
pub port: u16,
/// The model to use
pub model: String,
/// Additional Ollama-specific settings
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
impl Default for OllamaConfig {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 11434,
model: "llama3.2".to_string(), //llama3.2 //deepseek-r1:1.5b
additional: HashMap::new(),
}
}
}
/// Configuration for chat functionality
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ChatConfig {
/// Maximum number of messages to keep in history
pub max_history: usize,
/// Whether to enable streaming responses (`stream` is accepted as a TOML field alias)
#[serde(alias = "stream")]
pub enable_streaming: bool,
/// Temperature for response generation (0.0 to 1.0)
pub temperature: f32,
/// Maximum number of tokens in generated responses
pub max_tokens: u32,
/// Additional chat-specific settings
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
impl Default for ChatConfig {
fn default() -> Self {
Self {
max_history: 100,
enable_streaming: true,
temperature: 0.7,
max_tokens: 2048,
additional: HashMap::new(),
}
}
}
fn default_embedding_vector_dimensions() -> usize {
768
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct MemoryConfig {
/// **Default Tier-2 episodic store:** embedded **SQLite** file under this path (`episodic.sqlite` in the directory, or a path ending in `.sqlite`/`.db`). Used when [`Self::database_url`] is unset or does not request PostgreSQL.
pub episodic_path: String,
/// Optional: set to **`postgres://…`** / **`postgresql://…`** to use PostgreSQL for Tier 2 (`episodic_kv`) and Tier 3 semantic SQL (**requires** `kowalski-core` **`--features postgres`**). If omitted, Tier 2 stays on **SQLite** ([`Self::episodic_path`]) — the default.
#[serde(default)]
pub database_url: Option<String>,
/// Embedding width for **PostgreSQL** `semantic_memory.embedding` (`vector(N)`). Must match your embedder (e.g. **768** for Ollama `nomic-embed-text`) and the dimension in `migrations/postgres/003_semantic_memory.sql` (this crate).
#[serde(default = "default_embedding_vector_dimensions")]
pub embedding_vector_dimensions: usize,
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
episodic_path: "../target/episodic_db".to_string(), //just for testing!
database_url: None,
embedding_vector_dimensions: default_embedding_vector_dimensions(),
additional: HashMap::new(),
}
}
}
/// Returns true when [`MemoryConfig::database_url`] points at PostgreSQL (episodic + semantic SQL backends).
pub fn memory_uses_postgres(memory: &MemoryConfig) -> bool {
memory
.database_url
.as_ref()
.is_some_and(|u| u.starts_with("postgres://") || u.starts_with("postgresql://"))
}
#[cfg(test)]
mod postgres_flag_tests {
use super::{MemoryConfig, memory_uses_postgres};
#[test]
fn memory_uses_postgres_detects_url() {
let mut m = MemoryConfig::default();
assert!(!memory_uses_postgres(&m));
m.database_url = Some("postgres://localhost/db".to_string());
assert!(memory_uses_postgres(&m));
m.database_url = Some("postgresql://localhost/db".to_string());
assert!(memory_uses_postgres(&m));
}
}
/// Build-time `postgres` feature was not enabled while config requests a PostgreSQL URL.
pub fn postgres_feature_required_error() -> crate::error::KowalskiError {
crate::error::KowalskiError::Configuration(
"PostgreSQL support requires building with `--features postgres` (e.g. `cargo build -p kowalski-core --features postgres` or `cargo build -p kowalski-cli --features postgres`).".to_string(),
)
}
/// Trait for extending configuration with additional settings
pub trait ConfigExt {
/// Get a reference to the core configuration
fn core(&self) -> &Config;
/// Get a mutable reference to the core configuration
fn core_mut(&mut self) -> &mut Config;
/// Get additional configuration value by key
fn get_additional<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
self.core()
.additional
.get(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
/// Set additional configuration value
fn set_additional<T: serde::Serialize>(&mut self, key: &str, value: T) {
if let Ok(json) = serde_json::to_value(value) {
self.core_mut().additional.insert(key.to_string(), json);
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
ollama: OllamaConfig::default(),
llm: LLMConfig::default(),
mcp: McpConfig::default(),
chat: ChatConfig::default(),
memory: MemoryConfig::default(),
working_memory_retrieval_limit: 3,
episodic_memory_retrieval_limit: 3,
semantic_memory_retrieval_limit: 3,
additional: HashMap::new(),
}
}
}
/// Configuration for MCP servers
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpConfig {
#[serde(default)]
pub servers: Vec<McpServerConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
pub name: String,
/// Base URL for HTTP/SSE; ignored for `stdio` (use `command`).
#[serde(default)]
pub url: String,
/// Preferred transport, defaults to SSE as per spec
#[serde(default)]
pub transport: McpTransport,
/// Optional static headers (e.g., auth tokens)
#[serde(default)]
pub headers: HashMap<String, String>,
/// argv for [`McpTransport::Stdio`] (program + args).
#[serde(default)]
pub command: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum McpTransport {
#[default]
Sse,
Http,
/// Subprocess MCP (newline-delimited JSON-RPC on stdin/stdout).
Stdio,
}