jens-l commited on
Commit
dabb6ef
·
1 Parent(s): 90df85c

Added coding and testing agents

Browse files

- Added `GradioCodingAgent` for implementing applications based on planning results.
- Introduced `GradioTestingAgent` for validating application functionality and performance.
- Updated `app.py` to integrate both coding and testing agents, allowing for a seamless development workflow.
- Enhanced `.env.example` with new settings for coding and testing agents.
- Improved project structure and dependencies in `pyproject.toml`.
- Added utility functions in `utils.py` for file handling.
- Created tests for coding and testing agents to ensure functionality and reliability.
- Updated README with new features and setup instructions.

Files changed (13) hide show
  1. .env.example +10 -0
  2. .gitignore +2 -0
  3. README.md +184 -78
  4. app.py +134 -56
  5. coding_agent.py +394 -0
  6. pyproject.toml +12 -1
  7. sandbox/app.py +0 -69
  8. settings.py +82 -2
  9. test_coding_agent.py +123 -0
  10. test_testing_agent.py +284 -0
  11. testing_agent.py +615 -0
  12. utils.py +29 -0
  13. uv.lock +310 -2
.env.example CHANGED
@@ -10,3 +10,13 @@ GRADIO_DEBUG=true
10
  # Planning Agent Settings
11
  PLANNING_VERBOSITY=1
12
  MAX_PLANNING_STEPS=10
 
 
 
 
 
 
 
 
 
 
 
10
  # Planning Agent Settings
11
  PLANNING_VERBOSITY=1
12
  MAX_PLANNING_STEPS=10
13
+
14
+ # Coding Agent Settings
15
+ CODING_VERBOSITY=2
16
+ MAX_CODING_STEPS=20
17
+ CODE_MODEL_ID=Qwen/Qwen2.5-Coder-32B-Instruct
18
+
19
+ # Testing Agent Settings
20
+ TESTING_VERBOSITY=2
21
+ MAX_TESTING_STEPS=15
22
+ TEST_MODEL_ID=Qwen/Qwen2.5-Coder-32B-Instruct
.gitignore CHANGED
@@ -279,3 +279,5 @@ screenshots/
279
  *.bak
280
  *.backup
281
  *.old
 
 
 
279
  *.bak
280
  *.backup
281
  *.old
282
+
283
+ sandbox/
README.md CHANGED
@@ -1,121 +1,227 @@
1
  # 💗 Likable
2
 
3
- A Gradio application builder powered by Smolagents CodeAgent for intelligent planning.
4
 
5
- ## Features
6
 
7
- - **AI-Powered Planning**: Uses Smolagents CodeAgent to create comprehensive plans for Gradio applications
8
- - **Interactive Chat Interface**: Describe what you want to build in natural language
9
- - **Structured Planning Output**: Get detailed action plans, implementation strategies, and testing approaches
10
- - **Component Analysis**: Automatically identifies required Gradio components and dependencies
11
- - **Preview & Code Views**: Switch between live preview and generated code
12
- - **Environment-based Configuration**: Flexible configuration via environment variables
13
 
14
- ## Setup
 
 
 
 
 
 
15
 
16
- 1. **Install dependencies:**
 
 
 
 
 
 
 
 
 
 
17
  ```bash
18
- uv install
 
19
  ```
20
 
21
- 2. **Configure environment variables:**
22
  ```bash
23
- # Copy the example environment file
24
- cp .env.example .env
25
-
26
- # Edit .env with your configuration
27
- # At minimum, set your API_KEY
28
  ```
29
 
30
- 3. **Set up your API_KEY:**
31
-
32
- Add it to your `.env` file:
33
- ```
34
- API_KEY=your_actual_key_here
35
  ```
36
 
37
- 4. **Run the application:**
38
  ```bash
39
- python app.py
40
  ```
41
 
42
- ## Configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- The application uses environment variables for configuration. See `.env.example` for all available options:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- ### Core Configuration
47
- - `API_KEY`: Your API key (required)
48
- - `MODEL_ID`: Model to use for planning (default: Qwen/Qwen2.5-Coder-32B-Instruct)
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- ### Application Settings
51
- - `GRADIO_HOST`: Host to bind Gradio server (default: 127.0.0.1)
52
- - `GRADIO_PORT`: Port for Gradio server (default: 7860)
53
- - `GRADIO_DEBUG`: Enable debug mode (default: true)
54
 
55
- ### Planning Agent Settings
56
- - `PLANNING_VERBOSITY`: Agent verbosity level 0-2 (default: 1)
57
- - `MAX_PLANNING_STEPS`: Maximum planning steps (default: 10)
58
 
59
- ### Test Your Configuration
60
  ```bash
61
- # View current configuration
62
- python settings.py
63
 
64
- # Test the planning agent
65
- python test_planning_agent.py
66
 
67
- # Interactive demo
68
- python test_planning_agent.py demo
69
  ```
70
 
71
- ## Planning Agent
72
 
73
- The core of Likable is the `GradioPlanningAgent` which uses Smolagents to:
74
 
75
- - Analyze your application requirements
76
- - Create detailed action plans
77
- - Suggest appropriate Gradio components
78
- - Plan implementation strategies
79
- - Design testing approaches
80
- - Estimate complexity and dependencies
81
 
82
- ### Using the Planning Agent Directly
83
 
84
- ```python
85
- from planning_agent import GradioPlanningAgent
 
 
 
86
 
87
- # Uses configuration from settings.py (loads from .env)
88
- agent = GradioPlanningAgent()
89
- result = agent.plan_application("Create a simple calculator app")
 
 
 
90
 
91
- print(agent.format_plan_as_markdown(result))
 
92
  ```
93
 
94
- ## Project Structure
95
 
96
- ```
97
- likable/
98
- ├── app.py # Main Gradio application
99
- ├── planning_agent.py # Smolagents CodeAgent for planning
100
- ├── settings.py # Configuration management
101
- ├── test_planning_agent.py # Test script and examples
102
- ├── .env.example # Environment variables template
103
- ├── pyproject.toml # Project dependencies
104
- └── README.md # This file
105
- ```
106
 
107
- ## Environment Requirements
 
 
 
 
108
 
109
- - Python 3.12+
110
- - Inference API key
111
- - Internet connection for model inference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- ## Dependencies
114
 
115
- - `gradio` - Web UI framework
116
- - `smolagents` - AI agent framework
117
- - `python-dotenv` - Environment variable management
118
 
119
  ---
120
 
121
- *Built with ❤️ using Gradio and Smolagents*
 
1
  # 💗 Likable
2
 
3
+ **AI-powered Gradio app builder that plans and implements complete applications**
4
 
5
+ Likable is an intelligent development assistant that takes natural language descriptions of applications and turns them into fully functional Gradio apps with proper project structure, dependencies, and documentation.
6
 
7
+ ## Features
 
 
 
 
 
8
 
9
+ - **🎯 Intelligent Planning**: Uses AI to create comprehensive application plans
10
+ - **⚡ Automated Implementation**: Converts plans into working code with proper structure
11
+ - **📦 Project Management**: Sets up proper Python projects with `uv` package management
12
+ - **🔄 Iterative Development**: Refines implementations until completion
13
+ - **🎨 Live Preview**: Real-time preview of generated applications
14
+ - **📝 Code Editor**: Built-in code editor for manual adjustments
15
+ - **🛠️ Complete Setup**: Handles dependencies, README, and project structure
16
 
17
+ ## 🚀 Quick Start
18
+
19
+ ### Prerequisites
20
+
21
+ - Python 3.12+
22
+ - `uv` package manager
23
+ - API key for LLM service (HuggingFace, OpenAI, etc.)
24
+
25
+ ### Installation
26
+
27
+ 1. **Clone the repository**:
28
  ```bash
29
+ git clone https://github.com/yourusername/likable.git
30
+ cd likable
31
  ```
32
 
33
+ 2. **Install dependencies**:
34
  ```bash
35
+ uv sync
 
 
 
 
36
  ```
37
 
38
+ 3. **Set up environment variables**:
39
+ ```bash
40
+ cp .env.example .env
41
+ # Edit .env with your API keys and configuration
 
42
  ```
43
 
44
+ 4. **Run the application**:
45
  ```bash
46
+ uv run python app.py
47
  ```
48
 
49
+ 5. **Open your browser** to `http://localhost:7860`
50
+
51
+ ## 📋 Environment Variables
52
+
53
+ Create a `.env` file with the following variables:
54
+
55
+ ```env
56
+ # API Configuration for LLM Services
57
+ API_KEY=your_api_key_here
58
+ API_BASE_URL= # Optional: for custom LLM services
59
+ MODEL_ID=Qwen/Qwen2.5-Coder-32B-Instruct
60
+ CODE_MODEL_ID=Qwen/Qwen2.5-Coder-32B-Instruct # Can be different from planning model
61
+
62
+ # Gradio Configuration
63
+ GRADIO_HOST=127.0.0.1
64
+ GRADIO_PORT=7860
65
+ GRADIO_DEBUG=false
66
 
67
+ # Agent Settings
68
+ PLANNING_VERBOSITY=1
69
+ MAX_PLANNING_STEPS=10
70
+ CODING_VERBOSITY=2
71
+ MAX_CODING_STEPS=20
72
+ ```
73
+
74
+ ## 🏗️ Architecture
75
+
76
+ Likable uses a two-agent system:
77
+
78
+ ### 1. Planning Agent (`planning_agent.py`)
79
+ - **Purpose**: Analyzes user requirements and creates comprehensive plans
80
+ - **Technology**: Smolagents with LiteLLM integration
81
+ - **Output**: Structured planning results with:
82
+ - Action plans
83
+ - Implementation strategies
84
+ - Testing approaches
85
+ - Required Gradio components
86
+ - Dependencies list
87
+ - Complexity estimation
88
+
89
+ ### 2. Coding Agent (`coding_agent.py`)
90
+ - **Purpose**: Implements the planned application with proper project structure
91
+ - **Technology**: Smolagents CodeAgent with file operations
92
+ - **Features**:
93
+ - Sets up `uv` project structure
94
+ - Installs dependencies automatically
95
+ - Creates comprehensive README files
96
+ - Implements all planned features
97
+ - Performs iterative refinement
98
+
99
+ ## 🎯 How It Works
100
+
101
+ 1. **User Input**: Describe your desired application in natural language
102
+ 2. **Planning Phase**: AI analyzes requirements and creates detailed plans
103
+ 3. **Implementation Phase**: Coding agent creates complete project structure
104
+ 4. **Quality Assurance**: Iterative refinement ensures completeness
105
+ 5. **Deployment Ready**: Generated apps are immediately runnable
106
+
107
+ ## 📁 Project Structure
108
 
109
+ ```
110
+ likable/
111
+ ├── app.py # Main Gradio interface
112
+ ├── planning_agent.py # AI planning agent
113
+ ├── coding_agent.py # AI coding agent
114
+ ├── settings.py # Configuration management
115
+ ├── test_planning_agent.py # Planning agent tests
116
+ ├── test_coding_agent.py # Coding agent tests
117
+ ├── pyproject.toml # Project dependencies
118
+ ├── .env.example # Environment template
119
+ └── sandbox/ # Generated applications
120
+ └── gradio_app/ # Latest generated app
121
+ ├── app.py # Main application
122
+ ├── README.md # Documentation
123
+ └── pyproject.toml # App dependencies
124
+ ```
125
 
126
+ ## 🧪 Testing
 
 
 
127
 
128
+ Run tests to verify functionality:
 
 
129
 
 
130
  ```bash
131
+ # Test planning agent
132
+ uv run python test_planning_agent.py
133
 
134
+ # Test coding agent
135
+ uv run python test_coding_agent.py
136
 
137
+ # Test settings
138
+ uv run python settings.py
139
  ```
140
 
141
+ ## 🔧 Development
142
 
143
+ ### Adding New Features
144
 
145
+ 1. **Planning Agent Extensions**: Modify `planning_agent.py` to enhance planning capabilities
146
+ 2. **Coding Agent Tools**: Add new tools to `coding_agent.py` for specialized functionality
147
+ 3. **UI Improvements**: Update `app.py` for better user experience
 
 
 
148
 
149
+ ### Code Quality
150
 
151
+ The project uses:
152
+ - **Ruff**: Linting and formatting
153
+ - **Pre-commit**: Git hooks for quality assurance
154
+ - **Type hints**: For better code documentation
155
+ - **Docstrings**: Comprehensive documentation
156
 
157
+ ```bash
158
+ # Run linting
159
+ uv run ruff check
160
+
161
+ # Format code
162
+ uv run ruff format
163
 
164
+ # Install pre-commit hooks
165
+ uv run pre-commit install
166
  ```
167
 
168
+ ## 🎨 Example Applications
169
 
170
+ Likable can create various types of Gradio applications:
 
 
 
 
 
 
 
 
 
171
 
172
+ - **Text Processing**: Translation, summarization, analysis
173
+ - **Image Tools**: Generation, editing, classification
174
+ - **Data Applications**: Visualization, analysis, dashboards
175
+ - **AI Interfaces**: Chatbots, question-answering systems
176
+ - **Utility Apps**: Converters, calculators, tools
177
 
178
+ ## 🤝 Contributing
179
+
180
+ 1. Fork the repository
181
+ 2. Create a feature branch: `git checkout -b feature-name`
182
+ 3. Make your changes and add tests
183
+ 4. Run the test suite: `uv run python -m pytest`
184
+ 5. Submit a pull request
185
+
186
+ ## 📄 License
187
+
188
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
189
+
190
+ ## 🙏 Acknowledgments
191
+
192
+ - **Smolagents**: For the excellent agent framework
193
+ - **Gradio**: For the amazing UI framework
194
+ - **LiteLLM**: For seamless LLM integration
195
+ - **UV**: For fast Python package management
196
+
197
+ ## 🐛 Troubleshooting
198
+
199
+ ### Common Issues
200
+
201
+ 1. **API Key Errors**:
202
+ - Ensure your API key is set in `.env`
203
+ - Check API rate limits and quotas
204
+
205
+ 2. **UV Not Found**:
206
+ ```bash
207
+ # Install uv
208
+ curl -LsSf https://astral.sh/uv/install.sh | sh
209
+ ```
210
+
211
+ 3. **Project Setup Failures**:
212
+ - Ensure you have write permissions in the project directory
213
+ - Check that `uv` is properly installed and accessible
214
+
215
+ 4. **Agent Initialization Issues**:
216
+ - Verify your model ID is correct
217
+ - Check network connectivity for API calls
218
 
219
+ ### Getting Help
220
 
221
+ - Open an issue on GitHub
222
+ - Check the examples in `test_*.py` files
223
+ - Review the agent documentation in source code
224
 
225
  ---
226
 
227
+ **Happy Building! 🚀**
app.py CHANGED
@@ -4,13 +4,16 @@ import sys
4
 
5
  import gradio as gr
6
 
 
7
  from planning_agent import GradioPlanningAgent
8
  from settings import settings
 
9
 
10
  gr.NO_RELOAD = False
11
 
12
- # Initialize the planning agent globally
13
  planning_agent = None
 
14
 
15
 
16
  def get_planning_agent():
@@ -25,14 +28,28 @@ def get_planning_agent():
25
  return planning_agent
26
 
27
 
28
- # Enhanced AI response using the planning agent
29
- def ai_response_with_planning(message, history):
30
- """Generate AI response using the planning agent for actual planning."""
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- agent = get_planning_agent()
 
33
 
34
- if agent is None:
35
- # Fallback to mock response if agent fails to initialize
36
  response = (
37
  "Sorry, the planning agent is not available. "
38
  "Please check your API_KEY environment variable."
@@ -41,11 +58,26 @@ def ai_response_with_planning(message, history):
41
  history.append({"role": "assistant", "content": response})
42
  return history, ""
43
 
 
 
 
 
 
 
 
 
 
 
44
  try:
45
- # Use the planning agent for actual planning
46
- planning_result = agent.plan_application(message)
 
 
 
 
 
47
 
48
- # Format the response with key insights
49
  action_summary = (
50
  planning_result.action_plan[:300] + "..."
51
  if len(planning_result.action_plan) > 300
@@ -59,7 +91,7 @@ def ai_response_with_planning(message, history):
59
  [f"• {dep}" for dep in planning_result.dependencies[:5]]
60
  )
61
 
62
- response = f"""I'll help you plan that application! Here's what I've analyzed:
63
 
64
  **Complexity**: {planning_result.estimated_complexity}
65
 
@@ -72,31 +104,70 @@ def ai_response_with_planning(message, history):
72
  **High-Level Action Plan**:
73
  {action_summary}
74
 
75
- I've created a comprehensive plan including implementation details and testing \
76
- strategy. Check the detailed view for the complete plan!"""
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # Store the full planning result for later use
79
- # You could save this to a session state or database
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  except Exception as e:
82
- response = (
83
- f"I encountered an error while planning: {str(e)}. "
84
  "Let me try a simpler approach..."
85
  )
 
86
 
87
- history.append({"role": "user", "content": message})
88
- history.append({"role": "assistant", "content": response})
89
  return history, ""
90
 
91
 
92
- def load_file(path):
93
- if path is None:
94
- return ""
95
- # path is a string like "subdir/example.py"
96
- with open(path, encoding="utf-8") as f:
97
- return f.read()
98
-
99
-
100
  def save_file(path, new_text):
101
  if path is None:
102
  gr.Warning("⚠️ No file selected.")
@@ -109,13 +180,15 @@ def save_file(path, new_text):
109
 
110
 
111
  def load_and_render_app():
112
- """Load and render the Gradio app from sandbox/app.py"""
113
- app_path = "sandbox/app.py"
114
 
115
  if not os.path.exists(app_path):
116
  return gr.HTML(
117
- "<div style='padding: 20px; color: red;'>❌ No app.py found in \
118
- sandbox directory</div>"
 
 
119
  )
120
 
121
  try:
@@ -127,9 +200,10 @@ sandbox directory</div>"
127
  spec = importlib.util.spec_from_loader("dynamic_app", loader=None)
128
  module = importlib.util.module_from_spec(spec)
129
 
130
- # Add current directory to sys.path if not already there
131
- if os.getcwd() not in sys.path:
132
- sys.path.insert(0, os.getcwd())
 
133
 
134
  # Execute the code in the module's namespace
135
  exec(app_code, module.__dict__)
@@ -153,8 +227,10 @@ sandbox directory</div>"
153
 
154
  if app_instance is None:
155
  return gr.HTML(
156
- "<div style='padding: 20px; color: orange;'>⚠️ No Gradio app found. \
157
- Make sure your app.py creates a Gradio Blocks or Interface object.</div>"
 
 
158
  )
159
 
160
  # Return the app instance to be rendered
@@ -162,17 +238,17 @@ Make sure your app.py creates a Gradio Blocks or Interface object.</div>"
162
 
163
  except Exception as e:
164
  error_html = f"""
165
- <div style='padding: 20px; color: red; font-family: monospace;'>
166
- ❌ Error loading app:<br>
167
- <pre style='background: #f5f5f5; padding: 10px; margin-top: 10px; \
168
  border-radius: 4px;'>{str(e)}</pre>
169
- </div>
170
- """
171
  return gr.HTML(error_html)
172
 
173
 
174
- # Create the main Lovable-style UI
175
- def create_lovable_ui():
176
  with gr.Blocks(
177
  title="💗Likable",
178
  theme=gr.themes.Soft(),
@@ -180,9 +256,10 @@ def create_lovable_ui():
180
  fill_width=True,
181
  ) as demo:
182
  gr.Markdown("# 💗Likable")
183
- # gr.Markdown(
184
- # "*It's almost Lovable - Build Gradio apps using only a chat interface*"
185
- # )
 
186
 
187
  with gr.Row(elem_classes="main-container"):
188
  # Left side - Chat Interface
@@ -190,17 +267,17 @@ def create_lovable_ui():
190
  chatbot = gr.Chatbot(
191
  show_copy_button=True,
192
  avatar_images=(None, "🤖"),
193
- bubble_full_width=False,
194
  height="75vh",
195
  )
196
 
197
  with gr.Row():
198
  msg_input = gr.Textbox(
199
- placeholder="Describe what you want to build...",
200
  scale=4,
201
  container=False,
202
  )
203
- send_btn = gr.Button("Send", scale=1, variant="primary")
204
 
205
  # Right side - Preview/Code Toggle
206
  with gr.Column(scale=4, elem_classes="preview-container"):
@@ -225,12 +302,12 @@ def create_lovable_ui():
225
  )
226
  code_editor = gr.Code(
227
  scale=3,
228
- value=load_file("sandbox/app.py"),
 
 
229
  language="python",
230
  visible=True,
231
  interactive=True,
232
- # lines=27,
233
- # max_lines=27,
234
  autocomplete=True,
235
  )
236
 
@@ -248,15 +325,16 @@ def create_lovable_ui():
248
  outputs=[refresh_trigger],
249
  )
250
 
251
- # Event handlers for chat
 
252
  msg_input.submit(
253
- ai_response_with_planning,
254
  inputs=[msg_input, chatbot],
255
  outputs=[chatbot, msg_input],
256
  )
257
 
258
  send_btn.click(
259
- ai_response_with_planning,
260
  inputs=[msg_input, chatbot],
261
  outputs=[chatbot, msg_input],
262
  )
@@ -265,6 +343,6 @@ def create_lovable_ui():
265
 
266
 
267
  if __name__ == "__main__":
268
- demo = create_lovable_ui()
269
  gradio_config = settings.get_gradio_config()
270
  demo.launch(**gradio_config)
 
4
 
5
  import gradio as gr
6
 
7
+ from coding_agent import GradioCodingAgent
8
  from planning_agent import GradioPlanningAgent
9
  from settings import settings
10
+ from utils import load_file
11
 
12
  gr.NO_RELOAD = False
13
 
14
+ # Initialize the agents globally
15
  planning_agent = None
16
+ coding_agent = None
17
 
18
 
19
  def get_planning_agent():
 
28
  return planning_agent
29
 
30
 
31
+ def get_coding_agent():
32
+ """Get or initialize the coding agent (lazy loading)."""
33
+ global coding_agent
34
+ if coding_agent is None:
35
+ try:
36
+ coding_agent = GradioCodingAgent()
37
+ except Exception as e:
38
+ print(f"Error initializing coding agent: {e}")
39
+ return None
40
+ return coding_agent
41
+
42
+
43
+ # Enhanced AI response using both planning and coding agents
44
+ def ai_response_with_planning_and_coding(message, history):
45
+ """Generate AI response using the planning agent for planning and \
46
+ coding agent for implementation."""
47
 
48
+ planning_agent_instance = get_planning_agent()
49
+ coding_agent_instance = get_coding_agent()
50
 
51
+ if planning_agent_instance is None:
52
+ # Fallback to mock response if planning agent fails to initialize
53
  response = (
54
  "Sorry, the planning agent is not available. "
55
  "Please check your API_KEY environment variable."
 
58
  history.append({"role": "assistant", "content": response})
59
  return history, ""
60
 
61
+ if coding_agent_instance is None:
62
+ # Fallback if coding agent fails to initialize
63
+ response = (
64
+ "Sorry, the coding agent is not available. "
65
+ "Planning is available but implementation will be limited."
66
+ )
67
+ history.append({"role": "user", "content": message})
68
+ history.append({"role": "assistant", "content": response})
69
+ return history, ""
70
+
71
  try:
72
+ # Step 1: Use the planning agent for planning
73
+ history.append({"role": "user", "content": message})
74
+ history.append(
75
+ {"role": "assistant", "content": "🎯 Starting to plan your application..."}
76
+ )
77
+
78
+ planning_result = planning_agent_instance.plan_application(message)
79
 
80
+ # Format the planning response
81
  action_summary = (
82
  planning_result.action_plan[:300] + "..."
83
  if len(planning_result.action_plan) > 300
 
91
  [f"• {dep}" for dep in planning_result.dependencies[:5]]
92
  )
93
 
94
+ planning_response = f""" **Planning Complete!**
95
 
96
  **Complexity**: {planning_result.estimated_complexity}
97
 
 
104
  **High-Level Action Plan**:
105
  {action_summary}
106
 
107
+ 🚀 **Now starting implementation...**"""
108
+
109
+ history.append({"role": "assistant", "content": planning_response})
110
+
111
+ # Step 2: Use the coding agent for implementation
112
+ history.append(
113
+ {
114
+ "role": "assistant",
115
+ "content": "⚡ Implementing your application with proper \
116
+ project structure...",
117
+ }
118
+ )
119
+
120
+ coding_result = coding_agent_instance.iterative_implementation(planning_result)
121
 
122
+ # Format the implementation response
123
+ if coding_result.success:
124
+ implementation_response = f"""✅ **Implementation Complete!**
125
+
126
+ **Project Created**: `{coding_result.project_path}`
127
+ **Features Implemented**: {len(coding_result.implemented_features)} components
128
+ **Status**: Ready to run!
129
+
130
+ Your Gradio application has been created with:
131
+ - Proper `uv` project structure
132
+ - All required dependencies installed
133
+ - Complete README.md with usage instructions
134
+ - Functional app.py with all requested features
135
+
136
+ You can view and test your app in the **Preview** tab, or check the code in \
137
+ the **Code** tab.
138
+
139
+ To run locally: `cd {coding_result.project_path} && uv run python app.py`"""
140
+
141
+ if coding_result.remaining_tasks:
142
+ implementation_response += f"\n\n**Remaining Tasks**: \
143
+ {chr(10).join([f'• {task}' for task in coding_result.remaining_tasks])}"
144
+
145
+ else:
146
+ implementation_response = f"""⚠️ **Implementation Partially Complete**
147
+
148
+ **Project Path**: `{coding_result.project_path}`
149
+ **Issues Encountered**: {len(coding_result.error_messages)} errors
150
+
151
+ **Error Messages**:
152
+ {chr(10).join([f'• {error}' for error in coding_result.error_messages])}
153
+
154
+ **Remaining Tasks**:
155
+ {chr(10).join([f'• {task}' for task in coding_result.remaining_tasks])}
156
+
157
+ The project structure has been set up, but some features may need manual completion."""
158
+
159
+ history.append({"role": "assistant", "content": implementation_response})
160
 
161
  except Exception as e:
162
+ error_response = (
163
+ f"I encountered an error during planning and implementation: {str(e)}. "
164
  "Let me try a simpler approach..."
165
  )
166
+ history.append({"role": "assistant", "content": error_response})
167
 
 
 
168
  return history, ""
169
 
170
 
 
 
 
 
 
 
 
 
171
  def save_file(path, new_text):
172
  if path is None:
173
  gr.Warning("⚠️ No file selected.")
 
180
 
181
 
182
  def load_and_render_app():
183
+ """Load and render the Gradio app from sandbox/gradio_app/app.py"""
184
+ app_path = "sandbox/gradio_app/app.py"
185
 
186
  if not os.path.exists(app_path):
187
  return gr.HTML(
188
+ """<div style='padding: 20px; color: red;'>
189
+ ❌ No app.py found in sandbox/gradio_app directory.
190
+ Create an application first using the chat interface.
191
+ </div>"""
192
  )
193
 
194
  try:
 
200
  spec = importlib.util.spec_from_loader("dynamic_app", loader=None)
201
  module = importlib.util.module_from_spec(spec)
202
 
203
+ # Add sandbox directory to sys.path if not already there
204
+ sandbox_path = os.path.abspath("sandbox/gradio_app")
205
+ if sandbox_path not in sys.path:
206
+ sys.path.insert(0, sandbox_path)
207
 
208
  # Execute the code in the module's namespace
209
  exec(app_code, module.__dict__)
 
227
 
228
  if app_instance is None:
229
  return gr.HTML(
230
+ """<div style='padding: 20px; color: orange;'>
231
+ ⚠️ No Gradio app found. Make sure your app.py creates a Gradio Blocks or \
232
+ Interface object.
233
+ </div>"""
234
  )
235
 
236
  # Return the app instance to be rendered
 
238
 
239
  except Exception as e:
240
  error_html = f"""
241
+ <div style='padding: 20px; color: red; font-family: monospace;'>
242
+ ❌ Error loading app:<br>
243
+ <pre style='background: #f5f5f5; padding: 10px; margin-top: 10px; \
244
  border-radius: 4px;'>{str(e)}</pre>
245
+ </div>
246
+ """
247
  return gr.HTML(error_html)
248
 
249
 
250
+ # Create the main Likable UI
251
+ def create_likable_ui():
252
  with gr.Blocks(
253
  title="💗Likable",
254
  theme=gr.themes.Soft(),
 
256
  fill_width=True,
257
  ) as demo:
258
  gr.Markdown("# 💗Likable")
259
+ gr.Markdown(
260
+ "*AI-powered Gradio app builder - Plans and implements \
261
+ complete applications*"
262
+ )
263
 
264
  with gr.Row(elem_classes="main-container"):
265
  # Left side - Chat Interface
 
267
  chatbot = gr.Chatbot(
268
  show_copy_button=True,
269
  avatar_images=(None, "🤖"),
270
+ type="messages",
271
  height="75vh",
272
  )
273
 
274
  with gr.Row():
275
  msg_input = gr.Textbox(
276
+ placeholder="Describe the Gradio app you want to build...",
277
  scale=4,
278
  container=False,
279
  )
280
+ send_btn = gr.Button("Build App", scale=1, variant="primary")
281
 
282
  # Right side - Preview/Code Toggle
283
  with gr.Column(scale=4, elem_classes="preview-container"):
 
302
  )
303
  code_editor = gr.Code(
304
  scale=3,
305
+ value=load_file("sandbox/gradio_app/app.py")
306
+ if os.path.exists("sandbox/gradio_app/app.py")
307
+ else "# No app created yet - use the chat to create one!",
308
  language="python",
309
  visible=True,
310
  interactive=True,
 
 
311
  autocomplete=True,
312
  )
313
 
 
325
  outputs=[refresh_trigger],
326
  )
327
 
328
+ # Event handlers for chat - updated to use the combined planning and
329
+ # coding function
330
  msg_input.submit(
331
+ ai_response_with_planning_and_coding,
332
  inputs=[msg_input, chatbot],
333
  outputs=[chatbot, msg_input],
334
  )
335
 
336
  send_btn.click(
337
+ ai_response_with_planning_and_coding,
338
  inputs=[msg_input, chatbot],
339
  outputs=[chatbot, msg_input],
340
  )
 
343
 
344
 
345
  if __name__ == "__main__":
346
+ demo = create_likable_ui()
347
  gradio_config = settings.get_gradio_config()
348
  demo.launch(**gradio_config)
coding_agent.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Smolagents CodeAgent for implementing Gradio applications.
3
+
4
+ This module provides a specialized coding agent that can:
5
+ - Take a planning result from the planning agent
6
+ - Set up a proper Python Gradio project structure in the sandbox folder
7
+ - Use uv for package management
8
+ - Implement the full plan with proper error handling and iterative development
9
+ - Only exit when the full plan is implemented
10
+ """
11
+
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ from mcp import StdioServerParameters
19
+ from smolagents import LiteLLMModel, MCPClient, ToolCallingAgent
20
+
21
+ from planning_agent import PlanningResult
22
+ from settings import settings
23
+ from utils import load_file
24
+
25
+
26
+ @dataclass
27
+ class CodingResult:
28
+ """Result of the coding agent containing implementation details."""
29
+
30
+ success: bool
31
+ project_path: str
32
+ implemented_features: list[str]
33
+ remaining_tasks: list[str]
34
+ error_messages: list[str]
35
+ final_app_code: str
36
+
37
+
38
+ class GradioCodingAgent:
39
+ """
40
+ A specialized CodeAgent for implementing Gradio applications.
41
+
42
+ This agent takes planning results and creates complete, working
43
+ Gradio applications with proper project structure and dependencies.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ model_id: str | None = None,
49
+ api_base_url: str | None = None,
50
+ api_key: str | None = None,
51
+ verbosity_level: int | None = None,
52
+ max_steps: int | None = None,
53
+ ):
54
+ """
55
+ Initialize the Gradio Coding Agent.
56
+
57
+ Args:
58
+ model_id: Model ID to use for coding (uses settings if None)
59
+ api_base_url: API base URL (uses settings if None)
60
+ api_key: API key (uses settings if None)
61
+ verbosity_level: Level of verbosity for agent output (uses settings if None)
62
+ max_steps: Maximum number of coding steps (uses settings if None)
63
+ """
64
+ # Use settings as defaults, but allow override
65
+ self.model_id = model_id or settings.code_model_id
66
+ self.api_base_url = api_base_url or settings.api_base_url
67
+ self.api_key = api_key or settings.api_key
68
+ verbosity_level = verbosity_level or settings.coding_verbosity
69
+ max_steps = max_steps or settings.max_coding_steps
70
+
71
+ # Initialize the language model for the CodeAgent
72
+ self.model = LiteLLMModel(
73
+ model_id=self.model_id,
74
+ api_base=self.api_base_url,
75
+ api_key=self.api_key,
76
+ )
77
+
78
+ server_parameters = StdioServerParameters(
79
+ command="npx",
80
+ args=[
81
+ "-y",
82
+ "@modelcontextprotocol/server-filesystem",
83
+ "sandbox",
84
+ ],
85
+ )
86
+
87
+ self.mcp_client = MCPClient(server_parameters)
88
+
89
+ tool_collection = self.mcp_client.get_tools()
90
+
91
+ # Initialize the CodeAgent with tools for file operations and project setup
92
+ self.agent = ToolCallingAgent(
93
+ model=self.model,
94
+ tools=tool_collection,
95
+ verbosity_level=verbosity_level,
96
+ max_steps=max_steps,
97
+ )
98
+
99
+ self.sandbox_path = Path("sandbox")
100
+
101
+ # Store the original working directory for cleanup
102
+ self.original_cwd = os.getcwd()
103
+
104
+ def __del__(self):
105
+ """
106
+ Cleanup method called when the instance is about to be destroyed.
107
+
108
+ This method ensures:
109
+ - Working directory is restored to original location
110
+ - Any open resources are properly closed
111
+ - Temporary files are cleaned up if needed
112
+ """
113
+ try:
114
+ # Restore original working directory
115
+ if hasattr(self, "original_cwd") and os.path.exists(self.original_cwd):
116
+ os.chdir(self.original_cwd)
117
+
118
+ if hasattr(self, "mcp_client") and self.mcp_client:
119
+ self.mcp_client.disconnect()
120
+
121
+ except Exception:
122
+ pass
123
+
124
+ def setup_project_structure(self, project_name: str = "gradio_app") -> bool:
125
+ """
126
+ Set up the initial project structure using uv.
127
+
128
+ Args:
129
+ project_name: Name of the project
130
+
131
+ Returns:
132
+ bool: True if setup was successful
133
+ """
134
+ try:
135
+ # Ensure sandbox directory exists and is clean
136
+ if self.sandbox_path.exists():
137
+ shutil.rmtree(self.sandbox_path)
138
+ self.sandbox_path.mkdir(exist_ok=True)
139
+
140
+ # Change to sandbox directory
141
+ os.chdir(self.sandbox_path)
142
+
143
+ # Initialize with uv
144
+ subprocess.run(
145
+ ["uv", "init", project_name],
146
+ capture_output=True,
147
+ text=True,
148
+ check=True,
149
+ )
150
+
151
+ # Change to project directory
152
+ os.chdir(project_name)
153
+
154
+ # Add gradio as a dependency
155
+ subprocess.run(
156
+ ["uv", "add", "gradio"],
157
+ capture_output=True,
158
+ text=True,
159
+ check=True,
160
+ )
161
+
162
+ # Change back to workspace root
163
+ os.chdir("../..")
164
+
165
+ return True
166
+
167
+ except subprocess.CalledProcessError as e:
168
+ print(f"Error setting up project structure: {e}")
169
+ print(f"stdout: {e.stdout}")
170
+ print(f"stderr: {e.stderr}")
171
+ return False
172
+ except Exception as e:
173
+ print(f"Unexpected error setting up project: {e}")
174
+ return False
175
+
176
+ def implement_application(self, planning_result: PlanningResult) -> CodingResult:
177
+ """
178
+ Implement the full Gradio application based on the planning result.
179
+
180
+ Args:
181
+ planning_result: The planning result from the planning agent
182
+
183
+ Returns:
184
+ CodingResult containing implementation details
185
+ """
186
+ # Set up project structure
187
+ project_name = "gradio_app"
188
+ if not self.setup_project_structure(project_name):
189
+ return CodingResult(
190
+ success=False,
191
+ project_path="",
192
+ implemented_features=[],
193
+ remaining_tasks=["Failed to set up project structure"],
194
+ error_messages=["Could not initialize uv project"],
195
+ final_app_code="",
196
+ )
197
+
198
+ project_path = str(self.sandbox_path / project_name)
199
+
200
+ # Create comprehensive prompt for implementation
201
+ gradio_components = chr(10).join(
202
+ [f"- {comp}" for comp in planning_result.gradio_components]
203
+ )
204
+ dependencies = chr(10).join(
205
+ [f"- {dep}" for dep in planning_result.dependencies if dep != "gradio"]
206
+ )
207
+
208
+ # Create the user prompt for the specific implementation
209
+ user_prompt = f"""You are an expert Python developer and Gradio \
210
+ application architect.
211
+
212
+ Your task is to implement a complete, working Gradio application based on \
213
+ the provided plan.
214
+
215
+ PROJECT SETUP:
216
+ - You are working in the directory: {project_path}
217
+ - The project has been initialized with `uv` and `gradio` is already installed
218
+ - Use proper Python project structure with a main app.py file
219
+ - Add any additional dependencies needed using `uv add package_name`
220
+
221
+ IMPLEMENTATION REQUIREMENTS:
222
+ 1. Create a complete, functional Gradio application in app.py
223
+ 2. Follow the provided action plan and implementation plan exactly
224
+ 3. Implement ALL gradio components mentioned in the plan
225
+ 4. Add proper error handling and user feedback
226
+ 5. Create a comprehensive README.md with usage instructions
227
+ 6. Add all required dependencies to the project using `uv add`
228
+ 7. Make sure the app can be run with `uv run python app.py`
229
+ 8. Test the implementation and fix any issues
230
+
231
+ QUALITY STANDARDS:
232
+ - Write clean, well-documented code
233
+ - Use proper type hints where appropriate
234
+ - Follow Python best practices
235
+ - Add docstrings to functions and classes
236
+ - Handle edge cases and errors gracefully
237
+ - Make the UI intuitive and user-friendly
238
+ - When using multiline strings within multiline strings, properly escape them \
239
+ using triple quotes
240
+ Example: Instead of using f\"\"\"...\"\"\", use f'''...''' or escape inner quotes \
241
+ like f\"\"\"...\\\"\\\"\\\"...\\\"\\\"\\\"...\"\"\"
242
+
243
+ GRADIO COMPONENTS TO IMPLEMENT:
244
+ {gradio_components}
245
+
246
+ DEPENDENCIES TO ADD:
247
+ {dependencies}
248
+
249
+ ACTION PLAN TO FOLLOW:
250
+ {planning_result.action_plan}
251
+
252
+ IMPLEMENTATION PLAN TO FOLLOW:
253
+ {planning_result.implementation_plan}
254
+
255
+ TESTING PLAN TO CONSIDER:
256
+ {planning_result.testing_plan}
257
+
258
+ You must implement the complete application and ensure it works properly.
259
+ Use subprocess to run `uv add` commands to install any needed packages.
260
+ Create all necessary files and make sure the application runs without errors.
261
+
262
+ Please implement the complete Gradio application based on the planning result.
263
+
264
+ The application should be fully functional and implement all the features
265
+ described in the plans.
266
+
267
+ Working directory: {project_path}
268
+
269
+ Please:
270
+ 1. Start by creating/updating the README.md file with project description
271
+ and usage instructions
272
+ 2. Add any additional dependencies needed using `uv add package_name`
273
+ 3. Create the complete app.py file with all the Gradio components and
274
+ functionality
275
+ 4. Test the implementation to ensure it works
276
+ 5. Fix any issues that arise during testing
277
+
278
+ Make sure the final application is complete and functional.
279
+ /no_think
280
+ """
281
+
282
+ try:
283
+ # Run the coding agent to implement the application
284
+ self.agent.run(
285
+ user_prompt,
286
+ additional_args={
287
+ "current_app_py": load_file(str(Path(project_path) / "app.py")),
288
+ },
289
+ )
290
+
291
+ # Check if the implementation was successful
292
+ app_file = Path(project_path) / "app.py"
293
+ if app_file.exists():
294
+ with open(app_file, encoding="utf-8") as f:
295
+ final_app_code = f.read()
296
+
297
+ return CodingResult(
298
+ success=True,
299
+ project_path=project_path,
300
+ implemented_features=planning_result.gradio_components,
301
+ remaining_tasks=[],
302
+ error_messages=[],
303
+ final_app_code=final_app_code,
304
+ )
305
+ else:
306
+ return CodingResult(
307
+ success=False,
308
+ project_path=project_path,
309
+ implemented_features=[],
310
+ remaining_tasks=["Main app.py file was not created"],
311
+ error_messages=["Implementation failed to create app.py"],
312
+ final_app_code="",
313
+ )
314
+
315
+ except Exception as e:
316
+ return CodingResult(
317
+ success=False,
318
+ project_path=project_path,
319
+ implemented_features=[],
320
+ remaining_tasks=["Complete implementation"],
321
+ error_messages=[f"Coding agent error: {str(e)}"],
322
+ final_app_code="",
323
+ )
324
+
325
+ def iterative_implementation(
326
+ self, planning_result: PlanningResult, max_iterations: int = 3
327
+ ) -> CodingResult:
328
+ """
329
+ Implement the application with iterative refinement.
330
+
331
+ Args:
332
+ planning_result: The planning result from the planning agent
333
+ max_iterations: Maximum number of implementation iterations
334
+
335
+ Returns:
336
+ CodingResult containing final implementation details
337
+ """
338
+ last_result = None
339
+
340
+ for iteration in range(max_iterations):
341
+ print(f"🔄 Implementation iteration {iteration + 1}/{max_iterations}")
342
+
343
+ # Implement or refine the application
344
+ result = self.implement_application(planning_result)
345
+
346
+ if result.success and not result.remaining_tasks:
347
+ print(f"✅ Implementation successful in {iteration + 1} iteration(s)")
348
+ return result
349
+
350
+ last_result = result
351
+
352
+ if iteration < max_iterations - 1:
353
+ print(f"⚠️ Iteration {iteration + 1} incomplete. Refining...")
354
+ # For subsequent iterations, we could modify the prompt to focus
355
+ # on remaining tasks. This is a simplified version - in practice,
356
+ # you'd want more sophisticated iteration logic
357
+
358
+ print(f"⚠️ Implementation completed with {max_iterations} iterations")
359
+ return last_result or CodingResult(
360
+ success=False,
361
+ project_path="",
362
+ implemented_features=[],
363
+ remaining_tasks=["Complete implementation failed"],
364
+ error_messages=["Maximum iterations reached without completion"],
365
+ final_app_code="",
366
+ )
367
+
368
+
369
+ # Convenience function for the main app
370
+ def create_gradio_coding_agent() -> GradioCodingAgent:
371
+ """Create a GradioCodingAgent with default settings."""
372
+ return GradioCodingAgent()
373
+
374
+
375
+ if __name__ == "__main__":
376
+ # Example usage
377
+ from planning_agent import GradioPlanningAgent
378
+
379
+ # Test with a simple planning result
380
+ planning_agent = GradioPlanningAgent()
381
+ planning_result = planning_agent.plan_application(
382
+ "Create a simple text-to-text translator app"
383
+ )
384
+
385
+ # Create coding agent and implement
386
+ coding_agent = create_gradio_coding_agent()
387
+ coding_result = coding_agent.iterative_implementation(planning_result)
388
+
389
+ print("Coding Result:")
390
+ print(f"Success: {coding_result.success}")
391
+ print(f"Project Path: {coding_result.project_path}")
392
+ print(f"Implemented Features: {coding_result.implemented_features}")
393
+ print(f"Remaining Tasks: {coding_result.remaining_tasks}")
394
+ print(f"Error Messages: {coding_result.error_messages}")
pyproject.toml CHANGED
@@ -5,8 +5,12 @@ description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
 
8
  "gradio>=5.32.0",
9
- "smolagents[litellm]>=1.17.0",
 
 
 
10
  ]
11
 
12
  [dependency-groups]
@@ -36,3 +40,10 @@ quote-style = "double"
36
  indent-style = "space"
37
  skip-magic-trailing-comma = false
38
  line-ending = "auto"
 
 
 
 
 
 
 
 
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
8
+ "duckduckgo-search>=8.0.2",
9
  "gradio>=5.32.0",
10
+ "mcp>=1.9.2",
11
+ "smolagents[litellm,mcp]>=1.17.0",
12
+ "selenium>=4.25.0",
13
+ "requests>=2.32.0",
14
  ]
15
 
16
  [dependency-groups]
 
40
  indent-style = "space"
41
  skip-magic-trailing-comma = false
42
  line-ending = "auto"
43
+
44
+ [tool.uv.workspace]
45
+ members = [
46
+ "sandbox/gradio_app",
47
+ "sandbox/sandbox/gradio_app",
48
+ "test_sandbox/test_project",
49
+ ]
sandbox/app.py DELETED
@@ -1,69 +0,0 @@
1
- import gradio as gr
2
-
3
- # Global list to store tasks
4
- tasks = []
5
-
6
-
7
- def add_task(task_text):
8
- """Add a new task to the list"""
9
- if task_text.strip():
10
- tasks.append({"text": task_text.strip(), "completed": False})
11
- return update_task_display(), ""
12
-
13
-
14
- def toggle_task(task_index):
15
- """Toggle completion status of a task"""
16
- if 0 <= task_index < len(tasks):
17
- tasks[task_index]["completed"] = not tasks[task_index]["completed"]
18
- return update_task_display()
19
-
20
-
21
- def delete_task(task_index):
22
- """Delete a task from the list"""
23
- if 0 <= task_index < len(tasks):
24
- tasks.pop(task_index)
25
- return update_task_display()
26
-
27
-
28
- def update_task_display():
29
- """Update the task display"""
30
- if not tasks:
31
- return "No tasks yet!"
32
-
33
- display_text = ""
34
- for i, task in enumerate(tasks):
35
- status = "✓" if task["completed"] else "○"
36
- display_text += f"{i}: {status} {task['text']}\n"
37
- return display_text
38
-
39
-
40
- # Create Gradio interface
41
- with gr.Blocks(title="Simple To-Do List", theme=gr.themes.Monochrome()) as app:
42
- gr.Markdown("# Simple To-Do List")
43
-
44
- with gr.Row():
45
- task_input = gr.Textbox(
46
- placeholder="Enter a new task...", label="New Task", scale=3
47
- )
48
- add_btn = gr.Button("Add Task", scale=1)
49
-
50
- task_display = gr.Textbox(
51
- value="No tasks yet!", label="Tasks", lines=10, interactive=False
52
- )
53
-
54
- with gr.Row():
55
- task_index = gr.Number(label="Task Number", value=0, precision=0)
56
- toggle_btn = gr.Button("Toggle Complete")
57
- delete_btn = gr.Button("Delete Task")
58
-
59
- # Event handlers
60
- add_btn.click(add_task, inputs=[task_input], outputs=[task_display, task_input])
61
-
62
- task_input.submit(add_task, inputs=[task_input], outputs=[task_display, task_input])
63
-
64
- toggle_btn.click(toggle_task, inputs=[task_index], outputs=[task_display])
65
-
66
- delete_btn.click(delete_task, inputs=[task_index], outputs=[task_display])
67
-
68
- if __name__ == "__main__":
69
- app.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
settings.py CHANGED
@@ -23,6 +23,16 @@ class Settings:
23
  self.api_base_url: str | None = os.getenv("API_BASE_URL")
24
  self.api_key: str | None = os.getenv("API_KEY")
25
 
 
 
 
 
 
 
 
 
 
 
26
  # Application Settings
27
  self.gradio_host: str = os.getenv("GRADIO_HOST", "127.0.0.1")
28
  self.gradio_port: int = int(os.getenv("GRADIO_PORT", "7860"))
@@ -40,7 +50,10 @@ class Settings:
40
 
41
  if not self.api_key:
42
  print("⚠️ Warning: API_KEY not set in environment variables.")
43
- print(" The planning agent may not work without a valid API key.")
 
 
 
44
  print(" Set it in your .env file or as an environment variable.")
45
  print()
46
 
@@ -52,6 +65,22 @@ in valid range [0, 1, 2]"
52
  print(" Using default value of 1")
53
  self.planning_verbosity = 1
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def get_model_config(self) -> dict:
56
  """Get model configuration for the planning agent."""
57
  config = {"model_id": self.model_id, "api_key": self.api_key}
@@ -63,6 +92,17 @@ in valid range [0, 1, 2]"
63
 
64
  return config
65
 
 
 
 
 
 
 
 
 
 
 
 
66
  def get_gradio_config(self) -> dict:
67
  """Get Gradio launch configuration."""
68
  return {
@@ -78,17 +118,48 @@ in valid range [0, 1, 2]"
78
  "max_steps": self.max_planning_steps,
79
  }
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def __repr__(self) -> str:
82
  """String representation of settings (excluding sensitive data)."""
83
  return f"""Settings(
84
  model_id='{self.model_id}',
 
 
85
  api_key={'***' if self.api_key else 'None'},
86
  api_base_url='{self.api_base_url}',
87
  gradio_host='{self.gradio_host}',
88
  gradio_port={self.gradio_port},
89
  gradio_debug={self.gradio_debug},
90
  planning_verbosity={self.planning_verbosity},
91
- max_planning_steps={self.max_planning_steps}
 
 
 
 
92
  )"""
93
 
94
 
@@ -115,8 +186,17 @@ if __name__ == "__main__":
115
  print("Model Config:")
116
  print(settings.get_model_config())
117
  print()
 
 
 
118
  print("Gradio Config:")
119
  print(settings.get_gradio_config())
120
  print()
121
  print("Planning Config:")
122
  print(settings.get_planning_config())
 
 
 
 
 
 
 
23
  self.api_base_url: str | None = os.getenv("API_BASE_URL")
24
  self.api_key: str | None = os.getenv("API_KEY")
25
 
26
+ # Coding Agent Settings
27
+ self.code_model_id: str = os.getenv("CODE_MODEL_ID", self.model_id)
28
+ self.coding_verbosity: int = int(os.getenv("CODING_VERBOSITY", "2"))
29
+ self.max_coding_steps: int = int(os.getenv("MAX_CODING_STEPS", "20"))
30
+
31
+ # Testing Agent Settings
32
+ self.test_model_id: str = os.getenv("TEST_MODEL_ID", self.model_id)
33
+ self.testing_verbosity: int = int(os.getenv("TESTING_VERBOSITY", "2"))
34
+ self.max_testing_steps: int = int(os.getenv("MAX_TESTING_STEPS", "15"))
35
+
36
  # Application Settings
37
  self.gradio_host: str = os.getenv("GRADIO_HOST", "127.0.0.1")
38
  self.gradio_port: int = int(os.getenv("GRADIO_PORT", "7860"))
 
50
 
51
  if not self.api_key:
52
  print("⚠️ Warning: API_KEY not set in environment variables.")
53
+ print(
54
+ " The planning and coding agents may not work \
55
+ without a valid API key."
56
+ )
57
  print(" Set it in your .env file or as an environment variable.")
58
  print()
59
 
 
65
  print(" Using default value of 1")
66
  self.planning_verbosity = 1
67
 
68
+ if self.coding_verbosity not in [0, 1, 2]:
69
+ print(
70
+ f"⚠️ Warning: CODING_VERBOSITY={self.coding_verbosity} is not \
71
+ in valid range [0, 1, 2]"
72
+ )
73
+ print(" Using default value of 2")
74
+ self.coding_verbosity = 2
75
+
76
+ if self.testing_verbosity not in [0, 1, 2]:
77
+ print(
78
+ f"⚠️ Warning: TESTING_VERBOSITY={self.testing_verbosity} is not \
79
+ in valid range [0, 1, 2]"
80
+ )
81
+ print(" Using default value of 2")
82
+ self.testing_verbosity = 2
83
+
84
  def get_model_config(self) -> dict:
85
  """Get model configuration for the planning agent."""
86
  config = {"model_id": self.model_id, "api_key": self.api_key}
 
92
 
93
  return config
94
 
95
+ def get_code_model_config(self) -> dict:
96
+ """Get model configuration for the coding agent."""
97
+ config = {"model_id": self.code_model_id, "api_key": self.api_key}
98
+
99
+ if self.api_base_url:
100
+ config["api_base_url"] = self.api_base_url
101
+ if self.api_key:
102
+ config["api_key"] = self.api_key
103
+
104
+ return config
105
+
106
  def get_gradio_config(self) -> dict:
107
  """Get Gradio launch configuration."""
108
  return {
 
118
  "max_steps": self.max_planning_steps,
119
  }
120
 
121
+ def get_coding_config(self) -> dict:
122
+ """Get coding agent configuration."""
123
+ return {
124
+ "verbosity_level": self.coding_verbosity,
125
+ "max_steps": self.max_coding_steps,
126
+ }
127
+
128
+ def get_test_model_config(self) -> dict:
129
+ """Get model configuration for the testing agent."""
130
+ config = {"model_id": self.test_model_id, "api_key": self.api_key}
131
+
132
+ if self.api_base_url:
133
+ config["api_base_url"] = self.api_base_url
134
+ if self.api_key:
135
+ config["api_key"] = self.api_key
136
+
137
+ return config
138
+
139
+ def get_testing_config(self) -> dict:
140
+ """Get testing agent configuration."""
141
+ return {
142
+ "verbosity_level": self.testing_verbosity,
143
+ "max_steps": self.max_testing_steps,
144
+ }
145
+
146
  def __repr__(self) -> str:
147
  """String representation of settings (excluding sensitive data)."""
148
  return f"""Settings(
149
  model_id='{self.model_id}',
150
+ code_model_id='{self.code_model_id}',
151
+ test_model_id='{self.test_model_id}',
152
  api_key={'***' if self.api_key else 'None'},
153
  api_base_url='{self.api_base_url}',
154
  gradio_host='{self.gradio_host}',
155
  gradio_port={self.gradio_port},
156
  gradio_debug={self.gradio_debug},
157
  planning_verbosity={self.planning_verbosity},
158
+ max_planning_steps={self.max_planning_steps},
159
+ coding_verbosity={self.coding_verbosity},
160
+ max_coding_steps={self.max_coding_steps},
161
+ testing_verbosity={self.testing_verbosity},
162
+ max_testing_steps={self.max_testing_steps}
163
  )"""
164
 
165
 
 
186
  print("Model Config:")
187
  print(settings.get_model_config())
188
  print()
189
+ print("Code Model Config:")
190
+ print(settings.get_code_model_config())
191
+ print()
192
  print("Gradio Config:")
193
  print(settings.get_gradio_config())
194
  print()
195
  print("Planning Config:")
196
  print(settings.get_planning_config())
197
+ print()
198
+ print("Coding Config:")
199
+ print(settings.get_coding_config())
200
+ print()
201
+ print("Testing Config:")
202
+ print(settings.get_testing_config())
test_coding_agent.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test module for the Gradio Coding Agent.
3
+
4
+ This module provides tests to verify that the coding agent can:
5
+ - Set up proper project structure with uv
6
+ - Integrate with the planning agent
7
+ - Create functional Gradio applications
8
+ """
9
+
10
+ import os
11
+ import shutil
12
+ from pathlib import Path
13
+
14
+ from coding_agent import GradioCodingAgent
15
+ from planning_agent import PlanningResult
16
+
17
+
18
+ def test_setup_project_structure():
19
+ """Test that the project structure setup works correctly."""
20
+ agent = GradioCodingAgent()
21
+
22
+ # Clean up any existing test directory
23
+ test_sandbox = Path("test_sandbox")
24
+ if test_sandbox.exists():
25
+ shutil.rmtree(test_sandbox)
26
+
27
+ # Temporarily change the sandbox path for testing
28
+ original_sandbox = agent.sandbox_path
29
+ agent.sandbox_path = test_sandbox
30
+
31
+ try:
32
+ # Test project setup
33
+ success = agent.setup_project_structure("test_project")
34
+
35
+ # Verify the structure was created
36
+ project_path = test_sandbox / "test_project"
37
+ assert project_path.exists(), "Project directory should exist"
38
+ assert (project_path / "pyproject.toml").exists(), "pyproject.toml should exist"
39
+ assert (project_path / "README.md").exists(), "README.md should exist"
40
+
41
+ print("✅ Project structure setup test passed")
42
+ return success
43
+
44
+ finally:
45
+ # Restore original sandbox path
46
+ agent.sandbox_path = original_sandbox
47
+
48
+ # Clean up test directory
49
+ if test_sandbox.exists():
50
+ shutil.rmtree(test_sandbox)
51
+
52
+
53
+ def test_mock_implementation():
54
+ """Test implementation with a mock planning result."""
55
+
56
+ # Create a simple mock planning result
57
+ mock_planning = PlanningResult(
58
+ action_plan="Create a simple text input and output application",
59
+ implementation_plan="Use gr.Textbox for input and output",
60
+ testing_plan="Test with sample text input",
61
+ gradio_components=["gr.Textbox", "gr.Button"],
62
+ estimated_complexity="Simple",
63
+ dependencies=["gradio"],
64
+ )
65
+
66
+ agent = GradioCodingAgent()
67
+
68
+ # Note: This test requires API access and will only work with valid credentials
69
+ try:
70
+ print("🧪 Testing mock implementation (requires API access)...")
71
+ result = agent.implement_application(mock_planning)
72
+
73
+ print(f"Implementation result: Success={result.success}")
74
+ print(f"Project path: {result.project_path}")
75
+ print(f"Error messages: {result.error_messages}")
76
+
77
+ return result
78
+
79
+ except Exception as e:
80
+ print(f"⚠️ Mock implementation test failed (expected without API): {e}")
81
+ return None
82
+
83
+
84
+ def test_agent_initialization():
85
+ """Test that the coding agent initializes correctly."""
86
+ try:
87
+ agent = GradioCodingAgent()
88
+ assert agent is not None, "Agent should initialize"
89
+ assert agent.model is not None, "Model should be initialized"
90
+ assert agent.agent is not None, "CodeAgent should be initialized"
91
+
92
+ print("✅ Agent initialization test passed")
93
+ return True
94
+
95
+ except Exception as e:
96
+ print(f"❌ Agent initialization test failed: {e}")
97
+ return False
98
+
99
+
100
+ def main():
101
+ """Run all tests."""
102
+ print("🚀 Running Coding Agent Tests")
103
+ print("=" * 50)
104
+
105
+ # Test 1: Agent initialization
106
+ test_agent_initialization()
107
+ print()
108
+
109
+ # Test 2: Project structure setup
110
+ test_setup_project_structure()
111
+ print()
112
+
113
+ # Test 3: Mock implementation (optional, requires API)
114
+ if os.getenv("API_KEY"):
115
+ test_mock_implementation()
116
+ else:
117
+ print("⚠️ Skipping implementation test (no API_KEY set)")
118
+
119
+ print("\n✅ All available tests completed!")
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
test_testing_agent.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test cases for the Gradio Testing Agent.
3
+
4
+ This module contains unit tests and integration tests for the testing agent
5
+ functionality, including tool validation and agent behavior testing.
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+ import unittest
11
+ from pathlib import Path
12
+ from unittest.mock import Mock, patch
13
+
14
+ from coding_agent import CodingResult
15
+ from testing_agent import (
16
+ GradioTestingAgent,
17
+ TestingResult,
18
+ check_app_health,
19
+ create_gradio_testing_agent,
20
+ run_gradio_app,
21
+ setup_venv_with_uv,
22
+ stop_gradio_processes,
23
+ test_gradio_ui_basic,
24
+ )
25
+
26
+
27
+ class TestTestingAgentTools(unittest.TestCase):
28
+ """Test the individual tools used by the testing agent."""
29
+
30
+ def setUp(self):
31
+ """Set up test fixtures."""
32
+ self.temp_dir = tempfile.mkdtemp()
33
+ self.project_path = str(Path(self.temp_dir) / "test_project")
34
+ os.makedirs(self.project_path, exist_ok=True)
35
+
36
+ def tearDown(self):
37
+ """Clean up test fixtures."""
38
+ import shutil
39
+
40
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
41
+
42
+ def test_setup_venv_with_uv_missing_directory(self):
43
+ """Test setup_venv_with_uv with non-existent directory."""
44
+ result = setup_venv_with_uv("/non/existent/path")
45
+ self.assertIn("Error: Project directory", result)
46
+ self.assertIn("does not exist", result)
47
+
48
+ @patch("subprocess.run")
49
+ def test_setup_venv_with_uv_success(self, mock_run):
50
+ """Test successful virtual environment setup."""
51
+ mock_run.return_value = Mock(returncode=0)
52
+
53
+ result = setup_venv_with_uv(self.project_path)
54
+
55
+ self.assertIn("Successfully set up virtual environment", result)
56
+ mock_run.assert_called_once()
57
+
58
+ @patch("subprocess.run")
59
+ def test_setup_venv_with_uv_failure(self, mock_run):
60
+ """Test failed virtual environment setup."""
61
+ mock_run.return_value = Mock(returncode=1, stderr="uv error")
62
+
63
+ result = setup_venv_with_uv(self.project_path)
64
+
65
+ self.assertIn("Error setting up venv", result)
66
+ self.assertIn("uv error", result)
67
+
68
+ def test_run_gradio_app_missing_file(self):
69
+ """Test run_gradio_app with missing app.py file."""
70
+ result = run_gradio_app(self.project_path)
71
+ self.assertIn("Error: app.py not found", result)
72
+
73
+ @patch("subprocess.Popen")
74
+ def test_run_gradio_app_success(self, mock_popen):
75
+ """Test successful Gradio app launch."""
76
+ # Create app.py file
77
+ app_file = Path(self.project_path) / "app.py"
78
+ app_file.write_text("import gradio as gr\nprint('test')")
79
+
80
+ # Mock the process
81
+ mock_process = Mock()
82
+ mock_process.poll.return_value = None # Process is running
83
+ mock_process.pid = 12345
84
+ mock_popen.return_value = mock_process
85
+
86
+ result = run_gradio_app(self.project_path, timeout=1)
87
+
88
+ self.assertIn("Successfully started Gradio app", result)
89
+ mock_popen.assert_called_once()
90
+
91
+ @patch("requests.get")
92
+ def test_check_app_health_success(self, mock_get):
93
+ """Test successful health check."""
94
+ mock_response = Mock()
95
+ mock_response.status_code = 200
96
+ mock_response.elapsed.total_seconds.return_value = 0.5
97
+ mock_get.return_value = mock_response
98
+
99
+ result = check_app_health()
100
+
101
+ self.assertIn("Application is healthy", result)
102
+ self.assertIn("0.50s", result)
103
+
104
+ @patch("requests.get")
105
+ def test_check_app_health_connection_error(self, mock_get):
106
+ """Test health check with connection error."""
107
+ mock_get.side_effect = Exception("Connection failed")
108
+
109
+ result = check_app_health()
110
+
111
+ self.assertIn("Error checking application health", result)
112
+
113
+ def test_test_gradio_ui_basic_selenium_not_installed(self):
114
+ """Test UI testing when Selenium is not available."""
115
+ with patch(
116
+ "builtins.__import__", side_effect=ImportError("No module named 'selenium'")
117
+ ):
118
+ result = test_gradio_ui_basic()
119
+ self.assertIn("Error: Selenium not installed", result)
120
+
121
+ @patch("subprocess.run")
122
+ def test_stop_gradio_processes(self, mock_run):
123
+ """Test stopping Gradio processes."""
124
+ # Mock subprocess calls
125
+ mock_run.side_effect = [
126
+ Mock(returncode=0), # pkill successful
127
+ Mock(stdout="12345\n67890", returncode=0), # lsof
128
+ Mock(returncode=0), # kill first process
129
+ Mock(returncode=0), # kill second process
130
+ ]
131
+
132
+ result = stop_gradio_processes()
133
+
134
+ self.assertIn("Stopped Gradio processes by name", result)
135
+ self.assertIn("Killed process 12345", result)
136
+ self.assertIn("Killed process 67890", result)
137
+
138
+
139
+ class TestGradioTestingAgent(unittest.TestCase):
140
+ """Test the main GradioTestingAgent class."""
141
+
142
+ @patch("testing_agent.settings")
143
+ def setUp(self, mock_settings):
144
+ """Set up test fixtures."""
145
+ mock_settings.test_model_id = "test-model"
146
+ mock_settings.api_base_url = "http://test.api"
147
+ mock_settings.api_key = "test-key"
148
+ mock_settings.testing_verbosity = 1
149
+ mock_settings.max_testing_steps = 10
150
+
151
+ @patch("testing_agent.LiteLLMModel")
152
+ @patch("testing_agent.ToolCallingAgent")
153
+ def test_agent_initialization(self, mock_agent, mock_model):
154
+ """Test agent initialization with default settings."""
155
+ agent = GradioTestingAgent()
156
+
157
+ self.assertIsInstance(agent, GradioTestingAgent)
158
+ mock_model.assert_called_once()
159
+ mock_agent.assert_called_once()
160
+
161
+ def test_test_application_with_failed_coding_result(self):
162
+ """Test testing application when coding agent failed."""
163
+ agent = GradioTestingAgent()
164
+ failed_coding_result = CodingResult(
165
+ success=False,
166
+ project_path="/test/path",
167
+ implemented_features=[],
168
+ remaining_tasks=["Setup failed"],
169
+ error_messages=["Setup error"],
170
+ final_app_code="",
171
+ )
172
+
173
+ result = agent.test_application(failed_coding_result)
174
+
175
+ self.assertFalse(result.success)
176
+ self.assertEqual(result.project_path, "/test/path")
177
+ self.assertIn(
178
+ "Coding agent failed to create application", result.test_cases_failed
179
+ )
180
+
181
+ @patch("testing_agent.ToolCallingAgent")
182
+ def test_test_application_agent_error(self, mock_agent_class):
183
+ """Test testing application when agent execution fails."""
184
+ mock_agent = Mock()
185
+ mock_agent.run.side_effect = Exception("Agent error")
186
+ mock_agent_class.return_value = mock_agent
187
+
188
+ agent = GradioTestingAgent()
189
+ successful_coding_result = CodingResult(
190
+ success=True,
191
+ project_path="/test/path",
192
+ implemented_features=["Basic UI"],
193
+ remaining_tasks=[],
194
+ error_messages=[],
195
+ final_app_code="import gradio as gr",
196
+ )
197
+
198
+ result = agent.test_application(successful_coding_result)
199
+
200
+ self.assertFalse(result.success)
201
+ self.assertIn("Testing agent execution failed", result.test_cases_failed)
202
+
203
+ def test_parse_testing_response_success(self):
204
+ """Test parsing a successful testing response."""
205
+ agent = GradioTestingAgent()
206
+ response = """
207
+ Successfully set up virtual environment for /test/path
208
+ Successfully started Gradio app: Server running
209
+ Application is healthy. Status: 200, Response time: 0.25s
210
+ ✓ Page loaded successfully; ✓ Gradio container found
211
+ Screenshot saved to /tmp/test.png
212
+ """
213
+
214
+ result = agent._parse_testing_response(response, "/test/path")
215
+
216
+ self.assertTrue(result.success)
217
+ self.assertTrue(result.setup_successful)
218
+ self.assertTrue(result.server_launched)
219
+ self.assertTrue(result.ui_accessible)
220
+ self.assertIn("Virtual environment setup", result.test_cases_passed)
221
+ self.assertIn("UI component testing", result.test_cases_passed)
222
+ self.assertEqual(result.performance_metrics["response_time_seconds"], 0.25)
223
+
224
+ def test_parse_testing_response_failure(self):
225
+ """Test parsing a failed testing response."""
226
+ agent = GradioTestingAgent()
227
+ response = """
228
+ Error setting up venv: Command failed
229
+ Error running gradio app: File not found
230
+ Cannot connect to http://127.0.0.1:7860
231
+ Error during UI testing: Browser error
232
+ """
233
+
234
+ result = agent._parse_testing_response(response, "/test/path")
235
+
236
+ self.assertFalse(result.success)
237
+ self.assertFalse(result.setup_successful)
238
+ self.assertFalse(result.server_launched)
239
+ self.assertFalse(result.ui_accessible)
240
+
241
+ def test_generate_test_report(self):
242
+ """Test generating a test report."""
243
+ agent = GradioTestingAgent()
244
+ test_result = TestingResult(
245
+ success=True,
246
+ project_path="/test/path",
247
+ setup_successful=True,
248
+ server_launched=True,
249
+ ui_accessible=True,
250
+ test_cases_passed=["Setup", "Launch", "UI"],
251
+ test_cases_failed=[],
252
+ error_messages=[],
253
+ screenshots=["/tmp/test.png"],
254
+ performance_metrics={"response_time": 0.5},
255
+ logs="Test completed successfully",
256
+ )
257
+
258
+ report = agent.generate_test_report(test_result)
259
+
260
+ self.assertIn("# Gradio Application Test Report ✅", report)
261
+ self.assertIn("**Project Path**: `/test/path`", report)
262
+ self.assertIn("✅ Setup", report)
263
+ self.assertIn("✅ Launch", report)
264
+ self.assertIn("✅ UI", report)
265
+ self.assertIn("/tmp/test.png", report)
266
+
267
+
268
+ class TestTestingAgentFactory(unittest.TestCase):
269
+ """Test the factory function for creating testing agents."""
270
+
271
+ @patch("testing_agent.GradioTestingAgent")
272
+ def test_create_gradio_testing_agent(self, mock_agent_class):
273
+ """Test creating a testing agent with factory function."""
274
+ mock_agent = Mock()
275
+ mock_agent_class.return_value = mock_agent
276
+
277
+ agent = create_gradio_testing_agent()
278
+
279
+ self.assertEqual(agent, mock_agent)
280
+ mock_agent_class.assert_called_once_with()
281
+
282
+
283
+ if __name__ == "__main__":
284
+ unittest.main()
testing_agent.py ADDED
@@ -0,0 +1,615 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Smolagents ToolCallingAgent for testing Gradio applications.
3
+
4
+ This module provides a specialized testing agent that can:
5
+ - Set up virtual environments using uv
6
+ - Run Gradio applications in the sandbox folder
7
+ - Perform basic UI testing using browser automation
8
+ - Validate that the application is functional and responsive
9
+ - Generate test reports with screenshots and logs
10
+ """
11
+
12
+ import os
13
+ import subprocess
14
+ import time
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ from smolagents import LiteLLMModel, ToolCallingAgent, tool
19
+
20
+ from coding_agent import CodingResult
21
+ from settings import settings
22
+
23
+
24
+ @dataclass
25
+ class TestingResult:
26
+ """Result of the testing agent containing validation details."""
27
+
28
+ success: bool
29
+ project_path: str
30
+ setup_successful: bool
31
+ server_launched: bool
32
+ ui_accessible: bool
33
+ test_cases_passed: list[str]
34
+ test_cases_failed: list[str]
35
+ error_messages: list[str]
36
+ screenshots: list[str]
37
+ performance_metrics: dict[str, float]
38
+ logs: str
39
+
40
+
41
+ @tool
42
+ def setup_venv_with_uv(project_path: str) -> str:
43
+ """
44
+ Set up a virtual environment using uv for the Gradio project.
45
+
46
+ Args:
47
+ project_path: Path to the Gradio project directory
48
+
49
+ Returns:
50
+ Status message indicating success or failure
51
+ """
52
+ try:
53
+ # Change to project directory
54
+ original_cwd = os.getcwd()
55
+ project_dir = Path(project_path)
56
+
57
+ if not project_dir.exists():
58
+ return f"Error: Project directory {project_path} does not exist"
59
+
60
+ os.chdir(project_dir)
61
+
62
+ # Install dependencies using uv
63
+ result = subprocess.run(
64
+ ["uv", "sync"],
65
+ capture_output=True,
66
+ text=True,
67
+ timeout=300, # 5 minutes timeout
68
+ )
69
+
70
+ os.chdir(original_cwd)
71
+
72
+ if result.returncode == 0:
73
+ return f"Successfully set up virtual environment for {project_path}"
74
+ else:
75
+ return f"Error setting up venv: {result.stderr}"
76
+
77
+ except subprocess.TimeoutExpired:
78
+ os.chdir(original_cwd)
79
+ return "Error: uv sync timed out after 5 minutes"
80
+ except FileNotFoundError:
81
+ os.chdir(original_cwd)
82
+ return "Error: uv command not found. Please install uv first."
83
+ except Exception as e:
84
+ os.chdir(original_cwd)
85
+ return f"Unexpected error: {str(e)}"
86
+
87
+
88
+ @tool
89
+ def run_gradio_app(project_path: str, timeout: int = 30) -> str:
90
+ """
91
+ Run the Gradio application and check if it starts successfully.
92
+
93
+ Args:
94
+ project_path: Path to the Gradio project directory
95
+ timeout: Maximum time to wait for the app to start (in seconds)
96
+
97
+ Returns:
98
+ Status message with server information or error details
99
+ """
100
+ try:
101
+ project_dir = Path(project_path)
102
+ app_file = project_dir / "app.py"
103
+
104
+ if not app_file.exists():
105
+ return f"Error: app.py not found in {project_path}"
106
+
107
+ # Start the Gradio app in background
108
+ process = subprocess.Popen(
109
+ ["uv", "run", "python", "app.py"],
110
+ cwd=project_dir,
111
+ stdout=subprocess.PIPE,
112
+ stderr=subprocess.PIPE,
113
+ text=True,
114
+ )
115
+
116
+ # Wait for the server to start (look for "Running on" in output)
117
+ start_time = time.time()
118
+ server_info = ""
119
+
120
+ while time.time() - start_time < timeout:
121
+ if process.poll() is not None:
122
+ # Process has terminated
123
+ stdout, stderr = process.communicate()
124
+ return (
125
+ f"Error: App terminated early. STDOUT: {stdout}, STDERR: {stderr}"
126
+ )
127
+
128
+ time.sleep(1)
129
+
130
+ # Try to read some output to see if server started
131
+ try:
132
+ # Non-blocking read attempt
133
+ import select
134
+
135
+ if select.select([process.stdout], [], [], 0.1)[0]:
136
+ line = process.stdout.readline()
137
+ if line and "Running on" in line:
138
+ server_info = line.strip()
139
+ break
140
+ except Exception as e:
141
+ print(f"Error: {e}")
142
+ time.sleep(2)
143
+ break
144
+
145
+ if not server_info:
146
+ server_info = (
147
+ f"Server started (PID: {process.pid}), accessible at "
148
+ "http://127.0.0.1:7860"
149
+ )
150
+
151
+ return f"Successfully started Gradio app: {server_info}"
152
+
153
+ except Exception as e:
154
+ return f"Error running Gradio app: {str(e)}"
155
+
156
+
157
+ @tool
158
+ def check_app_health(url: str = "http://127.0.0.1:7860") -> str:
159
+ """
160
+ Check if the Gradio application is responding to HTTP requests.
161
+
162
+ Args:
163
+ url: URL of the Gradio application
164
+
165
+ Returns:
166
+ Health check status message
167
+ """
168
+ try:
169
+ import requests
170
+
171
+ response = requests.get(url, timeout=10)
172
+
173
+ if response.status_code == 200:
174
+ return (
175
+ f"Application is healthy. Status: {response.status_code}, "
176
+ f"Response time: {response.elapsed.total_seconds():.2f}s"
177
+ )
178
+ else:
179
+ return f"Application returned status {response.status_code}"
180
+
181
+ except requests.exceptions.ConnectionError:
182
+ return f"Error: Cannot connect to {url}. Application may not be running."
183
+ except requests.exceptions.Timeout:
184
+ return f"Error: Request to {url} timed out."
185
+ except Exception as e:
186
+ return f"Error checking application health: {str(e)}"
187
+
188
+
189
+ @tool
190
+ def test_gradio_ui_basic(url: str = "http://127.0.0.1:7860") -> str:
191
+ """
192
+ Perform basic UI testing of the Gradio application.
193
+
194
+ Args:
195
+ url: URL of the Gradio application
196
+
197
+ Returns:
198
+ Test results summary
199
+ """
200
+ try:
201
+ from selenium import webdriver
202
+ from selenium.webdriver.chrome.options import Options
203
+ from selenium.webdriver.common.by import By
204
+ from selenium.webdriver.support import expected_conditions as EC
205
+ from selenium.webdriver.support.ui import WebDriverWait
206
+
207
+ # Setup Chrome options for headless mode
208
+ chrome_options = Options()
209
+ chrome_options.add_argument("--headless")
210
+ chrome_options.add_argument("--no-sandbox")
211
+ chrome_options.add_argument("--disable-dev-shm-usage")
212
+
213
+ driver = webdriver.Chrome(options=chrome_options)
214
+
215
+ try:
216
+ # Navigate to the Gradio app
217
+ driver.get(url)
218
+
219
+ # Wait for the page to load
220
+ WebDriverWait(driver, 10).until(
221
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
222
+ )
223
+
224
+ # Check for Gradio-specific elements
225
+ gradio_app = driver.find_elements(
226
+ By.CSS_SELECTOR, ".gradio-container, #gradio-app, .app"
227
+ )
228
+
229
+ if not gradio_app:
230
+ return "Warning: No Gradio app container found on the page"
231
+
232
+ # Check for interactive elements (buttons, inputs)
233
+ inputs = driver.find_elements(By.CSS_SELECTOR, "input, textarea, button")
234
+
235
+ test_results = []
236
+ test_results.append("✓ Page loaded successfully")
237
+ test_results.append("✓ Gradio container found")
238
+ test_results.append(f"✓ Found {len(inputs)} interactive elements")
239
+
240
+ # Take a screenshot
241
+ screenshot_path = "/tmp/gradio_test_screenshot.png"
242
+ driver.save_screenshot(screenshot_path)
243
+ test_results.append(f"✓ Screenshot saved to {screenshot_path}")
244
+
245
+ return "; ".join(test_results)
246
+
247
+ finally:
248
+ driver.quit()
249
+
250
+ except ImportError:
251
+ return "Error: Selenium not installed. Install with: pip install selenium"
252
+ except Exception as e:
253
+ return f"Error during UI testing: {str(e)}"
254
+
255
+
256
+ @tool
257
+ def stop_gradio_processes() -> str:
258
+ """
259
+ Stop any running Gradio processes to clean up after testing.
260
+
261
+ Returns:
262
+ Status message about process cleanup
263
+ """
264
+ try:
265
+ stopped_processes = []
266
+
267
+ # Find processes running Gradio apps by name
268
+ result1 = subprocess.run(
269
+ ["pkill", "-f", "gradio"], capture_output=True, text=True
270
+ )
271
+
272
+ if result1.returncode == 0:
273
+ stopped_processes.append("Stopped Gradio processes by name")
274
+
275
+ # Also try to kill processes on port 7860
276
+ result2 = subprocess.run(["lsof", "-ti:7860"], capture_output=True, text=True)
277
+
278
+ if result2.stdout.strip():
279
+ pids = result2.stdout.strip().split("\n")
280
+ for pid in pids:
281
+ kill_result = subprocess.run(["kill", "-9", pid], capture_output=True)
282
+ if kill_result.returncode == 0:
283
+ stopped_processes.append(f"Killed process {pid}")
284
+
285
+ if stopped_processes:
286
+ return "; ".join(stopped_processes)
287
+ else:
288
+ return "No Gradio processes found to stop"
289
+
290
+ except Exception as e:
291
+ return f"Error stopping processes: {str(e)}"
292
+
293
+
294
+ class GradioTestingAgent:
295
+ """
296
+ A specialized ToolCallingAgent for testing Gradio applications.
297
+
298
+ This agent validates and tests Gradio applications created by the coding agent,
299
+ ensuring they are properly set up, runnable, and functional.
300
+ """
301
+
302
+ def __init__(
303
+ self,
304
+ model_id: str | None = None,
305
+ api_base_url: str | None = None,
306
+ api_key: str | None = None,
307
+ verbosity_level: int | None = None,
308
+ max_steps: int | None = None,
309
+ ):
310
+ """
311
+ Initialize the Gradio Testing Agent.
312
+
313
+ Args:
314
+ model_id: Model ID to use for testing (uses settings if None)
315
+ api_base_url: API base URL (uses settings if None)
316
+ api_key: API key (uses settings if None)
317
+ verbosity_level: Level of verbosity for agent output (uses settings if None)
318
+ max_steps: Maximum number of testing steps (uses settings if None)
319
+ """
320
+ # Use settings as defaults, but allow override
321
+ self.model_id = model_id or settings.test_model_id
322
+ self.api_base_url = api_base_url or settings.api_base_url
323
+ self.api_key = api_key or settings.api_key
324
+ verbosity_level = verbosity_level or settings.testing_verbosity
325
+ max_steps = max_steps or settings.max_testing_steps
326
+
327
+ # Initialize the language model for the ToolCallingAgent
328
+ self.model = LiteLLMModel(
329
+ model_id=self.model_id,
330
+ api_base=self.api_base_url,
331
+ api_key=self.api_key,
332
+ )
333
+
334
+ # Define the tools for testing
335
+ testing_tools = [
336
+ setup_venv_with_uv,
337
+ run_gradio_app,
338
+ check_app_health,
339
+ test_gradio_ui_basic,
340
+ stop_gradio_processes,
341
+ ]
342
+
343
+ # Initialize the ToolCallingAgent
344
+ self.agent = ToolCallingAgent(
345
+ model=self.model,
346
+ tools=testing_tools,
347
+ verbosity_level=verbosity_level,
348
+ max_steps=max_steps,
349
+ )
350
+
351
+ self.sandbox_path = Path("sandbox")
352
+
353
+ def test_application(self, coding_result: CodingResult) -> TestingResult:
354
+ """
355
+ Test the Gradio application created by the coding agent.
356
+
357
+ Args:
358
+ coding_result: The result from the coding agent
359
+
360
+ Returns:
361
+ TestingResult containing comprehensive test information
362
+ """
363
+ if not coding_result.success:
364
+ return TestingResult(
365
+ success=False,
366
+ project_path=coding_result.project_path,
367
+ setup_successful=False,
368
+ server_launched=False,
369
+ ui_accessible=False,
370
+ test_cases_passed=[],
371
+ test_cases_failed=["Coding agent failed to create application"],
372
+ error_messages=coding_result.error_messages,
373
+ screenshots=[],
374
+ performance_metrics={},
375
+ logs="Testing skipped due to coding failure",
376
+ )
377
+
378
+ project_path = coding_result.project_path
379
+
380
+ # Create comprehensive test prompt
381
+ test_prompt = f"""
382
+ You are a specialized testing agent for Gradio applications. Your task is to \
383
+ thoroughly test the Gradio application located at: {project_path}
384
+
385
+ Please perform the following testing steps in order:
386
+
387
+ 1. **Environment Setup**: Use setup_venv_with_uv to ensure the virtual environment \
388
+ is properly configured
389
+ 2. **Application Launch**: Use run_gradio_app to start the Gradio application
390
+ 3. **Health Check**: Use check_app_health to verify the application is responding
391
+ 4. **UI Testing**: Use test_gradio_ui_basic to test the user interface components
392
+ 5. **Cleanup**: Use stop_gradio_processes to clean up after testing
393
+
394
+ For each step, report:
395
+ - Whether the step succeeded or failed
396
+ - Any error messages encountered
397
+ - Performance observations (loading times, responsiveness)
398
+ - Screenshots taken (if any)
399
+
400
+ If any critical step fails, still attempt the remaining steps where possible to \
401
+ gather maximum diagnostic information.
402
+
403
+ The application should be a functional Gradio app with interactive components. Test for:
404
+ - Proper page loading
405
+ - Presence of Gradio components
406
+ - Interactive elements (buttons, inputs, etc.)
407
+ - Basic functionality
408
+
409
+ Provide a comprehensive summary of all test results at the end.
410
+ """
411
+
412
+ try:
413
+ # Run the testing workflow
414
+ result = self.agent.run(test_prompt)
415
+
416
+ # Parse the agent's response to create structured result
417
+ return self._parse_testing_response(result, project_path)
418
+
419
+ except Exception as e:
420
+ return TestingResult(
421
+ success=False,
422
+ project_path=project_path,
423
+ setup_successful=False,
424
+ server_launched=False,
425
+ ui_accessible=False,
426
+ test_cases_passed=[],
427
+ test_cases_failed=["Testing agent execution failed"],
428
+ error_messages=[str(e)],
429
+ screenshots=[],
430
+ performance_metrics={},
431
+ logs=f"Testing agent error: {str(e)}",
432
+ )
433
+
434
+ def _parse_testing_response(
435
+ self, response: str, project_path: str
436
+ ) -> TestingResult:
437
+ """
438
+ Parse the agent's testing response into a structured TestingResult.
439
+
440
+ Args:
441
+ response: Raw response from the testing agent
442
+ project_path: Path to the tested project
443
+
444
+ Returns:
445
+ Structured TestingResult
446
+ """
447
+ # Initialize default values
448
+ setup_successful = False
449
+ server_launched = False
450
+ ui_accessible = False
451
+ test_cases_passed = []
452
+ test_cases_failed = []
453
+ error_messages = []
454
+ screenshots = []
455
+ performance_metrics = {}
456
+
457
+ # Simple parsing logic based on common success/failure indicators
458
+ response_lower = response.lower()
459
+
460
+ # Check for setup success
461
+ if "successfully set up virtual environment" in response_lower:
462
+ setup_successful = True
463
+ test_cases_passed.append("Virtual environment setup")
464
+ elif "error setting up venv" in response_lower:
465
+ test_cases_failed.append("Virtual environment setup")
466
+
467
+ # Check for server launch
468
+ if "successfully started gradio app" in response_lower:
469
+ server_launched = True
470
+ test_cases_passed.append("Gradio application launch")
471
+ elif "error running gradio app" in response_lower:
472
+ test_cases_failed.append("Gradio application launch")
473
+
474
+ # Check for health status
475
+ if "application is healthy" in response_lower:
476
+ ui_accessible = True
477
+ test_cases_passed.append("Application health check")
478
+ elif "cannot connect to" in response_lower:
479
+ test_cases_failed.append("Application health check")
480
+
481
+ # Check for UI testing
482
+ if (
483
+ "page loaded successfully" in response_lower
484
+ and "gradio container found" in response_lower
485
+ ):
486
+ test_cases_passed.append("UI component testing")
487
+ elif "error during ui testing" in response_lower:
488
+ test_cases_failed.append("UI component testing")
489
+
490
+ # Look for screenshots
491
+ if "screenshot saved" in response_lower:
492
+ screenshots.append("/tmp/gradio_test_screenshot.png")
493
+
494
+ # Extract performance metrics if mentioned
495
+ if "response time:" in response_lower:
496
+ # Simple regex to extract response time
497
+ import re
498
+
499
+ time_match = re.search(r"response time: ([\d.]+)s", response_lower)
500
+ if time_match:
501
+ performance_metrics["response_time_seconds"] = float(
502
+ time_match.group(1)
503
+ )
504
+
505
+ # Determine overall success
506
+ success = (
507
+ setup_successful
508
+ and server_launched
509
+ and ui_accessible
510
+ and len(test_cases_failed) == 0
511
+ )
512
+
513
+ return TestingResult(
514
+ success=success,
515
+ project_path=project_path,
516
+ setup_successful=setup_successful,
517
+ server_launched=server_launched,
518
+ ui_accessible=ui_accessible,
519
+ test_cases_passed=test_cases_passed,
520
+ test_cases_failed=test_cases_failed,
521
+ error_messages=error_messages,
522
+ screenshots=screenshots,
523
+ performance_metrics=performance_metrics,
524
+ logs=response,
525
+ )
526
+
527
+ def generate_test_report(self, testing_result: TestingResult) -> str:
528
+ """
529
+ Generate a comprehensive test report in markdown format.
530
+
531
+ Args:
532
+ testing_result: The result from testing the application
533
+
534
+ Returns:
535
+ Markdown-formatted test report
536
+ """
537
+ status_emoji = "✅" if testing_result.success else "❌"
538
+
539
+ report = f"""
540
+ # Gradio Application Test Report {status_emoji}
541
+
542
+ ## Summary
543
+ - **Project Path**: `{testing_result.project_path}`
544
+ - **Overall Success**: {testing_result.success}
545
+ - **Environment Setup**: {"✅" if testing_result.setup_successful else "❌"}
546
+ - **Server Launch**: {"✅" if testing_result.server_launched else "❌"}
547
+ - **UI Accessibility**: {"✅" if testing_result.ui_accessible else "❌"}
548
+
549
+ ## Test Cases
550
+
551
+ ### Passed ({len(testing_result.test_cases_passed)})
552
+ {chr(10).join(f"- ✅ {case}" for case in testing_result.test_cases_passed)}
553
+
554
+ ### Failed ({len(testing_result.test_cases_failed)})
555
+ {chr(10).join(f"- ❌ {case}" for case in testing_result.test_cases_failed)}
556
+
557
+ ## Performance Metrics
558
+ {chr(10).join(f"- **{key}**: {value}" for key, value in \
559
+ testing_result.performance_metrics.items()) if testing_result.performance_metrics else \
560
+ "No performance metrics collected"}
561
+
562
+ ## Screenshots
563
+ {chr(10).join(f"- {screenshot}" for screenshot in testing_result.screenshots) \
564
+ if testing_result.screenshots else "No screenshots captured"}
565
+
566
+ ## Error Messages
567
+ {chr(10).join(f"- {error}" for error in testing_result.error_messages) \
568
+ if testing_result.error_messages else "No errors reported"}
569
+
570
+ ## Detailed Logs
571
+ ```
572
+ {testing_result.logs}
573
+ ```
574
+
575
+ ---
576
+ *Report generated by GradioTestingAgent*
577
+ """
578
+
579
+ return report.strip()
580
+
581
+
582
+ def create_gradio_testing_agent() -> GradioTestingAgent:
583
+ """
584
+ Create a Gradio testing agent with default settings.
585
+
586
+ Returns:
587
+ Configured GradioTestingAgent instance
588
+ """
589
+ return GradioTestingAgent()
590
+
591
+
592
+ if __name__ == "__main__":
593
+ # Example usage
594
+ from coding_agent import create_gradio_coding_agent
595
+ from planning_agent import GradioPlanningAgent
596
+
597
+ # Create agents
598
+ planning_agent = GradioPlanningAgent()
599
+ coding_agent = create_gradio_coding_agent()
600
+ testing_agent = create_gradio_testing_agent()
601
+
602
+ # Example workflow
603
+ print("Planning a simple calculator app...")
604
+ plan = planning_agent.plan_application(
605
+ "Create a simple calculator with basic arithmetic operations"
606
+ )
607
+
608
+ print("Implementing the application...")
609
+ implementation = coding_agent.implement_application(plan)
610
+
611
+ print("Testing the application...")
612
+ test_results = testing_agent.test_application(implementation)
613
+
614
+ print("Test Report:")
615
+ print(testing_agent.generate_test_report(test_results))
utils.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility functions shared across the Likable project.
3
+ """
4
+
5
+ import os
6
+
7
+
8
+ def load_file(path):
9
+ """Load the contents of a file and return as string.
10
+
11
+ Args:
12
+ path: Path to the file to load
13
+
14
+ Returns:
15
+ str: File contents, or empty string if path is None or file doesn't exist
16
+ """
17
+ if path is None:
18
+ return ""
19
+
20
+ # Check if file exists first
21
+ if not os.path.exists(path):
22
+ return ""
23
+
24
+ # path is a string like "subdir/example.py"
25
+ try:
26
+ with open(path, encoding="utf-8") as f:
27
+ return f.read()
28
+ except OSError:
29
+ return ""
uv.lock CHANGED
@@ -6,6 +6,12 @@ resolution-markers = [
6
  "python_full_version < '3.13'",
7
  ]
8
 
 
 
 
 
 
 
9
  [[package]]
10
  name = "aiofiles"
11
  version = "24.1.0"
@@ -168,6 +174,21 @@ wheels = [
168
  { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
169
  ]
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  [[package]]
172
  name = "cfgv"
173
  version = "3.4.0"
@@ -251,6 +272,20 @@ wheels = [
251
  { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
252
  ]
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  [[package]]
255
  name = "fastapi"
256
  version = "0.115.12"
@@ -392,6 +427,17 @@ wheels = [
392
  { url = "https://files.pythonhosted.org/packages/e2/4d/52a719a5e9a70022438d38238f2a9a3297b864c8ceaa61d77e3f1c1b472a/gradio-5.32.0-py3-none-any.whl", hash = "sha256:45fdb15784f4be19eca2beb1d45d107238ca614c177b20b0e3d4f2d6aee81bae", size = 54201078, upload-time = "2025-05-30T13:59:40.764Z" },
393
  ]
394
 
 
 
 
 
 
 
 
 
 
 
 
395
  [[package]]
396
  name = "gradio-client"
397
  version = "1.10.2"
@@ -470,6 +516,15 @@ wheels = [
470
  { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
471
  ]
472
 
 
 
 
 
 
 
 
 
 
473
  [[package]]
474
  name = "huggingface-hub"
475
  version = "0.32.3"
@@ -579,6 +634,15 @@ wheels = [
579
  { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
580
  ]
581
 
 
 
 
 
 
 
 
 
 
582
  [[package]]
583
  name = "jsonschema"
584
  version = "4.24.0"
@@ -611,8 +675,12 @@ name = "likable"
611
  version = "0.1.0"
612
  source = { virtual = "." }
613
  dependencies = [
 
614
  { name = "gradio" },
615
- { name = "smolagents", extra = ["litellm"] },
 
 
 
616
  ]
617
 
618
  [package.dev-dependencies]
@@ -623,8 +691,12 @@ dev = [
623
 
624
  [package.metadata]
625
  requires-dist = [
 
626
  { name = "gradio", specifier = ">=5.32.0" },
627
- { name = "smolagents", extras = ["litellm"], specifier = ">=1.17.0" },
 
 
 
628
  ]
629
 
630
  [package.metadata.requires-dev]
@@ -655,6 +727,48 @@ wheels = [
655
  { url = "https://files.pythonhosted.org/packages/c2/98/bec08f5a3e504013db6f52b5fd68375bd92b463c91eb454d5a6460e957af/litellm-1.72.0-py3-none-any.whl", hash = "sha256:88360a7ae9aa9c96278ae1bb0a459226f909e711c5d350781296d0640386a824", size = 7979630, upload-time = "2025-06-01T02:12:50.458Z" },
656
  ]
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  [[package]]
659
  name = "markdown-it-py"
660
  version = "3.0.0"
@@ -705,6 +819,41 @@ wheels = [
705
  { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
706
  ]
707
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  [[package]]
709
  name = "mdurl"
710
  version = "0.1.2"
@@ -878,6 +1027,18 @@ wheels = [
878
  { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" },
879
  ]
880
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  [[package]]
882
  name = "packaging"
883
  version = "25.0"
@@ -987,6 +1148,22 @@ wheels = [
987
  { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
988
  ]
989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  [[package]]
991
  name = "propcache"
992
  version = "0.3.1"
@@ -1044,6 +1221,15 @@ wheels = [
1044
  { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" },
1045
  ]
1046
 
 
 
 
 
 
 
 
 
 
1047
  [[package]]
1048
  name = "pydantic"
1049
  version = "2.11.5"
@@ -1101,6 +1287,20 @@ wheels = [
1101
  { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
1102
  ]
1103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  [[package]]
1105
  name = "pydub"
1106
  version = "0.25.1"
@@ -1119,6 +1319,15 @@ wheels = [
1119
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
1120
  ]
1121
 
 
 
 
 
 
 
 
 
 
1122
  [[package]]
1123
  name = "python-dateutil"
1124
  version = "2.9.0.post0"
@@ -1350,6 +1559,23 @@ wheels = [
1350
  { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692, upload-time = "2024-12-02T18:44:08.555Z" },
1351
  ]
1352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1353
  [[package]]
1354
  name = "semantic-version"
1355
  version = "2.10.0"
@@ -1398,6 +1624,10 @@ wheels = [
1398
  litellm = [
1399
  { name = "litellm" },
1400
  ]
 
 
 
 
1401
 
1402
  [[package]]
1403
  name = "sniffio"
@@ -1408,6 +1638,27 @@ wheels = [
1408
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
1409
  ]
1410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1411
  [[package]]
1412
  name = "starlette"
1413
  version = "0.46.2"
@@ -1490,6 +1741,37 @@ wheels = [
1490
  { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
1491
  ]
1492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1493
  [[package]]
1494
  name = "typer"
1495
  version = "0.16.0"
@@ -1544,6 +1826,11 @@ wheels = [
1544
  { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
1545
  ]
1546
 
 
 
 
 
 
1547
  [[package]]
1548
  name = "uvicorn"
1549
  version = "0.34.3"
@@ -1571,6 +1858,15 @@ wheels = [
1571
  { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
1572
  ]
1573
 
 
 
 
 
 
 
 
 
 
1574
  [[package]]
1575
  name = "websockets"
1576
  version = "15.0.1"
@@ -1602,6 +1898,18 @@ wheels = [
1602
  { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
1603
  ]
1604
 
 
 
 
 
 
 
 
 
 
 
 
 
1605
  [[package]]
1606
  name = "yarl"
1607
  version = "1.20.0"
 
6
  "python_full_version < '3.13'",
7
  ]
8
 
9
+ [manifest]
10
+ members = [
11
+ "gradio-app",
12
+ "likable",
13
+ ]
14
+
15
  [[package]]
16
  name = "aiofiles"
17
  version = "24.1.0"
 
174
  { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
175
  ]
176
 
177
+ [[package]]
178
+ name = "cffi"
179
+ version = "1.17.1"
180
+ source = { registry = "https://pypi.org/simple" }
181
+ dependencies = [
182
+ { name = "pycparser" },
183
+ ]
184
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
185
+ wheels = [
186
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
187
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
188
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
189
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
190
+ ]
191
+
192
  [[package]]
193
  name = "cfgv"
194
  version = "3.4.0"
 
272
  { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
273
  ]
274
 
275
+ [[package]]
276
+ name = "duckduckgo-search"
277
+ version = "8.0.2"
278
+ source = { registry = "https://pypi.org/simple" }
279
+ dependencies = [
280
+ { name = "click" },
281
+ { name = "lxml" },
282
+ { name = "primp" },
283
+ ]
284
+ sdist = { url = "https://files.pythonhosted.org/packages/ad/c0/e18c2148d33a9d87f6a0cc00acba30b4e547be0f8cb85ccb313a6e8fbac7/duckduckgo_search-8.0.2.tar.gz", hash = "sha256:3109a99967b29cab8862823bbe320d140d5c792415de851b9d6288de2311b3ec", size = 21807, upload-time = "2025-05-15T08:43:25.311Z" }
285
+ wheels = [
286
+ { url = "https://files.pythonhosted.org/packages/bf/6c/e36d22e76f4aa4e1ea7ea9b443bd49b5ffd2f13d430840f47e35284f797a/duckduckgo_search-8.0.2-py3-none-any.whl", hash = "sha256:b5ff8b6b8f169b8e1b15a788a5749aa900ebcefd6e1ab485787582f8d5b4f1ef", size = 18184, upload-time = "2025-05-15T08:43:23.713Z" },
287
+ ]
288
+
289
  [[package]]
290
  name = "fastapi"
291
  version = "0.115.12"
 
427
  { url = "https://files.pythonhosted.org/packages/e2/4d/52a719a5e9a70022438d38238f2a9a3297b864c8ceaa61d77e3f1c1b472a/gradio-5.32.0-py3-none-any.whl", hash = "sha256:45fdb15784f4be19eca2beb1d45d107238ca614c177b20b0e3d4f2d6aee81bae", size = 54201078, upload-time = "2025-05-30T13:59:40.764Z" },
428
  ]
429
 
430
+ [[package]]
431
+ name = "gradio-app"
432
+ version = "0.1.0"
433
+ source = { virtual = "sandbox/gradio_app" }
434
+ dependencies = [
435
+ { name = "gradio" },
436
+ ]
437
+
438
+ [package.metadata]
439
+ requires-dist = [{ name = "gradio", specifier = ">=5.32.0" }]
440
+
441
  [[package]]
442
  name = "gradio-client"
443
  version = "1.10.2"
 
516
  { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
517
  ]
518
 
519
+ [[package]]
520
+ name = "httpx-sse"
521
+ version = "0.4.0"
522
+ source = { registry = "https://pypi.org/simple" }
523
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" }
524
+ wheels = [
525
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" },
526
+ ]
527
+
528
  [[package]]
529
  name = "huggingface-hub"
530
  version = "0.32.3"
 
634
  { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
635
  ]
636
 
637
+ [[package]]
638
+ name = "jsonref"
639
+ version = "1.1.0"
640
+ source = { registry = "https://pypi.org/simple" }
641
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
642
+ wheels = [
643
+ { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
644
+ ]
645
+
646
  [[package]]
647
  name = "jsonschema"
648
  version = "4.24.0"
 
675
  version = "0.1.0"
676
  source = { virtual = "." }
677
  dependencies = [
678
+ { name = "duckduckgo-search" },
679
  { name = "gradio" },
680
+ { name = "mcp" },
681
+ { name = "requests" },
682
+ { name = "selenium" },
683
+ { name = "smolagents", extra = ["litellm", "mcp"] },
684
  ]
685
 
686
  [package.dev-dependencies]
 
691
 
692
  [package.metadata]
693
  requires-dist = [
694
+ { name = "duckduckgo-search", specifier = ">=8.0.2" },
695
  { name = "gradio", specifier = ">=5.32.0" },
696
+ { name = "mcp", specifier = ">=1.9.2" },
697
+ { name = "requests", specifier = ">=2.32.0" },
698
+ { name = "selenium", specifier = ">=4.25.0" },
699
+ { name = "smolagents", extras = ["litellm", "mcp"], specifier = ">=1.17.0" },
700
  ]
701
 
702
  [package.metadata.requires-dev]
 
727
  { url = "https://files.pythonhosted.org/packages/c2/98/bec08f5a3e504013db6f52b5fd68375bd92b463c91eb454d5a6460e957af/litellm-1.72.0-py3-none-any.whl", hash = "sha256:88360a7ae9aa9c96278ae1bb0a459226f909e711c5d350781296d0640386a824", size = 7979630, upload-time = "2025-06-01T02:12:50.458Z" },
728
  ]
729
 
730
+ [[package]]
731
+ name = "lxml"
732
+ version = "5.4.0"
733
+ source = { registry = "https://pypi.org/simple" }
734
+ sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
735
+ wheels = [
736
+ { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" },
737
+ { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" },
738
+ { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" },
739
+ { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" },
740
+ { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" },
741
+ { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" },
742
+ { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" },
743
+ { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" },
744
+ { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" },
745
+ { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" },
746
+ { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" },
747
+ { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" },
748
+ { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" },
749
+ { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" },
750
+ { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" },
751
+ { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" },
752
+ { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" },
753
+ { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
754
+ { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
755
+ { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
756
+ { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
757
+ { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
758
+ { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
759
+ { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
760
+ { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
761
+ { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
762
+ { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
763
+ { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
764
+ { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
765
+ { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
766
+ { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
767
+ { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
768
+ { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
769
+ { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
770
+ ]
771
+
772
  [[package]]
773
  name = "markdown-it-py"
774
  version = "3.0.0"
 
819
  { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
820
  ]
821
 
822
+ [[package]]
823
+ name = "mcp"
824
+ version = "1.9.2"
825
+ source = { registry = "https://pypi.org/simple" }
826
+ dependencies = [
827
+ { name = "anyio" },
828
+ { name = "httpx" },
829
+ { name = "httpx-sse" },
830
+ { name = "pydantic" },
831
+ { name = "pydantic-settings" },
832
+ { name = "python-multipart" },
833
+ { name = "sse-starlette" },
834
+ { name = "starlette" },
835
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
836
+ ]
837
+ sdist = { url = "https://files.pythonhosted.org/packages/ea/03/77c49cce3ace96e6787af624611b627b2828f0dca0f8df6f330a10eea51e/mcp-1.9.2.tar.gz", hash = "sha256:3c7651c053d635fd235990a12e84509fe32780cd359a5bbef352e20d4d963c05", size = 333066, upload-time = "2025-05-29T14:42:17.76Z" }
838
+ wheels = [
839
+ { url = "https://files.pythonhosted.org/packages/5d/a6/8f5ee9da9f67c0fd8933f63d6105f02eabdac8a8c0926728368ffbb6744d/mcp-1.9.2-py3-none-any.whl", hash = "sha256:bc29f7fd67d157fef378f89a4210384f5fecf1168d0feb12d22929818723f978", size = 131083, upload-time = "2025-05-29T14:42:16.211Z" },
840
+ ]
841
+
842
+ [[package]]
843
+ name = "mcpadapt"
844
+ version = "0.1.9"
845
+ source = { registry = "https://pypi.org/simple" }
846
+ dependencies = [
847
+ { name = "jsonref" },
848
+ { name = "mcp" },
849
+ { name = "pydantic" },
850
+ { name = "python-dotenv" },
851
+ ]
852
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/68/85c0946d567088d8d55f1c30cb942bcfec2585941a3f45b790e423b994c8/mcpadapt-0.1.9.tar.gz", hash = "sha256:03e601c4c083f3f4eb178e6a6bcd157bcb45e25c140ea0895567bab346b67645", size = 3540887, upload-time = "2025-05-24T19:40:35.823Z" }
853
+ wheels = [
854
+ { url = "https://files.pythonhosted.org/packages/83/78/0310684763e5753a3a8128dab6c87ba1e20dd907b696680592bebebc84b6/mcpadapt-0.1.9-py3-none-any.whl", hash = "sha256:9f2a6ad1155efdf1a43c11e8449ae9258295c4e140c3c6ff672983a8ac8bde33", size = 17469, upload-time = "2025-05-24T19:40:34.055Z" },
855
+ ]
856
+
857
  [[package]]
858
  name = "mdurl"
859
  version = "0.1.2"
 
1027
  { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" },
1028
  ]
1029
 
1030
+ [[package]]
1031
+ name = "outcome"
1032
+ version = "1.3.0.post0"
1033
+ source = { registry = "https://pypi.org/simple" }
1034
+ dependencies = [
1035
+ { name = "attrs" },
1036
+ ]
1037
+ sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
1038
+ wheels = [
1039
+ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
1040
+ ]
1041
+
1042
  [[package]]
1043
  name = "packaging"
1044
  version = "25.0"
 
1148
  { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
1149
  ]
1150
 
1151
+ [[package]]
1152
+ name = "primp"
1153
+ version = "0.15.0"
1154
+ source = { registry = "https://pypi.org/simple" }
1155
+ sdist = { url = "https://files.pythonhosted.org/packages/56/0b/a87556189da4de1fc6360ca1aa05e8335509633f836cdd06dd17f0743300/primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a", size = 113022, upload-time = "2025-04-17T11:41:05.315Z" }
1156
+ wheels = [
1157
+ { url = "https://files.pythonhosted.org/packages/f5/5a/146ac964b99ea7657ad67eb66f770be6577dfe9200cb28f9a95baffd6c3f/primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f", size = 3178914, upload-time = "2025-04-17T11:40:59.558Z" },
1158
+ { url = "https://files.pythonhosted.org/packages/bc/8a/cc2321e32db3ce64d6e32950d5bcbea01861db97bfb20b5394affc45b387/primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299", size = 2955079, upload-time = "2025-04-17T11:40:57.398Z" },
1159
+ { url = "https://files.pythonhosted.org/packages/c3/7b/cbd5d999a07ff2a21465975d4eb477ae6f69765e8fe8c9087dab250180d8/primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161", size = 3281018, upload-time = "2025-04-17T11:40:55.308Z" },
1160
+ { url = "https://files.pythonhosted.org/packages/1b/6e/a6221c612e61303aec2bcac3f0a02e8b67aee8c0db7bdc174aeb8010f975/primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080", size = 3255229, upload-time = "2025-04-17T11:40:47.811Z" },
1161
+ { url = "https://files.pythonhosted.org/packages/3b/54/bfeef5aca613dc660a69d0760a26c6b8747d8fdb5a7f20cb2cee53c9862f/primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83", size = 3014522, upload-time = "2025-04-17T11:40:50.191Z" },
1162
+ { url = "https://files.pythonhosted.org/packages/ac/96/84078e09f16a1dad208f2fe0f8a81be2cf36e024675b0f9eec0c2f6e2182/primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260", size = 3418567, upload-time = "2025-04-17T11:41:01.595Z" },
1163
+ { url = "https://files.pythonhosted.org/packages/6c/80/8a7a9587d3eb85be3d0b64319f2f690c90eb7953e3f73a9ddd9e46c8dc42/primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8", size = 3606279, upload-time = "2025-04-17T11:41:03.61Z" },
1164
+ { url = "https://files.pythonhosted.org/packages/0c/dd/f0183ed0145e58cf9d286c1b2c14f63ccee987a4ff79ac85acc31b5d86bd/primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32", size = 3149967, upload-time = "2025-04-17T11:41:07.067Z" },
1165
+ ]
1166
+
1167
  [[package]]
1168
  name = "propcache"
1169
  version = "0.3.1"
 
1221
  { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" },
1222
  ]
1223
 
1224
+ [[package]]
1225
+ name = "pycparser"
1226
+ version = "2.22"
1227
+ source = { registry = "https://pypi.org/simple" }
1228
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
1229
+ wheels = [
1230
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
1231
+ ]
1232
+
1233
  [[package]]
1234
  name = "pydantic"
1235
  version = "2.11.5"
 
1287
  { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
1288
  ]
1289
 
1290
+ [[package]]
1291
+ name = "pydantic-settings"
1292
+ version = "2.9.1"
1293
+ source = { registry = "https://pypi.org/simple" }
1294
+ dependencies = [
1295
+ { name = "pydantic" },
1296
+ { name = "python-dotenv" },
1297
+ { name = "typing-inspection" },
1298
+ ]
1299
+ sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
1300
+ wheels = [
1301
+ { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
1302
+ ]
1303
+
1304
  [[package]]
1305
  name = "pydub"
1306
  version = "0.25.1"
 
1319
  { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
1320
  ]
1321
 
1322
+ [[package]]
1323
+ name = "pysocks"
1324
+ version = "1.7.1"
1325
+ source = { registry = "https://pypi.org/simple" }
1326
+ sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
1327
+ wheels = [
1328
+ { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
1329
+ ]
1330
+
1331
  [[package]]
1332
  name = "python-dateutil"
1333
  version = "2.9.0.post0"
 
1559
  { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692, upload-time = "2024-12-02T18:44:08.555Z" },
1560
  ]
1561
 
1562
+ [[package]]
1563
+ name = "selenium"
1564
+ version = "4.33.0"
1565
+ source = { registry = "https://pypi.org/simple" }
1566
+ dependencies = [
1567
+ { name = "certifi" },
1568
+ { name = "trio" },
1569
+ { name = "trio-websocket" },
1570
+ { name = "typing-extensions" },
1571
+ { name = "urllib3", extra = ["socks"] },
1572
+ { name = "websocket-client" },
1573
+ ]
1574
+ sdist = { url = "https://files.pythonhosted.org/packages/5f/7e/4145666dd275760b56d0123a9439915af167932dd6caa19b5f8b281ae297/selenium-4.33.0.tar.gz", hash = "sha256:d90974db95d2cdeb34d2fb1b13f03dc904f53e6c5d228745b0635ada10cd625d", size = 882387, upload-time = "2025-05-23T17:45:22.046Z" }
1575
+ wheels = [
1576
+ { url = "https://files.pythonhosted.org/packages/7e/c0/092fde36918574e144613de73ba43c36ab8d31e7d36bb44c35261909452d/selenium-4.33.0-py3-none-any.whl", hash = "sha256:af9ea757813918bddfe05cc677bf63c8a0cd277ebf8474b3dd79caa5727fca85", size = 9370835, upload-time = "2025-05-23T17:45:19.448Z" },
1577
+ ]
1578
+
1579
  [[package]]
1580
  name = "semantic-version"
1581
  version = "2.10.0"
 
1624
  litellm = [
1625
  { name = "litellm" },
1626
  ]
1627
+ mcp = [
1628
+ { name = "mcp" },
1629
+ { name = "mcpadapt" },
1630
+ ]
1631
 
1632
  [[package]]
1633
  name = "sniffio"
 
1638
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
1639
  ]
1640
 
1641
+ [[package]]
1642
+ name = "sortedcontainers"
1643
+ version = "2.4.0"
1644
+ source = { registry = "https://pypi.org/simple" }
1645
+ sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
1646
+ wheels = [
1647
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
1648
+ ]
1649
+
1650
+ [[package]]
1651
+ name = "sse-starlette"
1652
+ version = "2.3.6"
1653
+ source = { registry = "https://pypi.org/simple" }
1654
+ dependencies = [
1655
+ { name = "anyio" },
1656
+ ]
1657
+ sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" }
1658
+ wheels = [
1659
+ { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" },
1660
+ ]
1661
+
1662
  [[package]]
1663
  name = "starlette"
1664
  version = "0.46.2"
 
1741
  { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
1742
  ]
1743
 
1744
+ [[package]]
1745
+ name = "trio"
1746
+ version = "0.30.0"
1747
+ source = { registry = "https://pypi.org/simple" }
1748
+ dependencies = [
1749
+ { name = "attrs" },
1750
+ { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
1751
+ { name = "idna" },
1752
+ { name = "outcome" },
1753
+ { name = "sniffio" },
1754
+ { name = "sortedcontainers" },
1755
+ ]
1756
+ sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" }
1757
+ wheels = [
1758
+ { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" },
1759
+ ]
1760
+
1761
+ [[package]]
1762
+ name = "trio-websocket"
1763
+ version = "0.12.2"
1764
+ source = { registry = "https://pypi.org/simple" }
1765
+ dependencies = [
1766
+ { name = "outcome" },
1767
+ { name = "trio" },
1768
+ { name = "wsproto" },
1769
+ ]
1770
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
1771
+ wheels = [
1772
+ { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
1773
+ ]
1774
+
1775
  [[package]]
1776
  name = "typer"
1777
  version = "0.16.0"
 
1826
  { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
1827
  ]
1828
 
1829
+ [package.optional-dependencies]
1830
+ socks = [
1831
+ { name = "pysocks" },
1832
+ ]
1833
+
1834
  [[package]]
1835
  name = "uvicorn"
1836
  version = "0.34.3"
 
1858
  { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
1859
  ]
1860
 
1861
+ [[package]]
1862
+ name = "websocket-client"
1863
+ version = "1.8.0"
1864
+ source = { registry = "https://pypi.org/simple" }
1865
+ sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" }
1866
+ wheels = [
1867
+ { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" },
1868
+ ]
1869
+
1870
  [[package]]
1871
  name = "websockets"
1872
  version = "15.0.1"
 
1898
  { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
1899
  ]
1900
 
1901
+ [[package]]
1902
+ name = "wsproto"
1903
+ version = "1.2.0"
1904
+ source = { registry = "https://pypi.org/simple" }
1905
+ dependencies = [
1906
+ { name = "h11" },
1907
+ ]
1908
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
1909
+ wheels = [
1910
+ { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
1911
+ ]
1912
+
1913
  [[package]]
1914
  name = "yarl"
1915
  version = "1.20.0"