kaushikpaul commited on
Commit
ca22196
·
verified ·
1 Parent(s): 96957fe

Deploy Space

Browse files
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: 5.44.1
6
  ---
7
 
8
  # AI Agentic Coder
9
 
10
- [![Live Demo – Hugging Face Space](https://img.shields.io/badge/Live%20Demo-Hugging%20Face%20Space-yellow?logo=huggingface&logoColor=white)](https://huggingface.co/spaces/kaushikpaul/AI-Agentic-Coder)
11
 
12
  An AI-powered agentic coding assistant that:
13
  - Turns your natural language requirements into a working Python module
@@ -17,7 +17,7 @@ An AI-powered agentic coding assistant that:
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://huggingface.co/spaces/kaushikpaul/AI-Agentic-Coder
21
 
22
  ## Features
23
  - **Idea → Running App in Minutes**
@@ -27,7 +27,7 @@ Backed by a multi-agent CrewAI pipeline, the app coordinates “engineering lead
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
- - Swap LLMs per‑agent via `config/agents.yaml` using OpenRouter model IDs. Compatible with OpenAI‑style endpoints and easy to tune per role.
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,11 +103,22 @@ pip install -r requirements.txt
103
  Create a `.env` file in the project root with the following variables (adjust as needed):
104
 
105
  ```ini
106
- # ——— LLM provider (using OpenRouter-hosted models in agents.yaml) ———
107
- OPENROUTER_API_KEY=your_openrouter_key
108
- # Optional but commonly needed when using OpenRouter via OpenAI-compatible clients:
109
- # OPENAI_API_BASE=https://openrouter.ai/api/v1
110
- # OPENAI_API_KEY=${OPENROUTER_API_KEY}
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  # ——— Google Cloud Storage (used by PythonCodeRunTool) ———
113
  GCP_PROJECT_ID=your_gcp_project_id
@@ -140,9 +151,9 @@ Key features used in this project:
140
  ## Configuration
141
 
142
  ### LLMs and Agents
143
- - File: `src/ai_agentic_coder/config/agents.yaml`
144
- - Default agents use OpenRouter-hosted models (e.g., `openrouter/meta-llama/llama-3.1-405b-instruct:free`, `openrouter/moonshotai/kimi-k2:free`).
145
- - To change models or providers, update the `llm` field for each agent. Ensure appropriate API keys and base configuration are set for your provider.
146
 
147
  ### Tasks & Outputs
148
  - File: `src/ai_agentic_coder/config/tasks.yaml`
@@ -152,6 +163,7 @@ Key features used in this project:
152
  - File: `src/ai_agentic_coder/gradio_ui.py`
153
  - You provide: `Requirements`, `Module Name` (without .py), `Class Name`.
154
  - The app displays a progress bar during execution and, on success, two URLs: a signed download URL and a live app URL.
 
155
 
156
  ### Execution Mode (Docker vs non‑Docker)
157
  - Files: `src/ai_agentic_coder/crew.py`
@@ -176,8 +188,8 @@ This removes the need to have Docker running locally.
176
  3. Enter a module name (e.g., `accounts`) and class name (e.g., `Account`).
177
  4. Click “Run AI Coder”.
178
  5. Wait a few minutes while the pipeline runs. When done, you’ll see:
179
- - A signed Google Cloud Storage URL to download the generated artifacts as a zip
180
- - A public/live URL of the generated Gradio demo app
181
 
182
  ## Outputs
183
  Generated files are saved under `src/ai_agentic_coder/output/`:
@@ -185,30 +197,37 @@ Generated files are saved under `src/ai_agentic_coder/output/`:
185
  - `{module_name}.py` — The generated backend module
186
  - `app.py` — A minimal Gradio UI demonstrating the backend (launched with share=True)
187
  - `test_{module_name}` — Unit test module for the backend
188
- - `gradio_public_url.txt` — A convenience file containing the live URL output
 
189
 
190
  ## Deployment
191
- - The project is already hosted on Hugging Face Spaces: https://huggingface.co/spaces/kaushikpaul/AI-Agentic-Coder
192
- - To deploy your own Space:
 
 
 
 
193
  - Set Space SDK to “Gradio” and point to `src/ai_agentic_coder/main.py` as the entry file.
194
  - Add required secrets in the Space settings:
195
- - `OPENROUTER_API_KEY`
 
196
  - `GCP_PROJECT_ID`, `GCP_BUCKET_NAME`, `GCP_SERVICE_KEY` (base64-encoded service account JSON)
197
  - Ensure the Python version matches (3.10–3.12) and install via `requirements.txt` or `pyproject.toml`.
198
 
199
  ## Troubleshooting
200
  - **Missing or invalid API keys/credentials**
201
- - Verify `.env` values. Ensure OpenRouter key and GCP service key are valid; confirm bucket exists and is accessible.
202
  - **GCS upload errors**
203
- - Confirm `GCP_SERVICE_KEY` contains a valid base64-encoded service account JSON with `storage.objects.create` permission.
204
  - **Live URL not detected**
205
- - The runner waits up to 60 seconds to capture a public URL. If the generated UI didn’t enable `share=True` or the network blocks tunnels, you may see: “Gradio started but no accessible URL was detected within 60 seconds.” Re-run or check network settings.
 
206
  - **Virtualenv issues on Windows**
207
  - Use `.venv\Scripts\activate` and ensure `python` points to the venv interpreter.
208
 
209
  ## Tech Stack
210
  - **Python**: 3.10–3.12
211
- - **Frameworks/Libraries**: CrewAI, Gradio 5, google-cloud-storage, python-dotenv, requests, httpx
212
  - **Orchestration**: YAML-configured agents and tasks via CrewAI
213
  - **UI**: Gradio Blocks with live progress and URL surfacing
214
 
 
2
  title: AI-Agentic-Coder
3
  app_file: src/ai_agentic_coder/main.py
4
  sdk: gradio
5
+ sdk_version: 6.14.0
6
  ---
7
 
8
  # AI Agentic Coder
9
 
10
+ [![Live Website](https://img.shields.io/badge/Live_Website-6c63ff?logo=rocket&logoColor=white&labelColor=5a52d3)](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]>=0.108.0,<1.0.0",
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>=5.22.0",
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]>=0.108.0,<1.0.0
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>=5.22.0
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 google cloud storage and run the python code that was generated
55
  expected_output: >
56
- Signed public url of the zip that is uploaded in google cloud storage and the public url of gradle url
57
- that is started returned after running the tool.
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.agents_config['engineering_lead'],
 
27
  verbose=True,
28
  )
29
 
30
  @agent
31
  def backend_engineer(self) -> Agent:
32
  return Agent(
33
- config=self.agents_config['backend_engineer'],
 
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.agents_config['frontend_engineer'],
 
45
  verbose=True,
46
  )
47
 
48
  @agent
49
  def test_engineer(self) -> Agent:
50
  return Agent(
51
- config=self.agents_config['test_engineer'],
 
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.agents_config['python_code_runner'],
 
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
- subprocess.run(
16
- ["pkill", "-f", "python.*src.ai_agentic_coder.output.app"],
17
- stderr=subprocess.DEVNULL,
18
- stdout=subprocess.DEVNULL
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", theme=gr.themes.Soft()) as demo:
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
- show_copy_button=True,
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
- show_copy_button=True,
106
  elem_classes=["card"]
107
  )
 
108
  live_url_box = gr.Textbox(
109
  label="Live App URL",
110
  interactive=False,
111
  visible=False,
112
- show_copy_button=True,
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(override=True)
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
- local_file_name = bucket_file_name + ".zip"
35
- output_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../output"))
36
 
37
- # Create a zip file of the output directory
38
- shutil.make_archive(bucket_file_name, format="zip", root_dir=output_dir)
 
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(local_file_name)
48
 
49
  # Delete the temporary zip file after uploading
50
- os.remove(local_file_name)
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=10)
 
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
- # Get the project root directory (src/ai_agentic_coder)
66
- # Resolve project root and module path
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 = project_root / "ai_agentic_coder" / "output"
77
 
78
- # Compose PYTHONPATH: [output_dir]:[project_root]:<existing>
79
- pythonpath_parts = [str(output_dir), str(project_root)]
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 = None
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 public_url and local_url:
137
- public_url = local_url
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