Skip to content

Conversation

@saichandrapandraju
Copy link
Collaborator

@saichandrapandraju saichandrapandraju commented Jun 29, 2025

Replaces global dictionary pattern with FastAPI's built-in app.state mechanism for managing detector objects across all detector implementations.

Changes

  • Base Class (DetectorBaseAPI): Added state management methods (set_detector(), get_detector(), cleanup_detector())
  • LLM Judge & HuggingFace Detectors: Migrated from detector_objects global dict to app.state
  • HuggingFace Detector: Added close() for cleanup logic

Closes: #14

Summary by Sourcery

Migrate detector lifecycle management from module-level globals to FastAPI’s built-in app.state mechanism and ensure proper resource cleanup

Enhancements:

  • Add set_detector, get_detector, and cleanup_detector methods to FastAPI app for storing detector instances
  • Update LLM Judge and HuggingFace detector apps to use app.state in their lifespan and request handlers
  • Implement a close() method in the HuggingFace Detector to release model, tokenizer, and GPU resources during shutdown

@sourcery-ai
Copy link

sourcery-ai bot commented Jun 29, 2025

Reviewer's Guide

This PR refactors detector lifecycle to leverage FastAPI's app.state instead of a global dictionary, migrating lifespans and handlers for both LLM Judge and HuggingFace detectors, and introduces a proper resource cleanup via a new close() implementation.

Sequence diagram for detector lifecycle using app.state

sequenceDiagram
    participant FastAPIApp as FastAPI app
    participant Detector as Detector (LLMJudgeDetector or HuggingFace Detector)
    participant RequestHandler as Request Handler

    FastAPIApp->>FastAPIApp: set_detector(Detector)
    RequestHandler->>FastAPIApp: get_detector()
    FastAPIApp-->>RequestHandler: Detector
    RequestHandler->>Detector: run(request)
    FastAPIApp->>Detector: close() (on shutdown)
    FastAPIApp->>FastAPIApp: cleanup_detector()
Loading

Class diagram for updated detector state management in FastAPI app

classDiagram
    class FastAPI {
        +state
        +set_detector(detector)
        +get_detector()
        +cleanup_detector()
    }
    class DetectorBaseAPI {
        +set_detector(detector)
        +get_detector()
        +cleanup_detector()
    }
    class LLMJudgeDetector {
        +judge: Judge
        +available_metrics
        +close()
        +run(request)
    }
    class Detector {
        +model
        +tokenizer
        +close()
        +run(input)
    }
    FastAPI <|-- DetectorBaseAPI
    DetectorBaseAPI <.. LLMJudgeDetector
    DetectorBaseAPI <.. Detector
Loading

Class diagram for new close() method in HuggingFace Detector

classDiagram
    class Detector {
        +model
        +tokenizer
        +close()
    }
    Detector : +close()
    Detector : +run(input)
Loading

File-Level Changes

Change Details Files
Add state-based detector management methods
  • Define set_detector to assign detector to app.state
  • Define get_detector to retrieve detector from app.state
  • Define cleanup_detector to clear the detector reference
detectors/common/app.py
Use app.state for LLM Judge detector lifecycle
  • Replace global detector_objects with app.set_detector/app.get_detector in lifespan
  • Update request handlers to retrieve detector via app.get_detector()
  • Invoke app.cleanup_detector() after closing the detector
detectors/llm_judge/app.py
Use app.state for HuggingFace detector lifecycle
  • Set detector via app.set_detector in the lifespan manager
  • Replace global detector_objects lookup in handlers with app.get_detector()
  • Call app.cleanup_detector() once detector.close() completes
detectors/huggingface/app.py
Implement close() for HuggingFace detector resource cleanup
  • Add close() method to free model and tokenizer references
  • Trigger garbage collection and clear CUDA cache if available
detectors/huggingface/detector.py
Refine type annotation for LLMJudgeDetector.judge
  • Annotate self.judge attribute explicitly as Judge
detectors/llm_judge/detector.py

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 @saichandrapandraju - 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/llm_judge/app.py:24` </location>
<code_context>
+    app.set_detector(LLMJudgeDetector())
+    yield
+    # Clean up resources
+    detector: LLMJudgeDetector = app.get_detector()
+    if detector and hasattr(detector, 'close'):
+        await detector.close()
</code_context>

<issue_to_address>
No check for detector existence before use.

If get_detector() returns None, accessing its attributes will cause an error. Please add a check and raise a clear exception if the detector is missing.

Suggested implementation:

```python
    detector: LLMJudgeDetector = app.get_detector()
    if detector is None:
        raise RuntimeError("No detector instance found during cleanup. Cannot close resources.")
    if hasattr(detector, 'close'):
        await detector.close()
    app.cleanup_detector()

```

```python
    detector: LLMJudgeDetector = app.get_detector()
    if detector is None:
        raise RuntimeError("No detector instance found. Cannot analyze content.")
    return ContentsAnalysisResponse(root=await detector.run(request))

```
</issue_to_address>

### Comment 2
<location> `detectors/huggingface/detector.py:285` </location>
<code_context>
         return contents_analyses
+
+
+    def close(self) -> None:
+        """Clean up model and tokenizer resources."""
+        
+        if self.model:
+            if hasattr(self.model, 'to') and self.cuda_device.type == "cuda":
+                self.model = self.model.to(torch.device("cpu"))
+            self.model = None
+
+        if self.tokenizer:
+            self.tokenizer = None
+
+        gc.collect()
+
+        if torch.cuda.is_available():
+            torch.cuda.empty_cache()
</code_context>

<issue_to_address>
close() method is not async but may be awaited.

Since close() is synchronous, awaiting it will cause a TypeError. Update close() to async if asynchronous cleanup is needed, or remove the await if not.
</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.

app.set_detector(LLMJudgeDetector())
yield
# Clean up resources
detector: LLMJudgeDetector = app.get_detector()
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): No check for detector existence before use.

If get_detector() returns None, accessing its attributes will cause an error. Please add a check and raise a clear exception if the detector is missing.

Suggested implementation:

    detector: LLMJudgeDetector = app.get_detector()
    if detector is None:
        raise RuntimeError("No detector instance found during cleanup. Cannot close resources.")
    if hasattr(detector, 'close'):
        await detector.close()
    app.cleanup_detector()
    detector: LLMJudgeDetector = app.get_detector()
    if detector is None:
        raise RuntimeError("No detector instance found. Cannot analyze content.")
    return ContentsAnalysisResponse(root=await detector.run(request))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

but isn't line 25 (if detector and hasattr(detector, 'close'):) doing this? (without raising error)?

Copy link

Choose a reason for hiding this comment

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

You're right—line 25 (if detector and hasattr(detector, 'close'):) does check for the detector's existence before attempting to close it, so it avoids an immediate error if the detector is missing. My suggestion to raise an explicit error is a more defensive approach: it helps make it clear during development or debugging if the detector was unexpectedly missing at shutdown, which could indicate a lifecycle management issue.

The trade-off is between silent cleanup (your current approach, which avoids unnecessary crashes during shutdown) and explicit error reporting (which can help catch subtle bugs early). If you expect the detector to always be present, raising an error can make issues more visible. If you want to prioritize graceful shutdowns even in edge cases, your current check is sufficient.

Let me know if you'd like a code snippet for explicit error handling, or if you prefer the current approach!

Comment on lines +48 to +50
detector: Detector = app.get_detector()
if not detector:
raise RuntimeError("Detector is not initialized")
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): We've found these issues:

@ruivieira ruivieira added the enhancement New feature or request label Jun 30, 2025
@ruivieira ruivieira changed the title Refactor Detector lifecycle to use FastAPI app.state feat: Refactor Detector lifecycle to use FastAPI app.state Jun 30, 2025
@ruivieira ruivieira moved this to In Review in TrustyAI planning Jun 30, 2025
@github-actions
Copy link

github-actions bot commented Jul 9, 2025

PR image build completed successfully!

📦 PR image: quay.io/trustyai/guardrails-detector-huggingface-runtime-ci:0a3bd137db51ef21f7be257760a256cc393bd93d

@ruivieira ruivieira merged commit 013afef into trustyai-explainability:main Jul 9, 2025
1 of 2 checks passed
@saichandrapandraju saichandrapandraju self-assigned this Jul 9, 2025
@saichandrapandraju saichandrapandraju deleted the global-state branch July 9, 2025 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ok-to-test

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

Replace global dictionaries with FastAPI app.state across all detectors

2 participants