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:
252
examples/agents/README.md
Normal file
252
examples/agents/README.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Agent Examples
|
||||
|
||||
This directory contains working examples of autonomous agents using the Issue Facade for coordination.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Install issue-facade**:
|
||||
```bash
|
||||
cd ../..
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
2. **Configure backend** (one-time setup):
|
||||
```bash
|
||||
export GITEA_API_TOKEN="your-token"
|
||||
issue backend add myproject gitea
|
||||
# Enter: URL, owner, repo when prompted
|
||||
issue backend set-default myproject
|
||||
```
|
||||
|
||||
3. **Set environment variables** for scripts:
|
||||
```bash
|
||||
export GITEA_URL=https://gitea.example.com
|
||||
export GITEA_TOKEN=your-token
|
||||
export GITEA_OWNER=your-org
|
||||
export GITEA_REPO=your-repo
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Simple Task Executor (`simple_task_executor.py`)
|
||||
|
||||
**What it does:**
|
||||
- Finds open issues labeled `ready`
|
||||
- Claims them by assigning to itself
|
||||
- Simulates work
|
||||
- Closes with completion comment
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Run once
|
||||
python simple_task_executor.py --once
|
||||
|
||||
# Run continuously
|
||||
python simple_task_executor.py
|
||||
|
||||
# Run for N iterations
|
||||
python simple_task_executor.py --max-iterations 5
|
||||
```
|
||||
|
||||
**Setup test data:**
|
||||
```bash
|
||||
# Create some test issues
|
||||
issue create "Test task 1" --label=ready
|
||||
issue create "Test task 2" --label=ready
|
||||
issue create "Test task 3" --label=ready
|
||||
```
|
||||
|
||||
### 2. Multi-Agent Pipeline (`multi_agent_pipeline.py`)
|
||||
|
||||
**What it does:**
|
||||
Simulates a CI/CD pipeline with specialized agents:
|
||||
- **Coder**: Implements features (needs-implementation → needs-review)
|
||||
- **Reviewer**: Reviews code (needs-review → needs-testing)
|
||||
- **Tester**: Runs tests (needs-testing → needs-deployment)
|
||||
- **Deployer**: Deploys to staging (needs-deployment → deployed + closed)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Run all agents in round-robin (one process)
|
||||
python multi_agent_pipeline.py --mode=roundrobin
|
||||
|
||||
# Or run each agent separately (in different 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
|
||||
```
|
||||
|
||||
**Setup test data:**
|
||||
```bash
|
||||
issue create "Feature: Add user profile" --label=feature --label=needs-implementation
|
||||
issue create "Feature: Export data to CSV" --label=feature --label=needs-implementation
|
||||
```
|
||||
|
||||
**Watch the pipeline:**
|
||||
```bash
|
||||
# In another terminal, watch progress
|
||||
watch -n 2 'issue list --format=json | jq -r ".[] | [.number, .state, (.labels | map(.name) | join(\",\"))] | @tsv"'
|
||||
```
|
||||
|
||||
### 3. Human-in-the-Loop (`human_in_loop.py`)
|
||||
|
||||
**What it does:**
|
||||
- Finds large features labeled `needs-breakdown`
|
||||
- Proposes breaking them into subtasks
|
||||
- Waits for human approval via comments
|
||||
- Creates subtasks only after approval
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Run once
|
||||
python human_in_loop.py --once
|
||||
|
||||
# Run continuously
|
||||
python human_in_loop.py
|
||||
```
|
||||
|
||||
**Setup test data:**
|
||||
```bash
|
||||
issue create "Feature: Implement complete authentication system" \
|
||||
--label=feature \
|
||||
--label=needs-breakdown \
|
||||
--description="Large feature that should be broken down into smaller tasks"
|
||||
```
|
||||
|
||||
**Approve the agent's proposal:**
|
||||
```bash
|
||||
# Agent will post a proposal comment. You reply with:
|
||||
issue comment <issue-number> "APPROVED"
|
||||
|
||||
# Or reject:
|
||||
issue comment <issue-number> "REJECTED: Not the right approach"
|
||||
|
||||
# Or request modifications:
|
||||
issue comment <issue-number> "MODIFY: Please add security audit as a subtask"
|
||||
```
|
||||
|
||||
### 4. Monitoring Agent (`monitoring_agent.py`)
|
||||
|
||||
**What it does:**
|
||||
- Monitors issue tracker health
|
||||
- Detects stale issues (not updated in N days)
|
||||
- Finds blocked issues
|
||||
- Identifies unassigned priority issues
|
||||
- Analyzes project velocity
|
||||
- Creates alert issues when problems found
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Run once (no alerts)
|
||||
python monitoring_agent.py --once --no-alerts
|
||||
|
||||
# Run continuously with hourly checks
|
||||
python monitoring_agent.py --check-interval=3600
|
||||
|
||||
# Run with custom stale threshold
|
||||
python monitoring_agent.py --stale-days=14 --once
|
||||
```
|
||||
|
||||
**View monitoring output:**
|
||||
The agent will create issues tagged with `monitoring` and `alert` when it detects problems.
|
||||
|
||||
## Agent Coordination Patterns
|
||||
|
||||
### Pattern 1: Label-Based Roles
|
||||
|
||||
Agents claim issues based on label matching:
|
||||
|
||||
```python
|
||||
AGENT_ROLES = {
|
||||
'agent:coder': ['feature', 'bug', 'refactor'],
|
||||
'agent:tester': ['needs-testing', 'test-failure'],
|
||||
'agent:reviewer': ['needs-review']
|
||||
}
|
||||
|
||||
issues = backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=[agent_type, 'feature']
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 2: State Machine
|
||||
|
||||
Issues flow through states:
|
||||
|
||||
```
|
||||
open → in_progress → needs_review → closed
|
||||
↓ ↓ ↓
|
||||
blocked blocked blocked
|
||||
```
|
||||
|
||||
### Pattern 3: Comment-Based Communication
|
||||
|
||||
Agents communicate via structured comments:
|
||||
|
||||
```python
|
||||
# Agent posts JSON in comment
|
||||
comment.body = "```agent-message\n" + json.dumps({
|
||||
'type': 'implementation_complete',
|
||||
'agent': 'agent-coder',
|
||||
'data': {'files_changed': ['auth.py'], 'tests_passing': True}
|
||||
}) + "\n```"
|
||||
|
||||
# Other agents parse it
|
||||
for comment in comments:
|
||||
if '```agent-message' in comment.body:
|
||||
msg = json.loads(extract_json(comment.body))
|
||||
if msg['type'] == 'implementation_complete':
|
||||
# React to completion
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Backend not configured"
|
||||
```bash
|
||||
issue backend list
|
||||
# If empty:
|
||||
issue backend add myproject gitea
|
||||
```
|
||||
|
||||
### "Authentication failed"
|
||||
```bash
|
||||
# Check token is valid
|
||||
curl -H "Authorization: token $GITEA_TOKEN" $GITEA_URL/api/v1/user
|
||||
```
|
||||
|
||||
### "No issues found"
|
||||
```bash
|
||||
# Verify you have issues
|
||||
issue list
|
||||
|
||||
# Create test data
|
||||
issue create "Test" --label=ready
|
||||
```
|
||||
|
||||
### Agent stuck/not processing
|
||||
```bash
|
||||
# Check what agent sees
|
||||
issue list --state=open --label=needs-implementation --format=json
|
||||
|
||||
# Verify labels match what agent expects
|
||||
issue show 42 --format=json | jq '.labels'
|
||||
```
|
||||
|
||||
## Tips for Development
|
||||
|
||||
1. **Use `--once` flag during development** to run single iterations
|
||||
2. **Use `--no-alerts` for monitoring agent** to avoid cluttering tracker
|
||||
3. **Run agents in separate terminals** to see concurrent execution
|
||||
4. **Use JSON format** for debugging: `issue list --format=json | jq`
|
||||
5. **Clean up test data**: `issue list --label=agent-generated --format=json | jq -r '.[].number' | xargs -I {} issue close {}`
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Modify these examples for your use case
|
||||
- Add error handling and retry logic
|
||||
- Implement actual work instead of `time.sleep()`
|
||||
- Add logging and metrics
|
||||
- Deploy as services with proper process management
|
||||
|
||||
See [AGENT_INTEGRATION.md](../../AGENT_INTEGRATION.md) for more patterns and strategies.
|
||||
300
examples/agents/human_in_loop.py
Executable file
300
examples/agents/human_in_loop.py
Executable file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Human-in-the-Loop Agent
|
||||
|
||||
This agent demonstrates agent-human collaboration:
|
||||
1. Agent proposes implementations
|
||||
2. Human reviews and approves via comments
|
||||
3. Agent proceeds only after approval
|
||||
4. Agent can ask questions and wait for answers
|
||||
|
||||
Usage:
|
||||
export GITEA_URL=https://gitea.example.com
|
||||
export GITEA_TOKEN=your-token
|
||||
export GITEA_OWNER=your-org
|
||||
export GITEA_REPO=your-repo
|
||||
|
||||
python human_in_loop.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, 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 HumanInLoopAgent:
|
||||
"""Agent that requires human approval for critical decisions."""
|
||||
|
||||
def __init__(self, agent_id: str = "agent-collaborative"):
|
||||
self.agent_id = agent_id
|
||||
self.backend = None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to backend."""
|
||||
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"✓ Connected to {base_url}/{owner}/{repo}")
|
||||
|
||||
def find_feature_request(self) -> Optional[Issue]:
|
||||
"""Find a feature request that needs breaking down."""
|
||||
print("\n🔍 Looking for feature requests...")
|
||||
|
||||
issues = self.backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['feature', 'needs-breakdown']
|
||||
))
|
||||
|
||||
if issues:
|
||||
print(f" Found {len(issues)} feature request(s)")
|
||||
return issues[0]
|
||||
else:
|
||||
print(" No feature requests found")
|
||||
return None
|
||||
|
||||
def propose_breakdown(self, feature: Issue) -> List[str]:
|
||||
"""Propose breaking down a feature into subtasks."""
|
||||
print(f"\n💡 Analyzing feature #{feature.number}: {feature.title}")
|
||||
|
||||
# Simulate AI analysis
|
||||
time.sleep(2)
|
||||
|
||||
# Generate proposed subtasks
|
||||
subtasks = [
|
||||
f"Design database schema for {feature.title}",
|
||||
f"Implement backend API for {feature.title}",
|
||||
f"Create frontend components for {feature.title}",
|
||||
f"Write integration tests for {feature.title}",
|
||||
f"Update documentation for {feature.title}"
|
||||
]
|
||||
|
||||
print(f" Proposed {len(subtasks)} subtasks")
|
||||
return subtasks
|
||||
|
||||
def request_approval(self, issue: Issue, subtasks: List[str]):
|
||||
"""Post proposal and request human approval."""
|
||||
print(f"\n📝 Requesting approval for feature breakdown...")
|
||||
|
||||
# Format proposal
|
||||
proposal = f"🤖 **Agent Proposal: Feature Breakdown**\n\n"
|
||||
proposal += f"I analyzed this feature and propose breaking it into {len(subtasks)} subtasks:\n\n"
|
||||
|
||||
for i, task in enumerate(subtasks, 1):
|
||||
proposal += f"{i}. {task}\n"
|
||||
|
||||
proposal += f"\n**Human Review Required:**\n"
|
||||
proposal += f"- Reply with `APPROVED` to create these subtasks\n"
|
||||
proposal += f"- Reply with `REJECTED: reason` to decline\n"
|
||||
proposal += f"- Reply with `MODIFY: suggestions` to request changes\n\n"
|
||||
proposal += f"*Waiting for your response...*"
|
||||
|
||||
# Post comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body=proposal,
|
||||
author=User(id=self.agent_id, username=self.agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.backend.add_comment(issue.id, comment)
|
||||
|
||||
# Add label
|
||||
issue.labels.append(Label(name='awaiting-approval'))
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
print(f" ✓ Approval requested on issue #{issue.number}")
|
||||
|
||||
def check_for_approval(self, issue: Issue, since: datetime) -> Optional[str]:
|
||||
"""Check if human has approved/rejected."""
|
||||
comments = self.backend.get_comments(issue.id)
|
||||
|
||||
# Look for recent human comments
|
||||
for comment in reversed(comments):
|
||||
# Skip agent's own comments
|
||||
if comment.author.username == self.agent_id:
|
||||
continue
|
||||
|
||||
# Only check comments after proposal
|
||||
if comment.created_at <= since:
|
||||
continue
|
||||
|
||||
# Check for approval keywords
|
||||
body_upper = comment.body.upper()
|
||||
|
||||
if 'APPROVED' in body_upper:
|
||||
return 'approved'
|
||||
elif 'REJECTED' in body_upper:
|
||||
return 'rejected'
|
||||
elif 'MODIFY' in body_upper:
|
||||
return 'modify'
|
||||
|
||||
return None
|
||||
|
||||
def create_subtasks(self, parent: Issue, subtasks: List[str]):
|
||||
"""Create subtask issues."""
|
||||
print(f"\n✅ Creating {len(subtasks)} subtasks...")
|
||||
|
||||
for i, task_title in enumerate(subtasks, 1):
|
||||
subtask = Issue(
|
||||
id=None,
|
||||
number=0,
|
||||
title=task_title,
|
||||
description=f"**Subtask of #{parent.number}**: {parent.title}\n\n"
|
||||
f"This is subtask {i} of {len(subtasks)}.\n\n"
|
||||
f"**Parent Issue:** #{parent.number}",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=[
|
||||
Label(name='subtask'),
|
||||
Label(name=f'parent:{parent.number}'),
|
||||
Label(name='agent-generated')
|
||||
]
|
||||
)
|
||||
|
||||
created = self.backend.create_issue(subtask)
|
||||
print(f" Created subtask #{created.number}: {task_title}")
|
||||
time.sleep(0.5) # Rate limit
|
||||
|
||||
# Update parent issue
|
||||
parent.labels = [l for l in parent.labels
|
||||
if l.name not in ['needs-breakdown', 'awaiting-approval']]
|
||||
parent.labels.append(Label(name='broken-down'))
|
||||
self.backend.update_issue(parent)
|
||||
|
||||
# Add completion comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body=f"✅ Subtasks created successfully.\n\n"
|
||||
f"Created {len(subtasks)} subtasks. "
|
||||
f"You can now assign these to team members or agents.",
|
||||
author=User(id=self.agent_id, username=self.agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.backend.add_comment(parent.id, comment)
|
||||
|
||||
print(f" ✓ All subtasks created for #{parent.number}")
|
||||
|
||||
def wait_for_approval(self, issue: Issue, proposal_time: datetime,
|
||||
timeout_minutes: int = 60):
|
||||
"""Wait for human approval with timeout."""
|
||||
print(f"\n⏳ Waiting for human approval (timeout: {timeout_minutes}min)...")
|
||||
|
||||
start_time = datetime.now(timezone.utc)
|
||||
timeout = timedelta(minutes=timeout_minutes)
|
||||
check_interval = 30 # Check every 30 seconds
|
||||
|
||||
while True:
|
||||
elapsed = datetime.now(timezone.utc) - start_time
|
||||
|
||||
if elapsed > timeout:
|
||||
print(f"\n⏱️ Timeout: No response after {timeout_minutes} minutes")
|
||||
return None
|
||||
|
||||
# Check for approval
|
||||
decision = self.check_for_approval(issue, proposal_time)
|
||||
|
||||
if decision:
|
||||
print(f"\n✓ Human response: {decision.upper()}")
|
||||
return decision
|
||||
|
||||
# Wait before checking again
|
||||
remaining = int((timeout - elapsed).total_seconds() / 60)
|
||||
print(f" Still waiting... ({remaining} minutes remaining)")
|
||||
time.sleep(check_interval)
|
||||
|
||||
def run_cycle(self):
|
||||
"""Run one complete cycle."""
|
||||
# Find feature to break down
|
||||
feature = self.find_feature_request()
|
||||
if not feature:
|
||||
return False
|
||||
|
||||
# Propose breakdown
|
||||
subtasks = self.propose_breakdown(feature)
|
||||
|
||||
# Request approval
|
||||
proposal_time = datetime.now(timezone.utc)
|
||||
self.request_approval(feature, subtasks)
|
||||
|
||||
# Wait for human decision
|
||||
decision = self.wait_for_approval(feature, proposal_time, timeout_minutes=60)
|
||||
|
||||
if decision == 'approved':
|
||||
print("\n🎉 Proposal approved! Creating subtasks...")
|
||||
self.create_subtasks(feature, subtasks)
|
||||
return True
|
||||
|
||||
elif decision == 'rejected':
|
||||
print("\n❌ Proposal rejected. Moving to next feature.")
|
||||
feature.labels = [l for l in feature.labels
|
||||
if l.name != 'awaiting-approval']
|
||||
feature.labels.append(Label(name='proposal-rejected'))
|
||||
self.backend.update_issue(feature)
|
||||
return False
|
||||
|
||||
elif decision == 'modify':
|
||||
print("\n🔄 Modification requested. Human will update requirements.")
|
||||
feature.labels = [l for l in feature.labels
|
||||
if l.name != 'awaiting-approval']
|
||||
feature.labels.append(Label(name='needs-revision'))
|
||||
self.backend.update_issue(feature)
|
||||
return False
|
||||
|
||||
else:
|
||||
print("\n⏱️ No response. Will retry later.")
|
||||
feature.labels = [l for l in feature.labels
|
||||
if l.name != 'awaiting-approval']
|
||||
self.backend.update_issue(feature)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
try:
|
||||
agent = HumanInLoopAgent(agent_id="agent-collaborative")
|
||||
agent.connect()
|
||||
|
||||
print("\n🤖 Human-in-the-Loop Agent")
|
||||
print(" This agent proposes feature breakdowns and waits for approval\n")
|
||||
|
||||
if '--once' in sys.argv:
|
||||
agent.run_cycle()
|
||||
else:
|
||||
# Run continuously
|
||||
print(" Running in loop mode (Ctrl+C to stop)\n")
|
||||
while True:
|
||||
if not agent.run_cycle():
|
||||
print("\n⏸️ No work to do, waiting 60 seconds...")
|
||||
time.sleep(60)
|
||||
|
||||
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()
|
||||
314
examples/agents/monitoring_agent.py
Executable file
314
examples/agents/monitoring_agent.py
Executable file
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Monitoring Agent
|
||||
|
||||
This agent monitors issue health and sends alerts:
|
||||
- Detects stale issues (not updated in N days)
|
||||
- Finds blocked issues waiting too long
|
||||
- Identifies high-priority issues without assignees
|
||||
- Reports on project velocity and bottlenecks
|
||||
|
||||
Usage:
|
||||
export GITEA_URL=https://gitea.example.com
|
||||
export GITEA_TOKEN=your-token
|
||||
export GITEA_OWNER=your-org
|
||||
export GITEA_REPO=your-repo
|
||||
|
||||
python monitoring_agent.py [--stale-days=7] [--check-interval=3600]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from typing import List, Dict
|
||||
|
||||
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 MonitoringAgent:
|
||||
"""Agent that monitors issue tracker health and alerts on problems."""
|
||||
|
||||
def __init__(self, agent_id: str = "agent-monitor", stale_days: int = 7):
|
||||
self.agent_id = agent_id
|
||||
self.stale_threshold = timedelta(days=stale_days)
|
||||
self.backend = None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to backend."""
|
||||
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"✓ Connected to {base_url}/{owner}/{repo}")
|
||||
|
||||
def check_stale_issues(self) -> List[Issue]:
|
||||
"""Find issues that haven't been updated recently."""
|
||||
print("\n🔍 Checking for stale issues...")
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - self.stale_threshold
|
||||
|
||||
# Get all open issues
|
||||
all_issues = self.backend.list_issues(IssueFilter(state='open'))
|
||||
|
||||
# Filter to stale ones
|
||||
stale = [issue for issue in all_issues
|
||||
if issue.updated_at < cutoff]
|
||||
|
||||
if stale:
|
||||
print(f" ⚠️ Found {len(stale)} stale issue(s)")
|
||||
for issue in stale:
|
||||
days_stale = (datetime.now(timezone.utc) - issue.updated_at).days
|
||||
print(f" #{issue.number}: {issue.title} "
|
||||
f"(stale for {days_stale} days)")
|
||||
else:
|
||||
print(f" ✓ No stale issues found")
|
||||
|
||||
return stale
|
||||
|
||||
def check_blocked_issues(self) -> List[Issue]:
|
||||
"""Find blocked issues."""
|
||||
print("\n🔍 Checking for blocked issues...")
|
||||
|
||||
blocked = self.backend.list_issues(IssueFilter(
|
||||
state='blocked'
|
||||
))
|
||||
|
||||
if blocked:
|
||||
print(f" ⚠️ Found {len(blocked)} blocked issue(s)")
|
||||
for issue in blocked:
|
||||
days_blocked = (datetime.now(timezone.utc) - issue.updated_at).days
|
||||
print(f" #{issue.number}: {issue.title} "
|
||||
f"(blocked for {days_blocked} days)")
|
||||
else:
|
||||
print(f" ✓ No blocked issues")
|
||||
|
||||
return blocked
|
||||
|
||||
def check_unassigned_priority(self) -> List[Issue]:
|
||||
"""Find high-priority issues without assignees."""
|
||||
print("\n🔍 Checking for unassigned priority issues...")
|
||||
|
||||
# Get high priority issues
|
||||
high_priority = self.backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['priority:high']
|
||||
))
|
||||
|
||||
critical = self.backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['priority:critical']
|
||||
))
|
||||
|
||||
all_priority = high_priority + critical
|
||||
unassigned = [issue for issue in all_priority if not issue.assignees]
|
||||
|
||||
if unassigned:
|
||||
print(f" ⚠️ Found {len(unassigned)} unassigned priority issue(s)")
|
||||
for issue in unassigned:
|
||||
priority = next((l.name for l in issue.labels
|
||||
if l.name.startswith('priority:')), 'unknown')
|
||||
print(f" #{issue.number}: {issue.title} [{priority}]")
|
||||
else:
|
||||
print(f" ✓ All priority issues are assigned")
|
||||
|
||||
return unassigned
|
||||
|
||||
def analyze_velocity(self) -> Dict[str, int]:
|
||||
"""Analyze project velocity (issues opened vs closed)."""
|
||||
print("\n📊 Analyzing project velocity...")
|
||||
|
||||
# Get recent activity
|
||||
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
|
||||
|
||||
# Count recently created
|
||||
all_issues = self.backend.list_issues(IssueFilter(limit=1000))
|
||||
recent_created = len([i for i in all_issues if i.created_at >= week_ago])
|
||||
|
||||
# Count recently closed
|
||||
closed_issues = self.backend.list_issues(IssueFilter(state='closed'))
|
||||
recent_closed = len([i for i in closed_issues
|
||||
if i.closed_at and i.closed_at >= week_ago])
|
||||
|
||||
# Count open
|
||||
open_issues = len(self.backend.list_issues(IssueFilter(state='open')))
|
||||
|
||||
print(f" Last 7 days:")
|
||||
print(f" Created: {recent_created} issues")
|
||||
print(f" Closed: {recent_closed} issues")
|
||||
print(f" Net change: {recent_created - recent_closed:+d}")
|
||||
print(f" Currently open: {open_issues}")
|
||||
|
||||
if recent_created > recent_closed * 1.5:
|
||||
print(f" ⚠️ Issues are piling up faster than being resolved!")
|
||||
elif recent_closed > recent_created:
|
||||
print(f" ✓ Good progress - closing more than opening")
|
||||
|
||||
return {
|
||||
'created': recent_created,
|
||||
'closed': recent_closed,
|
||||
'open': open_issues
|
||||
}
|
||||
|
||||
def analyze_bottlenecks(self) -> Dict[str, int]:
|
||||
"""Identify bottlenecks in the workflow."""
|
||||
print("\n🔍 Analyzing workflow bottlenecks...")
|
||||
|
||||
# Count issues by state
|
||||
states = defaultdict(int)
|
||||
all_open = self.backend.list_issues(IssueFilter(state='open'))
|
||||
|
||||
for issue in all_open:
|
||||
states[issue.state.value] += 1
|
||||
|
||||
# Count issues by label
|
||||
labels = defaultdict(int)
|
||||
for issue in all_open:
|
||||
for label in issue.labels:
|
||||
if label.name.startswith('needs-'):
|
||||
labels[label.name] += 1
|
||||
|
||||
print(f" Issues by state:")
|
||||
for state, count in sorted(states.items(), key=lambda x: -x[1]):
|
||||
print(f" {state}: {count}")
|
||||
|
||||
if labels:
|
||||
print(f" Issues by stage:")
|
||||
for label, count in sorted(labels.items(), key=lambda x: -x[1]):
|
||||
print(f" {label}: {count}")
|
||||
if count > 10:
|
||||
print(f" ⚠️ Potential bottleneck!")
|
||||
|
||||
return dict(labels)
|
||||
|
||||
def send_alert(self, title: str, details: List[str]):
|
||||
"""Send an alert by creating an issue."""
|
||||
print(f"\n🚨 Sending alert: {title}")
|
||||
|
||||
body = f"**Monitoring Alert**\n\n"
|
||||
body += f"The monitoring agent detected potential issues:\n\n"
|
||||
|
||||
for detail in details:
|
||||
body += f"- {detail}\n"
|
||||
|
||||
body += f"\n*Generated by {self.agent_id} at {datetime.now(timezone.utc).isoformat()}*"
|
||||
|
||||
alert_issue = Issue(
|
||||
id=None,
|
||||
number=0,
|
||||
title=f"🚨 Monitor Alert: {title}",
|
||||
description=body,
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=[
|
||||
Label(name='monitoring'),
|
||||
Label(name='alert'),
|
||||
Label(name='agent-generated')
|
||||
]
|
||||
)
|
||||
|
||||
created = self.backend.create_issue(alert_issue)
|
||||
print(f" ✓ Alert created as issue #{created.number}")
|
||||
|
||||
def run_health_check(self, send_alerts: bool = True):
|
||||
"""Run complete health check."""
|
||||
print(f"\n🤖 Running issue tracker health check...")
|
||||
print(f" Timestamp: {datetime.now(timezone.utc).isoformat()}\n")
|
||||
|
||||
alerts = []
|
||||
|
||||
# Check for problems
|
||||
stale = self.check_stale_issues()
|
||||
if stale:
|
||||
alerts.append(f"{len(stale)} stale issues (>= {self.stale_threshold.days} days)")
|
||||
|
||||
blocked = self.check_blocked_issues()
|
||||
if blocked:
|
||||
alerts.append(f"{len(blocked)} blocked issues")
|
||||
|
||||
unassigned = self.check_unassigned_priority()
|
||||
if unassigned:
|
||||
alerts.append(f"{len(unassigned)} unassigned priority issues")
|
||||
|
||||
# Analyze metrics
|
||||
velocity = self.analyze_velocity()
|
||||
if velocity['created'] > velocity['closed'] * 2:
|
||||
alerts.append(f"Issue backlog growing rapidly "
|
||||
f"({velocity['created']} created vs {velocity['closed']} closed)")
|
||||
|
||||
bottlenecks = self.analyze_bottlenecks()
|
||||
for stage, count in bottlenecks.items():
|
||||
if count > 10:
|
||||
alerts.append(f"{count} issues stuck in stage: {stage}")
|
||||
|
||||
# Send alert if needed
|
||||
if alerts and send_alerts:
|
||||
self.send_alert("Issue Tracker Health Check", alerts)
|
||||
elif not alerts:
|
||||
print(f"\n✅ All checks passed - no issues found!")
|
||||
|
||||
return len(alerts) == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description='Monitoring agent for issue tracker')
|
||||
parser.add_argument('--stale-days', type=int, default=7,
|
||||
help='Days before an issue is considered stale')
|
||||
parser.add_argument('--check-interval', type=int, default=3600,
|
||||
help='Interval between checks in seconds (default: 1 hour)')
|
||||
parser.add_argument('--once', action='store_true',
|
||||
help='Run once and exit')
|
||||
parser.add_argument('--no-alerts', action='store_true',
|
||||
help='Do not create alert issues')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
agent = MonitoringAgent(
|
||||
agent_id="agent-monitor",
|
||||
stale_days=args.stale_days
|
||||
)
|
||||
agent.connect()
|
||||
|
||||
if args.once:
|
||||
agent.run_health_check(send_alerts=not args.no_alerts)
|
||||
else:
|
||||
print(f"\n🤖 Monitoring agent starting...")
|
||||
print(f" Check interval: {args.check_interval}s")
|
||||
print(f" Stale threshold: {args.stale_days} days")
|
||||
print(f" Press Ctrl+C to stop\n")
|
||||
|
||||
while True:
|
||||
agent.run_health_check(send_alerts=not args.no_alerts)
|
||||
print(f"\n⏸️ Waiting {args.check_interval}s until next check...")
|
||||
time.sleep(args.check_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()
|
||||
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()
|
||||
213
examples/agents/simple_task_executor.py
Executable file
213
examples/agents/simple_task_executor.py
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Task Executor Agent
|
||||
|
||||
This agent demonstrates a basic workflow:
|
||||
1. Query for available tasks (open issues with specific labels)
|
||||
2. Claim a task by assigning it to itself
|
||||
3. Execute the task (simulated)
|
||||
4. Report completion and close the issue
|
||||
|
||||
Usage:
|
||||
export GITEA_URL=https://gitea.example.com
|
||||
export GITEA_TOKEN=your-token
|
||||
export GITEA_OWNER=your-org
|
||||
export GITEA_REPO=your-repo
|
||||
|
||||
python simple_task_executor.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path for imports
|
||||
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 SimpleTaskExecutor:
|
||||
"""A simple agent that claims and executes tasks from issue tracker."""
|
||||
|
||||
def __init__(self, agent_id: str = "agent-executor"):
|
||||
self.agent_id = agent_id
|
||||
self.backend = None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to Gitea backend from environment variables."""
|
||||
base_url = os.environ.get('GITEA_URL')
|
||||
token = os.environ.get('GITEA_TOKEN')
|
||||
owner = os.environ.get('GITEA_OWNER')
|
||||
repo = os.environ.get('GITEA_REPO')
|
||||
|
||||
if not all([base_url, token, owner, repo]):
|
||||
raise ValueError(
|
||||
"Missing required environment variables: "
|
||||
"GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO"
|
||||
)
|
||||
|
||||
self.backend = GiteaBackend()
|
||||
self.backend.connect({
|
||||
'base_url': base_url,
|
||||
'token': token,
|
||||
'owner': owner,
|
||||
'repo': repo
|
||||
})
|
||||
print(f"✓ Connected to {base_url}/{owner}/{repo}")
|
||||
|
||||
def find_available_task(self):
|
||||
"""Find an open task that's ready to be worked on."""
|
||||
print("\n🔍 Searching for available tasks...")
|
||||
|
||||
# Look for issues labeled as ready and not assigned
|
||||
issues = self.backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['ready'],
|
||||
limit=10
|
||||
))
|
||||
|
||||
# Filter to unassigned issues
|
||||
available = [issue for issue in issues if not issue.assignees]
|
||||
|
||||
if available:
|
||||
print(f" Found {len(available)} available task(s)")
|
||||
return available[0]
|
||||
else:
|
||||
print(" No available tasks found")
|
||||
return None
|
||||
|
||||
def claim_task(self, issue: Issue):
|
||||
"""Claim a task by assigning it to this agent."""
|
||||
print(f"\n📌 Claiming issue #{issue.number}: {issue.title}")
|
||||
|
||||
# Update issue state and assignee
|
||||
issue.state = IssueState.IN_PROGRESS
|
||||
issue.assignees = [User(id=self.agent_id, username=self.agent_id)]
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
# Add comment announcing claim
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body=f"🤖 Task claimed by {self.agent_id}\n\nStarting work...",
|
||||
author=User(id=self.agent_id, username=self.agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.backend.add_comment(issue.id, comment)
|
||||
print(f" ✓ Issue #{issue.number} claimed")
|
||||
|
||||
def execute_task(self, issue: Issue):
|
||||
"""Execute the task (simulated with sleep)."""
|
||||
print(f"\n⚙️ Executing task #{issue.number}...")
|
||||
|
||||
# Simulate work by sleeping
|
||||
work_time = 2
|
||||
for i in range(work_time):
|
||||
time.sleep(1)
|
||||
print(f" Working... ({i+1}/{work_time}s)")
|
||||
|
||||
# Add progress comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body=f"Implementation complete.\n\n"
|
||||
f"**Changes made:**\n"
|
||||
f"- Analyzed requirements\n"
|
||||
f"- Implemented solution\n"
|
||||
f"- Verified functionality\n\n"
|
||||
f"Ready for review.",
|
||||
author=User(id=self.agent_id, username=self.agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.backend.add_comment(issue.id, comment)
|
||||
print(f" ✓ Task #{issue.number} completed")
|
||||
|
||||
def complete_task(self, issue: Issue):
|
||||
"""Mark task as complete by closing the issue."""
|
||||
print(f"\n✅ Completing issue #{issue.number}...")
|
||||
|
||||
# Close the issue
|
||||
issue.state = IssueState.CLOSED
|
||||
issue.closed_at = datetime.now(timezone.utc)
|
||||
self.backend.update_issue(issue)
|
||||
|
||||
# Add completion comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body=f"🎉 Task completed by {self.agent_id}\n\n"
|
||||
f"All work has been finished and verified.",
|
||||
author=User(id=self.agent_id, username=self.agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
self.backend.add_comment(issue.id, comment)
|
||||
print(f" ✓ Issue #{issue.number} closed")
|
||||
|
||||
def run_once(self):
|
||||
"""Execute one task cycle."""
|
||||
# Find available task
|
||||
task = self.find_available_task()
|
||||
if not task:
|
||||
return False
|
||||
|
||||
# Claim and execute
|
||||
self.claim_task(task)
|
||||
self.execute_task(task)
|
||||
self.complete_task(task)
|
||||
|
||||
return True
|
||||
|
||||
def run_loop(self, max_iterations=None):
|
||||
"""Run continuously, processing tasks as they become available."""
|
||||
print(f"\n🤖 {self.agent_id} starting task execution loop...")
|
||||
print(f" Press Ctrl+C to stop\n")
|
||||
|
||||
iteration = 0
|
||||
while True:
|
||||
iteration += 1
|
||||
|
||||
if max_iterations and iteration > max_iterations:
|
||||
print(f"\n⚠️ Reached maximum iterations ({max_iterations})")
|
||||
break
|
||||
|
||||
# Try to process one task
|
||||
processed = self.run_once()
|
||||
|
||||
if processed:
|
||||
print(f"\n✓ Completed iteration {iteration}")
|
||||
else:
|
||||
print(f"\n⏸️ No tasks available (iteration {iteration})")
|
||||
print(" Waiting 5 seconds before checking again...")
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
try:
|
||||
# Create and initialize agent
|
||||
agent = SimpleTaskExecutor(agent_id="agent-executor")
|
||||
agent.connect()
|
||||
|
||||
# Run once or in loop
|
||||
import sys
|
||||
if '--once' in sys.argv:
|
||||
agent.run_once()
|
||||
elif '--max-iterations' in sys.argv:
|
||||
idx = sys.argv.index('--max-iterations')
|
||||
max_iter = int(sys.argv[idx + 1])
|
||||
agent.run_loop(max_iterations=max_iter)
|
||||
else:
|
||||
agent.run_loop()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ Interrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user