Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
88 changes: 38 additions & 50 deletions autobot-backend/agents/npu_code_search_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import aiofiles

from autobot_shared.embedding_model import EmbeddingUnavailableError, raise_if_embedding_refused
from autobot_shared.logging_manager import get_logger
from autobot_shared.redis_client import get_async_redis_client, get_redis_client
from autobot_shared.security.path_validator import PROJECT_ALLOWED_ROOTS, validate_path
Expand Down Expand Up @@ -664,23 +665,15 @@ async def _search_hybrid(
max_results: int,
semantic_weight: float = 0.7,
) -> List[CodeSearchResult]:
"""Perform hybrid search combining semantic and keyword matching. Issue #207, #620.

Args:
query: Search query
language: Filter by language
max_results: Maximum results
semantic_weight: Weight for semantic results (0-1)

Returns:
Combined and ranked results
"""
"""Combine semantic and keyword matches (``semantic_weight`` in 0-1), ranked. Issue #207, #620."""
keyword_weight = 1.0 - semantic_weight
semantic_results = []
keyword_results = []

try:
semantic_results = await self._search_code_embeddings(query, language, max_results * 2)
except EmbeddingUnavailableError:
raise # never keyword-only results in place of a refused semantic half (#18185)
except Exception as e:
self.logger.warning("Hybrid: semantic search failed: %s", e)

Expand Down Expand Up @@ -718,6 +711,17 @@ async def _execute_search_by_type(
return await self._search_hybrid(query, language, max_results)
return await self._search_semantic(query, language, max_results)

async def _cached_results(self, cache_key: str, start_time: float) -> List[CodeSearchResult] | None:
"""Results cached under ``cache_key``, recording the hit in stats; None on a miss."""
cached_result = await asyncio.to_thread(self.redis_client.get, cache_key)
if not cached_result:
return None
self.stats.redis_cache_hit = True
results = [CodeSearchResult(**r) for r in json.loads(cached_result)]
self.stats.search_time_ms = (time.time() - start_time) * 1000
self.stats.results_count = len(results)
return results

async def search_code(
self,
query: str,
Expand All @@ -730,14 +734,9 @@ async def search_code(

try:
cache_key = self._get_search_cache_key(query, search_type, language)
cached_result = await asyncio.to_thread(self.redis_client.get, cache_key)

if cached_result:
self.stats.redis_cache_hit = True
results = [CodeSearchResult(**r) for r in json.loads(cached_result)]
self.stats.search_time_ms = (time.time() - start_time) * 1000
self.stats.results_count = len(results)
return results[:max_results]
cached = await self._cached_results(cache_key, start_time)
if cached is not None:
return cached[:max_results]

results = await self._execute_search_by_type(query, search_type, language, max_results)
serializable_results = self._serialize_results(results)
Expand All @@ -756,6 +755,8 @@ async def search_code(
self.stats.results_count = len(results)
return results

except EmbeddingUnavailableError:
raise # a refused query space is a failed search, never "no matches" (#18185)
except Exception as e:
self.logger.error("Code search failed: %s", e)
return []
Expand Down Expand Up @@ -1043,11 +1044,12 @@ async def _run_npu_semantic_search(
filters={"language": language} if language else None,
enable_npu_acceleration=self.npu_available,
)
raise_if_embedding_refused(metrics) # an outage is not "no matches" (#18185)
code_results = [await self._convert_npu_result(r) for r in search_results]
self.stats = SearchStats(
total_files_indexed=len(code_results),
search_time_ms=metrics.total_search_time_ms,
npu_acceleration_used=(metrics.device_used != "cpu_fallback"),
npu_acceleration_used=(metrics.device_used not in ("cpu_fallback", "knowledge_base")),
redis_cache_hit=False,
results_count=len(code_results),
)
Expand All @@ -1062,16 +1064,7 @@ async def _run_npu_semantic_search(
async def _convert_embedding_search_result(
self, sr: Dict[str, Any], query: str, device_used: str
) -> CodeSearchResult:
"""Convert a single embedding search result to CodeSearchResult. Issue #620.

Args:
sr: Raw search result dict with content, metadata, score
query: Original search query for metadata
device_used: Device identifier for metadata

Returns:
Converted CodeSearchResult object. Issue #620.
"""
"""Convert a single embedding search result dict to CodeSearchResult. Issue #620."""
metadata = sr["metadata"]
file_path = metadata.get("file_path", "unknown")
line_number = metadata.get("line_number", 0)
Expand All @@ -1096,13 +1089,7 @@ async def _convert_embedding_search_result(
def _update_embedding_search_stats(
self, results: List[CodeSearchResult], search_time: float, query_result: Any
) -> None:
"""Update search statistics after embedding search. Issue #620.

Args:
results: List of search results
search_time: Search duration in milliseconds
query_result: Query result object with device_used and cache_hit
"""
"""Update search statistics after embedding search (``search_time`` in ms). Issue #620."""
self.stats = SearchStats(
total_files_indexed=len(results),
search_time_ms=search_time,
Expand All @@ -1114,24 +1101,13 @@ def _update_embedding_search_stats(
async def _search_code_embeddings(
self, query: str, language: str | None, max_results: int
) -> List[CodeSearchResult]:
"""Search code embeddings using CodeBERT. Issue #207, #620.

Args:
query: Search query (natural language or code)
language: Filter by programming language
max_results: Maximum results to return

Returns:
List of CodeSearchResult
"""
"""Search the code index with a CodeBERT query, the model that built it. Issue #207, #620, #18185."""
start_time = time.time()

try:
embedding_generator = await get_code_embedding_generator()
query_result = await self._embed_code_query(query, language)
await self._ensure_search_engine_initialized()

query_result = await embedding_generator.generate_embedding(query, language or "python")

search_results = await self.npu_search_engine.search_code_embeddings(
query_embedding=query_result.embedding,
language=language,
Expand All @@ -1158,6 +1134,14 @@ async def _search_code_embeddings(
self.logger.error("Code embedding search failed: %s", e)
raise

async def _embed_code_query(self, query: str, language: str | None) -> Any:
"""Embed ``query`` with the code index's model, or refuse: never another model's vector (#18185)."""
try:
embedding_generator = await get_code_embedding_generator()
return await embedding_generator.generate_embedding(query, language or "python")
except Exception as exc:
raise EmbeddingUnavailableError(f"code index model unavailable ({type(exc).__name__}); refused") from exc

async def _fallback_word_matching(
self, query: str, language: str | None, max_results: int
) -> List[CodeSearchResult]:
Expand Down Expand Up @@ -1199,11 +1183,15 @@ async def _search_semantic(self, query: str, language: str | None, max_results:
"""
try:
return await self._search_code_embeddings(query, language, max_results)
except EmbeddingUnavailableError:
raise # a refusal reaches the caller; it is not degraded to another tier (#18185)
except Exception as e:
self.logger.warning("Code embedding search failed: %s, trying NPU search", e)

try:
return await self._run_npu_semantic_search(query, language, max_results)
except EmbeddingUnavailableError:
raise
except Exception as e:
self.logger.warning("NPU semantic search failed: %s, using word matching", e)

Expand Down
4 changes: 2 additions & 2 deletions autobot-backend/ai_hardware_accelerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,10 +685,10 @@ async def _generate_text_embedding(self, content: Any, device: Any) -> np.ndarra
from utils.semantic_chunker import get_semantic_chunker

chunker = get_semantic_chunker()
await chunker._initialize_model()
await chunker.load_embedding_model()

sentences = [content] if isinstance(content, str) else content
embeddings = await chunker._compute_sentence_embeddings_async(sentences)
embeddings = await chunker.embed_sentences(sentences)
raw_embedding = embeddings[0] if len(embeddings) > 0 else np.zeros(HardwareAcceleratorConfig.MINILM_OUTPUT_DIM)

if self.text_projection:
Expand Down
12 changes: 5 additions & 7 deletions autobot-backend/api/code_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
index_project,
search_codebase,
)
from api.embedding_refusal import reraise_http_or_refusal
from api.schemas_code import (
CodeAnalyticsRequest,
CodeSearchCacheClearResultResponse,
Expand Down Expand Up @@ -211,10 +212,7 @@ async def index_codebase(request: CodeSearchIndexRequest):
error_code_prefix="CODE_SEARCH",
)
async def search_code(request: CodeSearchRequest):
"""
Search through indexed code.

Supports multiple search types:
"""Search through indexed code. Supports multiple search types:
- semantic: AI-powered semantic similarity search (NPU-accelerated when available)
- exact: Exact string matching
- regex: Regular expression search
Expand Down Expand Up @@ -272,6 +270,7 @@ async def search_code(request: CodeSearchRequest):
)

except Exception as e:
reraise_http_or_refusal(e) # 400 stays 400; a refused query space is a 503 with its reason (#18185)
logger.error("Search endpoint error: %s", e)
raise HTTPException(status_code=500, detail="Search failed")

Expand Down Expand Up @@ -762,9 +761,7 @@ async def analyze_declarations(request: CodeAnalyticsRequest):
error_code_prefix="CODE_SEARCH",
)
async def find_code_duplicates(request: CodeAnalyticsRequest):
"""
Find potential code duplicates and similar patterns for refactoring opportunities.

"""Find potential code duplicates and similar patterns for refactoring opportunities.
Issue #665: Refactored from 85 lines to use extracted helper methods.
Issue #380: Uses module-level constant for patterns.

Expand Down Expand Up @@ -796,6 +793,7 @@ async def find_code_duplicates(request: CodeAnalyticsRequest):
)

except Exception as e:
reraise_http_or_refusal(e)
logger.error("Duplicate detection error: %s", e)
raise HTTPException(status_code=500, detail="Duplicate detection failed")

Expand Down
4 changes: 2 additions & 2 deletions autobot-backend/api/codebase_analytics/chromadb_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ async def _process_embedding_batch(
Issue #620: Extracted from _generate_batch_embeddings_fallback. Issue #620.
"""
try:
batch_embeddings = await chunker._compute_sentence_embeddings_async(batch_docs)
batch_embeddings = await chunker.embed_sentences(batch_docs)
result = _convert_embeddings_to_lists(batch_embeddings)

if total_docs > 100 and (batch_index + batch_size) % (batch_size * 5) == 0:
Expand Down Expand Up @@ -218,7 +218,7 @@ async def _generate_batch_embeddings_fallback(

try:
chunker = get_semantic_chunker()
await chunker._initialize_model()
await chunker.load_embedding_model()
except Exception as e:
logger.warning("Semantic chunker unavailable: %s", e)
return []
Expand Down
4 changes: 2 additions & 2 deletions autobot-backend/api/codebase_analytics/npu_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ async def _get_semantic_chunker():
from utils.semantic_chunker import get_semantic_chunker

_semantic_chunker_cache = get_semantic_chunker()
await _semantic_chunker_cache._initialize_model()
await _semantic_chunker_cache.load_embedding_model()
logger.info("Initialized semantic chunker for fallback embeddings")
except Exception as e:
logger.warning("Failed to initialize semantic chunker: %s", e)
Expand Down Expand Up @@ -229,7 +229,7 @@ async def _generate_fallback_embeddings(

try:
# Use async batch embedding method
batch_embeddings = await chunker._compute_sentence_embeddings_async(batch_docs)
batch_embeddings = await chunker.embed_sentences(batch_docs)

# Convert numpy arrays to lists
for emb in batch_embeddings:
Expand Down
20 changes: 20 additions & 0 deletions autobot-backend/api/embedding_refusal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2025-2026 mrveiss
# SPDX-License-Identifier: Apache-2.0
# AutoBot - AI-Powered Automation Platform
# Author: mrveiss
"""Turn an embedding refusal into a visible HTTP failure (#18185)."""

from fastapi import HTTPException

from autobot_shared.embedding_model import EmbeddingUnavailableError


def reraise_http_or_refusal(exc: Exception) -> None:
"""First call in a route's broad ``except``: an HTTPException passes through, a refusal is a 503.

A refused query space is a failed search with a reason, never "no results" and never a bare 500.
"""
if isinstance(exc, HTTPException):
raise exc
if isinstance(exc, EmbeddingUnavailableError):
raise HTTPException(status_code=503, detail=str(exc)) from exc
6 changes: 3 additions & 3 deletions autobot-backend/api/natural_language_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from fastapi import APIRouter, HTTPException, Request

from api.embedding_refusal import reraise_http_or_refusal
from api.schemas_knowledge import (
NLCodeExplanationResponse,
NLDomainsResponse,
Expand Down Expand Up @@ -1028,9 +1029,7 @@ def _convert_suggestions_to_dicts(suggestions: list) -> list:
error_code_prefix="NATURAL_LANGUAGE_SEARCH",
)
async def natural_language_search(request: NLSearchRequest):
"""
Search codebase using natural language queries.

"""Search codebase using natural language queries.
Issue #665: Refactored to use extracted helper functions.

Examples:
Expand Down Expand Up @@ -1077,6 +1076,7 @@ async def natural_language_search(request: NLSearchRequest):
)

except Exception as e:
reraise_http_or_refusal(e) # a refused query space is a 503 with its reason (#18185)
logger.error("Natural language search error: %s", e)
raise HTTPException(status_code=500, detail="Internal server error")

Expand Down
29 changes: 22 additions & 7 deletions autobot-backend/api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SearchPerformanceAnalyticsResponse,
)
from api.system_health import register_singleton_probe
from autobot_shared.embedding_model import EmbeddingUnavailableError, raise_if_embedding_refused
from autobot_shared.error_boundaries import ErrorCategory, with_error_handling
from autobot_shared.logging_manager import get_llm_logger

Expand Down Expand Up @@ -85,14 +86,17 @@ def _convert_metrics_to_api_format(metrics) -> dict:
"""
Convert metrics object to API response format (Issue #665: extracted helper).
"""
return {
data = {
"total_documents_searched": metrics.total_documents_searched,
"embedding_generation_time_ms": metrics.embedding_generation_time_ms,
"similarity_computation_time_ms": metrics.similarity_computation_time_ms,
"total_search_time_ms": metrics.total_search_time_ms,
"device_used": metrics.device_used,
"hardware_utilization": metrics.hardware_utilization,
}
if getattr(metrics, "error", None): # a failed search carries its reason (#18185)
data["error"] = metrics.error
return data


@router.post("/semantic", response_model=NPUSearchResponse)
Expand Down Expand Up @@ -127,6 +131,9 @@ async def semantic_search(request: NPUSearchRequest):
force_device=force_device,
)

if getattr(metrics, "error", None): # embedding refused: a failed search, never "no results" (#18185)
raise HTTPException(status_code=503, detail=metrics.error)

results_data = _convert_search_results_to_api_format(search_results)
metrics_data = _convert_metrics_to_api_format(metrics)
total_time = (time.time() - start_time) * 1000
Expand Down Expand Up @@ -314,6 +321,7 @@ async def test_npu_connectivity():
query="test connectivity", similarity_top_k=3, enable_npu_acceleration=True
)

raise_if_embedding_refused(test_metrics) # reported as a failed connectivity test (#18185)
return {
"connectivity": "success",
"npu_worker_url": search_engine.npu_worker_url,
Expand All @@ -325,12 +333,19 @@ async def test_npu_connectivity():

except Exception as e:
logger.warning("NPU connectivity test failed: %s", e)
return {
"connectivity": "failed",
"error": "Internal server error",
"fallback_available": True,
"timestamp": time.time(),
}
return _connectivity_failure(e)


def _connectivity_failure(exc: Exception) -> dict:
"""A failed connectivity test. A refused query space has no fallback by design (#18185); for any
other failure whether one exists is not determined, so ``fallback_available`` is None."""
refused = isinstance(exc, EmbeddingUnavailableError)
return {
"connectivity": "failed",
"error": str(exc) if refused else "Internal server error",
"fallback_available": False if refused else None,
"timestamp": time.time(),
}


def _evaluate_device_timing(
Expand Down
Loading
Loading