refactor: Support CLI-based AI providers (Claude Code, Gemini, Codex)

Major refactoring to support flexible AI provider configuration instead of
requiring direct API access. Users can now use whatever AI CLI tool they have
installed (claude, gemini, codex, etc.) without API keys.

## Changes to automation/agents.py

**New Functions:**
- `get_ai_config()` - Reads config from env vars or git config
  - Environment: CDEV_AI_PROVIDER, CDEV_AI_COMMAND (highest priority)
  - Git config: cascadingdev.aiprovider, cascadingdev.aicommand
  - Default: claude-cli with "claude -p '{prompt}'"

- `call_ai_cli()` - Execute AI via CLI command
  - Passes content via temp file to avoid shell escaping
  - Supports {prompt} placeholder in command template
  - 60s timeout with error handling
  - Parses JSON from response (with/without code blocks)

- `call_ai_api()` - Direct API access (renamed from call_claude)
  - Unchanged functionality
  - Now used as fallback option

- `call_ai()` - Unified AI caller
  - Try CLI first (if configured)
  - Fall back to API (if ANTHROPIC_API_KEY set)
  - Graceful failure with warnings

**Updated Functions:**
- `normalize_discussion()` - calls call_ai() instead of call_claude()
- `track_questions()` - calls call_ai() instead of call_claude()
- `track_action_items()` - calls call_ai() instead of call_claude()
- `track_decisions()` - calls call_ai() instead of call_claude()

**Configuration Precedence:**
1. Environment variables (session-scoped)
2. Git config (repo-scoped)
3. Defaults (claude-cli)

## Changes to docs/AUTOMATION.md

**Updated Sections:**
- "Requirements" - Now lists CLI as Option 1 (recommended), API as Option 2
- "Configuration" - Complete rewrite with 5 provider examples:
  1. Claude CLI (default)
  2. Gemini CLI
  3. OpenAI Codex CLI
  4. Direct API (Anthropic)
  5. Custom AI command
- "Troubleshooting" - Added "AI command failed" section, updated error messages

**New Configuration Examples:**
```bash
# Claude Code (default)
git config cascadingdev.aicommand "claude -p '{prompt}'"

# Gemini
git config cascadingdev.aiprovider "gemini-cli"
git config cascadingdev.aicommand "gemini '{prompt}'"

# Custom
git config cascadingdev.aicommand "my-ai-tool --prompt '{prompt}' --format json"
```

## Benefits

1. **No API Key Required**: Use existing CLI tools (claude, gemini, etc.)
2. **Flexible Configuration**: Git config (persistent) or env vars (session)
3. **Provider Agnostic**: Works with any CLI that returns JSON
4. **Backward Compatible**: Still supports direct API if ANTHROPIC_API_KEY set
5. **User-Friendly**: Defaults to "claude -p" if available

## Testing

-  get_ai_config() tests:
  - Default: claude-cli with "claude -p '{prompt}'"
  - Git config override: gemini-cli with "gemini '{prompt}'"
  - Env var override: codex-cli with "codex '{prompt}'"
-  extract_mentions() still works (no AI required)
-  All 6 workflow tests pass

## Impact

Users with Claude Code installed can now use the automation without any
configuration - it just works! Same for users with gemini or codex CLIs.
Only requires git config setup if using non-default command.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rob 2025-10-30 18:31:27 -03:00
parent 72577bd30d
commit 5944269ed8
2 changed files with 248 additions and 32 deletions

View File

@ -1,6 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Claude-powered agents for extracting structured information from discussions. AI-powered agents for extracting structured information from discussions.
Supports multiple AI providers via CLI commands:
- Claude Code: claude -p "prompt"
- Gemini CLI: gemini "prompt"
- OpenAI Codex: codex "prompt"
- Or direct API: anthropic library
These agents process natural language discussions and extract: These agents process natural language discussions and extract:
- Questions (answered, open, partial) - Questions (answered, open, partial)
@ -12,27 +18,155 @@ from __future__ import annotations
import json import json
import os import os
import subprocess
import sys import sys
import tempfile
from pathlib import Path
from typing import Any, Mapping from typing import Any, Mapping
def call_claude(prompt: str, content: str) -> dict[str, Any] | None: def get_ai_config() -> dict[str, str]:
""" """
Call Claude API to process content with a given prompt. Get AI provider configuration from environment or git config.
Environment variables (highest priority):
CDEV_AI_PROVIDER: "claude-cli", "claude-api", "gemini-cli", "codex-cli"
CDEV_AI_COMMAND: Command template, e.g., "claude -p {prompt}"
Git config (fallback):
cascadingdev.aiprovider
cascadingdev.aicommand
Returns dict with 'provider' and 'command' keys.
"""
# Try environment first
provider = os.environ.get("CDEV_AI_PROVIDER")
command = os.environ.get("CDEV_AI_COMMAND")
# Fall back to git config
if not provider or not command:
try:
result = subprocess.run(
["git", "config", "--get", "cascadingdev.aiprovider"],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
provider = result.stdout.strip()
result = subprocess.run(
["git", "config", "--get", "cascadingdev.aicommand"],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
command = result.stdout.strip()
except Exception:
pass
# Default to claude-cli if nothing configured
if not provider:
provider = "claude-cli"
if not command:
command = "claude -p '{prompt}'"
return {"provider": provider, "command": command}
def call_ai_cli(prompt: str, content: str) -> dict[str, Any] | None:
"""
Call AI via CLI command (claude, gemini, codex, etc.).
The command template can use {prompt} and {content} placeholders.
Content is passed via a temporary file to avoid shell escaping issues.
Returns parsed JSON response or None on failure.
"""
config = get_ai_config()
command_template = config["command"]
# Create temporary file with content
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
content_file = Path(f.name)
f.write(content)
try:
# Build full prompt
full_prompt = f"{prompt}\n\nPlease read the discussion content from: {content_file}\n\nReturn ONLY valid JSON, no other text."
# Format command
# Support both {prompt} placeholder and direct append
if "{prompt}" in command_template:
command = command_template.format(prompt=full_prompt)
else:
# Assume command expects prompt as argument
command = f"{command_template} '{full_prompt}'"
# Execute command
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=60,
check=False
)
if result.returncode != 0:
sys.stderr.write(f"[agents] warning: AI command failed with code {result.returncode}\n")
if result.stderr:
sys.stderr.write(f"[agents] {result.stderr[:200]}\n")
return None
response_text = result.stdout.strip()
# Try to parse as JSON
# Look for JSON in code blocks or raw
if "```json" in response_text:
json_start = response_text.find("```json") + 7
json_end = response_text.find("```", json_start)
response_text = response_text[json_start:json_end].strip()
elif "```" in response_text:
json_start = response_text.find("```") + 3
json_end = response_text.find("```", json_start)
response_text = response_text[json_start:json_end].strip()
return json.loads(response_text)
except subprocess.TimeoutExpired:
sys.stderr.write("[agents] warning: AI command timed out after 60s\n")
return None
except json.JSONDecodeError as e:
sys.stderr.write(f"[agents] warning: Failed to parse AI response as JSON: {e}\n")
sys.stderr.write(f"[agents] Response preview: {response_text[:200]}...\n")
return None
except Exception as e:
sys.stderr.write(f"[agents] error calling AI: {e}\n")
return None
finally:
# Clean up temp file
try:
content_file.unlink()
except Exception:
pass
def call_ai_api(prompt: str, content: str) -> dict[str, Any] | None:
"""
Call AI via direct API (anthropic library).
Returns parsed JSON response or None on failure. Returns parsed JSON response or None on failure.
Requires ANTHROPIC_API_KEY environment variable. Requires ANTHROPIC_API_KEY environment variable.
""" """
api_key = os.environ.get("ANTHROPIC_API_KEY") api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key: if not api_key:
sys.stderr.write("[agents] warning: ANTHROPIC_API_KEY not set, skipping AI processing\n")
return None return None
try: try:
import anthropic import anthropic
except ImportError: except ImportError:
sys.stderr.write("[agents] warning: anthropic package not installed, skipping AI processing\n")
sys.stderr.write("[agents] Install with: pip install anthropic\n")
return None return None
try: try:
@ -54,7 +188,6 @@ def call_claude(prompt: str, content: str) -> dict[str, Any] | None:
response_text += block.text response_text += block.text
# Try to parse as JSON # Try to parse as JSON
# Look for JSON in code blocks or raw
if "```json" in response_text: if "```json" in response_text:
json_start = response_text.find("```json") + 7 json_start = response_text.find("```json") + 7
json_end = response_text.find("```", json_start) json_end = response_text.find("```", json_start)
@ -67,10 +200,40 @@ def call_claude(prompt: str, content: str) -> dict[str, Any] | None:
return json.loads(response_text) return json.loads(response_text)
except Exception as e: except Exception as e:
sys.stderr.write(f"[agents] error calling Claude: {e}\n") sys.stderr.write(f"[agents] error calling API: {e}\n")
return None return None
def call_ai(prompt: str, content: str) -> dict[str, Any] | None:
"""
Call configured AI provider (CLI or API).
First tries CLI-based provider (claude, gemini, codex).
Falls back to direct API if ANTHROPIC_API_KEY is set.
Returns parsed JSON response or None on failure.
"""
config = get_ai_config()
provider = config["provider"]
# Try CLI-based providers first
if provider.endswith("-cli") or not os.environ.get("ANTHROPIC_API_KEY"):
result = call_ai_cli(prompt, content)
if result is not None:
return result
# Fall back to API if available
if os.environ.get("ANTHROPIC_API_KEY"):
return call_ai_api(prompt, content)
# No AI available
if provider.endswith("-cli"):
sys.stderr.write(f"[agents] warning: AI CLI provider '{provider}' not responding, skipping AI processing\n")
else:
sys.stderr.write("[agents] warning: No AI provider configured, skipping AI processing\n")
return None
def normalize_discussion(content: str) -> dict[str, Any] | None: def normalize_discussion(content: str) -> dict[str, Any] | None:
""" """
Agent 1: Normalize natural language discussion to structured format. Agent 1: Normalize natural language discussion to structured format.
@ -108,7 +271,7 @@ Look for patterns like:
Only include items that are clearly present. Return empty arrays for missing categories.""" Only include items that are clearly present. Return empty arrays for missing categories."""
return call_claude(prompt, content) return call_ai(prompt, content)
def track_questions(content: str, existing_questions: list[dict[str, Any]]) -> dict[str, Any] | None: def track_questions(content: str, existing_questions: list[dict[str, Any]]) -> dict[str, Any] | None:
@ -145,7 +308,7 @@ Look for:
- A: or Answer: or Re: markers (these answer previous questions) - A: or Answer: or Re: markers (these answer previous questions)
- @Name mentions (directing questions to someone)""" - @Name mentions (directing questions to someone)"""
return call_claude(prompt, content) return call_ai(prompt, content)
def track_action_items(content: str, existing_items: list[dict[str, Any]]) -> dict[str, Any] | None: def track_action_items(content: str, existing_items: list[dict[str, Any]]) -> dict[str, Any] | None:
@ -181,7 +344,7 @@ Status transitions:
- ASSIGNED: Someone took ownership (@Name: I'll handle X) - ASSIGNED: Someone took ownership (@Name: I'll handle X)
- DONE: Marked complete (DONE:, "completed this")""" - DONE: Marked complete (DONE:, "completed this")"""
return call_claude(prompt, content) return call_ai(prompt, content)
def track_decisions(content: str, existing_decisions: list[dict[str, Any]]) -> dict[str, Any] | None: def track_decisions(content: str, existing_decisions: list[dict[str, Any]]) -> dict[str, Any] | None:
@ -231,7 +394,7 @@ Look for:
- Vote consensus (multiple READY votes on a proposal) - Vote consensus (multiple READY votes on a proposal)
- "Actually, let's change to..." (reversals)""" - "Actually, let's change to..." (reversals)"""
return call_claude(prompt, content) return call_ai(prompt, content)
def extract_mentions(content: str) -> list[dict[str, str]]: def extract_mentions(content: str) -> list[dict[str, str]]:

View File

@ -71,12 +71,29 @@ READY: 0 • CHANGES: 2 • REJECT: 0
### Requirements ### Requirements
Phase 2 supports multiple AI providers via CLI commands or direct API:
**Option 1: CLI-based (Recommended)**
```bash
# Uses whatever AI CLI tool you have installed
# Default: claude -p "prompt"
# Configure via git config (persistent)
git config cascadingdev.aiprovider "claude-cli"
git config cascadingdev.aicommand "claude -p '{prompt}'"
# Or via environment variables (session)
export CDEV_AI_PROVIDER="claude-cli"
export CDEV_AI_COMMAND="claude -p '{prompt}'"
```
**Option 2: Direct API (Alternative)**
```bash ```bash
pip install anthropic pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..." export ANTHROPIC_API_KEY="sk-ant-..."
``` ```
If `ANTHROPIC_API_KEY` is not set, Phase 2 features are silently skipped and only Phase 1 (votes) runs. If no AI provider is configured or responding, Phase 2 features are silently skipped and only Phase 1 (votes) runs.
### Features ### Features
@ -269,25 +286,48 @@ git commit -m "Test workflow" # Triggers automation
## Configuration ## Configuration
### Enabling AI Features ### AI Provider Options
**1. Claude CLI (Default)**
```bash
# No configuration needed if you have 'claude' command available
# The system defaults to: claude -p '{prompt}'
# To customize:
git config cascadingdev.aicommand "claude -p '{prompt}'"
```
**2. Gemini CLI**
```bash
git config cascadingdev.aiprovider "gemini-cli"
git config cascadingdev.aicommand "gemini '{prompt}'"
```
**3. OpenAI Codex CLI**
```bash
git config cascadingdev.aiprovider "codex-cli"
git config cascadingdev.aicommand "codex '{prompt}'"
```
**4. Direct API (Anthropic)**
```bash ```bash
# Install anthropic package
pip install anthropic pip install anthropic
# Set API key (add to ~/.bashrc or ~/.zshrc)
export ANTHROPIC_API_KEY="sk-ant-..." export ANTHROPIC_API_KEY="sk-ant-..."
# CLI commands will fallback to API if available
```
# Verify setup **5. Custom AI Command**
python3 -c "import anthropic; print('✓ Claude API ready')" ```bash
# Use any command that reads a prompt and returns JSON
git config cascadingdev.aicommand "my-ai-tool --prompt '{prompt}' --format json"
``` ```
### Disabling AI Features ### Disabling AI Features
Simply don't set `ANTHROPIC_API_KEY`. The system will: Simply don't configure any AI provider. The system will:
- Log a warning: `[agents] warning: ANTHROPIC_API_KEY not set, skipping AI processing` - Log a warning: `[agents] warning: No AI provider configured, skipping AI processing`
- Continue with Phase 1 (vote tracking only) - Continue with Phase 1 (vote tracking only)
- Still extract @mentions (doesn't require API) - Still extract @mentions (doesn't require AI)
## Future Enhancements ## Future Enhancements
@ -313,22 +353,35 @@ except ImportError:
import agents # Fallback for different execution contexts import agents # Fallback for different execution contexts
``` ```
### "ANTHROPIC_API_KEY not set" ### "No AI provider configured"
**Cause:** Claude API key not configured. **Cause:** No AI CLI command or API key configured.
**Solution:** Either: **Solution:** Choose one:
1. Add to environment: `export ANTHROPIC_API_KEY="sk-ant-..."` ```bash
2. Or accept Phase 1 only (votes only, still useful) # Option 1: Use Claude CLI (default)
git config cascadingdev.aicommand "claude -p '{prompt}'"
### "anthropic package not installed" # Option 2: Use Gemini CLI
git config cascadingdev.aiprovider "gemini-cli"
git config cascadingdev.aicommand "gemini '{prompt}'"
**Cause:** Python anthropic library not installed. # Option 3: Use Anthropic API
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
```
Or accept Phase 1 only (votes only, still useful)
### "AI command failed with code X"
**Cause:** AI CLI command returned error.
**Solution:** **Solution:**
```bash 1. Test command manually: `claude -p "test prompt"`
pip install anthropic 2. Check command is in PATH: `which claude`
``` 3. Verify command syntax in config: `git config cascadingdev.aicommand`
4. Check stderr output in warning message for details
### Summary sections not updating ### Summary sections not updating