Yash030 commited on
Commit
3ae68d6
·
0 Parent(s):

deploy: Hugging Face Space clean release

Browse files
.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ GEMINI_API_KEY=your-api-key
2
+ GROQ_API_KEY=your-api-key
3
+ FIRECRAWL_API_KEY=your-api-key
4
+ OPENROUTER_API_KEY=your-api-key
.gitignore ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environments & Virtual Envs
2
+ .env
3
+ .env.local
4
+ venv/
5
+ .venv/
6
+ env/
7
+ ENV/
8
+
9
+ # Python Cache & Bytecode
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ .pytest_cache/
14
+
15
+ # Agentic Workspaces / Temp Sandboxes
16
+ temp/
17
+
18
+ # OS specific files
19
+ .DS_Store
20
+ Thumbs.db
21
+ db.sqlite3
22
+
23
+ # IDE files
24
+ .vscode/
25
+ .idea/
26
+ *.suo
27
+ *.ntvs*
28
+ *.njsproj
29
+ *.sln
30
+ *.swp
31
+
32
+ #teach
33
+ MISSION.md
34
+ NOTES.md
35
+ RESOURCES.md
36
+ /lessons
37
+ AboutMe.md
38
+ /learning-records
39
+
40
+ #PRD
41
+ PRD.md
42
+
43
+ #Security Report
44
+ ArchitectureSecurityReport.md
45
+
46
+ #test
47
+ test/
48
+ deploy.ps1
49
+ README.hf.md
50
+
51
+ #venv
52
+ dev/
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Start with a slim Python base image
2
+ FROM python:3.11-slim
3
+
4
+ # Install system dependencies (Node.js, Go, Java JDK, and compilation tools)
5
+ RUN apt-get update && apt-get install -y \
6
+ curl \
7
+ gnupg \
8
+ build-essential \
9
+ git \
10
+ golang \
11
+ default-jdk \
12
+ && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
13
+ && apt-get install -y nodejs \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Pre-install typescript and ts-node globally as root for fast and offline TypeScript test executions
17
+ RUN npm install -g typescript ts-node
18
+
19
+ # Set up a new non-root user named "user" with UID 1000 for Hugging Face Spaces security compliance
20
+ RUN useradd -m -u 1000 user
21
+ USER user
22
+ ENV HOME=/home/user \
23
+ PATH=/home/user/.local/bin:$PATH
24
+
25
+ # Set up working directory inside the user's home folder
26
+ WORKDIR $HOME/app
27
+
28
+ # Copy and install Python dependencies as the user
29
+ COPY --chown=user requirements.txt .
30
+ RUN pip install --no-cache-dir --user -r requirements.txt
31
+
32
+ # Copy application assets with proper ownership
33
+ COPY --chown=user . $HOME/app
34
+
35
+ # Hugging Face Spaces always expose port 7860
36
+ EXPOSE 7860
37
+
38
+ # Run FastAPI using uvicorn (serving main:app)
39
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
40
+
OutputFormat.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code Generation & Output Format Specification
2
+
3
+ This document details how the Smart API DevTool packages, displays, and delivers generated code assets and test files to the developer.
4
+
5
+ ---
6
+
7
+ ## 1. User Interface Display (Tabbed Preview)
8
+ When the LangGraph self-healing agent completes generation, the Web UI renders a tabbed workspace containing five distinct views:
9
+
10
+ 1. **Overview & Auth Tab**: Displays extracted auth methods and the integration path recommendation (REST vs. SDK).
11
+ 2. **Endpoints Explorer Tab**: Shows an interactive dictionary of endpoints (GET, POST, DELETE), headers, query params, and request payloads.
12
+ 3. **Wrapper Class Tab**: Displays the syntax-highlighted source code of the generated client wrapper.
13
+ 4. **Unit Tests Tab**: Displays the generated testing suite script.
14
+ 5. **README Guide Tab**: Displays markdown usage instructions, library dependencies, and instantiation examples.
15
+
16
+ ---
17
+
18
+ ## 2. Download Deliverables & File Structure
19
+ Developers can download files individually or as a unified bundle.
20
+
21
+ ### Option A: The Integration Bundle (ZIP)
22
+ Clicking the **"Download Integration Bundle (ZIP)"** button packages all generated files into a single ZIP file dynamically in the browser using `JSZip`. The extracted bundle structure varies by language:
23
+
24
+ #### Python
25
+ ```text
26
+ my-api-integration/
27
+ ├── README.md # Setup instructions, dependencies, and code import examples
28
+ ├── client.py # The generated, type-safe wrapper class
29
+ └── test_client.py # The Pytest test suite for validation (unittest.mock)
30
+ ```
31
+
32
+ #### JavaScript
33
+ ```text
34
+ my-api-integration/
35
+ ├── README.md # Setup instructions and CommonJS import examples
36
+ ├── client.js # The client class (exported via module.exports = { MyClientClass };)
37
+ └── test_client.test.js # Standalone Node test script using named require and global fetch patch
38
+ ```
39
+
40
+ #### TypeScript
41
+ ```text
42
+ my-api-integration/
43
+ ├── README.md # Setup instructions and TS import examples
44
+ ├── client.ts # The client class (exported via export class MyClientClass { ... })
45
+ ├── test_client.test.ts # Standalone TS test script using named imports
46
+ └── tsconfig.json # Minimal TypeScript config for sandbox/ts-node runtimes
47
+ ```
48
+
49
+ #### Go
50
+ ```text
51
+ my-api-integration/
52
+ ├── README.md # Setup instructions and go package import examples
53
+ ├── client.go # Go package source file defining the client struct
54
+ ├── client_test.go # Native Go test suite using testing package and httptest.NewServer
55
+ └── go.mod # Temporary Go module name definition
56
+ ```
57
+
58
+ #### Java
59
+ ```text
60
+ my-api-integration/
61
+ ├── README.md # Setup instructions and compilation steps
62
+ ├── MyAPIClient.java # The Java client class (using java.net.http.HttpClient)
63
+ └── TestClient.java # Standalone Java class with main() method running assertions (-ea)
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 3. Reference Output Code Structures
69
+
70
+ Below are the standard, production-ready structures of generated client wrappers and test suites.
71
+
72
+ ### Python Client (Requests + Tenacity Retry)
73
+ The Python client utilizes `requests.Session` for connection pooling, type hints, custom error wrappers, and `tenacity.Retrying` dynamically at runtime to respect user configurations:
74
+
75
+ ```python
76
+ import requests
77
+ from tenacity import Retrying, stop_after_attempt, wait_exponential, retry_if_exception_type
78
+ from typing import Dict, Any, Optional
79
+
80
+ class APIError(Exception):
81
+ """Base exception for API errors."""
82
+ pass
83
+
84
+ class APIRateLimitError(APIError):
85
+ """Exception for rate limits (429)."""
86
+ pass
87
+
88
+ class APIServerError(APIError):
89
+ """Exception for server-side downtime (5xx)."""
90
+ pass
91
+
92
+ class APIClient:
93
+ def __init__(self, api_key: str, base_url: str = "https://api.example.com/v1", max_retries: int = 3):
94
+ if not api_key:
95
+ raise ValueError("API key cannot be empty.")
96
+ self.api_key = api_key
97
+ self.base_url = base_url.rstrip("/")
98
+ self.max_retries = max_retries
99
+ self.session = requests.Session()
100
+ self.session.headers.update({
101
+ "Authorization": f"Bearer {self.api_key}",
102
+ "Content-Type": "application/json"
103
+ })
104
+
105
+ def _retry_request(self, func):
106
+ """Dynamic tenacity runner to enforce instance-level retry limits."""
107
+ retrier = Retrying(
108
+ stop=stop_after_attempt(self.max_retries + 1),
109
+ wait=wait_exponential(multiplier=0.5, min=1, max=30),
110
+ retry=(
111
+ retry_if_exception_type(APIRateLimitError) |
112
+ retry_if_exception_type(APIServerError)
113
+ ),
114
+ reraise=True
115
+ )
116
+ return retrier(func)
117
+
118
+ def _make_request(self, method: str, path: str, json_data=None) -> Dict[str, Any]:
119
+ url = f"{self.base_url}/{path.lstrip('/')}"
120
+ response = self.session.request(method, url, json=json_data, timeout=10)
121
+
122
+ if response.status_code == 429:
123
+ raise APIRateLimitError(f"Rate limited: {response.text}")
124
+ elif response.status_code >= 500:
125
+ raise APIServerError(f"Server error: {response.status_code}")
126
+
127
+ response.raise_for_status()
128
+ return response.json()
129
+
130
+ def get_resource(self, resource_id: str) -> Dict[str, Any]:
131
+ return self._retry_request(lambda: self._make_request("GET", f"/resources/{resource_id}"))
132
+ ```
133
+
134
+ ### JavaScript Client & Test (CommonJS + Async IIFE Test Harness)
135
+ The JavaScript client uses named exports, the global `fetch` API, and standard Node.js assertions inside a self-contained sequential test block:
136
+
137
+ ```javascript
138
+ // client.js
139
+ class APIClient {
140
+ constructor(apiKey) {
141
+ if (!apiKey) throw new Error('API key cannot be empty.');
142
+ this.apiKey = apiKey;
143
+ this.baseUrl = 'https://api.example.com/v1';
144
+ }
145
+
146
+ async getResource(resourceId) {
147
+ const response = await fetch(`${this.baseUrl}/resources/${resourceId}`, {
148
+ method: 'GET',
149
+ headers: {
150
+ 'Authorization': `Bearer ${this.apiKey}`,
151
+ 'Content-Type': 'application/json'
152
+ }
153
+ });
154
+ if (!response.ok) throw new Error(`HTTP Error ${response.status}`);
155
+ return await response.json();
156
+ }
157
+ }
158
+
159
+ module.exports = { APIClient };
160
+ ```
161
+
162
+ ```javascript
163
+ // test_client.test.js
164
+ const assert = require('assert');
165
+ const { APIClient } = require('./client');
166
+
167
+ async function runTests() {
168
+ const originalFetch = globalThis.fetch;
169
+ try {
170
+ // Setup local request mock
171
+ globalThis.fetch = async (url, options) => {
172
+ assert.strictEqual(options.headers['Authorization'], 'Bearer test-key');
173
+ return {
174
+ ok: true,
175
+ json: async () => ({ id: '123', name: 'Sample' })
176
+ };
177
+ };
178
+
179
+ const client = new APIClient('test-key');
180
+ const data = await client.getResource('123');
181
+ assert.deepStrictEqual(data, { id: '123', name: 'Sample' });
182
+
183
+ console.log('All tests passed successfully.');
184
+ } catch (error) {
185
+ console.error('Test execution failed:', error);
186
+ process.exit(1);
187
+ } finally {
188
+ globalThis.fetch = originalFetch;
189
+ }
190
+ process.exit(0);
191
+ }
192
+
193
+ runTests();
194
+ ```
ProjectPlan.md ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Master Plan: Smart API DevTool
2
+
3
+ This document serves as the master coordinator for the **Smart API DevTool** project. It outlines our architectural roadmap, directory structure, Git workflow, and connects all local configuration, lesson, and implementation tracking files.
4
+
5
+ ---
6
+
7
+ ## 1. Project Overview & Architecture
8
+
9
+ This tool is designed to demonstrate python-based backend engineering, docker configurations, and modern agentic architectures:
10
+ 1. **Interactive Web Dashboard**: A local Python FastAPI server hosting a premium, dark-themed responsive single-page HTML/CSS/JS frontend.
11
+ 2. **Model Context Protocol (MCP) Server**: A standard protocol wrapper enabling AI agents (like Claude Desktop or Cursor) to call your scraping and wrapper-generation engine natively from the IDE.
12
+
13
+ ### System Architecture Flow (Unified Core Engine)
14
+
15
+ Our Core Engine uses **LangGraph** to orchestrate a single-agent **Self-Healing Test Loop** that runs generated unit tests inside our Docker container, captures failures, and auto-corrects code errors.
16
+
17
+ ```
18
+ ┌────────────────────────────────┐
19
+ │ Developer Request / Input │
20
+ └───────────────┬────────────────┘
21
+ │ (URL or Paste Docs)
22
+
23
+ ┌────────────────────────────────┐
24
+ │ Core Scraping Service │
25
+ │ (Firecrawl Keyless / Local) │
26
+ └───────────────┬────────────────┘
27
+ │ (Clean Markdown Docs)
28
+
29
+ ┌────────────────────────────────┐
30
+ │ Pre-Generation Grounding Check │
31
+ │ (Validate REST API Specs) │
32
+ └───────┬────────────────┬───────┘
33
+ │ │
34
+ (Specs Found) ▼ ▼ (No Specs Found)
35
+ ┌────────────────┐ ┌────────────────┐
36
+ │ Proceed to Gen │ │ Raise error │
37
+ └───────┬────────┘ │ (Fail Fast UI) │
38
+ │ └────────────────┘
39
+
40
+ ┌────────────────────────────────┐
41
+ │ Unified Provider Router │
42
+ │ (Gemini, Groq, OpenRouter, │
43
+ │ or Ollama) │
44
+ └───────┬────────────────┬───────┘
45
+ │ │
46
+ (Cloud API) ▼ ▼ (Local: qwen2.5-coder)
47
+ ┌──────────────┐ ┌──────────────┐
48
+ │ Cloud API │ │ Ollama API │
49
+ │ (Gemini/Groq/│ └──────┬───────┘
50
+ │ OpenRouter) │ │
51
+ └──────┬───────┘ │
52
+ │ │
53
+ └────────┬────────┘
54
+ │ (Initial Wrapper & Test Suite Code)
55
+
56
+ ┌────────────────────────────────┐
57
+ │ LangGraph: Generate Node │◄──────────┐
58
+ │ (State: code, tests, errors) │ │
59
+ └───────────────┬────────────────┘ │ (Failed)
60
+ │ │ Route back
61
+ ▼ │ with error
62
+ ┌────────────────────────────────┐ │ logs
63
+ │ LangGraph: Test Node │ │
64
+ │ (Executes tests inside the │ │
65
+ │ Docker container using local │ │
66
+ │ subprocesses: pytest, node, │ │
67
+ │ ts-node, go, java) │ │
68
+ └───────────────┬────────────────┘ │
69
+ │ │
70
+ ├─► [Test Fails & Retries < 3]─┘
71
+
72
+ ▼ [Test Passes OR Retries >= 3]
73
+ ┌────────────────────────────────┐
74
+ │ END / Deliver │
75
+ └───────┬────────────────┬───────┘
76
+ │ │
77
+ ▼ ▼
78
+ ┌────────────────────────┐┌────────────────────────┐
79
+ │ Web UI Output Panel ││ MCP Client Response │
80
+ │ (Browser Download) ││ (IDE Code Write) │
81
+ └────────────────────────┘└────────────────────────┘
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 2. Directory Structure
87
+
88
+ ```
89
+ smart-api-devtool/
90
+ ├── requirements.txt - Python dependencies (fastapi, uvicorn, langgraph, google-genai)
91
+ ├── main.py - Entrypoint: launches FastAPI server or MCP server mode
92
+ ├── Dockerfile - Docker setup pre-installing Python, Node, Go, and Java runtimes
93
+ ├── ProjectPlan.md - Master coordination plan (This file)
94
+ ├── MISSION.md - Project goals and learning mission
95
+ ├── RESOURCES.md - Curated list of documentation and wisdom links
96
+ ├── NOTES.md - Local notes & preferences
97
+ ├── OutputFormat.md - Layout formats for code outputs
98
+ ├── AboutMe.md - Author profile and hackathon directives
99
+ ├── PRD.md - Product Requirements Document
100
+ ├── src/
101
+ │ ├── config.py - Application configurations & env validation
102
+ │ ├── app.py - FastAPI server routes, static assets serving, and CORS config
103
+ │ ├── mcp_server.py - Model Context Protocol integration handler
104
+ │ ├── agent.py - LangGraph Agent Definition (State, Nodes, validation, and Graph)
105
+ │ └── services/
106
+ │ ├── scraper.py - Firecrawl HTTP client (supports keyless & API keys)
107
+ │ └── executor.py - Subprocess code runner executing python/node/go/java sandboxes
108
+ ├── public/ - Web Dashboard Assets
109
+ │ ├── index.html - High-end dark theme dashboard
110
+ │ ├── style.css - Custom glassmorphic styles & animations
111
+ │ └── app.js - Frontend application controller
112
+ ├── lessons/ - Curriculum for developer topics
113
+ └── learning-records/ - Log of key decisions and insights
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 3. Project References
119
+
120
+ Use these clickable file links to inspect details, check tasks, and verify progress:
121
+
122
+ ### Core Configuration Files
123
+ * **[MISSION.md](file:///d:/Downloads/Projects/My%20College%20Projects/Smart%20DevTool%20for%20API%20Integration/MISSION.md)**: Goals and scope boundaries.
124
+ * **[RESOURCES.md](file:///d:/Downloads/Projects/My%20College%20Projects/Smart%20DevTool%20for%20API%20Integration/RESOURCES.md)**: Curated scraping and API development guides.
125
+ * **[NOTES.md](file:///d:/Downloads/Projects/My%20College%20Projects/Smart%20DevTool%20for%20API%20Integration/NOTES.md)**: Scratchpad of active work items and preferences.
126
+ * **[Output Format Guide](file:///d:/Downloads/Projects/My%20College%20Projects/Smart%20DevTool%20for%20API%20Integration/OutputFormat.md)**: Details on ZIP structures and generated code client classes.
127
+ * **[Developer Profile](file:///d:/Downloads/Projects/My%20College%20Projects/Smart%20DevTool%20for%20API%20Integration/AboutMe.md)**: Bio and hackathon placement goals.
128
+ * **[PRD Specifications](file:///d:/Downloads/Projects/My%20College%20Projects/Smart%20DevTool%20for%20API%20Integration/PRD.md)**: Product Requirements Document with user stories and decisions.
129
+
130
+ ### System-Generated Artifacts
131
+ * **[Implementation Plan](file:///C:/Users/yashw/.gemini/antigravity/brain/bfcca984-987f-419e-96de-3d92c17e1877/implementation_plan.md)**: Detailed technical specifications for backend services, API structures, and frontend routes.
132
+ * **[Active Task Checklist](file:///C:/Users/yashw/.gemini/antigravity/brain/bfcca984-987f-419e-96de-3d92c17e1877/task.md)**: Current completion status of specific development steps.
133
+ * **[Walkthrough Report](file:///C:/Users/yashw/.gemini/antigravity/brain/bfcca984-987f-419e-96de-3d92c17e1877/walkthrough.md)**: Summary of final work, validations, and testing outputs.
134
+
135
+ ---
136
+
137
+ ## 4. Phase-by-Phase Roadmap
138
+
139
+ ### **Phase 1: Project Setup & Core Services (Completed)**
140
+ * Configure Python virtual environment and `requirements.txt`.
141
+ * Write the `Dockerfile` installing Python, Node, Go, and Java runtimes.
142
+ * Set up FastAPI app boilerplate (`main.py`, `src/app.py`).
143
+ * Code `scraper.py` using Firecrawl REST interface.
144
+ * Code test runner `executor.py` utilizing Python subprocesses to execute language-specific test runs inside a `/temp` folder sandbox.
145
+ * *Verification*: Verify URL scraping and local test-suite executions via scripts.
146
+
147
+ ### **Phase 2: LangGraph Self-Healing Agent (Completed)**
148
+ * Define the LangGraph State, `generate` Node, and `test` Node.
149
+ * Code the conditional transition edge (Self-Heal Loop up to 3 retries).
150
+ * Connect the compiled LangGraph to the FastAPI endpoints.
151
+ * *Verification*: Verify self-healing loop by running local tests.
152
+
153
+ ### **Phase 3: MCP Server Integration (Completed)**
154
+ * Implement `mcp_server.py` using standard Model Context Protocol stream handlers.
155
+ * Bind standard CLI flag (`python main.py --mcp`) to run the MCP server.
156
+
157
+ ### **Phase 4: Premium Web UI & Exporter (Completed)**
158
+ * Design HTML5 layout with glassmorphism CSS aesthetics.
159
+ * Incorporate model selector dropdowns (Gemini, Ollama, Groq, OpenRouter), and live terminal logging showing the self-healing test runs.
160
+ * Expose code exports (downloading wrapper, unit tests, and README file in a single ZIP).
161
+
162
+ ### **Phase 5: Hugging Face Spaces Deployment (Completed)**
163
+ * Deploy the application to a Hugging Face Space using the custom Dockerfile.
164
+ * *Verification*: Test the hosted web service dynamically in the cloud.
165
+
166
+ ### **Phase 6: Grounding Hardening & Git Delivery (Completed)**
167
+ * Implement LLM-based pre-generation documentation checks to prevent endpoint hallucinations.
168
+ * Apply strict dependency-free test execution rules across all five supported languages.
169
+ * Format the codebase, compile the walkthrough report, and commit changes using Conventional Commit patterns.
170
+
171
+ ---
172
+
173
+ ## 5. Future System Design Proposals
174
+
175
+ ### Architectural Question: Concurrency & Sandbox Isolation on Free Cloud Runtimes
176
+ > *"If there are 10 to 100 users using a local host, when they are using local host and if they do not have the package installed, we do not want to disturb their local system. If we use the Docker availability in Hugging Face and make it a common platform to run all the tasks for those 100 users, there is a resources bottleneck on the free tier (2 vCPUs, 16 GB RAM). How can we resolve this on a free-tier basis to improve project system design?"*
177
+
178
+ ### Proposed Solution: Piston-Based Serverless Code Execution
179
+ To eliminate compiler installation overhead inside the host Docker image and prevent CPU/RAM resource starvation under concurrent user spikes, we propose offloading sandbox executions to a serverless code execution engine:
180
+
181
+ #### 1. How it Works
182
+ - Instead of executing tests locally via Python subprocesses (`pytest`, `node`, `go test`, `javac`), the sandbox executor (`src/services/executor.py`) delegates execution to a **Piston API** endpoint.
183
+ - The executor constructs an HTTP POST request containing the generated wrapper source code, the test file code, and the language identifier, sending it to a public or private Piston API instance:
184
+ ```http
185
+ POST /api/v2/execute HTTP/1.1
186
+ Host: emkc.org
187
+ Content-Type: application/json
188
+
189
+ {
190
+ "language": "python",
191
+ "version": "3.10.0",
192
+ "files": [
193
+ {
194
+ "name": "client.py",
195
+ "content": "class MyAPIClient: ..."
196
+ },
197
+ {
198
+ "name": "test_client.py",
199
+ "content": "def test_get_user(): ..."
200
+ }
201
+ ]
202
+ }
203
+ ```
204
+ - The Piston container runs the tests in isolation and returns structured JSON containing the stdout, stderr, and process exit code.
205
+
206
+ #### 2. Architectural Impact
207
+ * **Pros:**
208
+ - **Ultra-Lightweight Containers:** Deletes heavy compilers (golang, default-jdk, nodejs) from the custom Dockerfile, shrinking the image size from ~2GB to <200MB.
209
+ - **Zero CPU/RAM Contention:** Shifting the compilation work off the Hugging Face Space protects it from OOM (Out of Memory) crashes when 100 concurrent users trigger code executions.
210
+ - **No Local Installs:** Users do not need any local compiler packages on their system, maintaining complete environment isolation.
211
+ * **Cons:**
212
+ - Introduces a network dependency on third-party public execution endpoints, which may be subject to external rate limits or availability.
README.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Smart API DevTool
3
+ emoji: 🚀
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Smart API DevTool
12
+
13
+ A local developer utility that automates third-party API integration. By taking an API documentation URL (or raw pasted text) and a description of your target use case, it scrapes endpoints, recommends integration paths, generates type-safe wrapper classes, and automatically compiles and self-heals any code bugs using localized sandboxes.
14
+
15
+ The engine validates the scraped content using an LLM **Pre-Check Grounding Validation** step to ensure REST specifications exist (failing fast on high-level landing pages), and uses **LangGraph** to coordinate a self-correcting loop that feeds compiler and test logs back to the LLM (up to 3 retries) until the generated assets compile and pass mock checks.
16
+
17
+ > [!NOTE]
18
+ > This application is built as a hybrid workspace: it serves both a premium **Web Dashboard** (FastAPI backend + Glassmorphic HTML/CSS/JS frontend) and a local **Model Context Protocol (MCP) Server** to let IDE agents (like Claude Desktop or Cursor) call the scraping and wrapper generation natively.
19
+
20
+ ---
21
+
22
+ ## Technical Stack & Runtimes
23
+
24
+ * **Core Engine**: Python 3.12, FastAPI, Uvicorn, Pydantic Settings
25
+ * **Agentic Graph**: LangGraph state machine orchestrator
26
+ * **AI Generation**: Google Gemini API (via official `google-genai` SDK), Groq API, OpenRouter API, & local Ollama (`qwen2.5-coder`)
27
+ * **Scraper**: Firecrawl REST scraping service (supporting both API keys and keyless fallback)
28
+ * **Sandboxed Compilers**: Spawns isolated local runtimes for Python (`pytest`), JavaScript (`node`), TypeScript (`ts-node`), Go (`go test`), and Java (`javac`/`java`)
29
+
30
+ > [!IMPORTANT]
31
+ > To execute multi-language sandboxes, the corresponding compilers/runtimes (like `node`, `go`, and `jdk`) must be installed on your local host system. If a runtime is absent, the execution service traps the exception and returns the diagnostic failure cleanly without crashing the core agent thread.
32
+
33
+ ---
34
+
35
+ ## Project Structure
36
+
37
+ ```text
38
+ ├── main.py # Entrypoint (CLI flags to launch FastAPI Web Server or MCP Server)
39
+ ├── requirements.txt # Virtual environment python dependencies
40
+ ├── Dockerfile # Unified build image containing Node, Go, Java, and Python runtimes
41
+ ├── src/
42
+ │ ├── app.py # FastAPI server routes, static assets serving, and CORS config
43
+ │ ├── config.py # Configuration manager validating env parameters via Pydantic
44
+ │ ├── agent.py # LangGraph StateGraph, pre-check validation, and code execution nodes
45
+ │ └── services/
46
+ │ ├── scraper.py # Firecrawl client featuring SSL and rate limit retries
47
+ │ └── executor.py # Sandbox subprocess runner with dynamic compiler timeouts
48
+ ├── public/ # Web Dashboard assets (HTML, glassmorphism CSS, and controllers)
49
+ ├── lessons/ # Academic curriculum detailing sandbox patterns and state machines
50
+ └── learning-records/ # Technical log tracking system designs and key architectural choices
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Local Setup
56
+
57
+ ### 1. Clone & Set Up Python Environment
58
+
59
+ Ensure Python 3.12+ is installed:
60
+
61
+ ```bash
62
+ python -m venv venv
63
+ venv\Scripts\activate # On Windows
64
+ source venv/bin/activate # On Unix/macOS
65
+ pip install -r requirements.txt
66
+ ```
67
+
68
+ ### 2. Configure Environment Variables
69
+
70
+ Create a `.env` file in the project root:
71
+
72
+ ```ini
73
+ GEMINI_API_KEY=your_gemini_api_key_here
74
+ GROQ_API_KEY=your_groq_api_key_here
75
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
76
+ FIRECRAWL_API_KEY=optional_firecrawl_api_key
77
+ OLLAMA_BASE_URL=http://localhost:11434
78
+ HOST=0.0.0.0
79
+ PORT=7860
80
+ ```
81
+
82
+ > [!TIP]
83
+ > If you don't supply a Firecrawl API key, the scraping service automatically falls back to Firecrawl's Cloud Keyless Mode. Similarly, if you want a local-only setup with zero costs, you can select the **Ollama** model provider in the interface to route generations to your local `qwen2.5-coder` instance.
84
+
85
+ ### 3. Run the FastAPI Web Server
86
+
87
+ ```bash
88
+ python main.py
89
+ ```
90
+
91
+ Open `http://localhost:7860` in your web browser to explore the dashboard.
92
+
93
+ ---
94
+
95
+ ## How the Self-Healing Loop Works
96
+
97
+ ```text
98
+ [Docs Scraping] ──► [Grounding Pre-Check] ──► [Initial Generator Node] ──► [Sandbox Test Node]
99
+ │ ▲ │
100
+ ▼ (No Specs Found) │ (Test Fails, Retry < 3) │
101
+ [Fail Fast UI] └─────────────────────────┴─► [Passes / Deliver]
102
+ ```
103
+
104
+ 1. **State Dictionary Initialization**: The agent state stores the scraped markdown, use case details, target language, and diagnostic records.
105
+ 2. **Pre-Check Grounding Validation**: Inspects the scraped content first. If the documentation does not contain actual REST specifications or routes, the workflow raises a validation error immediately, prompting the user for a correct URL.
106
+ 3. **Predict (Generator Node)**: The model outputs structured JSON matching the Pydantic schema containing wrapper code, README, endpoints list, and a test suite.
107
+ 4. **Act & Verify (Executor Node)**: The Python executor creates an isolated UUID directory under `temp/run_{uuid}`, writes code files, and triggers the language's native test runner (e.g. `pytest`, `node`, `ts-node`, `go test`, `javac`).
108
+ 5. **Self-Heal (Loop)**: If the test returns a non-zero exit code, the logs (stdout/stderr) are saved to the state, and the graph loops back to the generator, instructing the model to repair the code. If tests pass, it terminates immediately and packages the clean deliverables.
main.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import argparse
3
+ import uvicorn
4
+ from src.config import settings
5
+ from src.app import app
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="Smart API DevTool - FastAPI Server or MCP Server Mode")
9
+ parser.add_argument(
10
+ "--mcp",
11
+ action="store_true",
12
+ help="Run the tool in Model Context Protocol (MCP) server mode using stdin/stdout streams."
13
+ )
14
+
15
+ args = parser.parse_args()
16
+
17
+ if args.mcp:
18
+ print("Model Context Protocol (MCP) mode selected.", file=sys.stderr)
19
+ try:
20
+ from src.mcp_server import run_mcp_server
21
+ run_mcp_server()
22
+ except ImportError:
23
+ print("Error: MCP server module 'src/mcp_server.py' is not implemented yet (Phase 3). Please run without --mcp to start the FastAPI server.", file=sys.stderr)
24
+ sys.exit(1)
25
+ else:
26
+ # Launch FastAPI server
27
+ print(f"Starting FastAPI Web Server on http://{settings.host}:{settings.port}", file=sys.stderr)
28
+ uvicorn.run("src.app:app", host=settings.host, port=settings.port, reload=settings.reload, access_log=settings.access_log)
29
+
30
+ if __name__ == "__main__":
31
+ main()
32
+
public/assets/index-BpkGHxff.js ADDED
The diff for this file is too large to render. See raw diff
 
public/assets/index-Dq8pwRiF.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:"Inter", sans-serif;--font-mono:"JetBrains Mono", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-950:oklch(25.8% .092 26.042);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-950:oklch(26.2% .051 172.552);--color-indigo-400:oklch(67.3% .182 276.935);--color-purple-500:oklch(62.7% .265 303.9);--color-pink-400:oklch(71.8% .202 349.761);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-2xl:42rem;--container-4xl:56rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-heading:"Outfit", sans-serif}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.-inset-1\.5{inset:calc(var(--spacing) * -1.5)}.inset-0{inset:0}.inset-x-0{inset-inline:0}.inset-y-0{inset-block:0}.-top-1{top:calc(var(--spacing) * -1)}.-top-20{top:calc(var(--spacing) * -20)}.top-0{top:0}.top-4{top:calc(var(--spacing) * 4)}.top-\[20vh\]{top:20vh}.top-\[40vh\]{top:40vh}.-right-1{right:calc(var(--spacing) * -1)}.-right-20{right:calc(var(--spacing) * -20)}.-right-40{right:calc(var(--spacing) * -40)}.right-0{right:0}.right-1\/4{right:25%}.right-4{right:calc(var(--spacing) * 4)}.-bottom-1{bottom:calc(var(--spacing) * -1)}.-bottom-20{bottom:calc(var(--spacing) * -20)}.bottom-0{bottom:0}.-left-1{left:calc(var(--spacing) * -1)}.-left-20{left:calc(var(--spacing) * -20)}.-left-40{left:calc(var(--spacing) * -40)}.left-1\/4{left:25%}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[9999\]{z-index:9999}.col-span-1{grid-column:span 1/span 1}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-3{margin-block:calc(var(--spacing) * 3)}.my-auto{margin-block:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mt-auto{margin-top:auto}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1{height:var(--spacing)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-4\.5{height:calc(var(--spacing) * 4.5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-24{height:calc(var(--spacing) * 24)}.h-80{height:calc(var(--spacing) * 80)}.h-\[2px\]{height:2px}.h-\[400px\]{height:400px}.h-\[500px\]{height:500px}.h-\[600px\]{height:600px}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[300px\]{max-height:300px}.max-h-\[350px\]{max-height:350px}.min-h-0{min-height:0}.min-h-\[220px\]{min-height:220px}.min-h-\[350px\]{min-height:350px}.min-h-\[380px\]{min-height:380px}.min-h-screen{min-height:100vh}.w-1{width:var(--spacing)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-4\.5{width:calc(var(--spacing) * 4.5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-24{width:calc(var(--spacing) * 24)}.w-64{width:calc(var(--spacing) * 64)}.w-80{width:calc(var(--spacing) * 80)}.w-\[200\%\]{width:200%}.w-\[400px\]{width:400px}.w-\[500px\]{width:500px}.w-full{width:100%}.w-screen{width:100vw}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-\[15px\]{min-width:15px}.min-w-\[70px\]{min-width:70px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-\[spin_20s_linear_infinite\]{animation:20s linear infinite spin}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[\#1d2d4c\]{border-color:#1d2d4c}.border-\[\#050505\]{border-color:#050505}.border-amber-500\/20{border-color:#f99c0033}@supports (color:color-mix(in lab, red, red)){.border-amber-500\/20{border-color:color-mix(in oklab, var(--color-amber-500) 20%, transparent)}}.border-amber-900\/30{border-color:#7b33064d}@supports (color:color-mix(in lab, red, red)){.border-amber-900\/30{border-color:color-mix(in oklab, var(--color-amber-900) 30%, transparent)}}.border-black\/20{border-color:#0003}@supports (color:color-mix(in lab, red, red)){.border-black\/20{border-color:color-mix(in oklab, var(--color-black) 20%, transparent)}}.border-emerald-500{border-color:var(--color-emerald-500)}.border-emerald-500\/20{border-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.border-emerald-500\/20{border-color:color-mix(in oklab, var(--color-emerald-500) 20%, transparent)}}.border-red-500{border-color:var(--color-red-500)}.border-white{border-color:var(--color-white)}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.border-white\/5{border-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.border-white\/15{border-color:color-mix(in oklab, var(--color-white) 15%, transparent)}}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.border-white\/20{border-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.border-white\/30{border-color:color-mix(in oklab, var(--color-white) 30%, transparent)}}.border-zinc-400{border-color:var(--color-zinc-400)}.border-t-black{border-top-color:var(--color-black)}.bg-\[\#0a0c10\]{background-color:#0a0c10}.bg-\[\#0a0c10\]\/40{background-color:oklab(15.4018% -.0009171 -.00915512/.4)}.bg-\[\#0a0c10\]\/60{background-color:oklab(15.4018% -.000917099 -.00915512/.6)}.bg-\[\#0a0c10\]\/80{background-color:oklab(15.4018% -.0009171 -.00915512/.8)}.bg-\[\#0b0f19\]{background-color:#0b0f19}.bg-\[\#0d0d0d\]{background-color:#0d0d0d}.bg-\[\#0d0d0d\]\/90{background-color:oklab(15.9066% 7.45058e-9 0/.9)}.bg-\[\#0e0e0e\]\/20{background-color:oklab(16.3758% -1.49012e-8 0/.2)}.bg-\[\#0e0e0e\]\/50{background-color:oklab(16.3758% -1.49012e-8 0/.5)}.bg-\[\#0f111a\]{background-color:#0f111a}.bg-\[\#131b2e\]{background-color:#131b2e}.bg-\[\#07090e\]{background-color:#07090e}.bg-\[\#050505\]{background-color:#050505}.bg-\[\#050505\]\/45{background-color:oklab(11.4918% 7.45058e-9 7.45058e-9/.45)}.bg-\[\#050505\]\/50{background-color:oklab(11.4918% 7.45058e-9 7.45058e-9/.5)}.bg-\[\#050505\]\/60{background-color:oklab(11.4918% 7.45058e-9 7.45058e-9/.6)}.bg-\[\#050505\]\/80{background-color:oklab(11.4918% 7.45058e-9 7.45058e-9/.8)}.bg-\[\#050505\]\/95{background-color:oklab(11.4918% 7.45058e-9 7.45058e-9/.95)}.bg-\[\#080808\]{background-color:#080808}.bg-\[\#080808\]\/20{background-color:oklab(13.4409% 3.72529e-9 7.45058e-9/.2)}.bg-\[\#080808\]\/40{background-color:oklab(13.4409% 3.72529e-9 7.45058e-9/.4)}.bg-\[\#080808\]\/60{background-color:oklab(13.4409% 3.72529e-9 7.45058e-9/.6)}.bg-\[\#080808\]\/95{background-color:oklab(13.4409% 3.72529e-9 7.45058e-9/.95)}.bg-\[\#090909\]{background-color:#090909}.bg-\[\#121212\]{background-color:#121212}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab, red, red)){.bg-amber-500\/10{background-color:color-mix(in oklab, var(--color-amber-500) 10%, transparent)}}.bg-amber-950\/40{background-color:#46190166}@supports (color:color-mix(in lab, red, red)){.bg-amber-950\/40{background-color:color-mix(in oklab, var(--color-amber-950) 40%, transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/35{background-color:#00000059}@supports (color:color-mix(in lab, red, red)){.bg-black\/35{background-color:color-mix(in oklab, var(--color-black) 35%, transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab, red, red)){.bg-black\/40{background-color:color-mix(in oklab, var(--color-black) 40%, transparent)}}.bg-black\/45{background-color:#00000073}@supports (color:color-mix(in lab, red, red)){.bg-black\/45{background-color:color-mix(in oklab, var(--color-black) 45%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-400\/10{background-color:#00d2941a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-400\/10{background-color:color-mix(in oklab, var(--color-emerald-400) 10%, transparent)}}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-emerald-500\/20{background-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/20{background-color:color-mix(in oklab, var(--color-emerald-500) 20%, transparent)}}.bg-emerald-500\/70{background-color:#00bb7fb3}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/70{background-color:color-mix(in oklab, var(--color-emerald-500) 70%, transparent)}}.bg-emerald-500\/\[0\.02\]{background-color:#00bb7f05}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/\[0\.02\]{background-color:color-mix(in oklab, var(--color-emerald-500) 2%, transparent)}}.bg-emerald-950\/20{background-color:#002c2233}@supports (color:color-mix(in lab, red, red)){.bg-emerald-950\/20{background-color:color-mix(in oklab, var(--color-emerald-950) 20%, transparent)}}.bg-green-500\/40{background-color:#00c75866}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/40{background-color:color-mix(in oklab, var(--color-green-500) 40%, transparent)}}.bg-green-500\/80{background-color:#00c758cc}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/80{background-color:color-mix(in oklab, var(--color-green-500) 80%, transparent)}}.bg-purple-500\/\[0\.02\]{background-color:#ac4bff05}@supports (color:color-mix(in lab, red, red)){.bg-purple-500\/\[0\.02\]{background-color:color-mix(in oklab, var(--color-purple-500) 2%, transparent)}}.bg-red-500\/40{background-color:#fb2c3666}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/40{background-color:color-mix(in oklab, var(--color-red-500) 40%, transparent)}}.bg-red-500\/80{background-color:#fb2c36cc}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/80{background-color:color-mix(in oklab, var(--color-red-500) 80%, transparent)}}.bg-red-950\/20{background-color:#46080933}@supports (color:color-mix(in lab, red, red)){.bg-red-950\/20{background-color:color-mix(in oklab, var(--color-red-950) 20%, transparent)}}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950{background-color:var(--color-slate-950)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.bg-white\/5{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.bg-white\/10{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-white\/30{background-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.bg-white\/30{background-color:color-mix(in oklab, var(--color-white) 30%, transparent)}}.bg-white\/\[0\.01\]{background-color:#ffffff03}@supports (color:color-mix(in lab, red, red)){.bg-white\/\[0\.01\]{background-color:color-mix(in oklab, var(--color-white) 1%, transparent)}}.bg-white\/\[0\.02\]{background-color:#ffffff05}@supports (color:color-mix(in lab, red, red)){.bg-white\/\[0\.02\]{background-color:color-mix(in oklab, var(--color-white) 2%, transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab, red, red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab, var(--color-white) 3%, transparent)}}.bg-yellow-500\/40{background-color:#edb20066}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/40{background-color:color-mix(in oklab, var(--color-yellow-500) 40%, transparent)}}.bg-yellow-500\/80{background-color:#edb200cc}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/80{background-color:color-mix(in oklab, var(--color-yellow-500) 80%, transparent)}}.bg-zinc-500\/5{background-color:#71717b0d}@supports (color:color-mix(in lab, red, red)){.bg-zinc-500\/5{background-color:color-mix(in oklab, var(--color-zinc-500) 5%, transparent)}}.bg-zinc-500\/\[0\.02\]{background-color:#71717b05}@supports (color:color-mix(in lab, red, red)){.bg-zinc-500\/\[0\.02\]{background-color:color-mix(in oklab, var(--color-zinc-500) 2%, transparent)}}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-950\/20{background-color:#09090b33}@supports (color:color-mix(in lab, red, red)){.bg-zinc-950\/20{background-color:color-mix(in oklab, var(--color-zinc-950) 20%, transparent)}}.bg-zinc-950\/45{background-color:#09090b73}@supports (color:color-mix(in lab, red, red)){.bg-zinc-950\/45{background-color:color-mix(in oklab, var(--color-zinc-950) 45%, transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tr{--tw-gradient-position:to top right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-\[linear-gradient\(to_right\,\#ffffff03_1px\,transparent_1px\)\,linear-gradient\(to_bottom\,\#ffffff03_1px\,transparent_1px\)\]{background-image:linear-gradient(90deg,#ffffff03 1px,#0000 1px),linear-gradient(#ffffff03 1px,#0000 1px)}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-white\/\[0\.02\]{--tw-gradient-from:#ffffff05}@supports (color:color-mix(in lab, red, red)){.from-white\/\[0\.02\]{--tw-gradient-from:color-mix(in oklab, var(--color-white) 2%, transparent)}}.from-white\/\[0\.02\]{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white\/15{--tw-gradient-via:#ffffff26}@supports (color:color-mix(in lab, red, red)){.via-white\/15{--tw-gradient-via:color-mix(in oklab, var(--color-white) 15%, transparent)}}.via-white\/15{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-zinc-200{--tw-gradient-via:var(--color-zinc-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-zinc-400{--tw-gradient-via:var(--color-zinc-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-zinc-400{--tw-gradient-to:var(--color-zinc-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-zinc-500{--tw-gradient-to:var(--color-zinc-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-zinc-700{--tw-gradient-to:var(--color-zinc-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-\[size\:14px_14px\]{background-size:14px 14px}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.fill-emerald-950\/20{fill:#002c2233}@supports (color:color-mix(in lab, red, red)){.fill-emerald-950\/20{fill:color-mix(in oklab, var(--color-emerald-950) 20%, transparent)}}.fill-red-950\/20{fill:#46080933}@supports (color:color-mix(in lab, red, red)){.fill-red-950\/20{fill:color-mix(in oklab, var(--color-red-950) 20%, transparent)}}.fill-white\/15{fill:#ffffff26}@supports (color:color-mix(in lab, red, red)){.fill-white\/15{fill:color-mix(in oklab, var(--color-white) 15%, transparent)}}.fill-white\/\[0\.02\]{fill:#ffffff05}@supports (color:color-mix(in lab, red, red)){.fill-white\/\[0\.02\]{fill:color-mix(in oklab, var(--color-white) 2%, transparent)}}.stroke-emerald-500{stroke:var(--color-emerald-500)}.stroke-white{stroke:var(--color-white)}.stroke-white\/10{stroke:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.stroke-white\/10{stroke:color-mix(in oklab, var(--color-white) 10%, transparent)}}.object-cover{object-fit:cover}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pr-1{padding-right:var(--spacing)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pb-1\.5{padding-bottom:calc(var(--spacing) * 1.5)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-heading{font-family:var(--font-heading)}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#5f87e2\]{color:#5f87e2}.text-\[\#64748b\]{color:#64748b}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-black{color:var(--color-black)}.text-emerald-400{color:var(--color-emerald-400)}.text-indigo-400{color:var(--color-indigo-400)}.text-pink-400{color:var(--color-pink-400)}.text-red-400{color:var(--color-red-400)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.text-white\/15{color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.text-white\/15{color:color-mix(in oklab, var(--color-white) 15%, transparent)}}.text-white\/20{color:#fff3}@supports (color:color-mix(in lab, red, red)){.text-white\/20{color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.placeholder-slate-600::placeholder{color:var(--color-slate-600)}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(255\,255\,255\,0\.05\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#ffffff0d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(255\,255\,255\,0\.02\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#ffffff05);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-emerald-500\/10{--tw-shadow-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.shadow-emerald-500\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-emerald-500) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-white\/5{--tw-shadow-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.shadow-white\/5{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-white) 5%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-white\/10{--tw-shadow-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.shadow-white\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-white) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.blur-\[80px\]{--tw-blur:blur(80px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[100px\]{--tw-blur:blur(100px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[120px\]{--tw-blur:blur(120px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-sm{--tw-blur:blur(var(--blur-sm));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}@media (hover:hover){.group-hover\:translate-x-0\.5:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:-translate-y-0\.5:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:w-1\.5:hover{width:calc(var(--spacing) * 1.5)}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-\[1\.01\]:hover{scale:1.01}.hover\:border-red-500\/30:hover{border-color:#fb2c364d}@supports (color:color-mix(in lab, red, red)){.hover\:border-red-500\/30:hover{border-color:color-mix(in oklab, var(--color-red-500) 30%, transparent)}}.hover\:border-white\/10:hover{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/10:hover{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.hover\:border-white\/20:hover{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/20:hover{border-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.hover\:border-white\/30:hover{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/30:hover{border-color:color-mix(in oklab, var(--color-white) 30%, transparent)}}.hover\:bg-emerald-700:hover{background-color:var(--color-emerald-700)}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.hover\:bg-white\/20:hover{background-color:#fff3}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/20:hover{background-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.hover\:bg-white\/\[0\.04\]:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/\[0\.04\]:hover{background-color:color-mix(in oklab, var(--color-white) 4%, transparent)}}.hover\:bg-zinc-200:hover{background-color:var(--color-zinc-200)}.hover\:bg-zinc-800:hover{background-color:var(--color-zinc-800)}.hover\:bg-zinc-950\/40:hover{background-color:#09090b66}@supports (color:color-mix(in lab, red, red)){.hover\:bg-zinc-950\/40:hover{background-color:color-mix(in oklab, var(--color-zinc-950) 40%, transparent)}}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-slate-300:hover{color:var(--color-slate-300)}.hover\:text-white:hover{color:var(--color-white)}.hover\:text-zinc-300:hover{color:var(--color-zinc-300)}}.focus\:border-white\/30:focus{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.focus\:border-white\/30:focus{border-color:color-mix(in oklab, var(--color-white) 30%, transparent)}}.focus\:bg-\[\#0d0f15\]:focus{background-color:#0d0f15}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-white\/20:focus{--tw-ring-color:#fff3}@supports (color:color-mix(in lab, red, red)){.focus\:ring-white\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.98\]:active{scale:.98}.active\:scale-\[0\.99\]:active{scale:.99}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:flex-row{flex-direction:row}}@media (width>=48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-3{grid-column:span 3/span 3}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-7{grid-column:span 7/span 7}.md\:block{display:block}.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:gap-5{gap:calc(var(--spacing) * 5)}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:text-left{text-align:left}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#ffffff14;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#ffffff2e}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.terminal-caret{animation:1s step-end infinite blink}@keyframes infiniteScroll{0%{transform:translate(0)}to{transform:translate(-50%)}}.animate-infinite-scroll{animation:25s linear infinite infiniteScroll}@keyframes breathe{0%,to{opacity:.08;transform:scale(1)translate(0)}50%{opacity:.15;transform:scale(1.05)translate(5px,-10px)}}.aurora-glow-1{animation:15s ease-in-out infinite breathe}.aurora-glow-2{animation:10s ease-in-out infinite alternate breathe}.glass-panel{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);background:#050505bf;border:1px solid #ffffff14}.glass-card-hover{transition:all .2s cubic-bezier(.4,0,.2,1)}.glass-card-hover:hover{border-color:#ffffff26;transform:translateY(-1.5px);box-shadow:0 0 30px #ffffff05}.markdown-body{font-size:.875rem;line-height:1.6}.markdown-body h1,.markdown-body h2,.markdown-body h3{color:#fff;margin-top:1.25rem;margin-bottom:.5rem;font-family:Outfit,sans-serif;font-weight:700}.markdown-body h1{border-bottom:1px solid #ffffff14;padding-bottom:.25rem;font-size:1.2rem}.markdown-body h2{font-size:1.05rem}.markdown-body h3{font-size:.95rem}.markdown-body p{color:#94a3b8;margin-bottom:.75rem}.markdown-body ul,.markdown-body ol{margin-bottom:.75rem;padding-left:1.25rem}.markdown-body li{color:#94a3b8;margin-bottom:.25rem}.markdown-body code{color:#fff;background-color:#ffffff0f;border-radius:.25rem;padding:.125rem .25rem;font-family:JetBrains Mono,monospace;font-size:.75rem;font-weight:600}.markdown-body pre code{color:inherit;background-color:#0000;padding:0;font-weight:400}.results-card-fullscreen{z-index:9999!important;background-color:#070708!important;border:1px solid #ffffff1f!important;border-radius:12px!important;width:auto!important;height:auto!important;position:fixed!important;inset:1rem!important;box-shadow:0 25px 50px -12px #000c!important}.markdown-light p,.markdown-light li{color:#374151}.markdown-light h1,.markdown-light h2,.markdown-light h3{color:#09090b;border-color:#00000014}.markdown-light code{color:#18181b;background-color:#0000000d}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}
public/favicon.svg ADDED
public/icons.svg ADDED
public/index.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='16' fill='white'/%3E%3Cpolygon points='13 2 3 14 12 14 11 22 21 10 12 10' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' transform='translate(7, 7) scale(0.75)'/%3E%3C/svg%3E" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Smart API DevTool - Developer Workspace</title>
8
+
9
+ <!-- Google Fonts -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
13
+ <script type="module" crossorigin src="/assets/index-BpkGHxff.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-Dq8pwRiF.css">
15
+ </head>
16
+ <body class="bg-[#0b0f19] text-slate-100 min-h-screen antialiased overflow-x-hidden">
17
+ <div id="root"></div>
18
+ </body>
19
+ </html>
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ pydantic
4
+ pydantic-settings
5
+ google-genai
6
+ langgraph
7
+ langchain-google-genai
8
+ langchain-community
9
+ requests
10
+ pytest
11
+ python-dotenv
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Smart API DevTool source package
src/agent.py ADDED
@@ -0,0 +1,750 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import time
5
+ import logging
6
+ import requests
7
+ from typing import Dict, Any, Optional
8
+ from typing_extensions import TypedDict
9
+ from pydantic import BaseModel, Field
10
+
11
+ from google import genai
12
+ from google.genai import types
13
+ from langgraph.graph import StateGraph, END
14
+
15
+ from src.config import settings
16
+ from src.services.executor import run_tests
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ def retry_api_call(func, *args, max_attempts=5, initial_wait=2, backoff_factor=2, **kwargs):
21
+ """
22
+ Retries an API call with exponential backoff for transient errors (429, 5xx, or specific API errors).
23
+ """
24
+ attempt = 0
25
+ wait = initial_wait
26
+ while True:
27
+ try:
28
+ return func(*args, **kwargs)
29
+ except Exception as e:
30
+ attempt += 1
31
+ if attempt >= max_attempts:
32
+ raise e
33
+
34
+ # Check if the error is retryable
35
+ err_msg = str(e).lower()
36
+ is_retryable = False
37
+
38
+ # 1. Check Gemini API errors
39
+ if "genai" in type(e).__module__ or "APIError" in type(e).__name__:
40
+ status_code = getattr(e, "code", None) or getattr(e, "status", None)
41
+ if status_code in (429, 500, 502, 503, 504) or any(msg in err_msg for msg in ["503", "429", "temporary", "quota", "rate limit", "overloaded", "unavailable"]):
42
+ is_retryable = True
43
+
44
+ # 2. Check requests errors (Groq, OpenRouter, Ollama)
45
+ elif isinstance(e, requests.RequestException):
46
+ status_code = None
47
+ if e.response is not None:
48
+ status_code = e.response.status_code
49
+ if status_code in (429, 500, 502, 503, 504) or any(msg in err_msg for msg in ["503", "429", "rate limit", "overloaded"]):
50
+ is_retryable = True
51
+ elif isinstance(e, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)):
52
+ is_retryable = True
53
+
54
+ # 3. General check
55
+ elif any(msg in err_msg for msg in ["rate limit", "429", "503", "502", "500", "overloaded", "unavailable", "timeout"]):
56
+ is_retryable = True
57
+
58
+ if not is_retryable:
59
+ raise e
60
+
61
+ print(f"[Agent] API call failed with {type(e).__name__}: {e}. Retrying in {wait}s (attempt {attempt}/{max_attempts})...")
62
+ time.sleep(wait)
63
+ wait *= backoff_factor
64
+
65
+ # State definition
66
+ class AgentState(TypedDict):
67
+ scraped_text: str
68
+ use_case: str
69
+ language: str
70
+ model_provider: str
71
+ gemini_key: Optional[str]
72
+ gemini_model: Optional[str]
73
+ groq_key: Optional[str]
74
+ groq_model: Optional[str]
75
+ openrouter_key: Optional[str]
76
+ openrouter_model: Optional[str]
77
+ firecrawl_key: Optional[str]
78
+ retry_count: int
79
+ error_logs: str
80
+ overview: str
81
+ endpoints: str
82
+ code: str
83
+ tests: str
84
+ readme: str
85
+ test_passed: bool
86
+
87
+ # Output model for Gemini structured JSON
88
+ class APIIntegrationOutput(BaseModel):
89
+ overview: str = Field(..., description="Details on authentication methods and recommendation on REST vs SDK integration path.")
90
+ endpoints: str = Field(..., description="Interactive structured list of endpoints, including URLs, parameters, headers, and payload structures.")
91
+ code: str = Field(..., description="The complete, production-ready, type-safe API client wrapper class.")
92
+ tests: str = Field(..., description="The unit tests code suite designed to test the client wrapper class.")
93
+ readme: str = Field(..., description="Markdown usage instructions and setup guide for the wrapper.")
94
+
95
+ # Output model for pre-generation validation
96
+ class APIValidationOutput(BaseModel):
97
+ has_rest_apis: bool = Field(..., description="True if the text contains actual REST API endpoints, HTTP routes, paths, or API specifications. False if it is just a landing page, marketing text, generic tutorials, or contains no endpoints.")
98
+ explanation: str = Field(..., description="A brief explanation of why the document does or does not contain REST APIs or specifications.")
99
+
100
+ def parse_json_response(response_text: str, provider: str = "model") -> Dict[str, Any]:
101
+ """
102
+ Defensively extracts and parses a JSON object from a model's response.
103
+ """
104
+ cleaned_text = response_text.strip()
105
+
106
+ # 1. Try parsing directly
107
+ try:
108
+ return json.loads(cleaned_text)
109
+ except json.JSONDecodeError:
110
+ pass
111
+
112
+ # 2. Try extracting markdown json block
113
+ match = re.search(r"```json\s*(.*?)\s*```", cleaned_text, re.DOTALL)
114
+ if match:
115
+ try:
116
+ return json.loads(match.group(1).strip())
117
+ except json.JSONDecodeError:
118
+ pass
119
+
120
+ # 3. Try finding the first '{' and last '}'
121
+ start = cleaned_text.find('{')
122
+ end = cleaned_text.rfind('}')
123
+ if start != -1 and end != -1:
124
+ try:
125
+ return json.loads(cleaned_text[start:end+1].strip())
126
+ except json.JSONDecodeError:
127
+ pass
128
+
129
+ # Check if the JSON appears to be truncated (e.g. no closing '}' at the end of the text)
130
+ is_truncated = False
131
+ if cleaned_text and not cleaned_text.endswith('}'):
132
+ is_truncated = True
133
+
134
+ error_msg = f"Failed to parse JSON response from {provider}."
135
+ if is_truncated:
136
+ error_msg += " The response appears to be truncated (it does not end with a closing brace '}')."
137
+
138
+ error_msg += f" Raw response:\n{response_text}"
139
+ raise ValueError(error_msg)
140
+
141
+ def validate_documentation(
142
+ scraped_text: str,
143
+ model_provider: str,
144
+ gemini_key: Optional[str] = None,
145
+ gemini_model: Optional[str] = None,
146
+ groq_key: Optional[str] = None,
147
+ groq_model: Optional[str] = None,
148
+ openrouter_key: Optional[str] = None,
149
+ openrouter_model: Optional[str] = None
150
+ ):
151
+ """
152
+ Validates that the scraped text contains actual REST API specifications or endpoints.
153
+ If not, raises a ValueError with an explanation.
154
+ """
155
+ sample_text = scraped_text[:15000].strip()
156
+ if not sample_text:
157
+ raise ValueError("Documentation content is empty. Please provide valid API reference content.")
158
+
159
+ validation_prompt = f"""You are a senior API architect. Analyze the following scraped text from a documentation source and determine if it contains actual REST API endpoint specifications (such as HTTP methods: GET, POST, PUT, DELETE, etc., endpoint paths/routes, request headers, parameters, or JSON payload structures).
160
+
161
+ If the text contains actual REST API endpoint specifications or routes, set "has_rest_apis" to true.
162
+ If the text is just a high-level landing page, generic documentation, tutorials without endpoint paths, marketing text, or contains no actual REST API endpoints/routes, set "has_rest_apis" to false.
163
+
164
+ DOCUMENTATION CONTENT:
165
+ {sample_text}
166
+ """
167
+
168
+ has_rest_apis = True
169
+ explanation = ""
170
+
171
+ if model_provider == "gemini":
172
+ g_key = gemini_key or settings.gemini_api_key
173
+ g_model = gemini_model or "gemini-2.5-flash"
174
+ if not g_key:
175
+ raise ValueError("Google Gemini API Key is required but not provided. Please supply one in your configuration or UI.")
176
+
177
+ client = genai.Client(api_key=g_key)
178
+ response = retry_api_call(
179
+ client.models.generate_content,
180
+ model=g_model,
181
+ contents=validation_prompt,
182
+ config=types.GenerateContentConfig(
183
+ response_mime_type="application/json",
184
+ response_schema=APIValidationOutput,
185
+ temperature=0.0,
186
+ ),
187
+ )
188
+ parsed = response.parsed
189
+ has_rest_apis = parsed.has_rest_apis
190
+ explanation = parsed.explanation
191
+
192
+ elif model_provider == "groq":
193
+ g_key = groq_key or settings.groq_api_key
194
+ g_model = groq_model or "llama-3.3-70b-versatile"
195
+ if not g_key:
196
+ raise ValueError("Groq API Key is required but not provided. Please supply one in your configuration or UI.")
197
+
198
+ url = "https://api.groq.com/openai/v1/chat/completions"
199
+ headers = {
200
+ "Authorization": f"Bearer {g_key}",
201
+ "Content-Type": "application/json"
202
+ }
203
+
204
+ prompt_with_format = validation_prompt + "\n\nCRITICAL: Return ONLY a valid JSON object matching this schema:\n" + json.dumps({
205
+ "has_rest_apis": "boolean (True if the text contains actual REST API endpoints, HTTP routes, paths, or API specifications)",
206
+ "explanation": "string (Brief explanation of why)"
207
+ }, indent=2)
208
+
209
+ payload = {
210
+ "model": g_model,
211
+ "messages": [
212
+ {"role": "user", "content": prompt_with_format}
213
+ ],
214
+ "temperature": 0.0,
215
+ "max_tokens": 1000,
216
+ "response_format": {"type": "json_object"}
217
+ }
218
+
219
+ response = retry_api_call(requests.post, url, json=payload, headers=headers, timeout=60)
220
+ response.raise_for_status()
221
+ result = response.json()
222
+ response_text = result["choices"][0]["message"]["content"]
223
+
224
+ parsed = parse_json_response(response_text, f"Groq validation ({g_model})")
225
+ has_rest_apis = parsed.get("has_rest_apis", True)
226
+ explanation = parsed.get("explanation", "")
227
+
228
+ elif model_provider == "ollama":
229
+ prompt_with_format = validation_prompt + "\n\nCRITICAL: Return ONLY a valid JSON object matching this schema:\n" + json.dumps({
230
+ "has_rest_apis": "boolean (True if the text contains actual REST API endpoints, HTTP routes, paths, or API specifications)",
231
+ "explanation": "string (Brief explanation of why)"
232
+ }, indent=2)
233
+
234
+ url = f"{settings.ollama_base_url.rstrip('/')}/api/generate"
235
+ payload = {
236
+ "model": "qwen2.5-coder",
237
+ "prompt": prompt_with_format,
238
+ "format": "json",
239
+ "stream": False,
240
+ "options": {
241
+ "temperature": 0.0,
242
+ "num_predict": 1000
243
+ }
244
+ }
245
+
246
+ response = retry_api_call(requests.post, url, json=payload, timeout=60)
247
+ response.raise_for_status()
248
+ result = response.json()
249
+ response_text = result.get("response", "")
250
+
251
+ parsed = parse_json_response(response_text, "Ollama validation")
252
+ has_rest_apis = parsed.get("has_rest_apis", True)
253
+ explanation = parsed.get("explanation", "")
254
+
255
+ elif model_provider == "openrouter":
256
+ or_key = openrouter_key or settings.openrouter_api_key
257
+ or_model = openrouter_model or "openrouter/free"
258
+ if not or_key:
259
+ raise ValueError("OpenRouter API Key is required but not provided. Please supply one in your configuration or UI.")
260
+
261
+ url = "https://openrouter.ai/api/v1/chat/completions"
262
+ headers = {
263
+ "Authorization": f"Bearer {or_key}",
264
+ "Content-Type": "application/json",
265
+ "HTTP-Referer": "https://github.com/Yashwant00CR7/Smart-API-Integration-Dev-Tool",
266
+ "X-Title": "Smart API DevTool"
267
+ }
268
+
269
+ prompt_with_format = validation_prompt + "\n\nCRITICAL: Return ONLY a valid JSON object matching this schema:\n" + json.dumps({
270
+ "has_rest_apis": "boolean (True if the text contains actual REST API endpoints, HTTP routes, paths, or API specifications)",
271
+ "explanation": "string (Brief explanation of why)"
272
+ }, indent=2)
273
+
274
+ payload = {
275
+ "model": or_model,
276
+ "messages": [
277
+ {"role": "user", "content": prompt_with_format}
278
+ ],
279
+ "temperature": 0.0,
280
+ "max_tokens": 1000
281
+ }
282
+
283
+ response = retry_api_call(requests.post, url, json=payload, headers=headers, timeout=60)
284
+ response.raise_for_status()
285
+ result = response.json()
286
+ response_text = result["choices"][0]["message"]["content"]
287
+
288
+ parsed = parse_json_response(response_text, f"OpenRouter validation ({or_model})")
289
+ has_rest_apis = parsed.get("has_rest_apis", True)
290
+ explanation = parsed.get("explanation", "")
291
+
292
+ if not has_rest_apis:
293
+ raise ValueError(
294
+ f"REST APIs or specifications were not found in the scraped content. "
295
+ f"Explanation: {explanation} "
296
+ f"Please provide a different URL or paste the raw REST API specifications directly."
297
+ )
298
+
299
+ LANGUAGE_SPECS = {
300
+ "python": {
301
+ "framework": "pytest/unittest",
302
+ "import_instruction": "Import the client wrapper from the 'client' module (e.g., `from client import MyAPIClient`).",
303
+ "rules": [
304
+ "DO NOT use class-level static decorators (e.g., @retry) on instance methods when retry/timeout configuration is dynamic. Instead, instantiate the retrier dynamically inside the instance method (e.g., using tenacity.Retrying) to respect self.max_retries and self.timeout.",
305
+ "Ensure mock side_effect lists have enough elements to match maximum attempts (e.g., max_retries + 1) to prevent mock iterator exhaustion (StopIteration).",
306
+ "DO NOT import or use third-party mocking libraries (e.g., requests_mock, responses). ONLY use the Python standard library's `unittest.mock` module (such as `patch` and `MagicMock`) for all request mocking.",
307
+ "Ensure transient network errors (such as connection timeouts and connection errors) are included in the retrier's retry-conditions alongside rate limits (429) and server errors (5xx).",
308
+ "Use tenacity.Retrying as a dynamic context wrapper directly (e.g., `retrier = Retrying(...)` and `return retrier(lambda: ...)` or similar) rather than defining dynamic inner decorator functions.",
309
+ "Ensure exception regex string patterns in tests (e.g., `assertRaisesRegex`) exactly match the formatting of messages raised by the client wrapper class (do not assume extra prefixes or text unless present in both).",
310
+ "When mocking `requests.exceptions.HTTPError` in unit tests, always initialize it with a descriptive message string matching the HTTP status (e.g., `requests.exceptions.HTTPError('500 Server Error...', response=mock_response)`) so that `str(e)` does not return empty.",
311
+ "Wrap any JSON parsing (e.g., `response.json()`) in a try-except block to catch `ValueError` / `json.JSONDecodeError` and raise your custom API exception (e.g., ChargesAPIError), ensuring JSON parsing errors do not bubble up as raw ValueErrors."
312
+ ]
313
+ },
314
+ "javascript": {
315
+ "framework": "standard built-in assert module and node.js",
316
+ "import_instruction": "Import the client class from `./client` using a named import (e.g., `const { MyClientClass } = require('./client');`).",
317
+ "rules": [
318
+ "DO NOT use ES6 module import/export syntax. Use CommonJS require() and module.exports instead.",
319
+ "DO NOT use the 'import' keyword or the 'import()' function anywhere in either the client or the test code.",
320
+ "In the client code, always export the client class as a named property of module.exports matching the class name (e.g. `module.exports = { MyClientClass };`). In the test code, always import using named destructuring: `const { MyClientClass } = require('./client');`.",
321
+ "DO NOT require or import any third-party npm packages (such as node-fetch, loglevel, axios, lodash, etc.) in either the client or test script. Use only standard Node.js built-in modules (e.g. assert, fs, path).",
322
+ "DO NOT use Node's built-in http or https modules to perform network requests. You MUST use the global fetch API (fetch() or globalThis.fetch()) directly. DO NOT require or import fetch; it is globally available in Node.js v18+.",
323
+ "DO NOT use global test functions or runners like describe(), it(), test(), before(), or after(). Write the tests inside a single top-level async function execution harness `async function runTests() { ... } runTests();`. Wrap all assertions inside a single try/catch block. If any error or assertion fails, log the error and call `process.exit(1)`. If all tests pass, call `process.exit(0)`. Ensure every asynchronous client call is awaited inside this block.",
324
+ "To mock HTTP responses, assign a mock function directly to globalThis.fetch inside the test script instead of using external mock packages.",
325
+ "Wrap any JSON parsing (e.g., `await response.json()`) in a try-catch block to handle syntax errors, throwing a custom descriptive error."
326
+ ]
327
+ },
328
+ "typescript": {
329
+ "framework": "ts-node execution with standard assert",
330
+ "import_instruction": "Import the client class from `./client` using named imports (e.g. `import { MyClientClass } from './client';`).",
331
+ "rules": [
332
+ "DO NOT use third-party test libraries (e.g. Jest, Mocha, Expect). Write tests as a self-contained TypeScript file using the built-in assert module.",
333
+ "In the client code, export the class using named export (e.g. `export class MyClientClass { ... }`). In the test code, import it accordingly (e.g. `import { MyClientClass } from './client';`).",
334
+ "DO NOT import or require any third-party npm packages (such as node-fetch, loglevel, axios, etc.) in the client or test script.",
335
+ "DO NOT use Node's built-in http or https modules to perform network requests. You MUST use the global fetch API (fetch() or globalThis.fetch()) directly. DO NOT import fetch; it is globally available in Node.js v18+.",
336
+ "DO NOT use global test runner functions like describe(), it(), test(), before(), or after(). Write tests inside a single top-level `async function runTests() { ... } runTests();` harness using the built-in assert module, catching errors and exiting with `process.exit(1)` on failure, and exiting with `process.exit(0)` on success. Ensure all asynchronous calls are awaited.",
337
+ "To mock HTTP responses, assign a mock function directly to globalThis.fetch inside the test script.",
338
+ "Wrap any JSON parsing (e.g., `await response.json()`) in a try-catch block to handle syntax errors, throwing a custom descriptive error."
339
+ ]
340
+ },
341
+ "go": {
342
+ "framework": "native 'testing' package",
343
+ "import_instruction": "Import the sandbox package.",
344
+ "rules": [
345
+ "DO NOT use third-party HTTP mocking libraries (e.g. jarcoal/httpmock). Mock HTTP requests using standard library httptest.NewServer or by injecting a custom http.RoundTripper.",
346
+ "Reuse TCP connections by reusing the http.Client instance.",
347
+ "Write standard Go unit tests matching the signature func TestXxx(t *testing.T) from the 'testing' package.",
348
+ "Implement exponential backoff retry logic for rate limits (429) and server errors (5xx) dynamically using time.Sleep."
349
+ ]
350
+ },
351
+ "java": {
352
+ "framework": "Standard public Java class with public static void main(String[] args)",
353
+ "import_instruction": "Import the client class directly.",
354
+ "rules": [
355
+ "DO NOT use JUnit, TestNG, or third-party assertion libraries (e.g. AssertJ, Hamcrest). All assertions MUST use Java's native `assert` keyword.",
356
+ "The test file MUST be a single, standalone Java class with a `public static void main(String[] args)` method that executes all assertions sequentially. If any assertion fails, the program should crash, translating to a non-zero exit code in the sandbox.",
357
+ "DO NOT use third-party HTTP clients (e.g. OkHttp, Apache HttpClient). Use Java 11's built-in java.net.http.HttpClient or HttpURLConnection.",
358
+ "DO NOT use external mocking libraries (e.g. Mockito). Mock API responses by implementing a mock HTTP handler or a custom HttpClient request runner inside the test code.",
359
+ "Ensure that tests run successfully with assertions enabled via the -ea flag (configured in the executor environment)."
360
+ ]
361
+ }
362
+ }
363
+
364
+ def generate_code(state: AgentState) -> Dict[str, Any]:
365
+ """
366
+ Node that calls Gemini or Ollama to generate/regenerate API client wrapper code.
367
+ """
368
+ scraped_text = state.get("scraped_text", "").strip()
369
+ if not scraped_text or (("forbidden" in scraped_text.lower() or "failed" in scraped_text.lower() or "error" in scraped_text.lower()) and len(scraped_text) < 500):
370
+ raise ValueError("Scraped documentation is empty or represents a scraping error. Please provide valid API reference content.")
371
+ use_case = state.get("use_case", "")
372
+ language = state.get("language", "python").lower().strip()
373
+ model_provider = state.get("model_provider", "gemini")
374
+ gemini_key = state.get("gemini_key") or settings.gemini_api_key
375
+ groq_key = state.get("groq_key") or settings.groq_api_key
376
+ groq_model = state.get("groq_model") or "llama-3.3-70b-versatile"
377
+ openrouter_key = state.get("openrouter_key") or settings.openrouter_api_key
378
+ openrouter_model = state.get("openrouter_model") or "openrouter/free"
379
+ retry_count = state.get("retry_count", 0)
380
+ error_logs = state.get("error_logs", "")
381
+
382
+ print(f"[Agent] Generating code iteration {retry_count + 1} for language: {language}")
383
+
384
+ # Build dynamic language rules
385
+ spec = LANGUAGE_SPECS.get(language, {
386
+ "framework": "standard test framework",
387
+ "import_instruction": "Import client from `./client`.",
388
+ "rules": []
389
+ })
390
+ rules_list = [
391
+ f"Test Framework: Use {spec['framework']}.",
392
+ f"For the unit test suite script ('tests'): {spec['import_instruction']}"
393
+ ] + spec['rules']
394
+ language_rules_str = "\n".join(f"- {rule}" for rule in rules_list)
395
+
396
+ # Build prompt
397
+ if retry_count == 0 or not error_logs:
398
+ # Initial prompt
399
+ prompt = f"""You are a Staff Software Engineer. Your task is to generate a fully-functional, production-ready client API wrapper class and an accompanying unit test suite.
400
+
401
+ TARGET LANGUAGE:
402
+ {language}
403
+
404
+ USE CASE DETAILS:
405
+ {use_case}
406
+
407
+ API REFERENCE DOCUMENTATION:
408
+ {scraped_text}
409
+
410
+ CORE REQUIREMENTS:
411
+ 1. 'overview': Outline the authentication mechanism(s) used by the API and recommend the integration path (REST client vs native SDK). Keep it clean and concise.
412
+ 2. 'endpoints': Summarize the endpoints, HTTP methods, parameters, request headers, and payloads required for the use case.
413
+ 3. 'code': Write the full client wrapper class code. The code must:
414
+ - Handle connection timeouts, session reuse, and authorization headers.
415
+ - Include robust error handling and throw custom descriptive exceptions.
416
+ - Implement exponential backoff retry logic for transient errors (e.g., HTTP 429, 5xx) that honors user-configured retry/timeout parameters.
417
+ - NOT contain any placeholders, mock code, or incomplete implementations.
418
+ - Strictly utilize only the paths, HTTP methods, parameters, and payload schemas defined in the provided API Reference Documentation. DO NOT invent, guess, or hallucinate fictional endpoints.
419
+ - Never hardcode API keys or credentials. Retrieve keys dynamically (e.g., using environment variables or configuration files).
420
+ - Integrate production-grade logging using the target language's standard library logging utilities to record requests, retries, and errors, rather than bare print statements (For JavaScript/TypeScript, use standard console.warn/console.error instead of importing loglevel or other third-party npm packages).
421
+ - For JavaScript/TypeScript: DO NOT import, require, or reference any external npm packages (like node-fetch, axios, loglevel, etc.). You MUST use the global fetch API directly (available as fetch() or globalThis.fetch(), do not import it).
422
+ 4. 'tests': Write a complete, executable unit test suite script to validate the wrapper class. The tests must:
423
+ - Compile and run successfully in the target language environment.
424
+ - Use mock servers or standard library mock utilities rather than calling the real API.
425
+ - For JavaScript/TypeScript: DO NOT use global testing hooks like describe(), it(), test(), before(), or after() which are undefined in raw node executions. Write the tests as a sequentially executed script using Node's built-in assert module, catching errors and calling process.exit(1) on failure. Mock requests by patching globalThis.fetch directly.
426
+ 5. 'readme': Write a markdown usage guide explaining setup, configuration, and a quick-start example.
427
+
428
+ LANGUAGE-SPECIFIC RULES:
429
+ {language_rules_str}
430
+ """
431
+ else:
432
+ # Self-healing prompt
433
+ prompt = f"""You are a Staff Software Engineer. The previously generated API client wrapper or test suite failed verification.
434
+ Analyze the error logs and regenerate the complete files to fix the issue.
435
+
436
+ TARGET LANGUAGE:
437
+ {language}
438
+
439
+ USE CASE DETAILS:
440
+ {use_case}
441
+
442
+ API REFERENCE DOCUMENTATION:
443
+ {scraped_text}
444
+
445
+ ERROR LOGS FROM SANDBOX RUN:
446
+ {error_logs}
447
+
448
+ PREVIOUSLY GENERATED CLIENT CODE:
449
+ {state.get("code", "")}
450
+
451
+ PREVIOUSLY GENERATED TEST CODE:
452
+ {state.get("tests", "")}
453
+
454
+ INSTRUCTIONS:
455
+ 1. Correct the wrapper code ('code') or the test suite ('tests') or both to resolve the error.
456
+ 2. Ensure that standard library mocks are correctly imported and used.
457
+ 3. If the error shows missing package imports or runtime path issues, ensure imports are aligned with standard local directory layouts.
458
+ 4. Ensure the wrapper strictly adheres to the provided API Reference Documentation, handles credentials securely without hardcoding, and uses standard logging libraries.
459
+ 5. For JavaScript/TypeScript: NEVER require or import third-party packages (axios, node-fetch, loglevel, etc.), use the global fetch API directly, and do not use describe/it/test hooks.
460
+ 6. Output the complete updated fields: 'overview', 'endpoints', 'code', 'tests', and 'readme'.
461
+ """
462
+
463
+ if model_provider == "gemini":
464
+ if not gemini_key:
465
+ raise ValueError("Google Gemini API Key is required but not provided. Please supply one in your configuration or UI.")
466
+
467
+ gemini_model = state.get("gemini_model") or "gemini-2.5-flash"
468
+ client = genai.Client(api_key=gemini_key)
469
+ response = retry_api_call(
470
+ client.models.generate_content,
471
+ model=gemini_model,
472
+ contents=prompt,
473
+ config=types.GenerateContentConfig(
474
+ response_mime_type="application/json",
475
+ response_schema=APIIntegrationOutput,
476
+ temperature=0.1,
477
+ ),
478
+ )
479
+
480
+ parsed = response.parsed
481
+ return {
482
+ "overview": parsed.overview,
483
+ "endpoints": parsed.endpoints,
484
+ "code": parsed.code,
485
+ "tests": parsed.tests,
486
+ "readme": parsed.readme,
487
+ }
488
+
489
+ elif model_provider == "ollama":
490
+ prompt_with_format = prompt + "\n\nCRITICAL: Return ONLY a valid JSON object matching this schema:\n" + json.dumps({
491
+ "overview": "string (Overview of auth methods, integration path recommendation)",
492
+ "endpoints": "string (List of endpoints, request payloads, headers, query parameters)",
493
+ "code": "string (The complete client wrapper code)",
494
+ "tests": "string (The complete unit test suite)",
495
+ "readme": "string (README markdown guide)"
496
+ }, indent=2)
497
+
498
+ url = f"{settings.ollama_base_url.rstrip('/')}/api/generate"
499
+ payload = {
500
+ "model": "qwen2.5-coder",
501
+ "prompt": prompt_with_format,
502
+ "format": "json",
503
+ "stream": False,
504
+ "options": {
505
+ "temperature": 0.1,
506
+ "num_predict": 4096
507
+ }
508
+ }
509
+
510
+ response = retry_api_call(requests.post, url, json=payload, timeout=120)
511
+ response.raise_for_status()
512
+ result = response.json()
513
+ response_text = result.get("response", "")
514
+
515
+ if not result.get("done", True):
516
+ raise ValueError(
517
+ "Ollama generation was truncated (done is false). "
518
+ "The model ran out of token prediction space before completing the JSON wrapper. "
519
+ "Try reducing the size of your input documentation or choosing a larger context/predict limit."
520
+ )
521
+
522
+ parsed = parse_json_response(response_text, "Ollama model")
523
+ return {
524
+ "overview": parsed.get("overview", ""),
525
+ "endpoints": parsed.get("endpoints", ""),
526
+ "code": parsed.get("code", ""),
527
+ "tests": parsed.get("tests", ""),
528
+ "readme": parsed.get("readme", ""),
529
+ }
530
+ elif model_provider == "groq":
531
+ if not groq_key:
532
+ raise ValueError("Groq API Key is required but not provided. Please supply one in your configuration or UI.")
533
+
534
+ url = "https://api.groq.com/openai/v1/chat/completions"
535
+ headers = {
536
+ "Authorization": f"Bearer {groq_key}",
537
+ "Content-Type": "application/json"
538
+ }
539
+
540
+ is_reasoning_model = "qwen" in groq_model.lower() or "deepseek" in groq_model.lower()
541
+
542
+ # Instruct reasoning models to be concise in their thinking/reasoning process to avoid output token limits
543
+ thinking_instruction = ""
544
+ if is_reasoning_model:
545
+ thinking_instruction = (
546
+ "\nNOTE: Since you are a reasoning model, keep your thinking process concise. "
547
+ "Ensure that the final JSON object is completely generated and not truncated due to output token limits."
548
+ )
549
+
550
+ prompt_with_format = prompt + thinking_instruction + "\n\nCRITICAL: Return ONLY a valid JSON object matching this schema:\n" + json.dumps({
551
+ "overview": "string (Overview of auth methods, integration path recommendation)",
552
+ "endpoints": "string (List of endpoints, request payloads, headers, query parameters)",
553
+ "code": "string (The complete client wrapper code)",
554
+ "tests": "string (The complete unit test suite)",
555
+ "readme": "string (README markdown guide)"
556
+ }, indent=2)
557
+
558
+ payload = {
559
+ "model": groq_model,
560
+ "messages": [
561
+ {"role": "user", "content": prompt_with_format}
562
+ ],
563
+ "temperature": 0.1,
564
+ "max_tokens": 4096
565
+ }
566
+
567
+ # Qwen and reasoning models do not support JSON Object mode on Groq when thinking/reasoning is enabled
568
+ if not is_reasoning_model:
569
+ payload["response_format"] = {"type": "json_object"}
570
+
571
+ response = retry_api_call(requests.post, url, json=payload, headers=headers, timeout=120)
572
+ response.raise_for_status()
573
+ result = response.json()
574
+
575
+ choice = result["choices"][0]
576
+ response_text = choice["message"]["content"]
577
+ finish_reason = choice.get("finish_reason")
578
+
579
+ if finish_reason == "length":
580
+ raise ValueError(
581
+ f"Groq ({groq_model}) generation was truncated (finish_reason: length). "
582
+ f"The model ran out of output tokens before completing the JSON wrapper. "
583
+ f"Try reducing the size of your input documentation or using a model with a larger output limit."
584
+ )
585
+
586
+ parsed = parse_json_response(response_text, f"Groq model ({groq_model})")
587
+ return {
588
+ "overview": parsed.get("overview", ""),
589
+ "endpoints": parsed.get("endpoints", ""),
590
+ "code": parsed.get("code", ""),
591
+ "tests": parsed.get("tests", ""),
592
+ "readme": parsed.get("readme", ""),
593
+ }
594
+ elif model_provider == "openrouter":
595
+ if not openrouter_key:
596
+ raise ValueError("OpenRouter API Key is required but not provided. Please supply one in your configuration or .env file.")
597
+
598
+ url = "https://openrouter.ai/api/v1/chat/completions"
599
+ headers = {
600
+ "Authorization": f"Bearer {openrouter_key}",
601
+ "Content-Type": "application/json",
602
+ "HTTP-Referer": "https://github.com/Yashwant00CR7/Smart-API-Integration-Dev-Tool",
603
+ "X-Title": "Smart API DevTool"
604
+ }
605
+
606
+ prompt_with_format = prompt + "\n\nCRITICAL: Return ONLY a valid JSON object matching this schema:\n" + json.dumps({
607
+ "overview": "string (Overview of auth methods, integration path recommendation)",
608
+ "endpoints": "string (List of endpoints, request payloads, headers, query parameters)",
609
+ "code": "string (The complete client wrapper code)",
610
+ "tests": "string (The complete unit test suite)",
611
+ "readme": "string (README markdown guide)"
612
+ }, indent=2)
613
+
614
+ payload = {
615
+ "model": openrouter_model,
616
+ "messages": [
617
+ {"role": "user", "content": prompt_with_format}
618
+ ],
619
+ "temperature": 0.1,
620
+ "max_tokens": 4096
621
+ }
622
+
623
+ response = retry_api_call(requests.post, url, json=payload, headers=headers, timeout=120)
624
+ response.raise_for_status()
625
+ result = response.json()
626
+
627
+ choice = result["choices"][0]
628
+ response_text = choice["message"]["content"]
629
+ finish_reason = choice.get("finish_reason")
630
+
631
+ if finish_reason == "length":
632
+ raise ValueError(
633
+ f"OpenRouter ({openrouter_model}) generation was truncated (finish_reason: length). "
634
+ f"The model ran out of output tokens before completing the JSON wrapper. "
635
+ f"Try reducing the size of your input documentation or using a model with a larger output limit."
636
+ )
637
+
638
+ parsed = parse_json_response(response_text, f"OpenRouter model ({openrouter_model})")
639
+ return {
640
+ "overview": parsed.get("overview", ""),
641
+ "endpoints": parsed.get("endpoints", ""),
642
+ "code": parsed.get("code", ""),
643
+ "tests": parsed.get("tests", ""),
644
+ "readme": parsed.get("readme", ""),
645
+ }
646
+ else:
647
+ raise ValueError(f"Unsupported model provider: {model_provider}")
648
+
649
+ def execute_sandbox(state: AgentState) -> Dict[str, Any]:
650
+ """
651
+ Node that runs the generated client wrapper and unit tests in the isolated sandbox.
652
+ """
653
+ language = state.get("language", "python")
654
+ code = state.get("code", "")
655
+ tests = state.get("tests", "")
656
+ retry_count = state.get("retry_count", 0)
657
+
658
+ print(f"[Agent] Executing tests inside isolated sandbox (Iteration {retry_count + 1})...")
659
+
660
+ try:
661
+ test_passed, console_logs = run_tests(language, code, tests)
662
+ except Exception as e:
663
+ test_passed = False
664
+ console_logs = f"Subprocess executor failed with exception: {str(e)}"
665
+
666
+ print(f"[Agent] Test execution finished. Passed: {test_passed}")
667
+
668
+ return {
669
+ "test_passed": test_passed,
670
+ "error_logs": "" if test_passed else console_logs,
671
+ "retry_count": retry_count + 1
672
+ }
673
+
674
+ def should_continue(state: AgentState) -> str:
675
+ """
676
+ Conditional edge deciding whether to self-heal or exit.
677
+ """
678
+ if state.get("test_passed") or state.get("retry_count", 0) >= 3:
679
+ return "end"
680
+ return "generate"
681
+
682
+ # Build LangGraph workflow
683
+ workflow = StateGraph(AgentState)
684
+ workflow.add_node("generate", generate_code)
685
+ workflow.add_node("execute", execute_sandbox)
686
+
687
+ workflow.set_entry_point("generate")
688
+ workflow.add_edge("generate", "execute")
689
+ workflow.add_conditional_edges(
690
+ "execute",
691
+ should_continue,
692
+ {
693
+ "end": END,
694
+ "generate": "generate"
695
+ }
696
+ )
697
+
698
+ compiled_graph = workflow.compile()
699
+
700
+ def run_agent_workflow(
701
+ scraped_text: str,
702
+ use_case: str,
703
+ language: str,
704
+ model_provider: str,
705
+ gemini_key: Optional[str] = None,
706
+ gemini_model: Optional[str] = None,
707
+ groq_key: Optional[str] = None,
708
+ groq_model: Optional[str] = None,
709
+ openrouter_key: Optional[str] = None,
710
+ openrouter_model: Optional[str] = None,
711
+ firecrawl_key: Optional[str] = None
712
+ ) -> Dict[str, Any]:
713
+ """
714
+ Main entrypoint to trigger the self-healing agent loop.
715
+ """
716
+ # Pre-generation validation
717
+ validate_documentation(
718
+ scraped_text=scraped_text,
719
+ model_provider=model_provider,
720
+ gemini_key=gemini_key,
721
+ gemini_model=gemini_model,
722
+ groq_key=groq_key,
723
+ groq_model=groq_model,
724
+ openrouter_key=openrouter_key,
725
+ openrouter_model=openrouter_model
726
+ )
727
+
728
+ initial_state = {
729
+ "scraped_text": scraped_text,
730
+ "use_case": use_case,
731
+ "language": language,
732
+ "model_provider": model_provider,
733
+ "gemini_key": gemini_key,
734
+ "gemini_model": gemini_model,
735
+ "groq_key": groq_key,
736
+ "groq_model": groq_model,
737
+ "openrouter_key": openrouter_key,
738
+ "openrouter_model": openrouter_model,
739
+ "firecrawl_key": firecrawl_key,
740
+ "retry_count": 0,
741
+ "error_logs": "",
742
+ "overview": "",
743
+ "endpoints": "",
744
+ "code": "",
745
+ "tests": "",
746
+ "readme": "",
747
+ "test_passed": False
748
+ }
749
+
750
+ return compiled_graph.invoke(initial_state)
src/app.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, HTTPException, Body, Response
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.staticfiles import StaticFiles
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, List, Dict, Any
7
+
8
+ from src.config import settings
9
+ from src.services.scraper import scrape_url
10
+ from src.services.executor import run_tests
11
+ from src.agent import run_agent_workflow
12
+
13
+
14
+ app = FastAPI(
15
+ title="Smart API DevTool",
16
+ description="Backend API for crawling docs and auto-generating self-healing API wrappers.",
17
+ version="1.0.0"
18
+ )
19
+
20
+ # Enable CORS for frontend dashboard
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ class AnalyzeRequest(BaseModel):
30
+ url: Optional[str] = Field(None, description="The URL of the API documentation.")
31
+ raw_docs: Optional[str] = Field(None, description="Raw pasted API documentation text.")
32
+ use_case: str = Field(..., description="Details on what the wrapper will do.")
33
+ language: str = Field("python", description="Target programming language.")
34
+ model_provider: str = Field("gemini", description="Either 'gemini', 'ollama', 'groq' or 'openrouter'.")
35
+ gemini_key: Optional[str] = Field(None, description="Optional Google Gemini API Key.")
36
+ gemini_model: Optional[str] = Field("gemini-2.5-flash", description="Optional Google Gemini Model ID.")
37
+ groq_key: Optional[str] = Field(None, description="Optional Groq API Key.")
38
+ groq_model: Optional[str] = Field(None, description="Optional Groq Model ID.")
39
+ openrouter_key: Optional[str] = Field(None, description="Optional OpenRouter API Key.")
40
+ openrouter_model: Optional[str] = Field("openrouter/free", description="Optional OpenRouter Model ID.")
41
+ firecrawl_key: Optional[str] = Field(None, description="Optional Firecrawl API Key.")
42
+
43
+ @app.get("/api/health")
44
+ def health_check():
45
+ """Simple endpoint to verify server status."""
46
+ return {
47
+ "status": "healthy",
48
+ "configuration": {
49
+ "has_gemini_key": bool(settings.gemini_api_key),
50
+ "has_groq_key": bool(settings.groq_api_key),
51
+ "has_openrouter_key": bool(settings.openrouter_api_key),
52
+ "has_firecrawl_key": bool(settings.firecrawl_api_key),
53
+ "ollama_base_url": settings.ollama_base_url
54
+ }
55
+ }
56
+
57
+ @app.get("/favicon.ico", include_in_schema=False)
58
+ def favicon():
59
+ """Handles browser favicon requests with 204 No Content to prevent console 404 log littering."""
60
+ return Response(status_code=204)
61
+
62
+ @app.post("/api/analyze")
63
+ async def analyze_api(request: AnalyzeRequest):
64
+ """
65
+ Analyzes API documentation (via URL scraping or raw text) and triggers
66
+ the LangGraph self-healing agent loop to output verified wrapper classes.
67
+ """
68
+ try:
69
+ scraped_text = ""
70
+ if request.url:
71
+ scraped_text = scrape_url(request.url)
72
+ elif request.raw_docs:
73
+ scraped_text = request.raw_docs
74
+ else:
75
+ raise HTTPException(status_code=400, detail="Must provide either a URL or raw_docs.")
76
+
77
+ result = run_agent_workflow(
78
+ scraped_text=scraped_text,
79
+ use_case=request.use_case,
80
+ language=request.language,
81
+ model_provider=request.model_provider,
82
+ gemini_key=request.gemini_key,
83
+ gemini_model=request.gemini_model,
84
+ groq_key=request.groq_key,
85
+ groq_model=request.groq_model,
86
+ openrouter_key=request.openrouter_key,
87
+ openrouter_model=request.openrouter_model,
88
+ firecrawl_key=request.firecrawl_key
89
+ )
90
+
91
+ return {
92
+ "success": result.get("test_passed", False),
93
+ "overview": result.get("overview", ""),
94
+ "endpoints": result.get("endpoints", ""),
95
+ "code": result.get("code", ""),
96
+ "tests": result.get("tests", ""),
97
+ "readme": result.get("readme", ""),
98
+ "retry_count": result.get("retry_count", 0),
99
+ "error_logs": result.get("error_logs", ""),
100
+ "test_passed": result.get("test_passed", False)
101
+ }
102
+ except Exception as e:
103
+ raise HTTPException(status_code=500, detail=str(e))
104
+
105
+ # Mount static files programmatically if the public folder exists
106
+ public_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "public"))
107
+ if os.path.isdir(public_path):
108
+ app.mount("/", StaticFiles(directory=public_path, html=True), name="public")
109
+ else:
110
+ # Fallback message route for clean dev experience before frontend is built
111
+ @app.get("/")
112
+ def read_root():
113
+ return {
114
+ "message": "Welcome to Smart API DevTool Backend! The 'public' directory is not yet present. Exposing API routes at /api/health."
115
+ }
src/config.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+ from typing import Optional
4
+
5
+ class Settings(BaseSettings):
6
+ # API Keys & Third-party integrations
7
+ gemini_api_key: Optional[str] = None
8
+ groq_api_key: Optional[str] = None
9
+ openrouter_api_key: Optional[str] = None
10
+ firecrawl_api_key: Optional[str] = None
11
+ ollama_base_url: str = "http://localhost:11434"
12
+
13
+ # Server configuration
14
+ host: str = "0.0.0.0"
15
+ port: int = 7860
16
+ reload: bool = False
17
+ access_log: bool = False
18
+ gemini_model: str = "gemini-2.5-flash"
19
+ openrouter_model: str = "openrouter/free"
20
+
21
+ # Environment config
22
+ model_config = SettingsConfigDict(
23
+ env_file=".env",
24
+ env_file_encoding="utf-8",
25
+ extra="ignore"
26
+ )
27
+
28
+ settings = Settings()
29
+
src/mcp_server.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import json
3
+ import traceback
4
+ import logging
5
+ from typing import Dict, Any, Optional
6
+
7
+ from src.services.scraper import scrape_url
8
+ from src.agent import run_agent_workflow
9
+
10
+ def run_mcp_server():
11
+ """
12
+ Runs the Model Context Protocol (MCP) server over standard I/O (stdin/stdout).
13
+ To prevent any print statements, logs, or external library debug outputs from
14
+ corrupting the JSON-RPC stream, sys.stdout is redirected to sys.stderr, while
15
+ original stdout is preserved specifically for sending JSON-RPC response frames.
16
+ """
17
+ # Preserve original stdout for JSON-RPC communication
18
+ original_stdout = sys.stdout
19
+
20
+ # Redirect global stdout to stderr so all general print() calls go to stderr
21
+ sys.stdout = sys.stderr
22
+
23
+ # Reconfigure stdin/stdout text streams to use UTF-8 and handle encoding errors gracefully (especially on Windows)
24
+ if hasattr(sys.stdin, "reconfigure"):
25
+ try:
26
+ sys.stdin.reconfigure(encoding="utf-8", errors="replace")
27
+ except Exception as e:
28
+ print(f"[MCP Server] Warning reconfiguring stdin encoding: {str(e)}", file=sys.stderr)
29
+
30
+ if hasattr(original_stdout, "reconfigure"):
31
+ try:
32
+ original_stdout.reconfigure(encoding="utf-8", errors="replace")
33
+ except Exception as e:
34
+ print(f"[MCP Server] Warning reconfiguring stdout encoding: {str(e)}", file=sys.stderr)
35
+
36
+ # Redirect any existing standard logging stream handlers pointing to stdout to prevent JSON-RPC contamination
37
+ try:
38
+ for handler in logging.root.handlers:
39
+ if isinstance(handler, logging.StreamHandler) and handler.stream in (sys.__stdout__, original_stdout):
40
+ handler.setStream(sys.stderr)
41
+ except Exception as e:
42
+ print(f"[MCP Server] Warning redirecting logging handlers: {str(e)}", file=sys.stderr)
43
+
44
+ print("[MCP Server] Starting hardened MCP server stream loop...", file=sys.stderr)
45
+ print("[MCP Server] Redirected sys.stdout and standard logging handlers to sys.stderr to protect stream channel.", file=sys.stderr)
46
+
47
+ # Process standard input line by line
48
+ for line in sys.stdin:
49
+ if not line.strip():
50
+ continue
51
+
52
+ req_id = None
53
+ is_notification = True
54
+
55
+ def send_response(response: Dict[str, Any], force: bool = False):
56
+ # Notifications MUST NOT receive responses per JSON-RPC 2.0 spec,
57
+ # except for severe Parse/Invalid Request errors where we force a response.
58
+ if is_notification and not force:
59
+ return
60
+ try:
61
+ out_line = json.dumps(response) + "\n"
62
+ original_stdout.write(out_line)
63
+ original_stdout.flush()
64
+ except Exception as e:
65
+ print(f"[MCP Server] Error writing response to stdout: {str(e)}", file=sys.stderr)
66
+
67
+ def send_error(code: int, message: str, r_id: Optional[Any] = None, data: Optional[Any] = None):
68
+ err_resp = {
69
+ "jsonrpc": "2.0",
70
+ "error": {
71
+ "code": code,
72
+ "message": message
73
+ }
74
+ }
75
+ if data is not None:
76
+ err_resp["error"]["data"] = data
77
+ if r_id is not None:
78
+ err_resp["id"] = r_id
79
+ else:
80
+ err_resp["id"] = None
81
+
82
+ # Severe protocol validation errors (parse error, invalid request) are sent back
83
+ force_reply = code in [-32700, -32600]
84
+ send_response(err_resp, force=force_reply)
85
+
86
+ try:
87
+ request = json.loads(line)
88
+ if not isinstance(request, dict):
89
+ send_error(-32600, "Invalid Request: expected JSON object")
90
+ continue
91
+
92
+ # Determine if this message is a request or notification
93
+ is_notification = "id" not in request
94
+ req_id = request.get("id")
95
+ method = request.get("method")
96
+ jsonrpc = request.get("jsonrpc")
97
+
98
+ # Verify JSON-RPC version
99
+ if jsonrpc and jsonrpc != "2.0":
100
+ print(f"[MCP Server] Warning: received JSON-RPC version {jsonrpc}, expecting 2.0", file=sys.stderr)
101
+
102
+ if not method or not isinstance(method, str):
103
+ send_error(-32600, "Invalid Request: missing or invalid method field", req_id)
104
+ continue
105
+
106
+ # Parse parameters safely
107
+ params = request.get("params")
108
+ if params is None:
109
+ params = {}
110
+ elif not isinstance(params, dict):
111
+ send_error(-32602, "Invalid params: expected JSON object", req_id)
112
+ continue
113
+
114
+ print(f"[MCP Server] Received method: '{method}' (id: {req_id}, notification: {is_notification})", file=sys.stderr)
115
+
116
+ # 1. Protocol Lifecycle Handshake
117
+ if method == "initialize":
118
+ protocol_version = params.get("protocolVersion", "2024-11-05")
119
+ response = {
120
+ "jsonrpc": "2.0",
121
+ "id": req_id,
122
+ "result": {
123
+ "protocolVersion": protocol_version,
124
+ "capabilities": {
125
+ "tools": {}
126
+ },
127
+ "serverInfo": {
128
+ "name": "Smart-API-DevTool-Server",
129
+ "version": "1.0.0"
130
+ }
131
+ }
132
+ }
133
+ send_response(response)
134
+
135
+ elif method == "notifications/initialized":
136
+ print("[MCP Server] Initialized notification received.", file=sys.stderr)
137
+
138
+ elif method == "ping":
139
+ response = {
140
+ "jsonrpc": "2.0",
141
+ "id": req_id,
142
+ "result": {}
143
+ }
144
+ send_response(response)
145
+
146
+ # 2. Tool Discovery
147
+ elif method == "tools/list":
148
+ tools = [
149
+ {
150
+ "name": "scrape_url",
151
+ "description": "Scrapes the target API documentation URL using Firecrawl and returns the clean markdown content.",
152
+ "inputSchema": {
153
+ "type": "object",
154
+ "properties": {
155
+ "url": {
156
+ "type": "string",
157
+ "description": "The HTTP or HTTPS URL of the API documentation page to scrape."
158
+ },
159
+ "api_key": {
160
+ "type": "string",
161
+ "description": "Optional Firecrawl API Key. If not provided, it falls back to the server configuration."
162
+ }
163
+ },
164
+ "required": ["url"]
165
+ }
166
+ },
167
+ {
168
+ "name": "generate_wrapper",
169
+ "description": "Generates a complete, verified API client wrapper class, usage README guide, and unit tests using a self-healing LangGraph agentic loop.",
170
+ "inputSchema": {
171
+ "type": "object",
172
+ "properties": {
173
+ "scraped_text": {
174
+ "type": "string",
175
+ "description": "The raw text or scraped markdown documentation of the API."
176
+ },
177
+ "use_case": {
178
+ "type": "string",
179
+ "description": "Details of the target use case and functions to implement in the wrapper."
180
+ },
181
+ "language": {
182
+ "type": "string",
183
+ "description": "Target programming language (e.g. 'python', 'typescript', 'go', 'java'). Default is 'python'."
184
+ },
185
+ "model_provider": {
186
+ "type": "string",
187
+ "description": "The model provider to use ('gemini', 'ollama', 'groq', or 'openrouter'). Default is 'gemini'."
188
+ },
189
+ "gemini_key": {
190
+ "type": "string",
191
+ "description": "Optional Google Gemini API Key. Required if model_provider is 'gemini' and the server has no key configured."
192
+ },
193
+ "gemini_model": {
194
+ "type": "string",
195
+ "description": "Optional Google Gemini Model ID (e.g. 'gemini-2.5-flash')."
196
+ },
197
+ "groq_key": {
198
+ "type": "string",
199
+ "description": "Optional Groq API Key. Required if model_provider is 'groq' and the server has no key configured."
200
+ },
201
+ "groq_model": {
202
+ "type": "string",
203
+ "description": "Optional Groq Model ID (e.g., 'llama-3.3-70b-versatile')."
204
+ },
205
+ "openrouter_key": {
206
+ "type": "string",
207
+ "description": "Optional OpenRouter API Key. Required if model_provider is 'openrouter' and the server has no key configured."
208
+ },
209
+ "openrouter_model": {
210
+ "type": "string",
211
+ "description": "Optional OpenRouter Model ID (e.g., 'openrouter/free')."
212
+ },
213
+ "firecrawl_key": {
214
+ "type": "string",
215
+ "description": "Optional Firecrawl API Key."
216
+ }
217
+ },
218
+ "required": ["scraped_text", "use_case"]
219
+ }
220
+ }
221
+ ]
222
+ response = {
223
+ "jsonrpc": "2.0",
224
+ "id": req_id,
225
+ "result": {
226
+ "tools": tools
227
+ }
228
+ }
229
+ send_response(response)
230
+
231
+ # 3. Tool Execution
232
+ elif method == "tools/call":
233
+ tool_name = params.get("name")
234
+ arguments = params.get("arguments")
235
+
236
+ if not tool_name or not isinstance(tool_name, str):
237
+ send_error(-32602, "Invalid params: missing or invalid tool name", req_id)
238
+ continue
239
+
240
+ if arguments is None:
241
+ arguments = {}
242
+ elif not isinstance(arguments, dict):
243
+ send_response({
244
+ "jsonrpc": "2.0",
245
+ "id": req_id,
246
+ "result": {
247
+ "content": [{"type": "text", "text": "Error: 'arguments' must be a JSON object matching tool schema."}],
248
+ "isError": True
249
+ }
250
+ })
251
+ continue
252
+
253
+ print(f"[MCP Server] Calling tool '{tool_name}' (arguments present: {list(arguments.keys())})", file=sys.stderr)
254
+
255
+ if tool_name == "scrape_url":
256
+ url = arguments.get("url")
257
+ if not url or not isinstance(url, str):
258
+ send_response({
259
+ "jsonrpc": "2.0",
260
+ "id": req_id,
261
+ "result": {
262
+ "content": [{"type": "text", "text": "Error: 'url' parameter is required and must be a string."}],
263
+ "isError": True
264
+ }
265
+ })
266
+ continue
267
+
268
+ try:
269
+ scraped_markdown = scrape_url(url, api_key=arguments.get("api_key"))
270
+ send_response({
271
+ "jsonrpc": "2.0",
272
+ "id": req_id,
273
+ "result": {
274
+ "content": [{"type": "text", "text": scraped_markdown}]
275
+ }
276
+ })
277
+ except Exception as e:
278
+ print(f"[MCP Server] Error in scrape_url: {traceback.format_exc()}", file=sys.stderr)
279
+ send_response({
280
+ "jsonrpc": "2.0",
281
+ "id": req_id,
282
+ "result": {
283
+ "content": [{"type": "text", "text": f"Scrape failed: {str(e)}"}],
284
+ "isError": True
285
+ }
286
+ })
287
+
288
+ elif tool_name == "generate_wrapper":
289
+ scraped_text = arguments.get("scraped_text")
290
+ use_case = arguments.get("use_case")
291
+ language = arguments.get("language", "python")
292
+ model_provider = arguments.get("model_provider", "gemini")
293
+ gemini_key = arguments.get("gemini_key")
294
+ gemini_model = arguments.get("gemini_model")
295
+ groq_key = arguments.get("groq_key")
296
+ groq_model = arguments.get("groq_model")
297
+ openrouter_key = arguments.get("openrouter_key")
298
+ openrouter_model = arguments.get("openrouter_model")
299
+ firecrawl_key = arguments.get("firecrawl_key")
300
+
301
+ if not scraped_text or not isinstance(scraped_text, str) or not use_case or not isinstance(use_case, str):
302
+ send_response({
303
+ "jsonrpc": "2.0",
304
+ "id": req_id,
305
+ "result": {
306
+ "content": [{"type": "text", "text": "Error: 'scraped_text' and 'use_case' parameters must be non-empty strings."}],
307
+ "isError": True
308
+ }
309
+ })
310
+ continue
311
+
312
+ try:
313
+ agent_result = run_agent_workflow(
314
+ scraped_text=scraped_text,
315
+ use_case=use_case,
316
+ language=str(language),
317
+ model_provider=str(model_provider),
318
+ gemini_key=gemini_key,
319
+ gemini_model=gemini_model,
320
+ groq_key=groq_key,
321
+ groq_model=groq_model,
322
+ openrouter_key=openrouter_key,
323
+ openrouter_model=openrouter_model,
324
+ firecrawl_key=firecrawl_key
325
+ )
326
+
327
+ cleaned_result = {
328
+ "success": agent_result.get("test_passed", False),
329
+ "overview": agent_result.get("overview", ""),
330
+ "endpoints": agent_result.get("endpoints", ""),
331
+ "code": agent_result.get("code", ""),
332
+ "tests": agent_result.get("tests", ""),
333
+ "readme": agent_result.get("readme", ""),
334
+ "retry_count": agent_result.get("retry_count", 0),
335
+ "error_logs": agent_result.get("error_logs", "")
336
+ }
337
+
338
+ send_response({
339
+ "jsonrpc": "2.0",
340
+ "id": req_id,
341
+ "result": {
342
+ "content": [{"type": "text", "text": json.dumps(cleaned_result, indent=2)}]
343
+ }
344
+ })
345
+ except Exception as e:
346
+ print(f"[MCP Server] Error in generate_wrapper: {traceback.format_exc()}", file=sys.stderr)
347
+ send_response({
348
+ "jsonrpc": "2.0",
349
+ "id": req_id,
350
+ "result": {
351
+ "content": [{"type": "text", "text": f"Wrapper generation failed: {str(e)}"}],
352
+ "isError": True
353
+ }
354
+ })
355
+ else:
356
+ send_error(-32601, f"Method '{method}' tool '{tool_name}' not found", req_id)
357
+ else:
358
+ send_error(-32601, f"Method '{method}' not found", req_id)
359
+
360
+ except json.JSONDecodeError:
361
+ send_error(-32700, "Parse error: invalid JSON received")
362
+ except Exception as e:
363
+ print(f"[MCP Server] Unexpected exception: {traceback.format_exc()}", file=sys.stderr)
364
+ send_error(-32603, f"Internal error: {str(e)}", req_id)
365
+
366
+ print("[MCP Server] Stdin EOF reached. Exiting server.", file=sys.stderr)
367
+
368
+ if __name__ == "__main__":
369
+ run_mcp_server()
src/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Services package
src/services/executor.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import sys
4
+ import uuid
5
+ import stat
6
+ import shutil
7
+ import subprocess
8
+ from typing import Tuple
9
+
10
+ def remove_readonly(func, path, excinfo):
11
+ """
12
+ On Windows, files can sometimes be marked read-only or locked,
13
+ preventing shutil.rmtree from working. This helper removes
14
+ the read-only attribute and retries the removal.
15
+ """
16
+ try:
17
+ os.chmod(path, stat.S_IWRITE)
18
+ func(path)
19
+ except Exception:
20
+ pass
21
+
22
+ def run_tests(language: str, code: str, tests: str) -> Tuple[bool, str]:
23
+ """
24
+ Writes the wrapper code and unit test code to an isolated temporary sandbox directory,
25
+ runs the language-specific test suite via a subprocess, and returns a tuple
26
+ of (test_passed, console_logs).
27
+ """
28
+ # Create a unique sandbox directory under a root 'temp' folder
29
+ root_temp_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "temp"))
30
+ os.makedirs(root_temp_dir, exist_ok=True)
31
+
32
+ run_id = str(uuid.uuid4())
33
+ sandbox_dir = os.path.join(root_temp_dir, f"run_{run_id}")
34
+ os.makedirs(sandbox_dir, exist_ok=True)
35
+
36
+ language = language.lower().strip()
37
+ timeout_val = 30 if language in ["go", "golang", "java"] else 15
38
+
39
+ code_filename = ""
40
+ test_filename = ""
41
+ run_args = []
42
+ env = os.environ.copy()
43
+
44
+ # Enable Java assertions by default
45
+ if "JAVA_TOOL_OPTIONS" not in env:
46
+ env["JAVA_TOOL_OPTIONS"] = "-ea"
47
+ else:
48
+ env["JAVA_TOOL_OPTIONS"] += " -ea"
49
+
50
+ try:
51
+ if language in ["python", "py"]:
52
+ code_filename = "client.py"
53
+ test_filename = "test_client.py"
54
+ # Add sandbox directory to PYTHONPATH so pytest can import the client module
55
+ env["PYTHONPATH"] = f"{sandbox_dir}{os.pathsep}{env.get('PYTHONPATH', '')}"
56
+ # Use sys.executable to ensure we use the virtual environment's interpreter
57
+ run_args = [sys.executable, "-m", "pytest", "--tb=short", test_filename]
58
+
59
+ elif language in ["javascript", "js"]:
60
+ code_filename = "client.js"
61
+ test_filename = "test_client.test.js"
62
+ run_args = ["node", test_filename]
63
+
64
+ elif language in ["typescript", "ts"]:
65
+ code_filename = "client.ts"
66
+ test_filename = "test_client.test.ts"
67
+
68
+ # Write a basic tsconfig.json so ts-node works consistently
69
+ tsconfig_path = os.path.join(sandbox_dir, "tsconfig.json")
70
+ tsconfig_content = """{
71
+ "compilerOptions": {
72
+ "target": "es2020",
73
+ "module": "commonjs",
74
+ "esModuleInterop": true,
75
+ "strict": false,
76
+ "skipLibCheck": true
77
+ }
78
+ }"""
79
+ with open(tsconfig_path, "w", encoding="utf-8") as f:
80
+ f.write(tsconfig_content)
81
+
82
+ run_args = ["npx", "ts-node", "--transpile-only", test_filename]
83
+
84
+ elif language in ["go", "golang"]:
85
+ code_filename = "client.go"
86
+ test_filename = "client_test.go"
87
+
88
+ # Initialize a temporary go module to prevent module loading errors
89
+ subprocess.run(
90
+ ["go", "mod", "init", "sandbox"],
91
+ cwd=sandbox_dir,
92
+ capture_output=True,
93
+ timeout=5
94
+ )
95
+
96
+ run_args = ["go", "test", "-v", code_filename, test_filename]
97
+
98
+ elif language in ["java"]:
99
+ # Strip package declarations to avoid compile/runtime classpath resolution issues
100
+ code = re.sub(r"^\s*package\s+[\w\.]+;\s*", "", code, flags=re.MULTILINE)
101
+ tests = re.sub(r"^\s*package\s+[\w\.]+;\s*", "", tests, flags=re.MULTILINE)
102
+
103
+ def find_class_name(source: str, default: str) -> str:
104
+ # Matches public/non-public class names safely
105
+ match = re.search(r"(?:public\s+)?class\s+(\w+)", source)
106
+ return match.group(1) if match else default
107
+
108
+ code_classname = find_class_name(code, "MyAPIClient")
109
+ test_classname = find_class_name(tests, "TestClient")
110
+
111
+ code_filename = f"{code_classname}.java"
112
+ test_filename = f"{test_classname}.java"
113
+
114
+ else:
115
+ raise ValueError(f"Unsupported language for self-healing execution: {language}")
116
+
117
+ # Write files to sandbox
118
+ code_path = os.path.join(sandbox_dir, code_filename)
119
+ test_path = os.path.join(sandbox_dir, test_filename)
120
+
121
+ with open(code_path, "w", encoding="utf-8") as f:
122
+ f.write(code)
123
+ with open(test_path, "w", encoding="utf-8") as f:
124
+ f.write(tests)
125
+
126
+ # Execute the subprocess
127
+ if language == "java":
128
+ # 1. Compile Java files
129
+ compile_process = subprocess.run(
130
+ ["javac", code_filename, test_filename],
131
+ cwd=sandbox_dir,
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=timeout_val
135
+ )
136
+
137
+ if compile_process.returncode != 0:
138
+ return False, f"Compilation Error:\n{compile_process.stderr}\nStdout:\n{compile_process.stdout}"
139
+
140
+ # 2. Execute Java test entrypoint
141
+ test_class = test_filename[:-5]
142
+ run_process = subprocess.run(
143
+ ["java", "-ea", test_class],
144
+ cwd=sandbox_dir,
145
+ capture_output=True,
146
+ text=True,
147
+ timeout=timeout_val,
148
+ env=env
149
+ )
150
+
151
+ success = (run_process.returncode == 0)
152
+ logs = f"Stdout:\n{run_process.stdout}\nStderr:\n{run_process.stderr}"
153
+ return success, logs
154
+
155
+ else:
156
+ # On Windows, prepend cmd.exe /c for CMD-based tools like npx
157
+ actual_args = run_args
158
+ if os.name == "nt" and run_args and run_args[0] == "npx":
159
+ actual_args = ["cmd.exe", "/c"] + run_args
160
+
161
+ # Run the command with strict timeout to prevent CPU hung states
162
+ process = subprocess.run(
163
+ actual_args,
164
+ cwd=sandbox_dir,
165
+ capture_output=True,
166
+ text=True,
167
+ timeout=timeout_val,
168
+ env=env
169
+ )
170
+
171
+ success = (process.returncode == 0)
172
+ logs = f"Stdout:\n{process.stdout}\nStderr:\n{process.stderr}"
173
+ return success, logs
174
+
175
+ except subprocess.TimeoutExpired as e:
176
+ return False, f"Execution timed out after 15 seconds.\nStdout:\n{e.stdout}\nStderr:\n{e.stderr}"
177
+ except Exception as e:
178
+ return False, f"Executor encountered an internal exception: {str(e)}"
179
+ finally:
180
+ # Clean up sandbox directory using the Windows-resilient onerror handler
181
+ try:
182
+ shutil.rmtree(sandbox_dir, onerror=remove_readonly)
183
+ except Exception:
184
+ pass
src/services/scraper.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import requests
3
+ from typing import Optional
4
+ from urllib.parse import urlparse
5
+ from src.config import settings
6
+
7
+ def scrape_url(url: str, api_key: Optional[str] = None) -> str:
8
+ """
9
+ Scrapes the target URL using Firecrawl API and returns the markdown content.
10
+ If no api_key is provided, it falls back to settings.firecrawl_api_key.
11
+ If both are absent, it operates in Firecrawl Keyless mode (no Authorization header).
12
+
13
+ Includes security scheme validation and exponential backoff retry for HTTP 429 (Rate Limits).
14
+ """
15
+ # Security Validation: Enforce http/https to prevent internal file or protocol attacks
16
+ parsed = urlparse(url)
17
+ if parsed.scheme not in ["http", "https"]:
18
+ raise ValueError("Invalid URL protocol. Only HTTP and HTTPS schemes are supported.")
19
+
20
+ endpoint = "https://api.firecrawl.dev/v2/scrape"
21
+
22
+ headers = {
23
+ "Content-Type": "application/json"
24
+ }
25
+
26
+ # Use the passed key, or fallback to the environment configuration
27
+ key_to_use = api_key or settings.firecrawl_api_key
28
+ if key_to_use:
29
+ headers["Authorization"] = f"Bearer {key_to_use}"
30
+
31
+ payload = {
32
+ "url": url,
33
+ "formats": ["markdown"]
34
+ }
35
+
36
+ max_retries = 3
37
+ backoff = 1.0
38
+
39
+ for attempt in range(max_retries):
40
+ try:
41
+ response = requests.post(endpoint, json=payload, headers=headers, timeout=30)
42
+
43
+ # Handle rate limiting with backoff
44
+ if response.status_code == 429:
45
+ if attempt == max_retries - 1:
46
+ raise RuntimeError("Firecrawl rate limit exceeded. Max retries reached.")
47
+ time.sleep(backoff)
48
+ backoff *= 2
49
+ continue
50
+
51
+ if response.status_code == 401:
52
+ raise ValueError("Firecrawl authentication failed. Please check your API key.")
53
+
54
+ response.raise_for_status()
55
+
56
+ data = response.json()
57
+ if not data.get("success"):
58
+ error_msg = data.get("error", "Unknown error occurred during Firecrawl scraping.")
59
+ raise ValueError(f"Firecrawl scraping failed: {error_msg}")
60
+
61
+ markdown_content = data.get("data", {}).get("markdown", "")
62
+ if not markdown_content:
63
+ raise ValueError("Firecrawl succeeded but returned empty markdown content.")
64
+
65
+ return markdown_content
66
+
67
+ except requests.exceptions.RequestException as e:
68
+ if attempt == max_retries - 1:
69
+ raise RuntimeError(f"Network request to Firecrawl failed after {max_retries} attempts: {str(e)}")
70
+ time.sleep(backoff)
71
+ backoff *= 2
72
+
73
+ raise RuntimeError("Firecrawl scraping failed due to retry exhaustion.")