Add LexMexTool - Mexican federal law legal assistant - #6971
Conversation
Add tests for LexMexTool to ensure API key handling and functionality.
This file implements the LexMexTool for querying Mexican federal laws using the LEX-MEX API. It includes input validation, error handling, and response formatting.
Added README for LexMexTool with installation, usage, and common errors.
📝 WalkthroughWalkthroughChangesLexMexTool
Sequence Diagram(s)sequenceDiagram
participant CrewAI
participant LexMexTool
participant LEXMEX_API
CrewAI->>LexMexTool: Submit pregunta
LexMexTool->>LEXMEX_API: Send authenticated legal query
LEXMEX_API-->>LexMexTool: Return answer, citations, and confidence
LexMexTool-->>CrewAI: Return formatted response
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment Warning |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py`:
- Around line 80-98: Prevent caller-controlled hosts in LexMexTool._run by
removing api_base from the request URL and always using the fixed
LEXMEX_API_BASE constant. Ensure the X-API-Key resolved by _resolved_key is sent
only to the trusted LEX-MEX endpoint.
- Line 79: Update the api_key field in the Lexmex tool model to use Field with
default=None, exclude=True, and repr=False, ensuring supplied credentials are
omitted from model_dump serialization and representations.
In `@lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py`:
- Around line 13-36: Update test_happy_path to capture the mocked requests.post
invocation and assert the required LEX-MEX request contract: endpoint, request
body, API-key header, and timeout. Keep the existing response and output
assertions, using the mock call arguments to verify the successful query sends
the expected values.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e2ca9030-c62b-418d-b5f0-0b93c731ff9d
📒 Files selected for processing (5)
lib/crewai-tools/src/crewai_tools/__init__.pylib/crewai-tools/src/crewai_tools/tools/lexmex_tool/README.mdlib/crewai-tools/src/crewai_tools/tools/lexmex_tool/__init__.pylib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.pylib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py
| ) | ||
| args_schema: Type[BaseModel] = LexMexInput | ||
|
|
||
| api_key: Optional[str] = None |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the BaseTool serialization path and the LexMexTool credential field.
ast-grep outline lib/crewai/src/crewai/tools/base_tool.py --items all --match BaseTool
rg -n -C 4 'model_dump\(mode="json"\)|plain_serializer' \
lib/crewai/src/crewai/tools/base_tool.py
rg -n -C 2 '\bapi_key\b' \
lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.pyRepository: crewAIInc/crewAI
Length of output: 2203
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the exact serialization behavior and the complete credential usage path.
sed -n '100,155p' lib/crewai/src/crewai/tools/base_tool.py
sed -n '155,250p' lib/crewai/src/crewai/tools/base_tool.py
sed -n '65,140p' lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py
# Identify the Pydantic dependency and run a standalone serialization probe when available.
rg -n -C 2 'pydantic' pyproject.toml lib/crewai/pyproject.toml lib/crewai-tools/pyproject.toml 2>/dev/null || true
python3 - <<'PY'
try:
import pydantic
from pydantic import BaseModel, Field
from typing import Optional
class Probe(BaseModel):
api_key: Optional[str] = Field(default=None, exclude=True, repr=False)
api_base: str = "https://example.invalid"
class PlainProbe(BaseModel):
api_key: Optional[str] = None
api_base: str = "https://example.invalid"
print("pydantic_version=", pydantic.__version__)
print("excluded_dump=", Probe(api_key="secret").model_dump(mode="json"))
print("excluded_dump_none=", Probe().model_dump(mode="json"))
print("plain_dump=", PlainProbe(api_key="secret").model_dump(mode="json"))
except Exception as exc:
print(type(exc).__name__, str(exc))
PYRepository: crewAIInc/crewAI
Length of output: 10013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '100,155p' lib/crewai/src/crewai/tools/base_tool.py
sed -n '155,250p' lib/crewai/src/crewai/tools/base_tool.py
sed -n '65,140p' lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py
rg -n -C 2 'pydantic' pyproject.toml lib/crewai/pyproject.toml lib/crewai-tools/pyproject.toml 2>/dev/null || true
python3 - <<'PY'
try:
import pydantic
from pydantic import BaseModel, Field
from typing import Optional
class Probe(BaseModel):
api_key: Optional[str] = Field(default=None, exclude=True, repr=False)
api_base: str = "https://example.invalid"
class PlainProbe(BaseModel):
api_key: Optional[str] = None
api_base: str = "https://example.invalid"
print("pydantic_version=", pydantic.__version__)
print("excluded_dump=", Probe(api_key="secret").model_dump(mode="json"))
print("excluded_dump_none=", Probe().model_dump(mode="json"))
print("plain_dump=", PlainProbe(api_key="secret").model_dump(mode="json"))
except Exception as exc:
print(type(exc).__name__, str(exc))
PYRepository: crewAIInc/crewAI
Length of output: 10013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Trace where BaseTool values are serialized and whether Agent/Task configuration
# serializes tools through the BaseTool schema.
rg -n -C 4 'model_dump\(|tools:\s|list\[BaseTool\]|Sequence\[BaseTool\]|BaseTool' \
lib/crewai/src/crewai | head -n 240
# Check whether LexMexTool already imports Field and inspect its class declaration.
sed -n '1,90p' lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py
rg -n -C 3 'class LexMexTool|Field\(' \
lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.pyRepository: crewAIInc/crewAI
Length of output: 22618
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 'model_dump\(|tools:\s|list\[BaseTool\]|Sequence\[BaseTool\]|BaseTool' \
lib/crewai/src/crewai | head -n 240
sed -n '1,90p' lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py
rg -n -C 3 'class LexMexTool|Field\(' \
lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.pyRepository: crewAIInc/crewAI
Length of output: 27057
Exclude api_key from serialized tool state.
CrewAI serialization calls model_dump(mode="json"), and the current field includes supplied credentials. Define it as Field(default=None, exclude=True, repr=False) to prevent credential persistence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py` at line
79, Update the api_key field in the Lexmex tool model to use Field with
default=None, exclude=True, and repr=False, ensuring supplied credentials are
omitted from model_dump serialization and representations.
| api_base: str = LEXMEX_API_BASE | ||
| timeout: int = 30 | ||
|
|
||
| def _resolved_key(self) -> str: | ||
| key = self.api_key or os.getenv("LEXMEX_API_KEY") | ||
| if not key: | ||
| raise ValueError( | ||
| "Falta la API key de LEX-MEX. Pásala como LexMexTool(api_key=...) " | ||
| "o define la variable de entorno LEXMEX_API_KEY. Genera una en " | ||
| "https://lex-mex.xyz tras registrarte (plan VIP o pay-as-you-go)." | ||
| ) | ||
| return key | ||
|
|
||
| def _run(self, pregunta: str) -> str: | ||
| resp = requests.post( | ||
| f"{self.api_base}/api/v1/consulta", | ||
| json={"pregunta": pregunta}, | ||
| headers={"X-API-Key": self._resolved_key()}, | ||
| timeout=self.timeout, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not allow callers to select the authenticated request host.
A caller can set api_base to an arbitrary URL. Line 97 then sends LEXMEX_API_KEY to that host. This enables API-key exfiltration and requests to internal network targets.
Use the fixed LEXMEX_API_BASE constant, or validate an HTTPS lex-mex.xyz allowlist before the request.
Proposed fix
- api_base: str = LEXMEX_API_BASE
timeout: int = 30
@@
- f"{self.api_base}/api/v1/consulta",
+ f"{LEXMEX_API_BASE}/api/v1/consulta",📝 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.
| api_base: str = LEXMEX_API_BASE | |
| timeout: int = 30 | |
| def _resolved_key(self) -> str: | |
| key = self.api_key or os.getenv("LEXMEX_API_KEY") | |
| if not key: | |
| raise ValueError( | |
| "Falta la API key de LEX-MEX. Pásala como LexMexTool(api_key=...) " | |
| "o define la variable de entorno LEXMEX_API_KEY. Genera una en " | |
| "https://lex-mex.xyz tras registrarte (plan VIP o pay-as-you-go)." | |
| ) | |
| return key | |
| def _run(self, pregunta: str) -> str: | |
| resp = requests.post( | |
| f"{self.api_base}/api/v1/consulta", | |
| json={"pregunta": pregunta}, | |
| headers={"X-API-Key": self._resolved_key()}, | |
| timeout=self.timeout, | |
| timeout: int = 30 | |
| def _resolved_key(self) -> str: | |
| key = self.api_key or os.getenv("LEXMEX_API_KEY") | |
| if not key: | |
| raise ValueError( | |
| "Falta la API key de LEX-MEX. Pásala como LexMexTool(api_key=...) " | |
| "o define la variable de entorno LEXMEX_API_KEY. Genera una en " | |
| "https://lex-mex.xyz tras registrarte (plan VIP o pay-as-you-go)." | |
| ) | |
| return key | |
| def _run(self, pregunta: str) -> str: | |
| resp = requests.post( | |
| f"{LEXMEX_API_BASE}/api/v1/consulta", | |
| json={"pregunta": pregunta}, | |
| headers={"X-API-Key": self._resolved_key()}, | |
| timeout=self.timeout, |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 93-98: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.post(
f"{self.api_base}/api/v1/consulta",
json={"pregunta": pregunta},
headers={"X-API-Key": self._resolved_key()},
timeout=self.timeout,
)
Note: [CWE-918] Server-Side Request Forgery (SSRF).
(ssrf-requests)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py` around
lines 80 - 98, Prevent caller-controlled hosts in LexMexTool._run by removing
api_base from the request URL and always using the fixed LEXMEX_API_BASE
constant. Ensure the X-API-Key resolved by _resolved_key is sent only to the
trusted LEX-MEX endpoint.
Source: Linters/SAST tools
| def test_happy_path(monkeypatch): | ||
| tool = LexMexTool(api_key="lmx_live_test") | ||
|
|
||
| class FakeResponse: | ||
| status_code = 200 | ||
|
|
||
| def raise_for_status(self): | ||
| pass | ||
|
|
||
| def json(self): | ||
| return { | ||
| "respuesta": "El despido justificado requiere...", | ||
| "fuentes": [{"cita": "LFT, art. 47"}], | ||
| "confianza": "alta", | ||
| } | ||
|
|
||
| monkeypatch.setattr( | ||
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | ||
| lambda *a, **kw: FakeResponse(), | ||
| ) | ||
|
|
||
| resultado = tool.run(pregunta="¿Causales de despido justificado?") | ||
| assert "despido justificado" in resultado | ||
| assert "LFT, art. 47" in resultado |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Assert the outgoing LEX-MEX request contract.
The test passes if the endpoint, request body, API-key header, or timeout is changed incorrectly. Assert the mocked call so the test covers the behavior required to make a successful query.
Proposed fix
+from unittest.mock import Mock
+
import pytest
@@
- monkeypatch.setattr(
+ mock_post = Mock(return_value=FakeResponse())
+ monkeypatch.setattr(
"crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post",
- lambda *a, **kw: FakeResponse(),
+ mock_post,
)
@@
assert "despido justificado" in resultado
assert "LFT, art. 47" in resultado
+ mock_post.assert_called_once_with(
+ "https://lex-mex.xyz/api/v1/consulta",
+ json={"pregunta": "¿Causales de despido justificado?"},
+ headers={"X-API-Key": "lmx_live_test"},
+ timeout=30,
+ )As per coding guidelines, **/*test*.py must test new functionality through behavior rather than implementation details.
📝 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.
| def test_happy_path(monkeypatch): | |
| tool = LexMexTool(api_key="lmx_live_test") | |
| class FakeResponse: | |
| status_code = 200 | |
| def raise_for_status(self): | |
| pass | |
| def json(self): | |
| return { | |
| "respuesta": "El despido justificado requiere...", | |
| "fuentes": [{"cita": "LFT, art. 47"}], | |
| "confianza": "alta", | |
| } | |
| monkeypatch.setattr( | |
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | |
| lambda *a, **kw: FakeResponse(), | |
| ) | |
| resultado = tool.run(pregunta="¿Causales de despido justificado?") | |
| assert "despido justificado" in resultado | |
| assert "LFT, art. 47" in resultado | |
| from unittest.mock import Mock | |
| def test_happy_path(monkeypatch): | |
| tool = LexMexTool(api_key="lmx_live_test") | |
| class FakeResponse: | |
| status_code = 200 | |
| def raise_for_status(self): | |
| pass | |
| def json(self): | |
| return { | |
| "respuesta": "El despido justificado requiere...", | |
| "fuentes": [{"cita": "LFT, art. 47"}], | |
| "confianza": "alta", | |
| } | |
| mock_post = Mock(return_value=FakeResponse()) | |
| monkeypatch.setattr( | |
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | |
| mock_post, | |
| ) | |
| resultado = tool.run(pregunta="¿Causales de despido justificado?") | |
| assert "despido justificado" in resultado | |
| assert "LFT, art. 47" in resultado | |
| mock_post.assert_called_once_with( | |
| "https://lex-mex.xyz/api/v1/consulta", | |
| json={"pregunta": "¿Causales de despido justificado?"}, | |
| headers={"X-API-Key": "lmx_live_test"}, | |
| timeout=30, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py` around
lines 13 - 36, Update test_happy_path to capture the mocked requests.post
invocation and assert the required LEX-MEX request contract: endpoint, request
body, API-key header, and timeout. Keep the existing response and output
assertions, using the mock call arguments to verify the successful query sends
the expected values.
Source: Coding guidelines
Description
Adds
LexMexTool, a tool for querying LEX-MEX — a Spanish-languagelegal assistant covering all 316 Mexican federal laws, kept in sync
with diputados.gob.mx. Answers are returned with the exact law and
article cited (no hallucinated sources).
What's included
crewai_tools/tools/lexmex_tool/lexmex_tool.py— the tool implementationcrewai_tools/tools/lexmex_tool/README.md— usage docs and required env varscrewai_tools/tools/lexmex_tool/__init__.py— module exporttests/tools/test_lexmex_tool.py— unit tests (API key resolution + happy path, no real network calls)LexMexToolincrewai_tools/__init__.py(import +__all__)Requirements
Requires a LEX-MEX API key (
LEXMEX_API_KEYenv var orapi_key=param), obtained by signing up at https://lex-mex.xyz.
Checklist (per BUILDING_TOOLS.md)
BaseToolwithargs_schemaand explicit field descriptions_run(...)implemented, lazy API key resolution with clear error messagestests/tools/, no real network calls (mocked)uv run pytestandpre-commit run -arun locally — not run in this environment; happy to fix any CI failures a maintainer flags