-
Notifications
You must be signed in to change notification settings - Fork 9
Guardrail: Integration #575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1b9f375
b401287
c336ca7
32e61b5
8c0b389
c50d30a
8a675ec
39d97e3
e09e0ac
21efd32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| from typing import Any | ||
rkritika1508 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| from uuid import UUID | ||
| import logging | ||
|
|
||
| import httpx | ||
|
|
||
| from app.core.config import settings | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def call_guardrails( | ||
| input_text: str, guardrail_config: list[dict], job_id: UUID | ||
| ) -> dict[str, Any]: | ||
| """ | ||
| Call the Kaapi guardrails service to validate and process input text. | ||
|
|
||
| Args: | ||
| input_text: Text to validate and process. | ||
| guardrail_config: List of validator configurations to apply. | ||
| job_id: Unique identifier for the request. | ||
|
|
||
| Returns: | ||
| JSON response from the guardrails service with validation results. | ||
| """ | ||
| payload = { | ||
| "request_id": str(job_id), | ||
| "input": input_text, | ||
| "validators": guardrail_config, | ||
| } | ||
|
|
||
| headers = { | ||
| "accept": "application/json", | ||
| "Authorization": f"Bearer {settings.KAAPI_GUARDRAILS_AUTH}", | ||
| "Content-Type": "application/json", | ||
| } | ||
|
|
||
| try: | ||
| with httpx.Client(timeout=10.0) as client: | ||
| response = client.post( | ||
| settings.KAAPI_GUARDRAILS_URL, | ||
| json=payload, | ||
| headers=headers, | ||
| ) | ||
|
|
||
| response.raise_for_status() | ||
| return response.json() | ||
| except Exception as e: | ||
| logger.warning( | ||
| f"[call_guardrails] Service unavailable. Bypassing guardrails. job_id={job_id}. error={e}" | ||
| ) | ||
|
|
||
| return { | ||
| "success": False, | ||
| "bypassed": True, | ||
| "data": { | ||
| "safe_text": input_text, | ||
| "rephrase_needed": False, | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,6 @@ | |
| from app.models.llm.request import ( | ||
| QueryParams, | ||
| LLMCallConfig, | ||
| CompletionConfig, | ||
| ConfigBlob, | ||
| KaapiLLMParams, | ||
| KaapiCompletionConfig, | ||
|
|
@@ -164,3 +163,112 @@ def test_llm_call_invalid_provider( | |
| ) | ||
|
|
||
| assert response.status_code == 422 | ||
|
|
||
|
|
||
| def test_llm_call_success_with_guardrails( | ||
| client: TestClient, | ||
| user_api_key_header: dict[str, str], | ||
| ) -> None: | ||
| """Test successful LLM call when guardrails are enabled (no validators).""" | ||
|
|
||
| with ( | ||
| patch("app.services.llm.jobs.start_high_priority_job") as mock_start_job, | ||
| patch("app.services.llm.guardrails.call_guardrails") as mock_guardrails, | ||
| ): | ||
| mock_start_job.return_value = "test-task-id" | ||
|
|
||
| mock_guardrails.return_value = { | ||
| "success": True, | ||
| "bypassed": False, | ||
| "data": { | ||
| "safe_text": "What is the capital of France?", | ||
| "rephrase_needed": False, | ||
| }, | ||
| } | ||
|
|
||
| payload = LLMCallRequest( | ||
| query=QueryParams(input="What is the capital of France?"), | ||
| config=LLMCallConfig( | ||
| blob=ConfigBlob( | ||
| completion=NativeCompletionConfig( | ||
| provider="openai-native", | ||
| params={ | ||
| "model": "gpt-4o", | ||
| "temperature": 0.7, | ||
| }, | ||
| ) | ||
| ) | ||
| ), | ||
| input_guardrails=[], | ||
| output_guardrails=[], | ||
| callback_url="https://example.com/callback", | ||
| ) | ||
|
|
||
| response = client.post( | ||
| "/api/v1/llm/call", | ||
| json=payload.model_dump(mode="json"), | ||
| headers=user_api_key_header, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
|
|
||
| body = response.json() | ||
| assert body["success"] is True | ||
| assert "response is being generated" in body["data"]["message"] | ||
|
|
||
| mock_start_job.assert_called_once() | ||
| mock_guardrails.assert_not_called() | ||
|
Comment on lines
+174
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Searching for call_guardrails invocations in non-test code ==="
rg -n "call_guardrails" --type=py -g '!**/test*' -B 2 -A 2
echo ""
echo "=== Searching for the llm route handler ==="
fd -e py -path "*/routes/*llm*" --type f | head -20Repository: ProjectTech4DevAI/kaapi-backend Length of output: 2325 🏁 Script executed: #!/bin/bash
echo "=== Finding llm route file ==="
find . -path "*/routes*" -name "*.py" | grep -i llm
echo ""
echo "=== Looking at route handler structure ==="
fd llm -e py -path "*routes*"
echo ""
echo "=== Check app structure ==="
find backend/app -type d -name routesRepository: ProjectTech4DevAI/kaapi-backend Length of output: 491 🏁 Script executed: #!/bin/bash
echo "=== Reading the LLM route handler ==="
wc -l backend/app/api/routes/llm.py
echo ""
echo "=== Finding the call endpoint in route handler ==="
rg -n "def.*call" backend/app/api/routes/llm.py -A 30Repository: ProjectTech4DevAI/kaapi-backend Length of output: 1648 🏁 Script executed: #!/bin/bash
echo "=== Full content of llm.py route handler ==="
cat -n backend/app/api/routes/llm.py
echo ""
echo "=== Finding start_job function ==="
rg -n "def start_job" --type py -g '!**/test*' -A 10Repository: ProjectTech4DevAI/kaapi-backend Length of output: 6825 🏁 Script executed: #!/bin/bash
echo "=== Checking URL paths used in existing tests ==="
rg -n 'client.post.*llm/call' backend/app/tests/api/routes/test_llm.py
echo ""
echo "=== Verify: does TestClient care about leading slash? ==="
rg -n 'client.post' backend/app/tests/api/routes/test_llm.py | head -10Repository: ProjectTech4DevAI/kaapi-backend Length of output: 425 🏁 Script executed: #!/bin/bash
echo "=== Get the exact POST URLs from test file ==="
rg -n 'client.post\(' backend/app/tests/api/routes/test_llm.py -A 1 | grep -E '(client.post|"/)' | head -20Repository: ProjectTech4DevAI/kaapi-backend Length of output: 435 🏁 Script executed: #!/bin/bash
echo "=== Get exact URLs from all test POST calls ==="
sed -n '39,41p; 77,79p; 113,115p; 134,136p; 159,161p; 207,209p; 262,264p' backend/app/tests/api/routes/test_llm.pyRepository: ProjectTech4DevAI/kaapi-backend Length of output: 843 🏁 Script executed: #!/bin/bash
echo "=== Check the second test function (lines 223-274) ==="
sed -n '223,274p' backend/app/tests/api/routes/test_llm.pyRepository: ProjectTech4DevAI/kaapi-backend Length of output: 1878 Remove unreachable
First test (lines 174–220): The Second test (lines 223–274): Similar dead mock setup. Additionally, this test is missing Minor: New tests use 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_llm_call_guardrails_bypassed_still_succeeds( | ||
| client: TestClient, | ||
| user_api_key_header: dict[str, str], | ||
| ) -> None: | ||
| """If guardrails service is unavailable (bypassed), request should still succeed.""" | ||
|
|
||
| with ( | ||
| patch("app.services.llm.jobs.start_high_priority_job") as mock_start_job, | ||
| patch("app.services.llm.guardrails.call_guardrails") as mock_guardrails, | ||
| ): | ||
| mock_start_job.return_value = "test-task-id" | ||
|
|
||
| mock_guardrails.return_value = { | ||
| "success": True, | ||
| "bypassed": True, | ||
| "data": { | ||
| "safe_text": "What is the capital of France?", | ||
| "rephrase_needed": False, | ||
| }, | ||
| } | ||
|
|
||
| payload = LLMCallRequest( | ||
| query=QueryParams(input="What is the capital of France?"), | ||
| config=LLMCallConfig( | ||
| blob=ConfigBlob( | ||
| completion=NativeCompletionConfig( | ||
| provider="openai-native", | ||
| params={ | ||
| "model": "gpt-4", | ||
| "temperature": 0.7, | ||
| }, | ||
| ) | ||
| ) | ||
| ), | ||
| input_guardrails=[{"type": "pii_remover"}], | ||
| output_guardrails=[], | ||
| callback_url="https://example.com/callback", | ||
| ) | ||
|
|
||
| response = client.post( | ||
| "/api/v1/llm/call", | ||
| json=payload.model_dump(mode="json"), | ||
| headers=user_api_key_header, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
|
|
||
| body = response.json() | ||
| assert body["success"] is True | ||
| assert "response is being generated" in body["data"]["message"] | ||
|
|
||
| mock_start_job.assert_called_once() | ||
rkritika1508 marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+223
to
+274
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing assertion on The docstring says the test validates that a bypassed guardrails response still succeeds, and As-is, this test is functionally identical to 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.