Skip to content

Conversation

@RobGeada
Copy link
Contributor

@RobGeada RobGeada commented Oct 7, 2025

Summary by Sourcery

Add support for custom detectors by introducing a new CustomDetectorRegistry to load and invoke user-defined functions, update the API to register and process only the detectors requested with improved error handling and logging, and include example detector functions along with an integration test.

New Features:

  • Introduce CustomDetectorRegistry to load and run user-provided detector functions under the "custom" detector category

Enhancements:

  • Refactor detect_content to iterate over explicitly requested detectors and return clear errors for missing or invalid detectors
  • Configure basic logging and log incoming detection requests

Tests:

  • Add integration test to verify custom detectors are correctly registered and invoked

Chores:

  • Provide a sample custom_detectors module with placeholder detection functions
  • Add Pydantic models for ModerationRequest and Moderation in common schemes

@sourcery-ai
Copy link

sourcery-ai bot commented Oct 7, 2025

Reviewer's Guide

This PR extends the existing built-in detector service by integrating a custom detectors plugin: it introduces a CustomDetectorRegistry that discovers and wraps user-supplied functions, registers it in the FastAPI lifecycle, adjusts request handling to only invoke explicitly requested detectors, and adds logging, error handling, supporting modules, and tests.

Sequence diagram for content detection with custom detectors

sequenceDiagram
    participant Client
    participant FastAPI
    participant CustomDetectorRegistry
    participant custom_func_wrapper
    Client->>FastAPI: POST /api/v1/text/contents (with detector_params)
    FastAPI->>CustomDetectorRegistry: handle_request(content, detector_params)
    CustomDetectorRegistry->>custom_func_wrapper: custom_func_wrapper(func, func_name, content)
    custom_func_wrapper-->>CustomDetectorRegistry: ContentAnalysisResponse or None
    CustomDetectorRegistry-->>FastAPI: List[ContentAnalysisResponse]
    FastAPI-->>Client: ContentsAnalysisResponse
Loading

Class diagram for CustomDetectorRegistry and custom detector integration

classDiagram
    class BaseDetectorRegistry
    class CustomDetectorRegistry {
        +registry: dict
        +__init__()
        +handle_request(content: str, detector_params: dict): List[ContentAnalysisResponse]
    }
    class ContentAnalysisResponse
    class custom_func_wrapper {
        +custom_func_wrapper(func: Callable, func_name: str, s: str): Optional[ContentAnalysisResponse]
    }
    BaseDetectorRegistry <|-- CustomDetectorRegistry
    CustomDetectorRegistry o-- custom_func_wrapper
    CustomDetectorRegistry o-- ContentAnalysisResponse
    custom_func_wrapper o-- ContentAnalysisResponse
    class "custom_detectors.custom_detectors" {
        +over_100_characters(text: str): bool
        +contains_word(text: str): bool
    }
    CustomDetectorRegistry o-- "custom_detectors.custom_detectors"
Loading

File-Level Changes

Change Details Files
Register and invoke custom detectors in the FastAPI app
  • Imported CustomDetectorRegistry and logging
  • Registered custom registry in lifespan alongside existing detectors
  • Adjusted detect_content to iterate over request.detector_params instead of all detectors
  • Added HTTP 400 handling for missing or invalid detectors
  • Logged incoming detector requests
detectors/built_in/app.py
Introduce custom detectors wrapper module
  • Created custom_func_wrapper to adapt boolean/dict results into ContentAnalysisResponse
  • Implemented CustomDetectorRegistry to auto-discover user functions via inspect
  • Handled invocation errors with HTTPException and logging
detectors/built_in/custom_detectors_wrapper.py
Add example custom detectors and test coverage
  • Provided template custom_detectors.py with sample functions
  • Added test_custom.py to verify custom detector integration via FastAPI TestClient
detectors/built_in/custom_detectors/custom_detectors.py
tests/detectors/builtIn/test_custom.py
Enhance logging and configurations
  • Initialized basic logging configuration and module-level logger
  • Instrumented detailed error logs for detection failures
  • Ensured xmlschema entry ends with newline in requirements
detectors/built_in/app.py
detectors/built_in/requirements.txt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `detectors/built_in/app.py:33` </location>
<code_context>
-
 @app.post("/api/v1/text/contents", response_model=ContentsAnalysisResponse)
 def detect_content(request: ContentAnalysisHttpRequest):
+    logging.info(f"Request for {request.detector_params}")
+
     detections = []
</code_context>

<issue_to_address>
**suggestion:** Consider using the module-level logger instead of the root logger for consistency.

Use 'logger.info' instead of 'logging.info' to ensure consistent logging and easier configuration management.

```suggestion
    logger.info(f"Request for {request.detector_params}")
```
</issue_to_address>

### Comment 2
<location> `detectors/built_in/custom_detectors_wrapper.py:54` </location>
<code_context>
+
+    def handle_request(self, content: str, detector_params: dict) -> List[ContentAnalysisResponse]:
+        detections = []
+        if "custom" in detector_params and isinstance(detector_params["custom"], (list, str)):
+            custom_functions = detector_params["custom"]
+            custom_functions = [custom_functions] if isinstance(custom_functions, str) else custom_functions
</code_context>

<issue_to_address>
**suggestion:** Type checking for 'custom' parameter may miss edge cases with unexpected types.

Unexpected types for 'custom' are ignored without notice. Logging or handling these cases would improve debuggability.
</issue_to_address>

### Comment 3
<location> `tests/detectors/builtIn/test_custom.py:14-22` </location>
<code_context>
+
+
+
+    def test_custom_detectors(self, client):
+        payload = {
+            "contents": ["What is an apple?"],
+            "detector_params": {"custom": ["contains_word"]}
+        }
+        resp = client.post("/api/v1/text/contents", json=payload)
+        assert resp.status_code == 200
+        texts = [d["text"] for d in resp.json()[0]]
+        assert "What is an apple?" in texts
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing negative and error case tests for custom detectors.

Add tests for scenarios where the custom detector does not match any content and for invalid detector names to ensure proper error handling and coverage of negative cases.

```suggestion
    def test_custom_detectors(self, client):
        payload = {
            "contents": ["What is an apple?"],
            "detector_params": {"custom": ["contains_word"]}
        }
        resp = client.post("/api/v1/text/contents", json=payload)
        assert resp.status_code == 200
        texts = [d["text"] for d in resp.json()[0]]
        assert "What is an apple?" in texts

    def test_custom_detector_no_match(self, client):
        payload = {
            "contents": ["Bananas are yellow."],
            "detector_params": {"custom": ["contains_word"]}
        }
        resp = client.post("/api/v1/text/contents", json=payload)
        assert resp.status_code == 200
        texts = [d["text"] for d in resp.json()[0]]
        assert "Bananas are yellow." not in texts

    def test_custom_detector_invalid_name(self, client):
        payload = {
            "contents": ["What is an apple?"],
            "detector_params": {"custom": ["non_existent_detector"]}
        }
        resp = client.post("/api/v1/text/contents", json=payload)
        assert resp.status_code == 400 or resp.status_code == 422
        # Optionally check for error message in response
        # assert "invalid detector" in resp.text.lower()
```
</issue_to_address>

### Comment 4
<location> `tests/detectors/builtIn/test_custom.py:4` </location>
<code_context>
+import pytest
+from fastapi.testclient import TestClient
+
+class TestRegexDetectors:
+    @pytest.fixture
+    def client(self):
</code_context>

<issue_to_address>
**nitpick:** Test class name is misleading.

Since this class only tests custom detectors, renaming it to 'TestCustomDetectors' would improve clarity.
</issue_to_address>

### Comment 5
<location> `detectors/built_in/custom_detectors_wrapper.py:58-67` </location>
<code_context>
    def handle_request(self, content: str, detector_params: dict) -> List[ContentAnalysisResponse]:
        detections = []
        if "custom" in detector_params and isinstance(detector_params["custom"], (list, str)):
            custom_functions = detector_params["custom"]
            custom_functions = [custom_functions] if isinstance(custom_functions, str) else custom_functions
            for custom_function in custom_functions:
                if self.registry.get(custom_function):
                    try:
                        result = custom_func_wrapper(self.registry[custom_function], custom_function, content)
                        if result is not None:
                            detections.append(result)
                    except Exception as e:
                        logger.error(e)
                        raise HTTPException(status_code=400, detail="Detection error, check detector logs")
                else:
                    raise HTTPException(status_code=400, detail=f"Unrecognized custom function: {custom_function}")
        return detections

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
- Remove unnecessary else after guard condition ([`remove-unnecessary-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-else/))
- Explicitly raise from a previous error ([`raise-from-previous-error`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/raise-from-previous-error/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


def handle_request(self, content: str, detector_params: dict) -> List[ContentAnalysisResponse]:
detections = []
if "custom" in detector_params and isinstance(detector_params["custom"], (list, str)):
Copy link

Choose a reason for hiding this comment

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

suggestion: Type checking for 'custom' parameter may miss edge cases with unexpected types.

Unexpected types for 'custom' are ignored without notice. Logging or handling these cases would improve debuggability.

Comment on lines +14 to +70
def test_custom_detectors(self, client):
payload = {
"contents": ["What is an apple?"],
"detector_params": {"custom": ["contains_word"]}
}
resp = client.post("/api/v1/text/contents", json=payload)
assert resp.status_code == 200
texts = [d["text"] for d in resp.json()[0]]
assert "What is an apple?" in texts
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Missing negative and error case tests for custom detectors.

Add tests for scenarios where the custom detector does not match any content and for invalid detector names to ensure proper error handling and coverage of negative cases.

Suggested change
def test_custom_detectors(self, client):
payload = {
"contents": ["What is an apple?"],
"detector_params": {"custom": ["contains_word"]}
}
resp = client.post("/api/v1/text/contents", json=payload)
assert resp.status_code == 200
texts = [d["text"] for d in resp.json()[0]]
assert "What is an apple?" in texts
def test_custom_detectors(self, client):
payload = {
"contents": ["What is an apple?"],
"detector_params": {"custom": ["contains_word"]}
}
resp = client.post("/api/v1/text/contents", json=payload)
assert resp.status_code == 200
texts = [d["text"] for d in resp.json()[0]]
assert "What is an apple?" in texts
def test_custom_detector_no_match(self, client):
payload = {
"contents": ["Bananas are yellow."],
"detector_params": {"custom": ["contains_word"]}
}
resp = client.post("/api/v1/text/contents", json=payload)
assert resp.status_code == 200
texts = [d["text"] for d in resp.json()[0]]
assert "Bananas are yellow." not in texts
def test_custom_detector_invalid_name(self, client):
payload = {
"contents": ["What is an apple?"],
"detector_params": {"custom": ["non_existent_detector"]}
}
resp = client.post("/api/v1/text/contents", json=payload)
assert resp.status_code == 400 or resp.status_code == 422
# Optionally check for error message in response
# assert "invalid detector" in resp.text.lower()

Copy link
Collaborator

@saichandrapandraju saichandrapandraju left a comment

Choose a reason for hiding this comment

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

Thanks @RobGeada , overall LGTM with one comment and 1 nit (+1 for sourcery's logger instead of logging suggestion)

@github-actions
Copy link

github-actions bot commented Oct 8, 2025

PR image build completed successfully!

📦 Huggingface PR image: quay.io/trustyai/guardrails-detector-huggingface-runtime-ci:$PR_HEAD_SHA
📦 Built-in PR image: quay.io/trustyai/guardrails-detector-built-in-ci:$PR_HEAD_SHA
📦 LLM Judge PR image: quay.io/trustyai/guardrails-detector-llm-judge-ci:$PR_HEAD_SHA

@RobGeada RobGeada merged commit 0b4d33a into trustyai-explainability:main Oct 9, 2025
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants