diff --git a/api/main.py b/api/main.py index 3b02091..1643ddb 100644 --- a/api/main.py +++ b/api/main.py @@ -1,18 +1,15 @@ +import logging import os from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -import logging - -from api.routes import rag from api.auth import validate_auth_config - -from index.vector_store import VectorStore +from api.routes import rag from index.graph_store import GraphStore from index.plugin_index import PluginIndex - +from index.vector_store import VectorStore from rag.control_plane import CONTROL_PLANE logger = logging.getLogger(__name__) @@ -153,11 +150,10 @@ def status(): @app.middleware("http") async def guard_requests(request: Request, call_next): - if request.url.path.startswith("/rag"): - if CONTROL_PLANE.status()["status"] != "READY": - return JSONResponse( - status_code=503, - content={"detail": "Control plane not ready"} - ) + if request.url.path.startswith("/rag") and CONTROL_PLANE.status()["status"] != "READY": + return JSONResponse( + status_code=503, + content={"detail": "Control plane not ready"} + ) return await call_next(request) \ No newline at end of file diff --git a/api/middleware/trace.py b/api/middleware/trace.py index e873216..13feb92 100644 --- a/api/middleware/trace.py +++ b/api/middleware/trace.py @@ -1,10 +1,10 @@ -import uuid import time -from typing import Dict +import uuid + class TraceContext: def __init__(self): - self.store: Dict[str, dict] = {} + self.store: dict[str, dict] = {} def start(self, query: str): trace_id = str(uuid.uuid4()) diff --git a/api/routes/rag.py b/api/routes/rag.py index 46a9e0e..f2ab584 100644 --- a/api/routes/rag.py +++ b/api/routes/rag.py @@ -1,13 +1,13 @@ -from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import StreamingResponse -from pydantic import BaseModel -from typing import Optional import json import os import traceback -from rag.control_plane import CONTROL_PLANE +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + from api.auth import require_auth +from rag.control_plane import CONTROL_PLANE router = APIRouter() @@ -24,8 +24,8 @@ def _debug_tracebacks_enabled() -> bool: class QueryRequest(BaseModel): query: str - repo: Optional[str] = None - bundle: Optional[str] = None + repo: str | None = None + bundle: str | None = None # ========================================================= @@ -45,8 +45,8 @@ def get_engine(): return engine - except Exception as e: - raise RuntimeError(f"Engine access failed: {str(e)}") + except Exception as e: # noqa: BLE001 -- boundary: any engine-access failure becomes a clean RuntimeError for the caller + raise RuntimeError(f"Engine access failed: {e!s}") # ========================================================= @@ -67,7 +67,7 @@ def query(req: QueryRequest, actor: str = Depends(require_auth)): "api_version": "v6" } - except Exception as e: + except Exception as e: # noqa: BLE001 -- top-level HTTP boundary: must catch anything to return a clean 500 instead of an unhandled crash detail = {"error": str(e)} # Stack traces can leak file paths, internals, and other sensitive # detail into the HTTP response -- only include one when a deployer @@ -106,7 +106,7 @@ def event_stream(): yield f"data: {json.dumps({'type': 'done'})}\n\n" - except Exception as e: + except Exception as e: # noqa: BLE001 -- SSE generator boundary: must catch anything to emit an error event instead of killing the stream yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n" return StreamingResponse( diff --git a/embeddings/embedder.py b/embeddings/embedder.py index e9c157a..b6092a6 100644 --- a/embeddings/embedder.py +++ b/embeddings/embedder.py @@ -1,7 +1,6 @@ -from sentence_transformers import SentenceTransformer -from typing import List, Union -import numpy as np +import numpy as np +from sentence_transformers import SentenceTransformer # ========================================================= # EMBEDDER V4 - OMNIBIOAI @@ -25,7 +24,7 @@ def __init__(self, model_name: str = "all-MiniLM-L6-v2"): # ========================================================= # MAIN ENCODE FUNCTION # ========================================================= - def encode(self, texts: Union[str, List[str]], batch_size: int = 32) -> List[List[float]]: + def encode(self, texts: str | list[str], batch_size: int = 32) -> list[list[float]]: """ Convert text → embeddings """ @@ -67,7 +66,7 @@ def _normalize(self, embeddings: np.ndarray) -> np.ndarray: # ========================================================= # UTILITY: SINGLE EMBEDDING # ========================================================= - def encode_single(self, text: str) -> List[float]: + def encode_single(self, text: str) -> list[float]: """ Convenience method for single query embedding """ diff --git a/index/graph_store.py b/index/graph_store.py index b3d5230..bf05de6 100644 --- a/index/graph_store.py +++ b/index/graph_store.py @@ -1,6 +1,5 @@ from collections import defaultdict, deque -from typing import Dict, List, Tuple, Set, Any - +from typing import Any # ========================================================= # GRAPH STORE V4 - OMNIBIOAI @@ -21,7 +20,7 @@ class GraphStore: def __init__(self): # adjacency list: node -> [(neighbor, relation)] - self.edges: Dict[str, List[Tuple[str, str]]] = defaultdict(list) + self.edges: dict[str, list[tuple[str, str]]] = defaultdict(list) # ========================================================= # ADD EDGE @@ -39,7 +38,7 @@ def add_edge(self, a: str, b: str, relation: str = "related"): # ========================================================= # MAIN SEARCH ENTRY # ========================================================= - def search(self, query: str, max_depth: int = 2) -> List[Dict[str, Any]]: + def search(self, query: str, max_depth: int = 2) -> list[dict[str, Any]]: """ Semantic graph expansion search (V4) @@ -52,8 +51,8 @@ def search(self, query: str, max_depth: int = 2) -> List[Dict[str, Any]]: if not query: return [] - visited: Set[str] = set() - results: List[Dict[str, Any]] = [] + visited: set[str] = set() + results: list[dict[str, Any]] = [] seed_nodes = self._find_seed_nodes(query) @@ -70,13 +69,13 @@ def search(self, query: str, max_depth: int = 2) -> List[Dict[str, Any]]: # ========================================================= # SEED NODE DETECTION # ========================================================= - def _find_seed_nodes(self, query: str) -> List[str]: + def _find_seed_nodes(self, query: str) -> list[str]: seeds = [] q_tokens = set(query.split()) - for node in self.edges.keys(): + for node in self.edges: node_l = node.lower() node_tokens = set(node_l.split()) @@ -100,8 +99,8 @@ def _bfs_expand( start: str, query: str, max_depth: int, - visited: Set[str] - ) -> List[Dict[str, Any]]: + visited: set[str] + ) -> list[dict[str, Any]]: queue = deque([(start, 0)]) results = [] @@ -153,7 +152,7 @@ def _score_match(self, query: str, a: str, b: str) -> float: # ========================================================= # GRAPH STATS (FOR UI) # ========================================================= - def size(self) -> Dict[str, int]: + def size(self) -> dict[str, int]: node_count = len(self.edges) edge_count = sum(len(v) for v in self.edges.values()) @@ -166,7 +165,7 @@ def size(self) -> Dict[str, int]: # ========================================================= # DEBUG EXPORT (UI GRAPH VISUALIZATION READY) # ========================================================= - def export(self) -> Dict[str, Any]: + def export(self) -> dict[str, Any]: nodes = list(self.edges.keys()) edges = [] diff --git a/index/vector_store.py b/index/vector_store.py index 80dbf2a..42b11e5 100644 --- a/index/vector_store.py +++ b/index/vector_store.py @@ -1,8 +1,9 @@ +import logging import os import pickle -import numpy as np + import faiss -import logging +import numpy as np logger = logging.getLogger(__name__) @@ -103,7 +104,7 @@ def search(self, query_vec, top_k: int = 5): return results - def filter_search(self, query_vec, top_k: int = 5, field: str = None, value: str = None): + def filter_search(self, query_vec, top_k: int = 5, field: str | None = None, value: str | None = None): """FAISS search with post-filtering on a metadata field. Retrieves top_k * 3 candidates from FAISS then keeps only those whose diff --git a/ingestion/doc_loader.py b/ingestion/doc_loader.py index 42a02e6..211feb1 100644 --- a/ingestion/doc_loader.py +++ b/ingestion/doc_loader.py @@ -41,7 +41,7 @@ def load_documents(repo_paths): "text": text, "source": fpath }) - except Exception as e: + except Exception as e: # noqa: BLE001 -- one unreadable file must not abort the whole indexing walk print(f"⚠️ Could not read {fpath}: {e}") print(f"📄 Loaded {len(docs)} documents") return docs \ No newline at end of file diff --git a/processing/chunker.py b/processing/chunker.py index 09be2f5..4e89934 100644 --- a/processing/chunker.py +++ b/processing/chunker.py @@ -1,5 +1,4 @@ import re -from typing import List, Optional MAX_CHARS = 2000 @@ -7,7 +6,7 @@ _HEADER_RE = re.compile(r'^(#{1,3})\s+.+$', re.MULTILINE) -def _split_at_word_boundary(text: str, max_chars: int) -> List[str]: +def _split_at_word_boundary(text: str, max_chars: int) -> list[str]: """Split text at word boundaries, keeping each piece ≤ max_chars.""" if len(text) <= max_chars: return [text] @@ -23,7 +22,7 @@ def _split_at_word_boundary(text: str, max_chars: int) -> List[str]: return chunks -def _split_at_paragraphs(text: str, max_chars: int) -> List[str]: +def _split_at_paragraphs(text: str, max_chars: int) -> list[str]: """Split at blank-line boundaries; fall back to word-boundary for oversize paragraphs.""" if max_chars <= 0: return _split_at_word_boundary(text, MAX_CHARS) if text.strip() else [] @@ -31,8 +30,8 @@ def _split_at_paragraphs(text: str, max_chars: int) -> List[str]: return [text] if text.strip() else [] paragraphs = [p for p in re.split(r'\n\n+', text) if p.strip()] - result: List[str] = [] - current_parts: List[str] = [] + result: list[str] = [] + current_parts: list[str] = [] current_len = 0 for para in paragraphs: @@ -58,7 +57,7 @@ def _split_at_paragraphs(text: str, max_chars: int) -> List[str]: return result -def chunk_text(text: str, chunk_size: int = 500) -> List[str]: +def chunk_text(text: str, chunk_size: int = 500) -> list[str]: """ Markdown-structure-aware chunker. chunk_size is kept for API compatibility. @@ -73,7 +72,7 @@ def chunk_text(text: str, chunk_size: int = 500) -> List[str]: return [] # Protect fenced code blocks — replace with non-splitting placeholders - fences: List[str] = [] + fences: list[str] = [] def _stash(m: re.Match) -> str: fences.append(m.group(0)) @@ -82,9 +81,9 @@ def _stash(m: re.Match) -> str: protected = _FENCE_RE.sub(_stash, text) # Split into sections: list of (header_line | None, body_str, header_level) - sections: List[tuple] = [] + sections: list[tuple] = [] last_end = 0 - current_header: Optional[str] = None + current_header: str | None = None current_level = 0 for m in _HEADER_RE.finditer(protected): @@ -100,8 +99,8 @@ def _stash(m: re.Match) -> str: sections.append((current_header, tail, current_level)) # Build chunks, maintaining a header breadcrumb stack - header_stack: List[str] = [] - all_chunks: List[str] = [] + header_stack: list[str] = [] + all_chunks: list[str] = [] for header_line, body, level in sections: if header_line is not None: @@ -133,7 +132,7 @@ def _stash(m: re.Match) -> str: all_chunks.append(full) # fallback: emit as-is # Restore fenced code blocks in every chunk - restored: List[str] = [] + restored: list[str] = [] for chunk in all_chunks: for i, fence in enumerate(fences): chunk = chunk.replace(f'\x00FENCE{i}\x00', fence) diff --git a/rag/control_plane.py b/rag/control_plane.py index 487960d..a0e20bd 100644 --- a/rag/control_plane.py +++ b/rag/control_plane.py @@ -1,8 +1,7 @@ -import time import threading +import time from dataclasses import dataclass, field -from typing import Optional, Dict, Any - +from typing import Any # ========================================================= # CONTROL PLANE STATE @@ -12,8 +11,8 @@ class ControlPlaneState: status: str = "INIT" # INIT | BUILDING | READY | FAILED started_at: float = field(default_factory=time.time) - last_error: Optional[str] = None - metrics: Dict[str, Any] = field(default_factory=dict) + last_error: str | None = None + metrics: dict[str, Any] = field(default_factory=dict) # ========================================================= diff --git a/rag/engine.py b/rag/engine.py index a434f2f..284d77a 100644 --- a/rag/engine.py +++ b/rag/engine.py @@ -1,11 +1,13 @@ -import os import json -import time import logging -import requests +import os +import time +from collections.abc import Generator +from typing import Any + import numpy as np +import requests import yaml -from typing import Generator, List, Dict, Any, Optional logger = logging.getLogger(__name__) @@ -22,7 +24,7 @@ def _get_cross_encoder(): from sentence_transformers import CrossEncoder _CROSS_ENCODER = CrossEncoder(_CE_MODEL, device="cpu") logger.info(f"CrossEncoder loaded: {_CE_MODEL}") - except Exception as e: + except Exception as e: # noqa: BLE001 -- optional dependency: any load failure degrades to no reranking rather than crashing logger.warning(f"CrossEncoder unavailable ({e}); reranking will be skipped") _CROSS_ENCODER = False # sentinel: tried and failed return _CROSS_ENCODER if _CROSS_ENCODER is not False else None @@ -154,7 +156,7 @@ def __init__(self, vector_store): def _embed(self, text: str): if not isinstance(text, str): - raise ValueError("Query must be a string") + raise TypeError("Query must be a string") vec = ollama_embed(text, model=self.embed_model) @@ -171,7 +173,7 @@ def _embed(self, text: str): # ===================================================== # CROSS-ENCODER RERANKING # ===================================================== - def rerank(self, query: str, docs: List[Dict[str, Any]], top_k: int = 5) -> List[Dict[str, Any]]: + def rerank(self, query: str, docs: list[dict[str, Any]], top_k: int = 5) -> list[dict[str, Any]]: """Re-order docs by cross-encoder relevance score. Returns the top_k highest-scoring docs. Falls back to the original @@ -200,7 +202,7 @@ def rerank(self, query: str, docs: List[Dict[str, Any]], top_k: int = 5) -> List # ===================================================== # RETRIEVAL (FAISS ONLY) # ===================================================== - def retrieve(self, query: str, top_k: int = 5, repo: str = None, bundle: str = None, + def retrieve(self, query: str, top_k: int = 5, repo: str | None = None, bundle: str | None = None, rerank: bool = False): query_vec = self._embed(query).reshape(1, -1) @@ -242,7 +244,7 @@ def retrieve(self, query: str, top_k: int = 5, repo: str = None, bundle: str = N # ===================================================== # CONTEXT BUILDER # ===================================================== - def build_context(self, docs: List[Dict[str, Any]]) -> str: + def build_context(self, docs: list[dict[str, Any]]) -> str: if not docs: return "No relevant context found." @@ -274,7 +276,7 @@ def build_prompt(self, query: str, context: str) -> str: # ===================================================== # MAIN PIPELINE # ===================================================== - def answer(self, query: str, repo: str = None, bundle: str = None): + def answer(self, query: str, repo: str | None = None, bundle: str | None = None): docs = self.retrieve(query, repo=repo, bundle=bundle) context = self.build_context(docs) @@ -282,8 +284,8 @@ def answer(self, query: str, repo: str = None, bundle: str = None): try: response = ollama_generate(prompt) - except Exception as e: - response = f"[LLM_ERROR] {str(e)}" + except Exception as e: # noqa: BLE001 -- LLM call boundary: any failure becomes an inline error string instead of crashing the request + response = f"[LLM_ERROR] {e!s}" return { "query": query, @@ -316,11 +318,11 @@ def stream_llm(self, query: str, context: str) -> Generator[str, None, None]: yield token if chunk.get("done"): break - except Exception as e: - yield f"[LLM_ERROR] {str(e)}" + except Exception as e: # noqa: BLE001 -- LLM streaming boundary: any failure becomes an inline error token instead of killing the generator + yield f"[LLM_ERROR] {e!s}" # ===================================================== # FASTAPI COMPATIBILITY # ===================================================== - def query(self, question: str, repo: str = None, bundle: str = None): + def query(self, question: str, repo: str | None = None, bundle: str | None = None): return self.answer(question, repo=repo, bundle=bundle) \ No newline at end of file diff --git a/rag/query_router.py b/rag/query_router.py index 9a37b9d..2687dac 100644 --- a/rag/query_router.py +++ b/rag/query_router.py @@ -1,9 +1,6 @@ -import requests import json -import time -import uuid -from typing import List, Dict, Any +import requests # ========================================================= # RAG QUERY ROUTER V4 @@ -45,7 +42,7 @@ def detect_intent(self, query: str) -> str: # ----------------------------- # VECTOR SEARCH # ----------------------------- - def vector_search(self, query: str) -> List[Dict]: + def vector_search(self, query: str) -> list[dict]: if not self.vector_store or not self.embedder: return [] @@ -58,7 +55,7 @@ def vector_search(self, query: str) -> List[Dict]: return self.vector_store.search(query_vector, top_k=self.top_k) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-source boundary: a broken subsystem must not break hybrid_retrieve's other sources print("[Vector Error]", e) return [] @@ -71,7 +68,7 @@ def graph_search(self, query: str): try: return self.graph_store.search(query) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-source boundary: a broken subsystem must not break hybrid_retrieve's other sources print("[Graph Error]", e) return [] @@ -84,7 +81,7 @@ def plugin_search(self, query: str): try: return self.plugin_index.search(query) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-source boundary: a broken subsystem must not break hybrid_retrieve's other sources print("[Plugin Error]", e) return [] @@ -103,7 +100,7 @@ def hybrid_retrieve(self, query: str): # ----------------------------- # CONTEXT BUILDER # ----------------------------- - def build_context(self, results: Dict): + def build_context(self, results: dict): blocks = [] @@ -161,11 +158,12 @@ def stream_llm(self, query: str, context: str): token = data.get("message", {}).get("content", "") if token: yield token - except: + except (json.JSONDecodeError, UnicodeDecodeError, AttributeError, KeyError) as e: + print("[Stream Parse Error]", e) continue - except Exception as e: - yield f"[STREAM_ERROR] {str(e)}" + except Exception as e: # noqa: BLE001 -- streaming boundary: any failure becomes an inline error token instead of killing the generator + yield f"[STREAM_ERROR] {e!s}" # ----------------------------- # MAIN PIPELINE diff --git a/rag/tool_executor.py b/rag/tool_executor.py index f93bdc9..9cb18d3 100644 --- a/rag/tool_executor.py +++ b/rag/tool_executor.py @@ -1,5 +1,4 @@ -from typing import Dict, List, Any - +from typing import Any # ========================================================= # TOOL EXECUTOR V4 - OMNIBIOAI @@ -26,7 +25,7 @@ def __init__(self, vector_store, graph_store, plugin_index, embedder): # ========================================================= # MAIN ENTRY # ========================================================= - def run(self, plan: Dict[str, Any], query: str) -> List[Dict]: + def run(self, plan: dict[str, Any], query: str) -> list[dict]: results = [] steps = plan.get("steps", []) @@ -47,9 +46,9 @@ def run(self, plan: Dict[str, Any], query: str) -> List[Dict]: elif step == "hybrid_expand": results.extend(self._hybrid_expand(query)) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-step boundary: one broken tool step must not abort the whole plan results.append({ - "text": f"[TOOL_ERROR] {step}: {str(e)}", + "text": f"[TOOL_ERROR] {step}: {e!s}", "source": "error" }) @@ -58,53 +57,53 @@ def run(self, plan: Dict[str, Any], query: str) -> List[Dict]: # ========================================================= # VECTOR SEARCH # ========================================================= - def _vector(self, query: str) -> List[Dict]: + def _vector(self, query: str) -> list[dict]: if not self.vector_store: return [] try: emb = self.embedder.encode([query])[0] return self.vector_store.search(emb, top_k=5) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-tool boundary: returns an error result instead of raising return [{ - "text": f"Vector search failed: {str(e)}", + "text": f"Vector search failed: {e!s}", "source": "vector_error" }] # ========================================================= # GRAPH SEARCH # ========================================================= - def _graph(self, query: str) -> List[Dict]: + def _graph(self, query: str) -> list[dict]: if not self.graph_store: return [] try: return self.graph_store.search(query) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-tool boundary: returns an error result instead of raising return [{ - "text": f"Graph search failed: {str(e)}", + "text": f"Graph search failed: {e!s}", "source": "graph_error" }] # ========================================================= # PLUGIN SEARCH # ========================================================= - def _plugin(self, query: str) -> List[Dict]: + def _plugin(self, query: str) -> list[dict]: if not self.plugin_index: return [] try: return self.plugin_index.search(query) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-tool boundary: returns an error result instead of raising return [{ - "text": f"Plugin search failed: {str(e)}", + "text": f"Plugin search failed: {e!s}", "source": "plugin_error" }] # ========================================================= # MEMORY SEARCH (V4 READY HOOK) # ========================================================= - def _memory(self, query: str) -> List[Dict]: + def _memory(self, query: str) -> list[dict]: """ Placeholder for RAG V4 memory system: - conversation memory @@ -120,7 +119,7 @@ def _memory(self, query: str) -> List[Dict]: # ========================================================= # HYBRID EXPANSION (V4 AGENT FEATURE) # ========================================================= - def _hybrid_expand(self, query: str) -> List[Dict]: + def _hybrid_expand(self, query: str) -> list[dict]: """ Future: expand query using graph + embeddings fusion """ diff --git a/retrieval/retriever.py b/retrieval/retriever.py index 3e168a4..328625f 100644 --- a/retrieval/retriever.py +++ b/retrieval/retriever.py @@ -1,5 +1,6 @@ from embeddings.embedder import Embedder + class Retriever: def __init__(self, vector_store): self.embedder = Embedder() diff --git a/scripts/build_index.py b/scripts/build_index.py index 64c8775..318847c 100644 --- a/scripts/build_index.py +++ b/scripts/build_index.py @@ -1,15 +1,16 @@ -import sys -import os import hashlib +import os +import sys + import numpy as np import yaml sys.path.append(os.path.abspath(".")) from index.vector_store import VectorStore -from rag.engine import ollama_embed # SINGLE SOURCE OF TRUTH from ingestion.doc_loader import load_documents from processing.chunker import chunk_text +from rag.engine import ollama_embed # SINGLE SOURCE OF TRUTH MIN_CHUNK_CHARS = 10 # discard overflow tails from chunker.py's hard char-slice @@ -164,7 +165,7 @@ def build_index(): try: vec = ollama_embed(chunk) # <<< SINGLE EMBEDDING SOURCE vec = normalize_vector(vec) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-chunk boundary: one bad chunk must not abort the whole indexing run print(f"⚠️ Skipping chunk from {source}: {e}") stats["embed_failed"] += 1 continue diff --git a/scripts/run_eval.py b/scripts/run_eval.py old mode 100644 new mode 100755 index 0fa8b8b..7e9639b --- a/scripts/run_eval.py +++ b/scripts/run_eval.py @@ -12,8 +12,8 @@ import argparse import json -import sys import os +import sys # Allow imports from project root sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) @@ -67,7 +67,7 @@ def run_eval(index_dir: str, eval_path: str): try: docs = engine.retrieve(query, top_k=TOP_K, repo=repo, bundle=bundle, rerank=use_rerank) - except Exception as e: + except Exception as e: # noqa: BLE001 -- per-query boundary: one failing query must not abort the whole eval run label = FAIL_MARKER matched = f"[ERROR] {e}" results.append({"query": query, "passed": False, "matched": matched, "expected": expected}) diff --git a/tests/test_chunker.py b/tests/test_chunker.py index 8aaa0b0..a1930be 100644 --- a/tests/test_chunker.py +++ b/tests/test_chunker.py @@ -1,13 +1,11 @@ """Comprehensive tests for the markdown-aware chunker (processing/chunker.py).""" -import pytest from processing.chunker import ( - chunk_text, - _split_at_word_boundary, - _split_at_paragraphs, MAX_CHARS, + _split_at_paragraphs, + _split_at_word_boundary, + chunk_text, ) - # --------------------------------------------------------------------------- # _split_at_word_boundary # --------------------------------------------------------------------------- @@ -310,6 +308,7 @@ def test_very_long_code_block_not_fragmented(self): closing_count = sum(c.count("```") for c in chunks) # Opening appears exactly once; closing appears at least once (may be in same chunk) assert opening_count == 1 + assert closing_count >= 1 def test_multiple_code_blocks_each_preserved(self): block1 = "```python\nprint('hello')\n```" diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 6a517f2..d9fe30f 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -1,7 +1,8 @@ -import pytest -import numpy as np -from unittest.mock import MagicMock, patch import sys +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest # Mock sentence_transformers mock_st = MagicMock() @@ -10,6 +11,7 @@ from embeddings.embedder import Embedder + @pytest.fixture def embedder(): with patch("embeddings.embedder.SentenceTransformer") as mock_model: diff --git a/tests/test_index_graph_store.py b/tests/test_index_graph_store.py index da0c505..ba3faa2 100644 --- a/tests/test_index_graph_store.py +++ b/tests/test_index_graph_store.py @@ -1,6 +1,8 @@ import pytest + from index.graph_store import GraphStore + @pytest.fixture def gs(): return GraphStore() diff --git a/tests/test_index_plugin.py b/tests/test_index_plugin.py index 400c000..4804e19 100644 --- a/tests/test_index_plugin.py +++ b/tests/test_index_plugin.py @@ -1,5 +1,6 @@ from index.plugin_index import PluginIndex + def test_plugin_index(): pi = PluginIndex([{"text": "t1", "plugin": "p1"}]) assert len(pi.docs) == 1 diff --git a/tests/test_index_vector_store.py b/tests/test_index_vector_store.py index 823472e..97e307b 100644 --- a/tests/test_index_vector_store.py +++ b/tests/test_index_vector_store.py @@ -1,7 +1,8 @@ -import pytest -import numpy as np -from unittest.mock import MagicMock, patch, mock_open import sys +from unittest.mock import MagicMock, mock_open, patch + +import numpy as np +import pytest # Mock faiss mock_faiss = MagicMock() @@ -10,6 +11,7 @@ from index.vector_store import VectorStore + @pytest.fixture def vs(): return VectorStore() @@ -115,6 +117,7 @@ def test_save_success(vs): mock_mkdirs.assert_called_once_with("/tmp/vs_test", exist_ok=True) mock_faiss.write_index.assert_called_once() + mock_file.assert_called_once() mock_pdump.assert_called_once() @@ -135,6 +138,7 @@ def test_load_success(vs): mock_faiss.read_index = MagicMock(return_value=mock_loaded_index) result = vs.load("/tmp/vs_test") + mock_pload.assert_called_once() assert result is True assert vs.dim == 768 assert vs.metadata == saved_data["metadata"] diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py index 7e2a756..62da900 100644 --- a/tests/test_ingestion.py +++ b/tests/test_ingestion.py @@ -1,7 +1,8 @@ -import pytest -from unittest.mock import patch, mock_open +from unittest.mock import mock_open, patch + from ingestion.doc_loader import load_documents + def test_load_documents(): fake_walk = [("/repo", [], ["README.md", "notes.txt"])] with patch("os.path.exists", return_value=True), \ diff --git a/tests/test_main_api.py b/tests/test_main_api.py index fef7fd7..7ba25e7 100644 --- a/tests/test_main_api.py +++ b/tests/test_main_api.py @@ -1,8 +1,8 @@ +import sys +from unittest.mock import AsyncMock, MagicMock, patch + import pytest from fastapi.testclient import TestClient -from unittest.mock import MagicMock, patch, AsyncMock -import asyncio -import sys # Mock heavy dependencies properly mock_faiss = MagicMock() @@ -26,19 +26,23 @@ def test_health_endpoint(): assert response.json()["status"] == "ok" def test_status_endpoint(): - with patch("api.main.CONTROL_PLANE.status", return_value={"status": "READY"}): - with patch("api.main.graph_store") as mock_gs: - mock_gs.size.return_value = {"nodes": 3, "edges": 4} - response = client.get("/status") - assert response.status_code == 200 - assert response.json()["graph_edges"] == 4 + with ( + patch("api.main.CONTROL_PLANE.status", return_value={"status": "READY"}), + patch("api.main.graph_store") as mock_gs, + ): + mock_gs.size.return_value = {"nodes": 3, "edges": 4} + response = client.get("/status") + assert response.status_code == 200 + assert response.json()["graph_edges"] == 4 def test_guard_requests_middleware_ready(): - with patch("api.main.CONTROL_PLANE.status", return_value={"status": "READY"}): - with patch("api.routes.rag.get_engine"): - response = client.post("/rag/query", json={"query": "q"}) - assert response.status_code != 503 + with ( + patch("api.main.CONTROL_PLANE.status", return_value={"status": "READY"}), + patch("api.routes.rag.get_engine"), + ): + response = client.post("/rag/query", json={"query": "q"}) + assert response.status_code != 503 def test_guard_requests_middleware_not_ready(): with patch("api.main.CONTROL_PLANE.status", return_value={"status": "INIT"}): @@ -47,14 +51,14 @@ def test_guard_requests_middleware_not_ready(): assert response.json()["detail"] == "Control plane not ready" def test_build_graph_seed(): - from api.main import build_graph_seed, graph_store + from api.main import build_graph_seed mock_gs = MagicMock() with patch("api.main.graph_store", mock_gs): build_graph_seed() assert mock_gs.add_edge.call_count == 4 def test_build_plugin_index(): - from api.main import build_plugin_index, plugin_index + from api.main import build_plugin_index mock_pi = MagicMock() with patch("api.main.plugin_index", mock_pi): build_plugin_index() diff --git a/tests/test_rag_control_plane.py b/tests/test_rag_control_plane.py index caa7aa5..0f79e80 100644 --- a/tests/test_rag_control_plane.py +++ b/tests/test_rag_control_plane.py @@ -1,7 +1,9 @@ -import pytest -import time from unittest.mock import MagicMock, patch -from rag.control_plane import ControlPlane, ControlPlaneState + +import pytest + +from rag.control_plane import ControlPlane + @pytest.fixture def cp(): diff --git a/tests/test_rag_engine.py b/tests/test_rag_engine.py index ebb2c91..3304cf9 100644 --- a/tests/test_rag_engine.py +++ b/tests/test_rag_engine.py @@ -1,8 +1,10 @@ -import pytest -import numpy as np from unittest.mock import MagicMock, patch -from rag.engine import RAGEngine, ollama_embed, ollama_generate, cosine + +import numpy as np +import pytest + import rag.engine as _engine_mod +from rag.engine import RAGEngine, cosine, ollama_embed, ollama_generate # ========================================================= # UNIT TESTS FOR ollama_embed @@ -89,7 +91,7 @@ def test_engine_embed_success(mock_embed, engine): @patch("rag.engine.ollama_embed") def test_engine_embed_invalid_type(mock_embed, engine): - with pytest.raises(ValueError, match="Query must be a string"): + with pytest.raises(TypeError, match="Query must be a string"): engine._embed(123) @patch("rag.engine.ollama_embed") @@ -168,6 +170,7 @@ def test_engine_answer_success(mock_gen, engine, mock_vector_store): # Mock retrieve to return something with patch.object(engine, "retrieve", return_value=[{"source": "s1"}]) as mock_retrieve: res = engine.answer("query") + mock_retrieve.assert_called_once_with("query", repo=None, bundle=None) assert res["answer"] == "final answer" assert res["sources"] == ["s1"] assert res["version"] == "v6-faiss" @@ -254,10 +257,12 @@ def test_engine_retrieve_bundle_takes_priority_over_repo(mock_embed, engine, moc def test_engine_answer_passes_scope(mock_embed, engine, mock_vector_store): mock_embed.return_value = np.array([0.1] * 768, dtype=np.float32) - with patch.object(engine, "retrieve", return_value=[]) as mock_retrieve: - with patch("rag.engine.ollama_generate", return_value="ans"): - engine.answer("q", repo="r", bundle="b") - mock_retrieve.assert_called_once_with("q", repo="r", bundle="b") + with ( + patch.object(engine, "retrieve", return_value=[]) as mock_retrieve, + patch("rag.engine.ollama_generate", return_value="ans"), + ): + engine.answer("q", repo="r", bundle="b") + mock_retrieve.assert_called_once_with("q", repo="r", bundle="b") # ========================================================= diff --git a/tests/test_rag_memory_store.py b/tests/test_rag_memory_store.py index 92760e9..db457ee 100644 --- a/tests/test_rag_memory_store.py +++ b/tests/test_rag_memory_store.py @@ -1,5 +1,6 @@ from rag.memory_store import MemoryStoreV4 + def test_memory_store(): ms = MemoryStoreV4(max_len=2) ms.add("user", "hi") diff --git a/tests/test_rag_query_router.py b/tests/test_rag_query_router.py index 4fcc462..c9e5e92 100644 --- a/tests/test_rag_query_router.py +++ b/tests/test_rag_query_router.py @@ -1,7 +1,10 @@ -import pytest import json from unittest.mock import MagicMock, patch -from rag.query_router import RAGQueryRouterV4, init_engine, get_engine + +import pytest + +from rag.query_router import RAGQueryRouterV4, get_engine, init_engine + @pytest.fixture def mock_vs(): @@ -67,12 +70,14 @@ def test_plugin_search_error(router, mock_pi): assert router.plugin_search("q") == [] def test_hybrid_retrieve(router): - with patch.object(router, "detect_intent", return_value="intent"): - with patch.object(router, "vector_search", return_value=[]): - with patch.object(router, "graph_search", return_value=[]): - with patch.object(router, "plugin_search", return_value=[]): - res = router.hybrid_retrieve("q") - assert res["intent"] == "intent" + with ( + patch.object(router, "detect_intent", return_value="intent"), + patch.object(router, "vector_search", return_value=[]), + patch.object(router, "graph_search", return_value=[]), + patch.object(router, "plugin_search", return_value=[]), + ): + res = router.hybrid_retrieve("q") + assert res["intent"] == "intent" def test_build_context(router): results = { @@ -104,11 +109,13 @@ def test_stream_llm_error(mock_post, router): assert "[STREAM_ERROR] HTTP error" in tokens[0] def test_query(router): - with patch.object(router, "hybrid_retrieve", return_value={"intent": "i", "vector": [], "graph": [], "plugin": []}): - with patch.object(router, "stream_llm", return_value=["ans"]): - res = router.query("q") - assert res["intent"] == "i" - assert res["answer"] == "ans" + with ( + patch.object(router, "hybrid_retrieve", return_value={"intent": "i", "vector": [], "graph": [], "plugin": []}), + patch.object(router, "stream_llm", return_value=["ans"]), + ): + res = router.query("q") + assert res["intent"] == "i" + assert res["answer"] == "ans" def test_engine_singleton(): init_engine(MagicMock()) diff --git a/tests/test_rag_routes.py b/tests/test_rag_routes.py index 2d32490..c4c5cab 100644 --- a/tests/test_rag_routes.py +++ b/tests/test_rag_routes.py @@ -1,11 +1,12 @@ +import json +from unittest.mock import MagicMock, patch + import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from unittest.mock import MagicMock, patch -import json # Import the router and models from the target file -from api.routes.rag import router, get_engine +from api.routes.rag import get_engine, router # Create a dummy app to test the router app = FastAPI() diff --git a/tests/test_rag_tool_executor.py b/tests/test_rag_tool_executor.py index 0d8ce72..03521e2 100644 --- a/tests/test_rag_tool_executor.py +++ b/tests/test_rag_tool_executor.py @@ -1,7 +1,10 @@ -import pytest from unittest.mock import MagicMock, patch + +import pytest + from rag.tool_executor import ToolExecutorV4 + @pytest.fixture def mock_vs(): return MagicMock() diff --git a/tests/test_retriever.py b/tests/test_retriever.py index e77c641..5cf6a04 100644 --- a/tests/test_retriever.py +++ b/tests/test_retriever.py @@ -1,7 +1,10 @@ -import pytest from unittest.mock import MagicMock, patch + +import pytest + from retrieval.retriever import Retriever + @pytest.fixture def mock_vs(): return MagicMock()