Spaces:
Sleeping
Sleeping
Deploy Space
Browse files- README.md +41 -22
- pyproject.toml +4 -2
- requirements.txt +5 -3
- src/ai_agentic_coder/.gradio/certificate.pem +31 -0
- src/ai_agentic_coder/config/agents.yaml +0 -5
- src/ai_agentic_coder/config/tasks.yaml +4 -5
- src/ai_agentic_coder/crew.py +17 -6
- src/ai_agentic_coder/crewai_wrapper.py +65 -6
- src/ai_agentic_coder/generated_app_runner.py +77 -0
- src/ai_agentic_coder/gradio_ui.py +8 -4
- src/ai_agentic_coder/main.py +3 -2
- src/ai_agentic_coder/model_client.py +141 -0
- src/ai_agentic_coder/preview_proxy.py +107 -0
- src/ai_agentic_coder/tools/python_code_run_tool.py +119 -29
- uv.lock +0 -0
README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
| 2 |
title: AI-Agentic-Coder
|
| 3 |
app_file: src/ai_agentic_coder/main.py
|
| 4 |
sdk: gradio
|
| 5 |
-
sdk_version:
|
| 6 |
---
|
| 7 |
|
| 8 |
# AI Agentic Coder
|
| 9 |
|
| 10 |
-
[](https://projects.kaushikpaul.co.in/ai-agentic-coder)
|
| 11 |
|
| 12 |
An AI-powered agentic coding assistant that:
|
| 13 |
- Turns your natural language requirements into a working Python module
|
|
|
|
| 17 |
Backed by a multi-agent CrewAI pipeline, the app coordinates “engineering lead”, “backend”, “frontend”, “QA”, and a “runner” agent to deliver end-to-end results.
|
| 18 |
|
| 19 |
## Live Demo
|
| 20 |
+
- Visit the hosted Space: https://projects.kaushikpaul.co.in/ai-agentic-coder
|
| 21 |
|
| 22 |
## Features
|
| 23 |
- **Idea → Running App in Minutes**
|
|
|
|
| 27 |
- **Production‑Friendly Reliability**
|
| 28 |
- Built‑in retry limits and execution timeouts for coding/testing agents, plus automatic cleanup of previous app processes to avoid port conflicts.
|
| 29 |
- **Model‑Flexible by Design**
|
| 30 |
+
- Switch between OpenRouter and OpenCode Go from `.env`. Models are configured with environment variables instead of hardcoded YAML values.
|
| 31 |
- **Modern Developer UX**
|
| 32 |
- Polished Gradio UI with non‑blocking background execution, streaming progress, one‑click example loader, and strict URL extraction/validation on completion.
|
| 33 |
- **Secure Artifact Delivery**
|
|
|
|
| 103 |
Create a `.env` file in the project root with the following variables (adjust as needed):
|
| 104 |
|
| 105 |
```ini
|
| 106 |
+
# ——— LLM provider ———
|
| 107 |
+
# Defaults to OpenCode Go when USE_OPENROUTER is false or omitted.
|
| 108 |
+
USE_OPENROUTER=false
|
| 109 |
+
OPENCODE_GO_API_KEY=your_opencode_go_key
|
| 110 |
+
OPENCODE_GO_MODEL=minimax-m2.7
|
| 111 |
+
# Optional: auto, openai, or anthropic. auto reads OpenCode Go model metadata and falls back safely.
|
| 112 |
+
OPENCODE_GO_API_STYLE=auto
|
| 113 |
+
|
| 114 |
+
# To use OpenRouter instead:
|
| 115 |
+
# USE_OPENROUTER=true
|
| 116 |
+
# OPENROUTER_API_KEY=your_openrouter_key
|
| 117 |
+
# OPENROUTER_MODEL=moonshotai/kimi-k2:free
|
| 118 |
+
|
| 119 |
+
# Optional shared tuning:
|
| 120 |
+
LLM_TEMPERATURE=0.2
|
| 121 |
+
LLM_TIMEOUT=300
|
| 122 |
|
| 123 |
# ——— Google Cloud Storage (used by PythonCodeRunTool) ———
|
| 124 |
GCP_PROJECT_ID=your_gcp_project_id
|
|
|
|
| 151 |
## Configuration
|
| 152 |
|
| 153 |
### LLMs and Agents
|
| 154 |
+
- File: `src/ai_agentic_coder/model_client.py`
|
| 155 |
+
- Provider selection is driven by `.env`: `USE_OPENROUTER=true` uses `OPENROUTER_MODEL`; otherwise the app uses OpenCode Go with `OPENCODE_GO_MODEL`.
|
| 156 |
+
- OpenCode Go defaults to `minimax-m2.7`. The app checks OpenCode Go model metadata to choose the correct API style, so Anthropic-style models use `/messages` and OpenAI-compatible models use `/chat/completions`. You can override detection with `OPENCODE_GO_API_STYLE=openai` or `OPENCODE_GO_API_STYLE=anthropic`.
|
| 157 |
|
| 158 |
### Tasks & Outputs
|
| 159 |
- File: `src/ai_agentic_coder/config/tasks.yaml`
|
|
|
|
| 163 |
- File: `src/ai_agentic_coder/gradio_ui.py`
|
| 164 |
- You provide: `Requirements`, `Module Name` (without .py), `Class Name`.
|
| 165 |
- The app displays a progress bar during execution and, on success, two URLs: a signed download URL and a live app URL.
|
| 166 |
+
- Generated demos are served through the main Gradio app at `/generated-app/`, which works on Hugging Face Spaces without relying on Gradio tunnel/share URLs.
|
| 167 |
|
| 168 |
### Execution Mode (Docker vs non‑Docker)
|
| 169 |
- Files: `src/ai_agentic_coder/crew.py`
|
|
|
|
| 188 |
3. Enter a module name (e.g., `accounts`) and class name (e.g., `Account`).
|
| 189 |
4. Click “Run AI Coder”.
|
| 190 |
5. Wait a few minutes while the pipeline runs. When done, you’ll see:
|
| 191 |
+
- A 30-minute signed Google Cloud Storage URL to download the generated artifacts as a zip
|
| 192 |
+
- A live URL of the generated Gradio demo app, proxied through the main app
|
| 193 |
|
| 194 |
## Outputs
|
| 195 |
Generated files are saved under `src/ai_agentic_coder/output/`:
|
|
|
|
| 197 |
- `{module_name}.py` — The generated backend module
|
| 198 |
- `app.py` — A minimal Gradio UI demonstrating the backend (launched with share=True)
|
| 199 |
- `test_{module_name}` — Unit test module for the backend
|
| 200 |
+
- `gradio_public_url.txt` — CrewAI task output containing the returned URLs
|
| 201 |
+
- `latest_run_result.json` — Exact tool result used by the UI, so signed URL query parameters are preserved
|
| 202 |
|
| 203 |
## Deployment
|
| 204 |
+
- The project is already hosted on Hugging Face Spaces: https://projects.kaushikpaul.co.in/ai-agentic-coder
|
| 205 |
+
- To deploy with the helper script:
|
| 206 |
+
- Set `HF_TOKEN` with write access to the Space.
|
| 207 |
+
- Optionally set `HF_SPACE_ID`; it defaults to `kaushikpaul/AI-Agentic-Coder`.
|
| 208 |
+
- Run `uv run python scripts/deploy_space.py`.
|
| 209 |
+
- To deploy your own Space manually:
|
| 210 |
- Set Space SDK to “Gradio” and point to `src/ai_agentic_coder/main.py` as the entry file.
|
| 211 |
- Add required secrets in the Space settings:
|
| 212 |
+
- `USE_OPENROUTER`
|
| 213 |
+
- `OPENCODE_GO_API_KEY`, `OPENCODE_GO_MODEL` or `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
|
| 214 |
- `GCP_PROJECT_ID`, `GCP_BUCKET_NAME`, `GCP_SERVICE_KEY` (base64-encoded service account JSON)
|
| 215 |
- Ensure the Python version matches (3.10–3.12) and install via `requirements.txt` or `pyproject.toml`.
|
| 216 |
|
| 217 |
## Troubleshooting
|
| 218 |
- **Missing or invalid API keys/credentials**
|
| 219 |
+
- Verify `.env` values. Ensure the selected LLM provider key and GCP service key are valid; confirm bucket exists and is accessible.
|
| 220 |
- **GCS upload errors**
|
| 221 |
+
- Confirm `GCP_SERVICE_KEY` contains a valid base64-encoded service account JSON with `storage.objects.create` and signing capability. The service account should also be able to access the target bucket.
|
| 222 |
- **Live URL not detected**
|
| 223 |
+
- The generated preview is available at `/generated-app/` for `AI_AGENTIC_CODER_PREVIEW_TTL_MINUTES` minutes, defaulting to 30. A new run stops the previous preview and reuses the same route.
|
| 224 |
+
- If your app is behind a custom domain or proxy, set `AI_AGENTIC_CODER_BASE_URL` so returned preview links use the exact public origin.
|
| 225 |
- **Virtualenv issues on Windows**
|
| 226 |
- Use `.venv\Scripts\activate` and ensure `python` points to the venv interpreter.
|
| 227 |
|
| 228 |
## Tech Stack
|
| 229 |
- **Python**: 3.10–3.12
|
| 230 |
+
- **Frameworks/Libraries**: CrewAI, Gradio 6, google-cloud-storage, python-dotenv, requests, httpx
|
| 231 |
- **Orchestration**: YAML-configured agents and tasks via CrewAI
|
| 232 |
- **UI**: Gradio Blocks with live progress and URL surfacing
|
| 233 |
|
pyproject.toml
CHANGED
|
@@ -5,15 +5,16 @@ description = "ai_agentic_coder using crewAI"
|
|
| 5 |
authors = [{ name = "Kaushik Paul", email = "kaushik.paul755@gmail.com" }]
|
| 6 |
requires-python = ">=3.10,<3.13"
|
| 7 |
dependencies = [
|
| 8 |
-
"crewai[tools]>=
|
| 9 |
"mailjet-rest>=1.5.1",
|
| 10 |
"autogen-agentchat>=0.4.9.2",
|
| 11 |
"autogen-ext[grpc,mcp,ollama,openai]>=0.4.9.2",
|
| 12 |
"bs4>=0.0.2",
|
| 13 |
-
"gradio>=
|
| 14 |
"httpx>=0.28.1",
|
| 15 |
"ipywidgets>=8.1.5",
|
| 16 |
"langsmith>=0.3.18",
|
|
|
|
| 17 |
"lxml>=5.3.1",
|
| 18 |
"mcp-server-fetch>=2025.1.17",
|
| 19 |
"mcp[cli]>=1.5.0",
|
|
@@ -30,6 +31,7 @@ dependencies = [
|
|
| 30 |
"setuptools>=78.1.0",
|
| 31 |
"smithery>=0.1.0",
|
| 32 |
"google-cloud-storage>=3.3.1",
|
|
|
|
| 33 |
]
|
| 34 |
|
| 35 |
[project.scripts]
|
|
|
|
| 5 |
authors = [{ name = "Kaushik Paul", email = "kaushik.paul755@gmail.com" }]
|
| 6 |
requires-python = ">=3.10,<3.13"
|
| 7 |
dependencies = [
|
| 8 |
+
"crewai[tools]>=1.14.4",
|
| 9 |
"mailjet-rest>=1.5.1",
|
| 10 |
"autogen-agentchat>=0.4.9.2",
|
| 11 |
"autogen-ext[grpc,mcp,ollama,openai]>=0.4.9.2",
|
| 12 |
"bs4>=0.0.2",
|
| 13 |
+
"gradio>=6.14.0",
|
| 14 |
"httpx>=0.28.1",
|
| 15 |
"ipywidgets>=8.1.5",
|
| 16 |
"langsmith>=0.3.18",
|
| 17 |
+
"litellm>=1.74.9",
|
| 18 |
"lxml>=5.3.1",
|
| 19 |
"mcp-server-fetch>=2025.1.17",
|
| 20 |
"mcp[cli]>=1.5.0",
|
|
|
|
| 31 |
"setuptools>=78.1.0",
|
| 32 |
"smithery>=0.1.0",
|
| 33 |
"google-cloud-storage>=3.3.1",
|
| 34 |
+
"huggingface-hub>=1.15.0",
|
| 35 |
]
|
| 36 |
|
| 37 |
[project.scripts]
|
requirements.txt
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
-
crewai[tools]>=
|
| 2 |
mailjet-rest>=1.5.1
|
| 3 |
autogen-agentchat>=0.4.9.2
|
| 4 |
autogen-ext[grpc,mcp,ollama,openai]>=0.4.9.2
|
| 5 |
bs4>=0.0.2
|
| 6 |
-
gradio>=
|
| 7 |
httpx>=0.28.1
|
| 8 |
ipywidgets>=8.1.5
|
| 9 |
langsmith>=0.3.18
|
|
|
|
| 10 |
lxml>=5.3.1
|
| 11 |
mcp-server-fetch>=2025.1.17
|
| 12 |
mcp[cli]>=1.5.0
|
|
@@ -22,4 +23,5 @@ semantic-kernel>=1.25.0
|
|
| 22 |
sendgrid>=6.11.0
|
| 23 |
setuptools>=78.1.0
|
| 24 |
smithery>=0.1.0
|
| 25 |
-
google-cloud-storage>=3.3.1
|
|
|
|
|
|
| 1 |
+
crewai[tools]>=1.14.4
|
| 2 |
mailjet-rest>=1.5.1
|
| 3 |
autogen-agentchat>=0.4.9.2
|
| 4 |
autogen-ext[grpc,mcp,ollama,openai]>=0.4.9.2
|
| 5 |
bs4>=0.0.2
|
| 6 |
+
gradio>=6.14.0
|
| 7 |
httpx>=0.28.1
|
| 8 |
ipywidgets>=8.1.5
|
| 9 |
langsmith>=0.3.18
|
| 10 |
+
litellm>=1.74.9
|
| 11 |
lxml>=5.3.1
|
| 12 |
mcp-server-fetch>=2025.1.17
|
| 13 |
mcp[cli]>=1.5.0
|
|
|
|
| 23 |
sendgrid>=6.11.0
|
| 24 |
setuptools>=78.1.0
|
| 25 |
smithery>=0.1.0
|
| 26 |
+
google-cloud-storage>=3.3.1
|
| 27 |
+
huggingface-hub>=1.15.0
|
src/ai_agentic_coder/.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
| 3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
| 4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
| 5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
| 6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
| 7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
| 8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
| 9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
| 10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
| 11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
| 12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
| 13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
| 14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
| 15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
| 16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
| 17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
| 18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
| 19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
| 20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
| 21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
| 22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
| 23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
| 24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
| 25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
| 26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
| 27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
| 28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
| 29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
| 30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
| 31 |
+
-----END CERTIFICATE-----
|
src/ai_agentic_coder/config/agents.yaml
CHANGED
|
@@ -9,7 +9,6 @@ engineering_lead:
|
|
| 9 |
The module should be named {module_name} and the class should be named {class_name}
|
| 10 |
backstory: >
|
| 11 |
You're a seasoned engineering lead with a knack for writing clear and concise designs.
|
| 12 |
-
llm: openrouter/openai/gpt-4.1-mini
|
| 13 |
|
| 14 |
|
| 15 |
backend_engineer:
|
|
@@ -24,7 +23,6 @@ backend_engineer:
|
|
| 24 |
You're a seasoned python engineer with a knack for writing clean, efficient code.
|
| 25 |
You follow the design instructions carefully.
|
| 26 |
You produce 1 python module named {module_name} that implements the design and achieves the requirements.
|
| 27 |
-
llm: openrouter/openai/gpt-4.1-mini
|
| 28 |
|
| 29 |
frontend_engineer:
|
| 30 |
role: >
|
|
@@ -35,7 +33,6 @@ frontend_engineer:
|
|
| 35 |
backstory: >
|
| 36 |
You're a seasoned python engineer highly skilled at writing simple Gradio UIs for a backend class.
|
| 37 |
You produce a simple gradio UI that demonstrates the given backend class; you write the gradio UI in a module app.py that is in the same directory as the backend module {module_name}.
|
| 38 |
-
llm: openrouter/openai/gpt-4.1-mini
|
| 39 |
|
| 40 |
test_engineer:
|
| 41 |
role: >
|
|
@@ -44,7 +41,6 @@ test_engineer:
|
|
| 44 |
Write unit tests for the given backend module {module_name} and create a test_{module_name} in the same directory as the backend module.
|
| 45 |
backstory: >
|
| 46 |
You're a seasoned QA engineer and software developer who writes great unit tests for python code.
|
| 47 |
-
llm: openrouter/openai/gpt-4.1-mini
|
| 48 |
|
| 49 |
python_code_runner:
|
| 50 |
role: >
|
|
@@ -53,4 +49,3 @@ python_code_runner:
|
|
| 53 |
Upload the zip to google cloud storage and run the given python code and return the output
|
| 54 |
backstory: >
|
| 55 |
You're a seasoned python engineer with a knack for uploading zip to google cloud storage and running python code.
|
| 56 |
-
llm: openrouter/openai/gpt-4.1-mini
|
|
|
|
| 9 |
The module should be named {module_name} and the class should be named {class_name}
|
| 10 |
backstory: >
|
| 11 |
You're a seasoned engineering lead with a knack for writing clear and concise designs.
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
backend_engineer:
|
|
|
|
| 23 |
You're a seasoned python engineer with a knack for writing clean, efficient code.
|
| 24 |
You follow the design instructions carefully.
|
| 25 |
You produce 1 python module named {module_name} that implements the design and achieves the requirements.
|
|
|
|
| 26 |
|
| 27 |
frontend_engineer:
|
| 28 |
role: >
|
|
|
|
| 33 |
backstory: >
|
| 34 |
You're a seasoned python engineer highly skilled at writing simple Gradio UIs for a backend class.
|
| 35 |
You produce a simple gradio UI that demonstrates the given backend class; you write the gradio UI in a module app.py that is in the same directory as the backend module {module_name}.
|
|
|
|
| 36 |
|
| 37 |
test_engineer:
|
| 38 |
role: >
|
|
|
|
| 41 |
Write unit tests for the given backend module {module_name} and create a test_{module_name} in the same directory as the backend module.
|
| 42 |
backstory: >
|
| 43 |
You're a seasoned QA engineer and software developer who writes great unit tests for python code.
|
|
|
|
| 44 |
|
| 45 |
python_code_runner:
|
| 46 |
role: >
|
|
|
|
| 49 |
Upload the zip to google cloud storage and run the given python code and return the output
|
| 50 |
backstory: >
|
| 51 |
You're a seasoned python engineer with a knack for uploading zip to google cloud storage and running python code.
|
|
|
src/ai_agentic_coder/config/tasks.yaml
CHANGED
|
@@ -51,12 +51,11 @@ test_task:
|
|
| 51 |
|
| 52 |
python_code_run_task:
|
| 53 |
description: >
|
| 54 |
-
Upload the zip to
|
| 55 |
expected_output: >
|
| 56 |
-
Signed
|
| 57 |
-
|
| 58 |
-
IMPORTANT: tool returns both the url, so just return the output returned by the tool, without any additional text or formatting.
|
| 59 |
agent: python_code_runner
|
| 60 |
context:
|
| 61 |
- code_task
|
| 62 |
-
output_file: src/ai_agentic_coder/output/gradio_public_url.txt
|
|
|
|
| 51 |
|
| 52 |
python_code_run_task:
|
| 53 |
description: >
|
| 54 |
+
Upload the zip to Google Cloud Storage and run the Python code that was generated.
|
| 55 |
expected_output: >
|
| 56 |
+
Signed URL of the zip uploaded to Google Cloud Storage and the live URL of the generated Gradio app.
|
| 57 |
+
IMPORTANT: The tool returns both URLs, so return the exact output returned by the tool without any additional text or formatting.
|
|
|
|
| 58 |
agent: python_code_runner
|
| 59 |
context:
|
| 60 |
- code_task
|
| 61 |
+
output_file: src/ai_agentic_coder/output/gradio_public_url.txt
|
src/ai_agentic_coder/crew.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
| 3 |
from crewai import Agent, Crew, Process, Task
|
| 4 |
from crewai.project import CrewBase, agent, crew, task
|
| 5 |
|
|
|
|
| 6 |
from .tools.python_code_run_tool import PythonCodeRunTool
|
| 7 |
|
| 8 |
def is_running_in_hf_space() -> bool:
|
|
@@ -20,17 +21,24 @@ class EngineeringTeam():
|
|
| 20 |
agents_config = 'config/agents.yaml'
|
| 21 |
tasks_config = 'config/tasks.yaml'
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
@agent
|
| 24 |
def engineering_lead(self) -> Agent:
|
| 25 |
return Agent(
|
| 26 |
-
config=self.
|
|
|
|
| 27 |
verbose=True,
|
| 28 |
)
|
| 29 |
|
| 30 |
@agent
|
| 31 |
def backend_engineer(self) -> Agent:
|
| 32 |
return Agent(
|
| 33 |
-
config=self.
|
|
|
|
| 34 |
verbose=True,
|
| 35 |
allow_code_execution=True,
|
| 36 |
code_execution_mode=run_in_docker,
|
|
@@ -41,14 +49,16 @@ class EngineeringTeam():
|
|
| 41 |
@agent
|
| 42 |
def frontend_engineer(self) -> Agent:
|
| 43 |
return Agent(
|
| 44 |
-
config=self.
|
|
|
|
| 45 |
verbose=True,
|
| 46 |
)
|
| 47 |
|
| 48 |
@agent
|
| 49 |
def test_engineer(self) -> Agent:
|
| 50 |
return Agent(
|
| 51 |
-
config=self.
|
|
|
|
| 52 |
verbose=True,
|
| 53 |
allow_code_execution=True,
|
| 54 |
code_execution_mode=run_in_docker,
|
|
@@ -59,7 +69,8 @@ class EngineeringTeam():
|
|
| 59 |
@agent
|
| 60 |
def python_code_runner(self) -> Agent:
|
| 61 |
return Agent(
|
| 62 |
-
config=self.
|
|
|
|
| 63 |
verbose=True,
|
| 64 |
tools=[PythonCodeRunTool()],
|
| 65 |
)
|
|
@@ -102,4 +113,4 @@ class EngineeringTeam():
|
|
| 102 |
tasks=self.tasks,
|
| 103 |
process=Process.sequential,
|
| 104 |
verbose=True,
|
| 105 |
-
)
|
|
|
|
| 3 |
from crewai import Agent, Crew, Process, Task
|
| 4 |
from crewai.project import CrewBase, agent, crew, task
|
| 5 |
|
| 6 |
+
from .model_client import create_llm
|
| 7 |
from .tools.python_code_run_tool import PythonCodeRunTool
|
| 8 |
|
| 9 |
def is_running_in_hf_space() -> bool:
|
|
|
|
| 21 |
agents_config = 'config/agents.yaml'
|
| 22 |
tasks_config = 'config/tasks.yaml'
|
| 23 |
|
| 24 |
+
def _agent_config(self, name: str) -> dict:
|
| 25 |
+
config = dict(self.agents_config[name])
|
| 26 |
+
config.pop("llm", None)
|
| 27 |
+
return config
|
| 28 |
+
|
| 29 |
@agent
|
| 30 |
def engineering_lead(self) -> Agent:
|
| 31 |
return Agent(
|
| 32 |
+
config=self._agent_config('engineering_lead'),
|
| 33 |
+
llm=create_llm(),
|
| 34 |
verbose=True,
|
| 35 |
)
|
| 36 |
|
| 37 |
@agent
|
| 38 |
def backend_engineer(self) -> Agent:
|
| 39 |
return Agent(
|
| 40 |
+
config=self._agent_config('backend_engineer'),
|
| 41 |
+
llm=create_llm(),
|
| 42 |
verbose=True,
|
| 43 |
allow_code_execution=True,
|
| 44 |
code_execution_mode=run_in_docker,
|
|
|
|
| 49 |
@agent
|
| 50 |
def frontend_engineer(self) -> Agent:
|
| 51 |
return Agent(
|
| 52 |
+
config=self._agent_config('frontend_engineer'),
|
| 53 |
+
llm=create_llm(),
|
| 54 |
verbose=True,
|
| 55 |
)
|
| 56 |
|
| 57 |
@agent
|
| 58 |
def test_engineer(self) -> Agent:
|
| 59 |
return Agent(
|
| 60 |
+
config=self._agent_config('test_engineer'),
|
| 61 |
+
llm=create_llm(),
|
| 62 |
verbose=True,
|
| 63 |
allow_code_execution=True,
|
| 64 |
code_execution_mode=run_in_docker,
|
|
|
|
| 69 |
@agent
|
| 70 |
def python_code_runner(self) -> Agent:
|
| 71 |
return Agent(
|
| 72 |
+
config=self._agent_config('python_code_runner'),
|
| 73 |
+
llm=create_llm(),
|
| 74 |
verbose=True,
|
| 75 |
tools=[PythonCodeRunTool()],
|
| 76 |
)
|
|
|
|
| 113 |
tasks=self.tasks,
|
| 114 |
process=Process.sequential,
|
| 115 |
verbose=True,
|
| 116 |
+
)
|
src/ai_agentic_coder/crewai_wrapper.py
CHANGED
|
@@ -4,19 +4,65 @@ import subprocess
|
|
| 4 |
import time
|
| 5 |
import threading
|
| 6 |
import re
|
|
|
|
|
|
|
|
|
|
| 7 |
import gradio as gr
|
| 8 |
|
| 9 |
from src.ai_agentic_coder.crew import EngineeringTeam
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
def run_crew(requirements, module_name, class_name, progress=gr.Progress()):
|
| 12 |
"""Run the crew with the given inputs and return ONLY the raw result text."""
|
| 13 |
try:
|
| 14 |
# Kill any running processes from previous runs
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
inputs = {
|
| 22 |
'requirements': requirements,
|
|
@@ -39,7 +85,10 @@ def run_crew(requirements, module_name, class_name, progress=gr.Progress()):
|
|
| 39 |
return f"❌ Error: {str(e)}"
|
| 40 |
|
| 41 |
# Generator function to manage state, progress, and output
|
| 42 |
-
def run_crew_wrapper(requirements, module_name, class_name):
|
|
|
|
|
|
|
|
|
|
| 43 |
progress = gr.Progress()
|
| 44 |
# Immediately disable the button, show Output as progress area, and hide URL boxes
|
| 45 |
yield (
|
|
@@ -102,6 +151,16 @@ def run_crew_wrapper(requirements, module_name, class_name):
|
|
| 102 |
return
|
| 103 |
|
| 104 |
raw_output = result_holder["output"] or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
# If the output looks like a failure (but no exception was thrown), treat it as an error
|
| 107 |
failure_like = bool(re.search(r"\b(failed|failure|error|exception|traceback)\b", raw_output, re.IGNORECASE))
|
|
|
|
| 4 |
import time
|
| 5 |
import threading
|
| 6 |
import re
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
import gradio as gr
|
| 11 |
|
| 12 |
from src.ai_agentic_coder.crew import EngineeringTeam
|
| 13 |
+
from src.ai_agentic_coder.tools.python_code_run_tool import RUN_RESULT_FILE
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
OUTPUT_DIR = Path(__file__).resolve().parent / "output"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _run_result_path() -> Path:
|
| 20 |
+
return OUTPUT_DIR / RUN_RESULT_FILE
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _clear_run_result() -> None:
|
| 24 |
+
try:
|
| 25 |
+
_run_result_path().unlink()
|
| 26 |
+
except FileNotFoundError:
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _load_run_result() -> dict[str, str]:
|
| 31 |
+
try:
|
| 32 |
+
data = json.loads(_run_result_path().read_text(encoding="utf-8"))
|
| 33 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
| 34 |
+
return {}
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
"download_url": str(data.get("download_url", "")),
|
| 38 |
+
"live_url": str(data.get("live_url", "")),
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _set_base_url_from_request(request: gr.Request | None) -> None:
|
| 43 |
+
if request is None:
|
| 44 |
+
return
|
| 45 |
+
|
| 46 |
+
host = request.headers.get("x-forwarded-host") or request.headers.get("host")
|
| 47 |
+
if not host:
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
proto = request.headers.get("x-forwarded-proto") or request.url.scheme or "http"
|
| 51 |
+
os.environ["AI_AGENTIC_CODER_BASE_URL"] = f"{proto}://{host}"
|
| 52 |
|
| 53 |
def run_crew(requirements, module_name, class_name, progress=gr.Progress()):
|
| 54 |
"""Run the crew with the given inputs and return ONLY the raw result text."""
|
| 55 |
try:
|
| 56 |
# Kill any running processes from previous runs
|
| 57 |
+
for pattern in (
|
| 58 |
+
"python.*src.ai_agentic_coder.output.app",
|
| 59 |
+
"python.*ai_agentic_coder.generated_app_runner",
|
| 60 |
+
):
|
| 61 |
+
subprocess.run(
|
| 62 |
+
["pkill", "-f", pattern],
|
| 63 |
+
stderr=subprocess.DEVNULL,
|
| 64 |
+
stdout=subprocess.DEVNULL
|
| 65 |
+
)
|
| 66 |
|
| 67 |
inputs = {
|
| 68 |
'requirements': requirements,
|
|
|
|
| 85 |
return f"❌ Error: {str(e)}"
|
| 86 |
|
| 87 |
# Generator function to manage state, progress, and output
|
| 88 |
+
def run_crew_wrapper(requirements, module_name, class_name, request: gr.Request | None = None):
|
| 89 |
+
_clear_run_result()
|
| 90 |
+
_set_base_url_from_request(request)
|
| 91 |
+
|
| 92 |
progress = gr.Progress()
|
| 93 |
# Immediately disable the button, show Output as progress area, and hide URL boxes
|
| 94 |
yield (
|
|
|
|
| 151 |
return
|
| 152 |
|
| 153 |
raw_output = result_holder["output"] or ""
|
| 154 |
+
run_result = _load_run_result()
|
| 155 |
+
|
| 156 |
+
if run_result.get("download_url") and run_result.get("live_url"):
|
| 157 |
+
yield (
|
| 158 |
+
gr.update(value="", visible=False, label="Output"),
|
| 159 |
+
gr.update(value=run_result["download_url"], visible=True),
|
| 160 |
+
gr.update(value=run_result["live_url"], visible=True),
|
| 161 |
+
gr.update(interactive=True, value="Run AI Coder")
|
| 162 |
+
)
|
| 163 |
+
return
|
| 164 |
|
| 165 |
# If the output looks like a failure (but no exception was thrown), treat it as an error
|
| 166 |
failure_like = bool(re.search(r"\b(failed|failure|error|exception|traceback)\b", raw_output, re.IGNORECASE))
|
src/ai_agentic_coder/generated_app_runner.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run generated Gradio apps with controlled launch settings."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import runpy
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
_LAUNCH_CALLED = False
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _preview_port() -> int:
|
| 18 |
+
return int(os.getenv("GENERATED_GRADIO_PORT", "7861"))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _root_path() -> str:
|
| 22 |
+
root_path = os.getenv("GENERATED_GRADIO_ROOT_PATH", "/generated-app").strip()
|
| 23 |
+
if not root_path.startswith("/"):
|
| 24 |
+
root_path = f"/{root_path}"
|
| 25 |
+
return root_path.rstrip("/") or "/generated-app"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _patch_launch(cls: type[Any]) -> None:
|
| 29 |
+
original_launch = cls.launch
|
| 30 |
+
if getattr(original_launch, "_ai_agentic_coder_controlled", False):
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
def controlled_launch(self: Any, *args: Any, **kwargs: Any) -> Any:
|
| 34 |
+
global _LAUNCH_CALLED
|
| 35 |
+
_LAUNCH_CALLED = True
|
| 36 |
+
kwargs["server_name"] = "127.0.0.1"
|
| 37 |
+
kwargs["server_port"] = _preview_port()
|
| 38 |
+
kwargs["share"] = False
|
| 39 |
+
kwargs["root_path"] = _root_path()
|
| 40 |
+
kwargs["prevent_thread_lock"] = False
|
| 41 |
+
return original_launch(self, *args, **kwargs)
|
| 42 |
+
|
| 43 |
+
controlled_launch._ai_agentic_coder_controlled = True # type: ignore[attr-defined]
|
| 44 |
+
cls.launch = controlled_launch # type: ignore[method-assign]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def main() -> None:
|
| 48 |
+
app_path = Path(os.environ["GENERATED_GRADIO_APP_PATH"]).resolve()
|
| 49 |
+
output_dir = app_path.parent
|
| 50 |
+
|
| 51 |
+
sys.path.insert(0, str(output_dir))
|
| 52 |
+
os.chdir(output_dir)
|
| 53 |
+
|
| 54 |
+
_patch_launch(gr.Blocks)
|
| 55 |
+
_patch_launch(gr.Interface)
|
| 56 |
+
|
| 57 |
+
globals_after_run = runpy.run_path(str(app_path), run_name="__main__")
|
| 58 |
+
|
| 59 |
+
if _LAUNCH_CALLED:
|
| 60 |
+
return
|
| 61 |
+
|
| 62 |
+
for value in globals_after_run.values():
|
| 63 |
+
if isinstance(value, gr.Blocks):
|
| 64 |
+
value.launch(
|
| 65 |
+
server_name="127.0.0.1",
|
| 66 |
+
server_port=_preview_port(),
|
| 67 |
+
share=False,
|
| 68 |
+
root_path=_root_path(),
|
| 69 |
+
prevent_thread_lock=False,
|
| 70 |
+
)
|
| 71 |
+
return
|
| 72 |
+
|
| 73 |
+
raise RuntimeError("Generated app did not define or launch a Gradio Blocks/Interface app.")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
main()
|
src/ai_agentic_coder/gradio_ui.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
import gradio as gr
|
| 3 |
|
| 4 |
from src.ai_agentic_coder.crewai_wrapper import run_crew_wrapper
|
|
|
|
| 5 |
|
| 6 |
# Example configuration
|
| 7 |
EXAMPLE_CONFIG = {
|
|
@@ -24,7 +25,7 @@ EXAMPLE_CONFIG = {
|
|
| 24 |
|
| 25 |
def create_interface():
|
| 26 |
"""Create and return the Gradio interface."""
|
| 27 |
-
with gr.Blocks(title="AI Agentic Coder"
|
| 28 |
gr.HTML("""
|
| 29 |
<div class="header">
|
| 30 |
<div class="brand">AI Agentic Coder</div>
|
|
@@ -90,7 +91,7 @@ def create_interface():
|
|
| 90 |
label="Output",
|
| 91 |
interactive=False,
|
| 92 |
lines=20,
|
| 93 |
-
|
| 94 |
elem_classes=["card"],
|
| 95 |
visible=False,
|
| 96 |
elem_id="output-box"
|
|
@@ -102,14 +103,15 @@ def create_interface():
|
|
| 102 |
label="Download URL",
|
| 103 |
interactive=False,
|
| 104 |
visible=False,
|
| 105 |
-
|
| 106 |
elem_classes=["card"]
|
| 107 |
)
|
|
|
|
| 108 |
live_url_box = gr.Textbox(
|
| 109 |
label="Live App URL",
|
| 110 |
interactive=False,
|
| 111 |
visible=False,
|
| 112 |
-
|
| 113 |
elem_classes=["card"]
|
| 114 |
)
|
| 115 |
|
|
@@ -174,4 +176,6 @@ def create_interface():
|
|
| 174 |
# Ensure progress overlay renders reliably
|
| 175 |
demo.queue()
|
| 176 |
|
|
|
|
|
|
|
| 177 |
return demo
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
|
| 4 |
from src.ai_agentic_coder.crewai_wrapper import run_crew_wrapper
|
| 5 |
+
from src.ai_agentic_coder.preview_proxy import register_preview_proxy
|
| 6 |
|
| 7 |
# Example configuration
|
| 8 |
EXAMPLE_CONFIG = {
|
|
|
|
| 25 |
|
| 26 |
def create_interface():
|
| 27 |
"""Create and return the Gradio interface."""
|
| 28 |
+
with gr.Blocks(title="AI Agentic Coder") as demo:
|
| 29 |
gr.HTML("""
|
| 30 |
<div class="header">
|
| 31 |
<div class="brand">AI Agentic Coder</div>
|
|
|
|
| 91 |
label="Output",
|
| 92 |
interactive=False,
|
| 93 |
lines=20,
|
| 94 |
+
buttons=["copy"],
|
| 95 |
elem_classes=["card"],
|
| 96 |
visible=False,
|
| 97 |
elem_id="output-box"
|
|
|
|
| 103 |
label="Download URL",
|
| 104 |
interactive=False,
|
| 105 |
visible=False,
|
| 106 |
+
buttons=["copy"],
|
| 107 |
elem_classes=["card"]
|
| 108 |
)
|
| 109 |
+
with gr.Row():
|
| 110 |
live_url_box = gr.Textbox(
|
| 111 |
label="Live App URL",
|
| 112 |
interactive=False,
|
| 113 |
visible=False,
|
| 114 |
+
buttons=["copy"],
|
| 115 |
elem_classes=["card"]
|
| 116 |
)
|
| 117 |
|
|
|
|
| 176 |
# Ensure progress overlay renders reliably
|
| 177 |
demo.queue()
|
| 178 |
|
| 179 |
+
register_preview_proxy(demo.app)
|
| 180 |
+
|
| 181 |
return demo
|
src/ai_agentic_coder/main.py
CHANGED
|
@@ -2,12 +2,13 @@
|
|
| 2 |
import warnings
|
| 3 |
import os
|
| 4 |
from dotenv import load_dotenv
|
|
|
|
| 5 |
|
| 6 |
from crewai.agent import Agent as _CrewaiAgent
|
| 7 |
from src.ai_agentic_coder.gradio_ui import create_interface
|
| 8 |
|
| 9 |
# Load environment variables
|
| 10 |
-
load_dotenv(
|
| 11 |
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
|
| 12 |
|
| 13 |
try:
|
|
@@ -24,4 +25,4 @@ except ImportError:
|
|
| 24 |
|
| 25 |
if __name__ == "__main__":
|
| 26 |
ai_agentic_coder = create_interface()
|
| 27 |
-
ai_agentic_coder.launch()
|
|
|
|
| 2 |
import warnings
|
| 3 |
import os
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
+
import gradio as gr
|
| 6 |
|
| 7 |
from crewai.agent import Agent as _CrewaiAgent
|
| 8 |
from src.ai_agentic_coder.gradio_ui import create_interface
|
| 9 |
|
| 10 |
# Load environment variables
|
| 11 |
+
load_dotenv()
|
| 12 |
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
|
| 13 |
|
| 14 |
try:
|
|
|
|
| 25 |
|
| 26 |
if __name__ == "__main__":
|
| 27 |
ai_agentic_coder = create_interface()
|
| 28 |
+
ai_agentic_coder.launch(theme=gr.themes.Soft())
|
src/ai_agentic_coder/model_client.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
from crewai import LLM
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
| 12 |
+
OPENCODE_GO_OPENAI_BASE_URL = "https://opencode.ai/zen/go/v1"
|
| 13 |
+
OPENCODE_GO_ANTHROPIC_BASE_URL = "https://opencode.ai/zen/go"
|
| 14 |
+
OPENCODE_GO_MODELS_URL = "https://opencode.ai/zen/go/v1/models"
|
| 15 |
+
|
| 16 |
+
DEFAULT_OPENROUTER_MODEL = "moonshotai/kimi-k2:free"
|
| 17 |
+
DEFAULT_OPENCODE_GO_MODEL = "deepseek-v4-flash"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _env_bool(name: str, default: bool = False) -> bool:
|
| 21 |
+
value = os.getenv(name)
|
| 22 |
+
if value is None:
|
| 23 |
+
return default
|
| 24 |
+
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _env_required(name: str) -> str:
|
| 28 |
+
value = os.getenv(name)
|
| 29 |
+
if not value:
|
| 30 |
+
raise RuntimeError(f"Missing required environment variable: {name}")
|
| 31 |
+
return value
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _opencode_model_id(model: str) -> str:
|
| 35 |
+
return model.removeprefix("opencode-go/").strip()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _openrouter_model_id(model: str) -> str:
|
| 39 |
+
model = model.strip()
|
| 40 |
+
if model.startswith("openrouter/"):
|
| 41 |
+
return model
|
| 42 |
+
return f"openrouter/{model}"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _strings_from(value: Any) -> list[str]:
|
| 46 |
+
if isinstance(value, str):
|
| 47 |
+
return [value.lower()]
|
| 48 |
+
if isinstance(value, dict):
|
| 49 |
+
strings: list[str] = []
|
| 50 |
+
for item in value.values():
|
| 51 |
+
strings.extend(_strings_from(item))
|
| 52 |
+
return strings
|
| 53 |
+
if isinstance(value, list):
|
| 54 |
+
strings = []
|
| 55 |
+
for item in value:
|
| 56 |
+
strings.extend(_strings_from(item))
|
| 57 |
+
return strings
|
| 58 |
+
return []
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@lru_cache(maxsize=32)
|
| 62 |
+
def _opencode_go_api_style(model: str, api_key: str) -> str:
|
| 63 |
+
configured = os.getenv("OPENCODE_GO_API_STYLE", "auto").strip().lower()
|
| 64 |
+
if configured in {"openai", "anthropic"}:
|
| 65 |
+
return configured
|
| 66 |
+
if configured != "auto":
|
| 67 |
+
raise RuntimeError("OPENCODE_GO_API_STYLE must be auto, openai, or anthropic")
|
| 68 |
+
|
| 69 |
+
model_id = _opencode_model_id(model)
|
| 70 |
+
try:
|
| 71 |
+
response = requests.get(
|
| 72 |
+
OPENCODE_GO_MODELS_URL,
|
| 73 |
+
headers={"Authorization": f"Bearer {api_key}"},
|
| 74 |
+
timeout=5,
|
| 75 |
+
)
|
| 76 |
+
response.raise_for_status()
|
| 77 |
+
payload = response.json()
|
| 78 |
+
models = payload.get("data", payload) if isinstance(payload, dict) else payload
|
| 79 |
+
if isinstance(models, list):
|
| 80 |
+
for item in models:
|
| 81 |
+
if not isinstance(item, dict):
|
| 82 |
+
continue
|
| 83 |
+
ids = {
|
| 84 |
+
str(item.get("id", "")),
|
| 85 |
+
str(item.get("model", "")),
|
| 86 |
+
str(item.get("name", "")),
|
| 87 |
+
}
|
| 88 |
+
if model_id not in ids and f"opencode-go/{model_id}" not in ids:
|
| 89 |
+
continue
|
| 90 |
+
details = " ".join(_strings_from(item))
|
| 91 |
+
if "messages" in details or "anthropic" in details:
|
| 92 |
+
return "anthropic"
|
| 93 |
+
if (
|
| 94 |
+
"chat/completions" in details
|
| 95 |
+
or "openai" in details
|
| 96 |
+
or "alibaba" in details
|
| 97 |
+
):
|
| 98 |
+
return "openai"
|
| 99 |
+
except requests.RequestException:
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
if model_id.startswith("minimax-"):
|
| 103 |
+
return "anthropic"
|
| 104 |
+
return "openai"
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def create_llm() -> LLM:
|
| 108 |
+
temperature = float(os.getenv("LLM_TEMPERATURE", "0.2"))
|
| 109 |
+
timeout = int(os.getenv("LLM_TIMEOUT", "300"))
|
| 110 |
+
|
| 111 |
+
if _env_bool("USE_OPENROUTER", default=False):
|
| 112 |
+
return LLM(
|
| 113 |
+
model=_openrouter_model_id(
|
| 114 |
+
os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL)
|
| 115 |
+
),
|
| 116 |
+
api_base=OPENROUTER_BASE_URL,
|
| 117 |
+
api_key=_env_required("OPENROUTER_API_KEY"),
|
| 118 |
+
temperature=temperature,
|
| 119 |
+
timeout=timeout,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
api_key = _env_required("OPENCODE_GO_API_KEY")
|
| 123 |
+
model = _opencode_model_id(os.getenv("OPENCODE_GO_MODEL", DEFAULT_OPENCODE_GO_MODEL))
|
| 124 |
+
api_style = _opencode_go_api_style(model, api_key)
|
| 125 |
+
|
| 126 |
+
if api_style == "anthropic":
|
| 127 |
+
return LLM(
|
| 128 |
+
model=f"anthropic/{model}",
|
| 129 |
+
api_base=OPENCODE_GO_ANTHROPIC_BASE_URL,
|
| 130 |
+
api_key=api_key,
|
| 131 |
+
temperature=temperature,
|
| 132 |
+
timeout=timeout,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
return LLM(
|
| 136 |
+
model=f"openai/{model}",
|
| 137 |
+
api_base=OPENCODE_GO_OPENAI_BASE_URL,
|
| 138 |
+
api_key=api_key,
|
| 139 |
+
temperature=temperature,
|
| 140 |
+
timeout=timeout,
|
| 141 |
+
)
|
src/ai_agentic_coder/preview_proxy.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Proxy the generated demo app through the main Gradio Space."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
from fastapi import Request, Response
|
| 9 |
+
from starlette.responses import RedirectResponse
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
PREVIEW_PATH = os.getenv("AI_AGENTIC_CODER_PREVIEW_PATH", "/generated-app").rstrip("/") or "/generated-app"
|
| 13 |
+
PREVIEW_PORT = int(os.getenv("AI_AGENTIC_CODER_PREVIEW_PORT", "7861"))
|
| 14 |
+
PREVIEW_API_PREFIXES = (
|
| 15 |
+
"/gradio_api",
|
| 16 |
+
"/queue",
|
| 17 |
+
"/call",
|
| 18 |
+
"/reset",
|
| 19 |
+
"/heartbeat",
|
| 20 |
+
"/component_server",
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _is_generated_app_request(request: Request) -> bool:
|
| 25 |
+
referrer = request.headers.get("referer", "")
|
| 26 |
+
return f"{PREVIEW_PATH}/" in referrer or referrer.rstrip("/").endswith(PREVIEW_PATH)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _is_generated_app_api_request(request: Request) -> bool:
|
| 30 |
+
return request.url.path.startswith(PREVIEW_API_PREFIXES) and _is_generated_app_request(request)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
async def _proxy_to_preview(request: Request, target_path: str) -> Response:
|
| 34 |
+
target_path = target_path.lstrip("/")
|
| 35 |
+
target = f"http://127.0.0.1:{PREVIEW_PORT}/{target_path}"
|
| 36 |
+
if request.url.query:
|
| 37 |
+
target = f"{target}?{request.url.query}"
|
| 38 |
+
|
| 39 |
+
headers = {
|
| 40 |
+
key: value
|
| 41 |
+
for key, value in request.headers.items()
|
| 42 |
+
if key.lower() not in {"host", "content-length"}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
async with httpx.AsyncClient(follow_redirects=False, timeout=300.0) as client:
|
| 47 |
+
upstream = await client.request(
|
| 48 |
+
request.method,
|
| 49 |
+
target,
|
| 50 |
+
headers=headers,
|
| 51 |
+
content=await request.body(),
|
| 52 |
+
)
|
| 53 |
+
except httpx.RequestError:
|
| 54 |
+
return Response(
|
| 55 |
+
"The generated preview app is not running yet, or it has expired.",
|
| 56 |
+
status_code=503,
|
| 57 |
+
media_type="text/plain",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
excluded_headers = {
|
| 61 |
+
"connection",
|
| 62 |
+
"content-encoding",
|
| 63 |
+
"content-length",
|
| 64 |
+
"transfer-encoding",
|
| 65 |
+
}
|
| 66 |
+
response_headers = {
|
| 67 |
+
key: value
|
| 68 |
+
for key, value in upstream.headers.items()
|
| 69 |
+
if key.lower() not in excluded_headers
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
location = response_headers.get("location")
|
| 73 |
+
if location and location.startswith(f"http://127.0.0.1:{PREVIEW_PORT}/"):
|
| 74 |
+
response_headers["location"] = location.replace(
|
| 75 |
+
f"http://127.0.0.1:{PREVIEW_PORT}",
|
| 76 |
+
PREVIEW_PATH,
|
| 77 |
+
1,
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
return Response(
|
| 81 |
+
upstream.content,
|
| 82 |
+
status_code=upstream.status_code,
|
| 83 |
+
headers=response_headers,
|
| 84 |
+
media_type=upstream.headers.get("content-type"),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def register_preview_proxy(app) -> None:
|
| 89 |
+
if getattr(app.state, "ai_agentic_coder_preview_proxy_registered", False):
|
| 90 |
+
return
|
| 91 |
+
app.state.ai_agentic_coder_preview_proxy_registered = True
|
| 92 |
+
|
| 93 |
+
@app.middleware("http")
|
| 94 |
+
async def generated_app_api_proxy(request: Request, call_next):
|
| 95 |
+
if _is_generated_app_api_request(request):
|
| 96 |
+
return await _proxy_to_preview(request, request.url.path)
|
| 97 |
+
return await call_next(request)
|
| 98 |
+
|
| 99 |
+
async def preview_root() -> RedirectResponse:
|
| 100 |
+
return RedirectResponse(f"{PREVIEW_PATH}/")
|
| 101 |
+
|
| 102 |
+
async def proxy(request: Request, path: str = "") -> Response:
|
| 103 |
+
return await _proxy_to_preview(request, path)
|
| 104 |
+
|
| 105 |
+
methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
|
| 106 |
+
app.add_api_route(PREVIEW_PATH, preview_root, methods=["GET"], include_in_schema=False)
|
| 107 |
+
app.add_api_route(f"{PREVIEW_PATH}/{{path:path}}", proxy, methods=methods, include_in_schema=False)
|
src/ai_agentic_coder/tools/python_code_run_tool.py
CHANGED
|
@@ -7,6 +7,11 @@ import select
|
|
| 7 |
import json
|
| 8 |
import base64
|
| 9 |
import shutil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
from datetime import datetime, timezone, timedelta
|
| 11 |
from pathlib import Path
|
| 12 |
|
|
@@ -15,6 +20,75 @@ from pydantic import BaseModel, Field
|
|
| 15 |
from google.cloud import storage
|
| 16 |
from google.oauth2 import service_account
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
class PythonCodeRunToolInput(BaseModel):
|
| 19 |
"""Input schema for PythonCodeRunTool."""
|
| 20 |
argument: str = Field(..., description="Description of the argument.")
|
|
@@ -25,17 +99,21 @@ class PythonCodeRunTool(BaseTool):
|
|
| 25 |
"This tool runs the python code"
|
| 26 |
)
|
| 27 |
|
| 28 |
-
def upload_to_gcp(self):
|
| 29 |
project_id = os.getenv("GCP_PROJECT_ID")
|
| 30 |
bucket_name = os.getenv("GCP_BUCKET_NAME")
|
|
|
|
|
|
|
|
|
|
| 31 |
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
| 32 |
-
bucket_file_name = f"ai-agentic-coder-{timestamp}"
|
| 33 |
gcp_service_key = os.getenv("GCP_SERVICE_KEY")
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
service_key = json.loads(base64.b64decode(gcp_service_key).decode('utf-8'))
|
| 41 |
creds = service_account.Credentials.from_service_account_info(service_key)
|
|
@@ -44,28 +122,41 @@ class PythonCodeRunTool(BaseTool):
|
|
| 44 |
client = storage.Client(project=project_id, credentials=creds)
|
| 45 |
bucket = client.get_bucket(bucket_name)
|
| 46 |
blob = bucket.blob(bucket_file_name)
|
| 47 |
-
blob.upload_from_filename(
|
| 48 |
|
| 49 |
# Delete the temporary zip file after uploading
|
| 50 |
-
os.remove(
|
| 51 |
|
| 52 |
# Get the signed URL for the uploaded file
|
| 53 |
signed_url = blob.generate_signed_url(
|
| 54 |
version="v4",
|
| 55 |
method="GET",
|
| 56 |
-
expiration=timedelta(minutes=
|
|
|
|
| 57 |
)
|
| 58 |
|
| 59 |
return signed_url
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def _run(self, argument: str) -> str:
|
| 62 |
# First upload the code to GCP
|
| 63 |
signed_url = self.upload_to_gcp()
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
project_root = Path(__file__).resolve().parents[2] # /.../AI-Agentic-Coder
|
| 68 |
-
module_path = "src.ai_agentic_coder.output.app"
|
| 69 |
|
| 70 |
# Build the environment so the subprocess can find our package.
|
| 71 |
# We need both the project root (src/) **and** the output directory
|
|
@@ -73,13 +164,18 @@ class PythonCodeRunTool(BaseTool):
|
|
| 73 |
env = os.environ.copy()
|
| 74 |
|
| 75 |
# Path to /src/ai_agentic_coder/output so `accounts.py` is importable
|
| 76 |
-
output_dir =
|
| 77 |
|
| 78 |
-
# Compose PYTHONPATH: [output_dir]:[
|
| 79 |
-
pythonpath_parts = [str(output_dir), str(
|
| 80 |
if env.get("PYTHONPATH"):
|
| 81 |
pythonpath_parts.append(env["PYTHONPATH"])
|
| 82 |
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
# Construct the command to run the app as a module in unbuffered mode
|
| 85 |
cmd = [sys.executable, "-u", "-m", module_path]
|
|
@@ -102,7 +198,7 @@ class PythonCodeRunTool(BaseTool):
|
|
| 102 |
start_new_session=True,
|
| 103 |
)
|
| 104 |
|
| 105 |
-
public_url =
|
| 106 |
local_url = None
|
| 107 |
|
| 108 |
start_time = time.time()
|
|
@@ -122,22 +218,14 @@ class PythonCodeRunTool(BaseTool):
|
|
| 122 |
if line:
|
| 123 |
print(line)
|
| 124 |
|
| 125 |
-
# Prefer public URL
|
| 126 |
-
m = re.search(r"Running on public URL:\s*(https?://\S+)", line)
|
| 127 |
-
if m:
|
| 128 |
-
public_url = m.group(1)
|
| 129 |
-
break
|
| 130 |
-
|
| 131 |
# Fallback: capture local URL
|
| 132 |
m_local = re.search(r"http://127\.0\.0\.1:\d+", line)
|
| 133 |
if m_local and not local_url:
|
| 134 |
local_url = m_local.group(0)
|
|
|
|
| 135 |
|
| 136 |
-
if not
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
if not public_url:
|
| 140 |
-
public_url = "http://127.0.0.1:7860/"
|
| 141 |
|
| 142 |
# Close our copy of stdout; the app keeps running detached.
|
| 143 |
try:
|
|
@@ -145,6 +233,8 @@ class PythonCodeRunTool(BaseTool):
|
|
| 145 |
except Exception:
|
| 146 |
pass
|
| 147 |
|
|
|
|
|
|
|
| 148 |
return_urls = f"{signed_url}, {public_url}"
|
| 149 |
|
| 150 |
-
return return_urls
|
|
|
|
| 7 |
import json
|
| 8 |
import base64
|
| 9 |
import shutil
|
| 10 |
+
import signal
|
| 11 |
+
import tempfile
|
| 12 |
+
import threading
|
| 13 |
+
import urllib.error
|
| 14 |
+
import urllib.request
|
| 15 |
from datetime import datetime, timezone, timedelta
|
| 16 |
from pathlib import Path
|
| 17 |
|
|
|
|
| 20 |
from google.cloud import storage
|
| 21 |
from google.oauth2 import service_account
|
| 22 |
|
| 23 |
+
|
| 24 |
+
PREVIEW_PATH = os.getenv("AI_AGENTIC_CODER_PREVIEW_PATH", "/generated-app").rstrip("/") or "/generated-app"
|
| 25 |
+
PREVIEW_PORT = int(os.getenv("AI_AGENTIC_CODER_PREVIEW_PORT", "7861"))
|
| 26 |
+
RUN_RESULT_FILE = "latest_run_result.json"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _output_dir() -> Path:
|
| 30 |
+
return Path(__file__).resolve().parents[1] / "output"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _expiry_minutes() -> int:
|
| 34 |
+
return int(os.getenv("AI_AGENTIC_CODER_PREVIEW_TTL_MINUTES", "30"))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _base_url() -> str:
|
| 38 |
+
explicit_base_url = os.getenv("AI_AGENTIC_CODER_BASE_URL")
|
| 39 |
+
if explicit_base_url:
|
| 40 |
+
return explicit_base_url.rstrip("/")
|
| 41 |
+
|
| 42 |
+
space_host = os.getenv("SPACE_HOST")
|
| 43 |
+
if space_host:
|
| 44 |
+
if space_host.startswith(("http://", "https://")):
|
| 45 |
+
return space_host.rstrip("/")
|
| 46 |
+
return f"https://{space_host.rstrip('/')}"
|
| 47 |
+
|
| 48 |
+
space_author = os.getenv("SPACE_AUTHOR_NAME")
|
| 49 |
+
space_repo = os.getenv("SPACE_REPO_NAME")
|
| 50 |
+
if space_author and space_repo:
|
| 51 |
+
return f"https://{space_author}-{space_repo}.hf.space".lower()
|
| 52 |
+
|
| 53 |
+
main_port = os.getenv("AI_AGENTIC_CODER_PORT") or os.getenv("GRADIO_SERVER_PORT") or "7860"
|
| 54 |
+
return f"http://127.0.0.1:{main_port}"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _preview_url() -> str:
|
| 58 |
+
return f"{_base_url()}{PREVIEW_PATH}/"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _terminate_later(process: subprocess.Popen, ttl_seconds: int) -> None:
|
| 62 |
+
def terminate() -> None:
|
| 63 |
+
if process.poll() is not None:
|
| 64 |
+
return
|
| 65 |
+
try:
|
| 66 |
+
os.killpg(process.pid, signal.SIGTERM)
|
| 67 |
+
process.wait(timeout=10)
|
| 68 |
+
except Exception:
|
| 69 |
+
try:
|
| 70 |
+
os.killpg(process.pid, signal.SIGKILL)
|
| 71 |
+
except Exception:
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
timer = threading.Timer(ttl_seconds, terminate)
|
| 75 |
+
timer.daemon = True
|
| 76 |
+
timer.start()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _wait_for_preview_server(process: subprocess.Popen, timeout_seconds: int = 60) -> bool:
|
| 80 |
+
deadline = time.time() + timeout_seconds
|
| 81 |
+
url = f"http://127.0.0.1:{PREVIEW_PORT}/"
|
| 82 |
+
while time.time() < deadline:
|
| 83 |
+
if process.poll() is not None:
|
| 84 |
+
return False
|
| 85 |
+
try:
|
| 86 |
+
with urllib.request.urlopen(url, timeout=1):
|
| 87 |
+
return True
|
| 88 |
+
except (urllib.error.URLError, TimeoutError):
|
| 89 |
+
time.sleep(0.5)
|
| 90 |
+
return process.poll() is None
|
| 91 |
+
|
| 92 |
class PythonCodeRunToolInput(BaseModel):
|
| 93 |
"""Input schema for PythonCodeRunTool."""
|
| 94 |
argument: str = Field(..., description="Description of the argument.")
|
|
|
|
| 99 |
"This tool runs the python code"
|
| 100 |
)
|
| 101 |
|
| 102 |
+
def upload_to_gcp(self) -> str:
|
| 103 |
project_id = os.getenv("GCP_PROJECT_ID")
|
| 104 |
bucket_name = os.getenv("GCP_BUCKET_NAME")
|
| 105 |
+
if not project_id or not bucket_name:
|
| 106 |
+
raise RuntimeError("GCP_PROJECT_ID and GCP_BUCKET_NAME are required.")
|
| 107 |
+
|
| 108 |
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
| 109 |
+
bucket_file_name = f"ai-agentic-coder-{timestamp}.zip"
|
| 110 |
gcp_service_key = os.getenv("GCP_SERVICE_KEY")
|
| 111 |
+
if not gcp_service_key:
|
| 112 |
+
raise RuntimeError("GCP_SERVICE_KEY is required.")
|
| 113 |
|
| 114 |
+
output_dir = _output_dir()
|
| 115 |
+
archive_base = Path(tempfile.gettempdir()) / f"ai-agentic-coder-{timestamp}"
|
| 116 |
+
archive_path = shutil.make_archive(str(archive_base), format="zip", root_dir=output_dir)
|
| 117 |
|
| 118 |
service_key = json.loads(base64.b64decode(gcp_service_key).decode('utf-8'))
|
| 119 |
creds = service_account.Credentials.from_service_account_info(service_key)
|
|
|
|
| 122 |
client = storage.Client(project=project_id, credentials=creds)
|
| 123 |
bucket = client.get_bucket(bucket_name)
|
| 124 |
blob = bucket.blob(bucket_file_name)
|
| 125 |
+
blob.upload_from_filename(archive_path, content_type="application/zip")
|
| 126 |
|
| 127 |
# Delete the temporary zip file after uploading
|
| 128 |
+
os.remove(archive_path)
|
| 129 |
|
| 130 |
# Get the signed URL for the uploaded file
|
| 131 |
signed_url = blob.generate_signed_url(
|
| 132 |
version="v4",
|
| 133 |
method="GET",
|
| 134 |
+
expiration=timedelta(minutes=_expiry_minutes()),
|
| 135 |
+
response_disposition=f'attachment; filename="{bucket_file_name}"',
|
| 136 |
)
|
| 137 |
|
| 138 |
return signed_url
|
| 139 |
|
| 140 |
+
def write_run_result(self, download_url: str, live_url: str) -> None:
|
| 141 |
+
result_path = _output_dir() / RUN_RESULT_FILE
|
| 142 |
+
result_path.write_text(
|
| 143 |
+
json.dumps(
|
| 144 |
+
{
|
| 145 |
+
"download_url": download_url,
|
| 146 |
+
"live_url": live_url,
|
| 147 |
+
"expires_in_minutes": _expiry_minutes(),
|
| 148 |
+
},
|
| 149 |
+
indent=2,
|
| 150 |
+
),
|
| 151 |
+
encoding="utf-8",
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
def _run(self, argument: str) -> str:
|
| 155 |
# First upload the code to GCP
|
| 156 |
signed_url = self.upload_to_gcp()
|
| 157 |
|
| 158 |
+
project_src = Path(__file__).resolve().parents[2]
|
| 159 |
+
module_path = "ai_agentic_coder.generated_app_runner"
|
|
|
|
|
|
|
| 160 |
|
| 161 |
# Build the environment so the subprocess can find our package.
|
| 162 |
# We need both the project root (src/) **and** the output directory
|
|
|
|
| 164 |
env = os.environ.copy()
|
| 165 |
|
| 166 |
# Path to /src/ai_agentic_coder/output so `accounts.py` is importable
|
| 167 |
+
output_dir = _output_dir()
|
| 168 |
|
| 169 |
+
# Compose PYTHONPATH: [output_dir]:[project_src]:<existing>
|
| 170 |
+
pythonpath_parts = [str(output_dir), str(project_src)]
|
| 171 |
if env.get("PYTHONPATH"):
|
| 172 |
pythonpath_parts.append(env["PYTHONPATH"])
|
| 173 |
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
| 174 |
+
env["GENERATED_GRADIO_APP_PATH"] = str(output_dir / "app.py")
|
| 175 |
+
env["GENERATED_GRADIO_PORT"] = str(PREVIEW_PORT)
|
| 176 |
+
env["GENERATED_GRADIO_ROOT_PATH"] = PREVIEW_PATH
|
| 177 |
+
env["GRADIO_ROOT_PATH"] = PREVIEW_PATH
|
| 178 |
+
env["GRADIO_SHARE"] = "False"
|
| 179 |
|
| 180 |
# Construct the command to run the app as a module in unbuffered mode
|
| 181 |
cmd = [sys.executable, "-u", "-m", module_path]
|
|
|
|
| 198 |
start_new_session=True,
|
| 199 |
)
|
| 200 |
|
| 201 |
+
public_url = _preview_url()
|
| 202 |
local_url = None
|
| 203 |
|
| 204 |
start_time = time.time()
|
|
|
|
| 218 |
if line:
|
| 219 |
print(line)
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
# Fallback: capture local URL
|
| 222 |
m_local = re.search(r"http://127\.0\.0\.1:\d+", line)
|
| 223 |
if m_local and not local_url:
|
| 224 |
local_url = m_local.group(0)
|
| 225 |
+
break
|
| 226 |
|
| 227 |
+
if not local_url and not _wait_for_preview_server(process):
|
| 228 |
+
raise RuntimeError("Generated Gradio app exited before it became available.")
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
# Close our copy of stdout; the app keeps running detached.
|
| 231 |
try:
|
|
|
|
| 233 |
except Exception:
|
| 234 |
pass
|
| 235 |
|
| 236 |
+
_terminate_later(process, _expiry_minutes() * 60)
|
| 237 |
+
self.write_run_result(signed_url, public_url)
|
| 238 |
return_urls = f"{signed_url}, {public_url}"
|
| 239 |
|
| 240 |
+
return return_urls
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|