Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ jobs:
python -m pip install --upgrade pip
python -m pip install hatchling
python -m pip install -e .
python -m pip install -r requirements.txt
python -m pip install -r requirements-dev.txt
- name: Run static analysis
run: |
ruff check src/
mypy src/
bandit -c pyproject.toml -r src/
- name: Run tests
run: |
PYTHONPATH=src python -m unittest discover -s tests -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ target/
build/
__pycache__/
.venv/
output/
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.5
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ MACA uses Google Gemini and Anthropic Claude for medium and above tasks. For sec
PYTHONPATH=src python -m unittest discover -s tests -v
```

### 4. Development & Static Analysis
We enforce code quality and security checks on both local builds and CI pipelines:
- **Ruff**: Code linting and formatting (auto-runs on commit).
- **Mypy**: Strict type-checking.
- **Bandit**: Security scanning for potential vulnerabilities.

The `./local/scripts/install_mac.sh` launcher script handles setting these up automatically. It will:
1. Install all dependencies from `requirements-dev.txt`.
2. Install **pre-commit** hooks (`pre-commit install`) that auto-fix code style issues on commit.
3. Run Ruff, Mypy, and Bandit audits locally. If any check fails, the build script exits with a non-zero exit code.

You can also run analysis manually inside your virtual environment:
```sh
# Run linter checks
ruff check src/

# Run type checker
mypy src/

# Run security checks
bandit -c pyproject.toml -r src/
```

---

## 💻 How to Use
Expand Down
22 changes: 22 additions & 0 deletions local/scripts/install_mac.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ python -m pip install --upgrade pip setuptools wheel
python -m pip install hatchling
python -m pip install -e .

echo "Installing dev dependencies..."
python -m pip install -r requirements-dev.txt

echo "Setting up pre-commit hooks..."
pre-commit install

echo "Running static analysis (Ruff, Mypy & Bandit)..."
if ! ruff check src/; then
echo "Build failed: Ruff linting errors found." >&2
exit 1
fi

if ! mypy src/; then
echo "Build failed: Mypy type-checking errors found." >&2
exit 1
fi

if ! bandit -c pyproject.toml -r src/; then
echo "Build failed: Bandit security issues found." >&2
exit 1
fi

if [ -x "$ROOT_DIR/local/scripts/setup_gemma.sh" ]; then
"$ROOT_DIR/local/scripts/setup_gemma.sh"
fi
Expand Down
27 changes: 27 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,30 @@ maca = "maca.main:main"

[tool.hatch.build.targets.wheel]
packages = ["src/maca"]

[tool.ruff]
line-length = 100
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E501"]

[tool.ruff.format]
quote-style = "double"

[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true

[[tool.mypy.overrides]]
module = "rich.*"
ignore_missing_imports = true

[tool.bandit]
exclude_dirs = ["tests", ".venv"]
skips = ["B101", "B110", "B310", "B404", "B603", "B607"]
15 changes: 15 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Development & testing dependencies for MACA.
#
# Install with:
# python -m pip install -r requirements-dev.txt
#
# These mirror the optional "dev" extra in pyproject.toml.

-r requirements.txt

pytest>=7.0.0
ruff>=0.1.0
black>=23.0.0
mypy>=1.0.0
bandit>=1.7.0
pre-commit>=3.5.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from setuptools import setup, find_packages
from setuptools import find_packages, setup

setup(
name="maca-ai",
Expand Down
7 changes: 4 additions & 3 deletions src/maca/agents/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import os
import re


class BaseAgent:
def __init__(self, name, model_client):
self.name = name
self.model_client = model_client

def run(self, prompt, **kwargs):
def run(self, *args, **kwargs):
raise NotImplementedError("Subclasses must implement run()")

# Helper tools available to the orchestrator/agents
Expand Down Expand Up @@ -46,7 +47,7 @@ def clean_code_content(self, content):
if first_line_end != -1:
line_content = content[:first_line_end].strip()
if line_content.startswith("```"):
content = content[first_line_end + 1:].strip()
content = content[first_line_end + 1 :].strip()
cleaned = True
if content.endswith("```"):
content = content[:-3].strip()
Expand All @@ -58,7 +59,7 @@ def clean_code_content(self, content):
def parse_files(self, response_text):
pattern = r"\[FILE:\s*([^\s\]]+)\]\s*(?:\r?\n)*```\w*\s*\n(.*?)\n```"
matches = re.findall(pattern, response_text, re.DOTALL)

files = {}
for filepath, content in matches:
files[filepath.strip()] = self.clean_code_content(content)
Expand Down
6 changes: 3 additions & 3 deletions src/maca/agents/coder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from maca.agents.base import BaseAgent


class CoderAgent(BaseAgent):
def __init__(self, name, model_client):
super().__init__(name, model_client)
Expand All @@ -27,7 +27,7 @@ def _build_system_instruction(self):
def _build_prompt(self, task_description, plan, repo_files_content, history):
history_context = self._format_history(history)
files_context = self._format_files_context(repo_files_content)

return (
f"User Task: {task_description}{history_context}\n\n"
f"Implementation Plan:\n{plan}\n"
Expand All @@ -43,7 +43,7 @@ def _format_history(self, history):
def _format_files_context(self, repo_files_content):
if not repo_files_content:
return ""

formatted_files = "\n\nExisting File Contents:\n"
for filepath, content in repo_files_content.items():
formatted_files += f"--- FILE: {filepath} ---\n{content}\n\n"
Expand Down
9 changes: 5 additions & 4 deletions src/maca/agents/planner.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
from maca.agents.base import BaseAgent


class PlannerAgent(BaseAgent):
def __init__(self, model_client):
super().__init__("Planner", model_client)

def run(self, task_description, repo_files=None, history=None):
files_str = "\n".join(repo_files) if repo_files else "Empty repository"

history_str = ""
if history:
history_str = "\n\nPrevious Conversation History:\n" + "\n".join(history)

system_instruction = (
"You are a technical Planner Agent. Your job is to analyze the user request "
"and create a structured markdown implementation plan. "
"Do NOT write any code implementation or scripts. Only write the steps and "
"identify which files need to be created or modified."
)

prompt = (
f"User Task: {task_description}{history_str}\n\n"
f"Current Files in Repository:\n{files_str}\n\n"
"Please output a detailed implementation plan in markdown format. "
"Clearly indicate the files to be created or modified using [NEW] and [MODIFY] tags."
)

return self.model_client.generate(prompt, system_instruction)
6 changes: 3 additions & 3 deletions src/maca/agents/reviewer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from maca.agents.base import BaseAgent


class ReviewerAgent(BaseAgent):
def __init__(self, model_client):
super().__init__("Reviewer", model_client)
Expand Down Expand Up @@ -29,7 +29,7 @@ def _build_system_instruction(self):
def _build_prompt(self, task_description, generated_files, history):
history_context = self._format_history(history)
files_context = self._format_files_context(generated_files)

return (
f"User Task: {task_description}{history_context}\n\n"
f"Generated Files to Review:\n{files_context}"
Expand All @@ -44,7 +44,7 @@ def _format_history(self, history):
def _format_files_context(self, files):
if not files:
return ""

formatted_files = ""
for filepath, content in files.items():
formatted_files += f"--- FILE: {filepath} ---\n{content}\n\n"
Expand Down
52 changes: 41 additions & 11 deletions src/maca/evaluator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from maca.models.local_gemma import LocalGemmaClient


class ComplexityEvaluator:
def __init__(self):
self.gemma_client = LocalGemmaClient()
Expand All @@ -23,7 +23,7 @@ def evaluate(self, prompt):
"Classification (SIMPLE, MEDIUM, COMPLEX, or VERY_COMPLEX):"
)
response = self.gemma_client.generate(gemma_prompt, system_instruction).strip().upper()

# Extract the keyword from response
for val in ["VERY_COMPLEX", "COMPLEX", "MEDIUM", "SIMPLE"]:
if val in response:
Expand All @@ -36,25 +36,55 @@ def evaluate(self, prompt):

def _heuristic_evaluate(self, prompt):
prompt_lower = prompt.lower()

# Very Complex Indicators
very_complex_keywords = [
"distributed", "concurrency", "multi-thread", "thread-safe", "acid", "replicat",
"ast parser", "lexer", "compiler", "interpreter", "cryptography", "blockchain",
"transaction log", "race condition", "consensus", "raft", "paxos", "from scratch"
"distributed",
"concurrency",
"multi-thread",
"thread-safe",
"acid",
"replicat",
"ast parser",
"lexer",
"compiler",
"interpreter",
"cryptography",
"blockchain",
"transaction log",
"race condition",
"consensus",
"raft",
"paxos",
"from scratch",
]
if any(kw in prompt_lower for kw in very_complex_keywords):
return "VERY_COMPLEX"

# Complex Indicators
complex_keywords = [
"database", "sql", "api", "scraper", "beautifulsoup", "selenium",
"web server", "flask", "django", "fastapi", "express", "multi-file",
"integration", "docker", "pipeline", "regex", "pandas", "matplotlib"
"database",
"sql",
"api",
"scraper",
"beautifulsoup",
"selenium",
"web server",
"flask",
"django",
"fastapi",
"express",
"multi-file",
"integration",
"docker",
"pipeline",
"regex",
"pandas",
"matplotlib",
]
if any(kw in prompt_lower for kw in complex_keywords) or len(prompt.split()) > 40:
return "COMPLEX"

# Default to SIMPLE or MEDIUM
if len(prompt.split()) > 15:
return "MEDIUM"
Expand Down
6 changes: 6 additions & 0 deletions src/maca/maca_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@

_gemini_key = None


def get_gemini_api_key():
global _gemini_key
if _gemini_key is not None:
return _gemini_key
_gemini_key = os.environ.get("GEMINI_API_KEY", "").strip()
return _gemini_key


_claude_key = None


def get_claude_api_key():
global _claude_key
if _claude_key is not None:
return _claude_key
_claude_key = os.environ.get("CLAUDE_API_KEY", "").strip()
return _claude_key


OLLAMA_API_URL = os.environ.get("OLLAMA_API_URL", "http://localhost:11434")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma2:2b")

Expand All @@ -44,6 +48,7 @@ def get_claude_api_key():
# If set to True, will mock Gemma/Gemini/Claude calls if unavailable/unconfigured
MOCK_GEMMA_FALLBACK = os.environ.get("MOCK_GEMMA_FALLBACK", "False").lower() == "true"


def validate_config(complexity, selected_agent=None):
"""Validate that the selected agent has the required configuration.

Expand Down Expand Up @@ -76,4 +81,5 @@ def validate_config(complexity, selected_agent=None):
"Gemini is required for Medium, Complex, and Very Complex tasks."
)


SANDBOX_READ_ONLY = False
Loading
Loading