-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Add LexMexTool - Mexican federal law legal assistant Closes #7258 #6971
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
Open
Volpsmx
wants to merge
13
commits into
crewAIInc:main
Choose a base branch
from
Volpsmx:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
3237d44
Implement tests for LexMexTool
Volpsmx 3f926f0
Add LexMexTool to imports and tool list
Volpsmx 1d1b62a
Add LexMexTool for querying Mexican federal laws
Volpsmx b66d907
Create README.md for LexMexTool
Volpsmx 37371bc
Initialize lexmex_tool module with LexMexTool import
Volpsmx fa0c363
Bump version to 1.1 with security enhancements
Volpsmx 4c09fd1
Enhance tests for LexMexTool API key behavior
Volpsmx bbedd85
Refactor timeout attribute with Field validation
Volpsmx f5de29d
Mejorar seguridad y robustez en LexMexTool
Volpsmx cd35a6c
Fix null cita crash, simplify redirect check, sync test mocks
Volpsmx 28f07fa
Fix null cita crash, simplify redirect check, sync test mocks
Volpsmx a05d9f3
Add regression test for 3xx redirect rejection
Volpsmx 04fdb01
Merge branch 'main' into main
Volpsmx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # LexMexTool | ||
|
|
||
| Consulta [LEX-MEX](https://lex-mex.xyz) — asesor jurídico con IA sobre | ||
| las 316 leyes federales mexicanas vigentes, sincronizadas con | ||
| `diputados.gob.mx`. Cada respuesta cita la ley y el artículo exacto | ||
| (sin alucinar fuentes). | ||
|
|
||
| ## Instalación | ||
|
|
||
| Esta tool solo necesita `requests` y `pydantic`, ambos ya son | ||
| dependencias de `crewai-tools`. No requiere paquete extra. | ||
|
|
||
| ## Variables de entorno | ||
|
|
||
| | Variable | Requerida | Descripción | | ||
| | --- | --- | --- | | ||
| | `LEXMEX_API_KEY` | Sí (o pásala como `api_key=` al instanciar) | API key de LEX-MEX. Se genera en `POST /api/v1/keys` tras registrarte en https://lex-mex.xyz (Google OAuth). Plan VIP ilimitado o pay-as-you-go por créditos. | | ||
|
|
||
| ## Uso | ||
|
|
||
| ```python | ||
| from crewai import Agent | ||
| from crewai_tools import LexMexTool | ||
|
|
||
| abogado = Agent( | ||
| role="Asesor legal", | ||
| goal="Responder dudas de derecho federal mexicano con cita exacta", | ||
| backstory="Experto en legislación federal mexicana.", | ||
| tools=[LexMexTool()], # toma LEXMEX_API_KEY del entorno | ||
| ) | ||
| ``` | ||
|
|
||
| ## Errores comunes | ||
|
|
||
| - `401` → API key inválida o revocada. | ||
| - `402` → sin saldo/plan suficiente para la consulta. | ||
| - `429` → límite diario de consultas alcanzado. |
3 changes: 3 additions & 0 deletions
3
lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool | ||
|
|
||
| __all__ = ["LexMexTool"] |
183 changes: 183 additions & 0 deletions
183
lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| """ | ||
| lexmex_tool.py — LEX-MEX v1.1 | ||
| ═══════════════════════════════════════════════════════════════════════ | ||
| Tool de CrewAI para consultar LEX-MEX (asesor jurídico de leyes | ||
| federales mexicanas) desde cualquier Agent de un Crew. | ||
|
|
||
| Requiere una API key real de LEX-MEX (plan VIP o pay-as-you-go), | ||
| obtenida por un humano en https://lex-mex.xyz tras registrarse | ||
| (Google OAuth). Esta tool NO emite ni gestiona la key, solo la usa. | ||
|
|
||
| Uso: | ||
| from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool | ||
| from crewai import Agent | ||
|
|
||
| abogado = Agent( | ||
| role="Asesor legal", | ||
| goal="Responder dudas de derecho federal mexicano con cita exacta", | ||
| backstory="Experto en legislación federal mexicana.", | ||
| tools=[LexMexTool(api_key="lmx_live_...")], | ||
| ) | ||
|
|
||
| # o vía variable de entorno LEXMEX_API_KEY | ||
| import os | ||
| os.environ["LEXMEX_API_KEY"] = "lmx_live_..." | ||
| tool = LexMexTool() | ||
|
|
||
| ─── Changelog v1.1 ──────────────────────────────────────────────────── | ||
| - Seguridad: api_key ahora se excluye de la serialización del modelo | ||
| (Field(exclude=True, repr=False)) para que nunca quede persistida en | ||
| logs ni en estados de Crew guardados. | ||
| - Seguridad: se eliminó el campo configurable `api_base`; el host | ||
| autenticado ahora es siempre la constante fija LEXMEX_API_BASE, para | ||
| que ningún caller pueda redirigir la API key a un host arbitrario | ||
| (hallazgo de CodeRabbit: API-key exfiltration risk). | ||
| - Se agregó el header X-LexMex-Client para que el backend de LexMex | ||
| pueda distinguir tráfico proveniente de esta tool. | ||
|
|
||
| ─── Changelog v1.2 ──────────────────────────────────────────────────── | ||
| - Seguridad: se agregó `allow_redirects=False` a la llamada HTTP y se | ||
| rechaza cualquier respuesta 3xx, para que la X-API-Key nunca se | ||
| reenvíe a un host distinto de lex-mex.xyz vía redirect (hallazgo de | ||
| CodeRabbit: `requests` no limpia headers custom en redirects). | ||
| - Robustez: se valida que la respuesta JSON sea un objeto, que | ||
| `respuesta` sea un string no vacío y que `fuentes` sea una lista | ||
| antes de formatear la salida, para no devolver un resultado | ||
| aparentemente exitoso ante un JSON malformado o incompleto. | ||
| - Se suavizaron las afirmaciones absolutas de precisión legal en el | ||
| docstring/description ("no alucina", "cita exacta") por lenguaje que | ||
| describe la salida como información legal general y recomienda | ||
| verificarla con la fuente oficial o un abogado. | ||
| - Se simplificó la validación de redirects para depender únicamente de | ||
| `status_code` (evita requerir `is_redirect`/`is_permanent_redirect`, | ||
| que no todo doble de prueba expone) y se blindó `fuentes_txt` contra | ||
| valores `cita` no-string (p. ej. `null`), que antes rompían el join. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import Optional, Type | ||
|
|
||
| import requests | ||
| from pydantic import BaseModel, Field | ||
|
|
||
| try: | ||
| from crewai.tools import BaseTool | ||
| except ImportError as e: # pragma: no cover | ||
| raise ImportError( | ||
| "LexMexTool requiere crewai. Instala con: pip install crewai" | ||
| ) from e | ||
|
|
||
|
|
||
| LEXMEX_API_BASE = "https://lex-mex.xyz" | ||
| LEXMEX_CLIENT_ID = "crewai-lexmex-tool/1.1.0" | ||
|
|
||
|
|
||
| class LexMexInput(BaseModel): | ||
| pregunta: str = Field( | ||
| ..., | ||
| description=( | ||
| "Consulta jurídica en español sobre derecho federal mexicano " | ||
| "(316 leyes federales indexadas, sincronizadas con " | ||
| "diputados.gob.mx). Ej: '¿Cuáles son las causales de despido " | ||
| "justificado según la LFT?'" | ||
| ), | ||
| min_length=1, | ||
| max_length=2000, | ||
| ) | ||
|
|
||
|
|
||
| class LexMexTool(BaseTool): | ||
| """Consulta leyes federales mexicanas con respuestas que citan fuente. | ||
|
|
||
| Motor RAG+LLM de lex-mex.xyz — devuelve información legal general | ||
| sobre derecho federal mexicano junto con la ley y el artículo en los | ||
| que se basó la respuesta. No sustituye asesoría legal profesional: | ||
| el usuario debe verificar la información con la fuente oficial | ||
| vigente (DOF / diputados.gob.mx) o con un abogado antes de tomar | ||
| decisiones legales. Requiere API key de plan VIP o de créditos | ||
| pay-as-you-go. | ||
| """ | ||
|
|
||
| name: str = "Consulta legal LEX-MEX" | ||
| description: str = ( | ||
| "Útil para obtener información legal general sobre leyes " | ||
| "federales mexicanas (laboral, civil, fiscal, penal, " | ||
| "mercantil, etc.). Devuelve la respuesta junto con las fuentes " | ||
| "legales en las que se basó (ley y artículo) y un nivel de " | ||
| "confianza. No sustituye asesoría legal profesional; verifica " | ||
| "la información con la fuente oficial o un abogado antes de " | ||
| "tomar decisiones. Input: una pregunta legal en español, texto " | ||
| "plano." | ||
| ) | ||
| args_schema: Type[BaseModel] = LexMexInput | ||
|
|
||
| # Se excluye de la serialización (model_dump/repr) para que la key | ||
| # nunca quede persistida en logs, estados de Crew guardados, etc. | ||
| api_key: Optional[str] = Field(default=None, exclude=True, repr=False) | ||
| timeout: int = Field(default=30, gt=0, description="Request timeout in seconds.") | ||
|
|
||
| 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(), | ||
| "X-LexMex-Client": LEXMEX_CLIENT_ID, | ||
| }, | ||
| timeout=self.timeout, | ||
| allow_redirects=False, | ||
| ) | ||
|
|
||
| if 300 <= resp.status_code < 400: | ||
| return ( | ||
| "Error: LEX-MEX respondió con una redirección inesperada; " | ||
| "la consulta se abortó por seguridad (la API key no se reenvía " | ||
| "a hosts distintos de lex-mex.xyz)." | ||
| ) | ||
| if resp.status_code == 401: | ||
| return "Error: API key de LEX-MEX inválida o revocada." | ||
| if resp.status_code == 402: | ||
| return "Error: sin saldo/plan suficiente en LEX-MEX para esta consulta." | ||
| if resp.status_code == 429: | ||
| return "Error: límite diario de consultas de LEX-MEX alcanzado." | ||
| resp.raise_for_status() | ||
|
|
||
| try: | ||
| data = resp.json() | ||
| except ValueError: | ||
| return "Error: LEX-MEX devolvió una respuesta no válida (JSON malformado)." | ||
|
|
||
| if not isinstance(data, dict): | ||
| return "Error: LEX-MEX devolvió una respuesta con formato inesperado." | ||
|
|
||
| respuesta = data.get("respuesta") | ||
| if not isinstance(respuesta, str) or not respuesta.strip(): | ||
| return "Error: LEX-MEX no devolvió una respuesta legal válida." | ||
|
|
||
| fuentes = data.get("fuentes") | ||
| if not isinstance(fuentes, list): | ||
| fuentes = [] | ||
| fuentes_txt = "; ".join( | ||
| f.get("cita") | ||
| if isinstance(f, dict) and isinstance(f.get("cita"), str) | ||
| else str(f) | ||
| for f in fuentes | ||
| ) or "sin fuentes citadas" | ||
|
|
||
| return ( | ||
| f"{respuesta}\n\n" | ||
| f"Fuentes: {fuentes_txt}\n" | ||
| f"Confianza: {data.get('confianza', 'n/d')}" | ||
| ) | ||
145 changes: 145 additions & 0 deletions
145
lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import pytest | ||
|
|
||
| from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool | ||
|
|
||
|
|
||
| def test_requires_api_key(monkeypatch): | ||
| monkeypatch.delenv("LEXMEX_API_KEY", raising=False) | ||
| tool = LexMexTool() | ||
| with pytest.raises(ValueError): | ||
| tool._resolved_key() | ||
|
|
||
|
|
||
| def test_reads_key_from_env(monkeypatch): | ||
| monkeypatch.setenv("LEXMEX_API_KEY", "lmx_live_env") | ||
| tool = LexMexTool() | ||
| assert tool._resolved_key() == "lmx_live_env" | ||
|
|
||
|
|
||
| def test_explicit_key_overrides_env(monkeypatch): | ||
| monkeypatch.setenv("LEXMEX_API_KEY", "lmx_live_env") | ||
| tool = LexMexTool(api_key="lmx_live_explicit") | ||
| assert tool._resolved_key() == "lmx_live_explicit" | ||
|
|
||
|
|
||
| def test_api_key_excluded_from_serialization(): | ||
| """CodeRabbit: la key nunca debe aparecer en model_dump/repr.""" | ||
| tool = LexMexTool(api_key="lmx_live_secret") | ||
| dumped = tool.model_dump() | ||
| assert "api_key" not in dumped | ||
| assert "lmx_live_secret" not in repr(tool) | ||
|
|
||
|
|
||
| def test_happy_path(monkeypatch): | ||
| """CodeRabbit: se verifica el contrato real de la llamada saliente | ||
| (URL fija, header X-API-Key, header X-LexMex-Client), no solo el | ||
| resultado final formateado.""" | ||
| tool = LexMexTool(api_key="lmx_live_test") | ||
| captured = {} | ||
|
|
||
| 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", | ||
| } | ||
|
|
||
| def fake_post(url, json, headers, timeout, allow_redirects): | ||
| captured["url"] = url | ||
| captured["json"] = json | ||
| captured["headers"] = headers | ||
| captured["timeout"] = timeout | ||
| captured["allow_redirects"] = allow_redirects | ||
| return FakeResponse() | ||
|
|
||
| monkeypatch.setattr( | ||
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | ||
| fake_post, | ||
| ) | ||
|
|
||
| resultado = tool.run(pregunta="¿Causales de despido justificado?") | ||
|
|
||
| assert captured["url"] == "https://lex-mex.xyz/api/v1/consulta" | ||
| assert captured["json"] == {"pregunta": "¿Causales de despido justificado?"} | ||
| assert captured["headers"]["X-API-Key"] == "lmx_live_test" | ||
| assert captured["headers"]["X-LexMex-Client"] == "crewai-lexmex-tool/1.1.0" | ||
| assert captured["timeout"] == 30 | ||
| assert captured["allow_redirects"] is False | ||
|
|
||
| assert "despido justificado" in resultado | ||
| assert "LFT, art. 47" in resultado | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| assert "alta" in resultado | ||
|
|
||
|
|
||
| def test_unauthorized(monkeypatch): | ||
| tool = LexMexTool(api_key="lmx_live_bad") | ||
|
|
||
| class FakeResponse: | ||
| status_code = 401 | ||
|
|
||
| monkeypatch.setattr( | ||
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | ||
| lambda *a, **kw: FakeResponse(), | ||
| ) | ||
|
|
||
| resultado = tool.run(pregunta="cualquier pregunta") | ||
| assert "inválida" in resultado.lower() | ||
|
|
||
|
|
||
| def test_insufficient_balance(monkeypatch): | ||
| tool = LexMexTool(api_key="lmx_live_test") | ||
|
|
||
| class FakeResponse: | ||
| status_code = 402 | ||
|
|
||
| monkeypatch.setattr( | ||
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | ||
| lambda *a, **kw: FakeResponse(), | ||
| ) | ||
|
|
||
| resultado = tool.run(pregunta="cualquier pregunta") | ||
| assert "saldo" in resultado.lower() | ||
|
|
||
|
|
||
| def test_rate_limited(monkeypatch): | ||
| tool = LexMexTool(api_key="lmx_live_test") | ||
|
|
||
| class FakeResponse: | ||
| status_code = 429 | ||
|
|
||
| monkeypatch.setattr( | ||
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | ||
| lambda *a, **kw: FakeResponse(), | ||
| ) | ||
|
|
||
| resultado = tool.run(pregunta="cualquier pregunta") | ||
| assert "límite" in resultado.lower() | ||
|
|
||
|
|
||
| def test_redirect_is_rejected_without_reading_body(monkeypatch): | ||
| """CodeRabbit: ante un 3xx, la tool debe cortar antes de llamar | ||
| .json() (defensa contra que alguien quite ese return a futuro y | ||
| la key termine yendo a un host fuera de lex-mex.xyz).""" | ||
| tool = LexMexTool(api_key="lmx_live_test") | ||
|
|
||
| class FakeRedirectResponse: | ||
| status_code = 302 | ||
|
|
||
| def json(self): | ||
| raise AssertionError( | ||
| ".json() no debe llamarse ante una respuesta de redirect" | ||
| ) | ||
|
|
||
| monkeypatch.setattr( | ||
| "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", | ||
| lambda *a, **kw: FakeRedirectResponse(), | ||
| ) | ||
|
|
||
| resultado = tool.run(pregunta="cualquier pregunta") | ||
| assert "redirección" in resultado.lower() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.