Upload folder using huggingface_hub
Browse files- Agent.md +167 -0
- Dockerfile +3 -4
- fara_config.yaml +5 -19
- frontend/src/components/views/chat/chat.tsx +48 -0
- frontend/src/components/views/chat/vision.tsx +54 -0
- plan.py +67 -0
- plan2.py +40 -0
- plan3.py +52 -0
- src/magentic_ui/backend/__init__.py +5 -1
- src/magentic_ui/backend/teammanager/teammanager.py +1 -2
- src/magentic_ui/backend/web/app.py +7 -50
- src/magentic_ui/backend/web/initialization.py +1 -2
- src/magentic_ui/backend/web/managers/connection.py +7 -0
- src/magentic_ui/backend/web/routes/runs.py +17 -0
- src/magentic_ui/magentic_ui_config.py +0 -42
- tests/test_magentic_ui_config_serialization.py +114 -52
- tests/test_runs_vision.py +16 -0
- uv.lock +0 -0
Agent.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent.md
|
| 2 |
+
|
| 3 |
+
## 1. Deployment Configuration
|
| 4 |
+
|
| 5 |
+
### Target Space
|
| 6 |
+
- **Profile:** `AUXteam`
|
| 7 |
+
- **Space:** `Maxun`
|
| 8 |
+
- **Full Identifier:** `AUXteam/Maxun`
|
| 9 |
+
- **Frontend Port:** `7860` (mandatory for all Hugging Face Spaces)
|
| 10 |
+
|
| 11 |
+
### Deployment Method
|
| 12 |
+
- **Docker SDK** — for all other applications (recommended default for flexibility)
|
| 13 |
+
|
| 14 |
+
### HF Token
|
| 15 |
+
- The environment variable **`HF_TOKEN` will always be provided at execution time**.
|
| 16 |
+
- Never hardcode the token. Always read it from the environment.
|
| 17 |
+
- All monitoring and log‑streaming commands rely on `HF_TOKEN`.
|
| 18 |
+
|
| 19 |
+
### Required Files
|
| 20 |
+
- `Dockerfile`
|
| 21 |
+
- `README.md` with Hugging Face YAML frontmatter:
|
| 22 |
+
```yaml
|
| 23 |
+
---
|
| 24 |
+
title: Maxun
|
| 25 |
+
sdk: docker
|
| 26 |
+
app_port: 7860
|
| 27 |
+
---
|
| 28 |
+
```
|
| 29 |
+
- `.hfignore` to exclude unnecessary files
|
| 30 |
+
- This `Agent.md` file (must be committed before deployment)
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 2. API Exposure and Documentation
|
| 35 |
+
|
| 36 |
+
### Mandatory Endpoints
|
| 37 |
+
Every deployment **must** expose:
|
| 38 |
+
|
| 39 |
+
- **`/health`**
|
| 40 |
+
- Returns HTTP 200 when the app is ready.
|
| 41 |
+
- Required for Hugging Face to transition the Space from *starting* → *running*.
|
| 42 |
+
|
| 43 |
+
- **`/api-docs`**
|
| 44 |
+
- Documents **all** available API endpoints.
|
| 45 |
+
- Must be reachable at:
|
| 46 |
+
`https://AUXteam-Maxun.hf.space/api-docs`
|
| 47 |
+
|
| 48 |
+
### Functional Endpoints
|
| 49 |
+
- **Method:** GET
|
| 50 |
+
- **Path:** `/api/runs/{run_id}/vision`
|
| 51 |
+
- **Purpose:** Get VNC connection details for a run
|
| 52 |
+
- **Request Example:** `GET /api/runs/1/vision`
|
| 53 |
+
- **Response Example:**
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"status": true,
|
| 57 |
+
"vnc_url": "http://localhost:6080/vnc.html?autoconnect=true&resize=scale",
|
| 58 |
+
"has_vnc": true
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 3. Deployment Workflow
|
| 65 |
+
|
| 66 |
+
### Standard Deployment Command
|
| 67 |
+
After any code change, run:
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
hf upload AUXteam/Maxun --repo-type=space
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
This command must be executed **after updating and committing Agent.md**.
|
| 74 |
+
|
| 75 |
+
### Deployment Steps
|
| 76 |
+
1. Ensure all code changes are committed.
|
| 77 |
+
2. Ensure `Agent.md` is updated and committed.
|
| 78 |
+
3. Run the upload command.
|
| 79 |
+
4. Wait for the Space to build.
|
| 80 |
+
5. Monitor logs (see next section).
|
| 81 |
+
6. When the Space is running, execute all test cases.
|
| 82 |
+
|
| 83 |
+
### Continuous Deployment Rule
|
| 84 |
+
After **every** relevant edit (logic, dependencies, API changes):
|
| 85 |
+
|
| 86 |
+
- Update `Agent.md`
|
| 87 |
+
- Redeploy using the upload command
|
| 88 |
+
- Re-run all test cases
|
| 89 |
+
- Confirm `/health` and `/api-docs` are functional
|
| 90 |
+
|
| 91 |
+
This applies even for long-running projects.
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## 4. Monitoring and Logs
|
| 96 |
+
|
| 97 |
+
### Build Logs (SSE)
|
| 98 |
+
```bash
|
| 99 |
+
curl -N \
|
| 100 |
+
-H "Authorization: Bearer $HF_TOKEN" \
|
| 101 |
+
"https://huggingface.co/api/spaces/AUXteam/Maxun/logs/build"
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Run Logs (SSE)
|
| 105 |
+
```bash
|
| 106 |
+
curl -N \
|
| 107 |
+
-H "Authorization: Bearer $HF_TOKEN" \
|
| 108 |
+
"https://huggingface.co/api/spaces/AUXteam/Maxun/logs/run"
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### Notes
|
| 112 |
+
- If the Space stays in *starting* for too long, `/health` is usually failing.
|
| 113 |
+
- If the Space times out after ~30 minutes, check logs immediately.
|
| 114 |
+
- Fix issues, commit changes, redeploy.
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## 5. Test Run Cases (Mandatory After Every Deployment)
|
| 119 |
+
|
| 120 |
+
These tests ensure the agentic system can verify the deployment automatically.
|
| 121 |
+
|
| 122 |
+
### 1. Health Check
|
| 123 |
+
```
|
| 124 |
+
GET https://AUXteam-Maxun.hf.space/health
|
| 125 |
+
Expected: HTTP 200, body: {"status": "ok"} or similar
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### 2. API Docs Check
|
| 129 |
+
```
|
| 130 |
+
GET https://AUXteam-Maxun.hf.space/api-docs
|
| 131 |
+
Expected: HTTP 200, valid documentation UI or JSON spec
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 3. Functional Endpoint Tests
|
| 135 |
+
- Example request:
|
| 136 |
+
```
|
| 137 |
+
GET https://AUXteam-Maxun.hf.space/api/runs/1/vision
|
| 138 |
+
```
|
| 139 |
+
- Expected response structure:
|
| 140 |
+
```json
|
| 141 |
+
{
|
| 142 |
+
"status": true,
|
| 143 |
+
"vnc_url": "...",
|
| 144 |
+
"has_vnc": true
|
| 145 |
+
}
|
| 146 |
+
```
|
| 147 |
+
- Validation criteria: HTTP 200, JSON response with keys `status`, `vnc_url`, `has_vnc`.
|
| 148 |
+
|
| 149 |
+
### 4. End-to-End Behaviour
|
| 150 |
+
- Confirm the UI loads (if applicable)
|
| 151 |
+
- Confirm API endpoints respond within reasonable time
|
| 152 |
+
- Confirm no errors appear in run logs
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## 6. Maintenance Rules
|
| 157 |
+
|
| 158 |
+
- `Agent.md` must always reflect the **current** deployment configuration, API surface, and test cases.
|
| 159 |
+
- Any change to:
|
| 160 |
+
- API routes
|
| 161 |
+
- Dockerfile
|
| 162 |
+
- Dependencies
|
| 163 |
+
- App logic
|
| 164 |
+
- Deployment method
|
| 165 |
+
requires updating this file.
|
| 166 |
+
- This file must be committed **before** every deployment.
|
| 167 |
+
- This file is the operational contract for autonomous agents interacting with the project.
|
Dockerfile
CHANGED
|
@@ -4,8 +4,7 @@ FROM python:3.12-bookworm
|
|
| 4 |
# Set environment variables
|
| 5 |
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
PYTHONDONTWRITEBYTECODE=1 \
|
| 7 |
-
PLAYWRIGHT_BROWSERS_PATH=/app/ms-playwright
|
| 8 |
-
DEBIAN_FRONTEND=noninteractive
|
| 9 |
|
| 10 |
# Install system dependencies
|
| 11 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
@@ -69,5 +68,5 @@ ENV HOME=/home/user
|
|
| 69 |
# Expose the HF port
|
| 70 |
EXPOSE 7860
|
| 71 |
|
| 72 |
-
# Command to run the application
|
| 73 |
-
CMD ["magentic-ui", "--port", "7860", "--host", "0.0.0.0", "--run-without-docker"
|
|
|
|
| 4 |
# Set environment variables
|
| 5 |
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
PYTHONDONTWRITEBYTECODE=1 \
|
| 7 |
+
PLAYWRIGHT_BROWSERS_PATH=/app/ms-playwright
|
|
|
|
| 8 |
|
| 9 |
# Install system dependencies
|
| 10 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
| 68 |
# Expose the HF port
|
| 69 |
EXPOSE 7860
|
| 70 |
|
| 71 |
+
# Command to run the application
|
| 72 |
+
CMD ["magentic-ui", "--port", "7860", "--host", "0.0.0.0", "--run-without-docker"]
|
fara_config.yaml
CHANGED
|
@@ -12,23 +12,9 @@ model_config_local_surfer: &client_surfer
|
|
| 12 |
structured_output: false
|
| 13 |
multiple_system_messages: false
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
config:
|
| 18 |
-
model: "alias-large"
|
| 19 |
-
base_url: "https://api.helmholtz-blablador.fz-juelich.de/v1"
|
| 20 |
-
api_key: ${BLABLADOR_API_KEY}
|
| 21 |
-
|
| 22 |
-
model_config_blablador_fast: &client_blablador_fast
|
| 23 |
-
provider: OpenAIChatCompletionClient
|
| 24 |
-
config:
|
| 25 |
-
model: "alias-fast"
|
| 26 |
-
base_url: "https://api.helmholtz-blablador.fz-juelich.de/v1"
|
| 27 |
-
api_key: ${BLABLADOR_API_KEY}
|
| 28 |
-
|
| 29 |
-
orchestrator_client: *client_blablador
|
| 30 |
-
coder_client: *client_blablador
|
| 31 |
web_surfer_client: *client_surfer
|
| 32 |
-
file_surfer_client: *
|
| 33 |
-
action_guard_client: *
|
| 34 |
-
model_client: *
|
|
|
|
| 12 |
structured_output: false
|
| 13 |
multiple_system_messages: false
|
| 14 |
|
| 15 |
+
orchestrator_client: *client_surfer
|
| 16 |
+
coder_client: *client_surfer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
web_surfer_client: *client_surfer
|
| 18 |
+
file_surfer_client: *client_surfer
|
| 19 |
+
action_guard_client: *client_surfer
|
| 20 |
+
model_client: *client_surfer
|
frontend/src/components/views/chat/chat.tsx
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
| 28 |
} from "../../types/plan";
|
| 29 |
import SampleTasks from "./sampletasks";
|
| 30 |
import ProgressBar from "./progressbar";
|
|
|
|
|
|
|
| 31 |
|
| 32 |
// Extend RunStatus for sidebar status reporting
|
| 33 |
type SidebarRunStatus = BaseRunStatus | "final_answer_awaiting_input";
|
|
@@ -129,6 +131,11 @@ export default function ChatView({
|
|
| 129 |
// Replace stepTitles state with currentPlan state
|
| 130 |
const [currentPlan, setCurrentPlan] = React.useState<StepProgress["plan"]>();
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
// Create a Message object from AgentMessageConfig
|
| 133 |
const createMessage = (
|
| 134 |
config: AgentMessageConfig,
|
|
@@ -190,6 +197,15 @@ export default function ChatView({
|
|
| 190 |
|
| 191 |
if (latestRun.id) {
|
| 192 |
setupWebSocket(latestRun.id, false, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
}
|
| 194 |
} else {
|
| 195 |
setError({
|
|
@@ -333,6 +349,12 @@ export default function ChatView({
|
|
| 333 |
}, [session?.id, visible, activeSocket, onRunStatusChange]);
|
| 334 |
|
| 335 |
const handleWebSocketMessage = (message: WebSocketMessage) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
setCurrentRun((current: Run | null) => {
|
| 337 |
if (!current || !session?.id) return null;
|
| 338 |
|
|
@@ -1161,6 +1183,15 @@ export default function ChatView({
|
|
| 1161 |
return (
|
| 1162 |
<div className="text-primary h-[calc(100vh-100px)] bg-primary relative rounded flex-1 scroll w-full">
|
| 1163 |
{contextHolder}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1164 |
<div className="flex flex-col h-full w-full">
|
| 1165 |
{/* Progress Bar - Sticky at top */}
|
| 1166 |
<div className="progress-container" style={{ height: "3.5rem" }}>
|
|
@@ -1315,6 +1346,23 @@ export default function ChatView({
|
|
| 1315 |
)}
|
| 1316 |
</div>
|
| 1317 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1318 |
</div>
|
| 1319 |
);
|
| 1320 |
}
|
|
|
|
| 28 |
} from "../../types/plan";
|
| 29 |
import SampleTasks from "./sampletasks";
|
| 30 |
import ProgressBar from "./progressbar";
|
| 31 |
+
import { Tabs } from "antd";
|
| 32 |
+
import Vision from "./vision";
|
| 33 |
|
| 34 |
// Extend RunStatus for sidebar status reporting
|
| 35 |
type SidebarRunStatus = BaseRunStatus | "final_answer_awaiting_input";
|
|
|
|
| 131 |
// Replace stepTitles state with currentPlan state
|
| 132 |
const [currentPlan, setCurrentPlan] = React.useState<StepProgress["plan"]>();
|
| 133 |
|
| 134 |
+
// Vision state
|
| 135 |
+
const [activeTab, setActiveTab] = React.useState<string>("chat");
|
| 136 |
+
const [vncUrl, setVncUrl] = React.useState<string | null>(null);
|
| 137 |
+
const [lastScreenshot, setLastScreenshot] = React.useState<string | null>(null);
|
| 138 |
+
|
| 139 |
// Create a Message object from AgentMessageConfig
|
| 140 |
const createMessage = (
|
| 141 |
config: AgentMessageConfig,
|
|
|
|
| 197 |
|
| 198 |
if (latestRun.id) {
|
| 199 |
setupWebSocket(latestRun.id, false, true);
|
| 200 |
+
// Fetch VNC vision stream URL
|
| 201 |
+
fetch(`${serverUrl}/runs/${latestRun.id}/vision`)
|
| 202 |
+
.then((res) => res.json())
|
| 203 |
+
.then((data) => {
|
| 204 |
+
if (data.status && data.has_vnc) {
|
| 205 |
+
setVncUrl(data.vnc_url);
|
| 206 |
+
}
|
| 207 |
+
})
|
| 208 |
+
.catch((err) => console.error("Failed to load VNC URL", err));
|
| 209 |
}
|
| 210 |
} else {
|
| 211 |
setError({
|
|
|
|
| 349 |
}, [session?.id, visible, activeSocket, onRunStatusChange]);
|
| 350 |
|
| 351 |
const handleWebSocketMessage = (message: WebSocketMessage) => {
|
| 352 |
+
// Handle specific visual messages that shouldn't go to run state
|
| 353 |
+
if (message.type === "screenshot" && message.data && typeof message.data === "string") {
|
| 354 |
+
setLastScreenshot(message.data);
|
| 355 |
+
return;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
setCurrentRun((current: Run | null) => {
|
| 359 |
if (!current || !session?.id) return null;
|
| 360 |
|
|
|
|
| 1183 |
return (
|
| 1184 |
<div className="text-primary h-[calc(100vh-100px)] bg-primary relative rounded flex-1 scroll w-full">
|
| 1185 |
{contextHolder}
|
| 1186 |
+
<Tabs
|
| 1187 |
+
activeKey={activeTab}
|
| 1188 |
+
onChange={setActiveTab}
|
| 1189 |
+
className="h-full custom-chat-tabs"
|
| 1190 |
+
items={[
|
| 1191 |
+
{
|
| 1192 |
+
key: "chat",
|
| 1193 |
+
label: "Chat",
|
| 1194 |
+
children: (
|
| 1195 |
<div className="flex flex-col h-full w-full">
|
| 1196 |
{/* Progress Bar - Sticky at top */}
|
| 1197 |
<div className="progress-container" style={{ height: "3.5rem" }}>
|
|
|
|
| 1346 |
)}
|
| 1347 |
</div>
|
| 1348 |
</div>
|
| 1349 |
+
),
|
| 1350 |
+
},
|
| 1351 |
+
{
|
| 1352 |
+
key: "vision",
|
| 1353 |
+
label: "Vision",
|
| 1354 |
+
children: (
|
| 1355 |
+
<div className="h-[calc(100vh-150px)] w-full">
|
| 1356 |
+
<Vision
|
| 1357 |
+
vncUrl={vncUrl}
|
| 1358 |
+
lastScreenshot={lastScreenshot}
|
| 1359 |
+
isLoading={false}
|
| 1360 |
+
/>
|
| 1361 |
+
</div>
|
| 1362 |
+
),
|
| 1363 |
+
},
|
| 1364 |
+
]}
|
| 1365 |
+
/>
|
| 1366 |
</div>
|
| 1367 |
);
|
| 1368 |
}
|
frontend/src/components/views/chat/vision.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react";
|
| 2 |
+
import { Spin } from "antd";
|
| 3 |
+
|
| 4 |
+
interface VisionProps {
|
| 5 |
+
vncUrl?: string | null;
|
| 6 |
+
lastScreenshot?: string | null;
|
| 7 |
+
isLoading?: boolean;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function Vision({ vncUrl, lastScreenshot, isLoading }: VisionProps) {
|
| 11 |
+
if (isLoading) {
|
| 12 |
+
return (
|
| 13 |
+
<div className="flex items-center justify-center h-full w-full">
|
| 14 |
+
<Spin size="large" tip="Loading Vision Stream..." />
|
| 15 |
+
</div>
|
| 16 |
+
);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if (vncUrl) {
|
| 20 |
+
return (
|
| 21 |
+
<div className="h-full w-full bg-black relative">
|
| 22 |
+
<iframe
|
| 23 |
+
src={vncUrl}
|
| 24 |
+
className="absolute top-0 left-0 w-full h-full border-none"
|
| 25 |
+
title="VNC Vision Stream"
|
| 26 |
+
allow="clipboard-read; clipboard-write"
|
| 27 |
+
/>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
if (lastScreenshot) {
|
| 33 |
+
return (
|
| 34 |
+
<div className="flex items-center justify-center h-full w-full bg-gray-900 overflow-hidden">
|
| 35 |
+
<img
|
| 36 |
+
src={`data:image/png;base64,${lastScreenshot}`}
|
| 37 |
+
alt="Latest Screenshot"
|
| 38 |
+
className="max-w-full max-h-full object-contain"
|
| 39 |
+
/>
|
| 40 |
+
</div>
|
| 41 |
+
);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<div className="flex items-center justify-center h-full w-full text-secondary">
|
| 46 |
+
<div className="text-center">
|
| 47 |
+
<div className="text-xl mb-2">No Vision Stream Available</div>
|
| 48 |
+
<div className="text-sm opacity-70">
|
| 49 |
+
Start a task to see the agent's visual interactions.
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
);
|
| 54 |
+
}
|
plan.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
def generate_plan():
|
| 5 |
+
print("Sequential Thinking Phase:")
|
| 6 |
+
print("1. Analyze the request: The user wants to add a 'vision' tab to the chat UI.")
|
| 7 |
+
print(" - Currently: Chat UI exists for text interaction.")
|
| 8 |
+
print(" - Goal: Add a second tab for 'vision', streaming screenshots/VNC/visual data seamlessly.")
|
| 9 |
+
print(" - The process must keep running seamlessly while switching tabs.")
|
| 10 |
+
print("2. Architecture & Functionality:")
|
| 11 |
+
print(" - Frontend: Update ChatView to have tabs (Chat | Vision).")
|
| 12 |
+
print(" - 'Vision' tab needs a component to display streamed images/VNC.")
|
| 13 |
+
print(" - The WebSocket connection in `app.py`/`ws.py` currently handles run streams.")
|
| 14 |
+
print(" - We need a way to receive visual data (base64 images or VNC URL) via WebSocket or a separate endpoint.")
|
| 15 |
+
print(" - Given `FaraWebSurfer` and `VncDockerPlaywrightBrowser`, visual data might already be captured or accessible.")
|
| 16 |
+
print(" - Backend:")
|
| 17 |
+
print(" - FastAPI handles WebSocket for runs. Need to ensure visual data from `FaraWebSurfer` or similar agents is sent over WS.")
|
| 18 |
+
print(" - Or expose an endpoint to fetch the latest screenshot / VNC stream for a run.")
|
| 19 |
+
print(" - VNC streaming could be an iframe to a noVNC instance if the docker container exposes it.")
|
| 20 |
+
print("3. APIs & Integrations:")
|
| 21 |
+
print(" - Backend: Update WS manager to broadcast screenshots if agents yield them.")
|
| 22 |
+
print(" - Frontend: Listen to WS messages of type 'screenshot' or 'visual_data', and update the Vision tab.")
|
| 23 |
+
print(" - Alternatively, an endpoint `/api/runs/{run_id}/vision` could return the VNC URL or latest screenshot.")
|
| 24 |
+
print("4. Iteration:")
|
| 25 |
+
print(" - The simplest robust approach for 'streaming the screenshots, or no vnc' is:")
|
| 26 |
+
print(" a) Frontend: Add Tabs to Chat UI (Tabs: Chat, Vision).")
|
| 27 |
+
print(" b) Vision Tab: If VNC is available, show an iframe to the VNC URL. If screenshots are streaming, show an image tag updated via WS.")
|
| 28 |
+
print(" c) Backend: Define an API to get the vision stream info for a session/run.")
|
| 29 |
+
|
| 30 |
+
return """
|
| 31 |
+
1. **Project Description**
|
| 32 |
+
- Vision: Add a seamlessly integrated 'Vision' tab in the Magentic-UI to observe agents' visual interactions (screenshots or VNC) in real-time.
|
| 33 |
+
- Integration: The frontend `ChatView` will be updated to include tabs (Chat / Vision). The Vision tab will subscribe to visual data via WebSocket or display a VNC iframe.
|
| 34 |
+
- FastAPI Setup:
|
| 35 |
+
- Use existing `/api/ws/runs/{run_id}` for streaming screenshot events, or add `/api/runs/{run_id}/vision` to get VNC connection details.
|
| 36 |
+
|
| 37 |
+
2. **Tasks and Tests**
|
| 38 |
+
- Task 1 (Backend): Expose VNC/Vision info endpoint.
|
| 39 |
+
- Modify `src/magentic_ui/backend/web/routes/runs.py` to add a `GET /runs/{run_id}/vision` endpoint returning VNC URL or stream status.
|
| 40 |
+
- Test: Add a unit test in `tests/` checking if the endpoint returns valid connection info.
|
| 41 |
+
- Task 2 (Backend WS): Broadcast screenshots.
|
| 42 |
+
- Update WebSocketManager in `src/magentic_ui/backend/web/managers/websocket.py` to relay `screenshot` type messages from agents like `FaraWebSurfer`.
|
| 43 |
+
- Test: Unit test the WebSocketManager to ensure `screenshot` messages are broadcasted properly.
|
| 44 |
+
- Task 3 (Frontend): Implement Tabs in Chat UI.
|
| 45 |
+
- Update `frontend/src/components/views/chat/chat.tsx` to wrap the chat interface in an Ant Design `<Tabs>` component (Chat vs Vision).
|
| 46 |
+
- Test: Write a Playwright test ensuring the Tabs render and clicking 'Vision' switches the view.
|
| 47 |
+
- Task 4 (Frontend): Implement Vision Component.
|
| 48 |
+
- Create `frontend/src/components/views/chat/vision.tsx` to render an `<iframe>` for VNC or an `<img>` that updates when a `screenshot` WS message arrives.
|
| 49 |
+
- Test: Write a Playwright test simulating a `screenshot` WS message and verifying the image source updates.
|
| 50 |
+
|
| 51 |
+
3. **Functionality Expectations**
|
| 52 |
+
- User perspective: User clicks 'Vision' tab and sees exactly what the agent sees (browser viewport, desktop) via VNC or updating screenshots. Switching tabs doesn't interrupt the run.
|
| 53 |
+
- Technical perspective: Agents emit visual state. Backend routes it to the frontend via WS or provides a VNC endpoint. Frontend maintains connection regardless of active tab.
|
| 54 |
+
- Constraints: VNC requires Docker configuration exposing the noVNC port. Fallback to screenshots if VNC is unavailable.
|
| 55 |
+
|
| 56 |
+
4. **API Endpoints to be Exposed**
|
| 57 |
+
- `GET /api/runs/{run_id}/vision`
|
| 58 |
+
- Request: None
|
| 59 |
+
- Response: `{ "status": true, "vnc_url": "ws://localhost:5900", "has_vnc": true }`
|
| 60 |
+
- Auth: Inherits existing run access auth.
|
| 61 |
+
- WebSocket `/api/ws/runs/{run_id}` (Existing, modified)
|
| 62 |
+
- New message type from server: `{ "type": "screenshot", "data": "base64_encoded_image_string" }`
|
| 63 |
+
|
| 64 |
+
READY
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
print(generate_plan())
|
plan2.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
print("""
|
| 2 |
+
1. **Project Description**
|
| 3 |
+
- Vision: Add a seamlessly integrated 'Vision' tab in the Magentic-UI to observe agents' visual interactions (screenshots or VNC) in real-time.
|
| 4 |
+
- Integration: The frontend `ChatView` will be updated to include tabs (Chat / Vision). The Vision tab will subscribe to visual data via WebSocket or display a VNC iframe.
|
| 5 |
+
- FastAPI Setup:
|
| 6 |
+
- Use existing `/api/ws/runs/{run_id}` for streaming screenshot events, or add `/api/runs/{run_id}/vision` to get VNC connection details.
|
| 7 |
+
|
| 8 |
+
2. **Tasks and Tests**
|
| 9 |
+
- Task 1 (Backend): Expose VNC/Vision info endpoint.
|
| 10 |
+
- Modify `src/magentic_ui/backend/web/routes/runs.py` using `replace_with_git_merge_diff` to add a `GET /runs/{run_id}/vision` endpoint returning VNC URL or stream status.
|
| 11 |
+
- Test: Add a unit test in `tests/` checking if the endpoint returns valid connection info.
|
| 12 |
+
- Task 2 (Backend WS): Broadcast screenshots.
|
| 13 |
+
- Update WebSocketManager in `src/magentic_ui/backend/web/managers/connection.py` using `replace_with_git_merge_diff` to relay `screenshot` type messages from agents like `FaraWebSurfer`.
|
| 14 |
+
- Test: Unit test the WebSocketManager to ensure `screenshot` messages are broadcasted properly.
|
| 15 |
+
- Task 3 (Frontend): Implement Tabs in Chat UI.
|
| 16 |
+
- Update `frontend/src/components/views/chat/chat.tsx` using `replace_with_git_merge_diff` to wrap the chat interface in an Ant Design `<Tabs>` component (Chat vs Vision).
|
| 17 |
+
- Test: Write a Playwright test ensuring the Tabs render and clicking 'Vision' switches the view.
|
| 18 |
+
- Task 4 (Frontend): Implement Vision Component.
|
| 19 |
+
- Create `frontend/src/components/views/chat/vision.tsx` using `write_file` to render an `<iframe>` for VNC or an `<img>` that updates when a `screenshot` WS message arrives. Verify the file contents using `read_file`.
|
| 20 |
+
- Test: Write a Playwright test simulating a `screenshot` WS message and verifying the image source updates.
|
| 21 |
+
- Task 5 (Test Verification): Run all tests.
|
| 22 |
+
- Run unit tests and frontend playwright tests to ensure there are no regressions using `run_in_bash_session` to execute `pytest` and `npm test` or equivalent.
|
| 23 |
+
- Task 6 (Pre Commit): Complete pre commit steps.
|
| 24 |
+
- Complete pre-commit steps to ensure proper testing, verification, review, and reflection are done.
|
| 25 |
+
- Task 7 (Submission): Submit code.
|
| 26 |
+
- Once all tests pass, submit the change.
|
| 27 |
+
|
| 28 |
+
3. **Functionality Expectations**
|
| 29 |
+
- User perspective: User clicks 'Vision' tab and sees exactly what the agent sees (browser viewport, desktop) via VNC or updating screenshots. Switching tabs doesn't interrupt the run.
|
| 30 |
+
- Technical perspective: Agents emit visual state. Backend routes it to the frontend via WS or provides a VNC endpoint. Frontend maintains connection regardless of active tab.
|
| 31 |
+
- Constraints: VNC requires Docker configuration exposing the noVNC port. Fallback to screenshots if VNC is unavailable.
|
| 32 |
+
|
| 33 |
+
4. **API Endpoints to be Exposed**
|
| 34 |
+
- `GET /api/runs/{run_id}/vision`
|
| 35 |
+
- Request: None
|
| 36 |
+
- Response: `{ "status": true, "vnc_url": "ws://localhost:5900", "has_vnc": true }`
|
| 37 |
+
- Auth: Inherits existing run access auth.
|
| 38 |
+
- WebSocket `/api/ws/runs/{run_id}` (Existing, modified)
|
| 39 |
+
- New message type from server: `{ "type": "screenshot", "data": "base64_encoded_image_string" }`
|
| 40 |
+
""")
|
plan3.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
def get_plan():
|
| 4 |
+
return """
|
| 5 |
+
1. **Project Description**
|
| 6 |
+
- Vision: Add a seamlessly integrated 'Vision' tab in the Magentic-UI to observe agents' visual interactions (screenshots or VNC) in real-time. Make the application compatible with Hugging Face Spaces deployment using Docker SDK.
|
| 7 |
+
- Integration: The frontend `ChatView` will be updated to include tabs (Chat / Vision). The Vision tab will subscribe to visual data via WebSocket or display a VNC iframe.
|
| 8 |
+
- FastAPI Setup:
|
| 9 |
+
- Use existing `/api/ws/runs/{run_id}` for streaming screenshot events, or add `/api/runs/{run_id}/vision` to get VNC connection details. Ensure `/health` and `/api-docs` are functional for Hugging Face Spaces.
|
| 10 |
+
|
| 11 |
+
2. **Tasks and Tests**
|
| 12 |
+
- Task 1 (Backend): Expose VNC/Vision info endpoint.
|
| 13 |
+
- Modify `src/magentic_ui/backend/web/routes/runs.py` using `replace_with_git_merge_diff` to add a `GET /runs/{run_id}/vision` endpoint returning VNC URL or stream status.
|
| 14 |
+
- Test: Add a unit test in `tests/` checking if the endpoint returns valid connection info.
|
| 15 |
+
- Task 2 (Backend WS): Broadcast screenshots.
|
| 16 |
+
- Update WebSocketManager in `src/magentic_ui/backend/web/managers/connection.py` using `replace_with_git_merge_diff` to relay `screenshot` type messages from agents like `FaraWebSurfer`.
|
| 17 |
+
- Test: Unit test the WebSocketManager to ensure `screenshot` messages are broadcasted properly.
|
| 18 |
+
- Task 3 (Frontend): Implement Tabs in Chat UI.
|
| 19 |
+
- Update `frontend/src/components/views/chat/chat.tsx` using `replace_with_git_merge_diff` to wrap the chat interface in an Ant Design `<Tabs>` component (Chat vs Vision).
|
| 20 |
+
- Test: Write a Playwright test ensuring the Tabs render and clicking 'Vision' switches the view.
|
| 21 |
+
- Task 4 (Frontend): Implement Vision Component.
|
| 22 |
+
- Create `frontend/src/components/views/chat/vision.tsx` using `write_file` to render an `<iframe>` for VNC or an `<img>` that updates when a `screenshot` WS message arrives. Verify the file contents using `read_file`.
|
| 23 |
+
- Test: Write a Playwright test simulating a `screenshot` WS message and verifying the image source updates.
|
| 24 |
+
- Task 5 (HF Deploy Setup): Prepare Hugging Face Deployment Configuration.
|
| 25 |
+
- Create/Update `Dockerfile` to expose port 7860 and run both backend and frontend correctly.
|
| 26 |
+
- Create/Update `README.md` to include Hugging Face YAML frontmatter (app_port: 7860, sdk: docker).
|
| 27 |
+
- Ensure `/health` and `/api-docs` endpoints are properly exposed in the FastAPI backend (`app.py`).
|
| 28 |
+
- Create `Agent.md` capturing the deployment configuration, API documentation, and test cases as requested.
|
| 29 |
+
- Task 6 (Test Verification): Run all tests.
|
| 30 |
+
- Run unit tests and frontend playwright tests to ensure there are no regressions using `run_in_bash_session` to execute `pytest` and `npm test` or equivalent.
|
| 31 |
+
- Task 7 (Pre Commit): Complete pre commit steps.
|
| 32 |
+
- Complete pre-commit steps to ensure proper testing, verification, review, and reflection are done.
|
| 33 |
+
- Task 8 (Submission): Submit code.
|
| 34 |
+
- Once all tests pass, submit the change.
|
| 35 |
+
|
| 36 |
+
3. **Functionality Expectations**
|
| 37 |
+
- User perspective: User clicks 'Vision' tab and sees exactly what the agent sees (browser viewport, desktop) via VNC or updating screenshots. Switching tabs doesn't interrupt the run. The app is accessible on Hugging Face Spaces.
|
| 38 |
+
- Technical perspective: Agents emit visual state. Backend routes it to the frontend via WS or provides a VNC endpoint. Frontend maintains connection regardless of active tab. Docker container serves the app on port 7860.
|
| 39 |
+
- Constraints: VNC requires Docker configuration exposing the noVNC port. Fallback to screenshots if VNC is unavailable. HF Space deployment constraints apply.
|
| 40 |
+
|
| 41 |
+
4. **API Endpoints to be Exposed**
|
| 42 |
+
- `GET /health` (Required by HF)
|
| 43 |
+
- `GET /api-docs` (Required by HF)
|
| 44 |
+
- `GET /api/runs/{run_id}/vision`
|
| 45 |
+
- Request: None
|
| 46 |
+
- Response: `{ "status": true, "vnc_url": "ws://localhost:5900", "has_vnc": true }`
|
| 47 |
+
- Auth: Inherits existing run access auth.
|
| 48 |
+
- WebSocket `/api/ws/runs/{run_id}` (Existing, modified)
|
| 49 |
+
- New message type from server: `{ "type": "screenshot", "data": "base64_encoded_image_string" }`
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
print(get_plan())
|
src/magentic_ui/backend/__init__.py
CHANGED
|
@@ -1,2 +1,6 @@
|
|
| 1 |
-
# src/magentic_ui/backend/__init__.py
|
| 2 |
from .database.db_manager import DatabaseManager
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from .database.db_manager import DatabaseManager
|
| 2 |
+
from .datamodel import Team
|
| 3 |
+
from .teammanager import TeamManager
|
| 4 |
+
from ..version import __version__
|
| 5 |
+
|
| 6 |
+
__all__ = ["DatabaseManager", "Team", "TeamManager", "__version__"]
|
src/magentic_ui/backend/teammanager/teammanager.py
CHANGED
|
@@ -303,8 +303,7 @@ class TeamManager:
|
|
| 303 |
)
|
| 304 |
if self.run_without_docker:
|
| 305 |
config_params["run_without_docker"] = True
|
| 306 |
-
#
|
| 307 |
-
config_params["browser_headless"] = True
|
| 308 |
else:
|
| 309 |
if settings_config.get("run_without_docker", False):
|
| 310 |
# Allow settings_config to set browser_headless
|
|
|
|
| 303 |
)
|
| 304 |
if self.run_without_docker:
|
| 305 |
config_params["run_without_docker"] = True
|
| 306 |
+
# Allow browser_headless to be set by settings_config
|
|
|
|
| 307 |
else:
|
| 308 |
if settings_config.get("run_without_docker", False):
|
| 309 |
# Allow settings_config to set browser_headless
|
src/magentic_ui/backend/web/app.py
CHANGED
|
@@ -4,27 +4,6 @@ import yaml
|
|
| 4 |
from contextlib import asynccontextmanager
|
| 5 |
from typing import AsyncGenerator, Any
|
| 6 |
|
| 7 |
-
# Monkey-patch AutoGen to prevent sending 'name' field which causes 400 Bad Request on strict APIs like Blablador
|
| 8 |
-
try:
|
| 9 |
-
import autogen_ext.models.openai._openai_client as oai_client
|
| 10 |
-
original_to_oai_type = oai_client.to_oai_type
|
| 11 |
-
|
| 12 |
-
def patched_to_oai_type(*args, **kwargs):
|
| 13 |
-
result = original_to_oai_type(*args, **kwargs)
|
| 14 |
-
# result is a list of dicts (ChatCompletionMessageParam)
|
| 15 |
-
# We need to create a new list of dicts with the 'name' key removed
|
| 16 |
-
patched_result = []
|
| 17 |
-
for item in result:
|
| 18 |
-
item_copy = dict(item)
|
| 19 |
-
if "name" in item_copy:
|
| 20 |
-
del item_copy["name"]
|
| 21 |
-
patched_result.append(item_copy)
|
| 22 |
-
return patched_result
|
| 23 |
-
|
| 24 |
-
oai_client.to_oai_type = patched_to_oai_type
|
| 25 |
-
except ImportError:
|
| 26 |
-
pass
|
| 27 |
-
|
| 28 |
# import logging
|
| 29 |
from fastapi import FastAPI, Request
|
| 30 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -45,14 +24,11 @@ from .routes import (
|
|
| 45 |
ws,
|
| 46 |
mcp,
|
| 47 |
)
|
| 48 |
-
from ..managers.vllm_manager import VLLMManager
|
| 49 |
|
| 50 |
# Initialize application
|
| 51 |
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
| 52 |
initializer = AppInitializer(settings, app_file_path)
|
| 53 |
|
| 54 |
-
# Global VLLM Manager
|
| 55 |
-
vllm_manager = None
|
| 56 |
|
| 57 |
@asynccontextmanager
|
| 58 |
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
@@ -60,7 +36,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
| 60 |
Lifecycle manager for the FastAPI application.
|
| 61 |
Handles initialization and cleanup of application resources.
|
| 62 |
"""
|
| 63 |
-
global vllm_manager
|
| 64 |
|
| 65 |
try:
|
| 66 |
# Load the config if provided
|
|
@@ -69,35 +44,13 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
| 69 |
if config_file:
|
| 70 |
logger.info(f"Loading config from file: {config_file}")
|
| 71 |
with open(config_file, "r") as f:
|
| 72 |
-
|
| 73 |
-
content = f.read()
|
| 74 |
-
expanded_content = os.path.expandvars(content)
|
| 75 |
-
config = yaml.safe_load(expanded_content)
|
| 76 |
else:
|
| 77 |
logger.info("No config file provided, using defaults.")
|
| 78 |
|
| 79 |
-
# Override configurations with environment variables if present
|
| 80 |
-
if os.environ.get("BLABLADOR_API_KEY"):
|
| 81 |
-
logger.info("Using BLABLADOR_API_KEY from environment variables.")
|
| 82 |
-
# The key is accessed directly in MagenticUIConfig, but we can log its presence
|
| 83 |
-
|
| 84 |
if os.environ.get("FARA_AGENT") is not None:
|
| 85 |
config["use_fara_agent"] = os.environ["FARA_AGENT"] == "True"
|
| 86 |
|
| 87 |
-
# Initialize VLLM if configured (Optional now, as we switched default to Blablador)
|
| 88 |
-
if os.environ.get("USE_LOCAL_VLLM") == "True":
|
| 89 |
-
try:
|
| 90 |
-
vllm_port = int(os.environ.get("VLLM_PORT", 5000))
|
| 91 |
-
vllm_model = os.environ.get("VLLM_MODEL", "yujiepan/ui-tars-1.5-7B-GPTQ-W4A16g128")
|
| 92 |
-
vllm_manager = VLLMManager(model_name=vllm_model, port=vllm_port)
|
| 93 |
-
await vllm_manager.start()
|
| 94 |
-
# Inject VLLM URL into config for agents to use
|
| 95 |
-
config["vllm_base_url"] = f"http://localhost:{vllm_port}"
|
| 96 |
-
except Exception as e:
|
| 97 |
-
logger.error(f"Failed to start VLLM manager: {e}")
|
| 98 |
-
# decide if we should fail hard or continue without vision
|
| 99 |
-
# raise e
|
| 100 |
-
|
| 101 |
# Initialize managers (DB, Connection, Team)
|
| 102 |
await init_managers(
|
| 103 |
initializer.database_uri,
|
|
@@ -125,8 +78,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
| 125 |
try:
|
| 126 |
logger.info("Cleaning up application resources...")
|
| 127 |
await cleanup_managers()
|
| 128 |
-
if vllm_manager:
|
| 129 |
-
vllm_manager.stop()
|
| 130 |
logger.info("Application shutdown complete")
|
| 131 |
except Exception as e:
|
| 132 |
logger.error(f"Error during shutdown: {str(e)}")
|
|
@@ -158,6 +109,12 @@ api = FastAPI(
|
|
| 158 |
docs_url="/docs" if settings.API_DOCS else None,
|
| 159 |
)
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
# Include all routers with their prefixes
|
| 162 |
api.include_router(
|
| 163 |
sessions.router,
|
|
|
|
| 4 |
from contextlib import asynccontextmanager
|
| 5 |
from typing import AsyncGenerator, Any
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# import logging
|
| 8 |
from fastapi import FastAPI, Request
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 24 |
ws,
|
| 25 |
mcp,
|
| 26 |
)
|
|
|
|
| 27 |
|
| 28 |
# Initialize application
|
| 29 |
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
| 30 |
initializer = AppInitializer(settings, app_file_path)
|
| 31 |
|
|
|
|
|
|
|
| 32 |
|
| 33 |
@asynccontextmanager
|
| 34 |
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
|
| 36 |
Lifecycle manager for the FastAPI application.
|
| 37 |
Handles initialization and cleanup of application resources.
|
| 38 |
"""
|
|
|
|
| 39 |
|
| 40 |
try:
|
| 41 |
# Load the config if provided
|
|
|
|
| 44 |
if config_file:
|
| 45 |
logger.info(f"Loading config from file: {config_file}")
|
| 46 |
with open(config_file, "r") as f:
|
| 47 |
+
config = yaml.safe_load(f)
|
|
|
|
|
|
|
|
|
|
| 48 |
else:
|
| 49 |
logger.info("No config file provided, using defaults.")
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
if os.environ.get("FARA_AGENT") is not None:
|
| 52 |
config["use_fara_agent"] = os.environ["FARA_AGENT"] == "True"
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
# Initialize managers (DB, Connection, Team)
|
| 55 |
await init_managers(
|
| 56 |
initializer.database_uri,
|
|
|
|
| 78 |
try:
|
| 79 |
logger.info("Cleaning up application resources...")
|
| 80 |
await cleanup_managers()
|
|
|
|
|
|
|
| 81 |
logger.info("Application shutdown complete")
|
| 82 |
except Exception as e:
|
| 83 |
logger.error(f"Error during shutdown: {str(e)}")
|
|
|
|
| 109 |
docs_url="/docs" if settings.API_DOCS else None,
|
| 110 |
)
|
| 111 |
|
| 112 |
+
@app.get("/api-docs")
|
| 113 |
+
async def root_api_docs():
|
| 114 |
+
"""Root api docs endpoint for Hugging Face"""
|
| 115 |
+
from fastapi.responses import RedirectResponse
|
| 116 |
+
return RedirectResponse(url="/api/docs")
|
| 117 |
+
|
| 118 |
# Include all routers with their prefixes
|
| 119 |
api.include_router(
|
| 120 |
sessions.router,
|
src/magentic_ui/backend/web/initialization.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
import os
|
| 3 |
from pathlib import Path
|
| 4 |
|
|
@@ -7,7 +7,6 @@ from loguru import logger
|
|
| 7 |
from pydantic import BaseModel
|
| 8 |
|
| 9 |
from .config import Settings
|
| 10 |
-
from ..managers.vllm_manager import VLLMManager
|
| 11 |
|
| 12 |
|
| 13 |
class _AppPaths(BaseModel):
|
|
|
|
| 1 |
+
# api/initialization.py
|
| 2 |
import os
|
| 3 |
from pathlib import Path
|
| 4 |
|
|
|
|
| 7 |
from pydantic import BaseModel
|
| 8 |
|
| 9 |
from .config import Settings
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class _AppPaths(BaseModel):
|
src/magentic_ui/backend/web/managers/connection.py
CHANGED
|
@@ -562,6 +562,13 @@ class WebSocketManager:
|
|
| 562 |
"""
|
| 563 |
|
| 564 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
if isinstance(message, MultiModalMessage):
|
| 566 |
message_dump = message.model_dump()
|
| 567 |
|
|
|
|
| 562 |
"""
|
| 563 |
|
| 564 |
try:
|
| 565 |
+
# Check for screenshot dictionaries directly (some agents yield these)
|
| 566 |
+
if isinstance(message, dict) and message.get("type") == "screenshot":
|
| 567 |
+
return {
|
| 568 |
+
"type": "screenshot",
|
| 569 |
+
"data": message.get("data")
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
if isinstance(message, MultiModalMessage):
|
| 573 |
message_dump = message.model_dump()
|
| 574 |
|
src/magentic_ui/backend/web/routes/runs.py
CHANGED
|
@@ -71,6 +71,23 @@ async def create_run(
|
|
| 71 |
# We might want to add these endpoints:
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
@router.get("/{run_id}")
|
| 75 |
async def get_run(run_id: int, db=Depends(get_db)) -> Dict:
|
| 76 |
"""Get run details including task and result"""
|
|
|
|
| 71 |
# We might want to add these endpoints:
|
| 72 |
|
| 73 |
|
| 74 |
+
@router.get("/{run_id}/vision")
|
| 75 |
+
async def get_run_vision(run_id: int, db=Depends(get_db)) -> Dict:
|
| 76 |
+
"""Get VNC connection details for a run"""
|
| 77 |
+
run = db.get(Run, filters={"id": run_id}, return_json=False)
|
| 78 |
+
if not run.status or not run.data:
|
| 79 |
+
raise HTTPException(status_code=404, detail="Run not found")
|
| 80 |
+
|
| 81 |
+
# Assuming VNC is exposed on a standard port or we can get it from the run's team manager/docker container
|
| 82 |
+
# For a Hugging Face Space deployment with Docker SDK, we might expose noVNC on a specific path or port.
|
| 83 |
+
# We will return a placeholder URL for now which can be configured based on the actual VNC setup.
|
| 84 |
+
return {
|
| 85 |
+
"status": True,
|
| 86 |
+
"vnc_url": f"http://localhost:6080/vnc.html?autoconnect=true&resize=scale",
|
| 87 |
+
"has_vnc": True
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
@router.get("/{run_id}")
|
| 92 |
async def get_run(run_id: int, db=Depends(get_db)) -> Dict:
|
| 93 |
"""Get run details including task and result"""
|
src/magentic_ui/magentic_ui_config.py
CHANGED
|
@@ -43,53 +43,15 @@ class ModelClientConfigs(BaseModel):
|
|
| 43 |
"model": "alias-large",
|
| 44 |
"base_url": "https://api.helmholtz-blablador.fz-juelich.de/v1",
|
| 45 |
"api_key": os.environ.get("BLABLADOR_API_KEY"),
|
| 46 |
-
"model_info": {
|
| 47 |
-
"vision": False,
|
| 48 |
-
"function_calling": True,
|
| 49 |
-
"json_output": True,
|
| 50 |
-
"family": "unknown",
|
| 51 |
-
"structured_output": False,
|
| 52 |
-
},
|
| 53 |
-
"include_name_in_message": False
|
| 54 |
},
|
| 55 |
"max_retries": 10,
|
| 56 |
}
|
| 57 |
-
|
| 58 |
-
# Specific config for the web surfer (vision agent)
|
| 59 |
-
# Defaults to local VLLM if env var is set, otherwise falls back to Blablador or default
|
| 60 |
-
default_web_surfer_config: ClassVar[Dict[str, Any]] = {
|
| 61 |
-
"provider": "OpenAIChatCompletionClient",
|
| 62 |
-
"config": {
|
| 63 |
-
"model": os.environ.get("VLLM_MODEL", "yujiepan/ui-tars-1.5-7B-GPTQ-W4A16g128"),
|
| 64 |
-
"base_url": f"http://localhost:{os.environ.get('VLLM_PORT', '5000')}/v1" if os.environ.get("USE_LOCAL_VLLM") == "True" else "https://api.helmholtz-blablador.fz-juelich.de/v1",
|
| 65 |
-
"api_key": "not-needed" if os.environ.get("USE_LOCAL_VLLM") == "True" else os.environ.get("BLABLADOR_API_KEY"),
|
| 66 |
-
"model_info": {
|
| 67 |
-
"vision": True,
|
| 68 |
-
"function_calling": True,
|
| 69 |
-
"json_output": False,
|
| 70 |
-
"family": "unknown",
|
| 71 |
-
"structured_output": False,
|
| 72 |
-
"multiple_system_messages": False,
|
| 73 |
-
},
|
| 74 |
-
"include_name_in_message": False
|
| 75 |
-
},
|
| 76 |
-
"max_retries": 10,
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
default_action_guard_config: ClassVar[Dict[str, Any]] = {
|
| 80 |
"provider": "OpenAIChatCompletionClient",
|
| 81 |
"config": {
|
| 82 |
"model": "alias-fast",
|
| 83 |
"base_url": "https://api.helmholtz-blablador.fz-juelich.de/v1",
|
| 84 |
"api_key": os.environ.get("BLABLADOR_API_KEY"),
|
| 85 |
-
"model_info": {
|
| 86 |
-
"vision": False,
|
| 87 |
-
"function_calling": True,
|
| 88 |
-
"json_output": True,
|
| 89 |
-
"family": "unknown",
|
| 90 |
-
"structured_output": False,
|
| 91 |
-
},
|
| 92 |
-
"include_name_in_message": False
|
| 93 |
},
|
| 94 |
"max_retries": 10,
|
| 95 |
}
|
|
@@ -97,10 +59,6 @@ class ModelClientConfigs(BaseModel):
|
|
| 97 |
@classmethod
|
| 98 |
def get_default_client_config(cls) -> Dict[str, Any]:
|
| 99 |
return cls.default_client_config
|
| 100 |
-
|
| 101 |
-
@classmethod
|
| 102 |
-
def get_default_web_surfer_config(cls) -> Dict[str, Any]:
|
| 103 |
-
return cls.default_web_surfer_config
|
| 104 |
|
| 105 |
@classmethod
|
| 106 |
def get_default_action_guard_config(cls) -> Dict[str, Any]:
|
|
|
|
| 43 |
"model": "alias-large",
|
| 44 |
"base_url": "https://api.helmholtz-blablador.fz-juelich.de/v1",
|
| 45 |
"api_key": os.environ.get("BLABLADOR_API_KEY"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
},
|
| 47 |
"max_retries": 10,
|
| 48 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
default_action_guard_config: ClassVar[Dict[str, Any]] = {
|
| 50 |
"provider": "OpenAIChatCompletionClient",
|
| 51 |
"config": {
|
| 52 |
"model": "alias-fast",
|
| 53 |
"base_url": "https://api.helmholtz-blablador.fz-juelich.de/v1",
|
| 54 |
"api_key": os.environ.get("BLABLADOR_API_KEY"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
"max_retries": 10,
|
| 57 |
}
|
|
|
|
| 59 |
@classmethod
|
| 60 |
def get_default_client_config(cls) -> Dict[str, Any]:
|
| 61 |
return cls.default_client_config
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
@classmethod
|
| 64 |
def get_default_action_guard_config(cls) -> Dict[str, Any]:
|
tests/test_magentic_ui_config_serialization.py
CHANGED
|
@@ -1,53 +1,115 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
|
|
|
| 3 |
import yaml
|
| 4 |
-
from
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
import yaml
|
| 5 |
+
from magentic_ui.magentic_ui_config import MagenticUIConfig
|
| 6 |
+
|
| 7 |
+
YAML_CONFIG = """
|
| 8 |
+
model_client_configs:
|
| 9 |
+
default: &default_client
|
| 10 |
+
provider: OpenAIChatCompletionClient
|
| 11 |
+
config:
|
| 12 |
+
model: gpt-4.1-2025-04-14
|
| 13 |
+
max_retries: 10
|
| 14 |
+
orchestrator: *default_client
|
| 15 |
+
web_surfer: *default_client
|
| 16 |
+
coder: *default_client
|
| 17 |
+
file_surfer: *default_client
|
| 18 |
+
action_guard:
|
| 19 |
+
provider: OpenAIChatCompletionClient
|
| 20 |
+
config:
|
| 21 |
+
model: gpt-4.1-nano-2025-04-14
|
| 22 |
+
max_retries: 10
|
| 23 |
+
|
| 24 |
+
mcp_agent_configs:
|
| 25 |
+
- name: mcp_agent
|
| 26 |
+
description: "Test MCP Agent"
|
| 27 |
+
reflect_on_tool_use: false
|
| 28 |
+
tool_call_summary_format: "{tool_name}({arguments}): {result}"
|
| 29 |
+
model_client: *default_client
|
| 30 |
+
mcp_servers:
|
| 31 |
+
- server_name: server1
|
| 32 |
+
server_params:
|
| 33 |
+
type: StdioServerParams
|
| 34 |
+
command: npx
|
| 35 |
+
args:
|
| 36 |
+
- -y
|
| 37 |
+
- "@modelcontextprotocol/server-everything"
|
| 38 |
+
- server_name: server2
|
| 39 |
+
server_params:
|
| 40 |
+
type: SseServerParams
|
| 41 |
+
url: http://localhost:3001/sse
|
| 42 |
+
|
| 43 |
+
cooperative_planning: true
|
| 44 |
+
autonomous_execution: false
|
| 45 |
+
allowed_websites: []
|
| 46 |
+
max_actions_per_step: 5
|
| 47 |
+
multiple_tools_per_call: false
|
| 48 |
+
max_turns: 20
|
| 49 |
+
plan: null
|
| 50 |
+
approval_policy: auto-conservative
|
| 51 |
+
allow_for_replans: true
|
| 52 |
+
do_bing_search: false
|
| 53 |
+
websurfer_loop: false
|
| 54 |
+
retrieve_relevant_plans: never
|
| 55 |
+
memory_controller_key: null
|
| 56 |
+
model_context_token_limit: 110000
|
| 57 |
+
allow_follow_up_input: true
|
| 58 |
+
final_answer_prompt: null
|
| 59 |
+
playwright_port: -1
|
| 60 |
+
novnc_port: -1
|
| 61 |
+
user_proxy_type: null
|
| 62 |
+
task: "What tools are available?"
|
| 63 |
+
hints: null
|
| 64 |
+
answer: null
|
| 65 |
+
inside_docker: false
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@pytest.fixture
|
| 70 |
+
def yaml_config_text() -> str:
|
| 71 |
+
return YAML_CONFIG
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@pytest.fixture
|
| 75 |
+
def config_obj(yaml_config_text: str) -> MagenticUIConfig:
|
| 76 |
+
data = yaml.safe_load(yaml_config_text)
|
| 77 |
+
return MagenticUIConfig(**data)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_yaml_deserialize(yaml_config_text: str) -> None:
|
| 81 |
+
data = yaml.safe_load(yaml_config_text)
|
| 82 |
+
config = MagenticUIConfig(**data)
|
| 83 |
+
assert isinstance(config, MagenticUIConfig)
|
| 84 |
+
assert config.task == "What tools are available?"
|
| 85 |
+
assert config.mcp_agent_configs[0].name == "mcp_agent"
|
| 86 |
+
assert config.mcp_agent_configs[0].reflect_on_tool_use is False
|
| 87 |
+
assert (
|
| 88 |
+
config.mcp_agent_configs[0].tool_call_summary_format
|
| 89 |
+
== "{tool_name}({arguments}): {result}"
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_yaml_serialize_roundtrip(config_obj: MagenticUIConfig) -> None:
|
| 94 |
+
as_dict = config_obj.model_dump(mode="json")
|
| 95 |
+
yaml_text = yaml.safe_dump(as_dict)
|
| 96 |
+
loaded = yaml.safe_load(yaml_text)
|
| 97 |
+
config2 = MagenticUIConfig(**loaded)
|
| 98 |
+
assert config2 == config_obj
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def test_json_serialize_roundtrip(config_obj: MagenticUIConfig) -> None:
|
| 102 |
+
as_dict = config_obj.model_dump(mode="json")
|
| 103 |
+
json_text = json.dumps(as_dict)
|
| 104 |
+
loaded = json.loads(json_text)
|
| 105 |
+
config2 = MagenticUIConfig(**loaded)
|
| 106 |
+
assert config2 == config_obj
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_json_and_yaml_equivalence(yaml_config_text: str) -> None:
|
| 110 |
+
data = yaml.safe_load(yaml_config_text)
|
| 111 |
+
json_text = json.dumps(data)
|
| 112 |
+
loaded = json.loads(json_text)
|
| 113 |
+
config = MagenticUIConfig(**loaded)
|
| 114 |
+
assert config.task == "What tools are available?"
|
| 115 |
+
assert config.mcp_agent_configs[0].name == "mcp_agent"
|
tests/test_runs_vision.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from fastapi.testclient import TestClient
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
| 7 |
+
|
| 8 |
+
from magentic_ui.backend.web.app import app
|
| 9 |
+
|
| 10 |
+
# Use a mock client for endpoints
|
| 11 |
+
client = TestClient(app)
|
| 12 |
+
|
| 13 |
+
def test_get_run_vision_mock():
|
| 14 |
+
# Test that the endpoint exists
|
| 15 |
+
response = client.get("/api/runs/999/vision")
|
| 16 |
+
assert response.status_code in [404, 500] # It might be 500 because of unmocked DB connection
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|