Padmanav commited on
Commit
f8b17b7
·
1 Parent(s): 93b849b
.github/workflows/ci.yml CHANGED
@@ -22,6 +22,9 @@ jobs:
22
  - name: Lint
23
  run: ruff check app/ tests/
24
 
 
 
 
25
  - name: Security scan
26
  run: bandit -r app/ -q
27
 
 
22
  - name: Lint
23
  run: ruff check app/ tests/
24
 
25
+ - name: Type check
26
+ run: mypy app/
27
+
28
  - name: Security scan
29
  run: bandit -r app/ -q
30
 
app/api/dependencies.py CHANGED
@@ -5,7 +5,7 @@ from app.core.config import get_settings
5
  settings = get_settings()
6
 
7
 
8
- def verify_api_key(x_api_key: Optional[str] = Header(default=None)):
9
  if not settings.api_key:
10
  # API_KEY not configured — open access (dev/HF Spaces mode)
11
  return
 
5
  settings = get_settings()
6
 
7
 
8
+ def verify_api_key(x_api_key: Optional[str] = Header(default=None)) -> None:
9
  if not settings.api_key:
10
  # API_KEY not configured — open access (dev/HF Spaces mode)
11
  return
app/api/main.py CHANGED
@@ -29,7 +29,7 @@ app.include_router(router, prefix="/api/v1")
29
 
30
 
31
  @app.get("/")
32
- def root():
33
  return {
34
  "message": "AI Code Review Agent",
35
  "version": "0.1.0",
 
29
 
30
 
31
  @app.get("/")
32
+ def root() -> dict[str, str]:
33
  return {
34
  "message": "AI Code Review Agent",
35
  "version": "0.1.0",
app/api/routes.py CHANGED
@@ -4,6 +4,7 @@ from app.core.logger import get_logger
4
  from fastapi import APIRouter, HTTPException, Depends
5
  from app.api.dependencies import verify_api_key
6
  from app.api.limiter import limiter
 
7
  from app.core.metrics import metrics
8
  from app.models.repository import RepositoryRequest, RepositoryResponse
9
  from app.models.report import EngineeringReport
@@ -22,12 +23,12 @@ logger = get_logger(__name__)
22
 
23
 
24
  @router.get("/health")
25
- def health_check():
26
  return {"status": "ok", "message": "AI Code Review Agent is running"}
27
 
28
  @limiter.limit("5/minute")
29
  @router.post("/analyze", response_model=EngineeringReport, dependencies=[Depends(verify_api_key)])
30
- async def analyze_repository(request: RepositoryRequest):
31
  """
32
  Full pipeline:
33
  1. Clone repository
@@ -75,7 +76,7 @@ async def analyze_repository(request: RepositoryRequest):
75
 
76
  @limiter.limit("10/minute")
77
  @router.post("/quick-analyze", response_model=RepositoryResponse, dependencies=[Depends(verify_api_key)])
78
- async def quick_analyze(request: RepositoryRequest):
79
  """
80
  Only run repository analysis agent.
81
  Faster, for testing purposes.
@@ -100,6 +101,6 @@ async def quick_analyze(request: RepositoryRequest):
100
  delete_repository(local_path)
101
 
102
  @router.get("/metrics")
103
- def get_metrics():
104
  """Observability endpoint: agent run counts, error rates, latencies."""
105
  return metrics.snapshot()
 
4
  from fastapi import APIRouter, HTTPException, Depends
5
  from app.api.dependencies import verify_api_key
6
  from app.api.limiter import limiter
7
+ from typing import Any
8
  from app.core.metrics import metrics
9
  from app.models.repository import RepositoryRequest, RepositoryResponse
10
  from app.models.report import EngineeringReport
 
23
 
24
 
25
  @router.get("/health")
26
+ def health_check() -> dict[str, str]:
27
  return {"status": "ok", "message": "AI Code Review Agent is running"}
28
 
29
  @limiter.limit("5/minute")
30
  @router.post("/analyze", response_model=EngineeringReport, dependencies=[Depends(verify_api_key)])
31
+ async def analyze_repository(request: RepositoryRequest) -> EngineeringReport:
32
  """
33
  Full pipeline:
34
  1. Clone repository
 
76
 
77
  @limiter.limit("10/minute")
78
  @router.post("/quick-analyze", response_model=RepositoryResponse, dependencies=[Depends(verify_api_key)])
79
+ async def quick_analyze(request: RepositoryRequest) -> RepositoryResponse:
80
  """
81
  Only run repository analysis agent.
82
  Faster, for testing purposes.
 
101
  delete_repository(local_path)
102
 
103
  @router.get("/metrics")
104
+ def get_metrics() -> dict[str, Any]:
105
  """Observability endpoint: agent run counts, error rates, latencies."""
106
  return metrics.snapshot()
app/core/config.py CHANGED
@@ -1,12 +1,11 @@
1
  import tempfile
2
  import os
3
- from pydantic_settings import BaseSettings
4
- from pydantic import ConfigDict
5
  from functools import lru_cache
6
 
7
 
8
  class Settings(BaseSettings):
9
- model_config = ConfigDict(env_file=".env", case_sensitive=False, extra="ignore")
10
 
11
  # Set OPENROUTER_API_KEY as a HF Space Secret
12
  openrouter_api_key: str = ""
 
1
  import tempfile
2
  import os
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
 
4
  from functools import lru_cache
5
 
6
 
7
  class Settings(BaseSettings):
8
+ model_config = SettingsConfigDict(env_file=".env", case_sensitive=False, extra="ignore")
9
 
10
  # Set OPENROUTER_API_KEY as a HF Space Secret
11
  openrouter_api_key: str = ""
app/core/llm.py CHANGED
@@ -2,6 +2,7 @@ import httpx
2
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
3
  from app.core.config import get_settings
4
  from app.core.logger import get_logger
 
5
 
6
  settings = get_settings()
7
  logger = get_logger(__name__)
@@ -18,7 +19,7 @@ OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
18
  extra={"attempt": rs.attempt_number, "error": str(rs.outcome.exception())},
19
  ),
20
  )
21
- async def call_llm(prompt: str, system_prompt: str = None, max_tokens: int = 2000) -> str:
22
  """Async call to OpenRouter API with retry logic."""
23
  messages = []
24
  if system_prompt:
@@ -45,4 +46,4 @@ async def call_llm(prompt: str, system_prompt: str = None, max_tokens: int = 200
45
  response.raise_for_status()
46
 
47
  data = response.json()
48
- return data["choices"][0]["message"]["content"]
 
2
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
3
  from app.core.config import get_settings
4
  from app.core.logger import get_logger
5
+ from typing import Optional
6
 
7
  settings = get_settings()
8
  logger = get_logger(__name__)
 
19
  extra={"attempt": rs.attempt_number, "error": str(rs.outcome.exception())},
20
  ),
21
  )
22
+ async def call_llm(prompt: str, system_prompt: Optional[str] = None, max_tokens: int = 2000) -> str:
23
  """Async call to OpenRouter API with retry logic."""
24
  messages = []
25
  if system_prompt:
 
46
  response.raise_for_status()
47
 
48
  data = response.json()
49
+ return str(data["choices"][0]["message"]["content"])
app/core/metrics.py CHANGED
@@ -1,25 +1,27 @@
1
  import time
2
  from collections import defaultdict
3
  from threading import Lock
 
 
4
 
5
  class MetricsCollector:
6
- def __init__(self):
7
  self._lock = Lock()
8
  self._counts: dict[str, int] = defaultdict(int)
9
  self._latencies: dict[str, list[float]] = defaultdict(list)
10
  self._errors: dict[str, int] = defaultdict(int)
11
  self._started_at = time.time()
12
 
13
- def record_run(self, agent: str, duration_seconds: float, success: bool):
14
  with self._lock:
15
  self._counts[agent] += 1
16
  self._latencies[agent].append(round(duration_seconds, 3))
17
  if not success:
18
  self._errors[agent] += 1
19
 
20
- def snapshot(self) -> dict:
21
  with self._lock:
22
- agents = {}
23
  for agent, count in self._counts.items():
24
  lats = self._latencies[agent]
25
  agents[agent] = {
@@ -33,4 +35,5 @@ class MetricsCollector:
33
  "agents": agents,
34
  }
35
 
 
36
  metrics = MetricsCollector()
 
1
  import time
2
  from collections import defaultdict
3
  from threading import Lock
4
+ from typing import Any
5
+
6
 
7
  class MetricsCollector:
8
+ def __init__(self) -> None:
9
  self._lock = Lock()
10
  self._counts: dict[str, int] = defaultdict(int)
11
  self._latencies: dict[str, list[float]] = defaultdict(list)
12
  self._errors: dict[str, int] = defaultdict(int)
13
  self._started_at = time.time()
14
 
15
+ def record_run(self, agent: str, duration_seconds: float, success: bool) -> None:
16
  with self._lock:
17
  self._counts[agent] += 1
18
  self._latencies[agent].append(round(duration_seconds, 3))
19
  if not success:
20
  self._errors[agent] += 1
21
 
22
+ def snapshot(self) -> dict[str, Any]:
23
  with self._lock:
24
+ agents: dict[str, Any] = {}
25
  for agent, count in self._counts.items():
26
  lats = self._latencies[agent]
27
  agents[agent] = {
 
35
  "agents": agents,
36
  }
37
 
38
+
39
  metrics = MetricsCollector()
app/models/issue.py CHANGED
@@ -1,5 +1,5 @@
1
  from pydantic import BaseModel, model_validator, field_validator
2
- from typing import List, Optional
3
 
4
 
5
  class Bug(BaseModel):
@@ -10,7 +10,7 @@ class Bug(BaseModel):
10
 
11
  @field_validator("file", mode="before")
12
  @classmethod
13
- def coerce_file(cls, v):
14
  if isinstance(v, list):
15
  return ", ".join(str(f) for f in v) if v else "unknown"
16
  return v or "unknown"
@@ -23,7 +23,7 @@ class Warning(BaseModel):
23
 
24
  @field_validator("file", mode="before")
25
  @classmethod
26
- def coerce_file(cls, v):
27
  if isinstance(v, list):
28
  return ", ".join(str(f) for f in v) if v else "unknown"
29
  return v or "unknown"
@@ -35,7 +35,7 @@ class Suggestion(BaseModel):
35
 
36
  @field_validator("file", mode="before")
37
  @classmethod
38
- def coerce_file(cls, v):
39
  if isinstance(v, list):
40
  return ", ".join(str(f) for f in v) if v else "unknown"
41
  return v or "unknown"
 
1
  from pydantic import BaseModel, model_validator, field_validator
2
+ from typing import List, Optional, Any
3
 
4
 
5
  class Bug(BaseModel):
 
10
 
11
  @field_validator("file", mode="before")
12
  @classmethod
13
+ def coerce_file(cls, v: Any) -> str:
14
  if isinstance(v, list):
15
  return ", ".join(str(f) for f in v) if v else "unknown"
16
  return v or "unknown"
 
23
 
24
  @field_validator("file", mode="before")
25
  @classmethod
26
+ def coerce_file(cls, v: Any) -> str:
27
  if isinstance(v, list):
28
  return ", ".join(str(f) for f in v) if v else "unknown"
29
  return v or "unknown"
 
35
 
36
  @field_validator("file", mode="before")
37
  @classmethod
38
+ def coerce_file(cls, v: Any) -> str:
39
  if isinstance(v, list):
40
  return ", ".join(str(f) for f in v) if v else "unknown"
41
  return v or "unknown"
app/services/.gitkeep DELETED
File without changes
app/services/__init__.py DELETED
File without changes
app/tools/ast_parser.py CHANGED
@@ -1,75 +1,53 @@
1
  import ast
2
  import os
3
- from typing import List, Dict
4
 
5
 
6
- def parse_python_file(file_path: str) -> Dict:
7
- """
8
- Parse a Python file using AST.
9
- Returns functions, classes, and imports found.
10
- """
11
- result = {
12
  "file": file_path,
13
  "functions": [],
14
  "classes": [],
15
  "imports": [],
16
- "errors": []
17
  }
18
-
19
  try:
20
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
21
  source = f.read()
22
-
23
  tree = ast.parse(source)
24
-
25
  for node in ast.walk(tree):
26
- # Extract functions
27
  if isinstance(node, ast.FunctionDef):
28
  result["functions"].append({
29
  "name": node.name,
30
  "line": node.lineno,
31
  "args": [arg.arg for arg in node.args.args],
32
- "docstring": ast.get_docstring(node) or ""
33
  })
34
-
35
- # Extract classes
36
  elif isinstance(node, ast.ClassDef):
37
  result["classes"].append({
38
  "name": node.name,
39
  "line": node.lineno,
40
- "docstring": ast.get_docstring(node) or ""
41
  })
42
-
43
- # Extract imports
44
  elif isinstance(node, ast.Import):
45
  for alias in node.names:
46
  result["imports"].append(alias.name)
47
-
48
  elif isinstance(node, ast.ImportFrom):
49
- module = node.module or ""
50
- result["imports"].append(module)
51
-
52
  except SyntaxError as e:
53
  result["errors"].append(f"SyntaxError: {str(e)}")
54
  except Exception as e:
55
  result["errors"].append(f"Error: {str(e)}")
56
-
57
  return result
58
 
59
 
60
- def parse_repository(local_path: str) -> List[Dict]:
61
- """
62
- Parse all Python files in a repository.
63
- """
64
- results = []
65
-
66
  for root, dirs, files in os.walk(local_path):
67
  dirs[:] = [d for d in dirs if d not in {
68
  ".git", "__pycache__", ".venv", "venv", "node_modules"
69
  }]
70
  for file in files:
71
  if file.endswith(".py"):
72
- file_path = os.path.join(root, file)
73
- results.append(parse_python_file(file_path))
74
-
75
  return results
 
1
  import ast
2
  import os
3
+ from typing import Any
4
 
5
 
6
+ def parse_python_file(file_path: str) -> dict[str, Any]:
7
+ result: dict[str, Any] = {
 
 
 
 
8
  "file": file_path,
9
  "functions": [],
10
  "classes": [],
11
  "imports": [],
12
+ "errors": [],
13
  }
 
14
  try:
15
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
16
  source = f.read()
 
17
  tree = ast.parse(source)
 
18
  for node in ast.walk(tree):
 
19
  if isinstance(node, ast.FunctionDef):
20
  result["functions"].append({
21
  "name": node.name,
22
  "line": node.lineno,
23
  "args": [arg.arg for arg in node.args.args],
24
+ "docstring": ast.get_docstring(node) or "",
25
  })
 
 
26
  elif isinstance(node, ast.ClassDef):
27
  result["classes"].append({
28
  "name": node.name,
29
  "line": node.lineno,
30
+ "docstring": ast.get_docstring(node) or "",
31
  })
 
 
32
  elif isinstance(node, ast.Import):
33
  for alias in node.names:
34
  result["imports"].append(alias.name)
 
35
  elif isinstance(node, ast.ImportFrom):
36
+ result["imports"].append(node.module or "")
 
 
37
  except SyntaxError as e:
38
  result["errors"].append(f"SyntaxError: {str(e)}")
39
  except Exception as e:
40
  result["errors"].append(f"Error: {str(e)}")
 
41
  return result
42
 
43
 
44
+ def parse_repository(local_path: str) -> list[dict[str, Any]]:
45
+ results: list[dict[str, Any]] = []
 
 
 
 
46
  for root, dirs, files in os.walk(local_path):
47
  dirs[:] = [d for d in dirs if d not in {
48
  ".git", "__pycache__", ".venv", "venv", "node_modules"
49
  }]
50
  for file in files:
51
  if file.endswith(".py"):
52
+ results.append(parse_python_file(os.path.join(root, file)))
 
 
53
  return results
app/tools/file_scanner.py CHANGED
@@ -40,7 +40,7 @@ def scan_repository(local_path: str) -> Dict:
40
  Scan a repository and return metadata.
41
  """
42
  all_files = []
43
- language_counts = {}
44
  total_lines = 0
45
  frameworks_found = set()
46
 
@@ -74,7 +74,7 @@ def scan_repository(local_path: str) -> Dict:
74
  # Detect primary language
75
  primary_language = "Unknown"
76
  if language_counts:
77
- primary_language = max(language_counts, key=language_counts.get)
78
 
79
  return {
80
  "all_files": all_files,
 
40
  Scan a repository and return metadata.
41
  """
42
  all_files = []
43
+ language_counts: dict[str, int] = {}
44
  total_lines = 0
45
  frameworks_found = set()
46
 
 
74
  # Detect primary language
75
  primary_language = "Unknown"
76
  if language_counts:
77
+ primary_language = max(language_counts, key=lambda k: language_counts[k])
78
 
79
  return {
80
  "all_files": all_files,
app/tools/github_tool.py CHANGED
@@ -41,8 +41,8 @@ def clone_repository(github_url: str) -> str:
41
 
42
  logger.info("Clone complete", extra={"path": str(local_path), "size_mb": round(size_mb, 1)})
43
  return str(local_path)
44
-
45
- def delete_repository(local_path: str):
46
  path = Path(local_path)
47
  if path.exists():
48
  shutil.rmtree(path, ignore_errors=True)
 
41
 
42
  logger.info("Clone complete", extra={"path": str(local_path), "size_mb": round(size_mb, 1)})
43
  return str(local_path)
44
+
45
+ def delete_repository(local_path: str) -> None:
46
  path = Path(local_path)
47
  if path.exists():
48
  shutil.rmtree(path, ignore_errors=True)
app/tools/pytest_runner.py CHANGED
@@ -1,32 +1,18 @@
1
  import subprocess # nosec B404
2
- from typing import Dict
3
 
4
 
5
- def run_tests(local_path: str) -> Dict:
6
- """
7
- Run pytest on a repository and return results.
8
- """
9
- result = {
10
- "passed": 0,
11
- "failed": 0,
12
- "errors": 0,
13
- "output": "",
14
- "success": False
15
  }
16
-
17
  try:
18
  process = subprocess.run( # nosec
19
  ["pytest", "--tb=short", "-q", "--no-header"],
20
- cwd=local_path,
21
- capture_output=True,
22
- text=True,
23
- timeout=120
24
  )
25
-
26
  result["output"] = process.stdout + process.stderr
27
-
28
- # Parse basic counts from output
29
- output = result["output"]
30
  if "passed" in output:
31
  result["success"] = True
32
  for line in output.splitlines():
@@ -39,12 +25,10 @@ def run_tests(local_path: str) -> Dict:
39
  result["failed"] = int(parts[i - 1])
40
  elif part == "error" and i > 0:
41
  result["errors"] = int(parts[i - 1])
42
-
43
  except subprocess.TimeoutExpired:
44
  result["output"] = "Tests timed out after 120 seconds"
45
  except FileNotFoundError:
46
  result["output"] = "pytest not found in repository"
47
  except Exception as e:
48
  result["output"] = f"Error running tests: {str(e)}"
49
-
50
  return result
 
1
  import subprocess # nosec B404
2
+ from typing import Any
3
 
4
 
5
+ def run_tests(local_path: str) -> dict[str, Any]:
6
+ result: dict[str, Any] = {
7
+ "passed": 0, "failed": 0, "errors": 0, "output": "", "success": False
 
 
 
 
 
 
 
8
  }
 
9
  try:
10
  process = subprocess.run( # nosec
11
  ["pytest", "--tb=short", "-q", "--no-header"],
12
+ cwd=local_path, capture_output=True, text=True, timeout=120
 
 
 
13
  )
 
14
  result["output"] = process.stdout + process.stderr
15
+ output: str = str(result["output"])
 
 
16
  if "passed" in output:
17
  result["success"] = True
18
  for line in output.splitlines():
 
25
  result["failed"] = int(parts[i - 1])
26
  elif part == "error" and i > 0:
27
  result["errors"] = int(parts[i - 1])
 
28
  except subprocess.TimeoutExpired:
29
  result["output"] = "Tests timed out after 120 seconds"
30
  except FileNotFoundError:
31
  result["output"] = "pytest not found in repository"
32
  except Exception as e:
33
  result["output"] = f"Error running tests: {str(e)}"
 
34
  return result
app/tools/static_analyzer.py CHANGED
@@ -1,66 +1,44 @@
1
  import subprocess # nosec B404
2
- from typing import Dict
3
 
4
 
5
- def run_ruff(local_path: str) -> Dict:
6
- """
7
- Run Ruff linter on repository.
8
- """
9
- result = {"output": "", "issues": [], "success": False}
10
-
11
  try:
12
  process = subprocess.run( # nosec
13
  ["ruff", "check", ".", "--output-format=text"],
14
- cwd=local_path,
15
- capture_output=True,
16
- text=True,
17
- timeout=60
18
  )
19
  result["output"] = process.stdout + process.stderr
20
  result["issues"] = [
21
- line for line in result["output"].splitlines() if line.strip()
22
  ]
23
  result["success"] = True
24
-
25
  except Exception as e:
26
  result["output"] = f"Ruff error: {str(e)}"
27
-
28
  return result
29
 
30
 
31
- def run_bandit(local_path: str) -> Dict:
32
- """
33
- Run Bandit security checker on repository.
34
- """
35
- result = {"output": "", "issues": [], "success": False}
36
-
37
  try:
38
  process = subprocess.run( # nosec
39
  ["bandit", "-r", ".", "-f", "txt", "-q"],
40
- cwd=local_path,
41
- capture_output=True,
42
- text=True,
43
- timeout=60
44
  )
45
  result["output"] = process.stdout + process.stderr
46
  result["issues"] = [
47
- line for line in result["output"].splitlines() if line.strip()
48
  ]
49
  result["success"] = True
50
-
51
  except Exception as e:
52
  result["output"] = f"Bandit error: {str(e)}"
53
-
54
  return result
55
 
56
 
57
- def run_full_analysis(local_path: str) -> Dict:
58
- """
59
- Run all static analysis tools.
60
- """
61
  ruff_results = run_ruff(local_path)
62
  bandit_results = run_bandit(local_path)
63
-
64
  return {
65
  "ruff": ruff_results,
66
  "bandit": bandit_results,
 
1
  import subprocess # nosec B404
2
+ from typing import Any
3
 
4
 
5
+ def run_ruff(local_path: str) -> dict[str, Any]:
6
+ result: dict[str, Any] = {"output": "", "issues": [], "success": False}
 
 
 
 
7
  try:
8
  process = subprocess.run( # nosec
9
  ["ruff", "check", ".", "--output-format=text"],
10
+ cwd=local_path, capture_output=True, text=True, timeout=60
 
 
 
11
  )
12
  result["output"] = process.stdout + process.stderr
13
  result["issues"] = [
14
+ line for line in str(result["output"]).splitlines() if line.strip()
15
  ]
16
  result["success"] = True
 
17
  except Exception as e:
18
  result["output"] = f"Ruff error: {str(e)}"
 
19
  return result
20
 
21
 
22
+ def run_bandit(local_path: str) -> dict[str, Any]:
23
+ result: dict[str, Any] = {"output": "", "issues": [], "success": False}
 
 
 
 
24
  try:
25
  process = subprocess.run( # nosec
26
  ["bandit", "-r", ".", "-f", "txt", "-q"],
27
+ cwd=local_path, capture_output=True, text=True, timeout=60
 
 
 
28
  )
29
  result["output"] = process.stdout + process.stderr
30
  result["issues"] = [
31
+ line for line in str(result["output"]).splitlines() if line.strip()
32
  ]
33
  result["success"] = True
 
34
  except Exception as e:
35
  result["output"] = f"Bandit error: {str(e)}"
 
36
  return result
37
 
38
 
39
+ def run_full_analysis(local_path: str) -> dict[str, Any]:
 
 
 
40
  ruff_results = run_ruff(local_path)
41
  bandit_results = run_bandit(local_path)
 
42
  return {
43
  "ruff": ruff_results,
44
  "bandit": bandit_results,
pyproject.toml CHANGED
@@ -19,4 +19,14 @@ asyncio_mode = "auto"
19
  asyncio_default_fixture_loop_scope = "function"
20
 
21
  [tool.bandit]
22
- exclude_dirs = ["tests", ".venv"]
 
 
 
 
 
 
 
 
 
 
 
19
  asyncio_default_fixture_loop_scope = "function"
20
 
21
  [tool.bandit]
22
+ exclude_dirs = ["tests", ".venv"]
23
+
24
+ [tool.mypy]
25
+ python_version = "3.11"
26
+ strict = false
27
+ ignore_missing_imports = true
28
+ disallow_untyped_defs = true
29
+ disallow_untyped_calls = false
30
+ warn_return_any = true
31
+ warn_unused_ignores = true
32
+ exclude = ["tests/", "gradio_app/"]
requirements.txt CHANGED
@@ -27,3 +27,4 @@ pytest-asyncio
27
  fastapi==0.115.12
28
  uvicorn==0.34.3
29
  tenacity==8.3.0
 
 
27
  fastapi==0.115.12
28
  uvicorn==0.34.3
29
  tenacity==8.3.0
30
+ mypy==1.10.0