Skip to content

Feature/ai/rag & prompt extraction - #2

Merged
Mohammed-Balkhair-hub merged 4 commits into
devfrom
feature/ai/rag_&_Prompt_extraction
Jan 30, 2026
Merged

Feature/ai/rag & prompt extraction#2
Mohammed-Balkhair-hub merged 4 commits into
devfrom
feature/ai/rag_&_Prompt_extraction

Conversation

@Mohammed-Balkhair-hub

@Mohammed-Balkhair-hub Mohammed-Balkhair-hub commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator

this branch completed setup governance framework controls extraction endpoint as well as establishing Vector DB for for framework files , controls & polices

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI-powered compliance framework extraction from PDFs via CLI and API endpoints.
    • Introduced applicant document evaluation against compliance frameworks.
    • Added semantic search and retrieval for control details with PDF chunk context.
    • Implemented health check endpoint for service monitoring.
    • Support for multi-section framework setup with batch PDF processing.
  • Documentation

    • Added comprehensive architecture documentation and module guides.
    • Included API endpoint documentation and usage examples.
    • Provided quick-start instructions for CLI and server deployment.

✏️ Tip: You can customize this high-level summary in your review settings.

chunk all frameworks files to build rag + extract controls from chosen framewirk files.
refine  controls extractor
enhance controls extraction + leverage controls in rag chunks to optimize retrieval for evaluation agent for later
@coderabbitai

coderabbitai Bot commented Jan 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a complete AI service for compliance framework evaluation with PDF extraction, LLM-based control extraction, vector embeddings, and retrieval-augmented generation (RAG). It includes API and CLI entry points, comprehensive documentation, and supporting infrastructure for managing frameworks and evaluating applicants.

Changes

Cohort / File(s) Summary
Project Configuration
.gitignore, pyproject.toml, README.md
Adds Python project metadata, dependencies (OpenAI, FastAPI, Qdrant, transformers), and development setup documentation.
Entry Points
main.py, run.py
Introduces CLI entry point for framework setup via setup_framework() and FastAPI server launcher via uvicorn.
API Application
src/api/app.py
Establishes FastAPI app with CORS configuration, routers for health and frameworks, and exception handlers for ExtractionError and ValueError.
API Routers
src/api/routers/health.py, src/api/routers/frameworks.py
Adds health check endpoint and POST /api/v1/frameworks/setup endpoint that validates multipart form input (framework_name, section_names, files) and delegates to FrameworkService.
Core Extraction & Evaluation
src/core/framework_extraction.py, src/core/evaluator.py
Implements extract_controls_from_framework() and extract_controls_from_pdfs() using OpenRouter API with JSON schema enforcement; adds evaluate_applicant() for compliance assessment against controls.
Text Processing
src/processing/pdf_parser.py, src/processing/text_chunker.py
Provides extract_text_from_pdf() using pdfplumber and chunk_text()/chunk_text_by_sentences() with semantic boundary preservation and chunk validation.
RAG System
src/rag/ingestion.py, src/rag/retrieval.py, src/rag/_shared.py
Implements index_framework() to index JSON controls and PDF chunks into Qdrant; adds retrieve_control_details() for semantic search with tool schema; provides shared embedder singleton.
Embeddings & Vector Store
src/embeddings/gemma_embedder.py, src/embeddings/qdrant_manager.py, src/embeddings/haystack_retriever.py
Introduces GemmaEmbedder wrapper for sentence-transformers; provides Qdrant CRUD operations (initialize, add_documents, search_similar, fetch_by_filter); adds Haystack-Qdrant retrieval pipeline.
Services & Utilities
src/services/framework_service.py, src/utils/framework_utils.py
Adds FrameworkService orchestrator for framework setup workflow; provides path utilities (save_extraction_json, get_input_paths, list_framework_jsons, get_vector_db_pdf_paths).
Package Initialization & Documentation
src/__init__.py, src/api/__init__.py, src/core/__init__.py, src/processing/__init__.py, src/rag/__init__.py, src/services/__init__.py, src/utils/__init__.py, src/embeddings/__init__.py, src/.../README.md, src/ARCHITECTURE.md
Establishes public API surface across all modules; documents architecture, module responsibilities, data flows, and storage conventions.
Test & Legacy Cleanup
test.ipynb, services/test/main.py, services/test/pyproject.toml
Adds Jupyter notebook demonstrating RAG retrieval usage; removes deprecated test service placeholder.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as FastAPI
    participant FrameworkSvc as FrameworkService
    participant PDFParser as PDF Parser
    participant Extractor as LLM Extractor
    participant Storage as File Storage
    participant RAG as RAG Indexer
    
    Client->>API: POST /api/v1/frameworks/setup<br/>(framework_name, files)
    API->>FrameworkSvc: setup_framework(name, pdf_sections)
    FrameworkSvc->>PDFParser: extract_text_from_pdf()
    PDFParser-->>FrameworkSvc: pdf_text
    FrameworkSvc->>Extractor: extract_controls_from_pdfs()<br/>(with OpenRouter API)
    Extractor-->>FrameworkSvc: controls_json
    FrameworkSvc->>Storage: save_extraction_json()
    Storage-->>FrameworkSvc: json_paths
    FrameworkSvc->>RAG: index_framework()<br/>(load JSONs & PDFs)
    RAG->>RAG: chunk & embed documents
    RAG->>RAG: store in Qdrant
    RAG-->>FrameworkSvc: indexing complete
    FrameworkSvc-->>API: result summary
    API-->>Client: SetupFrameworkResponse
Loading
sequenceDiagram
    participant Client as Agent/Client
    participant API as FastAPI
    participant RAG as retrieve_control_details()
    participant QdrantDB as Qdrant Vector DB
    participant Embedder as GemmaEmbedder
    
    Client->>API: retrieve_control_details(control_id, framework)
    API->>RAG: retrieve_control_details()
    RAG->>QdrantDB: fetch JSON cards by filter<br/>(control_id, source=json)
    QdrantDB-->>RAG: json_cards (metadata & text)
    RAG->>Embedder: embed(description) for semantic search
    Embedder-->>RAG: query_embedding
    RAG->>QdrantDB: search_similar_filtered()<br/>(embedding, source=pdf)
    QdrantDB-->>RAG: top_k pdf_chunks<br/>(text, score, metadata)
    RAG-->>API: {json_cards, pdf_chunks}
    API-->>Client: retrieval result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A compliance rabbit hops through PDFs with glee,
Extracting controls via LLM's decree,
Chunks into vectors, Qdrant stores the way,
RAG retrieves answers throughout the day!
Framework setup complete, from file to DB,
One service to rule them all—hop, hop, spree! 🌾

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/ai/rag & prompt extraction' clearly summarizes the main change: adding RAG (retrieval-augmented generation) and prompt-based extraction functionality to the AI service, which aligns with the comprehensive additions across embeddings, RAG, extraction, and API layers.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Docstrings were successfully generated.
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/ai/rag_&_Prompt_extraction

Tip

🧪 Unit Test Generation v2 is now available!

We have significantly improved our unit test generation capabilities.

To enable: Add this to your .coderabbit.yaml configuration:

reviews:
  finishing_touches:
    unit_tests:
      enabled: true

Try it out by using the @coderabbitai generate unit tests command on your code files or under ✨ Finishing Touches on the walkthrough!

Have feedback? Share your thoughts on our Discord thread!


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

🤖 Fix all issues with AI agents
In `@services/ai-service/main.py`:
- Around line 51-65: The file defines main() but never calls it; instead the if
__name__ == "__main__" block directly invokes setup_framework() with hardcoded
paths. Fix by consolidating startup logic into main() (use get_input_paths(),
print messages and call setup_framework as appropriate) and replace the direct
call in the __main__ block with a single call to main(); alternatively remove
the unused main() if you prefer the current inline behavior—update the __main__
block and/or main() so only one entrypoint (main or the inline setup_framework
call) remains.
- Line 61: The print statement print(f"  Framework outputs: config/frameworks/")
uses an unnecessary f-string; replace it with a normal string literal by
removing the leading 'f' so it becomes print("  Framework outputs:
config/frameworks/") — locate the exact print call (print(f"  Framework outputs:
config/frameworks/")) in main.py and update it accordingly.

In `@services/ai-service/pyproject.toml`:
- Line 4: Replace the placeholder description value in pyproject.toml (the
description = "Add your description here" entry) with a concise, meaningful
string that describes the AI service's purpose (e.g., what the service does, its
domain or primary feature). Update the description field only, keeping TOML
syntax intact and using double quotes, and ensure the new text succinctly
summarizes the project for package metadata consumers.

In `@services/ai-service/README.md`:
- Around line 9-32: Update the two fenced code blocks showing the directory
trees in README.md so they include a language identifier (`text`) after the
opening triple backticks; specifically add ```text to the block starting with
the `data/` tree and the block starting with the `config/` tree (the blocks that
show the `data/ inputs/ frameworks/ applicants/` tree and the `config/
frameworks/` / `data/ outputs/ evaluations/` tree) to satisfy markdownlint MD040
and improve rendering.

In `@services/ai-service/src/api/README.md`:
- Line 14: Update the README run instruction to use the correct repository path:
replace the "From `ai-service/`" text with "From `services/ai-service/`" in the
Run line in README.md so the example command (`python src/api/app.py` or `python
run.py`) accurately reflects the service location; verify both docs endpoints
`/api/docs` and `/api/redoc` remain unchanged.

In `@services/ai-service/src/api/routers/frameworks.py`:
- Around line 25-36: Before processing, check that section_names_list contains
only unique values and reject duplicates: after computing section_names_list
(used later as JSON filenames by save_extraction_json) validate
len(set(section_names_list)) == len(section_names_list) and if not raise an
HTTPException(400) with a clear message about duplicate section names; do this
check prior to the loop that zips section_names_list with files (and before any
file I/O) so duplicates cannot overwrite prior outputs.

In `@services/ai-service/src/ARCHITECTURE.md`:
- Around line 16-24: The table separator rows directly under the header row "|
Module | Purpose |" are missing spaces around the pipe characters and violate
markdownlint MD060; update those separator lines (the row of hyphens) to include
spaces around each pipe (e.g., "| --- | --- |") so they match the header
spacing, and apply the same fix to the other tables referenced later (the tables
around lines 342–376) to ensure consistent spacing across ARCHITECTURE.md.
- Around line 32-70: The fenced directory-structure code block in
ARCHITECTURE.md triggers markdownlint MD040 because it lacks a language; update
the opening fence from ``` to ```text so the block is explicitly marked as plain
text and keep the closing ``` unchanged, ensuring the directory tree content
(the src/ tree) is unchanged.

In `@services/ai-service/src/core/evaluator.py`:
- Around line 94-110: The JSON parsing in the try/except block of
services/ai-service/src/core/evaluator.py should be hardened so malformed or
unterminated fenced JSON doesn't raise and bypass the fallback; in the except
handler for json.JSONDecodeError when "```json" is present, extract the fenced
block defensively (use json_end != -1 to decide whether to slice to the closing
fence or to the end), then attempt a json.loads inside its own try/except and on
failure return the text fallback with an error message (instead of letting the
inner json.loads propagate); ensure the code paths around result_text,
json_start/json_end and the returned dict for the text fallback are updated
accordingly within the parsing logic in the function handling evaluation_report.
- Around line 61-75: The OpenAI request in evaluator.py uses
client.chat.completions.create (where response is assigned) without a timeout;
update the code to set a timeout either when creating the OpenAI client (pass
timeout=30.0 to the client constructor) or on this call by using
client.with_options(timeout=30.0).chat.completions.create so the chat completion
request for model "openai/gpt-4.1" will time out (e.g., 30s) and avoid hanging
workers.

In `@services/ai-service/src/core/framework_extraction.py`:
- Around line 143-156: The truncated-JSON recovery in framework_extraction.py
(the block using last_brace/last_bracket and json.loads) must validate the
parsed object shape before returning; after parsing into result_json, verify it
is a dict with keys like "framework_name" (string or match variable
framework_name) and "controls" (a list), and that each control entry has the
expected fields/types (e.g., id/name/details) — if validation fails, treat as a
parse failure and return {"framework_name": framework_name, "controls": []};
place this validation immediately after the json.loads call inside the try in
the function handling extraction to avoid accepting semantically incomplete
results.

In `@services/ai-service/src/embeddings/gemma_embedder.py`:
- Line 10: The module-level assignment os.environ["HF_HUB_OFFLINE"] = "1" causes
a global side effect; update GemmaEmbedder to accept an offline flag (e.g.,
offline param on __init__ or a DEFAULT_OFFLINE class attribute) and only
set/restore HF_HUB_OFFLINE around model download/loading in the method that
loads the model (e.g., the constructor or load_model function) so the
environment change is temporary and scoped to GemmaEmbedder; ensure you restore
the previous env value after loading to avoid affecting other code.

In `@services/ai-service/src/embeddings/haystack_retriever.py`:
- Around line 43-48: The scroll call in haystack_retriever.py uses a hardcoded
limit=10000 on qdrant_client.scroll which can truncate results; update the code
in the method that calls qdrant_client.scroll (look for the scroll_result
variable and collection_name usage) to paginate properly by looping and using
the offset/next_offset returned by scroll() (or repeatedly calling scroll with
updated offset until no more results) instead of a single large fixed limit, or
alternatively expose/configure the page size and document this limitation.
- Around line 40-63: The _load_documents_from_qdrant method swallows all
exceptions and populates self.documents which is never used by
retrieve_documents, causing silent failures and confusion; either remove the
unused method or make it meaningful: catch exceptions from qdrant_client.scroll
but log the full exception (use self.logger or a module logger) before
clearing/setting self.documents, and update retrieve_documents to use
self.documents (or conversely remove _load_documents_from_qdrant if you prefer
direct queries). Locate _load_documents_from_qdrant, qdrant_client.scroll, and
retrieve_documents to implement logging (including exception details) or delete
the unused method so behavior is consistent.

In `@services/ai-service/src/embeddings/qdrant_manager.py`:
- Around line 57-66: The current bare except around client.get_collection masks
real errors; update the try/except in qdrant_manager.py so you catch ValueError
(embedded/local client missing collection) and
qdrant_client.http.exceptions.UnexpectedResponse (remote client) specifically,
and only create the collection when ValueError or an UnexpectedResponse with
status_code == 404 is encountered; re-raise any other UnexpectedResponse or
unexpected exceptions so failures aren't hidden. Locate the try around
client.get_collection and the subsequent client.create_collection call
(referencing collection_name, vector_size, VectorParams, Distance) and implement
the two-case exception handling described.

In `@services/ai-service/src/services/framework_service.py`:
- Line 66: The loop using zip(section_names_order, controls_arrays) can silently
truncate if the two iterables differ in length; update that call to
zip(section_names_order, controls_arrays, strict=True) so Python raises a
ValueError on length mismatch. Locate the for loop that currently reads "for
section_name, controls_array in zip(section_names_order, controls_arrays):" in
framework_service.py and change it to include strict=True; if any surrounding
code relies on older Python versions, ensure the runtime supports Pythons with
zip(..., strict=True) or add an explicit length check (len(section_names_order)
!= len(controls_arrays)) before iterating.

In `@services/ai-service/src/utils/framework_utils.py`:
- Around line 96-100: The combined glob calls for "*.pdf" and "*.PDF" can
produce duplicate Path objects on case-insensitive filesystems; update the logic
in the block that builds pdfs (the variables and expressions using sub.is_dir(),
sub.glob("*.pdf"), sub.glob("*.PDF"), and vdb.glob(...)) to deduplicate results
after collecting both patterns—e.g., normalize each Path (resolve() or use
name.lower()) and keep a single entry per real file while preserving sort/order;
apply the same deduping for both the sub directory branch and the vdb fallback
so pdfs contains unique files only.

In `@services/ai-service/test.ipynb`:
- Around line 9-60: The notebook contains committed cell outputs leaking a local
path and extracted control text; clear all outputs before committing by running
a notebook-output clear (e.g., jupyter nbconvert --clear-output --inplace
<notebook> or use the Jupyter UI) and remove the printed results from the cell
that calls retrieve_control_details (the cell that invoked
retrieve_control_details("DG.1", "NDI", top_k_pdf=5) and references
RETRIEVE_CONTROL_DETAILS_TOOL_SCHEMA), then recommit the cleaned notebook.
🧹 Nitpick comments (21)
services/ai-service/pyproject.toml (2)

13-13: Move ipykernel to dev dependencies.

ipykernel is a Jupyter kernel interface used for interactive notebook development. It should not be a runtime dependency as it's not needed in production.

♻️ Proposed fix

Remove from runtime dependencies:

     "pypdf>=3.0.0",
-    "ipykernel>=7.1.0",
     "qdrant-client>=1.8.0",

Add to dev group:

 [dependency-groups]
 dev = [
     "fastapi[standard]>=0.128.0",
+    "ipykernel>=7.1.0",
 ]

22-28: Consolidate FastAPI dependencies to avoid redundancy.

fastapi is in main dependencies and fastapi[standard] is in dev dependencies. The [standard] extra includes uvicorn, httpx, and CLI tooling. Consider either:

  1. Use fastapi[standard] in main dependencies if you need uvicorn for production.
  2. Or explicitly add only uvicorn to dev for development server needs.
♻️ Option 1: Use fastapi[standard] in main dependencies
-    "fastapi>=0.128.0",
+    "fastapi[standard]>=0.128.0",
 ]

 [dependency-groups]
 dev = [
-    "fastapi[standard]>=0.128.0",
+    "ipykernel>=7.1.0",
 ]
services/ai-service/src/processing/pdf_parser.py (1)

37-38: Silent exception swallowing may hide extraction issues.

Catching all exceptions and continuing without any logging makes debugging difficult. Consider logging a warning when a page fails to extract so operators can identify problematic PDFs.

🔧 Proposed fix to add logging
+import logging
+
+logger = logging.getLogger(__name__)
+

Then in the extraction loop:

             try:
                 page_text = page.extract_text()
                 if page_text:
                     text_content.append(page_text)
-            except Exception:
+            except Exception as e:
+                logger.warning("Failed to extract text from page %d of %s: %s", page.page_number, pdf_path, e)
                 continue
services/ai-service/src/embeddings/gemma_embedder.py (1)

66-90: Add exception chaining for better tracebacks.

Use raise ... from e to preserve the original exception context, which helps with debugging.

🔧 Proposed fix
-                raise RuntimeError(
+                raise RuntimeError(
                     f"Failed to load model: {error_msg}\n\n"
                     # ... message continues ...
                     "   - Set environment variable: export HF_TOKEN=your_token"
-                )
+                ) from e
-            raise RuntimeError(f"Failed to load model: {e}")
+            raise RuntimeError(f"Failed to load model: {e}") from e
services/ai-service/src/utils/framework_utils.py (2)

11-12: Duplicate _project_root() definition.

This function is also defined in services/ai-service/src/services/framework_service.py (lines 16-17). Consider extracting it to a shared location to avoid duplication.


81-82: Silent exception swallowing hides JSON parsing errors.

When a JSON file fails to parse, the error is silently ignored. This could hide issues like malformed JSON or encoding problems.

🔧 Proposed fix to add logging
+import logging
+
+logger = logging.getLogger(__name__)

Then:

         try:
             with open(p, "r", encoding="utf-8") as f:
                 data = json.load(f)
             out.append((p.stem, data))
-        except Exception:
+        except Exception as e:
+            logger.warning("Failed to load JSON file %s: %s", p, e)
             continue
services/ai-service/src/processing/text_chunker.py (1)

191-234: Remove or deprecate unused function.

The comment indicates chunk_text_by_sentences is "not used anymore," yet it's still exported in __init__.py and documented in README.md. Either remove it entirely or add a proper deprecation warning if it needs to be retained for backward compatibility.

🔧 Option 1: Remove the function

Delete lines 191-234 and remove chunk_text_by_sentences from processing/__init__.py and README.md.

🔧 Option 2: Add deprecation warning
+import warnings
+
 # not used anymore
 def chunk_text_by_sentences(
     text: str,
     sentences_per_chunk: int = 5,
     framework_name: Optional[str] = None
 ) -> List[Dict]:
     """
     Chunk text by sentences instead of fixed character size.
+
+    .. deprecated::
+        This function is deprecated. Use :func:`chunk_text` instead.
     
     Args:
         text: Text to chunk
         sentences_per_chunk: Number of sentences per chunk
         framework_name: Optional framework name for metadata
         
     Returns:
         List of chunk dictionaries
     """
+    warnings.warn(
+        "chunk_text_by_sentences is deprecated, use chunk_text instead",
+        DeprecationWarning,
+        stacklevel=2
+    )
     # Split into sentences
services/ai-service/src/core/framework_extraction.py (2)

200-223: Unbounded thread pool size can exhaust resources.

Setting max_workers=len(pdf_paths_list) creates one thread per PDF. For large batches, this could exhaust system resources or hit API rate limits simultaneously.

Proposed fix to cap thread pool size
-    with ThreadPoolExecutor(max_workers=len(pdf_paths_list)) as executor:
+    max_workers = min(len(pdf_paths_list), 5)  # Cap concurrent LLM calls
+    with ThreadPoolExecutor(max_workers=max_workers) as executor:
         controls_arrays = list(executor.map(extract_pdf, pdf_paths_list))

219-223: Move success log to else block per static analysis hint.

The print statement on Line 219 executes even when retries were exhausted but zero controls remain. Moving it to an else block ensures it only logs on genuine success.

Proposed fix
             while len(controls) == 0 and retries < MAX_RETRIES:
                 retries += 1
                 print(f"Empty controls for {pdf_path}, retry {retries}/{MAX_RETRIES} with fallback prompt")
                 controls_json = extract_controls_from_framework(
                     pdf_text, framework_name, use_fallback_prompt=True
                 )
                 controls = controls_json.get("controls", [])
-            print(f"Controls extracted from {pdf_path}")
-            return controls
+            else:
+                print(f"Controls extracted from {pdf_path}")
+                return controls
+            return controls
         except Exception as e:
             print(f"Error extracting from {pdf_path}: {e}")
             return []
services/ai-service/src/embeddings/qdrant_manager.py (2)

111-115: Add strict=True to zip() to catch length mismatches early.

Although Line 108-109 validates lengths, adding strict=True provides defense-in-depth and satisfies linter B905.

Proposed fix
-    for doc, embedding in zip(documents, embeddings):
+    for doc, embedding in zip(documents, embeddings, strict=True):

196-217: Chain exceptions with raise ... from e for better tracebacks.

Multiple raise RuntimeError(...) statements lose the original exception context. Using raise ... from e preserves the chain for debugging.

Proposed fix for exception chaining
     except AttributeError as e:
         error_msg = str(e)
         raise RuntimeError(
             f"Qdrant client method error: {error_msg}. "
             ...
-        )
+        ) from e
     except Exception as e:
         error_msg = str(e)
         if "gRPC" in error_msg or "grpc" in error_msg.lower():
             raise RuntimeError(
                 ...
-            )
+            ) from e
         raise RuntimeError(
             ...
-        )
+        ) from e
services/ai-service/src/embeddings/haystack_retriever.py (1)

87-88: Move import to module level to avoid repeated import overhead.

The import inside the method is executed on every call. Unless this is intentional to avoid circular imports, move it to the top of the file.

Proposed fix
 from haystack.dataclasses import Document
 from qdrant_client import QdrantClient
 from typing import List, Dict, Optional
 from .gemma_embedder import GemmaEmbedder
+from .qdrant_manager import search_similar
 
 ...
 
     def retrieve_documents(self, query: str, top_k: Optional[int] = None) -> List[Dict]:
         ...
-        # Search in Qdrant directly (more efficient than Haystack for this use case)
-        from .qdrant_manager import search_similar
-        
         results = search_similar(
services/ai-service/src/rag/_shared.py (1)

13-18: Singleton pattern is appropriate but not thread-safe.

If multiple threads call get_shared_embedder() simultaneously before initialization completes, multiple embedder instances could be created (race condition). For a single-threaded FastAPI/CLI context this is fine, but worth noting if concurrency increases.

Thread-safe alternative using threading.Lock
 from typing import Any, Optional
+import threading
 
 from src.embeddings import load_gemma_embedder
 
 _cached_embedder: Optional[Any] = None
+_lock = threading.Lock()
 
 
 def get_shared_embedder():
     """Return a single embedder instance, creating and caching on first use."""
     global _cached_embedder
-    if _cached_embedder is None:
-        _cached_embedder = load_gemma_embedder()
+    if _cached_embedder is None:
+        with _lock:
+            if _cached_embedder is None:  # Double-checked locking
+                _cached_embedder = load_gemma_embedder()
     return _cached_embedder
services/ai-service/src/rag/retrieval.py (2)

73-76: Qdrant client initialized on every call; consider caching.

Each invocation of retrieve_control_details creates a new Qdrant client and potentially re-creates the collection. For frequently-called agent tools, this adds overhead. Consider caching the client similarly to how the embedder is cached.

Proposed caching approach
+from functools import lru_cache
+
+@lru_cache(maxsize=16)
+def _get_qdrant_client(collection: str, dim: int):
+    return initialize_qdrant(collection_name=collection, vector_size=dim)
+
 def retrieve_control_details(
     control_id: str,
     framework_name: str,
     *,
     top_k_pdf: int = 5,
 ) -> Dict[str, Any]:
     ...
     embedder = get_shared_embedder()
     collection = f"{framework_name}_rag"
     dim = embedder.get_embedding_dim()
-    client = initialize_qdrant(collection_name=collection, vector_size=dim)
+    client = _get_qdrant_client(collection, dim)

52-109: No error handling for missing collection or embedder failures.

If the collection doesn't exist or the embedder fails to load, the function will raise an unhandled exception. For an agent tool, returning a structured error response may be preferable.

Example error handling wrapper
 def retrieve_control_details(
     control_id: str,
     framework_name: str,
     *,
     top_k_pdf: int = 5,
 ) -> Dict[str, Any]:
+    try:
         embedder = get_shared_embedder()
         collection = f"{framework_name}_rag"
         ...
         return {
             "json_cards": json_cards,
             "pdf_chunks": pdf_chunks,
         }
+    except Exception as e:
+        return {
+            "error": str(e),
+            "json_cards": [],
+            "pdf_chunks": [],
+        }
services/ai-service/src/rag/ingestion.py (1)

65-68: Log PDF extraction errors instead of silently continuing.

Bare exception handling without logging makes missing or corrupt PDFs difficult to detect and debug. Catch only the specific exceptions that extract_text_from_pdf can raise (FileNotFoundError and ValueError) and log them at warning level:

Suggested adjustment
+import logging
+
+logger = logging.getLogger(__name__)
+
     for pdf_path in pdf_paths:
         try:
             raw = extract_text_from_pdf(pdf_path)
-        except Exception:
+        except (FileNotFoundError, ValueError) as exc:
+            logger.warning("Skipping PDF %s: %s", pdf_path, exc)
             continue
services/ai-service/run.py (1)

1-3: Docstring mentions --reload but the script doesn't support it.

The docstring suggests using --reload for auto-restart, but uvicorn.run() is called without parsing CLI arguments. Either update the docstring or add argument parsing.

Additionally, uvicorn should be explicitly declared in dependencies if not already present via fastapi[standard].

💡 Option to enable reload support
-"""Run the API: python run.py (from ai-service directory). Use --reload for auto-restart on code changes."""
+"""Run the API: python run.py (from ai-service directory)."""
 import uvicorn
-uvicorn.run("src.api.app:app", host="0.0.0.0", port=8000)
+uvicorn.run("src.api.app:app", host="0.0.0.0", port=8000, reload=True)

Or for production without reload:

-"""Run the API: python run.py (from ai-service directory). Use --reload for auto-restart on code changes."""
+"""Run the API: python run.py (from ai-service directory)."""
services/ai-service/src/api/app.py (2)

18-24: CORS configuration is development-only.

The allowed origins are hardcoded to localhost. Consider using environment variables for production flexibility.

💡 Environment-based CORS configuration
+import os
+
+ALLOWED_ORIGINS = os.getenv(
+    "CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"
+).split(",")
+
 app.add_middleware(
     CORSMiddleware,
-    allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
+    allow_origins=ALLOWED_ORIGINS,
     allow_credentials=True,
     allow_methods=["*"],
     allow_headers=["*"],
 )

42-51: Global ValueError handler may mask programming errors.

Catching all ValueError exceptions globally could hide bugs in application code that raise ValueError unintentionally. Consider using a custom exception class for input validation errors instead.

💡 Use a custom validation exception
+class ValidationError(Exception):
+    """Raised for invalid user input."""
+    def __init__(self, message: str):
+        self.message = message
+        super().__init__(message)
+
-@app.exception_handler(ValueError)
-async def value_error_handler(request, exc: ValueError):
+@app.exception_handler(ValidationError)
+async def validation_error_handler(request, exc: ValidationError):
     return JSONResponse(
         status_code=400,
         content={
             "error": "bad_request",
-            "message": str(exc),
+            "message": exc.message,
             "detail": None,
         },
     )

Then raise ValidationError in routers instead of ValueError.

services/ai-service/src/services/framework_service.py (2)

88-91: Unused loop variable section_name shadows outer scope.

The loop unpacks section_name but only uses content and safe_name. Use _ for the unused variable to improve clarity.

🔧 Proposed fix
-            for (section_name, content), safe_name in zip(pdf_sections, section_names_order):
+            for (_, content), safe_name in zip(pdf_sections, section_names_order, strict=True):
                 out_pdf = vector_db_dir / f"{safe_name}.pdf"
                 out_pdf.write_bytes(content)
                 pdf_paths_for_rag.append(str(out_pdf))

98-98: datetime.utcnow() is deprecated.

datetime.utcnow() is deprecated since Python 3.12. Use datetime.now(timezone.utc) instead for timezone-aware UTC timestamps.

🔧 Proposed fix
-from datetime import datetime
+from datetime import datetime, timezone
 ...
-                "created_at": datetime.utcnow(),
+                "created_at": datetime.now(timezone.utc),

Comment on lines +51 to +65
def main():
"""Main entry point."""
print("\n" + "=" * 60)
print("Compliance Framework Extraction System")
print("=" * 60)

paths = get_input_paths()
print("\nDirectory structure:")
print(f" Input PDFs (Frameworks): {paths['frameworks']}")
print(f" Input PDFs (Applicants): {paths['applicants']}")
print(f" Framework outputs: config/frameworks/")

print("\nUsage:")
print(" setup_framework('data/inputs/frameworks/<dir>', 'framework_name')")
print("=" * 60 + "\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

main() function is defined but not called.

The __main__ block directly calls setup_framework() with hardcoded paths instead of calling main(). Either call main() or remove it if unused.

🔧 Option 1: Call main() and move setup logic there
 def main():
     """Main entry point."""
     print("\n" + "=" * 60)
     print("Compliance Framework Extraction System")
     print("=" * 60)

     paths = get_input_paths()
     print("\nDirectory structure:")
     print(f"  Input PDFs (Frameworks): {paths['frameworks']}")
     print(f"  Input PDFs (Applicants): {paths['applicants']}")
-    print(f"  Framework outputs: config/frameworks/")
+    print("  Framework outputs: config/frameworks/")

     print("\nUsage:")
     print("   setup_framework('data/inputs/frameworks/<dir>', 'framework_name')")
     print("=" * 60 + "\n")
+
+    # Example invocation
+    framework_path = "data/inputs/frameworks/NDI"
+    framework_name = "NDI"
+    setup_framework(framework_path, framework_name)


 if __name__ == "__main__":
-    framework_path = "data/inputs/frameworks/NDI"
-    framework_name = "NDI"
-    setup_framework(framework_path, framework_name)
+    main()

Also applies to: 68-71

🧰 Tools
🪛 Ruff (0.14.14)

[error] 61-61: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In `@services/ai-service/main.py` around lines 51 - 65, The file defines main()
but never calls it; instead the if __name__ == "__main__" block directly invokes
setup_framework() with hardcoded paths. Fix by consolidating startup logic into
main() (use get_input_paths(), print messages and call setup_framework as
appropriate) and replace the direct call in the __main__ block with a single
call to main(); alternatively remove the unused main() if you prefer the current
inline behavior—update the __main__ block and/or main() so only one entrypoint
(main or the inline setup_framework call) remains.

print("\nDirectory structure:")
print(f" Input PDFs (Frameworks): {paths['frameworks']}")
print(f" Input PDFs (Applicants): {paths['applicants']}")
print(f" Framework outputs: config/frameworks/")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove extraneous f-string prefix.

This string has no placeholders, so the f prefix is unnecessary.

🔧 Proposed fix
-    print(f"  Framework outputs: config/frameworks/")
+    print("  Framework outputs: config/frameworks/")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(f" Framework outputs: config/frameworks/")
print(" Framework outputs: config/frameworks/")
🧰 Tools
🪛 Ruff (0.14.14)

[error] 61-61: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In `@services/ai-service/main.py` at line 61, The print statement print(f" 
Framework outputs: config/frameworks/") uses an unnecessary f-string; replace it
with a normal string literal by removing the leading 'f' so it becomes print(" 
Framework outputs: config/frameworks/") — locate the exact print call (print(f" 
Framework outputs: config/frameworks/")) in main.py and update it accordingly.

[project]
name = "ai-service"
version = "0.1.0"
description = "Add your description here"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the placeholder description.

The description "Add your description here" is a placeholder that should be replaced with meaningful project metadata describing the AI service's purpose.

📝 Suggested fix
-description = "Add your description here"
+description = "AI service for governance framework controls extraction, RAG, and policy evaluation"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description = "Add your description here"
description = "AI service for governance framework controls extraction, RAG, and policy evaluation"
🤖 Prompt for AI Agents
In `@services/ai-service/pyproject.toml` at line 4, Replace the placeholder
description value in pyproject.toml (the description = "Add your description
here" entry) with a concise, meaningful string that describes the AI service's
purpose (e.g., what the service does, its domain or primary feature). Update the
description field only, keeping TOML syntax intact and using double quotes, and
ensure the new text succinctly summarizes the project for package metadata
consumers.

Comment on lines +9 to +32
```
data/
├── inputs/
│ ├── frameworks/ # Place framework PDF files here
│ └── applicants/ # Place applicant PDF files here
```

**Usage:**
- **Framework PDFs**: Place compliance framework PDFs in `data/inputs/frameworks/`
- **Applicant PDFs**: Place applicant documents to evaluate in `data/inputs/applicants/`

### Output Directories (Generated automatically)

```
config/
├── frameworks/ # Framework outputs (one JSON per section)
│ └── {framework_name}/
│ └── {section_name}.json
└── vector_db/ # Qdrant vector database storage

data/
└── outputs/
└── evaluations/ # Evaluation reports (JSON files)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifiers to the two directory-tree fenced blocks.

markdownlint MD040 flags the fences at Line 9 and Line 22 as missing language. Adding text keeps lint clean and improves rendering.

Suggested fix
-```
+```text
 data/
 ├── inputs/
 │   ├── frameworks/          # Place framework PDF files here
 │   └── applicants/         # Place applicant PDF files here
-```
+```

-```
+```text
 config/
 ├── frameworks/             # Framework outputs (one JSON per section)
 │   └── {framework_name}/
 │       └── {section_name}.json
 └── vector_db/             # Qdrant vector database storage

 data/
 └── outputs/
     └── evaluations/        # Evaluation reports (JSON files)
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 22-22: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@services/ai-service/README.md` around lines 9 - 32, Update the two fenced
code blocks showing the directory trees in README.md so they include a language
identifier (`text`) after the opening triple backticks; specifically add ```text
to the block starting with the `data/` tree and the block starting with the
`config/` tree (the blocks that show the `data/ inputs/ frameworks/ applicants/`
tree and the `config/ frameworks/` / `data/ outputs/ evaluations/` tree) to
satisfy markdownlint MD040 and improve rendering.

- `GET /health` — Health check.
- `POST /api/v1/frameworks/setup` — Setup framework: form fields `framework_name`, `section_names` (list), `files` (list of PDFs). Same order for section_names and files.

**Run:** From `ai-service/`: `python src/api/app.py` or `python run.py`. Docs: `/api/docs`, `/api/redoc`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align the run-from path with the repo layout.

Line 14 points to ai-service/, but this service lives under services/ai-service/. Aligning avoids confusion for new users.

Suggested fix
-**Run:** From `ai-service/`: `python src/api/app.py` or `python run.py`. Docs: `/api/docs`, `/api/redoc`.
+**Run:** From `services/ai-service/`: `python src/api/app.py` or `python run.py`. Docs: `/api/docs`, `/api/redoc`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Run:** From `ai-service/`: `python src/api/app.py` or `python run.py`. Docs: `/api/docs`, `/api/redoc`.
**Run:** From `services/ai-service/`: `python src/api/app.py` or `python run.py`. Docs: `/api/docs`, `/api/redoc`.
🤖 Prompt for AI Agents
In `@services/ai-service/src/api/README.md` at line 14, Update the README run
instruction to use the correct repository path: replace the "From `ai-service/`"
text with "From `services/ai-service/`" in the Run line in README.md so the
example command (`python src/api/app.py` or `python run.py`) accurately reflects
the service location; verify both docs endpoints `/api/docs` and `/api/redoc`
remain unchanged.

Comment on lines +43 to +48
scroll_result = self.qdrant_client.scroll(
collection_name=self.collection_name,
limit=10000,
with_payload=True,
with_vectors=False
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded limit=10000 may not retrieve all documents.

If the collection grows beyond 10,000 documents, this scroll will silently truncate results. Consider using pagination via the offset returned by scroll() or documenting this limitation.

🤖 Prompt for AI Agents
In `@services/ai-service/src/embeddings/haystack_retriever.py` around lines 43 -
48, The scroll call in haystack_retriever.py uses a hardcoded limit=10000 on
qdrant_client.scroll which can truncate results; update the code in the method
that calls qdrant_client.scroll (look for the scroll_result variable and
collection_name usage) to paginate properly by looping and using the
offset/next_offset returned by scroll() (or repeatedly calling scroll with
updated offset until no more results) instead of a single large fixed limit, or
alternatively expose/configure the page size and document this limitation.

Comment on lines +57 to +66
try:
client.get_collection(collection_name)
except Exception:
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=vector_size,
distance=Distance.COSINE
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and check if it exists
find . -name "qdrant_manager.py" -type f

Repository: AFAskar/Governance-Agent

Length of output: 122


🏁 Script executed:

# Read the relevant section (lines 50-70) to see the current code
sed -n '50,70p' services/ai-service/src/embeddings/qdrant_manager.py

Repository: AFAskar/Governance-Agent

Length of output: 684


🏁 Script executed:

# Check the imports at the top of the file to see what's already imported
head -30 services/ai-service/src/embeddings/qdrant_manager.py

Repository: AFAskar/Governance-Agent

Length of output: 704


🏁 Script executed:

# Search for exception handling related to Qdrant in the codebase
rg "UnexpectedResponse|NotFoundError|RpcException" --type py -n

Repository: AFAskar/Governance-Agent

Length of output: 291


🌐 Web query:

qdrant-client Python exception types collection not found

💡 Result:

In qdrant-client (Python), a “collection not found” coming back from the HTTP/REST API is raised as:

  • qdrant_client.http.exceptions.UnexpectedResponse (base: ApiException), with e.status_code == 404 and the server error in e.content / e.structured().[1][2]
  • In some call paths, you may instead see qdrant_client.http.exceptions.ResponseHandlingException, which wraps the underlying exception in e.source.[2]

Example (recommended way to branch on “collection missing”):

from qdrant_client.http.exceptions import UnexpectedResponse, ResponseHandlingException

try:
    client.get_collection("my_collection")
except UnexpectedResponse as e:
    if e.status_code == 404:
        # collection does not exist
        ...
    else:
        raise
except ResponseHandlingException as e:
    # unwrap if needed
    if isinstance(e.source, UnexpectedResponse) and e.source.status_code == 404:
        ...
    else:
        raise

You’ll typically see it printed like: qdrant_client.http.exceptions.UnexpectedResponse: Unexpected Response: 404 (Not Found) when the collection doesn’t exist.[3]

Note: if you’re using the local (embedded) client/backend, some “not found” conditions may be raised as plain ValueError instead of HTTP exceptions.[4]

Sources:
[1] Qdrant client exceptions source (UnexpectedResponse, status_code, structured()) https://python-client.qdrant.tech/_modules/qdrant_client/http/exceptions
[2] Qdrant client exceptions API docs (ApiException, UnexpectedResponse, ResponseHandlingException) https://python-client.qdrant.tech/qdrant_client.http.exceptions
[3] Example of 404 surfaced as UnexpectedResponse in practice (GitHub issue) qdrant/qdrant#4739
[4] Local client code raising ValueError for not-found scenarios https://python-client.qdrant.tech/_modules/qdrant_client/local/qdrant_local


Catching bare Exception masks specific Qdrant errors and hides unexpected failures.

Line 59 catches all exceptions to check collection existence, but this code supports both local (embedded) and remote (HTTP/REST) modes. The local client raises ValueError for missing collections, while the remote client raises qdrant_client.http.exceptions.UnexpectedResponse with status code 404. Catching all exceptions prevents proper error handling and debugging.

Catch the appropriate exceptions for each mode:

Proposed fix
+from qdrant_client.http.exceptions import UnexpectedResponse
+
     # Create collection if it doesn't exist
     try:
         client.get_collection(collection_name)
-    except Exception:
+    except (ValueError, UnexpectedResponse) as e:
+        # ValueError: local mode; UnexpectedResponse: remote mode
         client.create_collection(
             collection_name=collection_name,
             vectors_config=VectorParams(
                 size=vector_size,
                 distance=Distance.COSINE
             )
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
client.get_collection(collection_name)
except Exception:
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=vector_size,
distance=Distance.COSINE
)
)
from qdrant_client.http.exceptions import UnexpectedResponse
# Create collection if it doesn't exist
try:
client.get_collection(collection_name)
except (ValueError, UnexpectedResponse) as e:
# ValueError: local mode; UnexpectedResponse: remote mode
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=vector_size,
distance=Distance.COSINE
)
)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 59-59: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@services/ai-service/src/embeddings/qdrant_manager.py` around lines 57 - 66,
The current bare except around client.get_collection masks real errors; update
the try/except in qdrant_manager.py so you catch ValueError (embedded/local
client missing collection) and qdrant_client.http.exceptions.UnexpectedResponse
(remote client) specifically, and only create the collection when ValueError or
an UnexpectedResponse with status_code == 404 is encountered; re-raise any other
UnexpectedResponse or unexpected exceptions so failures aren't hidden. Locate
the try around client.get_collection and the subsequent client.create_collection
call (referencing collection_name, vector_size, VectorParams, Distance) and
implement the two-case exception handling described.

sections_out: list[dict[str, Any]] = []
total_controls = 0

for section_name, controls_array in zip(section_names_order, controls_arrays):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add strict=True to zip() to catch length mismatches.

If section_names_order and controls_arrays have different lengths due to a bug, zip() will silently truncate. Using strict=True raises an error on mismatch.

🔧 Proposed fix
-            for section_name, controls_array in zip(section_names_order, controls_arrays):
+            for section_name, controls_array in zip(section_names_order, controls_arrays, strict=True):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for section_name, controls_array in zip(section_names_order, controls_arrays):
for section_name, controls_array in zip(section_names_order, controls_arrays, strict=True):
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 66-66: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
In `@services/ai-service/src/services/framework_service.py` at line 66, The loop
using zip(section_names_order, controls_arrays) can silently truncate if the two
iterables differ in length; update that call to zip(section_names_order,
controls_arrays, strict=True) so Python raises a ValueError on length mismatch.
Locate the for loop that currently reads "for section_name, controls_array in
zip(section_names_order, controls_arrays):" in framework_service.py and change
it to include strict=True; if any surrounding code relies on older Python
versions, ensure the runtime supports Pythons with zip(..., strict=True) or add
an explicit length check (len(section_names_order) != len(controls_arrays))
before iterating.

Comment on lines +96 to +100
if sub.is_dir():
pdfs = sorted(sub.glob("*.pdf")) + sorted(sub.glob("*.PDF"))
if pdfs:
return pdfs
pdfs = sorted(vdb.glob("*.pdf")) + sorted(vdb.glob("*.PDF"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential duplicate paths on case-insensitive filesystems.

On macOS and Windows, glob("*.pdf") and glob("*.PDF") may return the same files, leading to duplicates in the result list.

🔧 Proposed fix using case-insensitive glob
-    if sub.is_dir():
-        pdfs = sorted(sub.glob("*.pdf")) + sorted(sub.glob("*.PDF"))
-        if pdfs:
-            return pdfs
-    pdfs = sorted(vdb.glob("*.pdf")) + sorted(vdb.glob("*.PDF"))
-    return pdfs
+    def _get_pdfs(directory: Path) -> List[Path]:
+        # Use case-insensitive matching to avoid duplicates on case-insensitive filesystems
+        return sorted(set(directory.glob("*.[pP][dD][fF]")))
+
+    if sub.is_dir():
+        pdfs = _get_pdfs(sub)
+        if pdfs:
+            return pdfs
+    return _get_pdfs(vdb)
🤖 Prompt for AI Agents
In `@services/ai-service/src/utils/framework_utils.py` around lines 96 - 100, The
combined glob calls for "*.pdf" and "*.PDF" can produce duplicate Path objects
on case-insensitive filesystems; update the logic in the block that builds pdfs
(the variables and expressions using sub.is_dir(), sub.glob("*.pdf"),
sub.glob("*.PDF"), and vdb.glob(...)) to deduplicate results after collecting
both patterns—e.g., normalize each Path (resolve() or use name.lower()) and keep
a single entry per real file while preserving sort/order; apply the same
deduping for both the sub directory branch and the vdb fallback so pdfs contains
unique files only.

Comment on lines +9 to +60
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/mohammedbalkhair/Documents/Governance-Agent/services/ai-service/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
" from .autonotebook import tqdm as notebook_tqdm\n"
]
}
],
"source": [
"import os\n",
"os.environ[\"HF_HUB_OFFLINE\"] = \"1\" \n",
"from src.rag import retrieve_control_details, RETRIEVE_CONTROL_DETAILS_TOOL_SCHEMA\n",
"\n",
"\n",
"\n",
"# 2. Retrieve (e.g. from an agent)\n",
"out = retrieve_control_details(\"DG.1\", \"NDI\", top_k_pdf=5)\n",
"# out[\"json_cards\"] → structured control summaries\n",
"# out[\"pdf_chunks\"] → relevant PDF passages\n",
"\n",
"# 3. Register as tool\n",
"# Use RETRIEVE_CONTROL_DETAILS_TOOL_SCHEMA in your agent’s tool list."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "a9e7ac20",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'json_cards': [{'text': 'Control DSI.OE.02. This metric measures the percentage of systems shared by the entity with the National Data Lake (NDL) against the total number of the entity’s systems requested by the NDL team. Note that a system will be considered integrated only if all required dimensions of its data are fully sourced to NDL. Calculation: Number of systems integrated with NDL by the entity / Total number of the entity’s systems requested by NDL * 100. Threshold: 70%. Scale: Unacceptable: ≤ 70%; Low: (70%, 75%]; Fair: (75%, 80%]; Good: (80%, 85%]; Excellent: (85%, 90%]; Leader: > 90%.',\n",
" 'metadata': {'chunk_id': 'json_NDI_DSI_OE_02_OperationalExcellence-OE',\n",
" 'framework_name': 'NDI',\n",
" 'source': 'json',\n",
" 'control_id': 'DSI.OE.02',\n",
" 'source_pdf': 'OperationalExcellence-OE'}}],\n",
" 'pdf_chunks': [{'text': 'NDL) against the total number of the entity’s systems requested\\nby the NDL team. Note that a system will be considered integrated only if all required\\ndimensions of its data are fully sourced to NDL.\\nThis metric aims to accelerate the efforts to enrich NDL with high-value and wide-\\nspectrum data assets generated by various government entities. It also helps in\\n10\\nمﺎﻋ\\n\\n18/11/2025\\nElement Name Element Details\\nachieving the goal of making NDL the unified single source of truth for analytical\\ndata assets.\\nDomain Name Data Sharing and Interoperability (DSI)\\nData Platforms National Data Lake (NDL)\\nDefinitions • Number of systems integrated with NDL by the entity\\n• Total number of the entity’s systems requested by NDL\\nCalculation = Number of systems integrated with NDL by the entity / Total number of the\\nentity’s systems requested by NDL * 100\\nMeasurement Unit Percentage\\nAcceptable Threshold 70%\\nScale Intervals Unacceptable: ≤ 70%\\nLow: (70%, 75%]\\nFair: (75%, 80%]\\nGood: (80%, 85%]\\nExcellent: (85%, 90%]\\nLeader: > 90%\\nVersion History\\nDependencies\\nElement Name Element Details\\nMetric ID DSI.OE.03\\nMetric Name Data sharing agreement processing\\nMetric Description This metric measures the amount of time taken by the data producer to process the\\ndata sharing requests raised by the consumer entities. This metric considers the time\\ntaken for either approving or rejecting the requests as part of the processing time.',\n",
" 'score': 0.84288440527831,\n",
" 'metadata': {'chunk_id': 'pdf_OperationalExcellence-OE_11',\n",
" 'framework_name': 'NDI',\n",
" 'start_char': 14175,\n",
" 'end_char': 15595,\n",
" 'length': 1419,\n",
" 'chunk_size': 1500,\n",
" 'overlap': 200,\n",
" 'source': 'pdf',\n",
" 'source_pdf': 'OperationalExcellence-OE'}}]}"
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear notebook outputs to avoid PII and content leakage.
The committed outputs include a local user path and extracted control text. Please clear outputs before committing to avoid leaking local paths and potentially sensitive content.

If helpful, you can clear outputs with:

jupyter nbconvert --clear-output --inplace services/ai-service/test.ipynb
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 13-13: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)


[warning] 17-17: Found useless expression. Either assign it to a variable or remove it.

(B018)

🤖 Prompt for AI Agents
In `@services/ai-service/test.ipynb` around lines 9 - 60, The notebook contains
committed cell outputs leaking a local path and extracted control text; clear
all outputs before committing by running a notebook-output clear (e.g., jupyter
nbconvert --clear-output --inplace <notebook> or use the Jupyter UI) and remove
the printed results from the cell that calls retrieve_control_details (the cell
that invoked retrieve_control_details("DG.1", "NDI", top_k_pdf=5) and references
RETRIEVE_CONTROL_DETAILS_TOOL_SCHEMA), then recommit the cleaned notebook.

@coderabbitai

coderabbitai Bot commented Jan 30, 2026

Copy link
Copy Markdown

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #3

coderabbitai Bot added a commit that referenced this pull request Jan 30, 2026
Docstrings generation was requested by @Mohammed-Balkhair-hub.

* #2 (comment)

The following files were modified:

* `services/ai-service/main.py`
* `services/ai-service/src/api/app.py`
* `services/ai-service/src/api/routers/frameworks.py`
* `services/ai-service/src/api/routers/health.py`
* `services/ai-service/src/core/evaluator.py`
* `services/ai-service/src/core/framework_extraction.py`
* `services/ai-service/src/embeddings/gemma_embedder.py`
* `services/ai-service/src/embeddings/haystack_retriever.py`
* `services/ai-service/src/embeddings/qdrant_manager.py`
* `services/ai-service/src/processing/pdf_parser.py`
* `services/ai-service/src/processing/text_chunker.py`
* `services/ai-service/src/rag/_shared.py`
* `services/ai-service/src/rag/ingestion.py`
* `services/ai-service/src/rag/retrieval.py`
* `services/ai-service/src/services/framework_service.py`
* `services/ai-service/src/utils/framework_utils.py`
@Mohammed-Balkhair-hub
Mohammed-Balkhair-hub merged commit fdcbf60 into dev Jan 30, 2026
1 check passed
This was referenced Feb 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant