Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
97 changes: 97 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: CI/CD Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
python-quality:
runs-on: ubuntu-22.04
name: Python Quality Checks
container: python:3.11-slim

steps:
- uses: actions/checkout@v4

- name: Install system dependencies
run: |
apt-get update && apt-get install -y git

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install mypy ruff bandit safety

- name: Code formatting check (ruff)
run: ruff format --check .

- name: Lint with ruff
run: ruff check . --output-format=full

- name: Type checking with mypy
run: mypy *.py --ignore-missing-imports --no-strict-optional

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI mypy invocation only checks top-level *.py files, so it won’t type-check the lightrag/ package where most code lives. Update the mypy command to cover the package (e.g., run mypy on "lightrag" and relevant entrypoints, or on "." and rely on excludes) so type checking actually enforces the repository code.

Suggested change
run: mypy *.py --ignore-missing-imports --no-strict-optional
run: mypy lightrag *.py --ignore-missing-imports --no-strict-optional

Copilot uses AI. Check for mistakes.

- name: Security check with bandit
run: bandit -r . -f json -o bandit-report.json || true

- name: Dependency security check with safety
run: safety check --json --output safety-report.json || true
Comment on lines +38 to +41

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bandit and safety are run with "|| true", so CI will still pass even when they detect issues. If these scans are intended to enforce security standards, remove the unconditional success (or fail only on high/critical findings) so the workflow can block unsafe changes.

Suggested change
run: bandit -r . -f json -o bandit-report.json || true
- name: Dependency security check with safety
run: safety check --json --output safety-report.json || true
run: bandit -r . -f json -o bandit-report.json
- name: Dependency security check with safety
run: safety check --json --output safety-report.json

Copilot uses AI. Check for mistakes.

- name: Security scan reports summary
run: |
echo "Security reports generated:" && ls -la || echo "No security reports found"

tests:
runs-on: ubuntu-22.04
name: Run Tests
needs: [python-quality]
container: python:3.11-slim

steps:
- uses: actions/checkout@v4

- name: Install system dependencies
run: |
apt-get update && apt-get install -y git

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov

- name: Run tests with coverage
run: |
pytest . --cov=. --cov-report=xml --cov-report=html

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella

security-scan:
runs-on: ubuntu-22.04
name: Container Security Scan
needs: [tests]
container: ubuntu:22.04

steps:
- uses: actions/checkout@v4

- name: Install tools
run: |
apt-get update
apt-get install -y wget curl git

- name: Run basic security scan
run: |
echo "Scanning for secrets and sensitive files..."
find . -type f -name "*.py" -exec grep -l "password\|secret\|key\|token" {} \; || echo "No obvious secrets found"

echo "Checking file permissions..."
find . -type f -perm /o+w | head -10 || echo "No world-writable files found"
74 changes: 74 additions & 0 deletions ACT_USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Act Configuration for Local GitHub Actions Testing

This document explains how to run the CI/CD pipeline locally using act.

## Prerequisites

- Docker installed and running
- act installed (already available at `/opt/homebrew/bin/act`)

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcodes an installation path for act ("/opt/homebrew/bin/act"), which is environment-specific (e.g., macOS/Homebrew) and may confuse users on Linux/Windows. Consider describing how to verify act is installed (e.g., act --version) instead of pinning a local path.

Suggested change
- act installed (already available at `/opt/homebrew/bin/act`)
- act installed (verify with `act --version`)

Copilot uses AI. Check for mistakes.

## Quick Start

Run the full CI/CD pipeline locally:

```bash
act -j python-quality
act -j rust-quality
act -j tests
act -j docker-scan
Comment on lines +15 to +18

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These act commands reference jobs (rust-quality, docker-scan) that don’t exist in .github/workflows/ci.yml (which defines python-quality, tests, and security-scan). Update the job names here so local act usage matches the actual workflow.

Copilot uses AI. Check for mistakes.
```

Or run all jobs:

```bash
act
```

## Specific Job Commands

### Python Quality Checks
```bash
act -j python-quality
```
Runs: black, isort, flake8, mypy, bandit, safety

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says Python quality runs black/isort/flake8, but the workflow uses ruff for formatting/linting. Update the listed tools so the documentation matches CI.

Suggested change
Runs: black, isort, flake8, mypy, bandit, safety
Runs: ruff (formatting & linting), mypy, bandit, safety

Copilot uses AI. Check for mistakes.

### Rust Quality Checks
```bash
act -j rust-quality
```
Runs: rustfmt, clippy, cargo audit (if Rust code exists)

### Tests
```bash
act -j tests
```
Runs: pytest with coverage

### Security Scans
```bash
act -j docker-scan
```
Runs: Trivy vulnerability scanner

## Dry Run
To see what would be executed without running:

```bash
act --dry-run
```

## Environment Variables
The pipeline works with default settings. If you need specific environment variables:

```bash
act --secret-file my.secrets
```

## Artifacts
Local runs will create artifacts in the `act-artifacts` directory.

## Troubleshooting

1. **Docker issues**: Ensure Docker is running
2. **Permission issues**: Use `sudo act` if needed
3. **Cache issues**: Clear with `act --rm`
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added lightrag/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added lightrag/__pycache__/lightrag.cpython-312.pyc
Binary file not shown.
Binary file added lightrag/__pycache__/llm.cpython-312.pyc
Binary file not shown.
16 changes: 8 additions & 8 deletions lightrag/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ class StorageNameSpace:
namespace: str
global_config: dict

async def index_done_callback(self):
async def index_done_callback(self) -> None:
"""commit the storage operations after indexing"""
pass

async def query_done_callback(self):
async def query_done_callback(self) -> None:
"""commit the storage operations after querying"""
pass

Expand All @@ -50,7 +50,7 @@ class BaseVectorStorage(StorageNameSpace):
async def query(self, query: str, top_k: int) -> list[dict]:
raise NotImplementedError

async def upsert(self, data: dict[str, dict]):
async def upsert(self, data: dict[str, dict]) -> None:
"""Use 'content' field from value for embedding, use key as id.
If embedding_func is None, use 'embedding' field from value
"""
Expand All @@ -74,10 +74,10 @@ async def filter_keys(self, data: list[str]) -> set[str]:
"""return un-exist keys"""
raise NotImplementedError

async def upsert(self, data: dict[str, T]):
async def upsert(self, data: dict[str, T]) -> None:

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseKVStorage.upsert is annotated as returning None, but JsonKVStorage.upsert returns a dict of inserted items. This breaks the type contract and will be flagged by mypy. Either change the base return type to match the implementations or update the implementations to return None.

Suggested change
async def upsert(self, data: dict[str, T]) -> None:
async def upsert(self, data: dict[str, T]) -> dict[str, T]:

Copilot uses AI. Check for mistakes.
raise NotImplementedError

async def drop(self):
async def drop(self) -> None:
raise NotImplementedError


Expand Down Expand Up @@ -108,15 +108,15 @@ async def get_node_edges(
) -> Union[list[tuple[str, str]], None]:
raise NotImplementedError

async def upsert_node(self, node_id: str, node_data: dict[str, str]):
async def upsert_node(self, node_id: str, node_data: dict[str, str]) -> None:
raise NotImplementedError

async def upsert_edge(
self, source_node_id: str, target_node_id: str, edge_data: dict[str, str]
):
) -> None:
raise NotImplementedError

async def clustering(self, algorithm: str):
async def clustering(self, algorithm: str) -> None:
raise NotImplementedError

async def embed_nodes(self, algorithm: str) -> tuple[np.ndarray, list[str]]:
Expand Down
22 changes: 11 additions & 11 deletions lightrag/lightrag.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dataclasses import asdict, dataclass, field
from datetime import datetime
from functools import partial
from typing import Type, cast
from typing import Type, cast, Union, Callable

from .llm import (
gpt_4o_mini_complete,
Expand Down Expand Up @@ -84,7 +84,7 @@ class LightRAG:
embedding_func_max_async: int = 16

# LLM
llm_model_func: callable = gpt_4o_mini_complete # hf_model_complete#
llm_model_func: Callable = gpt_4o_mini_complete # hf_model_complete#
llm_model_name: str = "meta-llama/Llama-3.2-1B-Instruct" #'meta-llama/Llama-3.2-1B'#'google/gemma-2-2b-it'
llm_model_max_token_size: int = 32768
llm_model_max_async: int = 16
Expand All @@ -98,9 +98,9 @@ class LightRAG:

# extension
addon_params: dict = field(default_factory=dict)
convert_response_to_json_func: callable = convert_response_to_json
convert_response_to_json_func: Callable = convert_response_to_json

def __post_init__(self):
def __post_init__(self) -> None:
log_file = os.path.join(self.working_dir, "lightrag.log")
set_logger(log_file)
logger.info(f"Logger initialized for working directory: {self.working_dir}")
Expand Down Expand Up @@ -132,7 +132,7 @@ def __post_init__(self):
)

self.embedding_func = limit_async_func_call(self.embedding_func_max_async)(
self.embedding_func
self.embedding_func # type: ignore
)
Comment on lines 134 to 136

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "type: ignore" here indicates the wrapper’s typing doesn’t match the callable being passed. Instead of suppressing type checking, consider improving the typing of limit_async_func_call (e.g., using ParamSpec/TypeVar to preserve the wrapped callable’s signature and return type) so mypy can validate this call without ignores.

Copilot uses AI. Check for mistakes.

self.entities_vdb = self.vector_db_storage_cls(
Expand All @@ -157,11 +157,11 @@ def __post_init__(self):
partial(self.llm_model_func, hashing_kv=self.llm_response_cache)
)

def insert(self, string_or_strings):
def insert(self, string_or_strings: Union[str, list[str]]) -> None:
loop = always_get_an_event_loop()
return loop.run_until_complete(self.ainsert(string_or_strings))

async def ainsert(self, string_or_strings):
async def ainsert(self, string_or_strings: Union[str, list[str]]) -> None:
try:
if isinstance(string_or_strings, str):
string_or_strings = [string_or_strings]
Expand Down Expand Up @@ -223,7 +223,7 @@ async def ainsert(self, string_or_strings):
finally:
await self._insert_done()

async def _insert_done(self):
async def _insert_done(self) -> None:
tasks = []
for storage_inst in [
self.full_docs,
Expand All @@ -239,11 +239,11 @@ async def _insert_done(self):
tasks.append(cast(StorageNameSpace, storage_inst).index_done_callback())
await asyncio.gather(*tasks)

def query(self, query: str, param: QueryParam = QueryParam()):
def query(self, query: str, param: QueryParam = QueryParam()) -> str:
loop = always_get_an_event_loop()
return loop.run_until_complete(self.aquery(query, param))

async def aquery(self, query: str, param: QueryParam = QueryParam()):
async def aquery(self, query: str, param: QueryParam = QueryParam()) -> str:
Comment on lines +242 to +246

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value QueryParam() is created at function definition time. Even though QueryParam currently contains only primitive fields, it’s still a mutable object and can be mutated across calls. Prefer param: QueryParam | None = None and instantiate QueryParam() inside the function when param is None.

Copilot uses AI. Check for mistakes.
if param.mode == "local":
response = await local_query(
query,
Expand Down Expand Up @@ -287,7 +287,7 @@ async def aquery(self, query: str, param: QueryParam = QueryParam()):
await self._query_done()
return response

async def _query_done(self):
async def _query_done(self) -> None:
tasks = []
for storage_inst in [self.llm_response_cache]:
if storage_inst is None:
Expand Down
2 changes: 1 addition & 1 deletion lightrag/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ async def bedrock_complete_if_cache(


@lru_cache(maxsize=1)
def initialize_hf_model(model_name):
def initialize_hf_model(model_name: str):
hf_tokenizer = AutoTokenizer.from_pretrained(
Comment on lines 218 to 220

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize_hf_model still lacks a return type annotation even though it returns a (model, tokenizer) tuple. Add an explicit return type to maintain consistent type coverage and improve mypy checking.

Copilot uses AI. Check for mistakes.
model_name, device_map="auto", trust_remote_code=True
)
Expand Down
6 changes: 3 additions & 3 deletions lightrag/operate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import json
import re
from typing import Union
from typing import Union, Callable, Optional, List, Dict, Tuple, Set

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several imported typing names (Optional, List, Dict, Tuple, Set) are not used in this module. With ruff enabled in CI, this will fail F401 unused-import checks. Remove the unused imports (keep only what’s used, e.g. Union and Callable).

Suggested change
from typing import Union, Callable, Optional, List, Dict, Tuple, Set
from typing import Union, Callable

Copilot uses AI. Check for mistakes.
from collections import Counter, defaultdict
import warnings
from .utils import (
Expand Down Expand Up @@ -52,7 +52,7 @@ async def _handle_entity_relation_summary(
description: str,
global_config: dict,
) -> str:
use_llm_func: callable = global_config["llm_model_func"]
use_llm_func: Callable = global_config["llm_model_func"]
llm_max_tokens = global_config["llm_model_max_token_size"]
tiktoken_model_name = global_config["tiktoken_model_name"]
summary_max_tokens = global_config["entity_summary_to_max_tokens"]
Expand Down Expand Up @@ -242,7 +242,7 @@ async def extract_entities(
relationships_vdb: BaseVectorStorage,
global_config: dict,
) -> Union[BaseGraphStorage, None]:
use_llm_func: callable = global_config["llm_model_func"]
use_llm_func: Callable = global_config["llm_model_func"]
entity_extract_max_gleaning = global_config["entity_extract_max_gleaning"]

ordered_chunks = list(chunks.items())
Expand Down
6 changes: 3 additions & 3 deletions lightrag/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from dataclasses import dataclass
from functools import wraps
from hashlib import md5
from typing import Any, Union
from typing import Any, Union, Callable
import xml.etree.ElementTree as ET

import numpy as np
Expand Down Expand Up @@ -37,7 +37,7 @@ def set_logger(log_file: str):
class EmbeddingFunc:
embedding_dim: int
max_token_size: int
func: callable
func: Callable

async def __call__(self, *args, **kwargs) -> np.ndarray:
return await self.func(*args, **kwargs)
Expand Down Expand Up @@ -163,7 +163,7 @@ def is_float_regex(value):
return bool(re.match(r"^[-+]?[0-9]*\.?[0-9]+$", value))


def truncate_list_by_token_size(list_data: list, key: callable, max_token_size: int):
def truncate_list_by_token_size(list_data: list, key: Callable, max_token_size: int):
"""Truncate a list of data by token size"""
if max_token_size <= 0:
return []
Expand Down
Loading
Loading