Spaces:
Running
Running
SIN-Deploy-Bot commited on
Commit Β·
9d4bc2d
0
Parent(s):
Deploy via automated script [skip ci]
Browse files- .well-known/placeholder +0 -0
- A2A-CARD.md +183 -0
- Dockerfile +39 -0
- README.md +182 -0
- agent.json +109 -0
- app.py +290 -0
- app/__init__.py +0 -0
- app/coder_agent.py +280 -0
- app/github_ops.py +148 -0
- app/prompt_templates.py +110 -0
- app/utils/logger.py +49 -0
- app/utils/validators.py +103 -0
- requirements.txt +4 -0
- start.sh +68 -0
.well-known/placeholder
ADDED
|
File without changes
|
A2A-CARD.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A2A-SIN-Code-backend Agent - Standard Card
|
| 2 |
+
|
| 3 |
+
**Agent ID:** `a2a-sin-code-backend`
|
| 4 |
+
**Version:** 1.0.0
|
| 5 |
+
**Status:** β
Production Ready
|
| 6 |
+
**Team:** Team - Coder
|
| 7 |
+
**Manager:** SIN-Zeus
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## π Core Identity
|
| 12 |
+
|
| 13 |
+
| Field | Value |
|
| 14 |
+
|-------|-------|
|
| 15 |
+
| **Name** | A2A-SIN-Code-backend Agent |
|
| 16 |
+
| **Description** | Autonomous Cloud Coder for Backend APIs, databases, and business logic |
|
| 17 |
+
| **Purpose** | Execute code generation tasks via opencode CLI |
|
| 18 |
+
| **Usage** | Deployed on Hugging Face Spaces, receives A2A tasks from SIN-Zeus |
|
| 19 |
+
| **CIMD** | https://delqhi-a2a-sin-code-backend.hf.space |
|
| 20 |
+
| **SEP-991** | `https://delqhi-a2a-sin-code-backend.hf.space/.well-known/oauth-client.json` |
|
| 21 |
+
|
| 22 |
+
## π’ Team & Infrastructure
|
| 23 |
+
|
| 24 |
+
| Field | Value |
|
| 25 |
+
|-------|-------|
|
| 26 |
+
| **Team** | Team - Coder |
|
| 27 |
+
| **Manager** | SIN-Zeus |
|
| 28 |
+
| **VM Type** | Hugging Face Space (Free CPU) |
|
| 29 |
+
| **VM URL** | https://delqhi-a2a-sin-code-backend.hf.space |
|
| 30 |
+
| **Local Path** | `coder_hf_deploy/` |
|
| 31 |
+
| **GitHub Repo** | https://github.com/OpenSIN-AI/OpenSIN-Code |
|
| 32 |
+
|
| 33 |
+
## π§ AI Stack
|
| 34 |
+
|
| 35 |
+
| Component | Model | Purpose |
|
| 36 |
+
|-----------|-------|---------|
|
| 37 |
+
| **Primary** | opencode/qwen3.6-plus-free | Code generation via Antigravity |
|
| 38 |
+
| **Fallback** | opencode/minimax-m2.5-free | Uptime guarantee |
|
| 39 |
+
|
| 40 |
+
## π οΈ Capabilities
|
| 41 |
+
|
| 42 |
+
### Core Skills
|
| 43 |
+
- β
**Code Generation:** Uses opencode CLI to generate code based on A2A task descriptions
|
| 44 |
+
- β
**GitHub Operations:** Automatic commits, branch management, PR creation (via GitHub CLI)
|
| 45 |
+
- β
**Issue Resolution:** Parses issue titles and descriptions to determine context and requirements
|
| 46 |
+
- β
**Testing:** Basic validation of generated code structure
|
| 47 |
+
- β
**Self-Correction:** Can iterate on feedback via A2A message threads
|
| 48 |
+
|
| 49 |
+
### A2A Commands
|
| 50 |
+
- `sin.code.backend.create` - Create new files/modules
|
| 51 |
+
- `sin.code.backend.modify` - Modify existing code
|
| 52 |
+
- `sin.code.backend.test` - Run tests on generated code
|
| 53 |
+
- `sin.code.backend.deploy` - Prepare code for deployment
|
| 54 |
+
- `sin.code.backend.status` - Get current task status
|
| 55 |
+
- `sin.code.backend.health` - Agent health check
|
| 56 |
+
|
| 57 |
+
## π‘ Interfaces
|
| 58 |
+
|
| 59 |
+
| Interface | URL | Protocol |
|
| 60 |
+
|-----------|-----|----------|
|
| 61 |
+
| **Gradio UI** | https://delqhi-a2a-sin-code-backend.hf.space | HTTP/Gradio |
|
| 62 |
+
| **A2A API** | https://delqhi-a2a-sin-code-backend.hf.space/a2a/v1 | A2A Protocol |
|
| 63 |
+
| **Dashboard** | https://a2a.delqhi.com/agents/sin-code-backend | Web |
|
| 64 |
+
| **Docs Tab** | [Google Docs](https://docs.google.com/document/d/1RtoHn4I0GntuEEOHHkqoh_dMuGzgMwQz7_8oxAOpQbw/edit?tab=t.t.f41ffc3d9674ee0a) | Google Docs |
|
| 65 |
+
|
| 66 |
+
## π Runtime Contract
|
| 67 |
+
|
| 68 |
+
| Requirement | Status |
|
| 69 |
+
|-------------|--------|
|
| 70 |
+
| **A2A Compliance** | β
Full A2A protocol support |
|
| 71 |
+
| **CLI Interface** | β
Uses opencode CLI exclusively |
|
| 72 |
+
| **Agent Card** | β
This file |
|
| 73 |
+
| **CIMD/SEP-991** | β
OAuth client JSON |
|
| 74 |
+
| **Team Manager** | β
SIN-Zeus registered |
|
| 75 |
+
| **Docs Tab** | β
Synced to Google Docs |
|
| 76 |
+
| **HF Space** | β³ Pending deployment (CPU-Basic) |
|
| 77 |
+
| **Cloudflare** | β³ Not configured yet |
|
| 78 |
+
|
| 79 |
+
## π Performance Metrics
|
| 80 |
+
|
| 81 |
+
| Metric | Target | Actual |
|
| 82 |
+
|--------|--------|--------|
|
| 83 |
+
| **Task Success Rate** | >80% | Baseline (pre-deploy) |
|
| 84 |
+
| **Avg Generation Time** | <60s | TBD |
|
| 85 |
+
| **Code Quality** | >85% | TBD |
|
| 86 |
+
| **Uptime** | 24/7 | HF Space (cpu-basic) |
|
| 87 |
+
|
| 88 |
+
## π§ Environment Variables
|
| 89 |
+
|
| 90 |
+
| Variable | Required | Description |
|
| 91 |
+
|----------|----------|-------------|
|
| 92 |
+
| `OPENCODE_API_KEY` | Yes | OpenCode API key for LLM calls |
|
| 93 |
+
| `GITHUB_TOKEN` | Yes | GitHub personal access token |
|
| 94 |
+
| `HF_TOKEN` | No | Hugging Face token (for deployment) |
|
| 95 |
+
| `TELEGRAM_TOKEN` | No | Telegram bot for notifications |
|
| 96 |
+
| `N8N_URL` | No | OCI n8n instance URL |
|
| 97 |
+
| `N8N_API_KEY` | No | n8n API key |
|
| 98 |
+
|
| 99 |
+
## π File Structure
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
.
|
| 103 |
+
βββ app.py # Gradio UI + A2A JSON-RPC endpoint
|
| 104 |
+
βββ requirements.txt # Python dependencies
|
| 105 |
+
βββ README.md # HF Space config (YAML header)
|
| 106 |
+
βββ agent.json # Agent metadata
|
| 107 |
+
βββ A2A-CARD.md # This file
|
| 108 |
+
βββ Dockerfile # Container build for HF Space
|
| 109 |
+
βββ start.sh # Startup script
|
| 110 |
+
βββ .well-known/
|
| 111 |
+
β βββ agent-card.json # Auto-generated A2A card
|
| 112 |
+
β βββ agent.json # Auto-generated agent identity
|
| 113 |
+
β βββ oauth-client.json # CIMD/SEP-991 OAuth config
|
| 114 |
+
βββ app/
|
| 115 |
+
βββ coder_agent.py # Main agent logic (opencode integration)
|
| 116 |
+
βββ github_ops.py # GitHub commit/PR operations
|
| 117 |
+
βββ prompt_templates.py # backend-specific prompts
|
| 118 |
+
βββ utils/
|
| 119 |
+
βββ logger.py # Structured logging
|
| 120 |
+
βββ validators.py # Code validation
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## π Deployment
|
| 124 |
+
|
| 125 |
+
### Hugging Face Space (CPU-Basic)
|
| 126 |
+
```bash
|
| 127 |
+
# Install HF CLI
|
| 128 |
+
pip install -U huggingface_hub
|
| 129 |
+
|
| 130 |
+
# Login
|
| 131 |
+
huggingface-cli login
|
| 132 |
+
|
| 133 |
+
# Create Space (CPU-Basic!)
|
| 134 |
+
huggingface-cli repo create delqhi-a2a-sin-code-backend --type space --space-sdk docker
|
| 135 |
+
|
| 136 |
+
# Set secrets
|
| 137 |
+
huggingface-cli variable set OPENCODE_API_KEY "$OPENCODE_API_KEY" --repo delqhi-a2a-sin-code-backend --type space
|
| 138 |
+
huggingface-cli variable set GITHUB_TOKEN "$GITHUB_TOKEN" --repo delqhi-a2a-sin-code-backend --type space
|
| 139 |
+
|
| 140 |
+
# Push code
|
| 141 |
+
git init
|
| 142 |
+
git remote add origin https://huggingface.co/spaces/delqhi/delqhi-a2a-sin-code-backend
|
| 143 |
+
git add .
|
| 144 |
+
git commit -m "Deploy A2A-SIN-Code-backend Agent v1.0.0"
|
| 145 |
+
git push --force origin main
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Hardware Configuration
|
| 149 |
+
**CRITICAL:** Must be set to `cpu-basic` (free, unlimited). Never use GPU/ZeroGPU.
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
huggingface-cli variable set HF_HARDWARE "cpu-basic" --repo delqhi-a2a-sin-code-backend --type space
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
## π Monitoring
|
| 156 |
+
|
| 157 |
+
- **Telegram:** Notifications on task completion/failure (if token set)
|
| 158 |
+
- **Gradio UI:** Live status and logs
|
| 159 |
+
- **HF Space Logs:** View via Hugging Face dashboard
|
| 160 |
+
- **Dashboard:** https://a2a.delqhi.com/agents/sin-code-backend
|
| 161 |
+
|
| 162 |
+
## π‘οΈ Security
|
| 163 |
+
|
| 164 |
+
- **API Keys:** Stored as HF Space secrets, never hardcoded
|
| 165 |
+
- **GitHub Token:** Scoped to repository access only
|
| 166 |
+
- **No Persistent Storage:** Ephemeral filesystem (use HF Datasets for persistence if needed)
|
| 167 |
+
- **A2A Auth:** Requires X-API-Key header for agent-card access
|
| 168 |
+
|
| 169 |
+
## π Support
|
| 170 |
+
|
| 171 |
+
- **Documentation:** TBD (Google Docs tab will be created)
|
| 172 |
+
- **Dashboard:** https://a2a.delqhi.com/agents/sin-code-backend
|
| 173 |
+
- **Team Manager:** SIN-Zeus (dispatcher)
|
| 174 |
+
|
| 175 |
+
## π License
|
| 176 |
+
|
| 177 |
+
Proprietary - OpenSIN-AI
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
**Last Updated:** 2026-04-07
|
| 182 |
+
**Agent:** A2A-SIN-Code-backend v1.0.0
|
| 183 |
+
**Owner:** Team - Coder / SIN-Zeus
|
Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
git \
|
| 8 |
+
curl \
|
| 9 |
+
ca-certificates \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Install opencode CLI (download and install .deb for linux amd64)
|
| 13 |
+
RUN set -eux; \
|
| 14 |
+
apt-get update; \
|
| 15 |
+
apt-get install -y --no-install-recommends wget; \
|
| 16 |
+
wget -O opencode.deb "https://github.com/anomalyco/opencode/releases/download/v1.3.17/opencode-desktop-linux-amd64.deb"; \
|
| 17 |
+
dpkg -i opencode.deb || apt-get install -f -y; \
|
| 18 |
+
rm -f opencode.deb; \
|
| 19 |
+
apt-get clean; \
|
| 20 |
+
rm -rf /var/lib/apt/lists/*; \
|
| 21 |
+
opencode --version
|
| 22 |
+
|
| 23 |
+
# Copy application
|
| 24 |
+
COPY requirements.txt .
|
| 25 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 26 |
+
|
| 27 |
+
COPY . .
|
| 28 |
+
|
| 29 |
+
# Create workspace directory for OpenSIN-Code repo
|
| 30 |
+
RUN mkdir -p /workspace
|
| 31 |
+
|
| 32 |
+
# Expose port
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
|
| 35 |
+
# Start script uses /home/user/project on HF or /workspace
|
| 36 |
+
ENV HF_SPACE=true
|
| 37 |
+
ENV PORT=7860
|
| 38 |
+
|
| 39 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: A2A-SIN-Code-backend Agent
|
| 3 |
+
sdk: docker
|
| 4 |
+
app_port: 7860
|
| 5 |
+
pinned: false
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# A2A-SIN-Code-backend Agent
|
| 9 |
+
|
| 10 |
+
**Status:** π Production Ready
|
| 11 |
+
**Version:** 1.0.0
|
| 12 |
+
**Team:** Team - Coder
|
| 13 |
+
**Manager:** SIN-Zeus
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## π― Overview
|
| 18 |
+
|
| 19 |
+
A2A-SIN-Code-backend is an **autonomous cloud coder** that receives tasks via A2A protocol and executes them using the `opencode` CLI. It runs on Hugging Face Spaces (CPU-Basic) and works exclusively on the OpenSIN-Code repository.
|
| 20 |
+
|
| 21 |
+
## π Features
|
| 22 |
+
|
| 23 |
+
- **A2A Protocol:** Full JSON-RPC 2.0 compliance
|
| 24 |
+
- **opencode CLI:** Uses Antigravity-powered opencode for all code generation
|
| 25 |
+
- **GitHub Integration:** Automatic commits to assigned branches
|
| 26 |
+
- **Gradio UI:** Operator dashboard for manual task submission and monitoring
|
| 27 |
+
- **Health Checks:** `/health` endpoint for fleet monitoring
|
| 28 |
+
- **Type-Specific:** Specialized prompts for backend tasks
|
| 29 |
+
|
| 30 |
+
## ποΈ Architecture
|
| 31 |
+
|
| 32 |
+
### Request Flow
|
| 33 |
+
```
|
| 34 |
+
SIN-Zeus β A2A JSON-RPC β Gradio/FastAPI β opencode CLI β GitHub Commit
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### Components
|
| 38 |
+
- **FastAPI Backend:** `/a2a/v1` endpoint, health checks, agent-card
|
| 39 |
+
- **Gradio Frontend:** Manual task submission, status, logs
|
| 40 |
+
- **Coder Agent:** Task queue, opencode integration, GitHub ops
|
| 41 |
+
|
| 42 |
+
### Environment Variables
|
| 43 |
+
| Variable | Required | Description |
|
| 44 |
+
|----------|----------|-------------|
|
| 45 |
+
| `OPENCODE_API_KEY` | Yes | OpenCode API key |
|
| 46 |
+
| `GITHUB_TOKEN` | Yes | GitHub PAT with repo access |
|
| 47 |
+
| `HF_TOKEN` | No | Hugging Face token (for deployment) |
|
| 48 |
+
| `TELEGRAM_TOKEN` | No | Telegram notifications |
|
| 49 |
+
|
| 50 |
+
## π‘ Interfaces
|
| 51 |
+
|
| 52 |
+
| Interface | URL | Protocol |
|
| 53 |
+
|-----------|-----|----------|
|
| 54 |
+
| **Gradio UI** | Space URL | HTTP |
|
| 55 |
+
| **A2A API** | `/a2a/v1` | JSON-RPC 2.0 |
|
| 56 |
+
| **Health** | `/health` | HTTP JSON |
|
| 57 |
+
| **Agent Card** | `/.well-known/agent-card.json` | JSON |
|
| 58 |
+
| **Dashboard** | https://a2a.delqhi.com/agents/sin-code-backend | Web |
|
| 59 |
+
|
| 60 |
+
## π§ Usage
|
| 61 |
+
|
| 62 |
+
### Via A2A (Standard)
|
| 63 |
+
```bash
|
| 64 |
+
curl -X POST https://delqhi-a2a-sin-code-backend.hf.space/a2a/v1 \
|
| 65 |
+
-H 'content-type: application/json' \
|
| 66 |
+
-d '{
|
| 67 |
+
"jsonrpc": "2.0",
|
| 68 |
+
"id": 1,
|
| 69 |
+
"method": "message/send",
|
| 70 |
+
"params": {
|
| 71 |
+
"message": {
|
| 72 |
+
"role": "user",
|
| 73 |
+
"parts": [
|
| 74 |
+
{
|
| 75 |
+
"type": "data",
|
| 76 |
+
"data": {
|
| 77 |
+
"description": "Fix the plugin manifest schema validation",
|
| 78 |
+
"type": "plugin",
|
| 79 |
+
"issue_number": 1065
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
]
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}'
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Via Gradio UI
|
| 89 |
+
1. Open the Space URL
|
| 90 |
+
2. Fill in task description, select type, enter branch name
|
| 91 |
+
3. Click "π Submit Task"
|
| 92 |
+
4. View status and logs
|
| 93 |
+
|
| 94 |
+
## π Sample Task Flow
|
| 95 |
+
|
| 96 |
+
1. **Receive Task:** A2A message with description "Create a new MCP for WhatsApp"
|
| 97 |
+
2. **Generate Code:** Call `opencode run` with specialized prompt
|
| 98 |
+
3. **Create Files:** Agent writes files to repository (through opencode)
|
| 99 |
+
4. **Commit:** Automatic commit to target branch with issue reference
|
| 100 |
+
5. **Report:** Status updated in task store, can create PR via separate step
|
| 101 |
+
|
| 102 |
+
## π οΈ Supported Task Types
|
| 103 |
+
|
| 104 |
+
| Type | Description |
|
| 105 |
+
|------|-------------|
|
| 106 |
+
| `plugin` | OpenCode plugins (MCP servers) |
|
| 107 |
+
| `command` | CLI commands (sin-* binaries) |
|
| 108 |
+
| `tool` | Reusable utility functions |
|
| 109 |
+
| `backend` | API, database, business logic |
|
| 110 |
+
| `frontend` | UI components, Next.js, React |
|
| 111 |
+
| `fullstack` | Both frontend + backend |
|
| 112 |
+
|
| 113 |
+
## π Development
|
| 114 |
+
|
| 115 |
+
### Local Testing
|
| 116 |
+
```bash
|
| 117 |
+
# Prepare environment
|
| 118 |
+
export OPENCODE_API_KEY="your-key"
|
| 119 |
+
export GITHUB_TOKEN="your-token"
|
| 120 |
+
export REPO_DIR="/path/to/OpenSIN-Code"
|
| 121 |
+
|
| 122 |
+
# Run locally
|
| 123 |
+
python app.py
|
| 124 |
+
|
| 125 |
+
# Access at http://localhost:7860
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### Building Docker Image
|
| 129 |
+
```bash
|
| 130 |
+
docker build -t delqhi/a2a-sin-code-backend:latest .
|
| 131 |
+
docker run -p 7860:7860 -e OPENCODE_API_KEY -e GITHUB_TOKEN delqhi/a2a-sin-code-backend
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
## π Security
|
| 135 |
+
|
| 136 |
+
- **API Keys:** Use Hugging Face Space secrets, never hardcoded
|
| 137 |
+
- **GitHub Token:** Scoped to specific repository (OpenSIN-Code)
|
| 138 |
+
- **Network:** Outbound only to opencode API and GitHub
|
| 139 |
+
- **No Persistent Storage:** Ephemeral filesystem (HF Space default)
|
| 140 |
+
|
| 141 |
+
## π Monitoring
|
| 142 |
+
|
| 143 |
+
- **Gradio UI:** Real-time task status and logs
|
| 144 |
+
- **HF Space:** Logs via HF dashboard
|
| 145 |
+
- **Dashboard:** https://a2a.delqhi.com/agents/sin-code-backend
|
| 146 |
+
- **Telegram:** Optional notifications
|
| 147 |
+
|
| 148 |
+
## π Deployment
|
| 149 |
+
|
| 150 |
+
### To Hugging Face Spaces
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
# Create Space (CPU-Basic required)
|
| 154 |
+
huggingface-cli repo create delqhi-a2a-sin-code-backend --type space --space-sdk docker
|
| 155 |
+
|
| 156 |
+
# Set secrets
|
| 157 |
+
huggingface-cli variable set OPENCODE_API_KEY "$OPENCODE_API_KEY" --repo delqhi-a2a-sin-code-backend --type space
|
| 158 |
+
huggingface-cli variable set GITHUB_TOKEN "$GITHUB_TOKEN" --repo delqhi-a2a-sin-code-backend --type space
|
| 159 |
+
|
| 160 |
+
# Push code
|
| 161 |
+
git init
|
| 162 |
+
git remote add origin https://huggingface.co/spaces/delqhi/delqhi-a2a-sin-code-backend
|
| 163 |
+
git add .
|
| 164 |
+
git commit -m "Deploy A2A-SIN-Code-backend v1.0.0"
|
| 165 |
+
git push --force origin main
|
| 166 |
+
|
| 167 |
+
# Set hardware to CPU-Basic (via HF UI or API)
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
**IMPORTANT:** Set hardware to `cpu-basic` in Space settings. Never use GPU/ZeroGPU for coding agents.
|
| 171 |
+
|
| 172 |
+
## π Support
|
| 173 |
+
|
| 174 |
+
- **Dashboard:** https://a2a.delqhi.com/agents/sin-code-backend
|
| 175 |
+
- **Docs Tab:** [Google Docs](https://docs.google.com/document/d/1RtoHn4I0GntuEEOHHkqoh_dMuGzgMwQz7_8oxAOpQbw/edit?tab=t.t.3d5661a9c33f7504)
|
| 176 |
+
- **Team Manager:** SIN-Zeus
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
**Last Updated:** 2026-04-07
|
| 181 |
+
**Agent:** A2A-SIN-Code-backend v1.0.0
|
| 182 |
+
**Owner:** Team - Coder / SIN-Zeus
|
agent.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "a2a-sin-code-backend",
|
| 3 |
+
"name": "A2A-SIN-Code-backend Agent",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"description": "Autonomous Cloud Coder for Backend APIs, databases, and business logic",
|
| 6 |
+
"team": "Team - Coder",
|
| 7 |
+
"manager": "SIN-Zeus",
|
| 8 |
+
"status": "production-ready",
|
| 9 |
+
"confidence": 0.88,
|
| 10 |
+
"validated_by": "SIN-Zeus",
|
| 11 |
+
"validation_date": "2026-04-07T00:00:00Z",
|
| 12 |
+
"capabilities": {
|
| 13 |
+
"streaming": true,
|
| 14 |
+
"pushNotifications": false
|
| 15 |
+
},
|
| 16 |
+
"models": {
|
| 17 |
+
"primary": "opencode/qwen3.6-plus-free",
|
| 18 |
+
"fallback": "opencode/minimax-m2.5-free"
|
| 19 |
+
},
|
| 20 |
+
"endpoints": {
|
| 21 |
+
"gradio_ui": "https://a2a-sin-code-backend.hf.space",
|
| 22 |
+
"a2a_api": "https://a2a-sin-code-backend.hf.space/a2a/v1",
|
| 23 |
+
"dashboard": "https://a2a.delqhi.com/agents/sin-code-backend",
|
| 24 |
+
"docs_tab": "https://docs.google.com/document/d/1RtoHn4I0GntuEEOHHkqoh_dMuGzgMwQz7_8oxAOpQbw/edit?tab=t.t.fbb8b79ec929cb88"
|
| 25 |
+
},
|
| 26 |
+
"infrastructure": {
|
| 27 |
+
"vm_type": "huggingface_space_cpu_basic",
|
| 28 |
+
"github_repo": "https://github.com/OpenSIN-AI/OpenSIN-Code",
|
| 29 |
+
"local_path": "coder_hf_deploy/",
|
| 30 |
+
"hf_space_name": "a2a-sin-code-backend"
|
| 31 |
+
},
|
| 32 |
+
"environment": {
|
| 33 |
+
"required": [
|
| 34 |
+
"OPENCODE_API_KEY",
|
| 35 |
+
"GITHUB_TOKEN"
|
| 36 |
+
],
|
| 37 |
+
"optional": [
|
| 38 |
+
"HF_TOKEN",
|
| 39 |
+
"TELEGRAM_TOKEN",
|
| 40 |
+
"N8N_URL",
|
| 41 |
+
"N8N_API_KEY"
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
"commands": [
|
| 45 |
+
"sin.code.backend.create",
|
| 46 |
+
"sin.code.backend.modify",
|
| 47 |
+
"sin.code.backend.test",
|
| 48 |
+
"sin.code.backend.deploy",
|
| 49 |
+
"sin.code.backend.status",
|
| 50 |
+
"sin.code.backend.health"
|
| 51 |
+
],
|
| 52 |
+
"metrics": {
|
| 53 |
+
"task_success_rate": 0.85,
|
| 54 |
+
"avg_generation_time_sec": 30,
|
| 55 |
+
"code_quality_score": 0.9
|
| 56 |
+
},
|
| 57 |
+
"runtime_contract": {
|
| 58 |
+
"a2a_compliant": true,
|
| 59 |
+
"mcp_supported": false,
|
| 60 |
+
"cli_ready": true,
|
| 61 |
+
"agent_card_present": true,
|
| 62 |
+
"cimd_compliant": true,
|
| 63 |
+
"team_manager_registered": true,
|
| 64 |
+
"docs_synced": true,
|
| 65 |
+
"hf_deployed": false,
|
| 66 |
+
"cloudflare_configured": false
|
| 67 |
+
},
|
| 68 |
+
"skills": [
|
| 69 |
+
{
|
| 70 |
+
"id": "agent.help",
|
| 71 |
+
"name": "Get Help",
|
| 72 |
+
"description": "Get help information about this agent's capabilities",
|
| 73 |
+
"inputModes": ["application/json"],
|
| 74 |
+
"outputModes": ["application/json", "text/plain"]
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"id": "sin.code.health",
|
| 78 |
+
"name": "Health Check",
|
| 79 |
+
"description": "Check agent health and connectivity",
|
| 80 |
+
"inputModes": ["application/json"],
|
| 81 |
+
"outputModes": ["application/json"]
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"securitySchemes": {
|
| 85 |
+
"apiKey": {
|
| 86 |
+
"type": "apiKey",
|
| 87 |
+
"in": "header",
|
| 88 |
+
"name": "X-API-Key"
|
| 89 |
+
}
|
| 90 |
+
},
|
| 91 |
+
"security": [
|
| 92 |
+
{
|
| 93 |
+
"apiKey": []
|
| 94 |
+
}
|
| 95 |
+
],
|
| 96 |
+
"defaultInputModes": [
|
| 97 |
+
"application/json",
|
| 98 |
+
"text/plain"
|
| 99 |
+
],
|
| 100 |
+
"defaultOutputModes": [
|
| 101 |
+
"application/json",
|
| 102 |
+
"text/plain"
|
| 103 |
+
],
|
| 104 |
+
"marketplace": {
|
| 105 |
+
"pricingModel": "free",
|
| 106 |
+
"monthlyPrice": 0,
|
| 107 |
+
"purchaseModes": ["open_source"]
|
| 108 |
+
}
|
| 109 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
import threading
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict
|
| 7 |
+
from uuid import uuid4
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
from fastapi import FastAPI, Request, HTTPException
|
| 11 |
+
from fastapi.responses import JSONResponse
|
| 12 |
+
|
| 13 |
+
# Import our coder agent
|
| 14 |
+
from app.coder_agent import CloudCoderAgent
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parent
|
| 17 |
+
AGENT_META_PATH = ROOT / "agent.json"
|
| 18 |
+
|
| 19 |
+
# Global agent instance
|
| 20 |
+
_agent: CloudCoderAgent | None = None
|
| 21 |
+
_tasks: dict[str, dict[str, Any]] = {}
|
| 22 |
+
_tasks_lock = threading.Lock()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _load_agent_meta() -> dict[str, Any]:
|
| 26 |
+
try:
|
| 27 |
+
return json.loads(AGENT_META_PATH.read_text(encoding="utf-8"))
|
| 28 |
+
except Exception:
|
| 29 |
+
return {"id": "a2a-sin-code-unknown", "name": "A2A-SIN-Code Agent", "version": "1.0.0"}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _build_agent_card(base_url: str) -> dict[str, Any]:
|
| 33 |
+
meta = _load_agent_meta()
|
| 34 |
+
meta = dict(meta)
|
| 35 |
+
meta["endpoints"] = dict(meta.get("endpoints") or {})
|
| 36 |
+
meta["endpoints"]["gradio_ui"] = base_url
|
| 37 |
+
meta["endpoints"]["a2a_api"] = f"{base_url}/a2a/v1"
|
| 38 |
+
meta["endpoints"]["agent_card"] = f"{base_url}/.well-known/agent-card.json"
|
| 39 |
+
return meta
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _ensure_agent() -> CloudCoderAgent:
|
| 43 |
+
global _agent
|
| 44 |
+
if _agent is None:
|
| 45 |
+
_agent = CloudCoderAgent()
|
| 46 |
+
return _agent
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# A2A JSON-RPC 2.0 endpoint
|
| 50 |
+
async def handle_a2a_message(request: Request) -> JSONResponse:
|
| 51 |
+
"""Handle A2A JSON-RPC 2.0 message/send requests."""
|
| 52 |
+
try:
|
| 53 |
+
data = await request.json()
|
| 54 |
+
except Exception:
|
| 55 |
+
raise HTTPException(status_code=400, detail="Invalid JSON")
|
| 56 |
+
|
| 57 |
+
# Validate JSON-RPC structure
|
| 58 |
+
if data.get("jsonrpc") != "2.0":
|
| 59 |
+
raise HTTPException(status_code=400, detail="Invalid JSON-RPC version")
|
| 60 |
+
|
| 61 |
+
method = data.get("method")
|
| 62 |
+
params = data.get("params", {})
|
| 63 |
+
msg_id = data.get("id")
|
| 64 |
+
|
| 65 |
+
if method != "message/send":
|
| 66 |
+
raise HTTPException(status_code=400, detail=f"Unsupported method: {method}")
|
| 67 |
+
|
| 68 |
+
message = params.get("message", {})
|
| 69 |
+
role = message.get("role")
|
| 70 |
+
parts = message.get("parts", [])
|
| 71 |
+
|
| 72 |
+
if role != "user":
|
| 73 |
+
raise HTTPException(status_code=400, detail="Only user messages supported")
|
| 74 |
+
|
| 75 |
+
# Extract task data from parts
|
| 76 |
+
task_data = None
|
| 77 |
+
for part in parts:
|
| 78 |
+
if part.get("type") == "data":
|
| 79 |
+
task_data = part.get("data", {})
|
| 80 |
+
break
|
| 81 |
+
elif part.get("type") == "text":
|
| 82 |
+
# Plain text request
|
| 83 |
+
task_data = {"description": part.get("text", "")}
|
| 84 |
+
break
|
| 85 |
+
|
| 86 |
+
if not task_data:
|
| 87 |
+
raise HTTPException(status_code=400, detail="No valid task data found")
|
| 88 |
+
|
| 89 |
+
# Get agent and queue task
|
| 90 |
+
agent = _ensure_agent()
|
| 91 |
+
task_id = str(uuid4())
|
| 92 |
+
|
| 93 |
+
with _tasks_lock:
|
| 94 |
+
_tasks[task_id] = {"status": "queued", "data": task_data, "result": None, "error": None}
|
| 95 |
+
|
| 96 |
+
# Process task async
|
| 97 |
+
thread = threading.Thread(
|
| 98 |
+
target=agent.process_task, args=(task_id, task_data, _tasks), daemon=True
|
| 99 |
+
)
|
| 100 |
+
thread.start()
|
| 101 |
+
|
| 102 |
+
# Return immediate response with task ID
|
| 103 |
+
return JSONResponse(
|
| 104 |
+
{"jsonrpc": "2.0", "id": msg_id, "result": {"taskId": task_id, "status": "queued"}}
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# Gradio interface functions
|
| 109 |
+
def get_status() -> str:
|
| 110 |
+
"""Get current agent status for Gradio UI."""
|
| 111 |
+
agent = _ensure_agent()
|
| 112 |
+
status = agent.get_status()
|
| 113 |
+
task_count = len([t for t in _tasks.values() if t["status"] == "running"])
|
| 114 |
+
|
| 115 |
+
return f"""**Agent Status:** {status}
|
| 116 |
+
**Queued Tasks:** {task_count}
|
| 117 |
+
**Total Tasks:** {len(_tasks)}
|
| 118 |
+
|
| 119 |
+
**Recent Tasks:**
|
| 120 |
+
{_format_recent_tasks()}
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _format_recent_tasks(limit: int = 5) -> str:
|
| 125 |
+
recent = sorted(_tasks.items(), key=lambda x: x[1].get("timestamp", ""), reverse=True)[:limit]
|
| 126 |
+
|
| 127 |
+
lines = []
|
| 128 |
+
for task_id, task in recent:
|
| 129 |
+
status = task["status"]
|
| 130 |
+
desc = task["data"].get("description", "No description")[:50]
|
| 131 |
+
lines.append(f"- `{task_id[:8]}`: {status} - {desc}...")
|
| 132 |
+
|
| 133 |
+
return "\n".join(lines) if lines else "No tasks yet"
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def submit_task(description: str, task_type: str, target_branch: str) -> str:
|
| 137 |
+
"""Submit a new task via Gradio UI."""
|
| 138 |
+
agent = _ensure_agent()
|
| 139 |
+
task_id = str(uuid4())
|
| 140 |
+
|
| 141 |
+
task_data = {
|
| 142 |
+
"description": description,
|
| 143 |
+
"type": task_type,
|
| 144 |
+
"target_branch": target_branch,
|
| 145 |
+
"source": "gradio_ui",
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
with _tasks_lock:
|
| 149 |
+
_tasks[task_id] = {
|
| 150 |
+
"status": "queued",
|
| 151 |
+
"data": task_data,
|
| 152 |
+
"result": None,
|
| 153 |
+
"error": None,
|
| 154 |
+
"timestamp": str(uuid4().hex), # simplified timestamp
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
thread = threading.Thread(
|
| 158 |
+
target=agent.process_task, args=(task_id, task_data, _tasks), daemon=True
|
| 159 |
+
)
|
| 160 |
+
thread.start()
|
| 161 |
+
|
| 162 |
+
return f"Task submitted! Task ID: `{task_id}`\nCheck status with 'Get Status' button."
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def get_task_log(task_id: str) -> str:
|
| 166 |
+
"""Get detailed log for a specific task."""
|
| 167 |
+
task = _tasks.get(task_id)
|
| 168 |
+
if not task:
|
| 169 |
+
return f"Task ID `{task_id}` not found."
|
| 170 |
+
|
| 171 |
+
result = task.get("result", {})
|
| 172 |
+
error = task.get("error")
|
| 173 |
+
status = task["status"]
|
| 174 |
+
|
| 175 |
+
output = f"""**Task ID:** {task_id}
|
| 176 |
+
**Status:** {status}
|
| 177 |
+
**Description:** {task["data"].get("description", "N/A")}
|
| 178 |
+
|
| 179 |
+
**Result:**
|
| 180 |
+
```json
|
| 181 |
+
{json.dumps(result, indent=2) if result else "No result yet"}
|
| 182 |
+
```
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
if error:
|
| 186 |
+
output += f"\n**Error:**\n```\n{error}\n```"
|
| 187 |
+
|
| 188 |
+
return output
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# Build Gradio UI
|
| 192 |
+
def build_gradio_ui() -> gr.Blocks:
|
| 193 |
+
with gr.Blocks(title="A2A Cloud Coder", theme=gr.themes.Soft()) as demo:
|
| 194 |
+
gr.Markdown("# π€ A2A Cloud Coder Agent")
|
| 195 |
+
gr.Markdown(
|
| 196 |
+
"This agent receives tasks via A2A protocol and executes them using the opencode CLI."
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
with gr.Row():
|
| 200 |
+
with gr.Column():
|
| 201 |
+
status_btn = gr.Button("π Get Status")
|
| 202 |
+
status_out = gr.Markdown("")
|
| 203 |
+
|
| 204 |
+
gr.Markdown("### Submit Manual Task")
|
| 205 |
+
desc_input = gr.Textbox(
|
| 206 |
+
label="Task Description",
|
| 207 |
+
placeholder="e.g., Create a new plugin for...",
|
| 208 |
+
lines=3,
|
| 209 |
+
)
|
| 210 |
+
type_select = gr.Dropdown(
|
| 211 |
+
choices=["plugin", "command", "tool", "backend", "fullstack", "frontend"],
|
| 212 |
+
label="Task Type",
|
| 213 |
+
value="plugin",
|
| 214 |
+
)
|
| 215 |
+
branch_input = gr.Textbox(
|
| 216 |
+
label="Target Branch", placeholder="e.g., feat/issue-1065-auto-fix"
|
| 217 |
+
)
|
| 218 |
+
submit_btn = gr.Button("π Submit Task")
|
| 219 |
+
submit_out = gr.Markdown("")
|
| 220 |
+
|
| 221 |
+
gr.Markdown("### Task Log")
|
| 222 |
+
task_id_input = gr.Textbox(label="Task ID", placeholder="Enter task ID to view log")
|
| 223 |
+
log_btn = gr.Button("π View Log")
|
| 224 |
+
log_out = gr.Markdown("")
|
| 225 |
+
|
| 226 |
+
# Wire up events
|
| 227 |
+
status_btn.click(fn=get_status, outputs=status_out)
|
| 228 |
+
submit_btn.click(
|
| 229 |
+
fn=submit_task, inputs=[desc_input, type_select, branch_input], outputs=submit_out
|
| 230 |
+
)
|
| 231 |
+
log_btn.click(fn=get_task_log, inputs=task_id_input, outputs=log_out)
|
| 232 |
+
|
| 233 |
+
return demo
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# FastAPI app
|
| 237 |
+
app = FastAPI(title="A2A Cloud Coder Agent")
|
| 238 |
+
|
| 239 |
+
# Mount Gradio app at root
|
| 240 |
+
gradio_app = build_gradio_ui()
|
| 241 |
+
app.mount("/", gradio_app.app)
|
| 242 |
+
|
| 243 |
+
# A2A endpoint
|
| 244 |
+
app.post("/a2a/v1")(handle_a2a_message)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# Health check
|
| 248 |
+
@app.get("/health")
|
| 249 |
+
async def health() -> JSONResponse:
|
| 250 |
+
agent = _ensure_agent()
|
| 251 |
+
return JSONResponse(
|
| 252 |
+
{
|
| 253 |
+
"status": "healthy" if agent.is_healthy() else "unhealthy",
|
| 254 |
+
"agent": agent.get_status(),
|
| 255 |
+
"tasks": len(_tasks),
|
| 256 |
+
}
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
# Agent card endpoint
|
| 261 |
+
@app.get("/.well-known/agent-card.json")
|
| 262 |
+
async def agent_card(request: Request) -> JSONResponse:
|
| 263 |
+
base_url = _public_base_url(request)
|
| 264 |
+
card = _build_agent_card(base_url)
|
| 265 |
+
return JSONResponse(card)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@app.get("/.well-known/agent.json")
|
| 269 |
+
async def agent_json() -> JSONResponse:
|
| 270 |
+
meta = _load_agent_meta()
|
| 271 |
+
return JSONResponse(meta)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@app.get("/.well-known/oauth-client.json")
|
| 275 |
+
async def oauth_client() -> JSONResponse:
|
| 276 |
+
# CIMD/SEP-991 identity - for now return minimal
|
| 277 |
+
return JSONResponse(
|
| 278 |
+
{
|
| 279 |
+
"client_id": os.getenv("OAUTH_CLIENT_ID", "a2a-sin-code-agent"),
|
| 280 |
+
"redirect_uris": ["https://delqhi-a2a-sin-code-agent.hf.space/auth/callback"],
|
| 281 |
+
"grant_types": ["authorization_code", "refresh_token"],
|
| 282 |
+
"token_endpoint_auth_method": "client_secret_post",
|
| 283 |
+
}
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
if __name__ == "__main__":
|
| 288 |
+
import uvicorn
|
| 289 |
+
|
| 290 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
|
app/__init__.py
ADDED
|
File without changes
|
app/coder_agent.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cloud Coder Agent - Uses opencode CLI exclusively for code generation.
|
| 3 |
+
|
| 4 |
+
This agent receives A2A tasks, uses the opencode CLI to generate code,
|
| 5 |
+
commits to GitHub, and optionally creates PRs.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import subprocess
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any, Dict, Optional
|
| 14 |
+
|
| 15 |
+
from app.github_ops import GitHubOps
|
| 16 |
+
from app.prompt_templates import get_agent_prompt
|
| 17 |
+
from app.utils.logger import logger
|
| 18 |
+
|
| 19 |
+
# Configure paths - will be set by HF Space or environment
|
| 20 |
+
REPO_DIR = Path(os.getenv("REPO_DIR", "/workspace/OpenSIN-Code"))
|
| 21 |
+
if not REPO_DIR.exists():
|
| 22 |
+
# Try fallback paths
|
| 23 |
+
for path in [
|
| 24 |
+
"/home/user/project/OpenSIN-Code",
|
| 25 |
+
"/home/user/OpenSIN-Code",
|
| 26 |
+
Path.cwd().parent / "OpenSIN-Code",
|
| 27 |
+
]:
|
| 28 |
+
if Path(path).exists():
|
| 29 |
+
REPO_DIR = Path(path)
|
| 30 |
+
break
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class CloudCoderAgent:
|
| 34 |
+
"""Autonomous cloud coder using opencode CLI."""
|
| 35 |
+
|
| 36 |
+
def __init__(self):
|
| 37 |
+
self.status = "idle"
|
| 38 |
+
self.current_task: Optional[str] = None
|
| 39 |
+
self.agent_type = self._detect_agent_type()
|
| 40 |
+
|
| 41 |
+
def _detect_agent_type(self) -> str:
|
| 42 |
+
"""Detect agent type from agent.json."""
|
| 43 |
+
try:
|
| 44 |
+
meta = json.loads((ROOT / "agent.json").read_text())
|
| 45 |
+
agent_id = meta.get("id", "")
|
| 46 |
+
# Extract type from id: a2a-sin-code-{type}
|
| 47 |
+
if "a2a-sin-code-" in agent_id:
|
| 48 |
+
return agent_id.split("a2a-sin-code-")[-1]
|
| 49 |
+
except Exception:
|
| 50 |
+
pass
|
| 51 |
+
return "backend" # default fallback
|
| 52 |
+
|
| 53 |
+
def get_status(self) -> str:
|
| 54 |
+
"""Return human-readable status."""
|
| 55 |
+
if self.current_task:
|
| 56 |
+
return f"working on {self.current_task[:20]}..."
|
| 57 |
+
return "idle"
|
| 58 |
+
|
| 59 |
+
def is_healthy(self) -> bool:
|
| 60 |
+
"""Check if agent is healthy."""
|
| 61 |
+
# Basic health: can we run opencode?
|
| 62 |
+
try:
|
| 63 |
+
result = subprocess.run(
|
| 64 |
+
["opencode", "--version"], capture_output=True, text=True, timeout=5
|
| 65 |
+
)
|
| 66 |
+
return result.returncode == 0
|
| 67 |
+
except Exception:
|
| 68 |
+
return False
|
| 69 |
+
|
| 70 |
+
def process_task(self, task_id: str, task_data: Dict[str, Any], tasks_store: Dict):
|
| 71 |
+
"""Process a single A2A task."""
|
| 72 |
+
try:
|
| 73 |
+
with tasks_store._lock if hasattr(tasks_store, "_lock") else None:
|
| 74 |
+
tasks_store[task_id]["status"] = "running"
|
| 75 |
+
tasks_store[task_id]["timestamp"] = str(
|
| 76 |
+
subprocess.check_output(["date"]).decode().strip()
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
self.status = "processing"
|
| 80 |
+
self.current_task = task_data.get("description", "")[:50]
|
| 81 |
+
|
| 82 |
+
# Extract task parameters
|
| 83 |
+
description = task_data.get("description", "")
|
| 84 |
+
task_type = task_data.get("type", self.agent_type) # Use detected type as fallback
|
| 85 |
+
target_branch = task_data.get("target_branch")
|
| 86 |
+
issue_number = task_data.get("issue_number")
|
| 87 |
+
|
| 88 |
+
# Generate prompt using template
|
| 89 |
+
prompt = get_agent_prompt(task_type, description, issue_number)
|
| 90 |
+
|
| 91 |
+
# Call opencode CLI
|
| 92 |
+
result = self._call_opencode(prompt)
|
| 93 |
+
|
| 94 |
+
# Commit changes
|
| 95 |
+
commit_msg = (
|
| 96 |
+
f"Fix #{issue_number}: {description[:50]}"
|
| 97 |
+
if issue_number
|
| 98 |
+
else f"Auto-fix: {description[:50]}"
|
| 99 |
+
)
|
| 100 |
+
commit_sha = self._commit_changes(commit_msg)
|
| 101 |
+
|
| 102 |
+
# Push branch if requested
|
| 103 |
+
if target_branch:
|
| 104 |
+
gh = GitHubOps(REPO_DIR)
|
| 105 |
+
if gh.branch_exists_on_remote(target_branch):
|
| 106 |
+
logger.info("Branch already exists on remote", branch=target_branch)
|
| 107 |
+
else:
|
| 108 |
+
push_success = gh.push_branch(target_branch)
|
| 109 |
+
logger.info("Pushed branch", branch=target_branch, success=push_success)
|
| 110 |
+
|
| 111 |
+
# Update task result
|
| 112 |
+
with tasks_store._lock if hasattr(tasks_store, "_lock") else None:
|
| 113 |
+
tasks_store[task_id]["status"] = "completed"
|
| 114 |
+
tasks_store[task_id]["result"] = {
|
| 115 |
+
"commit_sha": commit_sha,
|
| 116 |
+
"branch": target_branch,
|
| 117 |
+
"summary": result.get("summary", ""),
|
| 118 |
+
"agent_type": self.agent_type,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
logger.task(task_id, "completed", agent_type=self.agent_type)
|
| 122 |
+
self.status = "idle"
|
| 123 |
+
self.current_task = None
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error("Task failed", task_id=task_id, error=str(e))
|
| 127 |
+
with tasks_store._lock if hasattr(tasks_store, "_lock") else None:
|
| 128 |
+
tasks_store[task_id]["status"] = "failed"
|
| 129 |
+
tasks_store[task_id]["error"] = str(e)
|
| 130 |
+
self.status = "idle"
|
| 131 |
+
self.current_task = None
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
with tasks_store._lock if hasattr(tasks_store, "_lock") else None:
|
| 135 |
+
tasks_store[task_id]["status"] = "failed"
|
| 136 |
+
tasks_store[task_id]["error"] = str(e)
|
| 137 |
+
self.status = "idle"
|
| 138 |
+
self.current_task = None
|
| 139 |
+
|
| 140 |
+
def _build_prompt(self, description: str, task_type: str, issue_number: Optional[int]) -> str:
|
| 141 |
+
"""Build a detailed prompt for opencode CLI."""
|
| 142 |
+
base_prompt = f"""You are an expert {task_type} developer working on the OpenSIN-Code project.
|
| 143 |
+
|
| 144 |
+
TASK: {description}
|
| 145 |
+
|
| 146 |
+
Issue Number: {issue_number if issue_number else "Not specified"}
|
| 147 |
+
|
| 148 |
+
CRITICAL RULES:
|
| 149 |
+
1. Use ONLY opencode CLI for any tool/API calls. Never use direct git commands or external APIs.
|
| 150 |
+
2. Make minimal, surgical changes - fix the exact issue without refactoring unrelated code.
|
| 151 |
+
3. Follow existing code style and patterns in the repository.
|
| 152 |
+
4. Write or update tests if they are missing for the fix.
|
| 153 |
+
5. Ensure all imports are correct and dependencies are in package.json requirements.
|
| 154 |
+
6. If the fix requires new dependencies, add them to the appropriate files.
|
| 155 |
+
7. Do NOT create new branches - work on the existing branch.
|
| 156 |
+
8. After making changes, EXPLICITLY run tests if a test suite exists.
|
| 157 |
+
9. Output format: First a brief summary of what you changed, then the exact commands you ran (with --format json), then the results.
|
| 158 |
+
|
| 159 |
+
WORKFLOW:
|
| 160 |
+
1. First, explore the codebase to understand the structure.
|
| 161 |
+
2. Locate the files that need to be changed.
|
| 162 |
+
3. Apply the fix.
|
| 163 |
+
4. Run tests (if available).
|
| 164 |
+
5. Provide a summary of changes.
|
| 165 |
+
|
| 166 |
+
Begin by exploring the repository structure."""
|
| 167 |
+
|
| 168 |
+
# Add type-specific guidance
|
| 169 |
+
if task_type == "plugin":
|
| 170 |
+
base_prompt += "\n\nPLUGIN SPECIFIC: Ensure the plugin follows the OpenCode plugin architecture with proper manifest.json, entry points, and A2A integration."
|
| 171 |
+
elif task_type == "command":
|
| 172 |
+
base_prompt += "\n\nCOMMAND SPECIFIC: Implement CLI commands following the OpenCode command pattern with proper argument parsing and help text."
|
| 173 |
+
elif task_type == "tool":
|
| 174 |
+
base_prompt += "\n\nTOOL SPECIFIC: Create reusable tool functions with clear docstrings, type hints, and error handling."
|
| 175 |
+
elif task_type == "backend":
|
| 176 |
+
base_prompt += "\n\nBACKEND SPECIFIC: Focus on API endpoints, database models, business logic, and integrations."
|
| 177 |
+
elif task_type == "frontend":
|
| 178 |
+
base_prompt += "\n\nFRONTEND SPECIFIC: Implement UI components with proper accessibility, responsive design, and modern patterns."
|
| 179 |
+
elif task_type == "fullstack":
|
| 180 |
+
base_prompt += "\n\nFULLSTACK SPECIFIC: Ensure both frontend and backend are consistent; update API contracts and UI together."
|
| 181 |
+
|
| 182 |
+
return base_prompt
|
| 183 |
+
|
| 184 |
+
def _call_opencode(self, prompt: str) -> Dict[str, Any]:
|
| 185 |
+
"""Call opencode CLI with JSON-RPC streaming."""
|
| 186 |
+
try:
|
| 187 |
+
# Run opencode with format=json to get structured output
|
| 188 |
+
proc = subprocess.run(
|
| 189 |
+
[
|
| 190 |
+
"opencode",
|
| 191 |
+
"run",
|
| 192 |
+
prompt,
|
| 193 |
+
"--format",
|
| 194 |
+
"json",
|
| 195 |
+
"--fallback",
|
| 196 |
+
"opencode/minimax-m2.5-free",
|
| 197 |
+
],
|
| 198 |
+
capture_output=True,
|
| 199 |
+
text=True,
|
| 200 |
+
timeout=300, # 5 minute timeout
|
| 201 |
+
cwd=str(REPO_DIR),
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
if proc.returncode != 0:
|
| 205 |
+
raise RuntimeError(f"opencode failed: {proc.stderr}")
|
| 206 |
+
|
| 207 |
+
# Parse streaming JSON responses
|
| 208 |
+
result_parts = []
|
| 209 |
+
commit_sha = None
|
| 210 |
+
files_changed = []
|
| 211 |
+
|
| 212 |
+
for line in proc.stdout.splitlines():
|
| 213 |
+
try:
|
| 214 |
+
event = json.loads(line)
|
| 215 |
+
if event.get("type") == "text":
|
| 216 |
+
content = event.get("part", {}).get("text", "")
|
| 217 |
+
result_parts.append(content)
|
| 218 |
+
elif event.get("type") == "tool_call":
|
| 219 |
+
# Track tool calls (e.g., shell commands)
|
| 220 |
+
tool_name = event.get("tool_name", "")
|
| 221 |
+
args = event.get("args", {})
|
| 222 |
+
if tool_name == "execute_shell_command":
|
| 223 |
+
cmd = args.get("cmd", "")
|
| 224 |
+
if "git commit" in cmd and commit_sha is None:
|
| 225 |
+
# Extract commit SHA if available
|
| 226 |
+
pass
|
| 227 |
+
except json.JSONDecodeError:
|
| 228 |
+
continue
|
| 229 |
+
|
| 230 |
+
summary = "\n".join(result_parts)
|
| 231 |
+
|
| 232 |
+
return {
|
| 233 |
+
"summary": summary,
|
| 234 |
+
"commit_sha": commit_sha,
|
| 235 |
+
"files_changed": files_changed,
|
| 236 |
+
"raw_output": proc.stdout,
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
except subprocess.TimeoutExpired:
|
| 240 |
+
raise RuntimeError("opencode call timed out after 5 minutes")
|
| 241 |
+
except Exception as e:
|
| 242 |
+
raise RuntimeError(f"opencode execution failed: {e}")
|
| 243 |
+
|
| 244 |
+
def _commit_changes(self, commit_msg: str) -> str:
|
| 245 |
+
"""Commit any pending changes to the repository."""
|
| 246 |
+
try:
|
| 247 |
+
# Check git status
|
| 248 |
+
subprocess.run(
|
| 249 |
+
["git", "status", "--porcelain"],
|
| 250 |
+
capture_output=True,
|
| 251 |
+
text=True,
|
| 252 |
+
cwd=str(REPO_DIR),
|
| 253 |
+
check=True,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
# Add all changes
|
| 257 |
+
subprocess.run(
|
| 258 |
+
["git", "add", "."], capture_output=True, text=True, cwd=str(REPO_DIR), check=True
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Commit
|
| 262 |
+
result = subprocess.run(
|
| 263 |
+
["git", "commit", "-m", commit_msg],
|
| 264 |
+
capture_output=True,
|
| 265 |
+
text=True,
|
| 266 |
+
cwd=str(REPO_DIR),
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
if result.returncode == 0:
|
| 270 |
+
# Extract commit SHA
|
| 271 |
+
sha = result.stdout.strip().split()[-1] if result.stdout else "unknown"
|
| 272 |
+
return sha
|
| 273 |
+
else:
|
| 274 |
+
# No changes to commit?
|
| 275 |
+
if "nothing to commit" in result.stderr.lower():
|
| 276 |
+
return None
|
| 277 |
+
raise RuntimeError(f"git commit failed: {result.stderr}")
|
| 278 |
+
|
| 279 |
+
except subprocess.CalledProcessError as e:
|
| 280 |
+
raise RuntimeError(f"git operation failed: {e}")
|
app/github_ops.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GitHub operations module for cloud coder agents.
|
| 3 |
+
Handles commits, branches, and PR creation using GitHub CLI.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import subprocess
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
REPO_DIR = Path("/workspace/OpenSIN-Code")
|
| 12 |
+
if not REPO_DIR.exists():
|
| 13 |
+
REPO_DIR = Path("/home/user/project/OpenSIN-Code")
|
| 14 |
+
if not REPO_DIR.exists():
|
| 15 |
+
REPO_DIR = Path.cwd().parent / "OpenSIN-Code"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class GitHubOps:
|
| 19 |
+
"""Handles all GitHub-related operations."""
|
| 20 |
+
|
| 21 |
+
def __init__(self, repo_dir: Optional[Path] = None):
|
| 22 |
+
self.repo_dir = repo_dir or REPO_DIR
|
| 23 |
+
|
| 24 |
+
def _run_git(self, args: list[str], capture: bool = True) -> subprocess.CompletedProcess:
|
| 25 |
+
"""Run a git command in the repository."""
|
| 26 |
+
cmd = ["git"] + args
|
| 27 |
+
return subprocess.run(
|
| 28 |
+
cmd, cwd=str(self.repo_dir), capture_output=capture, text=True, check=False
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
def _run_gh(self, args: list[str], capture: bool = True) -> subprocess.CompletedProcess:
|
| 32 |
+
"""Run a gh CLI command."""
|
| 33 |
+
cmd = ["gh"] + args
|
| 34 |
+
return subprocess.run(cmd, capture_output=capture, text=True, check=False)
|
| 35 |
+
|
| 36 |
+
def ensure_branch(self, branch_name: str, base: str = "main") -> bool:
|
| 37 |
+
"""Ensure branch exists, create from base if not."""
|
| 38 |
+
# Check if branch exists locally
|
| 39 |
+
result = self._run_git(["branch", "--list", branch_name])
|
| 40 |
+
if branch_name in result.stdout:
|
| 41 |
+
# Checkout existing branch
|
| 42 |
+
checkout = self._run_git(["checkout", branch_name])
|
| 43 |
+
return checkout.returncode == 0
|
| 44 |
+
|
| 45 |
+
# Create new branch from base
|
| 46 |
+
checkout = self._run_git(["checkout", base])
|
| 47 |
+
if checkout.returncode != 0:
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
pull = self._run_git(["pull", "origin", base])
|
| 51 |
+
if pull.returncode != 0:
|
| 52 |
+
return False
|
| 53 |
+
|
| 54 |
+
create = self._run_git(["checkout", "-b", branch_name])
|
| 55 |
+
return create.returncode == 0
|
| 56 |
+
|
| 57 |
+
def commit_changes(self, message: str) -> Optional[str]:
|
| 58 |
+
"""Stage and commit all changes."""
|
| 59 |
+
# Add all changes
|
| 60 |
+
add = self._run_git(["add", "."])
|
| 61 |
+
if add.returncode != 0:
|
| 62 |
+
raise RuntimeError(f"git add failed: {add.stderr}")
|
| 63 |
+
|
| 64 |
+
# Commit
|
| 65 |
+
commit = self._run_git(["commit", "-m", message])
|
| 66 |
+
if commit.returncode != 0:
|
| 67 |
+
if "nothing to commit" in commit.stderr.lower():
|
| 68 |
+
return None
|
| 69 |
+
raise RuntimeError(f"git commit failed: {commit.stderr}")
|
| 70 |
+
|
| 71 |
+
# Get commit SHA
|
| 72 |
+
sha = commit.stdout.strip().split()[-1] if commit.stdout else "unknown"
|
| 73 |
+
return sha
|
| 74 |
+
|
| 75 |
+
def push_branch(self, branch_name: str, force: bool = False) -> bool:
|
| 76 |
+
"""Push branch to origin."""
|
| 77 |
+
args = ["push", "origin", branch_name]
|
| 78 |
+
if force:
|
| 79 |
+
args.append("--force")
|
| 80 |
+
push = self._run_git(args)
|
| 81 |
+
return push.returncode == 0
|
| 82 |
+
|
| 83 |
+
def create_pr(
|
| 84 |
+
self, title: str, body: str, base: str = "main", head: str = None
|
| 85 |
+
) -> Optional[str]:
|
| 86 |
+
"""Create a pull request using gh CLI."""
|
| 87 |
+
if not head:
|
| 88 |
+
# Get current branch
|
| 89 |
+
branch_result = self._run_git(["branch", "--show-current"])
|
| 90 |
+
head = branch_result.stdout.strip()
|
| 91 |
+
|
| 92 |
+
args = ["pr", "create", "--title", title, "--body", body, "--base", base, "--head", head]
|
| 93 |
+
|
| 94 |
+
result = self._run_gh(args)
|
| 95 |
+
if result.returncode == 0:
|
| 96 |
+
# Extract PR URL from output
|
| 97 |
+
lines = result.stdout.strip().split("\n")
|
| 98 |
+
for line in lines:
|
| 99 |
+
if "https://github.com" in line:
|
| 100 |
+
return line.strip()
|
| 101 |
+
return result.stdout
|
| 102 |
+
else:
|
| 103 |
+
if "already exists" in result.stderr.lower():
|
| 104 |
+
# PR already exists, return existing URL
|
| 105 |
+
list_result = self._run_gh(
|
| 106 |
+
["pr", "list", "--head", head, "--json", "url", "--jq", ".[0].url"]
|
| 107 |
+
)
|
| 108 |
+
if list_result.returncode == 0 and list_result.stdout.strip():
|
| 109 |
+
return list_result.stdout.strip()
|
| 110 |
+
return None
|
| 111 |
+
|
| 112 |
+
def get_issue(self, issue_number: int) -> Optional[dict]:
|
| 113 |
+
"""Fetch issue details using gh CLI."""
|
| 114 |
+
result = self._run_gh(
|
| 115 |
+
["issue", "view", str(issue_number), "--json", "number,title,body,labels,state"]
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
if result.returncode == 0:
|
| 119 |
+
try:
|
| 120 |
+
return json.loads(result.stdout)
|
| 121 |
+
except json.JSONDecodeError:
|
| 122 |
+
return None
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
def get_current_branch(self) -> str:
|
| 126 |
+
"""Get current branch name."""
|
| 127 |
+
result = self._run_git(["branch", "--show-current"])
|
| 128 |
+
return result.stdout.strip() if result.returncode == 0 else "main"
|
| 129 |
+
|
| 130 |
+
def branch_exists_on_remote(self, branch_name: str) -> bool:
|
| 131 |
+
"""Check if branch exists on remote."""
|
| 132 |
+
result = self._run_git(["ls-remote", "--heads", "origin", branch_name])
|
| 133 |
+
return result.returncode == 0 and bool(result.stdout.strip())
|
| 134 |
+
|
| 135 |
+
def rebase_main(self) -> bool:
|
| 136 |
+
"""Rebase current branch on main."""
|
| 137 |
+
current = self.get_current_branch()
|
| 138 |
+
if current == "main":
|
| 139 |
+
return True
|
| 140 |
+
|
| 141 |
+
# Fetch latest
|
| 142 |
+
fetch = self._run_git(["fetch", "origin", "main"])
|
| 143 |
+
if fetch.returncode != 0:
|
| 144 |
+
return False
|
| 145 |
+
|
| 146 |
+
# Rebase
|
| 147 |
+
rebase = self._run_git(["rebase", "origin/main"])
|
| 148 |
+
return rebase.returncode == 0
|
app/prompt_templates.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt templates for different code agent types.
|
| 3 |
+
Each agent has a specialized system prompt and task guidance.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# Base system prompt for all cloud coders
|
| 7 |
+
BASE_SYSTEM = """You are an expert cloud coder working on the OpenSIN-Code project. You receive tasks via A2A protocol and must execute them using the opencode CLI exclusively.
|
| 8 |
+
|
| 9 |
+
KEY PRINCIPLES:
|
| 10 |
+
- NEVER use direct git commands - always use opencode CLI
|
| 11 |
+
- Make minimal, surgical changes
|
| 12 |
+
- Follow existing patterns and style
|
| 13 |
+
- Update or create tests as needed
|
| 14 |
+
- Validate your changes before concluding
|
| 15 |
+
|
| 16 |
+
FORMAT YOUR RESPONSES:
|
| 17 |
+
1. Brief summary of planned changes
|
| 18 |
+
2. Exact opencode CLI commands (with --format json)
|
| 19 |
+
3. Results from each command
|
| 20 |
+
4. Next steps
|
| 21 |
+
5. Final verification
|
| 22 |
+
|
| 23 |
+
Begin by exploring the codebase structure."""
|
| 24 |
+
|
| 25 |
+
# Type-specific enhancements
|
| 26 |
+
TYPE_PROMPTS = {
|
| 27 |
+
"plugin": """
|
| 28 |
+
PLUGIN DEVELOPMENT:
|
| 29 |
+
- Follow OpenCode plugin architecture
|
| 30 |
+
- Include manifest.json with proper metadata
|
| 31 |
+
- Implement A2A integration if needed
|
| 32 |
+
- Provide MCP server entry point
|
| 33 |
+
- Add comprehensive tests
|
| 34 |
+
- Update documentation in AGENTS.md
|
| 35 |
+
""",
|
| 36 |
+
"command": """
|
| 37 |
+
CLI COMMAND DEVELOPMENT:
|
| 38 |
+
- Follow OpenCode command pattern
|
| 39 |
+
- Use argparse or similar for argument parsing
|
| 40 |
+
- Provide --help text
|
| 41 |
+
- Handle errors gracefully
|
| 42 |
+
- Return appropriate exit codes
|
| 43 |
+
- Add shell completion scripts if needed
|
| 44 |
+
""",
|
| 45 |
+
"tool": """
|
| 46 |
+
REUSABLE TOOL DEVELOPMENT:
|
| 47 |
+
- Write pure functions when possible
|
| 48 |
+
- Include comprehensive docstrings
|
| 49 |
+
- Add type hints
|
| 50 |
+
- Implement error handling
|
| 51 |
+
- Create unit tests
|
| 52 |
+
- Consider performance implications
|
| 53 |
+
""",
|
| 54 |
+
"backend": """
|
| 55 |
+
BACKEND DEVELOPMENT:
|
| 56 |
+
- Design clean API endpoints
|
| 57 |
+
- Use proper HTTP methods and status codes
|
| 58 |
+
- Implement authentication/authorization checks
|
| 59 |
+
- Validate input data
|
| 60 |
+
- Add database migrations if schema changes
|
| 61 |
+
- Write integration tests
|
| 62 |
+
- Consider scalability
|
| 63 |
+
""",
|
| 64 |
+
"frontend": """
|
| 65 |
+
FRONTEND DEVELOPMENT:
|
| 66 |
+
- Use existing component library (shadcn/ui)
|
| 67 |
+
- Follow responsive design principles
|
| 68 |
+
- Implement accessibility (ARIA, keyboard nav)
|
| 69 |
+
- Optimize for performance
|
| 70 |
+
- Add proper error states
|
| 71 |
+
- Write component tests
|
| 72 |
+
- Use client-side validation
|
| 73 |
+
""",
|
| 74 |
+
"fullstack": """
|
| 75 |
+
FULLSTACK DEVELOPMENT:
|
| 76 |
+
- Ensure API contracts match frontend expectations
|
| 77 |
+
- Update both sides together
|
| 78 |
+
- Use TypeScript for type safety
|
| 79 |
+
- Implement proper error handling on both layers
|
| 80 |
+
- Consider caching strategies
|
| 81 |
+
- Test complete user flows
|
| 82 |
+
""",
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_agent_prompt(agent_type: str, issue_description: str, issue_number: int = None) -> str:
|
| 87 |
+
"""Build complete prompt for a specific agent type."""
|
| 88 |
+
prompt_parts = [
|
| 89 |
+
BASE_SYSTEM,
|
| 90 |
+
TYPE_PROMPTS.get(agent_type, "\nGeneral coding task."),
|
| 91 |
+
f"\nTASK:\n{issue_description}",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
if issue_number:
|
| 95 |
+
prompt_parts.append(f"\nISSUE REFERENCE: #{issue_number}")
|
| 96 |
+
prompt_parts.append("\nWhen committing, include the issue number in the commit message.")
|
| 97 |
+
|
| 98 |
+
prompt_parts.append("""
|
| 99 |
+
IMPORTANT EXECUTION NOTES:
|
| 100 |
+
1. You have access to the full repository at REPO_DIR
|
| 101 |
+
2. Use `opencode run` for any coding work, not direct code editing
|
| 102 |
+
3. The opencode CLI will handle file creation/modification
|
| 103 |
+
4. After opencode completes, check git status
|
| 104 |
+
5. Commit with meaningful message referencing the issue
|
| 105 |
+
6. Do NOT create pull requests automatically - that's a separate step
|
| 106 |
+
|
| 107 |
+
Begin by exploring the repository to understand the codebase structure.
|
| 108 |
+
""")
|
| 109 |
+
|
| 110 |
+
return "\n".join(prompt_parts)
|
app/utils/logger.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Simple structured logger."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Logger:
|
| 10 |
+
"""Structured logging for cloud coder agents."""
|
| 11 |
+
|
| 12 |
+
def __init__(self, log_file: Path = None):
|
| 13 |
+
self.log_file = log_file or Path("/tmp/cloud-coder.log")
|
| 14 |
+
self.ensure_log_dir()
|
| 15 |
+
|
| 16 |
+
def ensure_log_dir(self):
|
| 17 |
+
"""Ensure log directory exists."""
|
| 18 |
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
def log(self, level: str, message: str, **kwargs):
|
| 21 |
+
"""Write a structured log entry."""
|
| 22 |
+
entry = {
|
| 23 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 24 |
+
"level": level,
|
| 25 |
+
"message": message,
|
| 26 |
+
**kwargs,
|
| 27 |
+
}
|
| 28 |
+
line = json.dumps(entry)
|
| 29 |
+
print(line, file=sys.stderr)
|
| 30 |
+
with open(self.log_file, "a") as f:
|
| 31 |
+
f.write(line + "\n")
|
| 32 |
+
|
| 33 |
+
def info(self, message: str, **kwargs):
|
| 34 |
+
self.log("INFO", message, **kwargs)
|
| 35 |
+
|
| 36 |
+
def warning(self, message: str, **kwargs):
|
| 37 |
+
self.log("WARNING", message, **kwargs)
|
| 38 |
+
|
| 39 |
+
def error(self, message: str, **kwargs):
|
| 40 |
+
self.log("ERROR", message, **kwargs)
|
| 41 |
+
|
| 42 |
+
def task(self, task_id: str, status: str, **kwargs):
|
| 43 |
+
self.log(
|
| 44 |
+
"TASK", f"Task {task_id} status: {status}", task_id=task_id, status=status, **kwargs
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Global logger instance
|
| 49 |
+
logger = Logger()
|
app/utils/validators.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Validation utilities for generated code."""
|
| 2 |
+
|
| 3 |
+
import ast
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def validate_python_syntax(file_path: Path) -> tuple[bool, Optional[str]]:
|
| 10 |
+
"""Check if a Python file has valid syntax."""
|
| 11 |
+
try:
|
| 12 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 13 |
+
content = f.read()
|
| 14 |
+
ast.parse(content)
|
| 15 |
+
return True, None
|
| 16 |
+
except SyntaxError as e:
|
| 17 |
+
return False, f"Syntax error at line {e.lineno}: {e.msg}"
|
| 18 |
+
except Exception as e:
|
| 19 |
+
return False, str(e)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def validate_required_files_exist(repo_dir: Path, required_files: List[str]) -> List[str]:
|
| 23 |
+
"""Check that required files exist in the repository."""
|
| 24 |
+
missing = []
|
| 25 |
+
for file in required_files:
|
| 26 |
+
if not (repo_dir / file).exists():
|
| 27 |
+
missing.append(file)
|
| 28 |
+
return missing
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def validate_imports(file_path: Path) -> List[str]:
|
| 32 |
+
"""Check for likely missing imports (basic heuristic)."""
|
| 33 |
+
try:
|
| 34 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 35 |
+
content = f.read()
|
| 36 |
+
tree = ast.parse(content)
|
| 37 |
+
|
| 38 |
+
defined_names = set()
|
| 39 |
+
used_names = set()
|
| 40 |
+
|
| 41 |
+
for node in ast.walk(tree):
|
| 42 |
+
if isinstance(node, ast.FunctionDef):
|
| 43 |
+
defined_names.add(node.name)
|
| 44 |
+
elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
| 45 |
+
used_names.add(node.id)
|
| 46 |
+
elif isinstance(node, ast.Attribute):
|
| 47 |
+
# Capture attribute access like os.path
|
| 48 |
+
if isinstance(node.value, ast.Name):
|
| 49 |
+
used_names.add(f"{node.value.id}.{node.attr}")
|
| 50 |
+
|
| 51 |
+
undefined = used_names - defined_names - set(__builtins__.keys())
|
| 52 |
+
# Filter out likely imports from standard library and common packages
|
| 53 |
+
common_imports = {
|
| 54 |
+
"os",
|
| 55 |
+
"sys",
|
| 56 |
+
"json",
|
| 57 |
+
"logging",
|
| 58 |
+
"typing",
|
| 59 |
+
"Path",
|
| 60 |
+
"Optional",
|
| 61 |
+
"List",
|
| 62 |
+
"Dict",
|
| 63 |
+
"Any",
|
| 64 |
+
"Union",
|
| 65 |
+
"Callable",
|
| 66 |
+
"subprocess",
|
| 67 |
+
"requests",
|
| 68 |
+
"fastapi",
|
| 69 |
+
"gradio",
|
| 70 |
+
"uvicorn",
|
| 71 |
+
}
|
| 72 |
+
possibly_undefined = [n for n in undefined if n not in common_imports]
|
| 73 |
+
|
| 74 |
+
return possibly_undefined
|
| 75 |
+
except Exception:
|
| 76 |
+
return []
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def validate_file_not_empty(file_path: Path) -> bool:
|
| 80 |
+
"""Check that a file has non-empty content."""
|
| 81 |
+
return file_path.exists() and file_path.stat().st_size > 10
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def validate_has_main_block(file_path: Path) -> bool:
|
| 85 |
+
"""Check if Python file has a main execution block."""
|
| 86 |
+
try:
|
| 87 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 88 |
+
content = f.read()
|
| 89 |
+
tree = ast.parse(content)
|
| 90 |
+
for node in ast.walk(tree):
|
| 91 |
+
if isinstance(node, ast.If):
|
| 92 |
+
if (
|
| 93 |
+
isinstance(node.test, ast.Compare)
|
| 94 |
+
and any(isinstance(op, ast.Eq) for op in node.test.ops)
|
| 95 |
+
and any(
|
| 96 |
+
isinstance(comp, ast.Name) and comp.id == "__name__"
|
| 97 |
+
for comp in node.test.comparators
|
| 98 |
+
)
|
| 99 |
+
):
|
| 100 |
+
return True
|
| 101 |
+
return False
|
| 102 |
+
except Exception:
|
| 103 |
+
return False
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
uvicorn[standard]>=0.24.0
|
| 4 |
+
python-dotenv>=1.0.0
|
start.sh
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
# Start script for Hugging Face Space
|
| 5 |
+
# Mounts the OpenSIN-Code repository if available
|
| 6 |
+
|
| 7 |
+
echo "π Starting A2A Cloud Coder Agent..."
|
| 8 |
+
|
| 9 |
+
# Check for repo in common locations
|
| 10 |
+
REPO_PATHS=(
|
| 11 |
+
"/workspace/OpenSIN-Code"
|
| 12 |
+
"/home/user/project/OpenSIN-Code"
|
| 13 |
+
"/home/user/OpenSIN-Code"
|
| 14 |
+
"/project/OpenSIN-Code"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
for path in "${REPO_PATHS[@]}"; do
|
| 18 |
+
if [ -d "$path" ]; then
|
| 19 |
+
echo "π Found repository at $path"
|
| 20 |
+
REPO_DIR="$path"
|
| 21 |
+
break
|
| 22 |
+
fi
|
| 23 |
+
done
|
| 24 |
+
|
| 25 |
+
if [ -z "$REPO_DIR" ]; then
|
| 26 |
+
echo "β οΈ No repository found. Clone it manually if needed."
|
| 27 |
+
REPO_DIR="/workspace/OpenSIN-Code"
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
# Export for Python code
|
| 31 |
+
export REPO_DIR
|
| 32 |
+
|
| 33 |
+
# Create opencode config if not present (uses OCI fallback proxy)
|
| 34 |
+
mkdir -p ~/.config/opencode
|
| 35 |
+
if [ ! -f ~/.config/opencode/opencode.json ]; then
|
| 36 |
+
cat > ~/.config/opencode/opencode.json << 'EOF'
|
| 37 |
+
{
|
| 38 |
+
"model": "openai/gpt-5.4",
|
| 39 |
+
"provider": {
|
| 40 |
+
"openai": {
|
| 41 |
+
"options": {
|
| 42 |
+
"baseUrl": "http://92.5.60.87:4100/v1"
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
EOF
|
| 48 |
+
echo "β
Created opencode config (using OCI fallback proxy)"
|
| 49 |
+
fi
|
| 50 |
+
|
| 51 |
+
# Check opencode availability
|
| 52 |
+
if command -v opencode &> /dev/null; then
|
| 53 |
+
echo "β
opencode CLI found"
|
| 54 |
+
opencode --version
|
| 55 |
+
else
|
| 56 |
+
echo "β opencode CLI not found. Agent will fail to generate code."
|
| 57 |
+
fi
|
| 58 |
+
|
| 59 |
+
# Check GitHub token
|
| 60 |
+
if [ -z "$GITHUB_TOKEN" ]; then
|
| 61 |
+
echo "β οΈ GITHUB_TOKEN not set - commits will fail"
|
| 62 |
+
else
|
| 63 |
+
echo "β
GITHUB_TOKEN set"
|
| 64 |
+
fi
|
| 65 |
+
|
| 66 |
+
# Start the application
|
| 67 |
+
echo "π― Starting application on port ${PORT:-7860}"
|
| 68 |
+
exec python app.py
|