Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ archived/
node_modules/
obj/
bin/
__pycache__/
_dev/
.dev/
.vs/
Expand Down Expand Up @@ -68,4 +69,4 @@ publish/
*.crt
*.key
*.pem
certs/
certs/
2 changes: 1 addition & 1 deletion docs
Submodule docs updated from 23845b to a0321c
60 changes: 60 additions & 0 deletions e2e-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$ROOT"

echo "======================================="
echo " Running E2E Tests"
echo "======================================="
echo ""

# Choose build configuration (default Release to align with build.sh)
CONFIGURATION="${CONFIGURATION:-Release}"
KM_BIN="$ROOT/src/Main/bin/$CONFIGURATION/net10.0/KernelMemory.Main.dll"

# Ensure km binary is built at the selected configuration
if [ ! -f "$KM_BIN" ]; then
echo "km binary not found at $KM_BIN. Building ($CONFIGURATION)..."
dotnet build src/Main/Main.csproj -c "$CONFIGURATION"
fi

if [ ! -f "$KM_BIN" ]; then
echo "❌ km binary still not found at $KM_BIN after build. Set KM_BIN to a valid path."
exit 1
fi

export KM_BIN

FAILED=0
PASSED=0

# Run each test file
for test_file in tests/e2e/test_*.py; do
if [ -f "$test_file" ]; then
echo ""
echo "Running: $(basename "$test_file")"
echo "---------------------------------------"

if python3 "$test_file"; then
PASSED=$((PASSED + 1))
else
FAILED=$((FAILED + 1))
fi
fi
done

echo ""
echo "======================================="
echo " E2E Test Results"
echo "======================================="
echo "Passed: $PASSED"
echo "Failed: $FAILED"
echo "======================================="

if [ $FAILED -gt 0 ]; then
exit 1
fi

exit 0
15 changes: 14 additions & 1 deletion format.sh
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
dotnet format
#!/usr/bin/env bash

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$ROOT"
TMPDIR="$ROOT/.tmp"
mkdir -p "$TMPDIR"
export TMPDIR

dotnet format src/Core/Core.csproj
dotnet format src/Main/Main.csproj
dotnet format tests/Core.Tests/Core.Tests.csproj
dotnet format tests/Main.Tests/Main.Tests.csproj
Comment thread
dluc marked this conversation as resolved.
Outdated
9 changes: 6 additions & 3 deletions src/Core/Config/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public static AppConfig CreateDefault()

/// <summary>
/// Creates a default configuration with a single "personal" node
/// using local SQLite storage in the specified base directory
/// using local SQLite storage in the specified base directory.
/// Includes embeddings cache for efficient vector search operations.
/// </summary>
/// <param name="baseDir">Base directory for data storage</param>
public static AppConfig CreateDefault(string baseDir)
Expand All @@ -95,8 +96,10 @@ public static AppConfig CreateDefault(string baseDir)
Nodes = new Dictionary<string, NodeConfig>
{
["personal"] = NodeConfig.CreateDefaultPersonalNode(personalNodeDir)
}
// EmbeddingsCache and LLMCache intentionally omitted - add when features are implemented
},
EmbeddingsCache = CacheConfig.CreateDefaultSqliteCache(
Path.Combine(baseDir, "embeddings-cache.db"))
// LLMCache intentionally omitted - add when LLM features are implemented
};
}
}
23 changes: 16 additions & 7 deletions src/Core/Config/ConfigParser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using KernelMemory.Core.Config.Cache;
using KernelMemory.Core.Config.ContentIndex;
Expand Down Expand Up @@ -28,7 +29,8 @@ public static class ConfigParser
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
Converters = { new JsonStringEnumConverter() }
};

/// <summary>
Expand All @@ -46,13 +48,14 @@ public static class ConfigParser

/// <summary>
/// Loads configuration from a file, or creates default config if file doesn't exist.
/// The config file is always ensured to exist on disk after loading.
/// Optionally ensures the config file exists on disk after loading (for write operations).
/// Performs tilde expansion on paths (~/ → home directory)
/// </summary>
/// <param name="filePath">Path to configuration file</param>
/// <param name="ensureFileExists">If true, writes config to disk if missing (default: true for backward compatibility)</param>
/// <returns>Validated AppConfig instance</returns>
/// <exception cref="ConfigException">Thrown when file exists but parsing or validation fails</exception>
public static AppConfig LoadFromFile(string filePath)
public static AppConfig LoadFromFile(string filePath, bool ensureFileExists = true)
{
AppConfig config;

Expand All @@ -65,8 +68,11 @@ public static AppConfig LoadFromFile(string filePath)
// Create default config relative to config file location
config = AppConfig.CreateDefault(baseDir);

// Write the config file
WriteConfigFile(filePath, config);
// Write the config file only if requested
if (ensureFileExists)
{
WriteConfigFile(filePath, config);
}

return config;
}
Expand All @@ -82,8 +88,11 @@ public static AppConfig LoadFromFile(string filePath)
// Expand tilde paths
ExpandTildePaths(config);

// Always ensure the config file exists (recreate if deleted between load and save)
WriteConfigFileIfMissing(filePath, config);
// Optionally ensure the config file exists (recreate if deleted between load and save)
if (ensureFileExists)
{
WriteConfigFileIfMissing(filePath, config);
}

return config;
}
Expand Down
5 changes: 2 additions & 3 deletions src/Core/Config/Embeddings/HuggingFaceEmbeddingsConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Text.Json.Serialization;
using KernelMemory.Core.Config.Enums;
using KernelMemory.Core.Config.Validation;
using KernelMemory.Core.Embeddings;

namespace KernelMemory.Core.Config.Embeddings;

Expand All @@ -20,7 +19,7 @@ public sealed class HuggingFaceEmbeddingsConfig : EmbeddingsConfig
/// HuggingFace model name (e.g., "sentence-transformers/all-MiniLM-L6-v2", "BAAI/bge-base-en-v1.5").
/// </summary>
[JsonPropertyName("model")]
public string Model { get; set; } = EmbeddingConstants.DefaultHuggingFaceModel;
public string Model { get; set; } = Constants.EmbeddingDefaults.DefaultHuggingFaceModel;

/// <summary>
/// HuggingFace API key (token).
Expand All @@ -35,7 +34,7 @@ public sealed class HuggingFaceEmbeddingsConfig : EmbeddingsConfig
/// Can be changed for custom inference endpoints.
/// </summary>
[JsonPropertyName("baseUrl")]
public string BaseUrl { get; set; } = EmbeddingConstants.DefaultHuggingFaceBaseUrl;
public string BaseUrl { get; set; } = Constants.EmbeddingDefaults.DefaultHuggingFaceBaseUrl;

/// <inheritdoc />
public override void Validate(string path)
Expand Down
20 changes: 18 additions & 2 deletions src/Core/Config/NodeConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using KernelMemory.Core.Config.ContentIndex;
using KernelMemory.Core.Config.Embeddings;
using KernelMemory.Core.Config.Enums;
using KernelMemory.Core.Config.SearchIndex;
using KernelMemory.Core.Config.Storage;
Expand Down Expand Up @@ -106,7 +107,8 @@ public void Validate(string path)
}

/// <summary>
/// Creates a default "personal" node configuration
/// Creates a default "personal" node configuration with FTS and vector search.
/// Uses Ollama with qwen3-embedding model (1024 dimensions) for local, offline-capable vector search.
/// </summary>
/// <param name="nodeDir"></param>
internal static NodeConfig CreateDefaultPersonalNode(string nodeDir)
Expand All @@ -128,7 +130,21 @@ internal static NodeConfig CreateDefaultPersonalNode(string nodeDir)
Id = "sqlite-fts",
Type = SearchIndexTypes.SqliteFTS,
Path = Path.Combine(nodeDir, "fts.db"),
EnableStemming = true
EnableStemming = true,
Required = true
},
new VectorSearchIndexConfig
{
Id = "sqlite-vector",
Type = SearchIndexTypes.SqliteVector,
Path = Path.Combine(nodeDir, "vector.db"),
Dimensions = 1024,
UseSqliteVec = false,
Embeddings = new OllamaEmbeddingsConfig
{
Model = Constants.EmbeddingDefaults.DefaultOllamaModel,
BaseUrl = Constants.EmbeddingDefaults.DefaultOllamaBaseUrl
}
}
}
};
Expand Down
35 changes: 17 additions & 18 deletions src/Core/Config/SearchConfig.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using KernelMemory.Core.Config.Validation;
using KernelMemory.Core.Search;

namespace KernelMemory.Core.Config;

Expand All @@ -17,22 +16,22 @@ public sealed class SearchConfig : IValidatable
/// Default: 0.3 (moderate threshold).
/// </summary>
[JsonPropertyName("defaultMinRelevance")]
public float DefaultMinRelevance { get; set; } = SearchConstants.DefaultMinRelevance;
public float DefaultMinRelevance { get; set; } = Constants.SearchDefaults.DefaultMinRelevance;

/// <summary>
/// Default maximum number of results to return per search.
/// Default: 20 results.
/// </summary>
[JsonPropertyName("defaultLimit")]
public int DefaultLimit { get; set; } = SearchConstants.DefaultLimit;
public int DefaultLimit { get; set; } = Constants.SearchDefaults.DefaultLimit;

/// <summary>
/// Search timeout in seconds per node.
/// If a node takes longer than this, it times out and is excluded from results.
/// Default: 30 seconds.
/// </summary>
[JsonPropertyName("searchTimeoutSeconds")]
public int SearchTimeoutSeconds { get; set; } = SearchConstants.DefaultSearchTimeoutSeconds;
public int SearchTimeoutSeconds { get; set; } = Constants.SearchDefaults.DefaultSearchTimeoutSeconds;

/// <summary>
/// Default maximum results to retrieve from each node (memory safety).
Expand All @@ -41,7 +40,7 @@ public sealed class SearchConfig : IValidatable
/// Default: 1000 results per node.
/// </summary>
[JsonPropertyName("maxResultsPerNode")]
public int MaxResultsPerNode { get; set; } = SearchConstants.DefaultMaxResultsPerNode;
public int MaxResultsPerNode { get; set; } = Constants.SearchDefaults.DefaultMaxResultsPerNode;

/// <summary>
/// Default nodes to search when no explicit --nodes flag is provided.
Expand All @@ -50,7 +49,7 @@ public sealed class SearchConfig : IValidatable
/// </summary>
[JsonPropertyName("defaultNodes")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays")]
public string[] DefaultNodes { get; set; } = [SearchConstants.AllNodesWildcard];
public string[] DefaultNodes { get; set; } = [Constants.SearchDefaults.AllNodesWildcard];

/// <summary>
/// Nodes to exclude from search by default.
Expand All @@ -67,66 +66,66 @@ public sealed class SearchConfig : IValidatable
/// Default: 10 levels.
/// </summary>
[JsonPropertyName("maxQueryDepth")]
public int MaxQueryDepth { get; set; } = SearchConstants.MaxQueryDepth;
public int MaxQueryDepth { get; set; } = Constants.SearchDefaults.MaxQueryDepth;

/// <summary>
/// Maximum number of boolean operators (AND/OR/NOT) in a single query.
/// Prevents query complexity attacks.
/// Default: 50 operators.
/// </summary>
[JsonPropertyName("maxBooleanOperators")]
public int MaxBooleanOperators { get; set; } = SearchConstants.MaxBooleanOperators;
public int MaxBooleanOperators { get; set; } = Constants.SearchDefaults.MaxBooleanOperators;

/// <summary>
/// Maximum length of a field value in query (characters).
/// Prevents oversized query values.
/// Default: 1000 characters.
/// </summary>
[JsonPropertyName("maxFieldValueLength")]
public int MaxFieldValueLength { get; set; } = SearchConstants.MaxFieldValueLength;
public int MaxFieldValueLength { get; set; } = Constants.SearchDefaults.MaxFieldValueLength;

/// <summary>
/// Maximum time allowed for query parsing (milliseconds).
/// Prevents regex catastrophic backtracking.
/// Default: 1000ms (1 second).
/// </summary>
[JsonPropertyName("queryParseTimeoutMs")]
public int QueryParseTimeoutMs { get; set; } = SearchConstants.QueryParseTimeoutMs;
public int QueryParseTimeoutMs { get; set; } = Constants.SearchDefaults.QueryParseTimeoutMs;

/// <summary>
/// Default snippet length in characters when --snippet flag is used.
/// Default: 200 characters.
/// </summary>
[JsonPropertyName("snippetLength")]
public int SnippetLength { get; set; } = SearchConstants.DefaultSnippetLength;
public int SnippetLength { get; set; } = Constants.SearchDefaults.DefaultSnippetLength;

/// <summary>
/// Default maximum number of snippets per result when --snippet flag is used.
/// Default: 1 snippet.
/// </summary>
[JsonPropertyName("maxSnippetsPerResult")]
public int MaxSnippetsPerResult { get; set; } = SearchConstants.DefaultMaxSnippetsPerResult;
public int MaxSnippetsPerResult { get; set; } = Constants.SearchDefaults.DefaultMaxSnippetsPerResult;

/// <summary>
/// Separator string between multiple snippets.
/// Default: "..." (ellipsis).
/// </summary>
[JsonPropertyName("snippetSeparator")]
public string SnippetSeparator { get; set; } = SearchConstants.DefaultSnippetSeparator;
public string SnippetSeparator { get; set; } = Constants.SearchDefaults.DefaultSnippetSeparator;

/// <summary>
/// Prefix marker for highlighting matched terms.
/// Default: "&lt;mark&gt;" (HTML-style).
/// </summary>
[JsonPropertyName("highlightPrefix")]
public string HighlightPrefix { get; set; } = SearchConstants.DefaultHighlightPrefix;
public string HighlightPrefix { get; set; } = Constants.SearchDefaults.DefaultHighlightPrefix;

/// <summary>
/// Suffix marker for highlighting matched terms.
/// Default: "&lt;/mark&gt;" (HTML-style).
/// </summary>
[JsonPropertyName("highlightSuffix")]
public string HighlightSuffix { get; set; } = SearchConstants.DefaultHighlightSuffix;
public string HighlightSuffix { get; set; } = Constants.SearchDefaults.DefaultHighlightSuffix;

/// <summary>
/// Validates the search configuration.
Expand All @@ -135,10 +134,10 @@ public sealed class SearchConfig : IValidatable
public void Validate(string path)
{
// Validate min relevance score
if (this.DefaultMinRelevance < SearchConstants.MinRelevanceScore || this.DefaultMinRelevance > SearchConstants.MaxRelevanceScore)
if (this.DefaultMinRelevance < Constants.SearchDefaults.MinRelevanceScore || this.DefaultMinRelevance > Constants.SearchDefaults.MaxRelevanceScore)
{
throw new ConfigException($"{path}.DefaultMinRelevance",
$"Must be between {SearchConstants.MinRelevanceScore} and {SearchConstants.MaxRelevanceScore}");
$"Must be between {Constants.SearchDefaults.MinRelevanceScore} and {Constants.SearchDefaults.MaxRelevanceScore}");
}

// Validate default limit
Expand Down Expand Up @@ -167,7 +166,7 @@ public void Validate(string path)
}

// Validate no contradictory node configuration
if (this.DefaultNodes.Length == 1 && this.DefaultNodes[0] == SearchConstants.AllNodesWildcard)
if (this.DefaultNodes.Length == 1 && this.DefaultNodes[0] == Constants.SearchDefaults.AllNodesWildcard)
{
// Using wildcard - excludeNodes is OK
}
Expand Down
Loading
Loading