Compare commits
5 Commits
feature/sk
...
fix/52-inc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd343e1d7a | ||
|
|
ef980d70b3 | ||
|
|
db3c63c441 | ||
|
|
00eeadb9dd | ||
|
|
42c8370709 |
@@ -31,7 +31,7 @@ LEANN achieves this through *graph-based selective recomputation* with *high-deg
|
|||||||
<img src="assets/effects.png" alt="LEANN vs Traditional Vector DB Storage Comparison" width="70%">
|
<img src="assets/effects.png" alt="LEANN vs Traditional Vector DB Storage Comparison" width="70%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> **The numbers speak for themselves:** Index 60 million text chunks in just 6GB instead of 201GB. From emails to browser history, everything fits on your laptop. [See detailed benchmarks for different applications below ↓](#storage-comparison)
|
> **The numbers speak for themselves:** Index 60 million text chunks in just 6GB instead of 201GB. From emails to browser history, everything fits on your laptop. [See detailed benchmarks for different applications below ↓](#-storage-comparison)
|
||||||
|
|
||||||
|
|
||||||
🔒 **Privacy:** Your data never leaves your laptop. No OpenAI, no cloud, no "terms of service".
|
🔒 **Privacy:** Your data never leaves your laptop. No OpenAI, no cloud, no "terms of service".
|
||||||
@@ -70,8 +70,8 @@ uv venv
|
|||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
uv pip install leann
|
uv pip install leann
|
||||||
```
|
```
|
||||||
|
<!--
|
||||||
> Low-resource? See “Low-resource setups” in the [Configuration Guide](docs/configuration-guide.md#low-resource-setups).
|
> Low-resource? See “Low-resource setups” in the [Configuration Guide](docs/configuration-guide.md#low-resource-setups). -->
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
|
|||||||
@@ -148,6 +148,36 @@ Examples:
|
|||||||
type=str,
|
type=str,
|
||||||
help="Comma-separated list of file extensions to include (e.g., '.txt,.pdf,.pptx'). If not specified, uses default supported types.",
|
help="Comma-separated list of file extensions to include (e.g., '.txt,.pdf,.pptx'). If not specified, uses default supported types.",
|
||||||
)
|
)
|
||||||
|
build_parser.add_argument(
|
||||||
|
"--include-hidden",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=False,
|
||||||
|
help="Include hidden files and directories (paths starting with '.') during indexing (default: false)",
|
||||||
|
)
|
||||||
|
build_parser.add_argument(
|
||||||
|
"--doc-chunk-size",
|
||||||
|
type=int,
|
||||||
|
default=256,
|
||||||
|
help="Document chunk size in tokens/characters (default: 256)",
|
||||||
|
)
|
||||||
|
build_parser.add_argument(
|
||||||
|
"--doc-chunk-overlap",
|
||||||
|
type=int,
|
||||||
|
default=128,
|
||||||
|
help="Document chunk overlap (default: 128)",
|
||||||
|
)
|
||||||
|
build_parser.add_argument(
|
||||||
|
"--code-chunk-size",
|
||||||
|
type=int,
|
||||||
|
default=512,
|
||||||
|
help="Code chunk size in tokens/lines (default: 512)",
|
||||||
|
)
|
||||||
|
build_parser.add_argument(
|
||||||
|
"--code-chunk-overlap",
|
||||||
|
type=int,
|
||||||
|
default=50,
|
||||||
|
help="Code chunk overlap (default: 50)",
|
||||||
|
)
|
||||||
|
|
||||||
# Search command
|
# Search command
|
||||||
search_parser = subparsers.add_parser("search", help="Search documents")
|
search_parser = subparsers.add_parser("search", help="Search documents")
|
||||||
@@ -387,7 +417,10 @@ Examples:
|
|||||||
print(f" leann ask {example_name} --interactive")
|
print(f" leann ask {example_name} --interactive")
|
||||||
|
|
||||||
def load_documents(
|
def load_documents(
|
||||||
self, docs_paths: Union[str, list], custom_file_types: Union[str, None] = None
|
self,
|
||||||
|
docs_paths: Union[str, list],
|
||||||
|
custom_file_types: Union[str, None] = None,
|
||||||
|
include_hidden: bool = False,
|
||||||
):
|
):
|
||||||
# Handle both single path (string) and multiple paths (list) for backward compatibility
|
# Handle both single path (string) and multiple paths (list) for backward compatibility
|
||||||
if isinstance(docs_paths, str):
|
if isinstance(docs_paths, str):
|
||||||
@@ -431,6 +464,10 @@ Examples:
|
|||||||
|
|
||||||
all_documents = []
|
all_documents = []
|
||||||
|
|
||||||
|
# Helper to detect hidden path components
|
||||||
|
def _path_has_hidden_segment(p: Path) -> bool:
|
||||||
|
return any(part.startswith(".") and part not in [".", ".."] for part in p.parts)
|
||||||
|
|
||||||
# First, process individual files if any
|
# First, process individual files if any
|
||||||
if files:
|
if files:
|
||||||
print(f"\n🔄 Processing {len(files)} individual file{'s' if len(files) > 1 else ''}...")
|
print(f"\n🔄 Processing {len(files)} individual file{'s' if len(files) > 1 else ''}...")
|
||||||
@@ -443,8 +480,12 @@ Examples:
|
|||||||
|
|
||||||
files_by_dir = defaultdict(list)
|
files_by_dir = defaultdict(list)
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
parent_dir = str(Path(file_path).parent)
|
file_path_obj = Path(file_path)
|
||||||
files_by_dir[parent_dir].append(file_path)
|
if not include_hidden and _path_has_hidden_segment(file_path_obj):
|
||||||
|
print(f" ⚠️ Skipping hidden file: {file_path}")
|
||||||
|
continue
|
||||||
|
parent_dir = str(file_path_obj.parent)
|
||||||
|
files_by_dir[parent_dir].append(str(file_path_obj))
|
||||||
|
|
||||||
# Load files from each parent directory
|
# Load files from each parent directory
|
||||||
for parent_dir, file_list in files_by_dir.items():
|
for parent_dir, file_list in files_by_dir.items():
|
||||||
@@ -455,6 +496,7 @@ Examples:
|
|||||||
file_docs = SimpleDirectoryReader(
|
file_docs = SimpleDirectoryReader(
|
||||||
parent_dir,
|
parent_dir,
|
||||||
input_files=file_list,
|
input_files=file_list,
|
||||||
|
# exclude_hidden only affects directory scans; input_files are explicit
|
||||||
filename_as_id=True,
|
filename_as_id=True,
|
||||||
).load_data()
|
).load_data()
|
||||||
all_documents.extend(file_docs)
|
all_documents.extend(file_docs)
|
||||||
@@ -553,6 +595,8 @@ Examples:
|
|||||||
# Check if file matches any exclude pattern
|
# Check if file matches any exclude pattern
|
||||||
try:
|
try:
|
||||||
relative_path = file_path.relative_to(docs_path)
|
relative_path = file_path.relative_to(docs_path)
|
||||||
|
if not include_hidden and _path_has_hidden_segment(relative_path):
|
||||||
|
continue
|
||||||
if self._should_exclude_file(relative_path, gitignore_matches):
|
if self._should_exclude_file(relative_path, gitignore_matches):
|
||||||
continue
|
continue
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -580,6 +624,7 @@ Examples:
|
|||||||
try:
|
try:
|
||||||
default_docs = SimpleDirectoryReader(
|
default_docs = SimpleDirectoryReader(
|
||||||
str(file_path.parent),
|
str(file_path.parent),
|
||||||
|
exclude_hidden=not include_hidden,
|
||||||
filename_as_id=True,
|
filename_as_id=True,
|
||||||
required_exts=[file_path.suffix],
|
required_exts=[file_path.suffix],
|
||||||
).load_data()
|
).load_data()
|
||||||
@@ -608,6 +653,7 @@ Examples:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
required_exts=code_extensions,
|
required_exts=code_extensions,
|
||||||
file_extractor={}, # Use default extractors
|
file_extractor={}, # Use default extractors
|
||||||
|
exclude_hidden=not include_hidden,
|
||||||
filename_as_id=True,
|
filename_as_id=True,
|
||||||
).load_data(show_progress=True)
|
).load_data(show_progress=True)
|
||||||
|
|
||||||
@@ -726,7 +772,40 @@ Examples:
|
|||||||
print(f"Index '{index_name}' already exists. Use --force to rebuild.")
|
print(f"Index '{index_name}' already exists. Use --force to rebuild.")
|
||||||
return
|
return
|
||||||
|
|
||||||
all_texts = self.load_documents(docs_paths, args.file_types)
|
# Configure chunking based on CLI args before loading documents
|
||||||
|
# Guard against invalid configurations
|
||||||
|
doc_chunk_size = max(1, int(args.doc_chunk_size))
|
||||||
|
doc_chunk_overlap = max(0, int(args.doc_chunk_overlap))
|
||||||
|
if doc_chunk_overlap >= doc_chunk_size:
|
||||||
|
print(
|
||||||
|
f"⚠️ Adjusting doc chunk overlap from {doc_chunk_overlap} to {doc_chunk_size - 1} (must be < chunk size)"
|
||||||
|
)
|
||||||
|
doc_chunk_overlap = doc_chunk_size - 1
|
||||||
|
|
||||||
|
code_chunk_size = max(1, int(args.code_chunk_size))
|
||||||
|
code_chunk_overlap = max(0, int(args.code_chunk_overlap))
|
||||||
|
if code_chunk_overlap >= code_chunk_size:
|
||||||
|
print(
|
||||||
|
f"⚠️ Adjusting code chunk overlap from {code_chunk_overlap} to {code_chunk_size - 1} (must be < chunk size)"
|
||||||
|
)
|
||||||
|
code_chunk_overlap = code_chunk_size - 1
|
||||||
|
|
||||||
|
self.node_parser = SentenceSplitter(
|
||||||
|
chunk_size=doc_chunk_size,
|
||||||
|
chunk_overlap=doc_chunk_overlap,
|
||||||
|
separator=" ",
|
||||||
|
paragraph_separator="\n\n",
|
||||||
|
)
|
||||||
|
self.code_parser = SentenceSplitter(
|
||||||
|
chunk_size=code_chunk_size,
|
||||||
|
chunk_overlap=code_chunk_overlap,
|
||||||
|
separator="\n",
|
||||||
|
paragraph_separator="\n\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
all_texts = self.load_documents(
|
||||||
|
docs_paths, args.file_types, include_hidden=args.include_hidden
|
||||||
|
)
|
||||||
if not all_texts:
|
if not all_texts:
|
||||||
print("No documents found")
|
print("No documents found")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -263,8 +263,16 @@ def compute_embeddings_openai(texts: list[str], model_name: str) -> np.ndarray:
|
|||||||
print(f"len of texts: {len(texts)}")
|
print(f"len of texts: {len(texts)}")
|
||||||
|
|
||||||
# OpenAI has limits on batch size and input length
|
# OpenAI has limits on batch size and input length
|
||||||
max_batch_size = 1000 # Conservative batch size
|
max_batch_size = 800 # Conservative batch size because the token limit is 300K
|
||||||
all_embeddings = []
|
all_embeddings = []
|
||||||
|
# get the avg len of texts
|
||||||
|
avg_len = sum(len(text) for text in texts) / len(texts)
|
||||||
|
print(f"avg len of texts: {avg_len}")
|
||||||
|
# if avg len is less than 1000, use the max batch size
|
||||||
|
if avg_len > 300:
|
||||||
|
max_batch_size = 500
|
||||||
|
|
||||||
|
# if avg len is less than 1000, use the max batch size
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|||||||
@@ -64,19 +64,6 @@ def handle_request(request):
|
|||||||
"required": ["index_name", "query"],
|
"required": ["index_name", "query"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "leann_status",
|
|
||||||
"description": "📊 Check the health and stats of your code indexes - like a medical checkup for your codebase knowledge!",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"index_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Optional: Name of specific index to check. If not provided, shows status of all indexes.",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "leann_list",
|
"name": "leann_list",
|
||||||
"description": "📋 Show all your indexed codebases - your personal code library! Use this to see what's available for search.",
|
"description": "📋 Show all your indexed codebases - your personal code library! Use this to see what's available for search.",
|
||||||
@@ -118,15 +105,6 @@ def handle_request(request):
|
|||||||
]
|
]
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
elif tool_name == "leann_status":
|
|
||||||
if args.get("index_name"):
|
|
||||||
# Check specific index status - for now, we'll use leann list and filter
|
|
||||||
result = subprocess.run(["leann", "list"], capture_output=True, text=True)
|
|
||||||
# We could enhance this to show more detailed status per index
|
|
||||||
else:
|
|
||||||
# Show all indexes status
|
|
||||||
result = subprocess.run(["leann", "list"], capture_output=True, text=True)
|
|
||||||
|
|
||||||
elif tool_name == "leann_list":
|
elif tool_name == "leann_list":
|
||||||
result = subprocess.run(["leann", "list"], capture_output=True, text=True)
|
result = subprocess.run(["leann", "list"], capture_output=True, text=True)
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,20 @@ This installs the `leann` CLI into an isolated tool environment and includes bot
|
|||||||
|
|
||||||
## 🚀 Quick Setup
|
## 🚀 Quick Setup
|
||||||
|
|
||||||
Add the LEANN MCP server to Claude Code:
|
Add the LEANN MCP server to Claude Code. Choose the scope based on how widely you want it available. Below is the command to install it globally; if you prefer a local install, skip this step:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude mcp add leann-server -- leann_mcp
|
# Global (recommended): available in all projects for your user
|
||||||
|
claude mcp add --scope user leann-server -- leann_mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
- `leann-server`: the display name of the MCP server in Claude Code (you can change it).
|
||||||
|
- `leann_mcp`: the Python entry point installed with LEANN that starts the MCP server.
|
||||||
|
|
||||||
|
Verify it is registered globally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp list | cat
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Available Tools
|
## 🛠️ Available Tools
|
||||||
@@ -25,27 +35,36 @@ Once connected, you'll have access to these powerful semantic search tools in Cl
|
|||||||
|
|
||||||
- **`leann_list`** - List all available indexes across your projects
|
- **`leann_list`** - List all available indexes across your projects
|
||||||
- **`leann_search`** - Perform semantic searches across code and documents
|
- **`leann_search`** - Perform semantic searches across code and documents
|
||||||
- **`leann_ask`** - Ask natural language questions and get AI-powered answers from your codebase
|
|
||||||
|
|
||||||
## 🎯 Quick Start Example
|
## 🎯 Quick Start Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Add locally if you did not add it globally (current folder only; default if --scope is omitted)
|
||||||
|
claude mcp add leann-server -- leann_mcp
|
||||||
|
|
||||||
# Build an index for your project (change to your actual path)
|
# Build an index for your project (change to your actual path)
|
||||||
leann build my-project --docs ./
|
# See the advanced examples below for more ways to configure indexing
|
||||||
|
# Set the index name (replace 'my-project' with your own)
|
||||||
|
leann build my-project --docs $(git ls-files)
|
||||||
|
|
||||||
# Start Claude Code
|
# Start Claude Code
|
||||||
claude
|
claude
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Advanced Usage Examples
|
## 🚀 Advanced Usage Examples to build the index
|
||||||
|
|
||||||
### Index Entire Git Repository
|
### Index Entire Git Repository
|
||||||
```bash
|
```bash
|
||||||
# Index all tracked files in your git repository, note right now we will skip submodules, but we can add it back easily if you want
|
# Index all tracked files in your Git repository.
|
||||||
|
# Note: submodules are currently skipped; we can add them back if needed.
|
||||||
leann build my-repo --docs $(git ls-files) --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw
|
leann build my-repo --docs $(git ls-files) --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw
|
||||||
|
|
||||||
# Index only specific file types from git
|
# Index only tracked Python files from Git.
|
||||||
leann build my-python-code --docs $(git ls-files "*.py") --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw
|
leann build my-python-code --docs $(git ls-files "*.py") --embedding-mode sentence-transformers --embedding-model all-MiniLM-L6-v2 --backend hnsw
|
||||||
|
|
||||||
|
# If you encounter empty requests caused by empty files (e.g., __init__.py), exclude zero-byte files. Thanks @ww2283 for pointing [that](https://github.com/yichuan-w/LEANN/issues/48) out
|
||||||
|
leann build leann-prospec-lig --docs $(find ./src -name "*.py" -not -empty) --embedding-mode openai --embedding-model text-embedding-3-small
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multiple Directories and Files
|
### Multiple Directories and Files
|
||||||
@@ -82,6 +101,7 @@ Help me understand this codebase. List available indexes and search for authenti
|
|||||||
<img src="../../assets/claude_code_leann.png" alt="LEANN in Claude Code" width="80%">
|
<img src="../../assets/claude_code_leann.png" alt="LEANN in Claude Code" width="80%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
If you see a prompt asking whether to proceed with LEANN, you can now use it in your chat!
|
||||||
|
|
||||||
## 🧠 How It Works
|
## 🧠 How It Works
|
||||||
|
|
||||||
@@ -117,3 +137,11 @@ To remove LEANN
|
|||||||
```
|
```
|
||||||
uv pip uninstall leann leann-backend-hnsw leann-core
|
uv pip uninstall leann leann-backend-hnsw leann-core
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To globally remove LEANN (for version update)
|
||||||
|
```
|
||||||
|
uv tool list | cat
|
||||||
|
uv tool uninstall leann-core
|
||||||
|
command -v leann || echo "leann gone"
|
||||||
|
command -v leann_mcp || echo "leann_mcp gone"
|
||||||
|
```
|
||||||
|
|||||||
1
packages/wechat-exporter/__init__.py
Normal file
1
packages/wechat-exporter/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__all__ = []
|
||||||
@@ -136,5 +136,9 @@ def export_sqlite(
|
|||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main():
|
||||||
app()
|
app()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ requires-python = ">=3.9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"leann-core",
|
"leann-core",
|
||||||
"leann-backend-hnsw",
|
"leann-backend-hnsw",
|
||||||
|
"typer>=0.12.3",
|
||||||
"numpy>=1.26.0",
|
"numpy>=1.26.0",
|
||||||
"torch",
|
"torch",
|
||||||
"tqdm",
|
"tqdm",
|
||||||
@@ -84,6 +85,11 @@ documents = [
|
|||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
py-modules = []
|
py-modules = []
|
||||||
|
packages = ["wechat_exporter"]
|
||||||
|
package-dir = { "wechat_exporter" = "packages/wechat-exporter" }
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
wechat-exporter = "wechat_exporter.main:main"
|
||||||
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
|
|||||||
Reference in New Issue
Block a user