Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dd9c357
ci: lint fixes [.pre-commit-config.yaml]
DeepRatAI Feb 26, 2026
ce66b41
ci: lint fixes [pyproject.toml]
DeepRatAI Feb 26, 2026
9d35518
ci: lint fixes [src/medex/agent/controller.py]
DeepRatAI Feb 26, 2026
51640a5
ci: lint fixes [src/medex/api/__init__.py]
DeepRatAI Feb 26, 2026
4830f0c
ci: lint fixes [src/medex/api/middleware.py]
DeepRatAI Feb 26, 2026
cd0546f
ci: lint fixes [src/medex/api/routes/tools.py]
DeepRatAI Feb 26, 2026
61e2fd2
ci: lint fixes [src/medex/db/repositories.py]
DeepRatAI Feb 26, 2026
19d3367
ci: lint fixes [src/medex/llm/parser.py]
DeepRatAI Feb 26, 2026
861d01c
ci: lint fixes [src/medex/llm/router.py]
DeepRatAI Feb 26, 2026
abcf756
ci: lint fixes [src/medex/llm/service.py]
DeepRatAI Feb 26, 2026
8093d22
ci: lint fixes [src/medex/medical/formatter.py]
DeepRatAI Feb 26, 2026
1f92501
ci: lint fixes [src/medex/medical/models.py]
DeepRatAI Feb 26, 2026
fc03ae6
ci: lint fixes [src/medex/medical/reasoner.py]
DeepRatAI Feb 26, 2026
5f301fb
ci: lint fixes [src/medex/rag/chunker.py]
DeepRatAI Feb 26, 2026
da6f1c2
ci: lint fixes [src/medex/rag/embedder.py]
DeepRatAI Feb 26, 2026
244e17e
ci: lint fixes [src/medex/rag/reranker.py]
DeepRatAI Feb 26, 2026
e9fadc5
ci: lint fixes [src/medex/security/audit.py]
DeepRatAI Feb 26, 2026
664f503
ci: lint fixes [src/medex/tools/medical/emergency_detector.py]
DeepRatAI Feb 26, 2026
3425f73
ci: lint fixes [tests/e2e/test_ui_e2e.py]
DeepRatAI Feb 26, 2026
eb6045f
ci: lint fixes [tests/test_differential_diagnosis.py]
DeepRatAI Feb 26, 2026
59cd439
ci: lint fixes [tests/test_infrastructure.py]
DeepRatAI Feb 26, 2026
d8a4c06
ci: lint fixes [tests/test_medex_logger.py]
DeepRatAI Feb 26, 2026
b19869c
ci: lint fixes [tests/test_observability.py]
DeepRatAI Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# =============================================================================
# MedeX — Pre-commit Hooks Configuration
# =============================================================================
# Install: pip install pre-commit && pre-commit install
# Run all: pre-commit run --all-files
# Update: pre-commit autoupdate
# =============================================================================

repos:
# ── Black (code formatter) ─────────────────────────────────────────────
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
language_version: python3
args: [--config=pyproject.toml]

# ── Ruff (linter + import sorter) ──────────────────────────────────────
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

# ── General file hygiene ───────────────────────────────────────────────
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-json
exclude: ".vscode/"
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
- id: debug-statements
- id: detect-private-key

# ── Security (bandit) ──────────────────────────────────────────────────
- repo: https://github.com/PyCQA/bandit
rev: 1.8.3
hooks:
- id: bandit
args: [-r, src/, -c, pyproject.toml, -ll]
pass_filenames: false
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ exclude = '''
[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
Expand All @@ -108,7 +110,7 @@ ignore = [
"B008", # do not perform function calls in argument defaults
]

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["medex"]

[tool.mypy]
Expand Down
8 changes: 4 additions & 4 deletions src/medex/agent/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ def set_rag_service(self, service: Any) -> None:

def set_memory_service(self, service: Any) -> None:
"""Set memory service."""
self.memory_service = memory_service
self.memory_service = service

# =========================================================================
# Main Entry Points
Expand Down Expand Up @@ -657,7 +657,7 @@ async def process(
logger.warning(f"Failed to load history: {e}")

# Initialize state
state = self.state_manager.initialize(context)
self.state_manager.initialize(context)

try:
# Run agent loop
Expand Down Expand Up @@ -713,7 +713,7 @@ async def process_stream(
)

# Initialize state
state = self.state_manager.initialize(context)
self.state_manager.initialize(context)

# Setup event forwarding
event_queue: asyncio.Queue[AgentEvent] = asyncio.Queue()
Expand Down Expand Up @@ -837,7 +837,7 @@ async def _handle_emergency(self, context: AgentContext) -> AgentResult:
# Add any tool results
if context.tool_results:
emergency_response += "**Assessment:**\n"
for tool, result in context.tool_results.items():
for _tool, result in context.tool_results.items():
if isinstance(result, dict):
emergency_response += f"- {result.get('recommendation', '')}\n"

Expand Down
4 changes: 1 addition & 3 deletions src/medex/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,11 @@
ConnectionState,
WebSocketHandler,
WSCloseCode,
)
from .websocket import WSMessage as WebSocketMessage
from .websocket import (
WSMessageType,
create_connection_manager,
create_websocket_handler,
)
from .websocket import WSMessage as WebSocketMessage

__all__ = [
# App
Expand Down
2 changes: 1 addition & 1 deletion src/medex/api/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
pass
from .models import ErrorCode


# =============================================================================
Expand Down
13 changes: 7 additions & 6 deletions src/medex/api/routes/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import logging
from datetime import datetime
from typing import Any

Expand Down Expand Up @@ -133,7 +134,7 @@ async def check_interactions(
"""
try:
# Call the tool directly (synchronous, uses local dictionary)
result: DrugInteractionResult = await check_drug_interactions(
result: DrugInteractionResult = await check_drug_interactions( # noqa: F821
drugs=request.drugs
)

Expand Down Expand Up @@ -161,7 +162,7 @@ async def check_interactions(
logger.error(f"Drug interaction check failed: {e}")
raise HTTPException(
status_code=500, detail=f"Error checking interactions: {str(e)}"
)
) from e


# =============================================================================
Expand All @@ -184,7 +185,7 @@ async def calculate_drug_dosage(request: DosageRequest) -> DosageResponse:
"""
try:
# Call the tool directly
result: DosageResult = await calculate_dosage(
result: DosageResult = await calculate_dosage( # noqa: F821
drug_name=request.drug_name,
patient_weight_kg=request.patient_weight,
patient_age_years=request.patient_age,
Expand All @@ -207,7 +208,7 @@ async def calculate_drug_dosage(request: DosageRequest) -> DosageResponse:
logger.error(f"Dosage calculation failed: {e}")
raise HTTPException(
status_code=500, detail=f"Error calculating dosage: {str(e)}"
)
) from e


# =============================================================================
Expand All @@ -230,7 +231,7 @@ async def interpret_lab(request: LabInterpretRequest) -> LabInterpretResponse:
"""
try:
# Call the tool directly
result: LabInterpretationResult = await interpret_lab_value(
result: LabInterpretationResult = await interpret_lab_value( # noqa: F821
test_name=request.test_name,
value=request.value,
unit=request.unit,
Expand All @@ -254,7 +255,7 @@ async def interpret_lab(request: LabInterpretRequest) -> LabInterpretResponse:
logger.error(f"Lab interpretation failed: {e}")
raise HTTPException(
status_code=500, detail=f"Error interpreting lab value: {str(e)}"
)
) from e


# =============================================================================
Expand Down
5 changes: 3 additions & 2 deletions src/medex/db/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,8 +981,9 @@ async def get_metrics_by_tool(
func.avg(ToolExecution.latency_ms).label("avg_latency"),
func.sum(
func.cast(
ToolExecution.cache_hit == True, type_=func.Integer
) # noqa: E712
ToolExecution.cache_hit == True, # noqa: E712
type_=func.Integer,
)
).label("cache_hits"),
)
.where(ToolExecution.tool_name == tool_name)
Expand Down
2 changes: 1 addition & 1 deletion src/medex/llm/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ def _extract_entities(self, content: str) -> dict[str, list[str]]:
# Extract CIE-10 codes
if self.config.extract_cie10:
cie10_matches = CIE10_PATTERN.findall(content)
entities["cie10_codes"] = list(set(c.upper() for c in cie10_matches))
entities["cie10_codes"] = list({c.upper() for c in cie10_matches})

# Extract drug mentions
if self.config.extract_drugs:
Expand Down
2 changes: 1 addition & 1 deletion src/medex/llm/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ async def stream(self, request: LLMRequest) -> AsyncIterator[StreamChunk]:

# Emit finish event
latency_ms = (time.time() - start_time) * 1000
ttft_ms = (
_ttft_ms = (
(first_token_time - start_time) * 1000 if first_token_time else None
)

Expand Down
2 changes: 1 addition & 1 deletion src/medex/llm/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async def query(
)

# Execute request
start_time = time.time()
_start_time = time.time()

if use_stream:
# Collect streaming response
Expand Down
1 change: 1 addition & 0 deletions src/medex/medical/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DiagnosticHypothesis,
DiagnosticPlan,
Medication,
Specialty,
TreatmentPlan,
TriageAssessment,
TriageLevel,
Expand Down
2 changes: 1 addition & 1 deletion src/medex/medical/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ def to_dict(self) -> dict[str, Any]:
"symptoms": [s.to_dict() for s in self.symptoms],
"vital_signs": self.vital_signs.to_dict() if self.vital_signs else None,
"physical_exam": self.physical_exam,
"lab_values": [l.to_dict() for l in self.lab_values],
"lab_values": [lv.to_dict() for lv in self.lab_values],
"imaging": self.imaging,
"triage": self.triage.to_dict() if self.triage else None,
"differential_diagnosis": [
Expand Down
12 changes: 6 additions & 6 deletions src/medex/medical/reasoner.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def analyze(self, case: ClinicalCase) -> list[DiagnosticHypothesis]:

# Build hypotheses
hypotheses = []
for dx_id, score, dx_pattern in scored_diagnoses[
for _dx_id, score, dx_pattern in scored_diagnoses[
: self.config.max_differential
]:
hypothesis = self._build_hypothesis(
Expand Down Expand Up @@ -425,7 +425,7 @@ def _calculate_diagnosis_score(
pattern_labs = dx_pattern.get("labs", [])
if pattern_labs:
lab_text = " ".join(lab_findings)
matches = sum(1 for l in pattern_labs if l in lab_text)
matches = sum(1 for lab in pattern_labs if lab in lab_text)
lab_score = matches / len(pattern_labs)
elif lab_findings:
lab_score = 0.1 # Some credit for having labs
Expand Down Expand Up @@ -464,9 +464,9 @@ def _build_hypothesis(
if s in symptoms_text:
supporting.append(f"Presenta: {s}")

for l in dx_pattern.get("labs", []):
if any(l in lf for lf in lab_findings):
supporting.append(f"Laboratorio: {l}")
for lab in dx_pattern.get("labs", []):
if any(lab in lf for lf in lab_findings):
supporting.append(f"Laboratorio: {lab}")

# Determine next steps
next_steps = self._get_diagnostic_steps(dx_pattern, case)
Expand Down Expand Up @@ -539,7 +539,7 @@ def _get_diagnostic_steps(
)

# Always add basic labs if not already ordered
existing_labs = {l.name.lower() for l in case.lab_values}
existing_labs = {lv.name.lower() for lv in case.lab_values}
if "hemograma" not in existing_labs and "hemoglobina" not in existing_labs:
steps.append("Hemograma completo")
if "bioquímica" not in existing_labs:
Expand Down
2 changes: 1 addition & 1 deletion src/medex/rag/chunker.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def chunk(self, document: Document) -> list[Chunk]:
sections = self._split_into_sections(content)

char_offset = 0
for section_idx, (header, section_content) in enumerate(sections):
for _section_idx, (header, section_content) in enumerate(sections):
section_chunks = self._chunk_section(
section_content,
document_id=document.id,
Expand Down
6 changes: 3 additions & 3 deletions src/medex/rag/embedder.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async def embed_chunks(self, chunks: list[Chunk]) -> list[Chunk]:
texts = [c.content for c in chunks]
embeddings = await self.embed_texts(texts)

for chunk, embedding in zip(chunks, embeddings):
for chunk, embedding in zip(chunks, embeddings, strict=False):
chunk.embedding = embedding.vector

return chunks
Expand Down Expand Up @@ -300,7 +300,7 @@ async def embed_texts(self, texts: list[str]) -> list[Embedding]:
all_vectors.extend(batch_vectors)

# Fill in results and cache
for idx, vector in zip(uncached_indices, all_vectors):
for idx, vector in zip(uncached_indices, all_vectors, strict=False):
embeddings[idx] = Embedding(
vector=vector,
model=self.config.model_name,
Expand Down Expand Up @@ -428,7 +428,7 @@ async def embed_texts(self, texts: list[str]) -> list[Embedding]:
all_vectors.extend(batch_vectors)

# Fill results
for idx, vector in zip(uncached_indices, all_vectors):
for idx, vector in zip(uncached_indices, all_vectors, strict=False):
if self.config.normalize:
vector = self._normalize(vector)

Expand Down
5 changes: 3 additions & 2 deletions src/medex/rag/reranker.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ async def rerank(
normalized_scores = [(s - min_score) / score_range for s in scores]

# Update results with rerank scores
for result, rerank_score in zip(results, normalized_scores):
for result, rerank_score in zip(results, normalized_scores, strict=False):
result.rerank_score = float(rerank_score)
result.relevance = self._score_to_relevance(rerank_score)

Expand Down Expand Up @@ -540,7 +540,8 @@ async def rerank(
chunk_scores = all_scores[result.chunk.id]
if chunk_scores:
weighted_score = (
sum(s * w for s, w in zip(chunk_scores, weights)) / total_weight
sum(s * w for s, w in zip(chunk_scores, weights, strict=False))
/ total_weight
)
result.rerank_score = weighted_score
result.relevance = self._score_to_relevance(weighted_score)
Expand Down
2 changes: 1 addition & 1 deletion src/medex/security/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def count(self, query: AuditQuery) -> int:
"""Count matching events."""
pass

async def close(self) -> None:
async def close(self) -> None: # noqa: B027
"""Close backend connection."""
pass

Expand Down
4 changes: 2 additions & 2 deletions src/medex/tools/medical/emergency_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ async def detect_emergency(
symptoms_text = " ".join(symptoms_lower)

# Check each red flag pattern
for flag_id, flag_data in RED_FLAGS.items():
for _flag_id, flag_data in RED_FLAGS.items():
matched = False

# Check primary symptoms
Expand Down Expand Up @@ -596,7 +596,7 @@ async def check_critical_values(
normalized_values[normalized_key] = value

# Check each critical value
for crit_id, crit_data in CRITICAL_VALUES.items():
for _crit_id, crit_data in CRITICAL_VALUES.items():
param = crit_data["parameter"].lower().replace(" ", "_")

# Check if this parameter is in the provided values
Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/test_ui_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ def test_page_has_lang_attribute(self, page: Page, medex_url: str):
"""HTML has lang attribute"""
page.goto(medex_url, wait_until="networkidle")

html_lang = page.locator("html").get_attribute("lang")
# Streamlit may not set this, so we just check it exists
# assert html_lang is not None
_html_lang = page.locator("html").get_attribute("lang")
# Reflex should set lang attribute
# assert _html_lang is not None

@pytest.mark.e2e
def test_images_have_alt_text(self, page: Page, medex_url: str):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_differential_diagnosis.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def test_fuzzy_match_synonyms(self):
("vomitos", "náuseas"), # Similar
]

for query, expected_symptom in test_cases:
for query, _expected_symptom in test_cases:
result = get_differential_for_symptom(query)
# Puede que no todos hagan match fuzzy, pero verificamos que no crashee
# y que los que sí existen funcionen
Expand Down
Loading
Loading