|
| 1 | +# KNWL AI Agent Instructions |
| 2 | + |
| 3 | +**v2 branch is under active development** - major architectural changes from v1. |
| 4 | + |
| 5 | +## Architecture Overview |
| 6 | + |
| 7 | +KNWL is a Graph RAG Python package with pluggable components orchestrated through three core systems: |
| 8 | + |
| 9 | +### 1. Configuration System (`knwl/config.py`) |
| 10 | + |
| 11 | +Hierarchical dictionary with **service variants** enabling runtime component swapping: |
| 12 | + |
| 13 | +```python |
| 14 | +"llm": { |
| 15 | + "default": "ollama", # specifies default variant |
| 16 | + "ollama": {"class": "knwl.llm.ollama.OllamaClient", "model": "o14", ...}, |
| 17 | + "openai": {"class": "knwl.llm.openai.OpenAIClient", "model": "gpt-4o-mini", ...} |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +**Key features:** |
| 22 | +- **Cross-references**: `"@/llm/ollama"` resolves to config at `llm.ollama` |
| 23 | +- **Path placeholders**: `$root` (project root), `$/tests` (tests/data), `$/data` expand dynamically |
| 24 | +- **Deep merge**: `override` parameter merges recursively, doesn't replace entire sections |
| 25 | +- Access via `get_config("llm", "model", override={...})` |
| 26 | + |
| 27 | +### 2. Dependency Injection (`knwl/di.py`) |
| 28 | + |
| 29 | +Decorator-based DI eliminates manual service instantiation: |
| 30 | + |
| 31 | +```python |
| 32 | +@service("llm", variant="ollama", param_name="ai") |
| 33 | +@singleton_service("graph", variant="nx") # reuses same instance |
| 34 | +@inject_config("api.host", "api.port") # pulls config values |
| 35 | +@defaults("json") # injects service's default config |
| 36 | +async def process(text: str, ai=None, graph=None, host=None, port=None): |
| 37 | + # All params automatically injected from config |
| 38 | + pass |
| 39 | +``` |
| 40 | + |
| 41 | +All decorators accept `override` dict for context-specific config without modifying defaults. |
| 42 | + |
| 43 | +### 3. Services Registry (`knwl/services.py`) |
| 44 | + |
| 45 | +Dynamic class loading via config: |
| 46 | +- Service definitions must include `"class": "full.module.path.ClassName"` |
| 47 | +- Parse names: `services.get_service("vector/chroma")` or `("vector", "chroma")` |
| 48 | +- Singletons cached by service+variant+override hash |
| 49 | + |
| 50 | +### 4. Framework Base (`knwl/framework_base.py`) |
| 51 | + |
| 52 | +All components inherit from `FrameworkBase` (ABC) providing: |
| 53 | +- `get_service(name, variant)` - fetch any configured service |
| 54 | +- `get_llm(variant)` - shorthand for LLM service |
| 55 | +- `ensure_path_exists(path)` - cross-platform path handling |
| 56 | +- `id` - unique UUID per instance |
| 57 | + |
| 58 | +## Core Data Flow |
| 59 | + |
| 60 | +``` |
| 61 | +Input Text → Documents → Chunks → Graph Extraction → SemanticGraph → Vector Storage |
| 62 | + ↓ |
| 63 | + KnwlExtraction (nodes+edges) |
| 64 | + ↓ |
| 65 | + Merge & Summarize → Storage (JSON/Chroma/NetworkX) |
| 66 | +``` |
| 67 | + |
| 68 | +**Main orchestration** in `GraphRAG` class (`knwl/semantic/graph_rag/graph_rag.py`): |
| 69 | + |
| 70 | +1. **`ingest(inputs)`** - Primary ingestion pipeline: |
| 71 | + - Processes `KnwlInput` → chunks text → extracts graph → stores in semantic graph |
| 72 | + - Returns `KnwlIngestion` with metadata |
| 73 | + - Optionally chunks and stores text if `ragger` is provided |
| 74 | + |
| 75 | +2. **`augment(inputs, params)`** - Query/augmentation with multiple strategies: |
| 76 | + - **local**: Keywords → nodes → retrieve neighborhood + chunks |
| 77 | + - **global**: Keywords → edges → retrieve endpoints + edge chunks |
| 78 | + - **naive**: Direct semantic chunk retrieval (no graph) |
| 79 | + - **hybrid**: Combines local + global contexts |
| 80 | + - Returns `KnwlContext` with augmented context and references |
| 81 | + |
| 82 | +## Models (`knwl/models/`) |
| 83 | + |
| 84 | +**All models are immutable Pydantic BaseModels** (`model_config = {"frozen": True}`): |
| 85 | + |
| 86 | +- Auto-generated `id` via hash of key fields (e.g., `KnwlNode.hash_keys(name, type)`) |
| 87 | +- Serialize with `model_dump(mode="json")` |
| 88 | +- Update via `model.model_copy(update={"field": new_value})` |
| 89 | + |
| 90 | +**Key models:** |
| 91 | +- `KnwlNode` - graph vertices (name, type, description, chunk_ids) |
| 92 | +- `KnwlEdge` - graph edges (source_id, targetId, description, keywords, weight) |
| 93 | +- `KnwlExtraction` - raw LLM extraction (dicts of nodes/edges keyed by name) |
| 94 | +- `KnwlGraph` - final graph (lists of `KnwlNode`/`KnwlEdge`) |
| 95 | +- `KnwlChunk`, `KnwlDocument`, `KnwlInput` - pipeline data structures |
| 96 | + |
| 97 | +## Component Architecture |
| 98 | + |
| 99 | +**Base classes define pluggable interfaces:** |
| 100 | +- `LLMBase` → `OllamaClient`, `OpenAIClient` |
| 101 | +- `ChunkingBase` → `TiktokenChunking` |
| 102 | +- `StorageBase` → `JsonStorage`, `SqliteStorage` |
| 103 | +- `VectorStorageBase` → `ChromaStorage` |
| 104 | +- `GraphStorageBase` → `NetworkXGraphStorage` |
| 105 | +- `GraphExtractionBase` → `BasicGraphExtraction`, `GleanGraphExtraction` |
| 106 | +- `GraphRAGBase` → `GraphRAG` |
| 107 | +- `SemanticGraphBase` → `SemanticGraph` |
| 108 | +- `RagBase` → `RagStore` |
| 109 | +- `GragStrategyBase` → `LocalGragStrategy`, `GlobalGragStrategy`, `NaiveGragStrategy`, `HybridGragStrategy` |
| 110 | +- `EntityExtractionBase`, `KeywordsExtractionBase`, `FormatterBase`, etc. |
| 111 | + |
| 112 | +**Storage namespaces** isolate data: |
| 113 | +```python |
| 114 | +JsonStorage(namespace="documents") # → {path}/documents.json |
| 115 | +ChromaStorage(namespace="nodes") # → Chroma collection "nodes" |
| 116 | +NetworkXGraphStorage(namespace="kg") # → {path}/kg.graphml |
| 117 | +``` |
| 118 | + |
| 119 | +## Testing |
| 120 | + |
| 121 | +**Fast tests** (skip LLM integration): |
| 122 | +```bash |
| 123 | +uv run pytest -m "not llm" |
| 124 | +``` |
| 125 | + |
| 126 | +**Full suite** (requires Ollama running): |
| 127 | +```bash |
| 128 | +uv run pytest |
| 129 | +``` |
| 130 | + |
| 131 | +**Test markers** (`pytest.ini`): |
| 132 | +- `@pytest.mark.llm` - needs Ollama/LLM |
| 133 | +- `@pytest.mark.asyncio` - async test |
| 134 | +- `@pytest.mark.integration` - external services |
| 135 | +- `@pytest.mark.slow` - long-running |
| 136 | +- `@pytest.mark.basic` - basic tests that don't require external services |
| 137 | + |
| 138 | +**Important**: DI container persists between tests - clear state in `setup_method()` if tests interfere. |
| 139 | + |
| 140 | +## Development Patterns |
| 141 | + |
| 142 | +### Adding a Service Variant |
| 143 | + |
| 144 | +1. **Create class** inheriting from base (e.g., `LLMBase`) |
| 145 | +2. **Add to config** in `knwl/config.py`: |
| 146 | + ```python |
| 147 | + "llm": { |
| 148 | + "default": "ollama", |
| 149 | + "my_llm": { |
| 150 | + "class": "knwl.llm.my_llm.MyLLMClient", |
| 151 | + "api_key": "...", |
| 152 | + "model": "..." |
| 153 | + } |
| 154 | + } |
| 155 | + ``` |
| 156 | +3. **Use via DI**: `@service("llm", variant="my_llm")` |
| 157 | + |
| 158 | +### Working with Immutable Models |
| 159 | + |
| 160 | +```python |
| 161 | +# ❌ WRONG - models are frozen |
| 162 | +node.description = "new desc" |
| 163 | + |
| 164 | +# ✅ CORRECT |
| 165 | +updated_node = node.model_copy(update={"description": "new desc"}) |
| 166 | +``` |
| 167 | + |
| 168 | +### Async Parallelism |
| 169 | + |
| 170 | +Prefer `asyncio.gather()` for concurrent operations: |
| 171 | +```python |
| 172 | +nodes = await asyncio.gather(*[ |
| 173 | + self.merge_nodes_into_graph(k, v) |
| 174 | + for k, v in extraction.nodes.items() |
| 175 | +]) |
| 176 | +``` |
| 177 | + |
| 178 | +### Config Overrides |
| 179 | + |
| 180 | +```python |
| 181 | +# Override specific config without modifying defaults |
| 182 | +override = {"llm": {"temperature": 0.5}} |
| 183 | + |
| 184 | +@service("llm", override=override) |
| 185 | +async def my_func(llm=None): |
| 186 | + # llm uses temperature=0.5 instead of default |
| 187 | + pass |
| 188 | +``` |
| 189 | + |
| 190 | +## Common Pitfalls |
| 191 | + |
| 192 | +1. **Frozen models**: Use `.model_copy(update={...})` not direct assignment |
| 193 | +2. **Config references**: `@/path/to/service` only works in config dict, not arbitrary strings |
| 194 | +3. **Service class paths**: Must be importable Python paths (e.g., `knwl.llm.ollama.OllamaClient`) |
| 195 | +4. **DI container state**: Persists across tests - may need cleanup |
| 196 | +5. **Path handling**: Always use `ensure_path_exists()` or `get_full_path()` from `FrameworkBase` |
| 197 | +6. **Namespace confusion**: Storage instances with same class but different namespaces are distinct |
| 198 | + |
| 199 | +## Entry Points |
| 200 | + |
| 201 | +- **Library**: `from knwl.semantic.graph_rag.graph_rag import GraphRAG; rag = GraphRAG(...); await rag.ingest(input)` |
| 202 | +- **API**: `api/main.py` - FastAPI REST service (uvicorn, configurable workers) |
| 203 | + |
| 204 | +**Running API**: |
| 205 | +```bash |
| 206 | +# Development (auto-reload) |
| 207 | +python api/main.py # reads config from api.host, api.port, api.development |
| 208 | + |
| 209 | +# Production |
| 210 | +uvicorn api.main:app --host 0.0.0.0 --port 9000 --workers 8 |
| 211 | +``` |
| 212 | + |
| 213 | +## Project Management |
| 214 | + |
| 215 | +**Package manager**: `uv` (not pip/poetry) |
| 216 | +- Dependencies in `pyproject.toml` |
| 217 | +- Install: `uv sync` |
| 218 | +- Run scripts: `uv run pytest`, `uv run python cli.py` |
| 219 | + |
| 220 | +**Design journal**: `journal/*.md` explains architectural decisions |
| 221 | +- `DependencyInjection.md` - DI framework rationale |
| 222 | +- `GraphRAG.md`, `GraphExtraction.md` - Graph RAG strategy |
| 223 | +- `Models.md` - Data model design |
| 224 | + |
| 225 | +## Key Files for Reference |
| 226 | + |
| 227 | +- `knwl/semantic/graph_rag/graph_rag.py` - Main `GraphRAG` orchestration class |
| 228 | +- `knwl/di.py` - DI framework (~930 lines, see `tests/test_di.py`) |
| 229 | +- `knwl/config.py` - Config structure, `get_config()`, merge logic |
| 230 | +- `knwl/services.py` - Service registry, dynamic class loading |
| 231 | +- `knwl/framework_base.py` - Base class for all components |
| 232 | +- `knwl/prompts/extraction_prompts.py` - LLM prompts for entity extraction |
| 233 | +- `knwl/semantic/graph/semantic_graph.py` - Semantic graph implementation |
| 234 | +- `knwl/semantic/rag/rag_store.py` - RAG store for chunk management |
| 235 | +- `tests/fixtures.py` - Test data and shared fixtures |
0 commit comments