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
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ MACA is a **local-first, hybrid AI coding assistant** that routes work by comple

- **Smart task routing:** analyzes each request and sends it to the best backend for the job.
- **Simple tasks:** local Gemma (`gemma2:2b`) via Ollama.
- **Medium / complex / very complex tasks:** Google Gemini (`gemini-3.5-flash`) via REST API when a key is configured.
- **Medium complexity tasks:** Google Gemini (`gemini-3.5-flash`) via REST API. If Gemini is offline/unconfigured, falls back to Anthropic Claude, then local Gemma.
- **Complex / Very Complex tasks:** Anthropic Claude (default `claude-opus-4-8` or custom model) via Messages API. If Claude is offline/unconfigured, falls back to Google Gemini, then local Gemma.
- **Multi-agent orchestration:** coordinates a planner, coder, and reviewer to work through tasks in sequence.
- **Planner Agent:** inspects the codebase and creates an implementation plan.
- **Coder Agent:** writes and applies the actual code changes.
Expand All @@ -33,21 +34,35 @@ MACA is a **local-first, hybrid AI coding assistant** that routes work by comple
```mermaid
graph TD
User([User Prompt]) --> Router[Task Router]
Router -->|SIMPLE| Gemma[Local Gemma Client]
Router -->|MEDIUM / COMPLEX / VERY_COMPLEX| Gemini[Gemini Client]

Gemma & Gemini --> Orchestrator[Multi-Agent Orchestrator]

Router -.->|1. Classify Task| Gemma[Local Gemma Client]
Gemma -.->|2. Complexity Result| Router

Router -->|SIMPLE| Gemma
Router -->|MEDIUM| Decider1{Backend Availability}
Router -->|COMPLEX / VERY_COMPLEX| Decider2{Backend Availability}

Decider1 -->|Gemini Available| Gemini[Gemini Client]
Decider1 -->|Fallback| Claude[Claude Client]
Decider1 -->|None Available| Gemma

Decider2 -->|Claude Available| Claude
Decider2 -->|Fallback| Gemini
Decider2 -->|None Available| Gemma

Gemma & Gemini & Claude --> Orchestrator[Multi-Agent Orchestrator]

subgraph Agents
Orchestrator --> Planner[Planner Agent]
Planner --> Coder[Coder Agent]
Coder --> Reviewer[Reviewer Agent]
end

Reviewer --> Write[File System Writer]
Write --> Workspace[(Workspace)]
```

> **Note on Complexity Classification**: The Task Router queries the local Gemma model (`gemma2:2b`) to classify prompt complexity. If Gemma is offline, it falls back to a regex-based keyword density and word count heuristic classifier.

---

## 🛠️ Setup Instructions
Expand All @@ -65,17 +80,21 @@ If you only need the Gemma/Ollama setup, use:
```

### 2. Configure API Keys (Environment or macOS Keychain)
MACA currently uses Gemini for medium and above tasks. For secure setup details, see [docs/keys_setup.md](docs/keys_setup.md).
MACA uses Google Gemini and Anthropic Claude for medium and above tasks. For secure setup details, see [docs/keys_setup.md](docs/keys_setup.md).

* **Quick Env Setup**:
```bash
export GEMINI_API_KEY="your-gemini-api-key"
export CLAUDE_API_KEY="your-claude-api-key"
```
You can optionally customize the Gemini model and request timeout (in seconds):

You can optionally customize the models and request timeouts (in seconds):
```bash
export GEMINI_MODEL="gemini-3.5-flash"
export GEMINI_TIMEOUT_SECONDS="45"
export GEMINI_TIMEOUT_SECONDS="120"

export CLAUDE_MODEL="claude-opus-4-8"
export CLAUDE_TIMEOUT_SECONDS="120"
```
* **Recommended macOS Keychain + .zshenv Setup**:
```bash
Expand Down Expand Up @@ -110,6 +129,7 @@ maca "write a simple hello world script in output.py"
Override default task routing:
```sh
maca --model gemini "implement a custom tokenizer"
maca --model claude "implement a custom tokenizer"
```

### Run in Mock Mode (Simulated Run)
Expand Down
30 changes: 22 additions & 8 deletions docs/keys_setup.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Secure Key Setup Guide for MACA

This guide documents how to acquire your **Google Gemini** API key and store it securely using the **macOS Keychain** so that the Multi-Agent Coding Assistant (MACA) can access it without exposing the secret in plain text.
This guide documents how to acquire your **Google Gemini** and **Anthropic Claude** API keys and store them securely using the **macOS Keychain** so that the Multi-Agent Coding Assistant (MACA) can access them without exposing the secrets in plain text.

---

Expand All @@ -13,6 +13,12 @@ This guide documents how to acquire your **Google Gemini** API key and store it
4. Click **Create API key** (select a Google Cloud project or create a new one).
5. Copy your generated key.

### B. Anthropic Claude API Key
1. Visit the [Anthropic Console](https://console.anthropic.com/).
2. Sign in with your account.
3. Navigate to **API Keys**.
4. Click **Create Key**, name it, and copy the generated key.

---

## 2. Secure Configuration via macOS Keychain
Expand All @@ -25,31 +31,39 @@ Open your terminal and run the following command (replace `YOUR_GEMINI_API_KEY`
security add-generic-password -a "$USER" -s "GEMINI_API_KEY" -w "YOUR_GEMINI_API_KEY"
```

### B. Store Claude API Key
Open your terminal and run the following command (replace `YOUR_CLAUDE_API_KEY` with your actual key):
```bash
security add-generic-password -a "$USER" -s "CLAUDE_API_KEY" -w "YOUR_CLAUDE_API_KEY"
```

---

## 3. Add the Keychain Secret to ~/.zshenv
## 3. Add the Keychain Secrets to ~/.zshenv

To make the secret available automatically in every new zsh session, add this line to your shell startup file:
To make the secrets available automatically in every new zsh session, add these lines to your shell startup file:

```bash
printf '%s\n' 'export GEMINI_API_KEY="$(security find-generic-password -a "$USER" -s "GEMINI_API_KEY" -w 2>/dev/null)"' >> ~/.zshenv
printf "%s\n" "export GEMINI_API_KEY=\"\$(security find-generic-password -a \"\$USER\" -s \"GEMINI_API_KEY\" -w 2>/dev/null)\"" >> ~/.zshenv
printf "%s\n" "export CLAUDE_API_KEY=\"\$(security find-generic-password -a \"\$USER\" -s \"CLAUDE_API_KEY\" -w 2>/dev/null)\"" >> ~/.zshenv
source ~/.zshenv
```

This uses the Keychain value for `GEMINI_API_KEY` without storing the raw secret in `.zshrc` or other shell config files.
This uses the Keychain values for `GEMINI_API_KEY` and `CLAUDE_API_KEY` without storing the raw secrets in `.zshrc` or other shell config files.

---

## 4. Verifying Keychain Storage

You can verify that the key was successfully written to your macOS Keychain by retrieving it:
You can verify that the keys were successfully written to your macOS Keychain by retrieving them:

```bash
security find-generic-password -a "$USER" -s "GEMINI_API_KEY" -w
security find-generic-password -a "$USER" -s "CLAUDE_API_KEY" -w
```

---

## 5. How MACA Accesses the Key
## 5. How MACA Accesses the Keys

MACA reads `GEMINI_API_KEY` from your shell environment when it starts. The recommended macOS setup is to export that value from your Keychain in `~/.zshenv`, which makes the key available to the CLI in every new zsh session.
MACA reads `GEMINI_API_KEY` and `CLAUDE_API_KEY` from your shell environment when it starts. The recommended macOS setup is to export those values from your Keychain in `~/.zshenv`, which makes the keys available to the CLI in every new zsh session.
65 changes: 55 additions & 10 deletions src/maca/maca_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,70 @@ 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", "")
_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")

GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.5-flash")
# Configurable timeouts (seconds) for cloud model API calls
try:
GEMINI_TIMEOUT_SECONDS = float(os.environ.get("GEMINI_TIMEOUT_SECONDS", "45"))
GEMINI_TIMEOUT_SECONDS = float(os.environ.get("GEMINI_TIMEOUT_SECONDS", "90"))
except ValueError:
GEMINI_TIMEOUT_SECONDS = 45.0
GEMINI_TIMEOUT_SECONDS = 90.0

try:
CLAUDE_TIMEOUT_SECONDS = float(os.environ.get("CLAUDE_TIMEOUT_SECONDS", "90"))
except ValueError:
CLAUDE_TIMEOUT_SECONDS = 90.0

GEMINI_TIMEOUT = GEMINI_TIMEOUT_SECONDS
CLAUDE_TIMEOUT = CLAUDE_TIMEOUT_SECONDS

# Claude model to use for API calls
CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL", "claude-opus-4-8")

# Gemini model to use for API calls
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.5-flash")

# 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.

# If set to True, will mock Gemma/Gemini calls if unavailable/unconfigured
MOCK_GEMMA_FALLBACK = os.environ.get("MOCK_GEMMA_FALLBACK", "True").lower() == "true"
Args:
complexity: The evaluated task complexity level.
selected_agent: The agent name that was selected for this task
(e.g. "GEMINI", "CLAUDE"). If None, falls back to
legacy behaviour of requiring Gemini for non-SIMPLE tasks.
"""
if complexity == "SIMPLE":
return # Local Gemma, no API key needed

def validate_config(complexity):
# Only SIMPLE tasks run Gemma (which is local and does not require API keys).
# All other tasks (MEDIUM, COMPLEX, VERY_COMPLEX) route to Gemini.
if complexity != "SIMPLE":
if selected_agent == "CLAUDE":
if not get_claude_api_key():
raise ValueError(
"CLAUDE_API_KEY is not set in environment variables. "
"Claude is required for this task's complexity level."
)
elif selected_agent == "GEMINI":
if not get_gemini_api_key():
raise ValueError(
"GEMINI_API_KEY is not set in environment variables. "
"Gemini is required for this task's complexity level."
)
else:
# Legacy fallback: require Gemini for any non-SIMPLE task
if not get_gemini_api_key():
raise ValueError(
"GEMINI_API_KEY is not set in environment variables. "
Expand Down
Loading
Loading