generated from coulomb/repo-seed
feat: transform to agent coordination platform with comprehensive documentation
Transform Issue Facade from a universal CLI tool into an agent coordination platform with comprehensive documentation and enhanced capabilities for autonomous coding agents. Major Changes: - Complete README rewrite focusing on agent-driven coordination - New comprehensive documentation (AGENT_INTEGRATION.md, CLAUDE.md, ROADMAP.md) - Capability integration setup with CAPABILITY.yaml and integration scripts - Enhanced Makefile with local development targets for easier workflows Bug Fixes: - Fix schema initialization using executescript() for multi-line SQL support - Disable FTS5 triggers due to compatibility issues (documented for future re-enablement) Features: - Enhanced CLI list command with full parameter passthrough - New examples directory with agent integration patterns - New comprehensive test suite (test_core_models.py, test_local_backend.py) Code Quality: - Remove @cached_property decorators for Label properties (simplification) - Clean up test organization (removed old test_gitea_integration.py) This milestone establishes Issue Facade as a production-ready coordination layer for multi-agent software development, with clear integration paths and comprehensive developer documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
358
examples/agents/multi_agent_pipeline.py
Executable file
358
examples/agents/multi_agent_pipeline.py
Executable file
@@ -0,0 +1,358 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Agent Pipeline
|
||||
|
||||
Demonstrates a CI/CD-like pipeline with multiple specialized agents:
|
||||
- Coder Agent: Implements features
|
||||
- Reviewer Agent: Reviews code
|
||||
- Tester Agent: Runs tests
|
||||
- Deployer Agent: Deploys to staging
|
||||
|
||||
Each agent monitors for issues in their stage and advances them through the pipeline.
|
||||
|
||||
Usage:
|
||||
export GITEA_URL=https://gitea.example.com
|
||||
export GITEA_TOKEN=your-token
|
||||
export GITEA_OWNER=your-org
|
||||
export GITEA_REPO=your-repo
|
||||
|
||||
# Run all agents in parallel (in separate terminals)
|
||||
python multi_agent_pipeline.py --agent=coder
|
||||
python multi_agent_pipeline.py --agent=reviewer
|
||||
python multi_agent_pipeline.py --agent=tester
|
||||
python multi_agent_pipeline.py --agent=deployer
|
||||
|
||||
# Or run in round-robin mode (all agents in one process)
|
||||
python multi_agent_pipeline.py --mode=roundrobin
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
||||
from issue_tracker.core.interfaces import IssueFilter
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""Base class for pipeline agents."""
|
||||
|
||||
def __init__(self, agent_id: str, role: str):
|
||||
self.agent_id = agent_id
|
||||
self.role = role
|
||||
self.backend = None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to backend from environment."""
|
||||
base_url = os.environ['GITEA_URL']
|
||||
token = os.environ['GITEA_TOKEN']
|
||||
owner = os.environ['GITEA_OWNER']
|
||||
repo = os.environ['GITEA_REPO']
|
||||
|
||||
self.backend = GiteaBackend()
|
||||
self.backend.connect({
|
||||
'base_url': base_url,
|
||||
'token': token,
|
||||
'owner': owner,
|
||||
'repo': repo
|
||||
})
|
||||
print(f"✓ {self.role} connected")
|
||||
|
||||
def log(self, message: str):
|
||||
"""Log a message with agent role prefix."""
|
||||
print(f"[{self.role}] {message}")
|
||||
|
||||
def add_comment(self, issue: Issue, message: str):
|
||||
"""Add a comment to an issue."""
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body=f"**{self.role}**: {message}",
|
||||
author=User(id=self.agent_id, username=self.agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.backend.add_comment(issue.id, comment)
|
||||
|
||||
def process_one(self) -> bool:
|
||||
"""Process one issue. Return True if work was done."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CoderAgent(BaseAgent):
|
||||
"""Agent that implements features."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("agent-coder", "Coder")
|
||||
|
||||
def process_one(self) -> bool:
|
||||
# Find issues needing implementation
|
||||
issues = self.backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['needs-implementation']
|
||||
))
|
||||
|
||||
if not issues:
|
||||
return False
|
||||
|
||||
issue = issues[0]
|
||||
self.log(f"Implementing #{issue.number}: {issue.title}")
|
||||
|
||||
# Update state
|
||||
issue.state = IssueState.IN_PROGRESS
|
||||
if not issue.assignees:
|
||||
issue.assignees = [User(id=self.agent_id, username=self.agent_id)]
|
||||
|
||||
# Remove old label, add new label
|
||||
issue.labels = [l for l in issue.labels if l.name != 'needs-implementation']
|
||||
issue.labels.append(Label(name='needs-review'))
|
||||
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
# Simulate work
|
||||
time.sleep(2)
|
||||
|
||||
# Add comment
|
||||
self.add_comment(issue,
|
||||
"Implementation complete.\n\n"
|
||||
"**Files changed:**\n"
|
||||
"- src/feature.py\n"
|
||||
"- tests/test_feature.py\n\n"
|
||||
"Ready for code review."
|
||||
)
|
||||
|
||||
self.log(f"✓ Completed #{issue.number}")
|
||||
return True
|
||||
|
||||
|
||||
class ReviewerAgent(BaseAgent):
|
||||
"""Agent that reviews code."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("agent-reviewer", "Reviewer")
|
||||
|
||||
def process_one(self) -> bool:
|
||||
# Find issues needing review
|
||||
issues = self.backend.list_issues(IssueFilter(
|
||||
state='in_progress',
|
||||
labels=['needs-review']
|
||||
))
|
||||
|
||||
if not issues:
|
||||
return False
|
||||
|
||||
issue = issues[0]
|
||||
self.log(f"Reviewing #{issue.number}: {issue.title}")
|
||||
|
||||
# Simulate review
|
||||
time.sleep(2)
|
||||
|
||||
# Update labels
|
||||
issue.labels = [l for l in issue.labels if l.name != 'needs-review']
|
||||
issue.labels.append(Label(name='needs-testing'))
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
# Add review comment
|
||||
self.add_comment(issue,
|
||||
"Code review complete. ✅\n\n"
|
||||
"**Review notes:**\n"
|
||||
"- Code quality: Good\n"
|
||||
"- Test coverage: 95%\n"
|
||||
"- Documentation: Complete\n\n"
|
||||
"Approved for testing."
|
||||
)
|
||||
|
||||
self.log(f"✓ Approved #{issue.number}")
|
||||
return True
|
||||
|
||||
|
||||
class TesterAgent(BaseAgent):
|
||||
"""Agent that runs tests."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("agent-tester", "Tester")
|
||||
|
||||
def process_one(self) -> bool:
|
||||
# Find issues needing testing
|
||||
issues = self.backend.list_issues(IssueFilter(
|
||||
state='in_progress',
|
||||
labels=['needs-testing']
|
||||
))
|
||||
|
||||
if not issues:
|
||||
return False
|
||||
|
||||
issue = issues[0]
|
||||
self.log(f"Testing #{issue.number}: {issue.title}")
|
||||
|
||||
# Simulate tests
|
||||
time.sleep(2)
|
||||
|
||||
# Update labels
|
||||
issue.labels = [l for l in issue.labels if l.name != 'needs-testing']
|
||||
issue.labels.append(Label(name='needs-deployment'))
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
# Add test results
|
||||
self.add_comment(issue,
|
||||
"All tests passing. ✅\n\n"
|
||||
"**Test results:**\n"
|
||||
"- Unit tests: 50/50 passed\n"
|
||||
"- Integration tests: 12/12 passed\n"
|
||||
"- Coverage: 96.5%\n\n"
|
||||
"Ready for deployment."
|
||||
)
|
||||
|
||||
self.log(f"✓ Tests passed #{issue.number}")
|
||||
return True
|
||||
|
||||
|
||||
class DeployerAgent(BaseAgent):
|
||||
"""Agent that deploys to staging."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("agent-deployer", "Deployer")
|
||||
|
||||
def process_one(self) -> bool:
|
||||
# Find issues needing deployment
|
||||
issues = self.backend.list_issues(IssueFilter(
|
||||
state='in_progress',
|
||||
labels=['needs-deployment']
|
||||
))
|
||||
|
||||
if not issues:
|
||||
return False
|
||||
|
||||
issue = issues[0]
|
||||
self.log(f"Deploying #{issue.number}: {issue.title}")
|
||||
|
||||
# Simulate deployment
|
||||
time.sleep(2)
|
||||
|
||||
# Close issue
|
||||
issue.state = IssueState.CLOSED
|
||||
issue.closed_at = datetime.now(timezone.utc)
|
||||
issue.labels = [l for l in issue.labels if l.name != 'needs-deployment']
|
||||
issue.labels.append(Label(name='deployed'))
|
||||
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
# Add deployment comment
|
||||
self.add_comment(issue,
|
||||
"Deployed to staging. 🚀\n\n"
|
||||
"**Deployment info:**\n"
|
||||
"- Environment: staging\n"
|
||||
"- Version: v1.2.3\n"
|
||||
"- Status: Healthy\n\n"
|
||||
"Feature is live and ready for user testing."
|
||||
)
|
||||
|
||||
self.log(f"✓ Deployed #{issue.number}")
|
||||
return True
|
||||
|
||||
|
||||
def run_single_agent(agent: BaseAgent, poll_interval: int = 5):
|
||||
"""Run a single agent in a loop."""
|
||||
agent.connect()
|
||||
agent.log("Starting pipeline agent...")
|
||||
agent.log("Press Ctrl+C to stop\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
if agent.process_one():
|
||||
agent.log("Task completed, checking for more work...")
|
||||
else:
|
||||
agent.log(f"No work available, waiting {poll_interval}s...")
|
||||
time.sleep(poll_interval)
|
||||
except KeyboardInterrupt:
|
||||
agent.log("Shutting down...")
|
||||
break
|
||||
except Exception as e:
|
||||
agent.log(f"Error: {e}")
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
def run_roundrobin(agents: List[BaseAgent], poll_interval: int = 5):
|
||||
"""Run all agents in round-robin fashion."""
|
||||
print("🤖 Starting multi-agent pipeline (round-robin mode)")
|
||||
print(" Agents:", ", ".join([a.role for a in agents]))
|
||||
print(" Press Ctrl+C to stop\n")
|
||||
|
||||
# Connect all agents
|
||||
for agent in agents:
|
||||
agent.connect()
|
||||
|
||||
while True:
|
||||
try:
|
||||
work_done = False
|
||||
for agent in agents:
|
||||
if agent.process_one():
|
||||
work_done = True
|
||||
time.sleep(1) # Brief pause between agents
|
||||
|
||||
if not work_done:
|
||||
print(f"⏸️ No work available for any agent, waiting {poll_interval}s...")
|
||||
time.sleep(poll_interval)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ Shutting down all agents...")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description='Multi-agent pipeline')
|
||||
parser.add_argument('--agent', choices=['coder', 'reviewer', 'tester', 'deployer'],
|
||||
help='Run specific agent')
|
||||
parser.add_argument('--mode', choices=['single', 'roundrobin'], default='single',
|
||||
help='Execution mode')
|
||||
parser.add_argument('--poll-interval', type=int, default=5,
|
||||
help='Polling interval in seconds')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.mode == 'roundrobin':
|
||||
agents = [
|
||||
CoderAgent(),
|
||||
ReviewerAgent(),
|
||||
TesterAgent(),
|
||||
DeployerAgent()
|
||||
]
|
||||
run_roundrobin(agents, args.poll_interval)
|
||||
else:
|
||||
if not args.agent:
|
||||
print("Error: --agent required in single mode")
|
||||
print("Use --mode=roundrobin to run all agents")
|
||||
sys.exit(1)
|
||||
|
||||
agent_map = {
|
||||
'coder': CoderAgent(),
|
||||
'reviewer': ReviewerAgent(),
|
||||
'tester': TesterAgent(),
|
||||
'deployer': DeployerAgent()
|
||||
}
|
||||
|
||||
agent = agent_map[args.agent]
|
||||
run_single_agent(agent, args.poll_interval)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ Interrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user