From 817460a03e8dfac2aafb6c37fa5903203140de87 Mon Sep 17 00:00:00 2001 From: Muhammed Emre Bayraktaroglu <69143179+memreo@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:22:14 +0200 Subject: [PATCH 1/3] feat: implement openai-compatible fallback for cloud mode --- .github/workflows/ci.yml | 4 ++ services/py-intelligence/app/func.py | 19 +++++++- services/py-intelligence/app/model.py | 34 +++++++++++++ services/py-intelligence/tests/test_main.py | 54 +++++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0040bd2..560e964 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: branches: ["main"] +env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + jobs: spring-services: name: Spring services diff --git a/services/py-intelligence/app/func.py b/services/py-intelligence/app/func.py index c3a9098..538e89f 100644 --- a/services/py-intelligence/app/func.py +++ b/services/py-intelligence/app/func.py @@ -16,6 +16,12 @@ "shortened": "Gemini", "cloud": True, }, + { + "name": "openai/gpt-oss-120b", + "provider": "openai", + "shortened": "GPT", + "cloud": True, + }, { "name": "Qwen/Qwen2.5-Coder-3B-Instruct-GGUF", "model_path": "/app/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf", @@ -159,7 +165,18 @@ def analyze( """ model = self.get_model_for_mode(mode) prompt = self._build_analysis_prompt(content, mode, use_rag, context, retrieved_docs or []) - raw_response = model.generate(prompt) + try: + raw_response = model.generate(prompt) + except Exception as e: + # Fallback to OpenAI GPT model if Gemini cloud provider fails + if mode == "cloud": + try: + fallback_model = next(m for m in self.models if m.cloud and m.provider == "openai") + raw_response = fallback_model.generate(prompt) + except StopIteration: + raise e + else: + raise e parsed_response = self._parse_model_response(raw_response) return self._normalize_response(parsed_response, retrieved_docs or [], use_rag) diff --git a/services/py-intelligence/app/model.py b/services/py-intelligence/app/model.py index 4fd9110..260d215 100644 --- a/services/py-intelligence/app/model.py +++ b/services/py-intelligence/app/model.py @@ -49,6 +49,11 @@ def _load(self): ) return self._client + if self.provider == "openai": + # Direct HTTP-based API calls; no heavy SDK setup required + self._client = True + return self._client + raise ValueError(f"Unknown provider: {self.provider}") def generate(self, prompt: str) -> str: @@ -72,6 +77,35 @@ def generate(self, prompt: str) -> str: ) return result["choices"][0]["message"]["content"] + if self.provider == "openai": + import os + import httpx + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise RuntimeError("OPENAI_API_KEY environment variable is not set.") + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + payload = { + "model": self.model_name, + "messages": [ + { + "role": "system", + "content": "You are DevPulse AI Insighter. Return valid JSON only.", + }, + {"role": "user", "content": prompt}, + ], + "temperature": 0.1, + } + api_base = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") + url = f"{api_base.rstrip('/')}/chat/completions" + response = httpx.post(url, json=payload, headers=headers, timeout=30.0) + response.raise_for_status() + result = response.json() + return result["choices"][0]["message"]["content"] + raise ValueError(f"Unknown provider: {self.provider}") def __str__(self) -> str: diff --git a/services/py-intelligence/tests/test_main.py b/services/py-intelligence/tests/test_main.py index 86e5e44..b91d751 100644 --- a/services/py-intelligence/tests/test_main.py +++ b/services/py-intelligence/tests/test_main.py @@ -375,3 +375,57 @@ def test_model_load_raises_value_error_for_unknown_provider() -> None: with pytest.raises(ValueError, match="Unknown provider: unknown_provider"): model._load() + + +def test_openai_model_load_and_generate() -> None: + """Verifies that the Model class successfully handles the OpenAI provider.""" + from app.model import Model + + openai_cfg = { + "name": "gpt-4o-mini", + "provider": "openai", + "shortened": "GPT", + "cloud": True, + } + model = Model(openai_cfg) + + # Test load + assert model._load() is True + + # Test generate + with patch("httpx.post") as mock_post, patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}): + mock_response = MagicMock() + mock_response.json.return_value = { + "choices": [{"message": {"content": '{"problem_type": "none"}'}}] + } + mock_post.return_value = mock_response + + res = model.generate("test prompt") + assert res == '{"problem_type": "none"}' + mock_post.assert_called_once() + + +@patch("app.apis.intelligence.models") +def test_analyze_fallback_to_openai(mock_models) -> None: + """Verifies that analyze falls back to OpenAI if Gemini fails in cloud mode.""" + from app.func import Intelligence + + intel = Intelligence() + + # Find the models in the instances + gemini_model = next(m for m in intel.models if m.provider == "google") + openai_model = next(m for m in intel.models if m.provider == "openai") + + # Mock Gemini model generate to raise an exception, and OpenAI model generate to return JSON + with patch.object(gemini_model, "generate", side_effect=Exception("Gemini Offline")), \ + patch.object(openai_model, "generate", return_value=json.dumps(LOCAL_ANALYSIS_RESPONSE)) as mock_openai_gen: + + res = intel.analyze( + content="test logs", + mode="cloud", + use_rag=False + ) + + assert res["problem_type"] == LOCAL_ANALYSIS_RESPONSE["problem_type"] + mock_openai_gen.assert_called_once() + From 2731bd5c40898cdccda403c28c0ad7573f98d78f Mon Sep 17 00:00:00 2001 From: Muhammed Emre Bayraktaroglu <69143179+memreo@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:30:06 +0200 Subject: [PATCH 2/3] style: apply black code formatting across source and test files --- services/py-intelligence/app/apis.py | 28 ++++-- services/py-intelligence/app/func.py | 23 ++++- services/py-intelligence/app/model.py | 13 ++- .../py-intelligence/app/utils/db_utils.py | 3 +- .../app/utils/embedding_utils.py | 16 +++- .../py-intelligence/tests/test_db_utils.py | 6 +- .../tests/test_embedding_utils.py | 20 +++- services/py-intelligence/tests/test_main.py | 96 +++++++++++++------ 8 files changed, 153 insertions(+), 52 deletions(-) diff --git a/services/py-intelligence/app/apis.py b/services/py-intelligence/app/apis.py index fec1d21..6a42bb8 100644 --- a/services/py-intelligence/app/apis.py +++ b/services/py-intelligence/app/apis.py @@ -71,7 +71,9 @@ def analyze( try: retrieved_docs = similarity_search(content, limit=3) except Exception as e: - raise HTTPException(status_code=500, detail=f"RAG retrieval failed: {str(e)}") + raise HTTPException( + status_code=500, detail=f"RAG retrieval failed: {str(e)}" + ) try: return intelligence.analyze( @@ -103,15 +105,21 @@ def create_rag_document( dict: The created RAG document. """ if not title.strip() or not content.strip(): - raise HTTPException(status_code=422, detail="'title' and 'content' must not be empty.") + raise HTTPException( + status_code=422, detail="'title' and 'content' must not be empty." + ) document = {"title": title, "content": content, "tags": tags} db_instance = get_db() if not db_instance.add_new_document(document): - raise HTTPException(status_code=500, detail="Failed to add document to the database.") + raise HTTPException( + status_code=500, detail="Failed to add document to the database." + ) if not create_all_embeddings(COLLECTION_NAME): - raise HTTPException(status_code=500, detail="Failed to update embeddings after adding document.") + raise HTTPException( + status_code=500, detail="Failed to update embeddings after adding document." + ) return { "id": str(document.get("_id", "")), @@ -131,10 +139,14 @@ def delete_rag_document(document_id: str) -> Response: """ success = get_db().delete_document(document_id) if not success: - raise HTTPException(status_code=500, detail="Failed to delete document from the database.") + raise HTTPException( + status_code=500, detail="Failed to delete document from the database." + ) success = create_all_embeddings(COLLECTION_NAME) if not success: - raise HTTPException(status_code=500, detail="Failed to update embeddings after deletion.") + raise HTTPException( + status_code=500, detail="Failed to update embeddings after deletion." + ) return Response(status_code=204) @@ -145,7 +157,9 @@ def delete_all_rag_documents() -> dict: """ success = get_db().delete_all_documents() if not success: - raise HTTPException(status_code=500, detail="Failed to delete all documents from the database.") + raise HTTPException( + status_code=500, detail="Failed to delete all documents from the database." + ) return {"message": "All documents deleted successfully"} diff --git a/services/py-intelligence/app/func.py b/services/py-intelligence/app/func.py index 538e89f..a6fb9a5 100644 --- a/services/py-intelligence/app/func.py +++ b/services/py-intelligence/app/func.py @@ -164,14 +164,18 @@ def analyze( the response cannot be parsed into a valid JSON structure. """ model = self.get_model_for_mode(mode) - prompt = self._build_analysis_prompt(content, mode, use_rag, context, retrieved_docs or []) + prompt = self._build_analysis_prompt( + content, mode, use_rag, context, retrieved_docs or [] + ) try: raw_response = model.generate(prompt) except Exception as e: # Fallback to OpenAI GPT model if Gemini cloud provider fails if mode == "cloud": try: - fallback_model = next(m for m in self.models if m.cloud and m.provider == "openai") + fallback_model = next( + m for m in self.models if m.cloud and m.provider == "openai" + ) raw_response = fallback_model.generate(prompt) except StopIteration: raise e @@ -215,7 +219,11 @@ def _build_analysis_prompt( ) rag_policy = self.prompts.get("rag_context_policy", "") incident_summary = self.prompts.get("log_analysis", "") - rag_block = self._format_rag_block(retrieved_docs) if use_rag and retrieved_docs else "[]" + rag_block = ( + self._format_rag_block(retrieved_docs) + if use_rag and retrieved_docs + else "[]" + ) return "\n".join( [ @@ -304,7 +312,10 @@ def _parse_model_response(self, raw_response: str) -> dict[str, Any]: raise ValueError("Model response was not valid JSON.") def _normalize_response( - self, response: dict[str, Any], retrieved_docs: list[dict[str, Any]], use_rag: bool + self, + response: dict[str, Any], + retrieved_docs: list[dict[str, Any]], + use_rag: bool, ) -> dict[str, Any]: """Ensures that the model's parsed JSON response is fully compliant and structurally sound. @@ -369,7 +380,9 @@ def _normalize_response( normalized["confidence"] = normalized["confidence"] or "low" return normalized - def _build_sources(self, retrieved_docs: list[dict[str, Any]]) -> list[dict[str, Any]]: + def _build_sources( + self, retrieved_docs: list[dict[str, Any]] + ) -> list[dict[str, Any]]: """Constructs a clean, safe, and truncated list of reference sources from retrieved docs. For each document, it extracts the ID, title, and tags, and clips the first 240 diff --git a/services/py-intelligence/app/model.py b/services/py-intelligence/app/model.py index 260d215..06f9f8a 100644 --- a/services/py-intelligence/app/model.py +++ b/services/py-intelligence/app/model.py @@ -27,7 +27,9 @@ def _load(self): try: from google import genai except ImportError as exc: - raise RuntimeError("Google GenAI dependencies are not installed.") from exc + raise RuntimeError( + "Google GenAI dependencies are not installed." + ) from exc self._client = genai.Client() return self._client @@ -36,7 +38,9 @@ def _load(self): try: from llama_cpp import Llama except ImportError as exc: - raise RuntimeError("llama-cpp-python is required for local GGUF inference.") from exc + raise RuntimeError( + "llama-cpp-python is required for local GGUF inference." + ) from exc if not self.model_path: raise RuntimeError("Local Qwen GGUF model_path is not configured.") @@ -60,7 +64,9 @@ def generate(self, prompt: str) -> str: client = self._load() if self.provider == "google": - response = client.models.generate_content(model=self.model_name, contents=prompt) + response = client.models.generate_content( + model=self.model_name, contents=prompt + ) return getattr(response, "text", "") or "" if self.provider == "qwen": @@ -80,6 +86,7 @@ def generate(self, prompt: str) -> str: if self.provider == "openai": import os import httpx + api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise RuntimeError("OPENAI_API_KEY environment variable is not set.") diff --git a/services/py-intelligence/app/utils/db_utils.py b/services/py-intelligence/app/utils/db_utils.py index cee6ae3..dd2fdd5 100644 --- a/services/py-intelligence/app/utils/db_utils.py +++ b/services/py-intelligence/app/utils/db_utils.py @@ -20,7 +20,8 @@ def __init__(self): collection_name = os.getenv("COLLECTION_NAME", "rag_documents") self.client = pymongo.MongoClient( - mongodb_uri, tlsCAFile=certifi.where() if "mongodb+srv" in mongodb_uri else None + mongodb_uri, + tlsCAFile=certifi.where() if "mongodb+srv" in mongodb_uri else None, ) self.db = self.client[db_name] self.collection = self.db[collection_name] diff --git a/services/py-intelligence/app/utils/embedding_utils.py b/services/py-intelligence/app/utils/embedding_utils.py index 0d592d2..a5b6e1c 100644 --- a/services/py-intelligence/app/utils/embedding_utils.py +++ b/services/py-intelligence/app/utils/embedding_utils.py @@ -18,7 +18,9 @@ def get_embedding(text: str) -> list[float]: global _model if _model is None: # nomic-embed-text-v1.5 produces 768-dim vectors, matching the nomic-embed-text dimension. - _model = SentenceTransformer("nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True) + _model = SentenceTransformer( + "nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True + ) # Generate the embedding embedding = _model.encode(text, convert_to_numpy=True) @@ -50,7 +52,9 @@ def create_all_embeddings(collection_name: str = None): documents = list(collection.find(query)) if not documents: - print(f"No documents without embeddings found in collection '{collection_name}'") + print( + f"No documents without embeddings found in collection '{collection_name}'" + ) return True print(f"Found {len(documents)} documents to embed in '{collection_name}'") @@ -64,7 +68,9 @@ def create_all_embeddings(collection_name: str = None): continue embedding = get_embedding(content) - collection.update_one({"_id": document["_id"]}, {"$set": {"embedding": embedding}}) + collection.update_one( + {"_id": document["_id"]}, {"$set": {"embedding": embedding}} + ) count += 1 except Exception as e: print(f"Error processing document {document['_id']}: {e}") @@ -74,7 +80,9 @@ def create_all_embeddings(collection_name: str = None): return True -def similarity_search(query: str, limit: int = 5, collection_name: str = None) -> list[dict]: +def similarity_search( + query: str, limit: int = 5, collection_name: str = None +) -> list[dict]: """ Perform a vector similarity search in MongoDB using a text query. diff --git a/services/py-intelligence/tests/test_db_utils.py b/services/py-intelligence/tests/test_db_utils.py index aea8edf..fc86316 100644 --- a/services/py-intelligence/tests/test_db_utils.py +++ b/services/py-intelligence/tests/test_db_utils.py @@ -11,7 +11,11 @@ def mock_db_config(): """ with patch.dict( "os.environ", - {"MONGODB_URI": "mongodb://localhost:27017", "DB_NAME": "test_db", "COLLECTION_NAME": "test_collection"}, + { + "MONGODB_URI": "mongodb://localhost:27017", + "DB_NAME": "test_db", + "COLLECTION_NAME": "test_collection", + }, ): yield diff --git a/services/py-intelligence/tests/test_embedding_utils.py b/services/py-intelligence/tests/test_embedding_utils.py index 620c978..2a57945 100644 --- a/services/py-intelligence/tests/test_embedding_utils.py +++ b/services/py-intelligence/tests/test_embedding_utils.py @@ -2,7 +2,11 @@ import pytest import numpy as np from unittest.mock import MagicMock, patch -from app.utils.embedding_utils import get_embedding, create_all_embeddings, similarity_search +from app.utils.embedding_utils import ( + get_embedding, + create_all_embeddings, + similarity_search, +) @pytest.fixture @@ -39,7 +43,9 @@ def test_get_embedding(mock_sentence_transformer): embedding = get_embedding("test text") assert embedding == [0.1, 0.2, 0.3] - mock_sentence_transformer.encode.assert_called_once_with("test text", convert_to_numpy=True) + mock_sentence_transformer.encode.assert_called_once_with( + "test text", convert_to_numpy=True + ) def test_create_all_embeddings_success(mock_db, mock_sentence_transformer): @@ -52,7 +58,10 @@ def test_create_all_embeddings_success(mock_db, mock_sentence_transformer): mock_db.return_value.db = {collection_name: mock_collection} # Mock documents to be processed - mock_collection.find.return_value = [{"_id": "1", "content": "text 1"}, {"_id": "2", "content": "text 2"}] + mock_collection.find.return_value = [ + {"_id": "1", "content": "text 1"}, + {"_id": "2", "content": "text 2"}, + ] # Mock embedding generation mock_sentence_transformer.encode.return_value = np.array([0.5, 0.6]) @@ -93,7 +102,10 @@ def test_similarity_search(mock_db, mock_sentence_transformer): mock_db.return_value.db = {collection_name: mock_collection} # Mock aggregation results - mock_collection.aggregate.return_value = [{"title": "Result 1", "embedding": [0.1]}, {"title": "Result 2"}] + mock_collection.aggregate.return_value = [ + {"title": "Result 1", "embedding": [0.1]}, + {"title": "Result 2"}, + ] # Mock query embedding mock_sentence_transformer.encode.return_value = np.array([0.9, 0.8]) diff --git a/services/py-intelligence/tests/test_main.py b/services/py-intelligence/tests/test_main.py index b91d751..744505d 100644 --- a/services/py-intelligence/tests/test_main.py +++ b/services/py-intelligence/tests/test_main.py @@ -33,7 +33,9 @@ def test_local_model_matches_docker_gguf() -> None: local_model = next(model for model in AVAILABLE_MODELS if not model["cloud"]) assert local_model["name"] == "Qwen/Qwen2.5-Coder-3B-Instruct-GGUF" - assert local_model["model_path"] == "/app/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf" + assert ( + local_model["model_path"] == "/app/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf" + ) @patch("app.apis.intelligence.get_model_for_mode") @@ -80,7 +82,13 @@ def test_analyze_endpoint_uses_rag(mock_search, mock_get_model) -> None: mock_model.generate.return_value = json.dumps( { **LOCAL_ANALYSIS_RESPONSE, - "sources": [{"id": "mock_id", "title": "Database timeout fix", "tags": ["db", "timeout"]}], + "sources": [ + { + "id": "mock_id", + "title": "Database timeout fix", + "tags": ["db", "timeout"], + } + ], "confidence": "high", } ) @@ -88,14 +96,20 @@ def test_analyze_endpoint_uses_rag(mock_search, mock_get_model) -> None: response = client.post( "/api/v1/analyze", - json={"content": "Deployment failed: database connection timeout", "mode": "local", "use_rag": True}, + json={ + "content": "Deployment failed: database connection timeout", + "mode": "local", + "use_rag": True, + }, ) assert response.status_code == 200 body = response.json() assert body["sources"][0]["title"] == "Database timeout fix" assert body["confidence"] == "high" - mock_search.assert_called_once_with("Deployment failed: database connection timeout", limit=3) + mock_search.assert_called_once_with( + "Deployment failed: database connection timeout", limit=3 + ) assert "Database timeout fix" in mock_model.generate.call_args.args[0] @@ -157,7 +171,9 @@ def test_parse_model_response_handling() -> None: assert intel._parse_model_response('```json\n{"test": 123}\n```') == {"test": 123} # Nested JSON in text - assert intel._parse_model_response('Some text {\n "test": 123\n} other text') == {"test": 123} + assert intel._parse_model_response('Some text {\n "test": 123\n} other text') == { + "test": 123 + } # Empty response raises ValueError with pytest.raises(ValueError, match="Model returned an empty response."): @@ -168,9 +184,9 @@ def test_parse_model_response_handling() -> None: intel._parse_model_response("not a json string") # Unescaped double quotes in JSON string - assert intel._parse_model_response('{"evidence": ["Module \'"./App.css"\' has no default export."]}') == { - "evidence": ["Module '\"./App.css\"' has no default export."] - } + assert intel._parse_model_response( + '{"evidence": ["Module \'"./App.css"\' has no default export."]}' + ) == {"evidence": ["Module '\"./App.css\"' has no default export."]} def test_normalize_response_defaults_and_rag_sources() -> None: @@ -188,7 +204,9 @@ def test_normalize_response_defaults_and_rag_sources() -> None: "severity": None, "summary": "Server down", } - normalized = intel._normalize_response(incomplete_res, retrieved_docs=[], use_rag=False) + normalized = intel._normalize_response( + incomplete_res, retrieved_docs=[], use_rag=False + ) assert normalized["problem_type"] == "infra_issue" assert normalized["severity"] == "unknown" @@ -201,13 +219,25 @@ def test_normalize_response_defaults_and_rag_sources() -> None: assert normalized["confidence"] == "low" # RAG enabled, empty sources: should populate sources from retrieved docs - retrieved = [{"_id": "1a", "title": "Doc 1", "tags": ["tag1"], "content": "This is a detailed snippet of Doc 1."}] - normalized_rag = intel._normalize_response({"problem_type": "x"}, retrieved_docs=retrieved, use_rag=True) + retrieved = [ + { + "_id": "1a", + "title": "Doc 1", + "tags": ["tag1"], + "content": "This is a detailed snippet of Doc 1.", + } + ] + normalized_rag = intel._normalize_response( + {"problem_type": "x"}, retrieved_docs=retrieved, use_rag=True + ) assert len(normalized_rag["sources"]) == 1 assert normalized_rag["sources"][0]["id"] == "1a" assert normalized_rag["sources"][0]["title"] == "Doc 1" - assert normalized_rag["sources"][0]["snippet"] == "This is a detailed snippet of Doc 1." + assert ( + normalized_rag["sources"][0]["snippet"] + == "This is a detailed snippet of Doc 1." + ) def test_removed_endpoints_return_404() -> None: @@ -249,7 +279,9 @@ def mock_add(doc): @patch("app.apis.create_all_embeddings") @patch("app.apis.db") -def test_delete_rag_document_endpoint_is_mapped(mock_db, mock_create_embeddings) -> None: +def test_delete_rag_document_endpoint_is_mapped( + mock_db, mock_create_embeddings +) -> None: """Verifies that deleting a document via /api/v1/rag/documents/{id} correctly removes it and updates embeddings.""" mock_db.delete_document.return_value = True mock_create_embeddings.return_value = True @@ -263,8 +295,12 @@ def test_delete_rag_document_endpoint_is_mapped(mock_db, mock_create_embeddings) @patch("app.apis.similarity_search") def test_rag_search_success(mock_search) -> None: """Verifies that semantic searching via /api/v1/rag/search returns ranked search results from embedding_utils.""" - mock_search.return_value = [{"_id": "mock_id", "title": "Mock Title", "content": "Mock Content", "tags": []}] - response = client.post("/api/v1/rag/search", json={"query": "crash loop", "limit": 3}) + mock_search.return_value = [ + {"_id": "mock_id", "title": "Mock Title", "content": "Mock Content", "tags": []} + ] + response = client.post( + "/api/v1/rag/search", json={"query": "crash loop", "limit": 3} + ) assert response.status_code == 200 res_data = response.json() @@ -322,7 +358,9 @@ def test_model_load_raises_runtime_error_on_import_error_google() -> None: model = Model(google_cfg) with patch.dict("sys.modules", {"google": None}): - with pytest.raises(RuntimeError, match="Google GenAI dependencies are not installed."): + with pytest.raises( + RuntimeError, match="Google GenAI dependencies are not installed." + ): model._load() @@ -340,7 +378,9 @@ def test_model_load_raises_runtime_error_on_import_error_qwen() -> None: model = Model(qwen_cfg) with patch.dict("sys.modules", {"llama_cpp": None}): - with pytest.raises(RuntimeError, match="llama-cpp-python is required for local GGUF inference."): + with pytest.raises( + RuntimeError, match="llama-cpp-python is required for local GGUF inference." + ): model._load() @@ -357,7 +397,9 @@ def test_model_load_raises_runtime_error_if_qwen_path_missing() -> None: model = Model(qwen_cfg) with patch("builtins.__import__"): - with pytest.raises(RuntimeError, match="Local Qwen GGUF model_path is not configured."): + with pytest.raises( + RuntimeError, match="Local Qwen GGUF model_path is not configured." + ): model._load() @@ -393,7 +435,9 @@ def test_openai_model_load_and_generate() -> None: assert model._load() is True # Test generate - with patch("httpx.post") as mock_post, patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}): + with patch("httpx.post") as mock_post, patch.dict( + "os.environ", {"OPENAI_API_KEY": "test-key"} + ): mock_response = MagicMock() mock_response.json.return_value = { "choices": [{"message": {"content": '{"problem_type": "none"}'}}] @@ -417,15 +461,13 @@ def test_analyze_fallback_to_openai(mock_models) -> None: openai_model = next(m for m in intel.models if m.provider == "openai") # Mock Gemini model generate to raise an exception, and OpenAI model generate to return JSON - with patch.object(gemini_model, "generate", side_effect=Exception("Gemini Offline")), \ - patch.object(openai_model, "generate", return_value=json.dumps(LOCAL_ANALYSIS_RESPONSE)) as mock_openai_gen: + with patch.object( + gemini_model, "generate", side_effect=Exception("Gemini Offline") + ), patch.object( + openai_model, "generate", return_value=json.dumps(LOCAL_ANALYSIS_RESPONSE) + ) as mock_openai_gen: - res = intel.analyze( - content="test logs", - mode="cloud", - use_rag=False - ) + res = intel.analyze(content="test logs", mode="cloud", use_rag=False) assert res["problem_type"] == LOCAL_ANALYSIS_RESPONSE["problem_type"] mock_openai_gen.assert_called_once() - From 4be332b7a36d879afcea978dd2212a7419b5f023 Mon Sep 17 00:00:00 2001 From: Muhammed Emre Bayraktaroglu <69143179+memreo@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:28:57 +0200 Subject: [PATCH 3/3] refactor: unify multi-line statements into single-line format across application and test files --- services/py-intelligence/app/apis.py | 28 ++------ services/py-intelligence/app/func.py | 18 ++--- services/py-intelligence/app/model.py | 12 +--- .../app/utils/embedding_utils.py | 16 ++--- .../tests/test_embedding_utils.py | 4 +- services/py-intelligence/tests/test_main.py | 72 ++++++------------- 6 files changed, 40 insertions(+), 110 deletions(-) diff --git a/services/py-intelligence/app/apis.py b/services/py-intelligence/app/apis.py index 6a42bb8..fec1d21 100644 --- a/services/py-intelligence/app/apis.py +++ b/services/py-intelligence/app/apis.py @@ -71,9 +71,7 @@ def analyze( try: retrieved_docs = similarity_search(content, limit=3) except Exception as e: - raise HTTPException( - status_code=500, detail=f"RAG retrieval failed: {str(e)}" - ) + raise HTTPException(status_code=500, detail=f"RAG retrieval failed: {str(e)}") try: return intelligence.analyze( @@ -105,21 +103,15 @@ def create_rag_document( dict: The created RAG document. """ if not title.strip() or not content.strip(): - raise HTTPException( - status_code=422, detail="'title' and 'content' must not be empty." - ) + raise HTTPException(status_code=422, detail="'title' and 'content' must not be empty.") document = {"title": title, "content": content, "tags": tags} db_instance = get_db() if not db_instance.add_new_document(document): - raise HTTPException( - status_code=500, detail="Failed to add document to the database." - ) + raise HTTPException(status_code=500, detail="Failed to add document to the database.") if not create_all_embeddings(COLLECTION_NAME): - raise HTTPException( - status_code=500, detail="Failed to update embeddings after adding document." - ) + raise HTTPException(status_code=500, detail="Failed to update embeddings after adding document.") return { "id": str(document.get("_id", "")), @@ -139,14 +131,10 @@ def delete_rag_document(document_id: str) -> Response: """ success = get_db().delete_document(document_id) if not success: - raise HTTPException( - status_code=500, detail="Failed to delete document from the database." - ) + raise HTTPException(status_code=500, detail="Failed to delete document from the database.") success = create_all_embeddings(COLLECTION_NAME) if not success: - raise HTTPException( - status_code=500, detail="Failed to update embeddings after deletion." - ) + raise HTTPException(status_code=500, detail="Failed to update embeddings after deletion.") return Response(status_code=204) @@ -157,9 +145,7 @@ def delete_all_rag_documents() -> dict: """ success = get_db().delete_all_documents() if not success: - raise HTTPException( - status_code=500, detail="Failed to delete all documents from the database." - ) + raise HTTPException(status_code=500, detail="Failed to delete all documents from the database.") return {"message": "All documents deleted successfully"} diff --git a/services/py-intelligence/app/func.py b/services/py-intelligence/app/func.py index a6fb9a5..9521f04 100644 --- a/services/py-intelligence/app/func.py +++ b/services/py-intelligence/app/func.py @@ -164,18 +164,14 @@ def analyze( the response cannot be parsed into a valid JSON structure. """ model = self.get_model_for_mode(mode) - prompt = self._build_analysis_prompt( - content, mode, use_rag, context, retrieved_docs or [] - ) + prompt = self._build_analysis_prompt(content, mode, use_rag, context, retrieved_docs or []) try: raw_response = model.generate(prompt) except Exception as e: # Fallback to OpenAI GPT model if Gemini cloud provider fails if mode == "cloud": try: - fallback_model = next( - m for m in self.models if m.cloud and m.provider == "openai" - ) + fallback_model = next(m for m in self.models if m.cloud and m.provider == "openai") raw_response = fallback_model.generate(prompt) except StopIteration: raise e @@ -219,11 +215,7 @@ def _build_analysis_prompt( ) rag_policy = self.prompts.get("rag_context_policy", "") incident_summary = self.prompts.get("log_analysis", "") - rag_block = ( - self._format_rag_block(retrieved_docs) - if use_rag and retrieved_docs - else "[]" - ) + rag_block = self._format_rag_block(retrieved_docs) if use_rag and retrieved_docs else "[]" return "\n".join( [ @@ -380,9 +372,7 @@ def _normalize_response( normalized["confidence"] = normalized["confidence"] or "low" return normalized - def _build_sources( - self, retrieved_docs: list[dict[str, Any]] - ) -> list[dict[str, Any]]: + def _build_sources(self, retrieved_docs: list[dict[str, Any]]) -> list[dict[str, Any]]: """Constructs a clean, safe, and truncated list of reference sources from retrieved docs. For each document, it extracts the ID, title, and tags, and clips the first 240 diff --git a/services/py-intelligence/app/model.py b/services/py-intelligence/app/model.py index 06f9f8a..0648f4e 100644 --- a/services/py-intelligence/app/model.py +++ b/services/py-intelligence/app/model.py @@ -27,9 +27,7 @@ def _load(self): try: from google import genai except ImportError as exc: - raise RuntimeError( - "Google GenAI dependencies are not installed." - ) from exc + raise RuntimeError("Google GenAI dependencies are not installed.") from exc self._client = genai.Client() return self._client @@ -38,9 +36,7 @@ def _load(self): try: from llama_cpp import Llama except ImportError as exc: - raise RuntimeError( - "llama-cpp-python is required for local GGUF inference." - ) from exc + raise RuntimeError("llama-cpp-python is required for local GGUF inference.") from exc if not self.model_path: raise RuntimeError("Local Qwen GGUF model_path is not configured.") @@ -64,9 +60,7 @@ def generate(self, prompt: str) -> str: client = self._load() if self.provider == "google": - response = client.models.generate_content( - model=self.model_name, contents=prompt - ) + response = client.models.generate_content(model=self.model_name, contents=prompt) return getattr(response, "text", "") or "" if self.provider == "qwen": diff --git a/services/py-intelligence/app/utils/embedding_utils.py b/services/py-intelligence/app/utils/embedding_utils.py index a5b6e1c..0d592d2 100644 --- a/services/py-intelligence/app/utils/embedding_utils.py +++ b/services/py-intelligence/app/utils/embedding_utils.py @@ -18,9 +18,7 @@ def get_embedding(text: str) -> list[float]: global _model if _model is None: # nomic-embed-text-v1.5 produces 768-dim vectors, matching the nomic-embed-text dimension. - _model = SentenceTransformer( - "nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True - ) + _model = SentenceTransformer("nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True) # Generate the embedding embedding = _model.encode(text, convert_to_numpy=True) @@ -52,9 +50,7 @@ def create_all_embeddings(collection_name: str = None): documents = list(collection.find(query)) if not documents: - print( - f"No documents without embeddings found in collection '{collection_name}'" - ) + print(f"No documents without embeddings found in collection '{collection_name}'") return True print(f"Found {len(documents)} documents to embed in '{collection_name}'") @@ -68,9 +64,7 @@ def create_all_embeddings(collection_name: str = None): continue embedding = get_embedding(content) - collection.update_one( - {"_id": document["_id"]}, {"$set": {"embedding": embedding}} - ) + collection.update_one({"_id": document["_id"]}, {"$set": {"embedding": embedding}}) count += 1 except Exception as e: print(f"Error processing document {document['_id']}: {e}") @@ -80,9 +74,7 @@ def create_all_embeddings(collection_name: str = None): return True -def similarity_search( - query: str, limit: int = 5, collection_name: str = None -) -> list[dict]: +def similarity_search(query: str, limit: int = 5, collection_name: str = None) -> list[dict]: """ Perform a vector similarity search in MongoDB using a text query. diff --git a/services/py-intelligence/tests/test_embedding_utils.py b/services/py-intelligence/tests/test_embedding_utils.py index 2a57945..8e69f90 100644 --- a/services/py-intelligence/tests/test_embedding_utils.py +++ b/services/py-intelligence/tests/test_embedding_utils.py @@ -43,9 +43,7 @@ def test_get_embedding(mock_sentence_transformer): embedding = get_embedding("test text") assert embedding == [0.1, 0.2, 0.3] - mock_sentence_transformer.encode.assert_called_once_with( - "test text", convert_to_numpy=True - ) + mock_sentence_transformer.encode.assert_called_once_with("test text", convert_to_numpy=True) def test_create_all_embeddings_success(mock_db, mock_sentence_transformer): diff --git a/services/py-intelligence/tests/test_main.py b/services/py-intelligence/tests/test_main.py index 744505d..f72dfd8 100644 --- a/services/py-intelligence/tests/test_main.py +++ b/services/py-intelligence/tests/test_main.py @@ -33,9 +33,7 @@ def test_local_model_matches_docker_gguf() -> None: local_model = next(model for model in AVAILABLE_MODELS if not model["cloud"]) assert local_model["name"] == "Qwen/Qwen2.5-Coder-3B-Instruct-GGUF" - assert ( - local_model["model_path"] == "/app/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf" - ) + assert local_model["model_path"] == "/app/models/qwen2.5-coder-3b-instruct-q4_k_m.gguf" @patch("app.apis.intelligence.get_model_for_mode") @@ -107,9 +105,7 @@ def test_analyze_endpoint_uses_rag(mock_search, mock_get_model) -> None: body = response.json() assert body["sources"][0]["title"] == "Database timeout fix" assert body["confidence"] == "high" - mock_search.assert_called_once_with( - "Deployment failed: database connection timeout", limit=3 - ) + mock_search.assert_called_once_with("Deployment failed: database connection timeout", limit=3) assert "Database timeout fix" in mock_model.generate.call_args.args[0] @@ -171,9 +167,7 @@ def test_parse_model_response_handling() -> None: assert intel._parse_model_response('```json\n{"test": 123}\n```') == {"test": 123} # Nested JSON in text - assert intel._parse_model_response('Some text {\n "test": 123\n} other text') == { - "test": 123 - } + assert intel._parse_model_response('Some text {\n "test": 123\n} other text') == {"test": 123} # Empty response raises ValueError with pytest.raises(ValueError, match="Model returned an empty response."): @@ -184,9 +178,9 @@ def test_parse_model_response_handling() -> None: intel._parse_model_response("not a json string") # Unescaped double quotes in JSON string - assert intel._parse_model_response( - '{"evidence": ["Module \'"./App.css"\' has no default export."]}' - ) == {"evidence": ["Module '\"./App.css\"' has no default export."]} + assert intel._parse_model_response('{"evidence": ["Module \'"./App.css"\' has no default export."]}') == { + "evidence": ["Module '\"./App.css\"' has no default export."] + } def test_normalize_response_defaults_and_rag_sources() -> None: @@ -204,9 +198,7 @@ def test_normalize_response_defaults_and_rag_sources() -> None: "severity": None, "summary": "Server down", } - normalized = intel._normalize_response( - incomplete_res, retrieved_docs=[], use_rag=False - ) + normalized = intel._normalize_response(incomplete_res, retrieved_docs=[], use_rag=False) assert normalized["problem_type"] == "infra_issue" assert normalized["severity"] == "unknown" @@ -227,17 +219,12 @@ def test_normalize_response_defaults_and_rag_sources() -> None: "content": "This is a detailed snippet of Doc 1.", } ] - normalized_rag = intel._normalize_response( - {"problem_type": "x"}, retrieved_docs=retrieved, use_rag=True - ) + normalized_rag = intel._normalize_response({"problem_type": "x"}, retrieved_docs=retrieved, use_rag=True) assert len(normalized_rag["sources"]) == 1 assert normalized_rag["sources"][0]["id"] == "1a" assert normalized_rag["sources"][0]["title"] == "Doc 1" - assert ( - normalized_rag["sources"][0]["snippet"] - == "This is a detailed snippet of Doc 1." - ) + assert normalized_rag["sources"][0]["snippet"] == "This is a detailed snippet of Doc 1." def test_removed_endpoints_return_404() -> None: @@ -279,9 +266,7 @@ def mock_add(doc): @patch("app.apis.create_all_embeddings") @patch("app.apis.db") -def test_delete_rag_document_endpoint_is_mapped( - mock_db, mock_create_embeddings -) -> None: +def test_delete_rag_document_endpoint_is_mapped(mock_db, mock_create_embeddings) -> None: """Verifies that deleting a document via /api/v1/rag/documents/{id} correctly removes it and updates embeddings.""" mock_db.delete_document.return_value = True mock_create_embeddings.return_value = True @@ -295,12 +280,8 @@ def test_delete_rag_document_endpoint_is_mapped( @patch("app.apis.similarity_search") def test_rag_search_success(mock_search) -> None: """Verifies that semantic searching via /api/v1/rag/search returns ranked search results from embedding_utils.""" - mock_search.return_value = [ - {"_id": "mock_id", "title": "Mock Title", "content": "Mock Content", "tags": []} - ] - response = client.post( - "/api/v1/rag/search", json={"query": "crash loop", "limit": 3} - ) + mock_search.return_value = [{"_id": "mock_id", "title": "Mock Title", "content": "Mock Content", "tags": []}] + response = client.post("/api/v1/rag/search", json={"query": "crash loop", "limit": 3}) assert response.status_code == 200 res_data = response.json() @@ -358,9 +339,7 @@ def test_model_load_raises_runtime_error_on_import_error_google() -> None: model = Model(google_cfg) with patch.dict("sys.modules", {"google": None}): - with pytest.raises( - RuntimeError, match="Google GenAI dependencies are not installed." - ): + with pytest.raises(RuntimeError, match="Google GenAI dependencies are not installed."): model._load() @@ -378,9 +357,7 @@ def test_model_load_raises_runtime_error_on_import_error_qwen() -> None: model = Model(qwen_cfg) with patch.dict("sys.modules", {"llama_cpp": None}): - with pytest.raises( - RuntimeError, match="llama-cpp-python is required for local GGUF inference." - ): + with pytest.raises(RuntimeError, match="llama-cpp-python is required for local GGUF inference."): model._load() @@ -397,9 +374,7 @@ def test_model_load_raises_runtime_error_if_qwen_path_missing() -> None: model = Model(qwen_cfg) with patch("builtins.__import__"): - with pytest.raises( - RuntimeError, match="Local Qwen GGUF model_path is not configured." - ): + with pytest.raises(RuntimeError, match="Local Qwen GGUF model_path is not configured."): model._load() @@ -435,13 +410,9 @@ def test_openai_model_load_and_generate() -> None: assert model._load() is True # Test generate - with patch("httpx.post") as mock_post, patch.dict( - "os.environ", {"OPENAI_API_KEY": "test-key"} - ): + with patch("httpx.post") as mock_post, patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}): mock_response = MagicMock() - mock_response.json.return_value = { - "choices": [{"message": {"content": '{"problem_type": "none"}'}}] - } + mock_response.json.return_value = {"choices": [{"message": {"content": '{"problem_type": "none"}'}}]} mock_post.return_value = mock_response res = model.generate("test prompt") @@ -461,11 +432,10 @@ def test_analyze_fallback_to_openai(mock_models) -> None: openai_model = next(m for m in intel.models if m.provider == "openai") # Mock Gemini model generate to raise an exception, and OpenAI model generate to return JSON - with patch.object( - gemini_model, "generate", side_effect=Exception("Gemini Offline") - ), patch.object( - openai_model, "generate", return_value=json.dumps(LOCAL_ANALYSIS_RESPONSE) - ) as mock_openai_gen: + with ( + patch.object(gemini_model, "generate", side_effect=Exception("Gemini Offline")), + patch.object(openai_model, "generate", return_value=json.dumps(LOCAL_ANALYSIS_RESPONSE)) as mock_openai_gen, + ): res = intel.analyze(content="test logs", mode="cloud", use_rag=False)